From cccbcf662e49b11296a1096f6e9f3a271de4bffd Mon Sep 17 00:00:00 2001 From: "mr. m" Date: Tue, 28 Apr 2026 00:38:09 +0200 Subject: [PATCH] no-bug: Start working on little zen --- prefs/zen/kbs.yaml | 3 + src/browser/base/content/zen-assets.inc.xhtml | 2 + .../base/content/zen-assets.jar.inc.mn | 2 +- .../base/content/zen-commands.inc.xhtml | 2 + .../components/preferences/zen-settings.js | 13 +- .../urlbar/content/UrlbarInput-mjs.patch | 74 +- .../BrowserWindowTracker-sys-mjs.patch | 21 +- .../modules/URILoadingHelper-sys-mjs.patch | 16 +- src/zen/ZenComponents.manifest | 2 + src/zen/common/ZenPreloadedScripts.js | 1 - src/zen/common/modules/ZenStartup.mjs | 1 - src/zen/common/modules/ZenUIManager.mjs | 7 +- src/zen/common/styles/zen-theme.css | 2 +- src/zen/common/zen-sets.js | 7 + src/zen/common/zenThemeModifier.js | 8 + src/zen/compact-mode/ZenCompactMode.mjs | 3 + src/zen/kbs/KbsComponents.manifest | 6 + ...tcuts.mjs => ZenKeyboardShortcuts.sys.mjs} | 640 +++++++++++------- .../global-shortcuts/ZenGlobalShortcuts.cpp | 134 ++++ .../kbs/global-shortcuts/ZenGlobalShortcuts.h | 61 ++ .../ZenGlobalShortcutsStub.cpp | 27 + .../cocoa/ZenGlobalShortcutsCocoa.mm | 210 ++++++ src/zen/kbs/global-shortcuts/cocoa/moz.build | 18 + src/zen/kbs/global-shortcuts/components.conf | 14 + src/zen/kbs/global-shortcuts/moz.build | 29 + .../nsIZenGlobalShortcuts.idl | 49 ++ .../windows/ZenGlobalShortcutsWindows.cpp | 166 +++++ .../windows/moz.build} | 10 +- src/zen/kbs/moz.build | 11 + .../LittleWindowComponents.manifest | 6 + src/zen/little-window/ZenLittleWindow.sys.mjs | 110 +++ src/zen/little-window/jar.inc.mn | 5 + src/zen/little-window/moz.build | 7 + src/zen/little-window/zen-little-window.css | 52 ++ src/zen/moz.build | 2 + src/zen/sessionstore/ZenWindowSync.sys.mjs | 7 + 36 files changed, 1425 insertions(+), 303 deletions(-) create mode 100644 src/zen/kbs/KbsComponents.manifest rename src/zen/kbs/{ZenKeyboardShortcuts.mjs => ZenKeyboardShortcuts.sys.mjs} (74%) create mode 100644 src/zen/kbs/global-shortcuts/ZenGlobalShortcuts.cpp create mode 100644 src/zen/kbs/global-shortcuts/ZenGlobalShortcuts.h create mode 100644 src/zen/kbs/global-shortcuts/ZenGlobalShortcutsStub.cpp create mode 100644 src/zen/kbs/global-shortcuts/cocoa/ZenGlobalShortcutsCocoa.mm create mode 100644 src/zen/kbs/global-shortcuts/cocoa/moz.build create mode 100644 src/zen/kbs/global-shortcuts/components.conf create mode 100644 src/zen/kbs/global-shortcuts/moz.build create mode 100644 src/zen/kbs/global-shortcuts/nsIZenGlobalShortcuts.idl create mode 100644 src/zen/kbs/global-shortcuts/windows/ZenGlobalShortcutsWindows.cpp rename src/zen/kbs/{jar.inc.mn => global-shortcuts/windows/moz.build} (62%) create mode 100644 src/zen/kbs/moz.build create mode 100644 src/zen/little-window/LittleWindowComponents.manifest create mode 100644 src/zen/little-window/ZenLittleWindow.sys.mjs create mode 100644 src/zen/little-window/jar.inc.mn create mode 100644 src/zen/little-window/moz.build create mode 100644 src/zen/little-window/zen-little-window.css diff --git a/prefs/zen/kbs.yaml b/prefs/zen/kbs.yaml index 18a53e190..d997a8982 100644 --- a/prefs/zen/kbs.yaml +++ b/prefs/zen/kbs.yaml @@ -7,3 +7,6 @@ - name: zen.keyboard.shortcuts.disable-mainkeyset-clear value: false # for debugging + +- name: zen.keyboard.shortcuts.global.enabled + value: true diff --git a/src/browser/base/content/zen-assets.inc.xhtml b/src/browser/base/content/zen-assets.inc.xhtml index f67426009..52239865c 100644 --- a/src/browser/base/content/zen-assets.inc.xhtml +++ b/src/browser/base/content/zen-assets.inc.xhtml @@ -28,6 +28,8 @@ + + # Startup "preloaded" scripts that requre globals such as gBrowser and gURLBar diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn index 9d470ace8..202ac8146 100644 --- a/src/browser/base/content/zen-assets.jar.inc.mn +++ b/src/browser/base/content/zen-assets.jar.inc.mn @@ -9,7 +9,6 @@ #include ../../../zen/mods/jar.inc.mn #include ../../../zen/spaces/jar.inc.mn #include ../../../zen/tabs/jar.inc.mn -#include ../../../zen/kbs/jar.inc.mn #include ../../../zen/glance/jar.inc.mn #include ../../../zen/folders/jar.inc.mn #include ../../../zen/welcome/jar.inc.mn @@ -19,3 +18,4 @@ #include ../../../zen/vendor/jar.inc.mn #include ../../../zen/fonts/jar.inc.mn #include ../../../zen/live-folders/jar.inc.mn +#include ../../../zen/little-window/jar.inc.mn diff --git a/src/browser/base/content/zen-commands.inc.xhtml b/src/browser/base/content/zen-commands.inc.xhtml index 4f9e0fca9..092a10a76 100644 --- a/src/browser/base/content/zen-commands.inc.xhtml +++ b/src/browser/base/content/zen-commands.inc.xhtml @@ -68,4 +68,6 @@ + + diff --git a/src/browser/components/preferences/zen-settings.js b/src/browser/components/preferences/zen-settings.js index 16ddcbc63..5c44045e4 100644 --- a/src/browser/components/preferences/zen-settings.js +++ b/src/browser/components/preferences/zen-settings.js @@ -8,13 +8,16 @@ const { nsZenMultiWindowFeature } = ChromeUtils.importESModule( { global: "current" } ); -const { nsKeyShortcutModifiers } = ChromeUtils.importESModule( - "chrome://browser/content/zen-components/ZenKeyboardShortcuts.mjs", - { - global: "current", - } +const { + nsKeyShortcutModifiers, + ZenKeyboardShortcuts, + VALID_SHORTCUT_GROUPS, +} = ChromeUtils.importESModule( + "resource:///modules/zen/ZenKeyboardShortcuts.sys.mjs" ); +const gZenKeyboardShortcutsManager = ZenKeyboardShortcuts.manager; + var gZenMarketplaceManager = { async init() { const checkForUpdates = document.getElementById("zenThemeMarketplaceCheckForUpdates"); diff --git a/src/browser/components/urlbar/content/UrlbarInput-mjs.patch b/src/browser/components/urlbar/content/UrlbarInput-mjs.patch index 6498c19ec..a71b52637 100644 --- a/src/browser/components/urlbar/content/UrlbarInput-mjs.patch +++ b/src/browser/components/urlbar/content/UrlbarInput-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/urlbar/content/UrlbarInput.mjs b/browser/components/urlbar/content/UrlbarInput.mjs -index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f5c811a82 100644 +index b23244f9d3278918b016bb3fcab19687bc2e292a..0bcb63f7abaf406077b52469eb913fcb75ba13f8 100644 --- a/browser/components/urlbar/content/UrlbarInput.mjs +++ b/browser/components/urlbar/content/UrlbarInput.mjs @@ -90,6 +90,13 @@ const lazy = XPCOMUtils.declareLazy({ @@ -75,7 +75,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f } if (isCanonized) { -@@ -2696,6 +2728,42 @@ export class UrlbarInput extends HTMLElement { +@@ -2696,6 +2728,45 @@ export class UrlbarInput extends HTMLElement { await this.#updateLayoutBreakoutDimensions(); } @@ -84,7 +84,10 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f + } + + get zenUrlbarBehavior() { -+ if (this.document.documentElement.hasAttribute("inDOMFullscreen")) { ++ if ( ++ this.document.documentElement.hasAttribute("inDOMFullscreen") || ++ this.document.documentElement.hasAttribute("zen-little-window") ++ ) { + return "float"; + } + return lazy.ZEN_URLBAR_BEHAVIOR; @@ -118,7 +121,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f startLayoutExtend() { if (!this.#allowBreakout || this.hasAttribute("breakout-extend")) { // Do not expand if the Urlbar does not support being expanded or it is -@@ -2710,6 +2778,13 @@ export class UrlbarInput extends HTMLElement { +@@ -2710,6 +2781,13 @@ export class UrlbarInput extends HTMLElement { this.setAttribute("breakout-extend", "true"); @@ -132,7 +135,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f // Enable the animation only after the first extend call to ensure it // doesn't run when opening a new window. if (!this.hasAttribute("breakout-extend-animate")) { -@@ -2729,6 +2804,27 @@ export class UrlbarInput extends HTMLElement { +@@ -2729,6 +2807,27 @@ export class UrlbarInput extends HTMLElement { return; } @@ -160,7 +163,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f this.removeAttribute("breakout-extend"); this.#updateTextboxPosition(); } -@@ -2759,7 +2855,7 @@ export class UrlbarInput extends HTMLElement { +@@ -2759,7 +2858,7 @@ export class UrlbarInput extends HTMLElement { forceUnifiedSearchButtonAvailable = false ) { let prevState = this.getAttribute("pageproxystate"); @@ -169,7 +172,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f this.setAttribute("pageproxystate", state); this._inputContainer.setAttribute("pageproxystate", state); this._identityBox?.setAttribute("pageproxystate", state); -@@ -3031,10 +3127,12 @@ export class UrlbarInput extends HTMLElement { +@@ -3031,10 +3130,12 @@ export class UrlbarInput extends HTMLElement { return; } this.style.top = px( @@ -182,7 +185,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f ); } -@@ -3093,9 +3191,10 @@ export class UrlbarInput extends HTMLElement { +@@ -3093,9 +3194,10 @@ export class UrlbarInput extends HTMLElement { return; } @@ -194,7 +197,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f ); this.style.setProperty( "--urlbar-height", -@@ -3597,6 +3696,7 @@ export class UrlbarInput extends HTMLElement { +@@ -3597,6 +3699,7 @@ export class UrlbarInput extends HTMLElement { } _toggleActionOverride(event) { @@ -202,7 +205,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f if ( event.keyCode == KeyEvent.DOM_VK_SHIFT || event.keyCode == KeyEvent.DOM_VK_ALT || -@@ -3709,8 +3809,8 @@ export class UrlbarInput extends HTMLElement { +@@ -3709,8 +3812,8 @@ export class UrlbarInput extends HTMLElement { if (!this.#isAddressbar) { return val; } @@ -213,7 +216,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f : val; // Only trim value if the directionality doesn't change to RTL and we're not // showing a strikeout https protocol. -@@ -4006,6 +4106,7 @@ export class UrlbarInput extends HTMLElement { +@@ -4006,6 +4109,7 @@ export class UrlbarInput extends HTMLElement { resultDetails = null, browser = this.window.gBrowser.selectedBrowser ) { @@ -221,7 +224,19 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f if (this.#isAddressbar) { this.#prepareAddressbarLoad( url, -@@ -4117,6 +4218,10 @@ export class UrlbarInput extends HTMLElement { +@@ -4088,6 +4192,11 @@ export class UrlbarInput extends HTMLElement { + * @returns {"current" | "tabshifted" | "tab" | "save" | "window"} + */ + _whereToOpen(event) { ++ if (this.document.documentElement.hasAttribute("zen-little-window")) { ++ // Little windows are single-tab popups -- never spawn extra tabs ++ // or new windows from the urlbar. ++ return "current"; ++ } + let isKeyboardEvent = KeyboardEvent.isInstance(event); + let reuseEmpty = isKeyboardEvent; + let where = undefined; +@@ -4117,6 +4226,10 @@ export class UrlbarInput extends HTMLElement { } reuseEmpty = true; } @@ -232,7 +247,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f if ( where == "tab" && reuseEmpty && -@@ -4124,6 +4229,9 @@ export class UrlbarInput extends HTMLElement { +@@ -4124,6 +4237,9 @@ export class UrlbarInput extends HTMLElement { ) { where = "current"; } @@ -242,7 +257,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f return where; } -@@ -4378,6 +4486,7 @@ export class UrlbarInput extends HTMLElement { +@@ -4378,6 +4494,7 @@ export class UrlbarInput extends HTMLElement { this.setResultForCurrentValue(null); this.handleCommand(); this.controller.clearLastQueryContextCache(); @@ -250,7 +265,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f this._suppressStartQuery = false; }); -@@ -4385,7 +4494,6 @@ export class UrlbarInput extends HTMLElement { +@@ -4385,7 +4502,6 @@ export class UrlbarInput extends HTMLElement { contextMenu.addEventListener("popupshowing", () => { // Close the results pane when the input field contextual menu is open, // because paste and go doesn't want a result selection. @@ -258,7 +273,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f let controller = this.document.commandDispatcher.getControllerForCommand("cmd_paste"); -@@ -4541,7 +4649,11 @@ export class UrlbarInput extends HTMLElement { +@@ -4541,7 +4657,11 @@ export class UrlbarInput extends HTMLElement { if (!engineName && !source && !this.hasAttribute("searchmode")) { return; } @@ -271,7 +286,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f if (this._searchModeIndicatorTitle) { this._searchModeIndicatorTitle.textContent = ""; this._searchModeIndicatorTitle.removeAttribute("data-l10n-id"); -@@ -4851,6 +4963,7 @@ export class UrlbarInput extends HTMLElement { +@@ -4851,6 +4971,7 @@ export class UrlbarInput extends HTMLElement { this.document.l10n.setAttributes( this.inputField, @@ -279,7 +294,20 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f l10nId, l10nId == "urlbar-placeholder-with-name" ? { name: engineName } -@@ -4964,6 +5077,11 @@ export class UrlbarInput extends HTMLElement { +@@ -4891,6 +5012,12 @@ export class UrlbarInput extends HTMLElement { + + _on_blur(event) { + lazy.logger.debug("Blur Event"); ++ if ( ++ this.document.commandDispatcher.focusedElement == this.inputField && ++ !lazy.UrlbarPrefs.get("closeOnWindowBlur") ++ ) { ++ return; ++ } + // We cannot count every blur events after a missed engagement as abandoment + // because the user may have clicked on some view element that executes + // a command causing a focus change. For example opening preferences from +@@ -4964,6 +5091,11 @@ export class UrlbarInput extends HTMLElement { } _on_click(event) { @@ -291,7 +319,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f switch (event.target) { case this.inputField: case this._inputContainer: -@@ -5042,7 +5160,7 @@ export class UrlbarInput extends HTMLElement { +@@ -5042,7 +5174,7 @@ export class UrlbarInput extends HTMLElement { } } @@ -300,7 +328,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f this.view.autoOpen({ event }); } else { if (this._untrimOnFocusAfterKeydown) { -@@ -5082,9 +5200,16 @@ export class UrlbarInput extends HTMLElement { +@@ -5082,9 +5214,16 @@ export class UrlbarInput extends HTMLElement { } _on_mousedown(event) { @@ -318,7 +346,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f if ( event.composedTarget != this.inputField && event.composedTarget != this._inputContainer -@@ -5094,6 +5219,10 @@ export class UrlbarInput extends HTMLElement { +@@ -5094,6 +5233,10 @@ export class UrlbarInput extends HTMLElement { this.focusedViaMousedown = !this.focused; this._preventClickSelectsAll = this.focused; @@ -329,7 +357,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f // Keep the focus status, since the attribute may be changed // upon calling this.focus(). -@@ -5129,7 +5258,7 @@ export class UrlbarInput extends HTMLElement { +@@ -5129,7 +5272,7 @@ export class UrlbarInput extends HTMLElement { } // 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. @@ -338,7 +366,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f break; } -@@ -5411,7 +5540,7 @@ export class UrlbarInput extends HTMLElement { +@@ -5411,7 +5554,7 @@ export class UrlbarInput extends HTMLElement { // When we are in actions search mode we can show more results so // increase the limit. let maxResults = diff --git a/src/browser/modules/BrowserWindowTracker-sys-mjs.patch b/src/browser/modules/BrowserWindowTracker-sys-mjs.patch index bd6f5cd91..2034e4e54 100644 --- a/src/browser/modules/BrowserWindowTracker-sys-mjs.patch +++ b/src/browser/modules/BrowserWindowTracker-sys-mjs.patch @@ -1,22 +1,37 @@ diff --git a/browser/modules/BrowserWindowTracker.sys.mjs b/browser/modules/BrowserWindowTracker.sys.mjs -index 9aecab66d8f23fac9f16cea2120a5fe903ae1122..692f2bfe3899a58925789503a6bb2a547cdbf7f3 100644 +index 9aecab66d8f23fac9f16cea2120a5fe903ae1122..e023c27bcb027d29ba9b3469eca5957d42040c46 100644 --- a/browser/modules/BrowserWindowTracker.sys.mjs +++ b/browser/modules/BrowserWindowTracker.sys.mjs -@@ -330,6 +330,7 @@ export const BrowserWindowTracker = { +@@ -210,7 +210,8 @@ export const BrowserWindowTracker = { + !win.closed && + (options.allowPopups || win.toolbar.visible) && + (options.allowTaskbarTabs || +- !win.document.documentElement.hasAttribute("taskbartab")) && ++ (!win.document.documentElement.hasAttribute("taskbartab") && ++ !win.document.documentElement.hasAttribute("zen-little-window"))) && + (!("private" in options) || + lazy.PrivateBrowsingUtils.permanentPrivateBrowsing || + lazy.PrivateBrowsingUtils.isWindowPrivate(win) == options.private) +@@ -330,6 +331,8 @@ export const BrowserWindowTracker = { args = null, remote = undefined, fission = undefined, + zenSyncedWindow = true, ++ zenLittleWindow = false, } = options; args = lazy.AIWindow.handleAIWindowOptions(options); -@@ -386,6 +387,12 @@ export const BrowserWindowTracker = { +@@ -386,6 +389,16 @@ export const BrowserWindowTracker = { windowFeatures, args ); + win._zenStartupSyncFlag = Services.prefs.getBoolPref("zen.window-sync.prefer-unsynced-windows") + ? (zenSyncedWindow ? 'unsynced' : 'synced') + : (zenSyncedWindow ? 'synced' : 'unsynced'); ++ if (zenLittleWindow) { ++ win._zenStartupLittleWindow = true; ++ win._zenStartupSyncFlag = 'unsynced'; ++ } + if (win._zenStartupSyncFlag === 'unsynced' && openerWindow) { + win._zenStartupUnsyncedUserContextId = openerWindow.gZenWorkspaces.getCurrentSpaceContainerId(); + } diff --git a/src/browser/modules/URILoadingHelper-sys-mjs.patch b/src/browser/modules/URILoadingHelper-sys-mjs.patch index 3ca4a6103..eda6ca7a8 100644 --- a/src/browser/modules/URILoadingHelper-sys-mjs.patch +++ b/src/browser/modules/URILoadingHelper-sys-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/modules/URILoadingHelper.sys.mjs b/browser/modules/URILoadingHelper.sys.mjs -index a005dbdf84609622ef8054f73f78c0c290e76125..d5bf6fb51c9af5e60f69a73612ee91598080730a 100644 +index a005dbdf84609622ef8054f73f78c0c290e76125..2d347ac12d53ae97b61750d421a489ce10af3376 100644 --- a/browser/modules/URILoadingHelper.sys.mjs +++ b/browser/modules/URILoadingHelper.sys.mjs @@ -224,6 +224,7 @@ function openInWindow(url, params, sourceWindow) { @@ -19,7 +19,17 @@ index a005dbdf84609622ef8054f73f78c0c290e76125..d5bf6fb51c9af5e60f69a73612ee9159 where = "tab"; targetBrowser = null; } else if ( -@@ -974,7 +975,7 @@ export const URILoadingHelper = { +@@ -724,7 +725,8 @@ export const URILoadingHelper = { + "navigator:browser" && + (!skipPopups || top.toolbar.visible) && + (!skipTaskbarTabs || +- !top.document.documentElement.hasAttribute("taskbartab")) && ++ (!top.document.documentElement.hasAttribute("taskbartab") && ++ !top.document.documentElement.hasAttribute("zen-little-window"))) && + (!forceNonPrivate || !PrivateBrowsingUtils.isWindowPrivate(top)) + ) { + return top; +@@ -974,7 +976,7 @@ export const URILoadingHelper = { ignoreQueryString || replaceQueryString, ignoreFragmentWhenComparing ); @@ -28,7 +38,7 @@ index a005dbdf84609622ef8054f73f78c0c290e76125..d5bf6fb51c9af5e60f69a73612ee9159 for (let i = 0; i < browsers.length; i++) { let browser = browsers[i]; let browserCompare = cleanURL( -@@ -1030,7 +1031,7 @@ export const URILoadingHelper = { +@@ -1030,7 +1032,7 @@ export const URILoadingHelper = { ); aSplitView.ownerGlobal.focus(); } else { diff --git a/src/zen/ZenComponents.manifest b/src/zen/ZenComponents.manifest index 5cfcfd855..2f6285e45 100644 --- a/src/zen/ZenComponents.manifest +++ b/src/zen/ZenComponents.manifest @@ -16,3 +16,5 @@ category app-startup nsBrowserGlue @mozilla.org/browser/browserglue;1 applicatio #include common/Components.manifest #include sessionstore/SessionComponents.manifest #include live-folders/LiveFoldersComponents.manifest +#include kbs/KbsComponents.manifest +#include little-window/LittleWindowComponents.manifest diff --git a/src/zen/common/ZenPreloadedScripts.js b/src/zen/common/ZenPreloadedScripts.js index 559d4c8e4..33ad173d7 100644 --- a/src/zen/common/ZenPreloadedScripts.js +++ b/src/zen/common/ZenPreloadedScripts.js @@ -13,7 +13,6 @@ "chrome://browser/content/zen-components/ZenCompactMode.mjs", "chrome://browser/content/ZenUIManager.mjs", "chrome://browser/content/zen-components/ZenMods.mjs", - "chrome://browser/content/zen-components/ZenKeyboardShortcuts.mjs", "chrome://browser/content/zen-components/ZenSessionStore.mjs", "chrome://browser/content/zen-components/ZenMediaController.mjs", "chrome://browser/content/zen-components/ZenGlanceManager.mjs", diff --git a/src/zen/common/modules/ZenStartup.mjs b/src/zen/common/modules/ZenStartup.mjs index dd37719f8..6b3de9ebe 100644 --- a/src/zen/common/modules/ZenStartup.mjs +++ b/src/zen/common/modules/ZenStartup.mjs @@ -32,7 +32,6 @@ class ZenStartup { return; } this.#hasInitializedLayout = true; - gZenKeyboardShortcutsManager.beforeInit(); try { const kNavbarItems = ["nav-bar", "PersonalToolbar"]; const kNewContainerId = "zen-appcontent-navbar-container"; diff --git a/src/zen/common/modules/ZenUIManager.mjs b/src/zen/common/modules/ZenUIManager.mjs index 5cd2ba2e8..cb79b917a 100644 --- a/src/zen/common/modules/ZenUIManager.mjs +++ b/src/zen/common/modules/ZenUIManager.mjs @@ -942,7 +942,9 @@ window.gZenVerticalTabsManager = { ?.includes("toolbar") || document.documentElement .getAttribute("chromehidden") - ?.includes("menubar") + ?.includes("menubar") || + document.documentElement.hasAttribute("zen-little-window") || + window._zenStartupLittleWindow ); }); @@ -1261,7 +1263,8 @@ window.gZenVerticalTabsManager = { const topButtons = document.getElementById("zen-sidebar-top-buttons"); const isCompactMode = - gZenCompactModeManager.preference && !forCustomizableMode; + (gZenCompactModeManager.preference && !forCustomizableMode) || + this.hidesTabsToolbar; const isVerticalTabs = this._prefsVerticalTabs || forCustomizableMode; const isSidebarExpanded = this._prefsSidebarExpanded || !isVerticalTabs; const isRightSide = this._prefsRightSide && isVerticalTabs; diff --git a/src/zen/common/styles/zen-theme.css b/src/zen/common/styles/zen-theme.css index 56d40d3cd..05ef5b3dc 100644 --- a/src/zen/common/styles/zen-theme.css +++ b/src/zen/common/styles/zen-theme.css @@ -293,7 +293,7 @@ } #main-window[windowtype="navigator:browser"]:not([chromehidden~='toolbar']) { - min-height: 495px !important; + min-height: var(--zen-minimum-window-height, 495px) !important; @media (-moz-windows-mica) or (-moz-platform: macos) or ((-moz-platform: linux) and -moz-pref('zen.widget.linux.transparency')) { diff --git a/src/zen/common/zen-sets.js b/src/zen/common/zen-sets.js index 1a1acb728..c68edeb95 100644 --- a/src/zen/common/zen-sets.js +++ b/src/zen/common/zen-sets.js @@ -136,6 +136,13 @@ document.addEventListener( case "cmd_zenNewNavigatorUnsynced": OpenBrowserWindow({ zenSyncedWindow: false }); break; + case "cmd_zenNewLittleWindow": { + const { ZenLittleWindow } = ChromeUtils.importESModule( + "resource:///modules/zen/ZenLittleWindow.sys.mjs" + ); + ZenLittleWindow.openLittleWindow(window); + break; + } case "cmd_zenNewLiveFolder": { const { ZenLiveFoldersManager } = ChromeUtils.importESModule( "resource:///modules/zen/ZenLiveFoldersManager.sys.mjs" diff --git a/src/zen/common/zenThemeModifier.js b/src/zen/common/zenThemeModifier.js index 31449efaa..fdb9473a9 100644 --- a/src/zen/common/zenThemeModifier.js +++ b/src/zen/common/zenThemeModifier.js @@ -158,6 +158,14 @@ ) { separation = 0; } + // Little windows are visually a single floating bar; we never want + // chrome padding around them. + if ( + document.documentElement.hasAttribute("zen-little-window") || + window._zenStartupLittleWindow + ) { + separation = 0; + } // In order to still use it on fullscreen, even if it's 0px, add .1px (almost invisible) separation = Math.max(kMinElementSeparation, separation); document.documentElement.style.setProperty( diff --git a/src/zen/compact-mode/ZenCompactMode.mjs b/src/zen/compact-mode/ZenCompactMode.mjs index a4b046bf2..376b65154 100644 --- a/src/zen/compact-mode/ZenCompactMode.mjs +++ b/src/zen/compact-mode/ZenCompactMode.mjs @@ -126,6 +126,9 @@ window.gZenCompactModeManager = { }, get shouldBeCompact() { + if (document.documentElement.hasAttribute("zen-little-window")) { + return false; + } return !document.documentElement .getAttribute("chromehidden") ?.includes("toolbar"); diff --git a/src/zen/kbs/KbsComponents.manifest b/src/zen/kbs/KbsComponents.manifest new file mode 100644 index 000000000..68c09ddaa --- /dev/null +++ b/src/zen/kbs/KbsComponents.manifest @@ -0,0 +1,6 @@ +# 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/. + +category browser-before-ui-startup resource:///modules/zen/ZenKeyboardShortcuts.sys.mjs ZenKeyboardShortcuts.init +category browser-quit-application-granted resource:///modules/zen/ZenKeyboardShortcuts.sys.mjs ZenKeyboardShortcuts.uninit diff --git a/src/zen/kbs/ZenKeyboardShortcuts.mjs b/src/zen/kbs/ZenKeyboardShortcuts.sys.mjs similarity index 74% rename from src/zen/kbs/ZenKeyboardShortcuts.mjs rename to src/zen/kbs/ZenKeyboardShortcuts.sys.mjs index 351bfd0a5..57a928402 100644 --- a/src/zen/kbs/ZenKeyboardShortcuts.mjs +++ b/src/zen/kbs/ZenKeyboardShortcuts.sys.mjs @@ -1,8 +1,26 @@ -// 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/. +/* 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 { nsZenMultiWindowFeature } from "chrome://browser/content/zen-components/ZenCommonUtils.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "ZenGlobalShortcuts", + "@mozilla.org/zen/global-shortcuts;1", + Ci.nsIZenGlobalShortcuts +); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "GLOBAL_SHORTCUTS_ENABLED", + "zen.keyboard.shortcuts.global.enabled", + true, + () => ZenKeyboardShortcuts.triggerShortcutRebuild() +); const KEYCODE_MAP = { F1: "VK_F1", @@ -124,14 +142,14 @@ const fixedL10nIds = { const ZEN_MAIN_KEYSET_ID = "mainKeyset"; const ZEN_DEVTOOLS_KEYSET_ID = "devtoolsKeyset"; -window.ZEN_KEYSET_ID = "zenKeyset"; +const ZEN_KEYSET_ID = "zenKeyset"; const ZEN_COMPACT_MODE_SHORTCUTS_GROUP = "zen-compact-mode"; const ZEN_WORKSPACE_SHORTCUTS_GROUP = "zen-workspace"; const ZEN_OTHER_SHORTCUTS_GROUP = "zen-other"; const ZEN_SPLIT_VIEW_SHORTCUTS_GROUP = "zen-split-view"; const FIREFOX_SHORTCUTS_GROUP = "zen-kbs-invalid"; -window.VALID_SHORTCUT_GROUPS = [ +export const VALID_SHORTCUT_GROUPS = [ ZEN_COMPACT_MODE_SHORTCUTS_GROUP, ZEN_WORKSPACE_SHORTCUTS_GROUP, ZEN_SPLIT_VIEW_SHORTCUTS_GROUP, @@ -230,7 +248,6 @@ export class nsKeyShortcutModifiers { if (!other) { return false; } - // If we are on macos, we can have accel and meta at the same time return ( this.#alt == other.#alt && this.#shift == other.#shift && @@ -238,8 +255,7 @@ export class nsKeyShortcutModifiers { (AppConstants.platform == "macosx" ? (this.#meta || this.#accel) == (other.#meta || other.#accel) && this.#control == other.#control - : // In other platforms, we can have control and accel counting as the same thing - this.#meta == other.#meta && + : this.#meta == other.#meta && (this.#control || this.#accel) == (other.#control || other.#accel)) ); } @@ -283,19 +299,15 @@ export class nsKeyShortcutModifiers { get control() { return this.#control; } - get alt() { return this.#alt; } - get shift() { return this.#shift; } - get meta() { return this.#meta; } - get accel() { return this.#accel; } @@ -312,6 +324,7 @@ class KeyShortcut { #disabled = false; #reserved = false; #internal = false; + #zenGlobal = false; constructor( id, @@ -323,13 +336,14 @@ class KeyShortcut { l10nId, disabled = false, reserved = false, - internal = false + internal = false, + zenGlobal = false ) { this.#id = id; this.#key = key?.toLowerCase(); this.#keycode = keycode; - if (!window.VALID_SHORTCUT_GROUPS.includes(group)) { + if (!VALID_SHORTCUT_GROUPS.includes(group)) { throw new Error("Illegal group value: " + group); } @@ -340,6 +354,7 @@ class KeyShortcut { this.#disabled = disabled; this.#reserved = reserved; this.#internal = internal; + this.#zenGlobal = zenGlobal; } isEmpty() { @@ -351,12 +366,10 @@ class KeyShortcut { for (let key of json) { rv.push(this.#parseFromJSON(key)); } - return rv; } static getGroupFromL10nId(l10nId, id) { - // Find inside defaultKeyboardGroups for (let group of Object.keys(defaultKeyboardGroups)) { for (let shortcut of defaultKeyboardGroups[group]) { if (shortcut == l10nId || shortcut == "id:" + id) { @@ -378,7 +391,8 @@ class KeyShortcut { json.l10nId, json.disabled, json.reserved, - json.internal + json.internal, + json.zenGlobal ); } @@ -399,7 +413,8 @@ class KeyShortcut { key.getAttribute("data-l10n-id"), key.getAttribute("disabled") == "true", key.getAttribute("reserved") == "true", - key.getAttribute("internal") == "true" + key.getAttribute("internal") == "true", + key.getAttribute("zenGlobal") == "true" ); } @@ -407,7 +422,6 @@ class KeyShortcut { if (!id || id.startsWith("zen-")) { return id; } - // Check if any action is on the list of fixed l10n ids if (fixedL10nIds[action]) { return fixedL10nIds[action]; } @@ -438,10 +452,6 @@ class KeyShortcut { } key.setAttribute("group", this.#group); - // note to "mr. macos": We add the `zen-` prefix because Firefox hasnt been built with the - // shortcuts in mind, it will simply just override the shortcuts with whatever the default is. - // note that this l10n id is not used for actually translating the key's label, but rather to - // identify the default keybinds. if (this.#l10nId) { // key.setAttribute('data-l10n-id', this.#l10nId); } @@ -458,6 +468,9 @@ class KeyShortcut { if (this.#internal) { key.setAttribute("internal", this.#internal); } + if (this.#zenGlobal) { + key.setAttribute("zenGlobal", this.#zenGlobal); + } key.setAttribute("zen-keybind", "true"); return key; @@ -466,67 +479,54 @@ class KeyShortcut { _modifyInternalAttribute(value) { this.#internal = value; } - getRealKeycode() { - if (this.#keycode === "") { - return null; - } - return this.#keycode; + return this.#keycode === "" ? null : this.#keycode; } - getID() { return this.#id; } - getAction() { return this.#action; } - - // Only used for migration! _setAction(action) { this.#action = action; } - + _setZenGlobal(value) { + this.#zenGlobal = !!value; + } getL10NID() { return this.#l10nId; } - getGroup() { return this.#group; } - getModifiers() { return this.#modifiers; } - getKeyName() { return this.#key?.toLowerCase(); } - getKeyCode() { return this.getRealKeycode(); } - getKeyNameOrCode() { return this.#key ? this.getKeyName() : this.getKeyCode(); } - isDisabled() { return this.#disabled; } - setDisabled(value) { this.#disabled = value; } - isReserved() { return this.#reserved; } - isInternal() { return this.#internal; } - + isZenGlobal() { + return this.#zenGlobal; + } isInvalid() { return this.#key == "" && this.#keycode == "" && this.#l10nId == null; } @@ -550,6 +550,7 @@ class KeyShortcut { disabled: this.#disabled, reserved: this.#reserved, internal: this.#internal, + zenGlobal: this.#zenGlobal, }; } @@ -559,7 +560,6 @@ class KeyShortcut { if (this.#key) { str += this.#key.toUpperCase(); } else if (this.#keycode) { - // Get the key from the value for (let [key, value] of Object.entries(KEYCODE_MAP)) { if (value == this.#keycode) { const normalizedKey = key.toLowerCase(); @@ -622,8 +622,7 @@ class KeyShortcut { return; } } - - this.#keycode = ""; // Clear the keycode + this.#keycode = ""; this.#key = shortcut; } } @@ -643,7 +642,6 @@ class nsZenKeyboardShortcutsLoader { try { return await IOUtils.readJSON(this.shortcutsFile); } catch (e) { - // Recreate shortcuts file Services.prefs.clearUserPref("zen.keyboard.shortcuts.version"); console.warn("Error loading shortcuts file", e); return null; @@ -658,12 +656,8 @@ class nsZenKeyboardShortcutsLoader { await IOUtils.remove(this.shortcutsFile); } - static zenGetDefaultShortcuts() { - // DO NOT CHANGE ANYTHING HERE - // For adding new default shortcuts, add them to inside the migration function - // and increment the version number. - - let keySet = document.getElementById(ZEN_MAIN_KEYSET_ID); + static zenGetDefaultShortcuts(aWindow) { + let keySet = aWindow.document.getElementById(ZEN_MAIN_KEYSET_ID); let newShortcutList = []; const correctDefaultShortcut = shortcut => { @@ -678,7 +672,6 @@ class nsZenKeyboardShortcutsLoader { } }; - // Firefox's standard keyset. Reverse order to keep the order of the keys for (let i = keySet.children.length - 1; i >= 0; i--) { let key = keySet.children[i]; let parsed = KeyShortcut.parseFromXHTML(key); @@ -686,7 +679,6 @@ class nsZenKeyboardShortcutsLoader { newShortcutList.push(parsed); } - // Compact mode's keyset newShortcutList.push( new KeyShortcut( "zen-compact-mode-toggle", @@ -710,7 +702,6 @@ class nsZenKeyboardShortcutsLoader { ) ); - // Workspace shortcuts for (let i = 10; i > 0; i--) { newShortcutList.push( new KeyShortcut( @@ -749,7 +740,6 @@ class nsZenKeyboardShortcutsLoader { ) ); - // Split view newShortcutList.push( new KeyShortcut( "zen-split-view-grid", @@ -808,8 +798,8 @@ class nsZenKeyboardShortcutsLoader { "javascriptTracingToggle", ]; - static zenGetDefaultDevToolsShortcuts() { - let keySet = document.getElementById(ZEN_DEVTOOLS_KEYSET_ID); + static zenGetDefaultDevToolsShortcuts(aWindow) { + let keySet = aWindow.document.getElementById(ZEN_DEVTOOLS_KEYSET_ID); let newShortcutList = []; for (let i = keySet.children.length - 1; i >= 0; i--) { let key = keySet.children[i]; @@ -817,7 +807,6 @@ class nsZenKeyboardShortcutsLoader { continue; } let parsed = KeyShortcut.parseFromXHTML(key, { group: "devTools" }); - // Move "inspector" shortcut to use "L" key instead of "I" if ( parsed.getID() == "key_inspector" || parsed.getID() == "key_inspectorMac" @@ -826,13 +815,12 @@ class nsZenKeyboardShortcutsLoader { } newShortcutList.push(parsed); } - return newShortcutList; } } class nsZenKeyboardShortcutsVersioner { - static LATEST_KBS_VERSION = 17; + static LATEST_KBS_VERSION = 19; constructor() {} @@ -845,9 +833,7 @@ class nsZenKeyboardShortcutsVersioner { } getVersionedData(data) { - return { - shortcuts: data, - }; + return { shortcuts: data }; } isVersionUpToDate() { @@ -858,9 +844,8 @@ class nsZenKeyboardShortcutsVersioner { return this.version < nsZenKeyboardShortcutsVersioner.LATEST_KBS_VERSION; } - migrateIfNeeded(data) { + migrateIfNeeded(data, aWindow, aManager) { if (!data) { - // Rebuid the shortcuts, just in case this.version = 0; } @@ -876,19 +861,20 @@ class nsZenKeyboardShortcutsVersioner { "to", nsZenKeyboardShortcutsVersioner.LATEST_KBS_VERSION ); - const newData = this.migrate(data, version); + const newData = this.migrate(data, version, aWindow, aManager); this.version = nsZenKeyboardShortcutsVersioner.LATEST_KBS_VERSION; return newData; } console.error("Unknown keyboard shortcuts version"); this.version = 0; - return this.migrateIfNeeded(data); + return this.migrateIfNeeded(data, aWindow, aManager); } - fillDefaultIfNotPresent(data) { - for (let shortcut of nsZenKeyboardShortcutsLoader.zenGetDefaultShortcuts()) { - // If it has an ID and we dont find it in the data, we add it + fillDefaultIfNotPresent(data, aWindow) { + for (let shortcut of nsZenKeyboardShortcutsLoader.zenGetDefaultShortcuts( + aWindow + )) { if (shortcut.getID() && !data.find(s => s.getID() == shortcut.getID())) { data.push(shortcut); } @@ -896,26 +882,20 @@ class nsZenKeyboardShortcutsVersioner { return data; } - fixedKeyboardShortcuts(data) { - // Apply migrations and ensure defaults exist - let out = this.fillDefaultIfNotPresent(this.migrateIfNeeded(data)); - + fixedKeyboardShortcuts(data, aWindow, aManager) { + let out = this.fillDefaultIfNotPresent( + this.migrateIfNeeded(data, aWindow, aManager), + aWindow + ); return out; } // eslint-disable-next-line complexity - migrate(data, version) { + migrate(data, version, aWindow, aManager) { if (version < 1) { - // Migrate from 0 to 1 - // Here, we do a complet reset of the shortcuts, - // since nothing seems to work properly. - data = nsZenKeyboardShortcutsLoader.zenGetDefaultShortcuts(); + data = nsZenKeyboardShortcutsLoader.zenGetDefaultShortcuts(aWindow); } if (version < 2) { - // Migrate from 1 to 2 - // In this new version, we are resolving the conflicts between - // shortcuts having keycode and key at the same time. - // If there's both, we remove the keycodes. for (let shortcut of data) { if (shortcut.getKeyCode() && shortcut.getKeyName()) { shortcut.setNewBinding(shortcut.getKeyName()); @@ -934,14 +914,8 @@ class nsZenKeyboardShortcutsVersioner { ); } if (version < 3) { - // Migrate from 2 to 3 - // In this new version, there was this *really* annoying bug. Shortcuts - // detection for internal keys was not working properly, so every internal - // shortcut was being saved as a user-editable shortcut. - // This migration will fix this issue. const defaultShortcuts = - nsZenKeyboardShortcutsLoader.zenGetDefaultShortcuts(); - // Get the default shortcut, compare the id and set the internal flag if needed + nsZenKeyboardShortcutsLoader.zenGetDefaultShortcuts(aWindow); for (let shortcut of data) { for (let defaultShortcut of defaultShortcuts) { if (shortcut.getID() == defaultShortcut.getID()) { @@ -951,14 +925,9 @@ class nsZenKeyboardShortcutsVersioner { } } if (version < 4) { - // Migrate from 3 to 4 - // In this new version, we are just removing the 'zen-toggle-sidebar' shortcut - // since it's not used anymore. data = data.filter(shortcut => shortcut.getID() != "zen-toggle-sidebar"); } if (version < 5) { - // Migrate from 4 to 5 - // Here, we are adding the 'zen-toggle-sidebar' shortcut back, but with a new action data.push( new KeyShortcut( "zen-toggle-sidebar", @@ -972,8 +941,6 @@ class nsZenKeyboardShortcutsVersioner { ); } if (version < 6) { - // Migrate from 5 to 6 - // In this new version, we add the "Copy URL" shortcut to the default shortcuts data.push( new KeyShortcut( "zen-copy-url", @@ -987,28 +954,16 @@ class nsZenKeyboardShortcutsVersioner { ); } if (version < 7) { - // Migrate from 6 to 7 - // In this new version, we add the devtools shortcuts - const listener = event => { - event.stopPropagation(); - + const listener = () => { const devToolsShortcuts = - nsZenKeyboardShortcutsLoader.zenGetDefaultDevToolsShortcuts(); - gZenKeyboardShortcutsManager.updatedDefaultDevtoolsShortcuts( - devToolsShortcuts - ); - - window.removeEventListener("zen-devtools-keyset-added", listener); + nsZenKeyboardShortcutsLoader.zenGetDefaultDevToolsShortcuts(aWindow); + aManager.updatedDefaultDevtoolsShortcuts(devToolsShortcuts); + aWindow.removeEventListener("zen-devtools-keyset-added", listener); }; - - // We need to load after an event because the devtools keyset is not in the DOM yet - // and we need to wait for it to be added. - gZenKeyboardShortcutsManager._hasToLoadDefaultDevtools = true; - window.addEventListener("zen-devtools-keyset-added", listener); + aManager._hasToLoadDefaultDevtools = true; + aWindow.addEventListener("zen-devtools-keyset-added", listener); } if (version < 8) { - // Migrate from 7 to 8 - // In this new version, we add the "Copy URL as Markdown" shortcut to the default shortcuts data.push( new KeyShortcut( "zen-copy-url-markdown", @@ -1026,17 +981,12 @@ class nsZenKeyboardShortcutsVersioner { ); } if (version < 9) { - // Migrate from version 8 to 9 - // Due to security concerns, replace "code:" actions with corresponding IDs - // we also remove 'zen-toggle-web-panel' since it's not used anymore data = data.filter( shortcut => shortcut.getID() != "zen-toggle-web-panel" ); for (let shortcut of data) { if (shortcut.getAction()?.startsWith("code:")) { const id = shortcut.getID(); - - // Map old shortcut IDs to new IDs const commandMap = { "zen-compact-mode-toggle": "cmd_zenCompactModeToggle", "zen-compact-mode-show-sidebar": "cmd_zenCompactModeShowSidebar", @@ -1051,14 +1001,10 @@ class nsZenKeyboardShortcutsVersioner { "zen-pinned-tab-reset-shortcut": "cmd_zenPinnedTabReset", "zen-toggle-sidebar": "cmd_zenToggleSidebar", }; - - // Dynamically handle workspace switch shortcuts (zen-workspace-switch-1 to 10) if (id?.startsWith("zen-workspace-switch-")) { const num = id.replace("zen-workspace-switch-", ""); commandMap[id] = `cmd_zenWorkspaceSwitch${num}`; } - - // Replace action if a corresponding command exists if (commandMap[id]) { shortcut._setAction(commandMap[id]); } @@ -1066,8 +1012,6 @@ class nsZenKeyboardShortcutsVersioner { } } if (version < 10) { - // Migrate from version 9 to 10 - // 1) Add the new pin/unpin tab toggle shortcut with Ctrl+Shift+D data.push( new KeyShortcut( "zen-toggle-pin-tab", @@ -1079,8 +1023,6 @@ class nsZenKeyboardShortcutsVersioner { "zen-toggle-pin-tab-shortcut" ) ); - - // 2) Add shortcut to expand Glance into a full tab: Default Accel+O data.push( new KeyShortcut( "zen-glance-expand", @@ -1093,9 +1035,7 @@ class nsZenKeyboardShortcutsVersioner { ) ); } - if (version < 11) { - // Migrate from version 10 to 11 data.push( new KeyShortcut( "zen-new-empty-split-view", @@ -1108,12 +1048,7 @@ class nsZenKeyboardShortcutsVersioner { ) ); } - if (version < 12) { - // Hard-remove deprecated or conflicting defaults regardless of version - // - Remove the built-in "Open File" keybinding; menu item remains available - // - Remove default "Bookmark All Tabs" keybinding (Ctrl+Shift+D) to avoid conflict - // - Remove "Stop" keybinding to avoid conflict with Firefox's built-in binding const shouldBeEmptyShortcuts = [ "openFileKb", "bookmarkAllTabsKb", @@ -1124,16 +1059,11 @@ class nsZenKeyboardShortcutsVersioner { shortcut.shouldBeEmpty = true; } } - - // Also remove zen-compact-mode-show-toolbar data = data.filter( shortcut => shortcut.getID() != "zen-compact-mode-show-toolbar" ); } - if (version < 13) { - // Migrate from version 12 to 13 - // Add shortcut to close all unpinned tabs: Default Accel+Shift+K data.push( new KeyShortcut( "zen-close-all-unpinned-tabs", @@ -1146,10 +1076,7 @@ class nsZenKeyboardShortcutsVersioner { ) ); } - if (version < 15) { - // Migrate from version 13 to 14 - // Add shortcut to open a new unsynced window: Default accelt+shift+N data.push( new KeyShortcut( "zen-new-unsynced-window", @@ -1161,8 +1088,6 @@ class nsZenKeyboardShortcutsVersioner { "zen-new-unsynced-window-shortcut" ) ); - // Also, change the default for new empty split from + to * on mac - // and disable the "Restore closed window" shortcut by default due to conflicts let emptySplitFound = false, undoCloseWindowFound = false; for (let shortcut of data) { @@ -1184,10 +1109,7 @@ class nsZenKeyboardShortcutsVersioner { } } } - if (version < 16) { - // Migrate from version 14 to 16. - // We move the action for "toggle compact mode" to "cmd_toggleCompactModeIgnoreHover" for (let shortcut of data) { if (shortcut.getID() == "zen-compact-mode-toggle") { shortcut._setAction("cmd_toggleCompactModeIgnoreHover"); @@ -1195,10 +1117,7 @@ class nsZenKeyboardShortcutsVersioner { } } } - if (version < 17) { - // Migrate from version 16 to 17. - // Add shortcut to Duplicate Tab data.push( new KeyShortcut( "zen-duplicate-tab", @@ -1211,54 +1130,93 @@ class nsZenKeyboardShortcutsVersioner { ) ); } - + if (version < 18) { + data.push( + new KeyShortcut( + "zen-new-little-window", + "N", + "", + ZEN_OTHER_SHORTCUTS_GROUP, + nsKeyShortcutModifiers.fromObject({ accel: true, alt: true }), + "cmd_zenNewLittleWindow", + "zen-new-little-window-shortcut", + /*disabled=*/ false, + /*reserved=*/ true, + /*internal=*/ false, + /*zenGlobal=*/ true + ) + ); + } + if (version < 19) { + for (let shortcut of data) { + if (shortcut.getID() == "zen-new-little-window") { + shortcut._setZenGlobal(true); + break; + } + } + } return data; } } -window.gZenKeyboardShortcutsManager = { +const KbsManager = { loader: new nsZenKeyboardShortcutsLoader(), _hasToLoadDevtools: false, _inlineCommands: [], + _initialized: false, + _initializingPromise: null, + _currentShortcutList: null, + versioner: null, - beforeInit() { - if (!this.inBrowserView) { - return; - } - // Create the main keyset before calling the async init function, - // This is because other browser-sets needs this element and the JS event - // handled wont wait for the async function to finish. - void this.getZenKeyset(); + beforeInit(aWindow) { + void this.getZenKeyset(aWindow); - this._hasCleared = Services.prefs.getBoolPref( + aWindow._zenKbsHasCleared = Services.prefs.getBoolPref( "zen.keyboard.shortcuts.disable-mainkeyset-clear", false ); - window.addEventListener( + const onDevtoolsKeysetAdded = () => this._hasAddedDevtoolShortcuts(); + aWindow.addEventListener( "zen-devtools-keyset-added", - this._hasAddedDevtoolShortcuts.bind(this) + onDevtoolsKeysetAdded ); + aWindow._zenKbsDevtoolsListener = onDevtoolsKeysetAdded; - this.init(); - }, - - async init() { - if (this.inBrowserView) { - const loadedShortcuts = await this._loadSaved(); - - this._currentShortcutList = - this.versioner.fixedKeyboardShortcuts(loadedShortcuts); - this._applyShortcuts(); - - await this._saveShortcuts(); - window.dispatchEvent( - new Event("ZenKeyboardShortcutsReady", { bubbles: true }) + if (!this._initialized && !this._initializingPromise) { + this._initializingPromise = this._init(aWindow).finally(() => { + this._initializingPromise = null; + }); + return; + } + if (this._initialized) { + // Subsequent windows just need their keyset populated. + this._applyShortcutsTo(aWindow); + this._applyZenGlobalListenersFor(aWindow); + aWindow.dispatchEvent( + new aWindow.Event("ZenKeyboardShortcutsReady", { bubbles: true }) ); } }, - get inBrowserView() { - return window.location.href == "chrome://browser/content/browser.xhtml"; + async _init(aWindow) { + const loadedShortcuts = await this._loadSaved(); + this._currentShortcutList = this.versioner.fixedKeyboardShortcuts( + loadedShortcuts, + aWindow, + this + ); + this._initialized = true; + this._applyShortcuts(); + await this._saveShortcuts(); + aWindow.dispatchEvent( + new aWindow.Event("ZenKeyboardShortcutsReady", { bubbles: true }) + ); + }, + + // Kept for back-compat with chrome callers; ZenStartup.mjs invokes it. + init() { + // Initialization is driven by `beforeInit(aWindow)` from + // browser-window-before-show. Nothing to do here. }, async _loadSaved() { @@ -1267,7 +1225,6 @@ window.gZenKeyboardShortcutsManager = { if (!data || !data.length) { return null; } - try { return KeyShortcut.parseFromSaved(data); } catch (e) { @@ -1275,16 +1232,6 @@ window.gZenKeyboardShortcutsManager = { "Zen CKS: Error parsing saved shortcuts. Resetting to defaults...", e ); - gNotificationBox.appendNotification( - "zen-shortcuts-corrupted", - { - label: { "l10n-id": "zen-shortcuts-corrupted" }, - image: - "chrome://browser/skin/notification-icons/persistent-storage-blocked.svg", - priority: gNotificationBox.PRIORITY_WARNING_HIGH, - }, - [] - ); return null; } }; @@ -1294,43 +1241,41 @@ window.gZenKeyboardShortcutsManager = { return loadedShortcuts; }, - getZenKeyset(browser = window) { - if (!browser.gZenKeyboardShortcutsManager._zenKeyset) { + getZenKeyset(browser) { + if (!browser._zenKeyset) { const existingKeyset = browser.document.getElementById(ZEN_KEYSET_ID); if (existingKeyset) { - browser.gZenKeyboardShortcutsManager._zenKeyset = existingKeyset; - return browser.gZenKeyboardShortcutsManager._zenKeyset; + browser._zenKeyset = existingKeyset; + return browser._zenKeyset; } - throw new Error("Zen keyset not found"); } - return browser.gZenKeyboardShortcutsManager._zenKeyset; + return browser._zenKeyset; }, - getZenDevtoolsKeyset() { - // note: we use `this` here because we are in the context of the browser - if (!this._zenDevtoolsKeyset) { + getZenDevtoolsKeyset(browser) { + if (!browser._zenDevtoolsKeyset) { const id = `zen-${ZEN_DEVTOOLS_KEYSET_ID}`; - const existingKeyset = document.getElementById(id); + const existingKeyset = browser.document.getElementById(id); if (existingKeyset) { - this._zenDevtoolsKeyset = existingKeyset; + browser._zenDevtoolsKeyset = existingKeyset; return existingKeyset; } - - this._zenDevtoolsKeyset = document.createXULElement("keyset"); - this._zenDevtoolsKeyset.id = id; - - const mainKeyset = document.getElementById(ZEN_DEVTOOLS_KEYSET_ID); - mainKeyset.before(this._zenDevtoolsKeyset); + browser._zenDevtoolsKeyset = browser.document.createXULElement("keyset"); + browser._zenDevtoolsKeyset.id = id; + const mainKeyset = browser.document.getElementById( + ZEN_DEVTOOLS_KEYSET_ID + ); + mainKeyset.before(browser._zenDevtoolsKeyset); } - return this._zenDevtoolsKeyset; + return browser._zenDevtoolsKeyset; }, - clearMainKeyset(element) { - if (this._hasCleared) { + clearMainKeyset(browser, element) { + if (browser._zenKbsHasCleared) { return; } - this._hasCleared = true; + browser._zenKbsHasCleared = true; const children = element.children; for (let i = children.length - 1; i >= 0; i--) { const key = children[i]; @@ -1363,35 +1308,154 @@ window.gZenKeyboardShortcutsManager = { }, _applyShortcuts() { - for (const browser of nsZenMultiWindowFeature.browsers) { - let mainKeyset = browser.document.getElementById(ZEN_MAIN_KEYSET_ID); - if (!mainKeyset) { - throw new Error("Main keyset not found"); + for (const browser of browserWindows()) { + this._applyShortcutsTo(browser); + } + this._applyZenGlobalShortcuts(); + }, + + _applyShortcutsTo(browser) { + let mainKeyset = browser.document.getElementById(ZEN_MAIN_KEYSET_ID); + if (!mainKeyset) { + throw new Error("Main keyset not found"); + } + this.clearMainKeyset(browser, mainKeyset); + + const keyset = this.getZenKeyset(browser); + keyset.innerHTML = ""; + + for (let key of this._currentShortcutList) { + if (key.isInternal()) { + continue; } - browser.gZenKeyboardShortcutsManager.clearMainKeyset(mainKeyset); + let child = key.toXHTMLElement(browser); + keyset.appendChild(child); + } - const keyset = this.getZenKeyset(browser); - keyset.innerHTML = ""; + this._applyDevtoolsShortcuts(browser); + mainKeyset.after(keyset); + }, - for (let key of this._currentShortcutList) { - if (key.isInternal()) { - continue; + _zenGlobalKeyName(shortcut) { + const name = shortcut.getKeyName(); + if (name && name.length === 1) { + return name.toUpperCase(); + } + const code = shortcut.getKeyCode(); + if (!code) { + return null; + } + if (code === "VK_SPACE") { + return "Space"; + } + const fMatch = /^VK_F(\d{1,2})$/.exec(code); + if (fMatch) { + const n = Number(fMatch[1]); + if (n >= 1 && n <= 12) { + return `F${n}`; + } + } + return null; + }, + + _zenGlobalModifierBits(modifiers) { + const iface = Ci.nsIZenGlobalShortcuts; + let bits = 0; + if (modifiers.shift) { + bits |= iface.MODIFIER_SHIFT; + } + if (modifiers.alt) { + bits |= iface.MODIFIER_ALT; + } + if (modifiers.meta) { + bits |= iface.MODIFIER_META; + } + if (modifiers.control) { + bits |= iface.MODIFIER_CTRL; + } + if (modifiers.accel) { + bits |= + AppConstants.platform == "macosx" + ? iface.MODIFIER_META + : iface.MODIFIER_CTRL; + } + return bits; + }, + + _applyZenGlobalListenersFor(browser) { + const map = browser._zenGlobalListenerMap; + if (map) { + for (const [name, listener] of map) { + browser.removeEventListener(name, listener); + } + map.clear(); + } else { + browser._zenGlobalListenerMap = new Map(); + } + + if (!lazy.GLOBAL_SHORTCUTS_ENABLED) { + return; + } + + for (const shortcut of this._currentShortcutList) { + if (!shortcut.isZenGlobal() || shortcut.isDisabled()) { + continue; + } + const id = shortcut.getID(); + const command = shortcut.getAction(); + const eventName = `zen-global-shortcut-${id}`; + const listener = () => { + if (!command) { + return; } - let child = key.toXHTMLElement(browser); - keyset.appendChild(child); - } + const cmdEl = browser.document.getElementById(command); + if (cmdEl) { + cmdEl.doCommand(); + } else { + console.warn( + `Zen CKS: no command element for "${command}" (shortcut "${id}")` + ); + } + }; + browser.addEventListener(eventName, listener); + browser._zenGlobalListenerMap.set(eventName, listener); + } + }, - this._applyDevtoolsShortcuts(browser); - mainKeyset.after(keyset); + _applyZenGlobalShortcuts() { + lazy.ZenGlobalShortcuts.unregisterAll(); + + for (const browser of browserWindows()) { + this._applyZenGlobalListenersFor(browser); + } + + if (!lazy.GLOBAL_SHORTCUTS_ENABLED) { + return; + } + + for (const shortcut of this._currentShortcutList) { + if (!shortcut.isZenGlobal() || shortcut.isDisabled()) { + continue; + } + const key = this._zenGlobalKeyName(shortcut); + if (!key) { + continue; + } + const id = shortcut.getID(); + const mods = this._zenGlobalModifierBits(shortcut.getModifiers()); + try { + lazy.ZenGlobalShortcuts.registerShortcut(id, key, mods); + } catch (e) { + console.warn(`Zen CKS: failed to register global shortcut "${id}"`, e); + } } }, _applyDevtoolsShortcuts(browser) { - if (!browser.gZenKeyboardShortcutsManager?._hasToLoadDevtools) { + if (!browser._zenKbsHasToLoadDevtools && !this._hasToLoadDevtools) { return; } - let devtoolsKeyset = - browser.gZenKeyboardShortcutsManager.getZenDevtoolsKeyset(browser); + let devtoolsKeyset = this.getZenDevtoolsKeyset(browser); for (let key of this._currentShortcutList) { if (key.getGroup() != "devTools") { continue; @@ -1404,12 +1468,10 @@ window.gZenKeyboardShortcutsManager = { continue; } const originalKey = browser.document.getElementById(key.getID()); - // We do not want to remove and create a new key in these cases, - // because it will lose the event listeners. + if (!originalKey) { + continue; + } key.replaceWithChild(originalKey); - // Move the key to the main keyset if it's not there, this is because - // changing modifiers will not work if they are under the devtools keyset - // for some really weird reason. if (originalKey.parentElement.id === ZEN_DEVTOOLS_KEYSET_ID) { devtoolsKeyset.prepend(originalKey); } @@ -1431,11 +1493,13 @@ window.gZenKeyboardShortcutsManager = { for (const shortcut of this._currentShortcutList) { json.push(shortcut.toJSONForm()); } - await this.loader.save(this.versioner.getVersionedData(json)); }, triggerShortcutRebuild() { + if (!this._initialized) { + return; + } this._applyShortcuts(); }, @@ -1443,8 +1507,6 @@ window.gZenKeyboardShortcutsManager = { if (!action) { throw new Error("Action cannot be null"); } - - // Unsetting shortcut for (let targetShortcut of this._currentShortcutList) { if (targetShortcut.getID() != action) { continue; @@ -1456,24 +1518,20 @@ window.gZenKeyboardShortcutsManager = { targetShortcut.setModifiers(modifiers); } } - await this._saveShortcuts(); this.triggerShortcutRebuild(); }, async getModifiableShortcuts() { let rv = []; - if (!this._currentShortcutList) { this._currentShortcutList = await this._loadSaved(); } - for (let shortcut of this._currentShortcutList) { if (shortcut.isUserEditable()) { rv.push(shortcut); } } - return rv; }, @@ -1483,24 +1541,20 @@ window.gZenKeyboardShortcutsManager = { if (targetShortcut.getID() == id) { continue; } - if ( targetShortcut.getModifiers().equals(modifiers) && targetShortcut.getKeyNameOrCode()?.toLowerCase() == realShortcut ) { - return { - hasConflicts: true, - conflictShortcut: targetShortcut, - }; + return { hasConflicts: true, conflictShortcut: targetShortcut }; } } - - return { - hasConflicts: false, - }; + return { hasConflicts: false }; }, getShortcutFromCommand(command) { + if (!this._currentShortcutList) { + return null; + } for (let targetShortcut of this._currentShortcutList) { if (targetShortcut.getAction() == command) { return targetShortcut; @@ -1510,19 +1564,81 @@ window.gZenKeyboardShortcutsManager = { }, /** - * Get the shortcut as a display format for a given action/command. - * - * @param {string} command The action/command to search for - * @returns {string|null} The shortcut as a string or null if not found + * @param {string} command + * @returns {string|null} */ getShortcutDisplayFromCommand(command) { if (!command) { return null; } const shortcut = this.getShortcutFromCommand(command); - if (shortcut) { - return shortcut.toDisplayString(); - } - return null; + return shortcut ? shortcut.toDisplayString() : null; + }, +}; + +function* browserWindows() { + const en = Services.wm.getEnumerator("navigator:browser"); + while (en.hasMoreElements()) { + const win = en.getNext(); + if (win.closed) { + continue; + } + yield win; + } +} + +function isBrowserWindow(aWindow) { + return aWindow?.location?.href === "chrome://browser/content/browser.xhtml"; +} + +function exposeWindowGlobals(aWindow) { + // Bridge for legacy chrome callers that referenced these as window globals. + aWindow.gZenKeyboardShortcutsManager = KbsManager; + aWindow.VALID_SHORTCUT_GROUPS = VALID_SHORTCUT_GROUPS; + aWindow.ZEN_KEYSET_ID = ZEN_KEYSET_ID; +} + +export const ZenKeyboardShortcuts = { + manager: KbsManager, + _initialized: false, + + init() { + if (this._initialized) { + return; + } + this._initialized = true; + Services.obs.addObserver(this, "browser-window-before-show"); + Services.obs.addObserver(this, "quit-application-granted"); + }, + + uninit() { + if (!this._initialized) { + return; + } + this._initialized = false; + try { + Services.obs.removeObserver(this, "browser-window-before-show"); + } catch (e) {} + try { + Services.obs.removeObserver(this, "quit-application-granted"); + } catch (e) {} + try { + lazy.ZenGlobalShortcuts.unregisterAll(); + } catch (e) {} + }, + + observe(aSubject, aTopic) { + switch (aTopic) { + case "browser-window-before-show": + if (!isBrowserWindow(aSubject)) { + return; + } + exposeWindowGlobals(aSubject); + KbsManager.beforeInit(aSubject); + break; + case "quit-application-granted": + this.uninit(); + break; + } }, }; diff --git a/src/zen/kbs/global-shortcuts/ZenGlobalShortcuts.cpp b/src/zen/kbs/global-shortcuts/ZenGlobalShortcuts.cpp new file mode 100644 index 000000000..6a7a67150 --- /dev/null +++ b/src/zen/kbs/global-shortcuts/ZenGlobalShortcuts.cpp @@ -0,0 +1,134 @@ +/* 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 "ZenGlobalShortcuts.h" + +#include "mozilla/dom/Document.h" +#include "nsContentUtils.h" +#include "nsGlobalWindowOuter.h" +#include "nsIWindowMediator.h" +#include "nsPIDOMWindow.h" +#include "nsReadableUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" + +namespace zen { + +ZenGlobalShortcuts* ZenGlobalShortcuts::sInstance = nullptr; + +NS_IMPL_ISUPPORTS(ZenGlobalShortcuts, nsIZenGlobalShortcuts) + +ZenGlobalShortcuts::ZenGlobalShortcuts() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!sInstance); + sInstance = this; +} + +ZenGlobalShortcuts::~ZenGlobalShortcuts() { + MOZ_ASSERT(NS_IsMainThread()); + for (auto& reg : mRegistrations) { + NativeUnregister(reg); + } + mRegistrations.Clear(); + NativeShutdown(); + sInstance = nullptr; +} + +NS_IMETHODIMP +ZenGlobalShortcuts::RegisterShortcut(const nsACString& aId, + const nsACString& aKey, + uint32_t aModifiers, bool* aRetVal) { + MOZ_ASSERT(NS_IsMainThread()); + *aRetVal = false; + + for (const auto& reg : mRegistrations) { + if (reg.id.Equals(aId)) return NS_ERROR_ALREADY_INITIALIZED; + } + + Registration reg; + reg.id = aId; + reg.internalId = mNextInternalId++; + + if (NS_FAILED(NativeRegister(reg, aKey, aModifiers))) { + return NS_OK; + } + + mRegistrations.AppendElement(std::move(reg)); + *aRetVal = true; + return NS_OK; +} + +NS_IMETHODIMP +ZenGlobalShortcuts::UnregisterShortcut(const nsACString& aId) { + MOZ_ASSERT(NS_IsMainThread()); + for (size_t i = 0; i < mRegistrations.Length(); ++i) { + if (mRegistrations[i].id.Equals(aId)) { + NativeUnregister(mRegistrations[i]); + mRegistrations.RemoveElementAt(i); + return NS_OK; + } + } + return NS_OK; +} + +NS_IMETHODIMP +ZenGlobalShortcuts::UnregisterAll() { + MOZ_ASSERT(NS_IsMainThread()); + for (auto& reg : mRegistrations) { + NativeUnregister(reg); + } + mRegistrations.Clear(); + return NS_OK; +} + +const ZenGlobalShortcuts::Registration* ZenGlobalShortcuts::FindByInternalId( + uint32_t aInternalId) const { + for (const auto& reg : mRegistrations) { + if (reg.internalId == aInternalId) return ® + } + return nullptr; +} + +// static +void ZenGlobalShortcuts::OnNativeShortcut(uint32_t aInternalId) { + if (!NS_IsMainThread()) { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "ZenGlobalShortcuts::OnNativeShortcut", + [aInternalId]() { OnNativeShortcut(aInternalId); })); + return; + } + if (!sInstance) return; + + const Registration* reg = sInstance->FindByInternalId(aInternalId); + if (!reg) return; + DispatchEventForId(reg->id); +} + +// static +void ZenGlobalShortcuts::DispatchEventForId(const nsACString& aId) { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr med = do_GetService(NS_WINDOWMEDIATOR_CONTRACTID); + if (!med) return; + + nsCOMPtr mostRecent; + med->GetMostRecentBrowserWindow(getter_AddRefs(mostRecent)); + if (!mostRecent) return; + + nsCOMPtr outer = nsPIDOMWindowOuter::From(mostRecent); + if (!outer) return; + + RefPtr doc = outer->GetExtantDoc(); + if (!doc) return; + + nsAutoString eventName; + eventName.AssignLiteral(u"zen-global-shortcut-"); + AppendUTF8toUTF16(aId, eventName); + + nsContentUtils::DispatchTrustedEvent(doc, nsGlobalWindowOuter::Cast(outer), + eventName, mozilla::CanBubble::eYes, + mozilla::Cancelable::eNo); +} + +} // namespace zen diff --git a/src/zen/kbs/global-shortcuts/ZenGlobalShortcuts.h b/src/zen/kbs/global-shortcuts/ZenGlobalShortcuts.h new file mode 100644 index 000000000..e82540850 --- /dev/null +++ b/src/zen/kbs/global-shortcuts/ZenGlobalShortcuts.h @@ -0,0 +1,61 @@ +/* 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_ZenGlobalShortcuts_h_ +#define mozilla_ZenGlobalShortcuts_h_ + +#include "nsIZenGlobalShortcuts.h" + +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace zen { + +/** + * @brief Singleton XPCOM service that registers OS-level global hotkeys + * and dispatches a trusted DOM event on the most recently focused + * browser window when one fires. + */ +class ZenGlobalShortcuts final : public nsIZenGlobalShortcuts { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIZENGLOBALSHORTCUTS + + ZenGlobalShortcuts(); + + // Per-shortcut record. Public so the per-OS backend can read/write its + // fields directly without going through accessors. + struct Registration { + nsCString id; + uint32_t internalId = 0; + void* nativeHandle = nullptr; + }; + + // Called by the per-OS layer when a registered shortcut is triggered + // by the system. Safe to call from any thread; bounces to the main + // thread before touching DOM state. + static void OnNativeShortcut(uint32_t aInternalId); + + private: + ~ZenGlobalShortcuts(); + + static ZenGlobalShortcuts* sInstance; + + const Registration* FindByInternalId(uint32_t aInternalId) const; + static void DispatchEventForId(const nsACString& aId); + + // Per-OS implementations live in cocoa/, windows/, or the stub. + static nsresult NativeRegister(Registration& aReg, const nsACString& aKey, + uint32_t aModifiers); + static void NativeUnregister(Registration& aReg); + static void NativeShutdown(); + + nsTArray mRegistrations; + uint32_t mNextInternalId = 1; +}; + +} // namespace zen + +#endif diff --git a/src/zen/kbs/global-shortcuts/ZenGlobalShortcutsStub.cpp b/src/zen/kbs/global-shortcuts/ZenGlobalShortcutsStub.cpp new file mode 100644 index 000000000..12296c2f7 --- /dev/null +++ b/src/zen/kbs/global-shortcuts/ZenGlobalShortcutsStub.cpp @@ -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/. */ + +#include "ZenGlobalShortcuts.h" + +// Linux/other-toolkit fallback. A real implementation needs X11 +// XGrabKey on the root window or, on Wayland, the +// org.freedesktop.portal.GlobalShortcuts portal over D-Bus. Until one +// is added, registrations always fail and JS-side code can fall back +// to in-window shortcuts. + +namespace zen { + +// static +nsresult ZenGlobalShortcuts::NativeRegister(Registration&, const nsACString&, + uint32_t) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +// static +void ZenGlobalShortcuts::NativeUnregister(Registration&) {} + +// static +void ZenGlobalShortcuts::NativeShutdown() {} + +} // namespace zen diff --git a/src/zen/kbs/global-shortcuts/cocoa/ZenGlobalShortcutsCocoa.mm b/src/zen/kbs/global-shortcuts/cocoa/ZenGlobalShortcutsCocoa.mm new file mode 100644 index 000000000..fd7b26a9c --- /dev/null +++ b/src/zen/kbs/global-shortcuts/cocoa/ZenGlobalShortcutsCocoa.mm @@ -0,0 +1,210 @@ +/* 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 "ZenGlobalShortcuts.h" + +#include "mozilla/TextEvents.h" +#include "nsReadableUtils.h" +#include "nsString.h" + +#import + +namespace zen { +namespace { + +using mozilla::CodeNameIndex; +using mozilla::WidgetKeyboardEvent; + +constexpr FourCharCode kZenHotKeySignature = 'zen '; + +// Mozilla-internal aliases referenced by NativeKeyToDOMCodeName.inc but +// not part of Carbon. Mirrors widget/cocoa/TextInputHandler.h so we +// don't need to drag the whole header in. +enum { + kVK_PC_ContextMenu = 0x6E, + kVK_Powerbook_KeypadEnter = 0x34, +}; + +class MacGlobalShortcuts final { + public: + MacGlobalShortcuts() = delete; + + static nsresult Register(ZenGlobalShortcuts::Registration& aReg, + const nsACString& aKey, uint32_t aModifiers); + static void Unregister(ZenGlobalShortcuts::Registration& aReg); + static void Shutdown(); + + private: + static bool EnsureHandler(); + static OSStatus HandleHotKey(EventHandlerCallRef, EventRef, void*); + static bool ResolveKey(const nsACString& aKey, UInt32& aOut); + static UInt32 ToCarbonModifiers(uint32_t aMods); + + static EventHandlerUPP sUPP; + static EventHandlerRef sHandler; +}; + +EventHandlerUPP MacGlobalShortcuts::sUPP = nullptr; +EventHandlerRef MacGlobalShortcuts::sHandler = nullptr; + +// static +OSStatus MacGlobalShortcuts::HandleHotKey(EventHandlerCallRef, EventRef inEvent, + void*) { + EventHotKeyID hkID; + if (GetEventParameter(inEvent, kEventParamDirectObject, typeEventHotKeyID, + nullptr, sizeof(hkID), nullptr, &hkID) == noErr) { + ZenGlobalShortcuts::OnNativeShortcut(hkID.id); + } + return noErr; +} + +// static +bool MacGlobalShortcuts::EnsureHandler() { + if (sHandler) return true; + + sUPP = NewEventHandlerUPP(HandleHotKey); + if (!sUPP) return false; + + EventTypeSpec spec = {kEventClassKeyboard, kEventHotKeyPressed}; + OSStatus status = + InstallApplicationEventHandler(sUPP, 1, &spec, nullptr, &sHandler); + if (status != noErr) { + DisposeEventHandlerUPP(sUPP); + sUPP = nullptr; + sHandler = nullptr; + return false; + } + return true; +} + +// Convert the JS-friendly key string into a DOM code-name (e.g. "A" -> +// "KeyA", "5" -> "Digit5", "F1"/"f1" -> "F1", "Space"/"space" -> "Space"). +// Returns false for inputs we don't accept. +static bool ToDOMCodeName(const nsACString& aKey, nsAString& aOut) { + aOut.Truncate(); + if (aKey.Length() == 1) { + char c = aKey[0]; + if (c >= 'a' && c <= 'z') c = char(c - 32); + if (c >= 'A' && c <= 'Z') { + aOut.AssignLiteral(u"Key"); + } else if (c >= '0' && c <= '9') { + aOut.AssignLiteral(u"Digit"); + } else { + return false; + } + aOut.Append(char16_t(c)); + return true; + } + // Multi-character: assume it's a DOM code name, normalized to leading + // upper-case ("space" -> "Space", "f1" -> "F1"). + AppendUTF8toUTF16(aKey, aOut); + if (!aOut.IsEmpty() && aOut[0] >= 'a' && aOut[0] <= 'z') { + aOut.BeginWriting()[0] = char16_t(aOut[0] - 32); + } + return true; +} + +struct CodeIndexToMacKey { + CodeNameIndex idx; + UInt32 keyCode; +}; + +// Generated from widget's mapping table. Order matches the .inc, so when +// multiple native keys map to the same DOM code (e.g. NumpadEnter -> +// kVK_ANSI_KeypadEnter and kVK_Powerbook_KeypadEnter), the first entry +// wins -- which is the one we'd want to pass to RegisterEventHotKey. +static constexpr CodeIndexToMacKey kCodeIndexToMacKeyTable[] = { +#define NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX(aNativeKey, aCodeNameIndex) \ + {mozilla::aCodeNameIndex, static_cast(aNativeKey)}, +#include "NativeKeyToDOMCodeName.inc" +#undef NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX +}; + +// static +bool MacGlobalShortcuts::ResolveKey(const nsACString& aKey, UInt32& aOut) { + nsAutoString domCode; + if (!ToDOMCodeName(aKey, domCode)) return false; + + CodeNameIndex idx = WidgetKeyboardEvent::GetCodeNameIndex(domCode); + if (idx == mozilla::CODE_NAME_INDEX_USE_STRING) return false; + + for (const auto& entry : kCodeIndexToMacKeyTable) { + if (entry.idx == idx) { + aOut = entry.keyCode; + return true; + } + } + return false; +} + +// static +UInt32 MacGlobalShortcuts::ToCarbonModifiers(uint32_t aMods) { + UInt32 m = 0; + if (aMods & nsIZenGlobalShortcuts::MODIFIER_SHIFT) m |= shiftKey; + if (aMods & nsIZenGlobalShortcuts::MODIFIER_CTRL) m |= controlKey; + if (aMods & nsIZenGlobalShortcuts::MODIFIER_ALT) m |= optionKey; + if (aMods & nsIZenGlobalShortcuts::MODIFIER_META) m |= cmdKey; + return m; +} + +// static +nsresult MacGlobalShortcuts::Register(ZenGlobalShortcuts::Registration& aReg, + const nsACString& aKey, + uint32_t aModifiers) { + if (!EnsureHandler()) return NS_ERROR_FAILURE; + + UInt32 keyCode; + if (!ResolveKey(aKey, keyCode)) return NS_ERROR_INVALID_ARG; + + EventHotKeyID hkID; + hkID.signature = kZenHotKeySignature; + hkID.id = aReg.internalId; + + EventHotKeyRef ref = nullptr; + OSStatus status = + RegisterEventHotKey(keyCode, ToCarbonModifiers(aModifiers), hkID, + GetApplicationEventTarget(), 0, &ref); + if (status != noErr || !ref) return NS_ERROR_FAILURE; + + aReg.nativeHandle = static_cast(ref); + return NS_OK; +} + +// static +void MacGlobalShortcuts::Unregister(ZenGlobalShortcuts::Registration& aReg) { + if (!aReg.nativeHandle) return; + UnregisterEventHotKey(static_cast(aReg.nativeHandle)); + aReg.nativeHandle = nullptr; +} + +// static +void MacGlobalShortcuts::Shutdown() { + if (sHandler) { + RemoveEventHandler(sHandler); + sHandler = nullptr; + } + if (sUPP) { + DisposeEventHandlerUPP(sUPP); + sUPP = nullptr; + } +} + +} // namespace + +// static +nsresult ZenGlobalShortcuts::NativeRegister(Registration& aReg, + const nsACString& aKey, + uint32_t aModifiers) { + return MacGlobalShortcuts::Register(aReg, aKey, aModifiers); +} + +// static +void ZenGlobalShortcuts::NativeUnregister(Registration& aReg) { + MacGlobalShortcuts::Unregister(aReg); +} + +// static +void ZenGlobalShortcuts::NativeShutdown() { MacGlobalShortcuts::Shutdown(); } + +} // namespace zen diff --git a/src/zen/kbs/global-shortcuts/cocoa/moz.build b/src/zen/kbs/global-shortcuts/cocoa/moz.build new file mode 100644 index 000000000..480939c2c --- /dev/null +++ b/src/zen/kbs/global-shortcuts/cocoa/moz.build @@ -0,0 +1,18 @@ +# 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_LIBRARY = "xul" + +SOURCES += [ + "ZenGlobalShortcutsCocoa.mm", +] + +LOCAL_INCLUDES += [ + "../", + "/widget", +] + +OS_LIBS += [ + "-framework Carbon", +] diff --git a/src/zen/kbs/global-shortcuts/components.conf b/src/zen/kbs/global-shortcuts/components.conf new file mode 100644 index 000000000..bdc26ec79 --- /dev/null +++ b/src/zen/kbs/global-shortcuts/components.conf @@ -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': '{b8e9f3a2-7c1d-4a5b-9e6f-3d8c2a1b5e74}', + 'interfaces': ['nsIZenGlobalShortcuts'], + 'contract_ids': ['@mozilla.org/zen/global-shortcuts;1'], + 'type': 'zen::ZenGlobalShortcuts', + 'headers': ['mozilla/ZenGlobalShortcuts.h'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, +] diff --git a/src/zen/kbs/global-shortcuts/moz.build b/src/zen/kbs/global-shortcuts/moz.build new file mode 100644 index 000000000..7a6b7874a --- /dev/null +++ b/src/zen/kbs/global-shortcuts/moz.build @@ -0,0 +1,29 @@ +# 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 += [ + "nsIZenGlobalShortcuts.idl", +] + +EXPORTS.mozilla += [ + "ZenGlobalShortcuts.h", +] + +SOURCES += [ + "ZenGlobalShortcuts.cpp", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + DIRS += ["cocoa"] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows": + DIRS += ["windows"] +else: + SOURCES += ["ZenGlobalShortcutsStub.cpp"] + +FINAL_LIBRARY = "xul" +XPIDL_MODULE = "zen_global_shortcuts" diff --git a/src/zen/kbs/global-shortcuts/nsIZenGlobalShortcuts.idl b/src/zen/kbs/global-shortcuts/nsIZenGlobalShortcuts.idl new file mode 100644 index 000000000..c4914f934 --- /dev/null +++ b/src/zen/kbs/global-shortcuts/nsIZenGlobalShortcuts.idl @@ -0,0 +1,49 @@ +/* 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" + +/** + * @brief OS-level global keyboard shortcut registration for Zen. + * + * Registers shortcuts with the operating system so that pressing the + * key combination triggers a callback even when no Zen window is + * focused (or the application is in the background). When a shortcut + * fires, a trusted DOM event is dispatched on the most recently + * focused browser window. The event type is + * "zen-global-shortcut-", where is the identifier passed at + * registration time. + */ +[scriptable, uuid(b8e9f3a2-7c1d-4a5b-9e6f-3d8c2a1b5e74)] +interface nsIZenGlobalShortcuts : nsISupports { + const unsigned long MODIFIER_NONE = 0; + const unsigned long MODIFIER_SHIFT = 1; + const unsigned long MODIFIER_CTRL = 2; + const unsigned long MODIFIER_ALT = 4; + const unsigned long MODIFIER_META = 8; + + /** + * @brief Register a global keyboard shortcut. + * @param aId Caller-chosen identifier; the dispatched event name will be + * "zen-global-shortcut-" + aId. Must be unique across active + * registrations. + * @param aKey Key name. Supported: "A".."Z", "0".."9", "F1".."F12", + * "Space". + * @param aModifiers Bitmask of MODIFIER_* constants. On macOS, META + * is Command; on Windows, META is the Windows key. + * @return true if the OS accepted the registration. + */ + boolean registerShortcut(in ACString aId, in ACString aKey, + in unsigned long aModifiers); + + /** + * @brief Unregister a previously registered shortcut by id. + */ + void unregisterShortcut(in ACString aId); + + /** + * @brief Unregister all shortcuts registered through this service. + */ + void unregisterAll(); +}; diff --git a/src/zen/kbs/global-shortcuts/windows/ZenGlobalShortcutsWindows.cpp b/src/zen/kbs/global-shortcuts/windows/ZenGlobalShortcutsWindows.cpp new file mode 100644 index 000000000..9f2db667c --- /dev/null +++ b/src/zen/kbs/global-shortcuts/windows/ZenGlobalShortcutsWindows.cpp @@ -0,0 +1,166 @@ +/* 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 "ZenGlobalShortcuts.h" + +#include "nsString.h" + +#include + +namespace zen { +namespace { + +constexpr wchar_t kWindowClassName[] = L"ZenGlobalShortcutsWindow"; + +class WinGlobalShortcuts final { + public: + WinGlobalShortcuts() = delete; + + static nsresult Register(ZenGlobalShortcuts::Registration& aReg, + const nsACString& aKey, uint32_t aModifiers); + static void Unregister(ZenGlobalShortcuts::Registration& aReg); + static void Shutdown(); + + private: + static bool EnsureWindow(); + static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + static bool ResolveKey(const nsACString& aKey, UINT& aOut); + static UINT ToWinModifiers(uint32_t aMods); + + static HWND sWindow; + static ATOM sClass; +}; + +HWND WinGlobalShortcuts::sWindow = nullptr; +ATOM WinGlobalShortcuts::sClass = 0; + +// static +LRESULT CALLBACK WinGlobalShortcuts::WndProc(HWND hwnd, UINT msg, WPARAM wParam, + LPARAM lParam) { + if (msg == WM_HOTKEY) { + ZenGlobalShortcuts::OnNativeShortcut(static_cast(wParam)); + return 0; + } + return DefWindowProcW(hwnd, msg, wParam, lParam); +} + +// static +bool WinGlobalShortcuts::EnsureWindow() { + if (sWindow) return true; + + HINSTANCE module = GetModuleHandleW(nullptr); + if (!sClass) { + WNDCLASSEXW wc = {}; + wc.cbSize = sizeof(wc); + wc.lpfnWndProc = WndProc; + wc.hInstance = module; + wc.lpszClassName = kWindowClassName; + sClass = RegisterClassExW(&wc); + if (!sClass) return false; + } + + sWindow = CreateWindowExW(0, kWindowClassName, L"", 0, 0, 0, 0, 0, + HWND_MESSAGE, nullptr, module, nullptr); + return sWindow != nullptr; +} + +// static +bool WinGlobalShortcuts::ResolveKey(const nsACString& aKey, UINT& aOut) { + if (aKey.Length() == 1) { + char c = aKey[0]; + if (c >= 'a' && c <= 'z') c = char(c - 32); + if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) { + aOut = static_cast(c); + return true; + } + return false; + } + if (aKey.LowerCaseEqualsLiteral("space")) { + aOut = VK_SPACE; + return true; + } + if ((aKey.Length() == 2 || aKey.Length() == 3) && + (aKey[0] == 'F' || aKey[0] == 'f')) { + int n = aKey[1] - '0'; + if (n < 0 || n > 9) return false; + if (aKey.Length() == 3) { + int d = aKey[2] - '0'; + if (d < 0 || d > 9) return false; + n = n * 10 + d; + } + if (n >= 1 && n <= 12) { + aOut = VK_F1 + (n - 1); + return true; + } + } + return false; +} + +// static +UINT WinGlobalShortcuts::ToWinModifiers(uint32_t aMods) { + UINT m = MOD_NOREPEAT; + if (aMods & nsIZenGlobalShortcuts::MODIFIER_SHIFT) m |= MOD_SHIFT; + if (aMods & nsIZenGlobalShortcuts::MODIFIER_CTRL) m |= MOD_CONTROL; + if (aMods & nsIZenGlobalShortcuts::MODIFIER_ALT) m |= MOD_ALT; + if (aMods & nsIZenGlobalShortcuts::MODIFIER_META) m |= MOD_WIN; + return m; +} + +// static +nsresult WinGlobalShortcuts::Register(ZenGlobalShortcuts::Registration& aReg, + const nsACString& aKey, + uint32_t aModifiers) { + if (!EnsureWindow()) return NS_ERROR_FAILURE; + + UINT vk; + if (!ResolveKey(aKey, vk)) return NS_ERROR_INVALID_ARG; + + if (!RegisterHotKey(sWindow, static_cast(aReg.internalId), + ToWinModifiers(aModifiers), vk)) { + return NS_ERROR_FAILURE; + } + aReg.nativeHandle = + reinterpret_cast(static_cast(aReg.internalId)); + return NS_OK; +} + +// static +void WinGlobalShortcuts::Unregister(ZenGlobalShortcuts::Registration& aReg) { + if (!sWindow || !aReg.nativeHandle) return; + UnregisterHotKey( + sWindow, + static_cast(reinterpret_cast(aReg.nativeHandle))); + aReg.nativeHandle = nullptr; +} + +// static +void WinGlobalShortcuts::Shutdown() { + if (sWindow) { + DestroyWindow(sWindow); + sWindow = nullptr; + } + if (sClass) { + UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr)); + sClass = 0; + } +} + +} // namespace + +// static +nsresult ZenGlobalShortcuts::NativeRegister(Registration& aReg, + const nsACString& aKey, + uint32_t aModifiers) { + return WinGlobalShortcuts::Register(aReg, aKey, aModifiers); +} + +// static +void ZenGlobalShortcuts::NativeUnregister(Registration& aReg) { + WinGlobalShortcuts::Unregister(aReg); +} + +// static +void ZenGlobalShortcuts::NativeShutdown() { WinGlobalShortcuts::Shutdown(); } + +} // namespace zen diff --git a/src/zen/kbs/jar.inc.mn b/src/zen/kbs/global-shortcuts/windows/moz.build similarity index 62% rename from src/zen/kbs/jar.inc.mn rename to src/zen/kbs/global-shortcuts/windows/moz.build index d8b3563d5..e57b6d149 100644 --- a/src/zen/kbs/jar.inc.mn +++ b/src/zen/kbs/global-shortcuts/windows/moz.build @@ -2,4 +2,12 @@ # 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/. - content/browser/zen-components/ZenKeyboardShortcuts.mjs (../../zen/kbs/ZenKeyboardShortcuts.mjs) +FINAL_LIBRARY = "xul" + +SOURCES += [ + "ZenGlobalShortcutsWindows.cpp", +] + +LOCAL_INCLUDES += [ + "../", +] diff --git a/src/zen/kbs/moz.build b/src/zen/kbs/moz.build new file mode 100644 index 000000000..7ed24a099 --- /dev/null +++ b/src/zen/kbs/moz.build @@ -0,0 +1,11 @@ +# 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/. + +EXTRA_JS_MODULES.zen += [ + "ZenKeyboardShortcuts.sys.mjs", +] + +DIRS += [ + "global-shortcuts", +] diff --git a/src/zen/little-window/LittleWindowComponents.manifest b/src/zen/little-window/LittleWindowComponents.manifest new file mode 100644 index 000000000..2a29a8e10 --- /dev/null +++ b/src/zen/little-window/LittleWindowComponents.manifest @@ -0,0 +1,6 @@ +# 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/. + +category browser-before-ui-startup resource:///modules/zen/ZenLittleWindow.sys.mjs ZenLittleWindow.init +category browser-quit-application-granted resource:///modules/zen/ZenLittleWindow.sys.mjs ZenLittleWindow.uninit diff --git a/src/zen/little-window/ZenLittleWindow.sys.mjs b/src/zen/little-window/ZenLittleWindow.sys.mjs new file mode 100644 index 000000000..371e1fe52 --- /dev/null +++ b/src/zen/little-window/ZenLittleWindow.sys.mjs @@ -0,0 +1,110 @@ +/* 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/. */ + +const URLBAR_HEIGHT = 340; +const URLBAR_WIDTH = 640; + +const FEATURES = + "titlebar,close,toolbar,location,personalbar=no,status,menubar=no," + + `resizable,minimizable,scrollbars,width=${URLBAR_WIDTH},height=${URLBAR_HEIGHT},centerscreen`; + +class nsZenLittleWindow { + #initialized = false; + + init() { + if (this.#initialized) { + return; + } + this.#initialized = true; + Services.obs.addObserver(this, "browser-window-before-show"); + } + + uninit() { + if (!this.#initialized) { + return; + } + this.#initialized = false; + try { + Services.obs.removeObserver(this, "browser-window-before-show"); + } catch (e) {} + } + + observe(subject, topic) { + if ( + topic === "browser-window-before-show" && + this.#isLittleWindow(subject) + ) { + this.#attachAutoclose(subject); + } + } + + /** + * Open a fresh little window, or focus an existing empty one if there + * already is a little window sitting on its empty tab. + * + * @param {Window} opener The browser window asking for the little window. + * @returns {Window|null} The window that received focus. + */ + openLittleWindow(opener) { + for (const win of this.#iterLittleWindows()) { + if (this.#isOnEmptyTab(win)) { + win.focus(); + return win; + } + } + if (typeof opener?.OpenBrowserWindow !== "function") { + return null; + } + let win = opener.OpenBrowserWindow({ + zenLittleWindow: true, + all: false, + features: FEATURES, + }); + win.focus(); + return win; + } + + #isLittleWindow(win) { + return ( + !!win._zenStartupLittleWindow || + win.document?.documentElement?.hasAttribute("zen-little-window") + ); + } + + #isOnEmptyTab(win) { + const tab = win.gBrowser?.selectedTab; + return !!tab?.hasAttribute("zen-empty-tab"); + } + + *#iterLittleWindows() { + const en = Services.wm.getEnumerator("navigator:browser"); + while (en.hasMoreElements()) { + const win = en.getNext(); + if (!win.closed && this.#isLittleWindow(win)) { + yield win; + } + } + } + + #attachAutoclose(win) { + const onClosed = event => { + if (event.detail?.onElementPicked && event.type === "ZenURLBarClosed") { + return; + } + if (!win.closed && this.#isOnEmptyTab(win)) { + win.close(); + } else { + // Resize window back to normal size + win.resizeTo(1240, 840); + } + }; + win.document.documentElement.setAttribute("zen-little-window", "true"); + win.resizeTo(URLBAR_WIDTH, URLBAR_HEIGHT); + win.focus(); + win.addEventListener("ZenURLBarClosed", onClosed, { once: true }); + win.addEventListener("blur", onClosed, { once: true }); + } +} + +export const ZenLittleWindow = new nsZenLittleWindow(); diff --git a/src/zen/little-window/jar.inc.mn b/src/zen/little-window/jar.inc.mn new file mode 100644 index 000000000..474805e6c --- /dev/null +++ b/src/zen/little-window/jar.inc.mn @@ -0,0 +1,5 @@ +# 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/. + + content/browser/zen-styles/zen-little-window.css (../../zen/little-window/zen-little-window.css) diff --git a/src/zen/little-window/moz.build b/src/zen/little-window/moz.build new file mode 100644 index 000000000..977f6828d --- /dev/null +++ b/src/zen/little-window/moz.build @@ -0,0 +1,7 @@ +# 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/. + +EXTRA_JS_MODULES.zen += [ + "ZenLittleWindow.sys.mjs", +] diff --git a/src/zen/little-window/zen-little-window.css b/src/zen/little-window/zen-little-window.css new file mode 100644 index 000000000..53406145d --- /dev/null +++ b/src/zen/little-window/zen-little-window.css @@ -0,0 +1,52 @@ +/* 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/. */ + +/* + * Little-window chrome: the URL bar fills the entire window. The vertical + * tab strip, sidebar buttons, bookmarks toolbar, and any other chrome are + * suppressed so the window acts as a quick search/launch box. + */ + +:root[zen-little-window="true"] { + /* + * The navigator toolbox (sidebar + tab strip) and its splitter are + * never visible in a little window. We use visibility: hidden rather + * than display: none so layout stays stable and the urlbar floats + * cleanly over its slot. + */ + #navigator-toolbox, + #zen-sidebar-splitter { + visibility: collapse !important; + } + + &[zen-has-empty-tab="true"] { + /* Keep in sync with URLBAR_HEIGHT in ZenLittleWindow.sys.mjs */ + --zen-minimum-window-height: 340px; + min-width: unset !important; + + #zen-appcontent-wrapper { + visibility: hidden; + } + + #urlbar[breakout-extend] { + min-width: 100% !important; + left: 50% !important; + top: 0 !important; + transform: translate(10px, 0) !important; + visibility: visible; + + & .urlbar-background { + outline: none !important; + } + + & .urlbar-input-container { + -moz-window-dragging: drag; + } + + & .urlbar-input-box { + -moz-window-dragging: no-drag; + } + } + } +} diff --git a/src/zen/moz.build b/src/zen/moz.build index 2d32efe20..00425ca02 100644 --- a/src/zen/moz.build +++ b/src/zen/moz.build @@ -10,6 +10,8 @@ DIRS += [ "common", "drag-and-drop", "glance", + "kbs", + "little-window", "live-folders", "mods", "tests", diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index 3a8eab4c6..595824727 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -212,6 +212,13 @@ class nsZenWindowSync { ) { return; } + if (aWindow._zenStartupLittleWindow) { + aWindow.document.documentElement.setAttribute( + "zen-little-window", + "true" + ); + delete aWindow._zenStartupLittleWindow; + } this.log("Setting up window sync for window", aWindow); // There are 2 possibilities to know if we are trying to open // a new *unsynced* window: