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: