Compare commits

...

7 Commits

31 changed files with 1588 additions and 12 deletions

View File

@@ -283,7 +283,8 @@ zen-page-info-shortcut = View Page Info
zen-find-shortcut = Find on Page
zen-search-find-again-shortcut = Find Again
zen-search-find-again-shortcut-prev = Find Previous
zen-search-find-again-shortcut-2 = Find Again (Alt)
zen-search-find-again-shortcut-alt = Find Again (Alt)
zen-search-find-again-shortcut-prev-alt = Find Previous (Alt)
zen-bookmark-this-page-shortcut = Bookmark This Page
zen-bookmark-show-library-shortcut = Show Bookmarks Library
zen-key-stop = Stop Loading

View File

@@ -20,6 +20,14 @@
- name: zen.view.compact.sidebar-keep-hover.duration
value: 150
# How far (in CSS pixels) the mouse may travel past the window bounds after
# leaving the window before the hovered sidebar/toolbar is collapsed
- name: zen.view.compact.outside-window-edge-offset.horizontal
value: 200
- name: zen.view.compact.outside-window-edge-offset.vertical
value: 100
- name: zen.view.compact.animate-sidebar
value: true

View File

@@ -46,6 +46,12 @@
- name: zen.view.context-menu.refresh
value: "@IS_TWILIGHT@"
- name: zen.view.drag-window-from-content
value: true
- name: zen.view.drag-window-from-content.height-percentage
value: 10
- name: zen.view.borderless-fullscreen
value: true

View File

