no-bug: Start working on little zen

This commit is contained in:
mr. m
2026-04-28 00:38:09 +02:00
parent 040bed5ce5
commit cccbcf662e
36 changed files with 1425 additions and 303 deletions

View File

@@ -7,3 +7,6 @@
- name: zen.keyboard.shortcuts.disable-mainkeyset-clear
value: false # for debugging
- name: zen.keyboard.shortcuts.global.enabled
value: true

View File

@@ -28,6 +28,8 @@
<link rel="stylesheet" type="text/css" href="chrome://browser/content/zen-styles/zen-welcome.css" />
<link rel="stylesheet" type="text/css" href="chrome://browser/content/zen-styles/zen-media-controls.css" />
<link rel="stylesheet" type="text/css" href="chrome://browser/content/zen-styles/zen-download-box-animation.css" />
<link rel="stylesheet" type="text/css" href="chrome://browser/content/zen-styles/zen-little-window.css" />
</linkset>
# Startup "preloaded" scripts that requre globals such as gBrowser and gURLBar

View File

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

View File

@@ -68,4 +68,6 @@
<command id="cmd_zenNewLiveFolder" />
<command id="cmd_zenDuplicateTab" />
<command id="cmd_zenNewLittleWindow" />
</commandset>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,7 +32,6 @@ class ZenStartup {
return;
}
this.#hasInitializedLayout = true;
gZenKeyboardShortcutsManager.beforeInit();
try {
const kNavbarItems = ["nav-bar", "PersonalToolbar"];
const kNewContainerId = "zen-appcontent-navbar-container";

View File

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

View File

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

View File

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

View File

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

View File

@@ -126,6 +126,9 @@ window.gZenCompactModeManager = {
},
get shouldBeCompact() {
if (document.documentElement.hasAttribute("zen-little-window")) {
return false;
}
return !document.documentElement
.getAttribute("chromehidden")
?.includes("toolbar");

View File

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

View File

@@ -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 &reg;
}
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<nsIWindowMediator> med = do_GetService(NS_WINDOWMEDIATOR_CONTRACTID);
if (!med) return;
nsCOMPtr<mozIDOMWindowProxy> mostRecent;
med->GetMostRecentBrowserWindow(getter_AddRefs(mostRecent));
if (!mostRecent) return;
nsCOMPtr<nsPIDOMWindowOuter> outer = nsPIDOMWindowOuter::From(mostRecent);
if (!outer) return;
RefPtr<mozilla::dom::Document> 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

View File

@@ -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<Registration> mRegistrations;
uint32_t mNextInternalId = 1;
};
} // namespace zen
#endif

View File

@@ -0,0 +1,27 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#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

View File

@@ -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 <Carbon/Carbon.h>
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<UInt32>(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<void*>(ref);
return NS_OK;
}
// static
void MacGlobalShortcuts::Unregister(ZenGlobalShortcuts::Registration& aReg) {
if (!aReg.nativeHandle) return;
UnregisterEventHotKey(static_cast<EventHotKeyRef>(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

View File

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

View File

@@ -0,0 +1,14 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
Classes = [
{
'cid': '{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,
},
]

View File

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

View File

@@ -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-<id>", where <id> 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();
};

View File

@@ -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 <windows.h>
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<uint32_t>(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<UINT>(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<int>(aReg.internalId),
ToWinModifiers(aModifiers), vk)) {
return NS_ERROR_FAILURE;
}
aReg.nativeHandle =
reinterpret_cast<void*>(static_cast<uintptr_t>(aReg.internalId));
return NS_OK;
}
// static
void WinGlobalShortcuts::Unregister(ZenGlobalShortcuts::Registration& aReg) {
if (!sWindow || !aReg.nativeHandle) return;
UnregisterHotKey(
sWindow,
static_cast<int>(reinterpret_cast<uintptr_t>(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

View File

@@ -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 += [
"../",
]

11
src/zen/kbs/moz.build Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,8 @@ DIRS += [
"common",
"drag-and-drop",
"glance",
"kbs",
"little-window",
"live-folders",
"mods",
"tests",

View File

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