Compare commits

..

16 Commits

Author SHA1 Message Date
mr. m
73ed322695 gh-14843: Prevent quick audio tracks from creating a new controller (gh-14871) 2026-08-07 02:01:57 +02:00
Ashvin Jangid
e7a3e1ee29 gh-14862: Fix tab switching when switching spaces with mouse wheel (gh-14863)
Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2026-08-06 20:29:00 +02:00
Ashvin Jangid
d6c86f570a gh-13761: Fix closing split tab in one window changing focus of other windows (gh-14855)
Co-authored-by: mr. m <mr.m@tuta.com>
2026-08-06 02:04:58 +02:00
mr. m
0e5f2dcd11 gh-14843: Fix media players height looking off if they differ from each other (gh-14853) 2026-08-05 16:19:13 +02:00
mr. m
c0ade4d063 gh-14841: Bump version (gh-14852) 2026-08-05 15:11:41 +02:00
Lokendra singh
11acda2db3 gh-14483: Fix boost font override breaking certain icons (gh-14819) 2026-08-05 11:00:46 +02:00
dpkass
77aa6ff6c9 gh-14847: Add draft filtering to GitHub Live Folders (gh-14839) 2026-08-05 02:52:14 +02:00
mr. m
871bdde4cc gh-14841: Sync upstream Firefox to version 153.0.3 (gh-14842) 2026-08-05 02:44:01 +02:00
mr. m
b3bd688ca4 gh-14843: Allow multiple media controllers (gh-14844) 2026-08-04 19:55:53 +02:00
mr. m
36c71c5c2b gh-14830: Fixed dragging on text makes the entire window to move (gh-14833) 2026-08-04 01:32:36 +02:00
mr. m
c74486102f gh-13108: Use mozilla API for gdkGetPointer (gh-14823) 2026-08-03 03:53:25 +02:00
mr. m
045ebdf0ae gh-13108: Start using std::atomic instead of mozilla::Atomic (gh-14820) 2026-08-02 21:14:14 +02:00
mr. m
bc540320f1 gh-14703: Allow top section of sites be draggable (gh-14816) 2026-08-02 20:28:06 +02:00
mr. m
9714c15c7a gh-13108: Make compact mode track mouse moves outside of window (gh-14818) 2026-08-02 20:24:58 +02:00
Ashvin Jangid
ccbd934482 gh-14805: Fix blank labels in keyboard shortcuts page (gh-14807)
Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2026-07-31 23:37:23 +02:00
Connor Griffin
1d05184a2a gh-9918: Fixes issue setting keybinds with shift + digit or symbol (gh-14357) 2026-07-31 23:23:55 +02:00
60 changed files with 2687 additions and 1026 deletions

View File

@@ -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

View File

@@ -1 +1 @@
30959a45eaaf0bc499f3054793c76c6d7f0c8bc2
8a0a534248f80d8c53a8997f24683521c3a50ebc

View File

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

View File

@@ -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

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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)

View File

@@ -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 =

View File

@@ -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");
}

View File

@@ -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`;

View File

@@ -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;
}
}

View File

@@ -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",
},
{

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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.1));
--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));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -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;

View File

@@ -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>

View File

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

View File

@@ -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)
);
}
}
/**

View File

@@ -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 });
});
}

View File

@@ -649,6 +649,7 @@ class nsZenWorkspaces {
if (Math.abs(delta) < scrollThreshold) {
return;
}
event.preventDefault();
// Determine scroll direction
let rawDirection = delta > 0 ? 1 : -1;
@@ -779,6 +780,7 @@ class nsZenWorkspaces {
restoreWorkspacesFromSessionStore(aWinData = {}) {
if (this.#hasInitialized || !this.workspaceEnabled) {
this._resolveInitialized?.();
return Promise.resolve();
}
const spacesFromStore = aWinData.spaces || [];

View File

@@ -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();

View File

@@ -14,3 +14,5 @@ support-files = [
["browser_media_next_track.js"]
["browser_media_shows_on_tab_switch.js"]
["browser_media_stack.js"]

View File

@@ -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",

View File

@@ -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();

View File

@@ -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,

View File

@@ -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);

View 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;
}
});

View File

@@ -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 }));
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,288 @@
// 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",
]);
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;
}
}
// The effective cursor, resolved the same way it is shown to the user.
return lazy.zenWindowDragUtils.isInteractiveCursor(
event.composedTarget,
event.clientX,
event.clientY
);
}
#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());
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,34 @@
/* 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);
/**
* @brief Whether the effective cursor for the given node signals
* interactive or draggable content (pointer, text, move, resize, ...).
* Unlike computed style, this resolves cursor: auto the same way the
* cursor actually shown to the user is chosen.
* @param node The deepest node hit by the event.
* @param clientX Viewport X coordinate of the event, in CSS pixels.
* @param clientY Viewport Y coordinate of the event, in CSS pixels.
*/
boolean isInteractiveCursor(in Node node, in float clientX, in float clientY);
};

View File

@@ -0,0 +1,90 @@
/* 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 "Units.h"
#include "mozilla/PresShell.h"
#include "mozilla/ServoStyleConsts.h"
#include "mozilla/dom/Element.h"
#include "nsContentUtils.h"
#include "nsIContent.h"
#include "nsIFrame.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;
}
NS_IMETHODIMP
nsZenWindowDragUtils::IsInteractiveCursor(nsINode* aNode, float aClientX,
float aClientY, bool* aResult) {
using mozilla::StyleCursorKind;
*aResult = false;
NS_ENSURE_ARG_POINTER(aNode);
nsIContent* content = nsIContent::FromNode(aNode);
nsIFrame* frame = content ? content->GetPrimaryFrame() : nullptr;
if (!frame) {
return NS_OK;
}
nsIFrame* rootFrame = frame->PresShell()->GetRootFrame();
if (!rootFrame) {
return NS_OK;
}
nsPoint point(mozilla::CSSPixel::ToAppUnits(aClientX),
mozilla::CSSPixel::ToAppUnits(aClientY));
point -= frame->GetOffsetTo(rootFrame);
switch (frame->GetCursor(point).mCursor) {
case StyleCursorKind::Pointer:
case StyleCursorKind::Cell:
case StyleCursorKind::Crosshair:
case StyleCursorKind::Text:
case StyleCursorKind::VerticalText:
case StyleCursorKind::Move:
case StyleCursorKind::Grab:
case StyleCursorKind::Grabbing:
case StyleCursorKind::EResize:
case StyleCursorKind::NResize:
case StyleCursorKind::NeResize:
case StyleCursorKind::NwResize:
case StyleCursorKind::SResize:
case StyleCursorKind::SeResize:
case StyleCursorKind::SwResize:
case StyleCursorKind::WResize:
case StyleCursorKind::EwResize:
case StyleCursorKind::NsResize:
case StyleCursorKind::NeswResize:
case StyleCursorKind::NwseResize:
case StyleCursorKind::ColResize:
case StyleCursorKind::RowResize:
case StyleCursorKind::AllScroll:
*aResult = true;
break;
default:
break;
}
return NS_OK;
}
} // namespace zen

View File

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

View File

@@ -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.12b",
"github": {
"repo": "zen-browser/desktop"
},