mirror of
https://github.com/zen-browser/desktop.git
synced 2026-08-01 20:58:54 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c06aae839f | ||
|
|
4ab322dd3b | ||
|
|
ccbd934482 | ||
|
|
1d05184a2a |
@@ -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
|
||||
|
||||
@@ -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: 30
|
||||
|
||||
- name: zen.view.borderless-fullscreen
|
||||
value: true
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,58 @@
|
||||
#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>
|
||||
#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);
|
||||
GdkSeat* seat =
|
||||
gdk_display_get_default_seat(gdk_window_get_display(toplevel));
|
||||
GdkDevice* pointer = seat ? gdk_seat_get_pointer(seat) : nullptr;
|
||||
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 +95,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
|
||||
|
||||
44
src/zen/drag-and-drop/nsZenNativeWindowMove.mm
Normal file
44
src/zen/drag-and-drop/nsZenNativeWindowMove.mm
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,4 +21,5 @@ DIRS += [
|
||||
"spaces",
|
||||
"space-routing",
|
||||
"sync",
|
||||
"window-drag",
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
10
src/zen/tests/window_drag/browser.toml
Normal file
10
src/zen/tests/window_drag/browser.toml
Normal 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"]
|
||||
109
src/zen/tests/window_drag/browser_window_drag_basic.js
Normal file
109
src/zen/tests/window_drag/browser_window_drag_basic.js
Normal 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"
|
||||
);
|
||||
});
|
||||
});
|
||||
57
src/zen/tests/window_drag/head.js
Normal file
57
src/zen/tests/window_drag/head.js
Normal 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);
|
||||
}
|
||||
333
src/zen/window-drag/actors/ZenWindowDragChild.sys.mjs
Normal file
333
src/zen/window-drag/actors/ZenWindowDragChild.sys.mjs
Normal file
@@ -0,0 +1,333 @@
|
||||
// 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",
|
||||
30
|
||||
);
|
||||
|
||||
// A small threshold to allow for minor mouse jitter during a normal click.
|
||||
// Anything beyond this is considered an intentional window drag.
|
||||
const DRAG_START_THRESHOLD_PX = 4;
|
||||
|
||||
const kInteractiveTags = new Set([
|
||||
"a",
|
||||
"area",
|
||||
"audio",
|
||||
"button",
|
||||
"canvas",
|
||||
"details",
|
||||
"dialog",
|
||||
"embed",
|
||||
"frame",
|
||||
"iframe",
|
||||
"img",
|
||||
"input",
|
||||
"label",
|
||||
"menu",
|
||||
"object",
|
||||
"optgroup",
|
||||
"option",
|
||||
"select",
|
||||
"summary",
|
||||
"textarea",
|
||||
"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);
|
||||
}
|
||||
|
||||
#isInteractiveElement(element) {
|
||||
if (kInteractiveTags.has(element.localName)) {
|
||||
return true;
|
||||
}
|
||||
// Covers [draggable="true"] and elements draggable by default,
|
||||
// like links and images.
|
||||
if (element.draggable) {
|
||||
return true;
|
||||
}
|
||||
if (element.isContentEditable) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
element.tabIndex >= 0 &&
|
||||
element !== this.document.body &&
|
||||
element !== this.document.documentElement
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const role = element.getAttribute?.("role");
|
||||
if (role && kInteractiveRoles.has(role.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
// Inline event handlers are a strong hint of a custom widget.
|
||||
if (
|
||||
element.onclick ||
|
||||
element.onmousedown ||
|
||||
element.onpointerdown ||
|
||||
element.ondragstart
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
||||
33
src/zen/window-drag/actors/ZenWindowDragParent.sys.mjs
Normal file
33
src/zen/window-drag/actors/ZenWindowDragParent.sys.mjs
Normal 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);
|
||||
}
|
||||
}
|
||||
8
src/zen/window-drag/moz.build
Normal file
8
src/zen/window-drag/moz.build
Normal file
@@ -0,0 +1,8 @@
|
||||
# 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",
|
||||
]
|
||||
Reference in New Issue
Block a user