@@ -803,6 +803,7 @@ const zenMissingKeyboardShortcutL10n = {
key_inspectorMac: "zen-key-inspector-mac",
key_findSelection: "zen-key-find-selection",
key_findPrevious2: "zen-search-find-again-shortcut-prev-alt",
// Devtools
key_toggleToolbox: "zen-devtools-toggle-shortcut",
@@ -826,6 +827,9 @@ var zenIgnoreKeyboardShortcutIDs = [
"key_exitFullScreen_old",
"key_exitFullScreen_compat",
"key_duplicateTab",
"key_addTabSplitView",
"key_separateTabSplitView",
"viewOpenTabsSidebarKb",
];
var zenIgnoreKeyboardShortcutL10n = [

View File

@@ -57,6 +57,22 @@ let JSWINDOWACTORS = {
remoteTypes: ["web", "file"],
enablePreference: "zen.glance.enabled",
},
ZenWindowDrag: {
parent: {
esModuleURI: "resource:///actors/ZenWindowDragParent.sys.mjs",
},
child: {
esModuleURI: "resource:///actors/ZenWindowDragChild.sys.mjs",
events: {
mousedown: {
mozSystemGroup: true,
},
},
},
messageManagerGroups: ["browsers"],
remoteTypes: ["web", "file"],
enablePreference: "zen.view.drag-window-from-content",
},
};
if (!Services.appinfo.inSafeMode) {

View File

@@ -34,6 +34,29 @@ XPCOMUtils.defineLazyPreferenceGetter(
true
);
// Distance (in CSS pixels) the mouse can travel past the window bounds after
// leaving the window before the hovered element is collapsed
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"COMPACT_MODE_OUTSIDE_WINDOW_HORIZONTAL_OFFSET",
"zen.view.compact.outside-window-edge-offset.horizontal",
250
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"COMPACT_MODE_OUTSIDE_WINDOW_VERTICAL_OFFSET",
"zen.view.compact.outside-window-edge-offset.vertical",
150
);
XPCOMUtils.defineLazyServiceGetter(
lazy,
"zenMouseTracker",
"@mozilla.org/zen/mouse-tracker;1",
Ci.nsIZenMouseTracker
);
ChromeUtils.defineLazyGetter(lazy, "mainAppWrapper", () =>
document.getElementById("zen-main-app-wrapper")
);
@@ -71,9 +94,21 @@ window.gZenCompactModeManager = {
tabIsRightObserver
);
const outsideMouseTrackerExitObserver =
this._onOutsideMouseTrackerExit.bind(this);
Services.obs.addObserver(
outsideMouseTrackerExitObserver,
"zen-mouse-tracker:exited"
);
window.addEventListener(
"unload",
() => {
this._stopTrackingMouseOutsideWindow();
Services.obs.removeObserver(
outsideMouseTrackerExitObserver,
"zen-mouse-tracker:exited"
);
Services.prefs.removeObserver(
"zen.tabs.vertical.right-side",
tabIsRightObserver
@@ -94,6 +129,10 @@ window.gZenCompactModeManager = {
this._clearAllHoverStates()
);
// Hide any element kept open by the outside mouse tracking as soon as the
// window loses focus
window.addEventListener("deactivate", () => this._collapseTrackedElement());
this._canShowBackgroundTabToast = Services.prefs.getBoolPref(
"zen.view.compact.show-background-tab-toast",
true
@@ -149,6 +188,7 @@ window.gZenCompactModeManager = {
return;
}
delete this._isTabBeingDragged;
this._stopTrackingMouseOutsideWindow();
this.sidebar.removeAttribute("zen-user-show");
// We use this element in order to make it persis across restarts, by using the XULStore.
// main-window can't store attributes other than window sizes, so we use this instead
@@ -723,6 +763,9 @@ window.gZenCompactModeManager = {
}
} else {
if (attr === "zen-has-hover") {
if (element === this._outsideTrackedElement) {
this._stopTrackingMouseOutsideWindow();
}
element.removeAttribute("zen-has-implicit-hover");
gURLBar.updateTextOverflow();
}
@@ -886,18 +929,23 @@ window.gZenCompactModeManager = {
}
window.cancelAnimationFrame(this._removeHoverFrames[target.id]);
this.flashElement(
target,
this.hideAfterHoverDuration,
"has-hover" + target.id,
"zen-has-hover"
);
if (!this._trackMouseOutsideWindow(entry.screenEdge, target)) {
// We can't track the mouse position outside of the window on
// this platform, fall back to hiding after a fixed duration
this.flashElement(
target,
this.hideAfterHoverDuration,
"has-hover" + target.id,
"zen-has-hover"
);
}
document.addEventListener(
"mousemove",
() => {
if (target.matches(":hover")) {
return;
}
// Closing the element also stops the outside mouse tracking
this._setElementExpandAttribute(target, false);
this.clearFlashTimeout("has-hover" + target.id);
},
@@ -941,7 +989,55 @@ window.gZenCompactModeManager = {
return bBox.left - error < x && x < bBox.right + error;
},
_trackMouseOutsideWindow(screenEdge, target) {
this._stopTrackingMouseOutsideWindow();
const maxEdgeOffset =
screenEdge === "left" || screenEdge === "right"
? lazy.COMPACT_MODE_OUTSIDE_WINDOW_HORIZONTAL_OFFSET
: lazy.COMPACT_MODE_OUTSIDE_WINDOW_VERTICAL_OFFSET;
try {
lazy.zenMouseTracker.registerWindow(window, screenEdge, maxEdgeOffset);
} catch (e) {
// The platform can't track the global mouse position (e.g. Linux)
return false;
}
this._outsideTrackedElement = target;
this.clearFlashTimeout("has-hover" + target.id);
window.requestAnimationFrame(() => {
if (this._outsideTrackedElement === target) {
this._setElementExpandAttribute(target, true);
}
});
return true;
},
_stopTrackingMouseOutsideWindow() {
const target = this._outsideTrackedElement;
if (!target) {
return;
}
this._outsideTrackedElement = null;
lazy.zenMouseTracker.unregisterWindow(window);
},
_collapseTrackedElement() {
const target = this._outsideTrackedElement;
if (!target) {
return;
}
// Closing the element also unregisters us from the mouse tracker
this._setElementExpandAttribute(target, false);
this.clearFlashTimeout("has-hover" + target.id);
},
_onOutsideMouseTrackerExit(subject) {
if (subject === window) {
this._collapseTrackedElement();
}
},
_clearAllHoverStates() {
this._stopTrackingMouseOutsideWindow();
// Clear hover attributes from all hoverable elements
for (let entry of this.hoverableElements) {
const target = entry.element;

View File

@@ -0,0 +1,246 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ZenMouseTracker.h"
#include "ZenMouseTrackerInternal.h"
#include <atomic>
#include "mozilla/Services.h"
#include "mozilla/WidgetUtils.h"
#include "nsIObserverService.h"
#include "nsIWidget.h"
#include "nsThreadUtils.h"
namespace zen {
NS_IMPL_ISUPPORTS(ZenMouseTracker, nsIZenMouseTracker, nsIObserver)
static ZenMouseTracker* sInstance = nullptr;
// Pointer moves are coalesced: the backends may report them faster than we
// want to run the checks, and on Windows they arrive from a low level hook
// where as little work as possible should happen. These are atomic since
// some backends (e.g. the macOS global event monitor) may deliver the moves
// off the main thread.
static std::atomic<int32_t> sLatestPointerX{0};
static std::atomic<int32_t> sLatestPointerY{0};
static std::atomic<bool> sPendingEvaluation{false};
ZenMouseTracker::ZenMouseTracker() {
MOZ_ASSERT(NS_IsMainThread());
sInstance = this;
}
ZenMouseTracker::~ZenMouseTracker() {
ZenNativeMouseMonitor::Stop();
if (sInstance == this) {
sInstance = nullptr;
}
}
// static
mozilla::Maybe<ZenMouseTracker::TrackedEdge> ZenMouseTracker::ParseEdge(
const nsACString& aScreenEdge) {
if (aScreenEdge.EqualsLiteral("left")) {
return mozilla::Some(TrackedEdge::Left);
}
if (aScreenEdge.EqualsLiteral("right")) {
return mozilla::Some(TrackedEdge::Right);
}
if (aScreenEdge.EqualsLiteral("top")) {
return mozilla::Some(TrackedEdge::Top);
}
if (aScreenEdge.EqualsLiteral("bottom")) {
return mozilla::Some(TrackedEdge::Bottom);
}
return mozilla::Nothing();
}
NS_IMETHODIMP
ZenMouseTracker::RegisterWindow(mozIDOMWindowProxy* aWindow,
const nsACString& aScreenEdge,
float aMaxEdgeOffset) {
MOZ_ASSERT(NS_IsMainThread());
const auto edge = ParseEdge(aScreenEdge);
if (!aWindow || edge.isNothing() || aMaxEdgeOffset < 0.0f) {
return NS_ERROR_INVALID_ARG;
}
nsCOMPtr<nsPIDOMWindowOuter> window = nsPIDOMWindowOuter::From(aWindow);
if (!window) {
return NS_ERROR_INVALID_ARG;
}
nsresult rv = ZenNativeMouseMonitor::Start();
if (NS_FAILED(rv)) {
return rv;
}
for (auto& tracked : mTracked) {
if (tracked.mWindow == window) {
tracked.mEdge = *edge;
tracked.mMaxEdgeOffset = aMaxEdgeOffset;
return NS_OK;
}
}
mTracked.AppendElement(TrackedWindow{window, *edge, aMaxEdgeOffset});
AddObservers();
return NS_OK;
}
void ZenMouseTracker::AddObservers() {
if (mObserving) {
return;
}
if (nsCOMPtr<nsIObserverService> obs =
mozilla::services::GetObserverService()) {
obs->AddObserver(this, "domwindowclosed", false);
obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
mObserving = true;
}
}
void ZenMouseTracker::RemoveObservers() {
if (!mObserving) {
return;
}
if (nsCOMPtr<nsIObserverService> obs =
mozilla::services::GetObserverService()) {
obs->RemoveObserver(this, "domwindowclosed");
obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
}
mObserving = false;
}
NS_IMETHODIMP
ZenMouseTracker::UnregisterWindow(mozIDOMWindowProxy* aWindow) {
MOZ_ASSERT(NS_IsMainThread());
if (!aWindow) {
return NS_ERROR_INVALID_ARG;
}
StopTrackingWindow(nsPIDOMWindowOuter::From(aWindow));
return NS_OK;
}
void ZenMouseTracker::StopTrackingWindow(nsPIDOMWindowOuter* aWindow) {
for (size_t i = 0; i < mTracked.Length(); i++) {
if (mTracked[i].mWindow == aWindow) {
mTracked.RemoveElementAt(i);
break;
}
}
OnTrackedListChanged();
}
void ZenMouseTracker::OnTrackedListChanged() {
if (mTracked.IsEmpty()) {
ZenNativeMouseMonitor::Stop();
RemoveObservers();
}
}
// static
void ZenMouseTracker::OnNativePointerMove(const mozilla::DesktopPoint& aPoint) {
sLatestPointerX = int32_t(aPoint.x);
sLatestPointerY = int32_t(aPoint.y);
if (sPendingEvaluation.exchange(true)) {
// An already queued evaluation will pick up the position we just stored
return;
}
nsresult rv = NS_DispatchToMainThread(
NS_NewRunnableFunction("zen::ZenMouseTracker::Evaluate", [] {
sPendingEvaluation = false;
if (RefPtr<ZenMouseTracker> tracker = sInstance) {
tracker->Evaluate(mozilla::DesktopPoint(float(sLatestPointerX),
float(sLatestPointerY)));
}
}));
if (NS_FAILED(rv)) {
// Don't let a failed dispatch (e.g. during shutdown) block all future
// evaluations
sPendingEvaluation = false;
}
}
void ZenMouseTracker::Evaluate(const mozilla::DesktopPoint& aPoint) {
nsTArray<nsCOMPtr<nsPIDOMWindowOuter>> exited;
for (size_t i = mTracked.Length(); i > 0; i--) {
auto& tracked = mTracked[i - 1];
RefPtr<nsIWidget> widget =
mozilla::widget::WidgetUtils::DOMWindowToWidget(tracked.mWindow);
if (!widget) {
mTracked.RemoveElementAt(i - 1);
continue;
}
if (!IsPointerWithinBounds(tracked, aPoint, widget)) {
exited.AppendElement(std::move(tracked.mWindow));
mTracked.RemoveElementAt(i - 1);
}
}
OnTrackedListChanged();
if (exited.IsEmpty()) {
return;
}
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
if (!obs) {
return;
}
for (auto& window : exited) {
obs->NotifyObservers(window, ZEN_MOUSE_TRACKER_EXITED_TOPIC, nullptr);
}
}
// static
bool ZenMouseTracker::IsPointerWithinBounds(const TrackedWindow& aTracked,
const mozilla::DesktopPoint& aPoint,
nsIWidget* aWidget) {
const auto scale = aWidget->GetDesktopToDeviceScale();
const float x = aPoint.x * scale.scale;
const float y = aPoint.y * scale.scale;
const auto origin = aWidget->WidgetToScreenOffset();
const auto size = aWidget->GetClientSize();
const float left = origin.x;
const float top = origin.y;
const float right = left + size.width;
const float bottom = top + size.height;
if (x >= left && x <= right && y >= top && y <= bottom) {
// Back inside the window; the in-window hover logic owns this case
return true;
}
const float maxOffset =
aTracked.mMaxEdgeOffset * float(aWidget->GetDefaultScale().scale);
switch (aTracked.mEdge) {
case TrackedEdge::Left:
return x >= left - maxOffset && x < left && y >= top && y <= bottom;
case TrackedEdge::Right:
return x > right && x <= right + maxOffset && y >= top && y <= bottom;
case TrackedEdge::Top:
return y >= top - maxOffset && y < top && x >= left && x <= right;
case TrackedEdge::Bottom:
return y > bottom && y <= bottom + maxOffset && x >= left && x <= right;
}
return false;
}
NS_IMETHODIMP
ZenMouseTracker::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* aData) {
if (!strcmp(aTopic, "domwindowclosed")) {
if (nsCOMPtr<nsPIDOMWindowOuter> window = do_QueryInterface(aSubject)) {
StopTrackingWindow(window);
}
return NS_OK;
}
if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
mTracked.Clear();
OnTrackedListChanged();
}
return NS_OK;
}
} // namespace zen

View File

@@ -0,0 +1,92 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_ZenMouseTracker_h_
#define mozilla_ZenMouseTracker_h_
#include "nsIZenMouseTracker.h"
#include "mozilla/Maybe.h"
#include "nsIObserver.h"
#include "nsCOMPtr.h"
#include "nsPIDOMWindow.h"
#include "nsTArray.h"
#include "Units.h"
class nsIWidget;
// Fired with the tracked window as subject once the pointer breaks the
// tracking conditions; tracking for that window has already stopped.
#define ZEN_MOUSE_TRACKER_EXITED_TOPIC "zen-mouse-tracker:exited"
namespace zen {
/**
* @brief Watches the global OS pointer position for registered windows and
* notifies observers once the pointer moves too far away from the window
* edge it left through.
*/
class ZenMouseTracker final : public nsIZenMouseTracker, public nsIObserver {
public:
NS_DECL_ISUPPORTS
NS_DECL_NSIZENMOUSETRACKER
NS_DECL_NSIOBSERVER
ZenMouseTracker();
/**
* @brief Called by the platform backends whenever the OS pointer moves.
* @param aPoint The pointer position in desktop pixels, relative to the
* origin of the (primary) screen. May be called outside of a clean event
* loop iteration (e.g. from a low level hook), evaluation is coalesced
* onto the main thread.
*/
static void OnNativePointerMove(const mozilla::DesktopPoint& aPoint);
private:
~ZenMouseTracker();
enum class TrackedEdge : uint8_t { Left, Right, Top, Bottom };
struct TrackedWindow {
nsCOMPtr<nsPIDOMWindowOuter> mWindow;
TrackedEdge mEdge;
float mMaxEdgeOffset; // In CSS pixels
};
static mozilla::Maybe<TrackedEdge> ParseEdge(const nsACString& aScreenEdge);
/**
* @brief Check every tracked window against the given pointer position and
* notify + stop tracking the ones the pointer moved too far away from.
*/
void Evaluate(const mozilla::DesktopPoint& aPoint);
/**
* @brief Whether the pointer is still allowed to keep the window's edge
* element open, given the window's bounds on screen.
*/
static bool IsPointerWithinBounds(const TrackedWindow& aTracked,
const mozilla::DesktopPoint& aPoint,
nsIWidget* aWidget);
void StopTrackingWindow(nsPIDOMWindowOuter* aWindow);
/**
* @brief Tear down the native monitor and our observers once the last
* tracked window is gone, so the service is fully idle while nothing is
* being tracked.
*/
void OnTrackedListChanged();
void AddObservers();
void RemoveObservers();
nsTArray<TrackedWindow> mTracked;
bool mObserving = false;
};
} // namespace zen
#endif

View File

@@ -0,0 +1,65 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ZenMouseTracker.h"
#include "ZenMouseTrackerInternal.h"
#include "nsCocoaUtils.h"
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
namespace zen {
// The global monitor only sees events delivered to other applications, so a
// local monitor is needed as well for moves that AppKit routes to our own
// app while the pointer is outside of the tracked window (AppKit sends mouse
// moved events to the key window regardless of the pointer position).
static id sLocalMonitor = nil;
static id sGlobalMonitor = nil;
static void ReportPointerPosition() {
NSPoint point = [NSEvent mouseLocation];
point.y = nsCocoaUtils::FlippedScreenY(point.y);
ZenMouseTracker::OnNativePointerMove(
mozilla::DesktopPoint(float(point.x), float(point.y)));
}
nsresult ZenNativeMouseMonitor::Start() {
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
if (sLocalMonitor || sGlobalMonitor) {
return NS_OK;
}
const NSEventMask mask = NSEventMaskMouseMoved | NSEventMaskLeftMouseDragged |
NSEventMaskRightMouseDragged |
NSEventMaskOtherMouseDragged;
sLocalMonitor =
[NSEvent addLocalMonitorForEventsMatchingMask:mask
handler:^(NSEvent* aEvent) {
ReportPointerPosition();
return aEvent;
}];
sGlobalMonitor =
[NSEvent addGlobalMonitorForEventsMatchingMask:mask
handler:^(NSEvent* aEvent) {
ReportPointerPosition();
}];
return NS_OK;
NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_NOT_AVAILABLE);
}
void ZenNativeMouseMonitor::Stop() {
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
if (sLocalMonitor) {
[NSEvent removeMonitor:sLocalMonitor];
sLocalMonitor = nil;
}
if (sGlobalMonitor) {
[NSEvent removeMonitor:sGlobalMonitor];
sGlobalMonitor = nil;
}
NS_OBJC_END_TRY_IGNORE_BLOCK;
}
} // namespace zen

View File

@@ -0,0 +1,48 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_ZenMouseTrackerInternal_h_
#define mozilla_ZenMouseTrackerInternal_h_
#include "nscore.h"
// On Linux there's no reliable way to observe the global pointer (Wayland
// doesn't expose it at all), so we don't track there and callers fall back
// to their timeout based behavior
#if defined(XP_MACOSX) || defined(XP_WIN)
# define NS_ZEN_CAN_TRACK_POINTER 1
#endif
namespace zen {
/**
* @brief Platform backend that delivers global pointer movements to
* ZenMouseTracker::OnNativePointerMove, including movements outside of any
* of our windows.
*/
class ZenNativeMouseMonitor final {
public:
#ifdef NS_ZEN_CAN_TRACK_POINTER
/**
* @brief Start delivering pointer moves. Safe to call while already
* started.
* @throws NS_ERROR_NOT_AVAILABLE when the platform cannot observe the
* global pointer (e.g. on Linux).
*/
static nsresult Start();
/**
* @brief Stop delivering pointer moves. Safe to call while stopped.
*/
static void Stop();
#else
static nsresult Start() { return NS_ERROR_NOT_AVAILABLE; }
static void Stop() {}
#endif
ZenNativeMouseMonitor() = delete;
};
} // namespace zen
#endif

View File

@@ -0,0 +1,51 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ZenMouseTracker.h"
#include "ZenMouseTrackerInternal.h"
#include <windows.h>
namespace zen {
static HHOOK sMouseHook = nullptr;
// Runs on the thread that installed the hook (the main thread). Keep this as
// light as possible: the system silently removes hooks that take too long.
static LRESULT CALLBACK MouseHookProc(int aCode, WPARAM aWParam,
LPARAM aLParam) {
if (aCode == HC_ACTION && aWParam == WM_MOUSEMOVE) {
const auto* info = reinterpret_cast<MSLLHOOKSTRUCT*>(aLParam);
// Screen physical pixels, which are what desktop pixels map to on Windows
ZenMouseTracker::OnNativePointerMove(
mozilla::DesktopPoint(float(info->pt.x), float(info->pt.y)));
}
return ::CallNextHookEx(nullptr, aCode, aWParam, aLParam);
}
nsresult ZenNativeMouseMonitor::Start() {
if (sMouseHook) {
return NS_OK;
}
// Pass the module that actually contains the hook proc (xul.dll), not the
// executable's module
HMODULE module = nullptr;
if (!::GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(&MouseHookProc),
&module)) {
return NS_ERROR_NOT_AVAILABLE;
}
sMouseHook = ::SetWindowsHookExW(WH_MOUSE_LL, MouseHookProc, module, 0);
return sMouseHook ? NS_OK : NS_ERROR_NOT_AVAILABLE;
}
void ZenNativeMouseMonitor::Stop() {
if (sMouseHook) {
::UnhookWindowsHookEx(sMouseHook);
sMouseHook = nullptr;
}
}
} // namespace zen

View File

@@ -0,0 +1,14 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
Classes = [
{
'cid': '{03d900f7-639f-4c81-b029-2883dd893ccb}',
'interfaces': ['nsIZenMouseTracker'],
'contract_ids': ['@mozilla.org/zen/mouse-tracker;1'],
'type': 'zen::ZenMouseTracker',
'headers': ['mozilla/ZenMouseTracker.h'],
'processes': ProcessSelector.MAIN_PROCESS_ONLY,
},
]

View File

@@ -0,0 +1,37 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
XPIDL_SOURCES += [
"nsIZenMouseTracker.idl",
]
EXPORTS.mozilla += [
"ZenMouseTracker.h",
"ZenMouseTrackerInternal.h",
]
SOURCES += [
"ZenMouseTracker.cpp",
]
XPCOM_MANIFESTS += [
"components.conf",
]
LOCAL_INCLUDES += [
"/widget",
]
if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
SOURCES += ["ZenMouseTrackerCocoa.mm"]
LOCAL_INCLUDES += [
"/widget/cocoa",
"/xpcom/base",
]
if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
SOURCES += ["ZenMouseTrackerWin.cpp"]
FINAL_LIBRARY = "xul"
XPIDL_MODULE = "zen_compact_mode"

View File

@@ -0,0 +1,41 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
interface mozIDOMWindowProxy;
/**
* @brief Tracks the OS pointer for windows the pointer has left.
* Once the pointer either crosses the window bounds on the
* axis perpendicular to the registered edge, or moves further than the
* given offset past that edge, the "zen-mouse-tracker:exited" observer
* notification is fired with the window as subject and tracking for that
* window stops.
*/
[scriptable, uuid(023e85e6-44e1-46b3-9cfb-6f3c7fa075f8)]
interface nsIZenMouseTracker : nsISupports {
/**
* @brief Start tracking the pointer for the given window.
*
* Re-registering a window updates its edge and offset. Tracking is
* automatically cleared when the window is closed.
*
* @param window The window the pointer just left.
* @param screenEdge The edge the pointer left through, one of "left",
* "right", "top" or "bottom".
* @param maxEdgeOffset How far (in CSS pixels) the pointer may travel past
* screenEdge before the exit notification is fired.
* @throws NS_ERROR_NOT_AVAILABLE when the platform cannot track the global
* pointer position (e.g. on Linux).
*/
void registerWindow(in mozIDOMWindowProxy window, in ACString screenEdge,
in float maxEdgeOffset);
/**
* @brief Stop tracking the pointer for the given window. No-op if the
* window isn't being tracked.
*/
void unregisterWindow(in mozIDOMWindowProxy window);
};

View File

@@ -15,6 +15,11 @@ SOURCES += [
"nsZenDragAndDrop.cpp",
]
if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
SOURCES += ["nsZenNativeWindowMove.mm"]
elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
XPCOM_MANIFESTS += [
"components.conf",
]

View File

@@ -4,6 +4,8 @@
#include "nsISupports.idl"
interface mozIDOMWindowProxy;
/**
* @brief Interface for Zen's drag and drop functionality.
*/
@@ -21,4 +23,14 @@ interface nsIZenDragAndDrop : nsISupports {
* @brief Indicate that a drag operation has ended.
*/
void onDragEnd();
/**
* @brief Start a native, OS-driven interactive move of the window,
* as if the user had grabbed its titlebar. Must be called while the
* primary mouse button is held down. The OS takes over the drag,
* providing native behaviors like macOS window tiling or Windows
* snap layouts, and ends it when the button is released.
* @param window The chrome window to move.
*/
void beginNativeWindowMove(in mozIDOMWindowProxy window);
};

View File

@@ -5,7 +5,57 @@
#include "nsZenDragAndDrop.h"
#include "nsBaseDragService.h"
#include "mozilla/WidgetUtils.h"
#include "nsIWidget.h"
#include "nsPIDOMWindow.h"
#if defined(XP_WIN)
# include <windows.h>
#elif defined(MOZ_WIDGET_GTK)
# include <gdk/gdk.h>
# include "mozilla/WidgetUtilsGtk.h"
#endif
namespace zen {
#if defined(XP_MACOSX)
nsresult StartNativeWindowMoveCocoa(nsIWidget* aWidget);
#endif
/**
* @brief Start a native, OS-driven move of the window backing aWidget.
* @see nsIZenDragAndDrop::beginNativeWindowMove for more details.
*/
static nsresult StartNativeWindowMove(nsIWidget* aWidget) {
#if defined(XP_MACOSX)
return StartNativeWindowMoveCocoa(aWidget);
#elif defined(XP_WIN)
HWND hwnd = static_cast<HWND>(aWidget->GetNativeData(NS_NATIVE_WINDOW));
NS_ENSURE_TRUE(hwnd, NS_ERROR_FAILURE);
// Hand the drag over to the OS as if the titlebar was grabbed; this
// enters the native move loop.
::ReleaseCapture();
::PostMessageW(hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0);
return NS_OK;
#elif defined(MOZ_WIDGET_GTK)
auto* gdkWindow =
static_cast<GdkWindow*>(aWidget->GetNativeData(NS_NATIVE_WINDOW));
NS_ENSURE_TRUE(gdkWindow, NS_ERROR_FAILURE);
GdkWindow* toplevel = gdk_window_get_toplevel(gdkWindow);
GdkDevice* pointer = mozilla::widget::GdkGetPointer();
NS_ENSURE_TRUE(pointer, NS_ERROR_FAILURE);
// A native move works on both X11 and Wayland, unlike moving the
// window programmatically.
gint rootX = 0;
gint rootY = 0;
gdk_device_get_position(pointer, nullptr, &rootX, &rootY);
gdk_window_begin_move_drag_for_device(toplevel, pointer, 1, rootX, rootY,
GDK_CURRENT_TIME);
return NS_OK;
#else
return NS_ERROR_NOT_IMPLEMENTED;
#endif
}
namespace {
static constexpr auto kZenDefaultDragImageOpacity =
@@ -44,4 +94,15 @@ nsZenDragAndDrop::OnDragEnd() {
return NS_OK;
}
NS_IMETHODIMP
nsZenDragAndDrop::BeginNativeWindowMove(mozIDOMWindowProxy* aWindow) {
NS_ENSURE_ARG_POINTER(aWindow);
nsCOMPtr<nsPIDOMWindowOuter> outer = nsPIDOMWindowOuter::From(aWindow);
NS_ENSURE_TRUE(outer, NS_ERROR_INVALID_ARG);
RefPtr<nsIWidget> widget =
mozilla::widget::WidgetUtils::DOMWindowToWidget(outer);
NS_ENSURE_TRUE(widget, NS_ERROR_FAILURE);
return StartNativeWindowMove(widget);
}
} // namespace zen

View File

@@ -0,0 +1,44 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#import <Cocoa/Cocoa.h>
#include "nsDebug.h"
#include "nsIWidget.h"
#include "nsObjCExceptions.h"
namespace zen {
// Cocoa side of StartNativeWindowMove()
nsresult StartNativeWindowMoveCocoa(nsIWidget* aWidget) {
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
NSWindow* window =
static_cast<NSWindow*>(aWidget->GetNativeData(NS_NATIVE_WINDOW));
NS_ENSURE_TRUE(window, NS_ERROR_FAILURE);
// performWindowDragWithEvent: needs a left-button event to start the
// WindowServer drag session. The triggering mousedown happened in a
// content process, so synthesize an equivalent event at the current
// mouse location.
NSPoint location = [window convertPointFromScreen:[NSEvent mouseLocation]];
NSEvent* event =
[NSEvent mouseEventWithType:NSEventTypeLeftMouseDragged
location:location
modifierFlags:0
timestamp:NSProcessInfo.processInfo.systemUptime
windowNumber:window.windowNumber
context:nil
eventNumber:0
clickCount:1
pressure:1.0];
NS_ENSURE_TRUE(event, NS_ERROR_FAILURE);
[window performWindowDragWithEvent:event];
return NS_OK;
NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
}
} // namespace zen

View File

@@ -85,9 +85,10 @@ const defaultKeyboardGroups = {
"zen-search-focus-shortcut",
"zen-search-focus-shortcut-alt",
"zen-find-shortcut",
"zen-search-find-again-shortcut-2",
"zen-search-find-again-shortcut",
"zen-search-find-again-shortcut-alt",
"zen-search-find-again-shortcut-prev",
"zen-search-find-again-shortcut-prev-alt",
],
pageOperations: [
"zen-text-action-copy-url-markdown-shortcut",
@@ -305,6 +306,29 @@ export class nsKeyShortcutModifiers {
}
class KeyShortcut {
static SHIFTED_SYMBOLS = {
1: "!",
2: "@",
3: "#",
4: "$",
5: "%",
6: "^",
7: "&",
8: "*",
9: "(",
0: ")",
"`": "~",
"-": "_",
"=": "+",
"[": "{",
"]": "}",
"\\": "|",
";": ":",
"'": '"',
",": "<",
".": ">",
"/": "?",
};
#id = "";
#key = "";
#keycode = "";
@@ -430,13 +454,35 @@ class KeyShortcut {
replaceWithChild(key) {
key.id = this.#id;
// When shift is pressed and the char changes when shifted (like 1 -> !),
// the XUL matches the shifted character so we need to emit the shifted character
// and drop the shift modifier so XUL can match
// This problem is also windows specific
let keyName = this.#key;
let modifiers = this.#modifiers;
if (AppConstants.platform == "win") {
const shiftedKey = KeyShortcut.SHIFTED_SYMBOLS[keyName];
if (shiftedKey && modifiers.shift) {
keyName = shiftedKey;
modifiers = new nsKeyShortcutModifiers(
modifiers.control,
modifiers.alt,
false, // -> for shift key
modifiers.meta,
modifiers.accel
);
}
}
if (this.#keycode) {
key.setAttribute("keycode", this.#keycode);
key.removeAttribute("key");
} else if (this.#key) {
} else if (keyName) {
// note to "mr. macos": Better use setAttribute, because without it, there's a
// risk of malforming the XUL element.
key.setAttribute("key", this.#key);
key.setAttribute("key", keyName);
key.removeAttribute("keycode");
} else {
key.removeAttribute("key");
@@ -451,7 +497,7 @@ class KeyShortcut {
if (this.#l10nId) {
// key.setAttribute('data-l10n-id', this.#l10nId);
}
key.setAttribute("modifiers", this.#modifiers.toString());
key.setAttribute("modifiers", modifiers.toString());
if (this.#action) {
key.setAttribute("command", this.#action);
}
@@ -848,7 +894,7 @@ class nsZenKeyboardShortcutsLoader {
}
class nsZenKeyboardShortcutsVersioner {
static LATEST_KBS_VERSION = 19;
static LATEST_KBS_VERSION = 20;
constructor() {}
@@ -1256,6 +1302,24 @@ class nsZenKeyboardShortcutsVersioner {
}
}
if (version < 20) {
// Migrate from version 19 to 20.
// - Disable "key_addTabSplitView" and "key_separateTabSplitView"
// since we already had "cmd_zenNewEmptySplit" and "cmd_zenSplitViewUnsplit" before Firefox 153.
// - Disable firefox's "viewOpenTabsSidebarKb" as it depends on firefox's native sidebar feature
const shouldBeDisabledShortcuts = [
"key_addTabSplitView",
"key_separateTabSplitView",
"viewOpenTabsSidebarKb",
];
for (let shortcut of data) {
if (shouldBeDisabledShortcuts.includes(shortcut.getID())) {
shortcut.shouldBeEmpty = true;
shortcut.setDisabled(true);
}
}
}
return data;
}
}

View File

@@ -9,6 +9,7 @@ EXTRA_PP_COMPONENTS += [
DIRS += [
"boosts",
"common",
"compact-mode",
"drag-and-drop",
"glance",
"live-folders",
@@ -21,4 +22,5 @@ DIRS += [
"spaces",
"space-routing",
"sync",
"window-drag",
]

View File

@@ -20,6 +20,7 @@ BROWSER_CHROME_MANIFESTS += [
"ub-actions/browser.toml",
"urlbar/browser.toml",
"welcome/browser.toml",
"window_drag/browser.toml",
"window_sync/browser.toml",
]

View File

@@ -0,0 +1,10 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
[DEFAULT]
support-files = [
"head.js",
]
["browser_window_drag_basic.js"]

View File

@@ -0,0 +1,109 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
let gDragStartCount = 0;
add_setup(async function () {
const observer = () => gDragStartCount++;
Services.obs.addObserver(observer, WINDOW_DRAG_TOPIC);
registerCleanupFunction(() => {
Services.obs.removeObserver(observer, WINDOW_DRAG_TOPIC);
});
});
add_task(async function test_drag_from_empty_top_area() {
await BrowserTestUtils.withNewTab(WINDOW_DRAG_TEST_PAGE, async browser => {
await synthesizeContentDrag(browser, 50, 50);
await TestUtils.waitForCondition(
() => gDragStartCount === 1,
"Dragging an empty area in the top region should start a window drag"
);
is(gDragStartCount, 1, "Exactly one window drag started");
});
});
add_task(async function test_no_drag_on_interactive_target() {
await BrowserTestUtils.withNewTab(WINDOW_DRAG_TEST_PAGE, async browser => {
// Start the gesture on top of the link. Messages from the same actor
// pair are ordered, so the control drag afterwards would arrive later.
await BrowserTestUtils.synthesizeMouse(
"#link",
5,
5,
{ type: "mousedown" },
browser
);
await BrowserTestUtils.synthesizeMouse(
"#link",
35,
15,
{ type: "mousemove", buttons: 1 },
browser
);
await BrowserTestUtils.synthesizeMouse(
"#link",
45,
20,
{ type: "mouseup" },
browser
);
// Control drag from an eligible area.
await synthesizeContentDrag(browser, 50, 50);
await TestUtils.waitForCondition(
() => gDragStartCount >= 2,
"The control drag should start a window drag"
);
is(
gDragStartCount,
2,
"Dragging a link must not start a window drag (only the control did)"
);
});
});
add_task(async function test_no_drag_below_top_region() {
await BrowserTestUtils.withNewTab(WINDOW_DRAG_TEST_PAGE, async browser => {
const innerHeight = await getContentInnerHeight(browser);
await synthesizeContentDrag(browser, 50, Math.floor(innerHeight * 0.6));
// Control drag from inside the top region.
await synthesizeContentDrag(browser, 50, 50);
await TestUtils.waitForCondition(
() => gDragStartCount >= 3,
"The control drag should start a window drag"
);
is(
gDragStartCount,
3,
"Dragging below the top region must not start a window drag"
);
});
});
add_task(async function test_pref_disables_window_drag() {
await SpecialPowers.pushPrefEnv({
set: [["zen.view.drag-window-from-content", false]],
});
await BrowserTestUtils.withNewTab(WINDOW_DRAG_TEST_PAGE, async browser => {
await synthesizeContentDrag(browser, 50, 50);
});
await SpecialPowers.popPrefEnv();
// Control drag with the pref back on, in a fresh tab so the actor is
// created again.
await BrowserTestUtils.withNewTab(WINDOW_DRAG_TEST_PAGE, async browser => {
await synthesizeContentDrag(browser, 50, 50);
await TestUtils.waitForCondition(
() => gDragStartCount >= 4,
"The control drag should start a window drag"
);
is(
gDragStartCount,
4,
"Dragging with the pref disabled must not start a window drag"
);
});
});

View File

@@ -0,0 +1,57 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const WINDOW_DRAG_TOPIC = "zen-window-drag-started";
const WINDOW_DRAG_TEST_PAGE = `https://example.com/document-builder.sjs?html=${encodeURIComponent(`
<!doctype html>
<style>
body { margin: 0; }
#link {
position: fixed;
top: 20px;
left: 300px;
width: 100px;
height: 30px;
display: block;
}
</style>
<a id="link" href="https://example.com/">a link</a>
`)}`;
/**
* Synthesizes a primary-button drag gesture inside the content area,
* starting at (x, y) and moving well past the drag threshold.
*/
async function synthesizeContentDrag(browser, x, y) {
await BrowserTestUtils.synthesizeMouse(
null,
x,
y,
{ type: "mousedown" },
browser
);
for (let i = 1; i <= 3; i++) {
await BrowserTestUtils.synthesizeMouse(
null,
x + i * 10,
y + i * 5,
{ type: "mousemove", buttons: 1 },
browser
);
}
await BrowserTestUtils.synthesizeMouse(
null,
x + 40,
y + 20,
{ type: "mouseup" },
browser
);
}
function getContentInnerHeight(browser) {
return SpecialPowers.spawn(browser, [], () => content.innerHeight);
}

View File

@@ -0,0 +1,330 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"dragRegionHeightPercentage",
"zen.view.drag-window-from-content.height-percentage",
10
);
XPCOMUtils.defineLazyServiceGetter(
lazy,
"zenWindowDragUtils",
"@mozilla.org/zen/window-drag-utils;1",
Ci.nsIZenWindowDragUtils
);
// Movement below this is considered a click, not a window drag. Fast
// clicks commonly slide a few pixels (especially on trackpads), and once
// the native move starts the OS swallows the mouseup — so keep this
// comfortably above click jitter or clicks in the region get lost.
const DRAG_START_THRESHOLD_PX = 10;
// Content that drives its own mouse interaction without being
// interactive HTML content in the spec sense.
const kAppContentTags = new Set(["audio", "canvas", "video"]);
const kInteractiveRoles = new Set([
"button",
"checkbox",
"combobox",
"grid",
"link",
"listbox",
"menu",
"menubar",
"menuitem",
"option",
"radio",
"scrollbar",
"searchbox",
"slider",
"spinbutton",
"switch",
"tab",
"textbox",
"toolbar",
"tree",
"treegrid",
]);
// Cursors that signal the page considers the area interactive or draggable.
const kInteractiveCursors = new Set([
"pointer",
"grab",
"grabbing",
"move",
"all-scroll",
"text",
"vertical-text",
"cell",
"crosshair",
"col-resize",
"row-resize",
"n-resize",
"e-resize",
"s-resize",
"w-resize",
"ne-resize",
"nw-resize",
"se-resize",
"sw-resize",
"ew-resize",
"ns-resize",
"nesw-resize",
"nwse-resize",
]);
const kGestureListenerOptions = { mozSystemGroup: true, capture: true };
const kGestureEvents = ["mousemove", "mouseup", "dragstart", "unload"];
export class ZenWindowDragChild extends JSWindowActorChild {
#tracking = false;
#dragging = false;
#startScreenX = 0;
#startScreenY = 0;
handleEvent(event) {
// Never let pages spoof the gesture with synthetic events.
if (!event.isTrusted) {
return;
}
switch (event.type) {
case "mousedown":
this.#onMouseDown(event);
break;
case "mousemove":
this.#onMouseMove(event);
break;
case "mouseup":
this.#onMouseUp(event);
break;
case "dragstart":
// The gesture turned out to be a real content drag. Let it win.
this.#reset();
break;
case "unload":
this.#reset();
break;
}
}
get #dragRegionHeight() {
const percentage =
Math.max(0, Math.min(100, lazy.dragRegionHeightPercentage)) / 100;
return this.contentWindow.innerHeight * percentage;
}
#screenPoint(event) {
const dpr = this.contentWindow.devicePixelRatio;
return {
screenX: Math.round(event.screenX * dpr),
screenY: Math.round(event.screenY * dpr),
};
}
#onMouseDown(event) {
if (this.#tracking || this.#dragging) {
// The native OS move swallows the mouseup, so a stale gesture may
// still be tracked. Recover and evaluate this mousedown normally.
this.#reset();
}
if (
event.button !== 0 ||
event.buttons !== 1 ||
event.detail !== 1 ||
event.defaultPrevented ||
event.ctrlKey ||
event.altKey ||
event.shiftKey ||
event.metaKey
) {
return;
}
const doc = this.document;
if (doc.fullscreenElement || doc.pointerLockElement) {
return;
}
// Only handle events from the top document itself; anything inside an
// (i)frame belongs to the page.
if (event.composedTarget?.ownerDocument !== doc) {
return;
}
if (event.clientY > this.#dragRegionHeight) {
return;
}
let overContent = true;
try {
overContent = this.#isEventOverDraggableContent(event);
} catch (e) {
console.error("ZenWindowDrag: eligibility check failed", e);
}
if (overContent) {
return;
}
const { screenX, screenY } = this.#screenPoint(event);
this.#startScreenX = screenX;
this.#startScreenY = screenY;
this.#tracking = true;
this.#addGestureListeners();
}
#onMouseMove(event) {
if (!this.#tracking) {
return;
}
// We missed the mouseup (e.g. the OS consumed it during the native
// move, or it happened outside the window).
if (!(event.buttons & 1)) {
this.#reset();
return;
}
if (this.#dragging) {
// Keep the page from extending a text selection under the gesture.
event.preventDefault();
return;
}
const point = this.#screenPoint(event);
const threshold =
DRAG_START_THRESHOLD_PX * this.contentWindow.devicePixelRatio;
if (
Math.hypot(
point.screenX - this.#startScreenX,
point.screenY - this.#startScreenY
) < threshold
) {
return;
}
this.#dragging = true;
// The mousedown may have moved the caret or started a stray selection.
this.contentWindow.getSelection()?.removeAllRanges();
// The OS takes over the drag from here.
this.sendAsyncMessage("ZenWindowDrag:StartDrag");
event.preventDefault();
}
#onMouseUp(event) {
if (!this.#tracking || event.button !== 0) {
return;
}
if (this.#dragging) {
event.preventDefault();
event.preventClickEvent();
}
this.#reset();
}
#reset() {
this.#tracking = false;
this.#dragging = false;
this.#removeGestureListeners();
}
#addGestureListeners() {
const win = this.contentWindow;
for (const type of kGestureEvents) {
win.addEventListener(type, this, kGestureListenerOptions);
}
}
#removeGestureListeners() {
const win = this.contentWindow;
if (!win) {
return;
}
for (const type of kGestureEvents) {
win.removeEventListener(type, this, kGestureListenerOptions);
}
}
/**
* Returns true if starting a window drag here would fight with content
* that the page wants to be clickable, draggable or selectable.
*
* @param {MouseEvent} event
*/
#isEventOverDraggableContent(event) {
// Scrollbars and other native anonymous parts only dispatch to the
// system group, which is us. Never treat those as a window drag.
if (event.originalTarget?.isNativeAnonymous) {
return true;
}
let target = event.composedTarget;
if (target?.nodeType === Node.TEXT_NODE) {
target = target.parentElement;
}
if (!target || target.nodeType !== Node.ELEMENT_NODE) {
return true;
}
for (let node = target; node; node = node.flattenedTreeParentNode) {
if (node.nodeType !== Node.ELEMENT_NODE) {
continue;
}
if (this.#isInteractiveElement(node)) {
return true;
}
}
return (
this.#hasInteractiveCursor(target) || this.#isOverSelectableText(event)
);
}
/**
* A drag starting over selectable text should select it, not move the
* window. rangeParent is the caret position Gecko computed for the
* event, so this also covers empty space on the same line, where
* dragging extends a selection.
*
* @param {MouseEvent} event
*/
#isOverSelectableText(event) {
const node = event.rangeParent;
if (node?.nodeType !== Node.TEXT_NODE) {
return false;
}
const parent = node.parentElement;
return (
!parent ||
this.contentWindow.getComputedStyle(parent).userSelect !== "none"
);
}
#isInteractiveElement(element) {
// Gecko's own notion of interactive, editable or draggable content.
if (lazy.zenWindowDragUtils.isInteractiveContent(element)) {
return true;
}
if (kAppContentTags.has(element.localName)) {
return true;
}
// Declarative signals the engine check doesn't cover: ARIA widget
// roles and explicit tab stops.
if (
element.tabIndex >= 0 &&
element !== this.document.body &&
element !== this.document.documentElement
) {
return true;
}
const role = element.getAttribute?.("role");
return !!role && kInteractiveRoles.has(role.toLowerCase());
}
#hasInteractiveCursor(element) {
const style = element.ownerGlobal?.getComputedStyle(element);
if (!style) {
return false;
}
// The keyword is always the last component of the computed value.
// Don't split on "," as url() cursor images may contain commas.
const cursor = style.cursor.match(/[a-z-]+$/)?.[0];
return kInteractiveCursors.has(cursor);
}
}

View File

@@ -0,0 +1,33 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
XPCOMUtils.defineLazyServiceGetter(
lazy,
"zenDragAndDropService",
"@mozilla.org/zen/drag-and-drop;1",
Ci.nsIZenDragAndDrop
);
export class ZenWindowDragParent extends JSWindowActorParent {
receiveMessage(message) {
if (message.name !== "ZenWindowDrag:StartDrag") {
return;
}
const win = this.browsingContext.topChromeWindow;
if (!win || win.closed || win.windowState === win.STATE_FULLSCREEN) {
return;
}
if (Cu.isInAutomation) {
// Tests can't exercise a real OS drag session; let them observe the
// decision instead.
Services.obs.notifyObservers(win, "zen-window-drag-started");
return;
}
lazy.zenDragAndDropService.beginNativeWindowMove(win);
}
}

View File

@@ -0,0 +1,13 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
Classes = [
{
'cid': '{13df8214-70b4-4d7b-af8c-86081cbc5801}',
'interfaces': ['nsIZenWindowDragUtils'],
'contract_ids': ['@mozilla.org/zen/window-drag-utils;1'],
'type': 'zen::nsZenWindowDragUtils',
'headers': ['mozilla/nsZenWindowDragUtils.h'],
},
]

View File

@@ -0,0 +1,27 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
FINAL_TARGET_FILES.actors += [
"actors/ZenWindowDragChild.sys.mjs",
"actors/ZenWindowDragParent.sys.mjs",
]
XPIDL_SOURCES += [
"nsIZenWindowDragUtils.idl",
]
EXPORTS.mozilla += [
"nsZenWindowDragUtils.h",
]
SOURCES += [
"nsZenWindowDragUtils.cpp",
]
XPCOM_MANIFESTS += [
"components.conf",
]
FINAL_LIBRARY = "xul"
XPIDL_MODULE = "zen_window_drag"

View File

@@ -0,0 +1,23 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
webidl Node;
/**
* @brief Utilities for deciding when a content-area gesture may move
* the window.
*/
[scriptable, uuid(595ba6e8-abe2-4619-acad-cf492306f608)]
interface nsIZenWindowDragUtils : nsISupports {
/**
* @brief Whether the node is content the page expects the user to
* interact with: interactive HTML content (links, form controls,
* etc.), editable content, or content that can be dragged
* (draggable attribute, links and loaded images).
* @param node The node to check.
*/
boolean isInteractiveContent(in Node node);
};

View File

@@ -0,0 +1,33 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsZenWindowDragUtils.h"
#include "mozilla/dom/Element.h"
#include "nsContentUtils.h"
#include "nsIContent.h"
namespace zen {
NS_IMPL_ISUPPORTS(nsZenWindowDragUtils, nsIZenWindowDragUtils)
NS_IMETHODIMP
nsZenWindowDragUtils::IsInteractiveContent(nsINode* aNode, bool* aResult) {
*aResult = false;
NS_ENSURE_ARG_POINTER(aNode);
nsIContent* content = nsIContent::FromNode(aNode);
if (!content) {
return NS_OK;
}
if (content->IsEditable() || nsContentUtils::ContentIsDraggable(content)) {
*aResult = true;
return NS_OK;
}
mozilla::dom::Element* element = mozilla::dom::Element::FromNode(content);
*aResult = element && element->IsInteractiveHTMLContent();
return NS_OK;
}
} // namespace zen

View File

@@ -0,0 +1,27 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef zen_nsZenWindowDragUtils_h_
#define zen_nsZenWindowDragUtils_h_
#include "nsIZenWindowDragUtils.h"
#define ZEN_WINDOW_DRAG_UTILS_CONTRACTID "@mozilla.org/zen/window-drag-utils;1"
namespace zen {
class nsZenWindowDragUtils final : public nsIZenWindowDragUtils {
NS_DECL_ISUPPORTS
NS_DECL_NSIZENWINDOWDRAGUTILS
public:
nsZenWindowDragUtils() = default;
private:
~nsZenWindowDragUtils() = default;
};
} // namespace zen
#endif