Compare commits

...

7 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
cbd9381997 Merge remote-tracking branch 'origin/dev' into little-zen
# Conflicts:
#	src/zen/kbs/ZenKeyboardShortcuts.sys.mjs

Co-authored-by: mr-cheffy <91018726+mr-cheffy@users.noreply.github.com>
2026-05-07 18:40:04 +00:00
mr. m
685cddf7c2 no-bug: Add space button 2026-05-01 19:11:12 +02:00
mr. m
27f40393d5 no-bug: Simplify stylings 2026-04-29 20:11:05 +02:00
mr. m
f99b8af86d no-bug: Collapse top buttons for little zen 2026-04-29 12:04:32 +02:00
mr. m
e3b0295e36 no-bug: Continue work 2026-04-29 02:21:22 +02:00
mr. m
d15f5331ff Merge branch 'dev' into little-zen 2026-04-28 00:38:39 +02:00
mr. m
cccbcf662e no-bug: Start working on little zen 2026-04-28 00:38:09 +02:00
55 changed files with 2226 additions and 399 deletions

View File

@@ -151,3 +151,5 @@ zen-window-sync-migration-dialog-accept = Got It
zen-appmenu-new-blank-window =
.label = New blank window
zen-spaces-search-placeholder =
.placeholder = Search your spaces...

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
@@ -20,3 +19,4 @@
#include ../../../zen/fonts/jar.inc.mn
#include ../../../zen/boosts/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

@@ -0,0 +1,23 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<panel id="zen-spaces-popup"
nonnativepopover="true"
type="arrow"
orient="vertical"
side="bottom"
hidden="true"
consumeoutsideclicks="never">
<hbox class="zen-spaces-list-header" flex="1">
<image class="zen-spaces-list-search-icon" src="chrome://global/skin/icons/search-glass.svg"/>
<html:input id="zen-spaces-list-search"
data-l10n-id="zen-spaces-search-placeholder"
type="search" />
</hbox>
<scrollbox class="zen-spaces-list-scrollbox" flex="1">
<vbox id="zen-spaces-list"></vbox>
<hbox id="zen-spaces-search-no-results" hidden="true" flex="1"
data-l10n-id="zen-spaces-search-no-results" />
</scrollbox>
</panel>

View File

