mirror of
https://github.com/zen-browser/desktop.git
synced 2026-08-27 17:01:30 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6c86f570a | ||
|
|
0e5f2dcd11 | ||
|
|
c0ade4d063 | ||
|
|
11acda2db3 | ||
|
|
77aa6ff6c9 | ||
|
|
871bdde4cc | ||
|
|
b3bd688ca4 | ||
|
|
36c71c5c2b | ||
|
|
c74486102f | ||
|
|
045ebdf0ae | ||
|
|
bc540320f1 | ||
|
|
9714c15c7a | ||
|
|
ccbd934482 | ||
|
|
1d05184a2a |
@@ -34,8 +34,8 @@ Zen is a firefox-based browser with the aim of pushing your productivity to a ne
|
||||
|
||||
### Firefox Versions
|
||||
|
||||
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `153.0.1`! 🚀
|
||||
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 153.0.1`!
|
||||
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `153.0.3`! 🚀
|
||||
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 153.0.3`!
|
||||
|
||||
### Contributing
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
30959a45eaaf0bc499f3054793c76c6d7f0c8bc2
|
||||
8a0a534248f80d8c53a8997f24683521c3a50ebc
|
||||
@@ -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
|
||||
|
||||
@@ -151,6 +151,9 @@ zen-sidebar-notification-updated-heading = Update Complete!
|
||||
zen-sidebar-notification-updated-label = What's new in { -brand-short-name }
|
||||
zen-sidebar-notification-updated-tooltip =
|
||||
.title = View Release Notes
|
||||
zen-sidebar-notification-donate-label = Support { -brand-short-name }
|
||||
zen-sidebar-notification-donate-tooltip =
|
||||
.title = Donate to the project
|
||||
zen-sidebar-notification-restart-safe-mode-label = Something broke?
|
||||
zen-sidebar-notification-restart-safe-mode-tooltip =
|
||||
.title = Restart in Safe Mode
|
||||
|
||||
@@ -20,6 +20,9 @@ zen-live-folder-github-option-assigned-self =
|
||||
zen-live-folder-github-option-review-requested =
|
||||
.label = Review Requests
|
||||
|
||||
zen-live-folder-github-option-include-drafts =
|
||||
.label = Include Draft Pull Requests
|
||||
|
||||
zen-live-folder-type-rss =
|
||||
.label = RSS Feed
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs
|
||||
index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1db40aecce 100644
|
||||
index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee4aa3be28 100644
|
||||
--- a/browser/components/sessionstore/SessionStore.sys.mjs
|
||||
+++ b/browser/components/sessionstore/SessionStore.sys.mjs
|
||||
@@ -129,6 +129,9 @@ const TAB_EVENTS = [
|
||||
@@ -12,7 +12,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
];
|
||||
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
@@ -199,6 +202,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||
@@ -201,6 +204,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||
TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs",
|
||||
TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
|
||||
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
||||
@@ -21,7 +21,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
});
|
||||
|
||||
ChromeUtils.defineLazyGetter(lazy, "blankURI", () => {
|
||||
@@ -1287,10 +1292,7 @@ var SessionStoreInternal = {
|
||||
@@ -1300,10 +1305,7 @@ var SessionStoreInternal = {
|
||||
*/
|
||||
get willAutoRestore() {
|
||||
return (
|
||||
@@ -33,7 +33,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1965,6 +1967,9 @@ var SessionStoreInternal = {
|
||||
@@ -1990,6 +1992,9 @@ var SessionStoreInternal = {
|
||||
case "TabPinned":
|
||||
case "TabUnpinned":
|
||||
case "SwapDocShells":
|
||||
@@ -43,7 +43,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
this.saveStateDelayed(win);
|
||||
break;
|
||||
case "TabGroupCreate":
|
||||
@@ -2074,6 +2079,10 @@ var SessionStoreInternal = {
|
||||
@@ -2099,6 +2104,10 @@ var SessionStoreInternal = {
|
||||
this._windows[aWindow.__SSi].isTaskbarTab = true;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
if (lazy.AIWindow.isAIWindowActiveAndEnabled(aWindow)) {
|
||||
this._windows[aWindow.__SSi].isAIWindow = true;
|
||||
}
|
||||
@@ -2110,7 +2119,7 @@ var SessionStoreInternal = {
|
||||
@@ -2135,7 +2144,7 @@ var SessionStoreInternal = {
|
||||
let isTaskbarTab = this._windows[aWindow.__SSi].isTaskbarTab;
|
||||
// A regular window is not a private window, taskbar tab window, or popup window
|
||||
let isRegularWindow =
|
||||
@@ -63,7 +63,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
|
||||
// perform additional initialization when the first window is loading
|
||||
if (lazy.RunState.isStopped) {
|
||||
@@ -2122,7 +2131,7 @@ var SessionStoreInternal = {
|
||||
@@ -2147,7 +2156,7 @@ var SessionStoreInternal = {
|
||||
// to disk to NOW() to enforce a full interval before the next write.
|
||||
lazy.SessionSaver.updateLastSaveTime();
|
||||
|
||||
@@ -72,7 +72,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
this._log.debug(
|
||||
"initializeWindow, the window is private or a web app. Saving SessionStartup.state for possibly restoring later"
|
||||
);
|
||||
@@ -2164,6 +2173,7 @@ var SessionStoreInternal = {
|
||||
@@ -2191,6 +2200,7 @@ var SessionStoreInternal = {
|
||||
null,
|
||||
"sessionstore-one-or-no-tab-restored"
|
||||
);
|
||||
@@ -80,7 +80,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
this._deferredAllWindowsRestored.resolve();
|
||||
}
|
||||
// this window was opened by _openWindowWithState
|
||||
@@ -2213,7 +2223,6 @@ var SessionStoreInternal = {
|
||||
@@ -2240,7 +2250,6 @@ var SessionStoreInternal = {
|
||||
if (closedWindowState) {
|
||||
let newWindowState;
|
||||
if (
|
||||
@@ -88,7 +88,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
!lazy.SessionStartup.willRestore()
|
||||
) {
|
||||
// We want to split the window up into pinned tabs and unpinned tabs.
|
||||
@@ -2249,6 +2258,7 @@ var SessionStoreInternal = {
|
||||
@@ -2276,6 +2285,7 @@ var SessionStoreInternal = {
|
||||
}
|
||||
|
||||
if (newWindowState) {
|
||||
@@ -96,23 +96,17 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
// Ensure that the window state isn't hidden
|
||||
this._restoreCount = 1;
|
||||
let state = { windows: [newWindowState] };
|
||||
@@ -2277,6 +2287,15 @@ var SessionStoreInternal = {
|
||||
@@ -2312,6 +2322,9 @@ var SessionStoreInternal = {
|
||||
});
|
||||
this._shouldRestoreLastSession = false;
|
||||
}
|
||||
+ else if (!aInitialState && isRegularWindow) {
|
||||
+ let windowPromises = [];
|
||||
+ for (let window of this._browserWindows) {
|
||||
+ windowPromises.push(lazy.TabStateFlusher.flushWindow(window));
|
||||
+ }
|
||||
+ Promise.all(windowPromises).finally(() => {
|
||||
+ lazy.ZenSessionStore.restoreNewWindow(aWindow, this);
|
||||
+ });
|
||||
+ lazy.ZenSessionStore.restoreNewWindow(aWindow, this);
|
||||
+ }
|
||||
|
||||
if (this._restoreLastWindow && aWindow.toolbar.visible) {
|
||||
// always reset (if not a popup window)
|
||||
@@ -2427,7 +2446,7 @@ var SessionStoreInternal = {
|
||||
@@ -2462,7 +2475,7 @@ var SessionStoreInternal = {
|
||||
|
||||
var tabbrowser = aWindow.gBrowser;
|
||||
|
||||
@@ -121,7 +115,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
|
||||
TAB_EVENTS.forEach(function (aEvent) {
|
||||
tabbrowser.tabContainer.removeEventListener(aEvent, this, true);
|
||||
@@ -2478,7 +2497,7 @@ var SessionStoreInternal = {
|
||||
@@ -2513,7 +2526,7 @@ var SessionStoreInternal = {
|
||||
|
||||
let isLastRegularWindow =
|
||||
Object.values(this._windows).filter(
|
||||
@@ -130,7 +124,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
).length == 1;
|
||||
this._log.debug(
|
||||
`onClose, closing window isLastRegularWindow? ${isLastRegularWindow}`
|
||||
@@ -2535,8 +2554,8 @@ var SessionStoreInternal = {
|
||||
@@ -2570,8 +2583,8 @@ var SessionStoreInternal = {
|
||||
// 2) Flush the window.
|
||||
// 3) When the flush is complete, revisit our decision to store the window
|
||||
// in _closedWindows, and add/remove as necessary.
|
||||
@@ -141,7 +135,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
}
|
||||
|
||||
completionPromise = lazy.TabStateFlusher.flushWindow(aWindow).then(() => {
|
||||
@@ -2556,8 +2575,9 @@ var SessionStoreInternal = {
|
||||
@@ -2591,8 +2604,9 @@ var SessionStoreInternal = {
|
||||
|
||||
// Save non-private windows if they have at
|
||||
// least one saveable tab or are the last window.
|
||||
@@ -153,7 +147,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
|
||||
if (!isLastWindow && winData.closedId > -1) {
|
||||
this._addClosedAction(
|
||||
@@ -2633,7 +2653,7 @@ var SessionStoreInternal = {
|
||||
@@ -2668,7 +2682,7 @@ var SessionStoreInternal = {
|
||||
* to call this method again asynchronously (for example, after
|
||||
* a window flush).
|
||||
*/
|
||||
@@ -162,7 +156,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
// Make sure SessionStore is still running, and make sure that we
|
||||
// haven't chosen to forget this window.
|
||||
if (
|
||||
@@ -2650,6 +2670,7 @@ var SessionStoreInternal = {
|
||||
@@ -2685,6 +2699,7 @@ var SessionStoreInternal = {
|
||||
// _closedWindows from a previous call to this function.
|
||||
let winIndex = this._closedWindows.indexOf(winData);
|
||||
let alreadyStored = winIndex != -1;
|
||||
@@ -170,7 +164,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
// If sidebar command is truthy, i.e. sidebar is open, store sidebar settings
|
||||
let shouldStore = hasSaveableTabs || isLastWindow;
|
||||
|
||||
@@ -3467,7 +3488,7 @@ var SessionStoreInternal = {
|
||||
@@ -3507,7 +3522,7 @@ var SessionStoreInternal = {
|
||||
if (!isPrivateWindow && tabState.isPrivate) {
|
||||
return;
|
||||
}
|
||||
@@ -179,7 +173,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4206,6 +4227,12 @@ var SessionStoreInternal = {
|
||||
@@ -4246,6 +4261,12 @@ var SessionStoreInternal = {
|
||||
Math.min(tabState.index, tabState.entries.length)
|
||||
);
|
||||
tabState.pinned = false;
|
||||
@@ -192,7 +186,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
|
||||
if (inBackground === false) {
|
||||
aWindow.gBrowser.selectedTab = newTab;
|
||||
@@ -4642,6 +4669,8 @@ var SessionStoreInternal = {
|
||||
@@ -4679,6 +4700,8 @@ var SessionStoreInternal = {
|
||||
// Append the tab if we're opening into a different window,
|
||||
tabIndex: aSource == aTargetWindow ? pos : Infinity,
|
||||
pinned: state.pinned,
|
||||
@@ -201,7 +195,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
userContextId: state.userContextId,
|
||||
skipLoad: true,
|
||||
preferredRemoteType,
|
||||
@@ -5146,9 +5175,10 @@ var SessionStoreInternal = {
|
||||
@@ -5176,9 +5199,10 @@ var SessionStoreInternal = {
|
||||
if (activePageData.title && activePageData.title != activePageData.url) {
|
||||
win.gBrowser.setInitialTabTitle(tab, activePageData.title, {
|
||||
isContentTitle: true,
|
||||
@@ -213,7 +207,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5513,7 +5543,7 @@ var SessionStoreInternal = {
|
||||
@@ -5543,7 +5567,7 @@ var SessionStoreInternal = {
|
||||
|
||||
for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) {
|
||||
let tab = tabbrowser.tabs[i];
|
||||
@@ -222,7 +216,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
removableTabs.push(tab);
|
||||
}
|
||||
}
|
||||
@@ -5626,7 +5656,7 @@ var SessionStoreInternal = {
|
||||
@@ -5656,7 +5680,7 @@ var SessionStoreInternal = {
|
||||
|
||||
// collect the data for all windows
|
||||
for (ix in this._windows) {
|
||||
@@ -231,7 +225,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
// window data is still in _statesToRestore
|
||||
continue;
|
||||
}
|
||||
@@ -5770,11 +5800,12 @@ var SessionStoreInternal = {
|
||||
@@ -5800,11 +5824,12 @@ var SessionStoreInternal = {
|
||||
}
|
||||
|
||||
let tabbrowser = aWindow.gBrowser;
|
||||
@@ -245,7 +239,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
// update the internal state data for this window
|
||||
for (let tab of tabs) {
|
||||
if (tab == aWindow.FirefoxViewHandler.tab) {
|
||||
@@ -5785,6 +5816,9 @@ var SessionStoreInternal = {
|
||||
@@ -5815,6 +5840,9 @@ var SessionStoreInternal = {
|
||||
tabsData.push(tabData);
|
||||
}
|
||||
|
||||
@@ -255,7 +249,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
// update tab group state for this window
|
||||
winData.groups = [];
|
||||
for (let tabGroup of aWindow.gBrowser.tabGroups) {
|
||||
@@ -5801,7 +5835,7 @@ var SessionStoreInternal = {
|
||||
@@ -5831,7 +5859,7 @@ var SessionStoreInternal = {
|
||||
// a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab,
|
||||
// since it's only inserted into the tab strip after it's selected).
|
||||
if (aWindow.FirefoxViewHandler.tab?.selected) {
|
||||
@@ -264,7 +258,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
winData.title = tabbrowser.tabs[0].label;
|
||||
}
|
||||
winData.selected = selectedIndex;
|
||||
@@ -6003,8 +6037,8 @@ var SessionStoreInternal = {
|
||||
@@ -6033,8 +6061,8 @@ var SessionStoreInternal = {
|
||||
// selectTab represents.
|
||||
let selectTab = 0;
|
||||
if (overwriteTabs) {
|
||||
@@ -275,7 +269,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
selectTab = Math.min(selectTab, winData.tabs.length);
|
||||
}
|
||||
|
||||
@@ -6026,6 +6060,7 @@ var SessionStoreInternal = {
|
||||
@@ -6056,6 +6084,7 @@ var SessionStoreInternal = {
|
||||
if (overwriteTabs) {
|
||||
for (let i = tabbrowser.browsers.length - 1; i >= 0; i--) {
|
||||
if (!tabbrowser.tabs[i].selected) {
|
||||
@@ -283,7 +277,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
tabbrowser.removeTab(tabbrowser.tabs[i]);
|
||||
}
|
||||
}
|
||||
@@ -6060,6 +6095,12 @@ var SessionStoreInternal = {
|
||||
@@ -6090,6 +6119,12 @@ var SessionStoreInternal = {
|
||||
savedTabGroup => !openTabGroupIdsInWindow.has(savedTabGroup.id)
|
||||
);
|
||||
}
|
||||
@@ -296,7 +290,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
|
||||
// Move the originally open tabs to the end.
|
||||
if (initialTabs) {
|
||||
@@ -6626,6 +6667,25 @@ var SessionStoreInternal = {
|
||||
@@ -6656,6 +6691,25 @@ var SessionStoreInternal = {
|
||||
|
||||
// Most of tabData has been restored, now continue with restoring
|
||||
// attributes that may trigger external events.
|
||||
@@ -322,7 +316,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
|
||||
if (tabData.pinned) {
|
||||
tabbrowser.pinTab(tab);
|
||||
@@ -6793,6 +6853,9 @@ var SessionStoreInternal = {
|
||||
@@ -6838,6 +6892,9 @@ var SessionStoreInternal = {
|
||||
aWindow.gURLBar.readOnly = false;
|
||||
}
|
||||
}
|
||||
@@ -332,7 +326,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
|
||||
let promiseParts = Promise.withResolvers();
|
||||
aWindow.setTimeout(() => {
|
||||
@@ -7588,7 +7651,7 @@ var SessionStoreInternal = {
|
||||
@@ -7633,7 +7690,7 @@ var SessionStoreInternal = {
|
||||
|
||||
let groupsToSave = new Map();
|
||||
for (let tIndex = 0; tIndex < window.tabs.length; ) {
|
||||
@@ -341,7 +335,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
// Adjust window.selected
|
||||
if (tIndex + 1 < window.selected) {
|
||||
window.selected -= 1;
|
||||
@@ -7603,7 +7666,7 @@ var SessionStoreInternal = {
|
||||
@@ -7648,7 +7705,7 @@ var SessionStoreInternal = {
|
||||
);
|
||||
// We don't want to increment tIndex here.
|
||||
continue;
|
||||
@@ -350,7 +344,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
// Convert any open groups into saved groups.
|
||||
let groupStateToSave = window.groups.find(
|
||||
groupState => groupState.id == window.tabs[tIndex].groupId
|
||||
@@ -8062,7 +8125,6 @@ var SessionStoreInternal = {
|
||||
@@ -8193,7 +8250,6 @@ var SessionStoreInternal = {
|
||||
timer.initWithCallback(
|
||||
function () {
|
||||
if (beats <= 0) {
|
||||
@@ -358,7 +352,7 @@ index 183543c5f29ff4c879d3058e4c09faf376a69cb7..ab482c32cc697ccef82bf0aeaa0e3a1d
|
||||
Glean.sessionRestore.shutdownFlushAllOutcomes.timed_out.add(1);
|
||||
deferred.resolve();
|
||||
}
|
||||
@@ -8540,6 +8602,7 @@ var SessionStoreInternal = {
|
||||
@@ -8671,6 +8727,7 @@ var SessionStoreInternal = {
|
||||
if (
|
||||
!savedTabGroupState.tabs.length ||
|
||||
this.getSavedTabGroup(savedTabGroupState.id)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/browser/components/urlbar/content/UrlbarInput.mjs b/browser/components/urlbar/content/UrlbarInput.mjs
|
||||
index 0b1b4b2f0f8d6b0fb126aa1224409ab6f1ffb8d8..942c6e1e3b6cd9de65df54a1d83884f2633c1284 100644
|
||||
index 0b1b4b2f0f8d6b0fb126aa1224409ab6f1ffb8d8..4405e1bf385477c2161ae3a5a62e32c1403a3630 100644
|
||||
--- a/browser/components/urlbar/content/UrlbarInput.mjs
|
||||
+++ b/browser/components/urlbar/content/UrlbarInput.mjs
|
||||
@@ -98,6 +98,13 @@ const lazy = XPCOMUtils.declareLazy({
|
||||
@@ -292,7 +292,11 @@ index 0b1b4b2f0f8d6b0fb126aa1224409ab6f1ffb8d8..942c6e1e3b6cd9de65df54a1d83884f2
|
||||
switch (event.target) {
|
||||
case this.inputField:
|
||||
case this._inputContainer:
|
||||
@@ -5374,7 +5495,7 @@ ${
|
||||
@@ -5371,10 +5492,11 @@ ${
|
||||
}
|
||||
if (untrim) {
|
||||
this._setValue(this._untrimmedValue);
|
||||
+ this.setSelectionRange(0, this.value.length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +305,7 @@ index 0b1b4b2f0f8d6b0fb126aa1224409ab6f1ffb8d8..942c6e1e3b6cd9de65df54a1d83884f2
|
||||
this.view.autoOpen({ event });
|
||||
} else {
|
||||
if (this._untrimOnFocusAfterKeydown) {
|
||||
@@ -5414,9 +5535,16 @@ ${
|
||||
@@ -5414,9 +5536,16 @@ ${
|
||||
}
|
||||
|
||||
_on_mousedown(event) {
|
||||
@@ -319,7 +323,7 @@ index 0b1b4b2f0f8d6b0fb126aa1224409ab6f1ffb8d8..942c6e1e3b6cd9de65df54a1d83884f2
|
||||
if (
|
||||
event.composedTarget != this.inputField &&
|
||||
event.composedTarget != this._inputContainer
|
||||
@@ -5426,6 +5554,10 @@ ${
|
||||
@@ -5426,6 +5555,10 @@ ${
|
||||
|
||||
this.focusedViaMousedown = !this.focused;
|
||||
this.#preventClickSelectsAll = this.focused;
|
||||
@@ -330,7 +334,7 @@ index 0b1b4b2f0f8d6b0fb126aa1224409ab6f1ffb8d8..942c6e1e3b6cd9de65df54a1d83884f2
|
||||
|
||||
// Keep the focus status, since the attribute may be changed
|
||||
// upon calling this.focus().
|
||||
@@ -5461,7 +5593,7 @@ ${
|
||||
@@ -5461,7 +5594,7 @@ ${
|
||||
}
|
||||
// Don't close the view when clicking on a tab; we may want to keep the
|
||||
// view open on tab switch, and the TabSelect event arrived earlier.
|
||||
@@ -339,7 +343,7 @@ index 0b1b4b2f0f8d6b0fb126aa1224409ab6f1ffb8d8..942c6e1e3b6cd9de65df54a1d83884f2
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -5750,7 +5882,7 @@ ${
|
||||
@@ -5750,7 +5883,7 @@ ${
|
||||
// When we are in actions search mode we can show more results so
|
||||
// increase the limit.
|
||||
let maxResults =
|
||||
|
||||
@@ -816,61 +816,57 @@
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#zen-media-playpause-button {
|
||||
.zen-media-playpause-button {
|
||||
list-style-image: url("media-play.svg");
|
||||
}
|
||||
|
||||
#zen-media-controls-toolbar.playing #zen-media-playpause-button {
|
||||
.zen-media-card.playing .zen-media-playpause-button {
|
||||
list-style-image: url("media-pause.svg");
|
||||
}
|
||||
|
||||
#zen-media-nexttrack-button {
|
||||
.zen-media-nexttrack-button {
|
||||
list-style-image: url("media-next.svg");
|
||||
}
|
||||
|
||||
#zen-media-previoustrack-button {
|
||||
.zen-media-previoustrack-button {
|
||||
list-style-image: url("media-previous.svg");
|
||||
}
|
||||
|
||||
#zen-media-controls-toolbar[muted] #zen-media-mute-button {
|
||||
.zen-media-card[muted] .zen-media-mute-button {
|
||||
list-style-image: url("media-mute.svg");
|
||||
}
|
||||
|
||||
#zen-media-mute-button {
|
||||
.zen-media-mute-button {
|
||||
list-style-image: url("media-unmute.svg");
|
||||
}
|
||||
|
||||
#zen-media-close-button {
|
||||
.zen-media-close-button {
|
||||
list-style-image: url("close.svg");
|
||||
}
|
||||
|
||||
#zen-media-focus-button:hover {
|
||||
.zen-media-focus-button:hover {
|
||||
list-style-image: url("screen.svg");
|
||||
}
|
||||
|
||||
#zen-media-close-button {
|
||||
list-style-image: url("close.svg");
|
||||
}
|
||||
|
||||
#zen-media-mute-mic-button {
|
||||
.zen-media-mute-mic-button {
|
||||
list-style-image: url("microphone-fill.svg");
|
||||
}
|
||||
|
||||
#zen-media-controls-toolbar[mic-muted] #zen-media-mute-mic-button {
|
||||
.zen-media-card[mic-muted] .zen-media-mute-mic-button {
|
||||
list-style-image: url("microphone-blocked-fill.svg");
|
||||
fill: rgb(224, 41, 29);
|
||||
}
|
||||
|
||||
#zen-media-mute-camera-button {
|
||||
.zen-media-mute-camera-button {
|
||||
list-style-image: url("video-fill.svg");
|
||||
}
|
||||
|
||||
#zen-media-controls-toolbar[camera-muted] #zen-media-mute-camera-button {
|
||||
.zen-media-card[camera-muted] .zen-media-mute-camera-button {
|
||||
list-style-image: url("video-blocked-fill.svg");
|
||||
fill: rgb(224, 41, 29);
|
||||
}
|
||||
|
||||
#zen-media-pip-button {
|
||||
.zen-media-pip-button {
|
||||
list-style-image: url("chrome://global/skin/media/picture-in-picture-open.svg");
|
||||
}
|
||||
|
||||
|
||||
@@ -81,8 +81,15 @@ export class nsZenBoostStyles {
|
||||
}
|
||||
|
||||
if (fontCase != "" || fontFamily != "") {
|
||||
const ignoredElements = [
|
||||
".google-symbols",
|
||||
"gf-load-icon-font",
|
||||
"mat-icon",
|
||||
".google-material-icons",
|
||||
"span",
|
||||
];
|
||||
style += `/* Text Format */\n`;
|
||||
style += `body *:not(.google-symbols, gf-load-icon-font, mat-icon, .google-material-icons) {\n`;
|
||||
style += `body *:not(${ignoredElements.join(",")}) {\n`;
|
||||
style += `${fontFamily}\n`;
|
||||
style += `${fontCase}\n`;
|
||||
style += `}\n`;
|
||||
|
||||
@@ -7,7 +7,7 @@ import checkForZenUpdates, {
|
||||
} from "chrome://browser/content/ZenUpdates.mjs";
|
||||
|
||||
class ZenStartup {
|
||||
#watermarkIgnoreElements = ["zen-toast-container"];
|
||||
#watermarkIgnoreElements = ["zen-toast-container", "zen-browser-background"];
|
||||
#hasInitializedLayout = false;
|
||||
|
||||
isReady = false;
|
||||
@@ -53,12 +53,11 @@ class ZenStartup {
|
||||
// overlap and interaction issues with vertical tabs
|
||||
document.getElementById("browser").prepend(deckTemplate);
|
||||
|
||||
gZenWorkspaces.init();
|
||||
setTimeout(() => {
|
||||
gZenWorkspaces.init().then(() => {
|
||||
gZenUIManager.init();
|
||||
this.#initUIComponents();
|
||||
this.#checkForWelcomePage();
|
||||
}, 0);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("ZenThemeModifier: Error initializing browser layout", e);
|
||||
}
|
||||
@@ -119,7 +118,9 @@ class ZenStartup {
|
||||
document.documentElement.removeAttribute("zen-before-loaded");
|
||||
return;
|
||||
}
|
||||
for (let elem of document.querySelectorAll("#browser > *, #urlbar")) {
|
||||
for (let elem of document.querySelectorAll(
|
||||
`#browser > *:not(${this.#watermarkIgnoreElements.map(id => "#" + id).join(", ")}), #urlbar`
|
||||
)) {
|
||||
elem.style.opacity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,11 @@ export default function checkForZenUpdates() {
|
||||
),
|
||||
l10nId: "zen-sidebar-notification-updated",
|
||||
special: true,
|
||||
icon: "chrome://browser/skin/zen-icons/sparkles.svg",
|
||||
},
|
||||
{
|
||||
url: "https://www.zen-browser.app/donate",
|
||||
l10nId: "zen-sidebar-notification-donate",
|
||||
icon: "chrome://browser/skin/zen-icons/heart-circle-fill.svg",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -27,7 +27,7 @@ body,
|
||||
}
|
||||
}
|
||||
|
||||
:root[zen-before-loaded="true"] #browser > *:not(#zen-toast-container),
|
||||
:root[zen-before-loaded="true"] #browser > *:not(#zen-toast-container, #zen-browser-background),
|
||||
:root[zen-before-loaded="true"] #urlbar {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
border-radius: var(--border-radius-medium);
|
||||
outline: var(--zen-sidebar-notification-outline);
|
||||
outline-offset: -1.5px;
|
||||
box-shadow: var(--zen-sidebar-notification-shadow);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -262,6 +262,7 @@
|
||||
light-dark(white, black)
|
||||
);
|
||||
--zen-sidebar-notification-shadow: 0 0 6px light-dark(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.3));
|
||||
--zen-sidebar-notification-outline: 1px solid light-dark(transparent, rgba(255, 255, 255, 0.15));
|
||||
|
||||
--zen-sidebar-themed-icon-fill: light-dark(color-mix(in srgb, var(--zen-primary-color) 50%, black), color-mix(in srgb, var(--zen-colors-primary) 15%, #ebebeb));
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
246
src/zen/compact-mode/ZenMouseTracker.cpp
Normal file
246
src/zen/compact-mode/ZenMouseTracker.cpp
Normal 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
|
||||
92
src/zen/compact-mode/ZenMouseTracker.h
Normal file
92
src/zen/compact-mode/ZenMouseTracker.h
Normal 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
|
||||
65
src/zen/compact-mode/ZenMouseTrackerCocoa.mm
Normal file
65
src/zen/compact-mode/ZenMouseTrackerCocoa.mm
Normal 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
|
||||
48
src/zen/compact-mode/ZenMouseTrackerInternal.h
Normal file
48
src/zen/compact-mode/ZenMouseTrackerInternal.h
Normal 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
|
||||
51
src/zen/compact-mode/ZenMouseTrackerWin.cpp
Normal file
51
src/zen/compact-mode/ZenMouseTrackerWin.cpp
Normal 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
|
||||
14
src/zen/compact-mode/components.conf
Normal file
14
src/zen/compact-mode/components.conf
Normal 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,
|
||||
},
|
||||
]
|
||||
37
src/zen/compact-mode/moz.build
Normal file
37
src/zen/compact-mode/moz.build
Normal 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"
|
||||
41
src/zen/compact-mode/nsIZenMouseTracker.idl
Normal file
41
src/zen/compact-mode/nsIZenMouseTracker.idl
Normal 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);
|
||||
};
|
||||
@@ -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,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
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,6 +253,13 @@ export class nsGithubLiveFolderProvider extends nsZenLiveFolderProvider {
|
||||
"sort:updated-desc",
|
||||
];
|
||||
|
||||
if (
|
||||
this.state.type === "pull-requests" &&
|
||||
!(this.state.options.includeDrafts ?? true)
|
||||
) {
|
||||
baseQuery.push("draft:false");
|
||||
}
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: "author:@me",
|
||||
@@ -336,6 +343,12 @@ export class nsGithubLiveFolderProvider extends nsZenLiveFolderProvider {
|
||||
checked: this.state.options.reviewRequested ?? false,
|
||||
hidden: this.state.type === "issues",
|
||||
},
|
||||
{
|
||||
l10nId: "zen-live-folder-github-option-include-drafts",
|
||||
key: "includeDrafts",
|
||||
checked: this.state.options.includeDrafts ?? true,
|
||||
hidden: this.state.type !== "pull-requests",
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
l10nId: "zen-live-folder-github-option-repo-filter",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,8 @@
|
||||
#zen-media-controls-toolbar {
|
||||
--progress-height: 4px;
|
||||
--button-spacing: 2px;
|
||||
--zen-media-stack-peek: 8px;
|
||||
--zen-media-stack-gap: 6px;
|
||||
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
@@ -18,50 +20,23 @@
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#zen-media-buttons-hbox {
|
||||
.zen-media-buttons-hbox {
|
||||
align-items: start;
|
||||
margin-top: -4px;
|
||||
--toolbarbutton-padding-outer: 2px;
|
||||
}
|
||||
|
||||
&:not([media-sharing]) {
|
||||
#media-device-buttons {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#media-device-buttons {
|
||||
.media-device-buttons {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
&[media-sharing] :is(#zen-media-playback-buttons, #zen-media-mute-button) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:not([can-pip]) {
|
||||
#zen-media-info-vbox {
|
||||
width: calc(100% - 26px);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#zen-media-pip-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#zen-media-prev-button,
|
||||
#zen-media-play-pause-button,
|
||||
#zen-media-next-button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
image.toolbarbutton-icon {
|
||||
padding: 5px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
#zen-media-progress-bar {
|
||||
.zen-media-progress-bar {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: var(--progress-height);
|
||||
@@ -109,56 +84,27 @@
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
& #zen-media-info-vbox label[overflow] {
|
||||
& .zen-media-info-vbox label[overflow] {
|
||||
animation: zen-back-and-forth-text 10s infinite ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
& #zen-media-focus-button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: url("chrome://browser/content/zen-images/note-indicator.svg") no-repeat;
|
||||
top: -70%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.8s ease;
|
||||
opacity: 1;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
&:is(:not(.playing:not([muted])), :hover) #zen-media-focus-button::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#zen-media-focus-button {
|
||||
align-self: center;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
position: relative;
|
||||
|
||||
& image {
|
||||
&:-moz-broken {
|
||||
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3C/svg%3E") !important;
|
||||
background: color-mix(in srgb, var(--zen-primary-color) 70%, transparent 30%);
|
||||
}
|
||||
& .zen-media-card {
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
& > toolbaritem {
|
||||
flex-grow: 1;
|
||||
transition: padding 0.3s ease-out;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
padding: 4px 6px;
|
||||
border-radius: var(--border-radius-medium);
|
||||
box-shadow: var(--zen-sidebar-notification-shadow);
|
||||
background-color: var(--zen-sidebar-notification-bg);
|
||||
width: 100%;
|
||||
& .zen-media-focus-button::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .zen-media-card::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: calc(var(--zen-media-stack-gap) + var(--zen-media-stack-peek));
|
||||
}
|
||||
}
|
||||
|
||||
.show-on-hover {
|
||||
@@ -174,8 +120,8 @@
|
||||
padding 0.2s ease;
|
||||
}
|
||||
|
||||
#zen-media-current-time,
|
||||
#zen-media-duration {
|
||||
.zen-media-current-time,
|
||||
.zen-media-duration {
|
||||
margin: 0 0 0 1px;
|
||||
font-size: x-small;
|
||||
opacity: 0.7;
|
||||
@@ -184,6 +130,105 @@
|
||||
}
|
||||
}
|
||||
|
||||
.zen-media-card {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
order: var(--zen-media-card-index, 0);
|
||||
width: 100%;
|
||||
padding: 4px 6px;
|
||||
border-radius: var(--border-radius-medium);
|
||||
outline: var(--zen-sidebar-notification-outline);
|
||||
outline-offset: -1.5px;
|
||||
box-shadow: var(--zen-sidebar-notification-shadow);
|
||||
background-color: var(--zen-sidebar-notification-bg);
|
||||
transform-origin: top center;
|
||||
transform: translateY(
|
||||
calc(
|
||||
var(--zen-media-card-index, 0) * (100% + var(--zen-media-stack-gap)) -
|
||||
var(--zen-media-stack-peek) * var(--zen-media-card-peek-level, 0)
|
||||
)
|
||||
)
|
||||
scale(calc(1 - 0.04 * var(--zen-media-card-peek-level, 0)));
|
||||
opacity: calc(1 - 0.4 * var(--zen-media-card-peek-level, 0));
|
||||
transition:
|
||||
transform 0.1s ease-out,
|
||||
opacity 0.2s ease-out,
|
||||
padding 0.3s ease-out;
|
||||
|
||||
&[stack-overflow] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:not([media-sharing]) .media-device-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&[media-sharing] :is(.zen-media-playback-buttons, .zen-media-mute-button) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:not([can-pip]) {
|
||||
.zen-media-info-vbox {
|
||||
width: calc(100% - 26px);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.zen-media-pip-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&[can-pip] {
|
||||
.zen-media-info-vbox {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.zen-media-pip-button {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
& .zen-media-focus-button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: url("chrome://browser/content/zen-images/note-indicator.svg") no-repeat;
|
||||
top: -70%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.8s ease;
|
||||
opacity: 1;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
&:not(.playing:not([muted])) .zen-media-focus-button::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Background cards never show the floating notes — only their top sliver
|
||||
is visible and the notes would poke through it */
|
||||
&[stacked-behind] .zen-media-focus-button::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.zen-media-focus-button {
|
||||
align-self: center;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
position: relative;
|
||||
|
||||
& image {
|
||||
&:-moz-broken {
|
||||
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3C/svg%3E") !important;
|
||||
background: color-mix(in srgb, var(--zen-primary-color) 70%, transparent 30%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#zen-media-controls-toolbar {
|
||||
display: none;
|
||||
animation: none;
|
||||
@@ -191,19 +236,29 @@
|
||||
|
||||
&:not([hidden]) {
|
||||
display: flex;
|
||||
height: 2.5rem;
|
||||
flex-direction: column-reverse;
|
||||
gap: var(--zen-media-stack-gap);
|
||||
/* The toolbar keeps its collapsed height even while expanded: the
|
||||
cards' flow positions overflow above the box, so the expanded stack
|
||||
floats over the sidebar instead of pushing it around. The collapsed
|
||||
height includes the room for the peeking stacked cards. */
|
||||
height: calc(
|
||||
var(--zen-media-collapsed-height, 2.5rem) + var(--zen-media-stack-peek) *
|
||||
var(--zen-media-stack-behind, 0)
|
||||
);
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
transition: height 0.2s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
#zen-media-title,
|
||||
#zen-media-artist {
|
||||
.zen-media-title,
|
||||
.zen-media-artist {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
#zen-media-artist {
|
||||
.zen-media-artist {
|
||||
opacity: 0.7;
|
||||
font-size: smaller;
|
||||
|
||||
@@ -212,19 +267,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
#zen-media-title {
|
||||
.zen-media-title {
|
||||
height: var(--size-item-small);
|
||||
font-size: math;
|
||||
}
|
||||
|
||||
#zen-media-main-vbox,
|
||||
#zen-media-info-vbox,
|
||||
#zen-media-progress-hbox {
|
||||
.zen-media-main-vbox,
|
||||
.zen-media-info-vbox,
|
||||
.zen-media-progress-hbox {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#zen-media-info-vbox {
|
||||
#zen-media-controls-toolbar:not([media-position-hidden]) & {
|
||||
.zen-media-info-vbox {
|
||||
.zen-media-card:not([media-position-hidden]) & {
|
||||
transition-delay: 0.01s !important;
|
||||
}
|
||||
overflow-x: hidden;
|
||||
@@ -254,23 +309,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
#zen-media-main-vbox {
|
||||
.zen-media-main-vbox {
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#zen-media-progress-hbox {
|
||||
.zen-media-progress-hbox {
|
||||
flex-grow: 1;
|
||||
height: var(--size-item-large);
|
||||
align-items: center;
|
||||
padding-top: 0 !important;
|
||||
|
||||
#zen-media-controls-toolbar[media-position-hidden] & {
|
||||
.zen-media-card[media-position-hidden] & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#zen-media-controls-hbox {
|
||||
.zen-media-controls-hbox {
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
max-width: 100%;
|
||||
@@ -279,33 +334,23 @@
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
#zen-media-playback-buttons {
|
||||
.zen-media-playback-buttons {
|
||||
justify-content: space-between;
|
||||
max-width: 10em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Hide #zen-media-focus-button if it doesn't fit in the toolbar */
|
||||
/* Hide .zen-media-focus-button if it doesn't fit in the toolbar */
|
||||
@container media-controls (max-width: 165px) {
|
||||
#zen-media-focus-button {
|
||||
.zen-media-focus-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#zen-media-info-container {
|
||||
.zen-media-info-container {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
#zen-media-controls-toolbar[can-pip] {
|
||||
#zen-media-info-vbox {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
#zen-media-pip-button {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
:root:not([zen-sidebar-expanded="true"]) {
|
||||
#zen-media-controls-toolbar {
|
||||
display: none;
|
||||
|
||||
@@ -8,48 +8,41 @@
|
||||
mode="icons"
|
||||
fullscreentoolbar="true"
|
||||
hidden="true">
|
||||
<toolbaritem>
|
||||
<vbox id="zen-media-main-vbox">
|
||||
<vbox>
|
||||
<hbox id="zen-media-info-container" class="show-on-hover">
|
||||
<vbox id="zen-media-info-vbox">
|
||||
<label id="zen-media-title" fadein="true"/>
|
||||
<label id="zen-media-artist" fadein="true"/>
|
||||
</vbox>
|
||||
<hbox id="zen-media-buttons-hbox">
|
||||
<toolbarbutton id="zen-media-pip-button"
|
||||
class="toolbarbutton-1" />
|
||||
<toolbarbutton id="zen-media-close-button"
|
||||
class="toolbarbutton-1" />
|
||||
<html:template id="zen-media-card-template">
|
||||
<toolbaritem class="zen-media-card">
|
||||
<vbox class="zen-media-main-vbox">
|
||||
<vbox>
|
||||
<hbox class="zen-media-info-container show-on-hover">
|
||||
<vbox class="zen-media-info-vbox">
|
||||
<label class="zen-media-title" fadein="true"/>
|
||||
<label class="zen-media-artist" fadein="true"/>
|
||||
</vbox>
|
||||
<hbox class="zen-media-buttons-hbox">
|
||||
<toolbarbutton class="zen-media-pip-button toolbarbutton-1" />
|
||||
<toolbarbutton class="zen-media-close-button toolbarbutton-1" />
|
||||
</hbox>
|
||||
</hbox>
|
||||
<hbox class="zen-media-progress-hbox show-on-hover">
|
||||
<label class="zen-media-current-time">0:00</label>
|
||||
<html:input type="range" class="zen-media-progress-bar"
|
||||
value="0" min="0" max="100" step="0.1"/>
|
||||
<label class="zen-media-duration">0:00</label>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<hbox class="zen-media-controls-hbox">
|
||||
<toolbarbutton class="zen-media-focus-button toolbarbutton-1" />
|
||||
<hbox class="zen-media-playback-buttons">
|
||||
<toolbarbutton class="zen-media-previoustrack-button toolbarbutton-1" />
|
||||
<toolbarbutton class="zen-media-playpause-button toolbarbutton-1" />
|
||||
<toolbarbutton class="zen-media-nexttrack-button toolbarbutton-1" />
|
||||
</hbox>
|
||||
<toolbarbutton class="zen-media-mute-button toolbarbutton-1" />
|
||||
<hbox class="media-device-buttons">
|
||||
<toolbarbutton class="zen-media-mute-mic-button toolbarbutton-1" />
|
||||
<toolbarbutton class="zen-media-mute-camera-button toolbarbutton-1" />
|
||||
</hbox>
|
||||
</hbox>
|
||||
<hbox id="zen-media-progress-hbox" class="show-on-hover">
|
||||
<label id="zen-media-current-time">0:00</label>
|
||||
<html:input type="range" id="zen-media-progress-bar"
|
||||
value="0" min="0" max="100" step="0.1"/>
|
||||
<label id="zen-media-duration">0:00</label>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<hbox id="zen-media-controls-hbox">
|
||||
<toolbarbutton id="zen-media-focus-button"
|
||||
class="toolbarbutton-1" />
|
||||
<hbox id="zen-media-playback-buttons">
|
||||
<toolbarbutton id="zen-media-previoustrack-button"
|
||||
class="toolbarbutton-1" />
|
||||
<toolbarbutton id="zen-media-playpause-button"
|
||||
class="toolbarbutton-1" />
|
||||
<toolbarbutton id="zen-media-nexttrack-button"
|
||||
class="toolbarbutton-1" />
|
||||
</hbox>
|
||||
<toolbarbutton id="zen-media-mute-button"
|
||||
class="toolbarbutton-1" />
|
||||
<hbox id="media-device-buttons">
|
||||
<toolbarbutton id="zen-media-mute-mic-button"
|
||||
class="toolbarbutton-1" />
|
||||
<toolbarbutton id="zen-media-mute-camera-button"
|
||||
class="toolbarbutton-1" />
|
||||
</hbox>
|
||||
</hbox>
|
||||
</vbox>
|
||||
</toolbaritem>
|
||||
</toolbaritem>
|
||||
</html:template>
|
||||
</toolbar>
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -14,6 +14,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
||||
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
|
||||
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
|
||||
TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
|
||||
gWindowSyncEnabled: "resource:///modules/zen/ZenWindowSync.sys.mjs",
|
||||
gSyncOnlyPinnedTabs: "resource:///modules/zen/ZenWindowSync.sys.mjs",
|
||||
DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
|
||||
@@ -914,6 +915,79 @@ export class nsZenSessionManager {
|
||||
aWindow.__isNewZenWindow = true;
|
||||
SessionStoreInternal._deferredInitialState = newState;
|
||||
SessionStoreInternal.initializeWindow(aWindow, newState);
|
||||
this.#refreshRestoredTabsState(aWindow, SessionStoreInternal).catch(e =>
|
||||
console.error("ZenSessionManager: Failed to refresh restored tabs", e)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The state we clone into a new window comes from the parent process
|
||||
* cache, which may lag behind the content processes (e.g. for a tab that
|
||||
* was opened or navigated right before opening the new window). After the
|
||||
* new window has been restored, flush the previous window and update each
|
||||
* counterpart tab that hasn't started loading yet.
|
||||
*
|
||||
* @param {Window} aWindow
|
||||
* The newly restored window.
|
||||
* @param {object} SessionStoreInternal
|
||||
* The SessionStore module instance.
|
||||
*/
|
||||
async #refreshRestoredTabsState(aWindow, SessionStoreInternal) {
|
||||
// Wait until the tabs have actually been created in the new window.
|
||||
await aWindow.gZenWorkspaces.promiseInitialized;
|
||||
if (aWindow.closed) {
|
||||
return;
|
||||
}
|
||||
// Only refresh from the most recently used window: any other window
|
||||
// already reported its state when it lost focus, so the cloned data
|
||||
// is up to date for it.
|
||||
let previousWindow = null;
|
||||
for (const win of SessionStoreInternal._browserWindows) {
|
||||
if (win !== aWindow && !win.closed) {
|
||||
previousWindow = win;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!previousWindow) {
|
||||
return;
|
||||
}
|
||||
for (const tab of previousWindow.gZenWorkspaces.allStoredTabs) {
|
||||
const syncId = tab.getAttribute("id");
|
||||
// A tab that has never loaded has no state in the content process,
|
||||
// so there's nothing newer to flush out of it.
|
||||
if (!syncId || tab.closing || !tab.linkedPanel) {
|
||||
continue;
|
||||
}
|
||||
const targetTab = aWindow.document.getElementById(syncId);
|
||||
// Only refresh a tab that hasn't started loading: setting the state
|
||||
// of a loaded tab would reload it.
|
||||
if (
|
||||
!aWindow.gBrowser.isTab(targetTab) ||
|
||||
targetTab.linkedPanel ||
|
||||
targetTab.closing
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
lazy.TabStateFlusher.flush(tab.linkedBrowser)
|
||||
.then(() => {
|
||||
// Things may have changed while the flush was in flight.
|
||||
if (
|
||||
aWindow.closed ||
|
||||
tab.closing ||
|
||||
targetTab.linkedPanel ||
|
||||
targetTab.closing
|
||||
) {
|
||||
return;
|
||||
}
|
||||
lazy.SessionStore.setTabState(
|
||||
targetTab,
|
||||
lazy.SessionStore.getTabState(tab)
|
||||
);
|
||||
})
|
||||
.catch(e =>
|
||||
console.error("ZenSessionManager: Failed to refresh tab state", e)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1488,9 +1488,16 @@ class nsZenWindowSync {
|
||||
const window = tab.documentGlobal;
|
||||
this.#runOnAllWindows(window, win => {
|
||||
const targetTab = this.getItemFromWindow(win, tab.id);
|
||||
if (targetTab) {
|
||||
win.gBrowser.removeTab(targetTab, { animate: true });
|
||||
if (!targetTab) {
|
||||
return;
|
||||
}
|
||||
if (targetTab.splitView) {
|
||||
win.gZenViewSplitter.removeTabFromGroup(targetTab, undefined, {
|
||||
forUnsplit: true,
|
||||
changeTab: false,
|
||||
});
|
||||
}
|
||||
win.gBrowser.removeTab(targetTab, { animate: true });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -779,6 +779,7 @@ class nsZenWorkspaces {
|
||||
|
||||
restoreWorkspacesFromSessionStore(aWinData = {}) {
|
||||
if (this.#hasInitialized || !this.workspaceEnabled) {
|
||||
this._resolveInitialized?.();
|
||||
return Promise.resolve();
|
||||
}
|
||||
const spacesFromStore = aWinData.spaces || [];
|
||||
|
||||
@@ -76,6 +76,10 @@ add_task(async function test_fetch_items_url_construction() {
|
||||
!query.includes("review-requested:@me"),
|
||||
"Should NOT include review-requested"
|
||||
);
|
||||
Assert.ok(
|
||||
!query.includes("draft:false"),
|
||||
"Should include draft pull requests by default"
|
||||
);
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
@@ -88,6 +92,7 @@ add_task(async function test_fetch_items_url_complex_options() {
|
||||
let instance = getGithubProviderForTest(sandbox, {
|
||||
authorMe: true,
|
||||
assignedMe: true,
|
||||
includeDrafts: false,
|
||||
reviewRequested: true,
|
||||
});
|
||||
|
||||
@@ -107,6 +112,10 @@ add_task(async function test_fetch_items_url_complex_options() {
|
||||
query.includes("review-requested:@me"),
|
||||
"Should include review-requested"
|
||||
);
|
||||
Assert.ok(
|
||||
query.includes("draft:false"),
|
||||
"Should exclude draft pull requests"
|
||||
);
|
||||
|
||||
Assert.ok(query.includes(" OR "), "Should contain OR operators");
|
||||
sandbox.restore();
|
||||
|
||||
@@ -14,3 +14,5 @@ support-files = [
|
||||
["browser_media_next_track.js"]
|
||||
|
||||
["browser_media_shows_on_tab_switch.js"]
|
||||
|
||||
["browser_media_stack.js"]
|
||||
|
||||
@@ -3,17 +3,6 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
// User flow:
|
||||
// 1. A page (think Spotify, YouTube Music) plays media and publishes
|
||||
// title/artist via navigator.mediaSession.metadata.
|
||||
// 2. User switches off that tab, media bar appears.
|
||||
// 3. The title and artist labels in the bar show what the page published.
|
||||
// 4. The page then updates the metadata mid-playback (next song starts).
|
||||
// 5. The bar updates live, without the user having to switch tabs again.
|
||||
//
|
||||
// This is what makes the bar feel connected to the playing page instead of
|
||||
// a generic "something is playing" indicator.
|
||||
|
||||
add_task(async function test_media_bar_shows_metadata_from_page() {
|
||||
const originalTab = gBrowser.selectedTab;
|
||||
const mediaTab = await addMediaTab();
|
||||
@@ -28,8 +17,9 @@ add_task(async function test_media_bar_shows_metadata_from_page() {
|
||||
await BrowserTestUtils.switchTab(gBrowser, originalTab);
|
||||
await waitForMediaBarVisible();
|
||||
|
||||
const titleEl = document.getElementById("zen-media-title");
|
||||
const artistEl = document.getElementById("zen-media-artist");
|
||||
const card = frontMediaCard().element;
|
||||
const titleEl = card.querySelector(".zen-media-title");
|
||||
const artistEl = card.querySelector(".zen-media-artist");
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => titleEl.textContent === "Sandstorm",
|
||||
|
||||
@@ -3,18 +3,6 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
// User flow:
|
||||
// 1. User plays a video, switches tabs, media bar appears.
|
||||
// 2. User clicks the mute button on the Zen media bar.
|
||||
// 3. The underlying tab actually goes silent (browser.audioMuted flips).
|
||||
// 4. The media bar reflects that with the `muted` attribute so the icon
|
||||
// changes.
|
||||
// 5. Clicking again unmutes.
|
||||
//
|
||||
// If this breaks, the user sees a mute button that looks toggled but the
|
||||
// audio keeps playing — or worse, the tab is muted but the button still
|
||||
// says "unmuted".
|
||||
|
||||
add_task(async function test_mute_from_media_bar() {
|
||||
const originalTab = gBrowser.selectedTab;
|
||||
const mediaTab = await addMediaTab();
|
||||
@@ -29,9 +17,10 @@ add_task(async function test_mute_from_media_bar() {
|
||||
!mediaTab.linkedBrowser.audioMuted,
|
||||
"precondition: playing tab starts unmuted"
|
||||
);
|
||||
const card = frontMediaCard().element;
|
||||
ok(
|
||||
!mediaBar().hasAttribute("muted"),
|
||||
"precondition: media bar has no muted attribute"
|
||||
!card.hasAttribute("muted"),
|
||||
"precondition: media card has no muted attribute"
|
||||
);
|
||||
|
||||
clickMediaButton("zen-media-mute-button");
|
||||
@@ -40,8 +29,8 @@ add_task(async function test_mute_from_media_bar() {
|
||||
"tab becomes muted after clicking the media bar mute button"
|
||||
);
|
||||
ok(
|
||||
mediaBar().hasAttribute("muted"),
|
||||
"media bar reflects the muted state in its attribute"
|
||||
card.hasAttribute("muted"),
|
||||
"media card reflects the muted state in its attribute"
|
||||
);
|
||||
|
||||
clickMediaButton("zen-media-mute-button");
|
||||
@@ -49,10 +38,7 @@ add_task(async function test_mute_from_media_bar() {
|
||||
() => !mediaTab.linkedBrowser.audioMuted,
|
||||
"clicking again unmutes the tab"
|
||||
);
|
||||
ok(
|
||||
!mediaBar().hasAttribute("muted"),
|
||||
"media bar drops the muted attribute"
|
||||
);
|
||||
ok(!card.hasAttribute("muted"), "media card drops the muted attribute");
|
||||
} finally {
|
||||
if (mediaTab.linkedBrowser.audioMuted) {
|
||||
mediaTab.toggleMuteAudio();
|
||||
|
||||
@@ -3,19 +3,6 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
// User flow:
|
||||
// 1. A music page registers a "nexttrack" action handler (like most
|
||||
// streaming sites do).
|
||||
// 2. User is on another tab, media bar is showing with the next-track
|
||||
// button enabled.
|
||||
// 3. User clicks next-track.
|
||||
// 4. The action fires inside the page — the page is responsible for
|
||||
// loading the next song. Zen's job here is to relay the click.
|
||||
//
|
||||
// Also guards the button-enablement logic: if the page does NOT register a
|
||||
// handler, the next-track button must be disabled. Otherwise clicks go
|
||||
// nowhere and users think the bar is broken.
|
||||
|
||||
add_task(async function test_next_track_relays_to_page() {
|
||||
const originalTab = gBrowser.selectedTab;
|
||||
const mediaTab = await addMediaTab();
|
||||
@@ -28,7 +15,9 @@ add_task(async function test_next_track_relays_to_page() {
|
||||
await BrowserTestUtils.switchTab(gBrowser, originalTab);
|
||||
await waitForMediaBarVisible();
|
||||
|
||||
const nextButton = document.getElementById("zen-media-nexttrack-button");
|
||||
const nextButton = frontMediaCard().element.querySelector(
|
||||
".zen-media-nexttrack-button"
|
||||
);
|
||||
|
||||
// supportedkeyschange propagates asynchronously; wait for the bar's
|
||||
// next-track button to become enabled before clicking.
|
||||
@@ -60,7 +49,9 @@ add_task(async function test_next_track_button_disabled_without_handler() {
|
||||
await BrowserTestUtils.switchTab(gBrowser, originalTab);
|
||||
await waitForMediaBarVisible();
|
||||
|
||||
const nextButton = document.getElementById("zen-media-nexttrack-button");
|
||||
const nextButton = frontMediaCard().element.querySelector(
|
||||
".zen-media-nexttrack-button"
|
||||
);
|
||||
Assert.equal(
|
||||
nextButton.disabled,
|
||||
true,
|
||||
|
||||
@@ -3,26 +3,8 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
// User flow:
|
||||
// 1. User opens a page with audio and hits play.
|
||||
// 2. User switches to a different tab.
|
||||
// 3. The Zen media control bar should appear (so the user can still
|
||||
// pause/skip without going back to the noisy tab).
|
||||
// 4. User switches back to the audio tab.
|
||||
// 5. The media bar should hide again — it's redundant next to the real
|
||||
// page controls.
|
||||
//
|
||||
// This covers the real contract users see: the DOMAudioPlaybackStarted →
|
||||
// TabSelect → showMediaControls chain in nsZenMediaController, plus the
|
||||
// inverse path on selecting the playing tab. A regression anywhere in that
|
||||
// chain (event wiring, the 500ms tab-switch debounce, the hidden attribute
|
||||
// flip) surfaces as a bar that either never shows or never hides.
|
||||
|
||||
// note: We keep setting timeouts because media player takes a bit to
|
||||
// get removed (after the animation, more specifically)
|
||||
|
||||
add_task(async function test_media_bar_shows_when_switching_off_playing_tab() {
|
||||
gZenMediaController.onControllerClose();
|
||||
gZenMediaController.closeAllCards();
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => !isMediaBarVisible(),
|
||||
"media bar hides again once the playing tab regains focus"
|
||||
@@ -55,9 +37,9 @@ add_task(async function test_media_bar_shows_when_switching_off_playing_tab() {
|
||||
);
|
||||
|
||||
Assert.equal(
|
||||
gZenMediaController._currentBrowser?.browserId,
|
||||
frontMediaCard()?.browser.browserId,
|
||||
mediaTab.linkedBrowser.browserId,
|
||||
"media controller is bound to the media tab's browser, not the selected tab"
|
||||
"media card is bound to the media tab's browser, not the selected tab"
|
||||
);
|
||||
|
||||
await BrowserTestUtils.switchTab(gBrowser, mediaTab);
|
||||
|
||||
61
src/zen/tests/media/browser_media_stack.js
Normal file
61
src/zen/tests/media/browser_media_stack.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
https://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
add_task(async function test_media_stack_shows_multiple_cards() {
|
||||
const originalTab = gBrowser.selectedTab;
|
||||
const tabA = await addMediaTab();
|
||||
const tabB = await addMediaTab();
|
||||
|
||||
try {
|
||||
await BrowserTestUtils.switchTab(gBrowser, tabA);
|
||||
await setMediaSessionMetadata(tabA, { title: "Song A", artist: "A" });
|
||||
await playVideoIn(tabA);
|
||||
|
||||
await BrowserTestUtils.switchTab(gBrowser, tabB);
|
||||
await setMediaSessionMetadata(tabB, { title: "Song B", artist: "B" });
|
||||
await playVideoIn(tabB);
|
||||
|
||||
await BrowserTestUtils.switchTab(gBrowser, originalTab);
|
||||
await waitForMediaBarVisible();
|
||||
|
||||
// Cards reappear on the 500ms tab-switch debounce; wait for both.
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => visibleMediaCards().length === 2,
|
||||
"both playing tabs get their own card in the stack"
|
||||
);
|
||||
|
||||
Assert.equal(
|
||||
frontMediaCard().browser.browserId,
|
||||
tabB.linkedBrowser.browserId,
|
||||
"front card belongs to the most recently started media"
|
||||
);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(() => {
|
||||
const titles = visibleMediaCards().map(
|
||||
card => card.querySelector(".zen-media-title").textContent
|
||||
);
|
||||
return titles.includes("Song A") && titles.includes("Song B");
|
||||
}, "each card shows its own session's metadata");
|
||||
|
||||
// Closing the front card only removes that session.
|
||||
clickMediaButton("zen-media-close-button");
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => visibleMediaCards().length === 1,
|
||||
"closing the front card only removes that card"
|
||||
);
|
||||
ok(isMediaBarVisible(), "media bar stays visible with the remaining card");
|
||||
Assert.equal(
|
||||
frontMediaCard().browser.browserId,
|
||||
tabA.linkedBrowser.browserId,
|
||||
"remaining card takes the front slot"
|
||||
);
|
||||
} finally {
|
||||
await pauseVideoIn(tabA);
|
||||
await pauseVideoIn(tabB);
|
||||
BrowserTestUtils.removeTab(tabA);
|
||||
BrowserTestUtils.removeTab(tabB);
|
||||
gBrowser.selectedTab = originalTab;
|
||||
}
|
||||
});
|
||||
@@ -63,12 +63,21 @@ async function waitForMediaBarVisible() {
|
||||
);
|
||||
}
|
||||
|
||||
// Click a toolbarbutton on the media bar. We dispatch a "command" event
|
||||
// directly because that's what the controller listens for and it sidesteps
|
||||
// the flakiness of synthesizing a mouse click on a small toolbar button.
|
||||
function clickMediaButton(id) {
|
||||
const button = document.getElementById(id);
|
||||
ok(button, `media bar button ${id} exists`);
|
||||
function frontMediaCard() {
|
||||
return gZenMediaController.frontCard;
|
||||
}
|
||||
|
||||
function visibleMediaCards() {
|
||||
return [...mediaBar().querySelectorAll(".zen-media-card")].filter(
|
||||
card => !card.hidden
|
||||
);
|
||||
}
|
||||
|
||||
function clickMediaButton(className) {
|
||||
const card = frontMediaCard();
|
||||
ok(card, "front media card exists");
|
||||
const button = card.element.querySelector(`.${className}`);
|
||||
ok(button, `media bar button ${className} exists`);
|
||||
button.dispatchEvent(new Event("command", { bubbles: true }));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
321
src/zen/window-drag/actors/ZenWindowDragChild.sys.mjs
Normal file
321
src/zen/window-drag/actors/ZenWindowDragChild.sys.mjs
Normal file
@@ -0,0 +1,321 @@
|
||||
// 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 = 4;
|
||||
|
||||
// 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.explicitOriginalTarget;
|
||||
if (target?.nodeType === Node.TEXT_NODE) {
|
||||
// The hit-test landed on rendered text; a drag here should select
|
||||
// it instead, unless it isn't selectable.
|
||||
if (this.#isSelectableText(target)) {
|
||||
return true;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
#isSelectableText(node) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
13
src/zen/window-drag/components.conf
Normal file
13
src/zen/window-drag/components.conf
Normal 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'],
|
||||
},
|
||||
]
|
||||
27
src/zen/window-drag/moz.build
Normal file
27
src/zen/window-drag/moz.build
Normal 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"
|
||||
23
src/zen/window-drag/nsIZenWindowDragUtils.idl
Normal file
23
src/zen/window-drag/nsIZenWindowDragUtils.idl
Normal 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);
|
||||
};
|
||||
33
src/zen/window-drag/nsZenWindowDragUtils.cpp
Normal file
33
src/zen/window-drag/nsZenWindowDragUtils.cpp
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/. */
|
||||
|
||||
#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
|
||||
27
src/zen/window-drag/nsZenWindowDragUtils.h
Normal file
27
src/zen/window-drag/nsZenWindowDragUtils.h
Normal 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
|
||||
@@ -5,8 +5,8 @@
|
||||
"binaryName": "zen",
|
||||
"version": {
|
||||
"product": "firefox",
|
||||
"version": "153.0.1",
|
||||
"candidate": "153.0.1",
|
||||
"version": "153.0.3",
|
||||
"candidate": "153.0.3",
|
||||
"candidateBuild": 1
|
||||
},
|
||||
"buildOptions": {
|
||||
@@ -20,7 +20,7 @@
|
||||
"brandShortName": "Zen",
|
||||
"brandFullName": "Zen Browser",
|
||||
"release": {
|
||||
"displayVersion": "1.21.10b",
|
||||
"displayVersion": "1.21.11b",
|
||||
"github": {
|
||||
"repo": "zen-browser/desktop"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user