@@ -5,6 +5,7 @@
#include zen-panels/theme-picker.inc
#include zen-panels/emojis-picker.inc
#include zen-panels/folders-search.inc
#include zen-panels/spaces-search.inc
#include zen-panels/site-data.inc
#include zen-panels/popups.inc

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..e5de81b3060a1ee76b1a6aff2e4ae1ca50f2caa9 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,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
l10nId,
l10nId == "urlbar-placeholder-with-name"
? { name: engineName }
@@ -4964,6 +5077,11 @@ export class UrlbarInput extends HTMLElement {
@@ -4964,6 +5085,11 @@ export class UrlbarInput extends HTMLElement {
}
_on_click(event) {
@@ -291,7 +306,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
switch (event.target) {
case this.inputField:
case this._inputContainer:
@@ -5042,7 +5160,7 @@ export class UrlbarInput extends HTMLElement {
@@ -5042,7 +5168,7 @@ export class UrlbarInput extends HTMLElement {
}
}
@@ -300,7 +315,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
this.view.autoOpen({ event });
} else {
if (this._untrimOnFocusAfterKeydown) {
@@ -5082,9 +5200,16 @@ export class UrlbarInput extends HTMLElement {
@@ -5082,9 +5208,16 @@ export class UrlbarInput extends HTMLElement {
}
_on_mousedown(event) {
@@ -318,7 +333,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
if (
event.composedTarget != this.inputField &&
event.composedTarget != this._inputContainer
@@ -5094,6 +5219,10 @@ export class UrlbarInput extends HTMLElement {
@@ -5094,6 +5227,10 @@ export class UrlbarInput extends HTMLElement {
this.focusedViaMousedown = !this.focused;
this._preventClickSelectsAll = this.focused;
@@ -329,7 +344,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 +5266,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 +353,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
break;
}
@@ -5411,7 +5540,7 @@ export class UrlbarInput extends HTMLElement {
@@ -5411,7 +5548,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

@@ -0,0 +1,27 @@
diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
index 515177a3a97094142593a98fb1b3023acf1ccb87..fb6f932547f9523a240f95915b7440dcdfa16975 100644
--- a/widget/cocoa/nsCocoaWindow.mm
+++ b/widget/cocoa/nsCocoaWindow.mm
@@ -5240,7 +5240,7 @@ static unsigned int WindowMaskForBorderStyle(BorderStyle aBorderStyle) {
// calls to ...orderFront: in TRY blocks. See bmo bug 470864.
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
mWindow.contentView.needsDisplay = YES;
- if (!nativeParentWindow || mPopupLevel != PopupLevel::Parent) {
+ if (!mZenShowLocked && (!nativeParentWindow || mPopupLevel != PopupLevel::Parent)) {
[mWindow orderFront:nil];
}
NS_OBJC_END_TRY_IGNORE_BLOCK;
@@ -5287,7 +5287,12 @@ static unsigned int WindowMaskForBorderStyle(BorderStyle aBorderStyle) {
// We don't want most alwaysontop / alert windows to pull focus when
// they're opened, as these tend to be for peripheral indicators and
// displays.
- if ((mAlwaysOnTop && mPiPType != PiPType::DocumentPiP) || mIsAlert) {
+ if (mZenShowLocked) {
+ // Zen: window-control service has this widget locked-hidden;
+ // skip the native order-front but let the rest of Show()'s
+ // bookkeeping run normally.
+ } else if ((mAlwaysOnTop && mPiPType != PiPType::DocumentPiP) ||
+ mIsAlert) {
[mWindow orderFront:nil];
} else {
[mWindow makeKeyAndOrderFront:nil];

View File

@@ -0,0 +1,18 @@
diff --git a/widget/gtk/nsWindow.cpp b/widget/gtk/nsWindow.cpp
index 89950bd72c77ed961e59faa2fadb8f21a78fd23a..8c74bfa08af3c16cc05e11ea34902cbcdde3c64a 100644
--- a/widget/gtk/nsWindow.cpp
+++ b/widget/gtk/nsWindow.cpp
@@ -1076,7 +1076,12 @@ void nsWindow::Show(bool aState) {
}
#endif
- NativeShow(aState);
+ // Zen: skip the actual GTK show when window-control has the widget
+ // locked-hidden; mIsShown is already set above so Mozilla's
+ // bookkeeping treats the widget as shown.
+ if (!(aState && mZenShowLocked)) {
+ NativeShow(aState);
+ }
RefreshWindowClass();
}

View File

@@ -0,0 +1,28 @@
diff --git a/widget/nsIWidget.h b/widget/nsIWidget.h
index c22e055b9254ed1c8943c232a8339c574563dffc..0a0cb7cad4e878f93c03078564dac051dde1d014 100644
--- a/widget/nsIWidget.h
+++ b/widget/nsIWidget.h
@@ -698,6 +698,14 @@ class nsIWidget : public nsSupportsWeakReference {
*/
virtual void Show(bool aState) = 0;
+ /**
+ * Zen: when set to true, Show(true) calls on this widget become
+ * no-ops until SetZenShowLocked(false) is called. Used to keep a
+ * window invisible while it's being set up.
+ */
+ void SetZenShowLocked(bool aLocked) { mZenShowLocked = aLocked; }
+ bool IsZenShowLocked() const { return mZenShowLocked; }
+
/**
* Whether or not a widget must be recreated after being hidden to show
* again properly.
@@ -2419,6 +2427,8 @@ class nsIWidget : public nsSupportsWeakReference {
mozilla::Maybe<FullscreenSavedState> mSavedBounds;
bool mUpdateCursor;
+ // Zen: when true, Show(true) is a no-op. See SetZenShowLocked().
+ bool mZenShowLocked = false;
bool mIMEHasFocus;
bool mIMEHasQuit;
// if the window is fully occluded (rendering may be paused in response)

View File

@@ -0,0 +1,59 @@
diff --git a/widget/windows/nsWindow.cpp b/widget/windows/nsWindow.cpp
index baed7d09e31291b26a1d0a1ddfd63b57f0ce67d0..9daa1140e9538427d002c6f8ff99cd68265249cc 100644
--- a/widget/windows/nsWindow.cpp
+++ b/widget/windows/nsWindow.cpp
@@ -1761,28 +1761,33 @@ void nsWindow::Show(bool aState) {
// cursor.
SetCursor(Cursor{eCursor_standard});
- switch (mFrameState->GetSizeMode()) {
- case nsSizeMode_Fullscreen:
- ::ShowWindow(mWnd, SW_SHOW);
- break;
- case nsSizeMode_Maximized:
- ::ShowWindow(mWnd, SW_SHOWMAXIMIZED);
- break;
- case nsSizeMode_Minimized:
- ::ShowWindow(mWnd, SW_SHOWMINIMIZED);
- break;
- default:
- if (CanTakeFocus() &&
- (!mAlwaysOnTop || mPiPType == PiPType::DocumentPiP)) {
- ::ShowWindow(mWnd, SW_SHOWNORMAL);
- } else {
- ::ShowWindow(mWnd, SW_SHOWNOACTIVATE);
- // Don't flicker the window if we're restoring session
- if (!sIsRestoringSession) {
- (void)GetAttention(2);
+ // Zen: skip the actual ShowWindow() call when window-control
+ // has the widget locked-hidden; mIsVisible is already set above
+ // so Mozilla's bookkeeping treats the widget as shown.
+ if (!mZenShowLocked) {
+ switch (mFrameState->GetSizeMode()) {
+ case nsSizeMode_Fullscreen:
+ ::ShowWindow(mWnd, SW_SHOW);
+ break;
+ case nsSizeMode_Maximized:
+ ::ShowWindow(mWnd, SW_SHOWMAXIMIZED);
+ break;
+ case nsSizeMode_Minimized:
+ ::ShowWindow(mWnd, SW_SHOWMINIMIZED);
+ break;
+ default:
+ if (CanTakeFocus() &&
+ (!mAlwaysOnTop || mPiPType == PiPType::DocumentPiP)) {
+ ::ShowWindow(mWnd, SW_SHOWNORMAL);
+ } else {
+ ::ShowWindow(mWnd, SW_SHOWNOACTIVATE);
+ // Don't flicker the window if we're restoring session
+ if (!sIsRestoringSession) {
+ (void)GetAttention(2);
+ }
}
- }
- break;
+ break;
+ }
}
if (!mHasBeenShown) {

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

@@ -295,6 +295,7 @@ window.gZenUIManager = {
onFloatingURLBarOpen() {
requestAnimationFrame(() => {
this.updateTabsToolbar();
window.dispatchEvent(new CustomEvent("ZenFloatingURLBarOpened"));
});
},
@@ -607,6 +608,12 @@ window.gZenUIManager = {
gURLBar._zenHandleUrlbarClose = null;
}
window.dispatchEvent(
new CustomEvent("ZenURLBarClosedEarly", {
detail: { onSwitch, onElementPicked },
})
);
const isFocusedBefore = gURLBar.focused;
setTimeout(() => {
// We use this attribute on Tabbrowser::addTab
@@ -951,7 +958,9 @@ window.gZenVerticalTabsManager = {
?.includes("toolbar") ||
document.documentElement
.getAttribute("chromehidden")
?.includes("menubar")
?.includes("menubar") ||
document.documentElement.hasAttribute("zen-little-window") ||
window._zenStartupLittleWindow
);
});
@@ -959,7 +968,18 @@ window.gZenVerticalTabsManager = {
this,
"_canReplaceNewTab",
"zen.urlbar.replace-newtab",
true
true,
null,
val => {
// On little windows, we always want to replace new tabs
if (
window._zenStartupLittleWindow ||
document.documentElement.hasAttribute("zen-little-window")
) {
return true;
}
return val;
}
);
var updateEvent = this._updateEvent.bind(this);
var onPrefChange = this._onPrefChange.bind(this);
@@ -1270,7 +1290,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

@@ -5,6 +5,7 @@
EXTRA_JS_MODULES += [
"sys/ZenActorsManager.sys.mjs",
"sys/ZenCustomizableUI.sys.mjs",
"sys/ZenSearchPopup.sys.mjs",
"sys/ZenUIMigration.sys.mjs",
]

View File

@@ -20,9 +20,13 @@ body,
position: inherit;
}
:root:is([inDOMFullscreen="true"], [chromehidden~="location"], [chromehidden~="toolbar"]) {
:root:is(
[inDOMFullscreen="true"], [chromehidden~="location"],
[chromehidden~="toolbar"], [zen-little-window="true"]
) {
#navigator-toolbox,
#zen-sidebar-splitter {
#zen-sidebar-splitter,
#zen-sidebar-top-buttons {
visibility: collapse;
}
}

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

@@ -3,6 +3,7 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { ZenLittleWindow } from "resource:///modules/zen/ZenLittleWindow.sys.mjs";
export const ZenCustomizableUI = new (class {
constructor() {}
@@ -38,10 +39,15 @@ export const ZenCustomizableUI = new (class {
// We do not have access to the window object here
init(window) {
this.#initLittleWindow(window);
this.#addSidebarButtons(window);
this.#modifyToolbarButtons(window);
}
#initLittleWindow(window) {
ZenLittleWindow.onLittleWindow(window);
}
#addSidebarButtons(window) {
const kDefaultSidebarWidth =
AppConstants.platform === "macosx" ? "230px" : "186px";

View File

@@ -0,0 +1,152 @@
/* 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/. */
/*
* Generic searchable XUL <panel> driver. One instance owns one panel +
* its search input + list + (optional) no-results element, and exposes
* `populate(items)` and `open(anchor, options)`.
*
* Each item passed to `populate` is `{ label, render?, onPick }`:
* - label: string used for the data-label search filter.
* - render: optional () => Element factory. If omitted a bare hbox
* with a <label> is created.
* - onPick: callback invoked when the item is clicked or activated
* via Enter on the keyboard.
*
* The driver handles:
* - filtering by lowercased substring match against data-label;
* - arrow-key / Tab navigation with [selected="true"] highlight;
* - Enter to activate the highlighted item;
* - autofocus of the search input on popupshown;
* - cleanup of all listeners on popuphidden.
*/
export class ZenSearchPopup {
#panel = null;
#searchInput = null;
#list = null;
#noResults = null;
#itemSelector = ".zen-search-popup-item";
#items = [];
/**
* @param {object} aOptions
* @param {Element} aOptions.panel The <panel> XUL element.
* @param {Element} aOptions.searchInput The search <html:input>.
* @param {Element} aOptions.list The container holding items.
* @param {Element} [aOptions.noResults] Optional "no results" element.
* @param {string} [aOptions.itemSelector] Per-item selector. Default
* is `.zen-search-popup-item`; custom items must carry that class
* or override this option.
*/
constructor({ panel, searchInput, list, noResults, itemSelector }) {
this.#panel = panel;
this.#searchInput = searchInput;
this.#list = list;
this.#noResults = noResults;
if (itemSelector) this.#itemSelector = itemSelector;
}
populate(items) {
this.#items = items;
this.#list.innerHTML = "";
const doc = this.#panel.ownerDocument;
for (const item of items) {
let node;
if (typeof item.render === "function") {
node = item.render();
} else {
node = doc.createXULElement("hbox");
const label = doc.createXULElement("label");
label.setAttribute("value", item.label);
node.appendChild(label);
}
node.classList.add(this.#itemSelector.replace(/^\./, ""));
node.setAttribute("data-label", item.label);
node.addEventListener("click", () => {
this.#panel.hidePopup();
item.onPick?.(item);
});
this.#list.appendChild(node);
}
}
open(anchor, { position = "after_end", onShown, onHidden } = {}) {
if (!this.#panel || !this.#list) return;
this.#panel.hidden = false;
if (this.#searchInput) this.#searchInput.value = "";
if (this.#noResults) this.#noResults.hidden = true;
const doc = this.#panel.ownerDocument;
const sel = this.#itemSelector;
const onSearch = () => {
const query = (this.#searchInput?.value || "").toLowerCase();
let visible = 0;
for (const item of this.#list.querySelectorAll(sel)) {
const label = item.getAttribute("data-label")?.toLowerCase() || "";
const found = label.includes(query);
item.hidden = !found;
if (found) visible++;
}
if (this.#noResults) this.#noResults.hidden = visible > 0;
};
if (this.#searchInput) {
this.#searchInput.addEventListener("input", onSearch);
}
const onKeyDown = event => {
if (
event.key === "ArrowDown" ||
event.key === "ArrowUp" ||
event.key === "Tab"
) {
event.preventDefault();
const isUp =
event.key === "ArrowUp" || (event.key === "Tab" && event.shiftKey);
const items = Array.from(this.#list.querySelectorAll(sel)).filter(
it => !it.hidden
);
if (!items.length) return;
let index = items.indexOf(
this.#list.querySelector(`${sel}[selected="true"]`)
);
index = isUp
? (index - 1 + items.length) % items.length
: (index + 1) % items.length;
items.forEach(it => it.removeAttribute("selected"));
const target = items[index];
target.setAttribute("selected", "true");
target.scrollIntoView({ block: "nearest", behavior: "smooth" });
} else if (event.key === "Enter") {
const sel2 = this.#list.querySelector(`${sel}[selected="true"]`);
if (sel2) sel2.click();
}
};
doc.addEventListener("keydown", onKeyDown);
const onPanelShown = event => {
if (event.target !== this.#panel) return;
this.#searchInput?.focus();
this.#searchInput?.select?.();
onShown?.();
};
this.#panel.addEventListener("popupshown", onPanelShown);
const onPanelHidden = event => {
if (event.target !== this.#panel) return;
if (this.#searchInput) {
this.#searchInput.removeEventListener("input", onSearch);
}
doc.removeEventListener("keydown", onKeyDown);
this.#panel.removeEventListener("popupshown", onPanelShown);
this.#panel.removeEventListener("popuphidden", onPanelHidden);
onHidden?.();
};
this.#panel.addEventListener("popuphidden", onPanelHidden);
this.#panel.openPopup(anchor, position);
}
}

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

@@ -3,6 +3,7 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import { nsZenDOMOperatedFeature } from "chrome://browser/content/zen-components/ZenCommonUtils.mjs";
import { ZenSearchPopup } from "resource:///modules/ZenSearchPopup.sys.mjs";
function formatRelativeTime(timestamp) {
const now = Date.now();
@@ -42,6 +43,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
);
#popup = null;
#searchPopup = null;
#popupTimer = null;
#mouseTimer = null;
#lastHighlightedGroup = null;
@@ -188,15 +190,12 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
#initTabsPopup() {
this.#popup = document.getElementById("zen-folder-tabs-popup");
const search = this.#popup.querySelector("#zen-folder-tabs-list-search");
const tabsList = this.#popup.querySelector("#zen-folder-tabs-list");
search.addEventListener("input", () => {
const query = search.value.toLowerCase();
for (const item of tabsList.children) {
item.hidden = !item.getAttribute("data-label").includes(query);
}
this.#searchPopup = new ZenSearchPopup({
panel: this.#popup,
searchInput: this.#popup.querySelector("#zen-folder-tabs-list-search"),
list: this.#popup.querySelector("#zen-folder-tabs-list"),
noResults: document.getElementById("zen-folder-tabs-search-no-results"),
itemSelector: ".folders-tabs-list-item",
});
this.#popup.addEventListener("mouseover", () => {
@@ -788,93 +787,18 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
document.getElementById("zen-folder-tabs-search-no-results").hidden = true;
this.#populateTabsList(activeGroup);
const search = this.#popup.querySelector("#zen-folder-tabs-list-search");
document.l10n.setArgs(search, {
"folder-name": activeGroup.name,
});
const tabsList = this.#popup.querySelector("#zen-folder-tabs-list");
const onSearchInput = () => {
const query = search.value.toLowerCase();
let foundTabs = 0;
for (const item of tabsList.children) {
const found = item.getAttribute("data-label").includes(query);
item.hidden = !found;
if (found) {
foundTabs++;
}
}
document.getElementById("zen-folder-tabs-search-no-results").hidden =
foundTabs > 0;
};
search.addEventListener("input", onSearchInput);
const onKeyDown = event => {
// Arrow down and up to navigate through the list
if (
event.key === "ArrowDown" ||
event.key === "ArrowUp" ||
event.key === "Tab"
) {
event.preventDefault();
let isUp =
event.key === "ArrowUp" || (event.key === "Tab" && event.shiftKey);
const items = Array.from(tabsList.children).filter(
item => !item.hidden
);
if (items.length === 0) {
return;
}
let index = items.indexOf(
tabsList.querySelector(".folders-tabs-list-item[selected]")
);
if (!isUp) {
index = (index + 1) % items.length;
} else {
index = (index - 1 + items.length) % items.length;
}
items.forEach(item => item.removeAttribute("selected"));
const targetItem = items[index];
targetItem.setAttribute("selected", "true");
targetItem.scrollIntoView({ block: "start", behavior: "smooth" });
} else if (event.key === "Enter") {
// Enter to select the currently highlighted item
const highlightedItem = tabsList.querySelector(
".folders-tabs-list-item[selected]"
);
if (highlightedItem) {
highlightedItem.click();
}
}
};
document.addEventListener("keydown", onKeyDown);
document.l10n.setArgs(
this.#popup.querySelector("#zen-folder-tabs-list-search"),
{ "folder-name": activeGroup.name }
);
const target = event.target;
target.setAttribute("open", true);
const handlePopupHidden = event => {
if (event.target !== this.#popup) {
return;
}
search.value = "";
target.removeAttribute("open");
search.removeEventListener("input", onSearchInput);
document.removeEventListener("keydown", onKeyDown);
};
this.#popup.addEventListener(
"popupshown",
() => {
search.focus();
search.select();
},
{ once: true }
);
this.#popup.addEventListener("popuphidden", handlePopupHidden, {
once: true,
this.#searchPopup.open(target, {
position: this.#searchPopupOptions,
onHidden: () => target.removeAttribute("open"),
});
this.#popup.openPopup(target, this.#searchPopupOptions);
}
get #searchPopupOptions() {

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,141 @@
/* 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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { ZenSpacesSearch } from "resource:///modules/zen/ZenSpacesSearch.sys.mjs";
const lazy = {};
XPCOMUtils.defineLazyServiceGetter(
lazy,
"ZenWindowControl",
"@mozilla.org/zen/window-control;1",
Ci.nsIZenWindowControl
);
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 {
init() {}
uninit() {}
/**
* 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;
}
}
let win = opener.OpenBrowserWindow({
zenLittleWindow: true,
all: false,
features: FEATURES,
});
win.windowUtils.suppressAnimation(true);
// Hide the OS-level window until the floating urlbar is ready, so the
// user never sees a half-laid-out chrome flash on top.
lazy.ZenWindowControl.hide(win);
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;
}
}
}
onLittleWindow(win) {
if (!this.#isLittleWindow(win)) {
return;
}
ZenSpacesSearch.init(win);
const observer = new win.ResizeObserver(entries => {
if (win.closed) {
return;
}
for (const entry of entries) {
if (entry.target.id === "urlbar") {
const { width, height } = entry.target.getBoundingClientRect();
win.resizeTo(width, height);
}
}
});
const onClosed = event => {
observer.disconnect();
if (!win.closed && !event.detail?.onElementPicked) {
lazy.ZenWindowControl.hide(win);
win.close();
} else {
const [width, height] = [1000, 600];
win.setResizable(true);
win.resizeTo(1000, 600);
win.docShell.treeOwner
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIAppWindow)
.center(null, true, true)
}
};
const urlbar = win.gURLBar;
observer.observe(urlbar);
// TODO: Handle window blur event
win.setResizable(false);
win.addEventListener(
"ZenFloatingURLBarOpened",
() => {
win.docShell.treeOwner
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIAppWindow)
.center(null, true, true)
if (AppConstants.platform == "macosx" && !Services.focus.activeWindow) {
Cc["@mozilla.org/widget/macdocksupport;1"]
.getService(Ci.nsIMacDockSupport)
.activateApplication(true);
}
win.focus();
urlbar.focus();
},
{ once: true }
);
win.addEventListener("ZenURLBarClosed", onClosed, { once: true });
win.addEventListener("unload", () => observer.disconnect(), { once: true });
// Hacky, but used to prevent flashing and still being able to render
lazy.ZenWindowControl.show(win);
lazy.ZenWindowControl.hide(win);
win.gZenWorkspaces.promiseInitialized.then(() => {
win.windowUtils.suppressAnimation(false);
lazy.ZenWindowControl.show(win);
});
}
}
export const ZenLittleWindow = new nsZenLittleWindow();

View File

@@ -0,0 +1,142 @@
/* 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 { ZenSearchPopup } from "resource:///modules/ZenSearchPopup.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
});
/*
* Owns the "send to a synced window" split-button that lives in the
* nav-bar of a little window. Main click opens the urlbar's current
* value in a fresh synced browser window using the active workspace;
* the dropdown opens a ZenSearchPopup over #zen-spaces-popup so the
* user can pick a different workspace to land in.
*/
class ZenSpacesSearchService {
/**
* Per-window setup.
* @param {Window} aWindow A little window.
*/
init(aWindow) {
if (!aWindow || aWindow._zenSpacesSearchInited) return;
aWindow._zenSpacesSearchInited = true;
const doc = aWindow.document;
const panel = doc.getElementById("zen-spaces-popup");
if (!panel) return;
const popup = new ZenSearchPopup({
panel,
searchInput: doc.getElementById("zen-spaces-list-search"),
list: doc.getElementById("zen-spaces-list"),
noResults: doc.getElementById("zen-spaces-search-no-results"),
itemSelector: ".zen-spaces-list-item",
});
const parts = this.#injectButton(aWindow);
if (!parts) return;
const { button, main, dropmarker } = parts;
main.addEventListener("click", event => {
if (event.button !== 0) return;
this.#openInWorkspace(aWindow, null);
});
dropmarker.addEventListener("click", event => {
if (event.button !== 0) return;
event.stopPropagation();
this.#openSpacesPopup(aWindow, popup, button);
});
}
#injectButton(aWindow) {
const doc = aWindow.document;
const target = doc.getElementById("nav-bar-customization-target");
if (!target) return null;
const button = doc.createXULElement("hbox");
button.id = "zen-little-window-send-to-window";
button.setAttribute("removable", "false");
const main = doc.createXULElement("hbox");
main.classList.add("zen-stw-main");
const prefix = doc.createXULElement("label");
prefix.classList.add("zen-stw-prefix");
prefix.setAttribute(
"data-l10n-id",
"zen-little-window-send-to-window-prefix"
);
const spaceName = doc.createXULElement("label");
spaceName.classList.add("zen-stw-space-name");
spaceName.setAttribute(
"value",
aWindow.gZenWorkspaces?.getActiveWorkspaceFromCache?.()?.name || ""
);
main.appendChild(prefix);
main.appendChild(spaceName);
const separator = doc.createXULElement("hbox");
separator.classList.add("zen-stw-separator");
const dropmarker = doc.createXULElement("hbox");
dropmarker.classList.add("zen-stw-dropmarker");
const dropIcon = doc.createXULElement("image");
dropIcon.classList.add("zen-stw-dropmarker-icon");
dropmarker.appendChild(dropIcon);
button.appendChild(main);
button.appendChild(separator);
button.appendChild(dropmarker);
target.appendChild(button);
return { button, main, dropmarker, spaceName };
}
#openSpacesPopup(aWindow, popup, anchor) {
const workspaces = lazy.ZenSessionStore.getClonedSpaces();
popup.populate(
workspaces.map(space => ({
label: space.name || space.uuid,
render: () => {
const node = aWindow.document.createXULElement("hbox");
const label = aWindow.document.createXULElement("label");
label.setAttribute("value", space.name || space.uuid);
label.classList.add("zen-spaces-list-item-label");
node.appendChild(label);
return node;
},
onPick: () => this.#openInWorkspace(aWindow, space.uuid),
}))
);
popup.open(anchor);
}
#openInWorkspace(aWindow, workspaceUuid) {
const url = aWindow.gURLBar?.value?.trim();
if (!url) return;
const args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
const urlString = Cc["@mozilla.org/supports-string;1"].createInstance(
Ci.nsISupportsString
);
urlString.data = url;
args.appendElement(urlString);
const opts = { args, zenSyncedWindow: true };
if (workspaceUuid) opts.zenInitialWorkspace = workspaceUuid;
const newWin = aWindow.OpenBrowserWindow(opts);
if (newWin) aWindow.close();
}
}
export const ZenSpacesSearch = new ZenSpacesSearchService();

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,8 @@
# 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",
"ZenSpacesSearch.sys.mjs",
]

View File

@@ -0,0 +1,162 @@
/* 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.
*/
#zen-little-window-send-to-window {
align-items: stretch;
border-radius: var(--border-radius-small);
background: light-dark(white, rgba(0, 0, 0, 0.4));
height: 32px;
margin-inline: 4px;
overflow: hidden;
user-select: none;
align-self: center;
box-shadow: 0 0 1px 2px rgba(0, 0, 0, 0.01);
.zen-stw-main,
.zen-stw-dropmarker {
align-items: center;
padding-inline: 12px;
transition: background 80ms ease;
&:hover {
background: var(--toolbarbutton-active-background, color-mix(in srgb, currentColor 12%, transparent));
}
}
.zen-stw-prefix {
color: color-mix(in srgb, currentColor 60%, transparent);
margin-inline-end: 6px;
}
.zen-stw-space-name {
color: var(--zen-primary-color, currentColor);
font-weight: 600;
}
.zen-stw-separator {
width: 1px;
background: color-mix(in srgb, currentColor 18%, transparent);
margin-block: 6px;
}
.zen-stw-dropmarker {
padding-inline: 8px;
}
.zen-stw-dropmarker-icon {
width: 12px;
height: 12px;
-moz-context-properties: fill;
fill: color-mix(in srgb, currentColor 65%, transparent);
list-style-image: url("chrome://global/skin/icons/arrow-down.svg");
}
}
#zen-spaces-popup {
--arrowpanel-padding: 0;
--zen-spaces-list-padding: 6px;
padding: var(--zen-spaces-list-padding);
min-width: 250px;
.zen-spaces-list-header {
display: flex;
flex-direction: row;
padding: 6px;
border-bottom: 1px solid color-mix(in srgb, currentColor, transparent 90%);
}
.zen-spaces-list-search-icon {
width: 14px;
height: 14px;
margin-inline: 4px 6px;
-moz-context-properties: fill;
fill: currentColor;
opacity: 0.6;
}
#zen-spaces-list-search {
flex: 1;
background: transparent;
border: none;
outline: none;
color: inherit;
font: inherit;
}
.zen-spaces-list-item {
padding: 6px 10px;
border-radius: 6px;
cursor: pointer;
&:hover,
&[selected="true"] {
background: var(--toolbarbutton-hover-background);
}
&[active="true"]::after {
content: "•";
margin-inline-start: auto;
opacity: 0.7;
}
}
#zen-spaces-search-no-results {
padding: 12px;
opacity: 0.7;
justify-content: center;
}
}
:root[zen-little-window="true"] {
toolbarspring[cui-areatype="toolbar"],
#nav-bar-customization-target > .toolbarbutton-1[disabled="true"] {
display: none;
}
&[zen-has-empty-tab="true"] {
/* Keep in sync with URLBAR_HEIGHT in ZenLittleWindow.sys.mjs */
--zen-minimum-window-height: 40px;
min-width: unset !important;
#zen-appcontent-wrapper {
visibility: hidden;
}
#urlbar[breakout-extend] {
min-width: 600px !important;
max-width: 600px !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;
}
}
}
@media (-moz-platform: macos) {
#nav-bar {
padding-inline-start: 6px;
}
}
#urlbar-container {
--border-radius-medium: var(--border-radius-small);
}
}

View File

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

View File

@@ -206,6 +206,13 @@ class nsZenWindowSync {
* @param {Window} aWindow - The browser window that is about to be shown.
*/
#onWindowBeforeShow(aWindow) {
if (aWindow._zenStartupLittleWindow) {
aWindow.document.documentElement.setAttribute(
"zen-little-window",
"true"
);
delete aWindow._zenStartupLittleWindow;
}
if (
aWindow.gZenWindowSync ||
aWindow.document.documentElement.hasAttribute("zen-unsynced-window")

View File

@@ -4,4 +4,5 @@
DIRS += [
"common",
"window-control",
]

View File

@@ -0,0 +1,47 @@
/* 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 "ZenWindowControl.h"
#include "WidgetUtils.h"
#include "nsCOMPtr.h"
#include "nsIWidget.h"
#include "nsPIDOMWindow.h"
namespace zen {
NS_IMPL_ISUPPORTS(ZenWindowControl, nsIZenWindowControl)
namespace {
static nsCOMPtr<nsIWidget> WidgetFor(mozIDOMWindowProxy* aWindow) {
if (!aWindow) return nullptr;
nsPIDOMWindowOuter* outer = nsPIDOMWindowOuter::From(aWindow);
if (!outer) return nullptr;
return mozilla::widget::WidgetUtils::DOMWindowToWidget(outer);
}
} // namespace
NS_IMETHODIMP
ZenWindowControl::Hide(mozIDOMWindowProxy* aWindow) {
nsCOMPtr<nsIWidget> widget = WidgetFor(aWindow);
if (!widget) return NS_ERROR_FAILURE;
// Hide first, then arm the lock so any subsequent Show(true) called
// from anywhere in the tree is rejected by the widget itself.
widget->Show(false);
widget->SetZenShowLocked(true);
return NS_OK;
}
NS_IMETHODIMP
ZenWindowControl::Show(mozIDOMWindowProxy* aWindow) {
nsCOMPtr<nsIWidget> widget = WidgetFor(aWindow);
if (!widget) return NS_ERROR_FAILURE;
widget->SetZenShowLocked(false);
widget->Show(true);
return NS_OK;
}
} // namespace zen

View File

@@ -0,0 +1,25 @@
/* 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_ZenWindowControl_h_
#define mozilla_ZenWindowControl_h_
#include "nsIZenWindowControl.h"
namespace zen {
class ZenWindowControl final : public nsIZenWindowControl {
public:
NS_DECL_ISUPPORTS
NS_DECL_NSIZENWINDOWCONTROL
ZenWindowControl() = default;
private:
~ZenWindowControl() = default;
};
} // namespace zen
#endif

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': '{c1d2e3f4-9abc-4def-8123-456789abcdef}',
'interfaces': ['nsIZenWindowControl'],
'contract_ids': ['@mozilla.org/zen/window-control;1'],
'type': 'zen::ZenWindowControl',
'headers': ['mozilla/ZenWindowControl.h'],
'processes': ProcessSelector.MAIN_PROCESS_ONLY,
},
]

View File

@@ -0,0 +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/.
XPIDL_SOURCES += [
"nsIZenWindowControl.idl",
]
EXPORTS.mozilla += [
"ZenWindowControl.h",
]
SOURCES += [
"ZenWindowControl.cpp",
]
XPCOM_MANIFESTS += [
"components.conf",
]
LOCAL_INCLUDES += [
"/widget",
]
FINAL_LIBRARY = "xul"
XPIDL_MODULE = "zen_window_control"

View File

@@ -0,0 +1,34 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
interface mozIDOMWindowProxy;
/**
* @brief Toggle the OS-level visibility of a chrome window without
* destroying it. Used by little-window flow to keep a window off-screen
* while the urlbar is wired up.
*
* Hide() locks the widget at the component level; once locked, the
* widget remains hidden until Show() is called. Calling Show() on a
* widget that is not currently locked is a no-op so external paths
* can't accidentally un-hide a widget that wasn't hidden through this
* service.
*/
[scriptable, uuid(c1d2e3f4-9abc-4def-8123-456789abcdef)]
interface nsIZenWindowControl : nsISupports {
/**
* Hide the window at the widget level
* (NSWindow.orderOut / ShowWindow(SW_HIDE) / gtk_widget_hide) and
* mark its widget as locked-hidden. Idempotent.
*/
void hide(in mozIDOMWindowProxy aWindow);
/**
* If the window's widget is currently locked-hidden by this service,
* un-hide it and clear the lock. Otherwise no-op.
*/
void show(in mozIDOMWindowProxy aWindow);
};