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
77 changed files with 2395 additions and 655 deletions

View File

@@ -129,7 +129,7 @@ jobs:
token: ${{ secrets.DEPLOY_KEY }}
commit-message: "chore: Sync upstream to `Firefox ${{ steps.build-data.outputs.version }}`"
branch: "chore/upstream-sync"
title: "no-bug: Sync upstream Firefox to version ${{ steps.build-data.outputs.version }}"
title: "Sync upstream Firefox to version ${{ steps.build-data.outputs.version }}"
body: |
This PR syncs the upstream Firefox to version ${{ steps.build-data.outputs.version }}.

View File

@@ -34,12 +34,12 @@ Zen is a firefox-based browser with the aim of pushing your productivity to a ne
### Firefox Versions
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `150.0.3`! 🚀
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 150.0.3`!
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `150.0.2`! 🚀
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 150.0.2`!
### Contributing
If you'd like to report a bug, please do so on our [GitHub Issues page](https://github.com/zen-browser/desktop/issues/) and for feature requests, you can use [GitHub Discussions](https://github.com/zen-browser/desktop/discussions).
If you'd like to report a bug, please do so on our [GitHub Issues page](https://github.com/zen-browser/desktop/issues/) and for feature requests, you can use [Github Discussions](https://github.com/zen-browser/desktop/discussions).
Zen is an open-source project, and we welcome contributions from the community! Please take a look at the [contribution guidelines](./docs/contribute.md) before getting started!

View File

@@ -1 +1 @@
69c3d9ff05fd352d761a41ec35f59b06e166e3bc
73901ca17f4a2159dd4488cea8684e9abbfdcc89

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

@@ -27,7 +27,7 @@
"surfer": "surfer",
"test": "python3 scripts/run_tests.py",
"test:dbg": "python3 scripts/run_tests.py --jsdebugger --debug-on-failure",
"ffprefs": "cd tools/ffprefs && cargo run --bin ffprefs -- ../../",
"ffprefs": "${CARGO:-cargo} run --manifest-path tools/ffprefs/Cargo.toml --bin ffprefs -- prefs engine",
"lc": "surfer license-check",
"lc:fix": "surfer license-check --fix",
"use-moz-src": "cd engine && ./mach use-moz-src",

View File

@@ -0,0 +1,16 @@
# 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/.
# Fullscreen API preferences
- name: full-screen-api.transition-duration.enter
value: "0 0"
- name: full-screen-api.transition-duration.leave
value: "0 0"
- name: full-screen-api.warning.delay
value: -1
- name: full-screen-api.warning.timeout
value: 0

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

@@ -18,7 +18,7 @@
value: true
- name: zen.workspaces.swipe-actions.delta-multiplier
value: 100
value: 90
- name: zen.workspaces.switch-animation-duration
value: 200

View File

@@ -60,4 +60,7 @@ def main():
if __name__ == "__main__":
import sys
if len(sys.argv) == 3:
_, DUMPS_FOLDER, ENGINE_DUMPS_FOLDER = sys.argv
main()

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/UrlbarValueFormatter.sys.mjs b/browser/components/urlbar/UrlbarValueFormatter.sys.mjs
index f28d277764158566bc9406ce0e6160d92d346a63..aee17083999f99670c2de7b47cb917e400da86a6 100644
index f28d277764158566bc9406ce0e6160d92d346a63..82dd5f8568ec1b12e87676e5c2b243824628b5af 100644
--- a/browser/components/urlbar/UrlbarValueFormatter.sys.mjs
+++ b/browser/components/urlbar/UrlbarValueFormatter.sys.mjs
@@ -77,7 +77,7 @@ export class UrlbarValueFormatter {
@@ -11,30 +11,7 @@ index f28d277764158566bc9406ce0e6160d92d346a63..aee17083999f99670c2de7b47cb917e4
});
}
@@ -105,6 +105,22 @@ export class UrlbarValueFormatter {
}
#ensureFormattedHostVisible(urlMetaData) {
+ // Make sure the domain is visible even with long subdomains when the single sidebar is being used
+ if (
+ this.#window.gZenVerticalTabsManager?._hasSetSingleToolbar
+ && Services.prefs.getBoolPref("zen.urlbar.show-domain-only-in-sidebar")
+ ) {
+ const hoverAttr = "zen-has-implicit-hover";
+ const hasHover = this.#window.gNavToolbox.hasAttribute(hoverAttr);
+ this.#window.gNavToolbox.removeAttribute(hoverAttr);
+
+ this.#inputField.scrollLeft = this.#inputField.scrollLeftMax;
+
+ if (hasHover) {
+ this.#window.gNavToolbox.setAttribute(hoverAttr, true);
+ }
+ return;
+ }
// Make sure the host is always visible. Since it is aligned on
// the first strong directional character, we set scrollLeft
// appropriately to ensure the domain stays visible in case of an
@@ -371,7 +387,7 @@ export class UrlbarValueFormatter {
@@ -371,7 +371,7 @@ export class UrlbarValueFormatter {
* @returns {boolean}
* True if formatting was applied and false if not.
*/
@@ -43,7 +20,7 @@ index f28d277764158566bc9406ce0e6160d92d346a63..aee17083999f99670c2de7b47cb917e4
let urlMetaData = this.#getUrlMetaData();
if (!urlMetaData) {
return false;
@@ -640,6 +656,7 @@ export class UrlbarValueFormatter {
@@ -640,6 +640,7 @@ export class UrlbarValueFormatter {
this.#window.requestAnimationFrame(() => {
if (instance == this.#resizeInstance) {
this.#ensureFormattedHostVisible();

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

@@ -1,5 +1,5 @@
diff --git a/browser/themes/shared/urlbar-searchbar.css b/browser/themes/shared/urlbar-searchbar.css
index 0ee35cfe67c9bce37a844d4b7d9715d4fb50f709..034cf876c8cef5a2b4c0ca5f209d127dbfc90b20 100644
index 0ee35cfe67c9bce37a844d4b7d9715d4fb50f709..5cdb8037e49a68ea4126af349690af8a16b5e59b 100644
--- a/browser/themes/shared/urlbar-searchbar.css
+++ b/browser/themes/shared/urlbar-searchbar.css
@@ -10,7 +10,7 @@
@@ -20,18 +20,7 @@ index 0ee35cfe67c9bce37a844d4b7d9715d4fb50f709..034cf876c8cef5a2b4c0ca5f209d127d
}
/* Document Picture-in-Picture API window */
@@ -200,6 +200,10 @@ toolbar[inactive="true"] .urlbar,
.urlbar:not([focused])[textoverflow="left"][domaindir="rtl"] > .urlbar-input-container > .urlbar-input-box > & {
mask-image: linear-gradient(to right, transparent var(--urlbar-scheme-size), black calc(var(--urlbar-scheme-size) + 3ch));
}
+
+ #navigator-toolbox[zen-has-implicit-hover="true"] .urlbar:not([focused])[textoverflow="left"] > .urlbar-input-container > .urlbar-input-box > & {
+ mask-image: linear-gradient(to right, transparent, black 3ch, black calc(100% - 3ch), transparent);
+ }
}
#urlbar-scheme {
@@ -333,10 +337,14 @@ toolbar[inactive="true"] .urlbar,
@@ -333,10 +333,14 @@ toolbar[inactive="true"] .urlbar,
.urlbar[breakout][breakout-extend] {
height: auto;

View File

@@ -385,12 +385,12 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
+ preferredEdge:preferredEdge
+ hiddenAnchor:shouldHideAnchor];
+ SyncPopoverBounds([(PopupWindow*)mWindow popover], popupFrame);
+
+
+
+ if (mPopupLevel == PopupLevel::Parent) {
+ [nativeParentWindow addChildWindow:mWindow ordered:NSWindowAbove];
+ }
+
+ // Exit early here since the popover is now shown.
+
+ mWindow.isBeingShown = NO;
+ return;
+ }
// If our popup window is a non-native context menu, tell the OS (and

View File

@@ -1,51 +0,0 @@
diff --git a/widget/cocoa/nsCocoaWindow.h b/widget/cocoa/nsCocoaWindow.h
--- a/widget/cocoa/nsCocoaWindow.h
+++ b/widget/cocoa/nsCocoaWindow.h
@@ -140,10 +140,11 @@
// NSPopover support for native appearance
NSPopover* mPopover;
NSViewController* mPopoverViewController;
BOOL mUsePopover;
+ NSRect mLastPopoverRect;
}
- (id)initWithContentRect:(NSRect)contentRect
styleMask:(NSUInteger)styleMask
backing:(NSBackingStoreType)bufferingType
diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
--- a/widget/cocoa/nsCocoaWindow.mm
+++ b/widget/cocoa/nsCocoaWindow.mm
@@ -5597,11 +5597,10 @@
return false;
}
nsMenuPopupFrame* popupFrame = GetPopupFrame();
return [mWindow isKindOfClass:[PopupWindow class]] &&
[(PopupWindow*)mWindow usePopover] && popupFrame &&
- popupFrame->ShouldFollowAnchor() &&
!popupFrame->PopupElement().GetBoolAttr(nsGkAtoms::nonnativepopover);
}
TransparencyMode nsCocoaWindow::GetTransparencyMode() {
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
@@ -8775,10 +8774,19 @@
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
if (!mPopover) {
return;
}
+ if (!positioningRect.size.width && !positioningRect.size.height &&
+ mLastPopoverRect.size.width && mLastPopoverRect.size.height) {
+ // Sometimes, specially when downloading addons from unofficial sources,
+ // the popover is shown in a very fast motion and the positioningRect is
+ // empty.
+ positioningRect = mLastPopoverRect;
+ }
+ mLastPopoverRect = positioningRect;
+
// Close existing popover if it's already shown
if (mPopover.shown) {
[mPopover close];
}

View File

@@ -4,10 +4,7 @@
[
{
"type": "phabricator",
"ids": [
"D284084",
"D299584"
],
"id": "D284084",
"name": "Native MacOS popovers",
"replaces": {
// Specifically trying to target FeatureCallout.sys.mjs's change.
@@ -25,9 +22,7 @@
" #include \"nsISupportsPrimitives.h\"\n": "",
" Atom(\"nonnative\", \"nonnative\"),\n": "",
"Atom(\"noscript\", \"noscript\"),": "Atom(\"noscript\", \"noscript\"),\n Atom(\"noshade\", \"noshade\"),",
"GetBoolAttr(nsGkAtoms::nonnative)": "GetBoolAttr(nsGkAtoms::nonnativepopover)"
"Atom(\"noscript\", \"noscript\"),": "Atom(\"noscript\", \"noscript\"),\n Atom(\"noshade\", \"noshade\"),"
}
},
{

View File

@@ -1,5 +1,5 @@
diff --git a/widget/SwipeTracker.cpp b/widget/SwipeTracker.cpp
index 887d06d3bd9cdaa934880e0ae7a11ec8b737fb61..979f93ddab0d661ac1a1d73e7a0ba27fa2c8f9b7 100644
index 887d06d3bd9cdaa934880e0ae7a11ec8b737fb61..7225d4264c9ec71ef1e5e717a1a62c4ef0aff0b7 100644
--- a/widget/SwipeTracker.cpp
+++ b/widget/SwipeTracker.cpp
@@ -3,6 +3,7 @@
@@ -20,15 +20,7 @@ index 887d06d3bd9cdaa934880e0ae7a11ec8b737fb61..979f93ddab0d661ac1a1d73e7a0ba27f
// gestureAmount needs to stay between -1 and 0 when swiping right and
// between 0 and 1 when swiping left.
double min =
@@ -84,13 +88,14 @@ bool SwipeTracker::ComputeSwipeSuccess() const {
// If the fingers were moving away from the target direction when they were
// lifted from the touchpad, abort the swipe.
if (mCurrentVelocity * targetValue <
- -StaticPrefs::widget_swipe_velocity_twitch_tolerance()) {
+ -StaticPrefs::widget_swipe_velocity_twitch_tolerance()
+ && !StaticPrefs::zen_swipe_is_fast_swipe()) {
return false;
}
@@ -90,7 +94,7 @@ bool SwipeTracker::ComputeSwipeSuccess() const {
return (mGestureAmount * targetValue +
mCurrentVelocity * targetValue *
@@ -37,7 +29,7 @@ index 887d06d3bd9cdaa934880e0ae7a11ec8b737fb61..979f93ddab0d661ac1a1d73e7a0ba27f
kSwipeSuccessThreshold;
}
@@ -141,7 +146,8 @@ nsEventStatus SwipeTracker::ProcessEvent(
@@ -141,7 +145,8 @@ nsEventStatus SwipeTracker::ProcessEvent(
// display the UI as if we were at the success threshold as that would
// give a false indication that navigation would happen.
if (!computedSwipeSuccess && (eventAmount >= kSwipeSuccessThreshold ||

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
@@ -698,6 +705,11 @@ window.gZenUIManager = {
this.urlbarShowDomainOnly
) {
let url = BrowserUIUtils.removeSingleTrailingSlashFromURL(aURL);
requestIdleCallback(() => {
// Scroll the urlbar all the way to the right so that
// the domain is always visible when the url is too long.
gURLBar.inputField.scrollLeft = gURLBar.inputField.scrollWidth;
});
let stripped = url.startsWith("https://") ? url.split("/")[2] : url;
if (stripped.startsWith("www.")) {
stripped = stripped.substring(4);
@@ -946,7 +958,9 @@ window.gZenVerticalTabsManager = {
?.includes("toolbar") ||
document.documentElement
.getAttribute("chromehidden")
?.includes("menubar")
?.includes("menubar") ||
document.documentElement.hasAttribute("zen-little-window") ||
window._zenStartupLittleWindow
);
});
@@ -954,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);
@@ -1265,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;
}
}
@@ -45,7 +49,6 @@ body,
overflow: clip;
isolation: isolate;
contain: content;
&::after,
&::before {
@@ -54,7 +57,6 @@ body,
inset: 0;
z-index: 0;
pointer-events: none;
will-change: background-color;
}
&::after {
@@ -108,6 +110,8 @@ body,
}
#zen-browser-background {
contain: paint;
/* This is conceptually a background, but putting this on a pseudo-element
* avoids it from suppressing the chrome-content separator border, etc.
*

View File

@@ -120,7 +120,6 @@
#urlbar .urlbar-input {
border-radius: 0 !important;
color: color-mix(in srgb, var(--input-color, FieldText) 70%, transparent);
}
#urlbar .urlbar-input-box {

View File

@@ -4,55 +4,48 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#zen-overflow-extensions-list {
display: none;
#zen-overflow-extensions-list:not(:empty) {
--uei-icon-size: 14px;
display: grid;
gap: 8px;
padding: 8px 2px;
padding-bottom: 0;
border-radius: calc(var(--border-radius-medium) - 4px);
grid-template-columns: repeat(auto-fit, minmax(32px, 1fr));
:root[zen-single-toolbar="true"] &:not(:empty) {
--uei-icon-size: 14px;
display: grid;
gap: 8px;
padding: 8px 2px;
padding-bottom: 0;
border-radius: calc(var(--border-radius-medium) - 4px);
grid-template-columns: repeat(auto-fit, minmax(32px, 1fr));
& .unified-extensions-item {
flex: 1;
margin: 0;
}
& .unified-extensions-item {
flex: 1;
margin: 0;
}
& .toolbarbutton-badge-stack {
margin: 0;
width: 100%;
height: 100%;
padding: 8px 0;
justify-content: center;
align-items: center;
}
& .toolbarbutton-badge-stack {
margin: 0;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
}
& .unified-extensions-item-action-button {
appearance: none;
background-color: var(--zen-toolbar-element-bg);
height: 30px;
margin: 0;
justify-content: center;
align-items: center;
border-radius: var(--border-radius-medium);
overflow: clip;
padding: 0;
& .unified-extensions-item-action-button {
appearance: none;
background-color: var(--zen-toolbar-element-bg);
height: 26px;
margin: 0;
justify-content: center;
align-items: center;
border-radius: calc(var(--border-radius-medium) - 2px);
overflow: visible;
padding: 0;
&:hover {
background-color: var(--zen-toolbar-element-bg-hover);
}
& .toolbarbutton-badge {
transform: translate(4px, 2px);
}
}
.unified-extensions-item-contents,
.unified-extensions-item-menu-button,
unified-extensions-item-messagebar-wrapper {
display: none;
&:hover {
background-color: var(--zen-toolbar-element-bg-hover);
}
}
.unified-extensions-item-contents,
.unified-extensions-item-menu-button,
unified-extensions-item-messagebar-wrapper {
display: none;
}
}

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

@@ -816,12 +816,14 @@ class nsZenWorkspaces {
let removedEmptyTab = false;
let initialTabWasEmpty = false;
if (this._shouldOverrideTabs) {
let initialTab = this._initialTab || gBrowser.selectedTab;
initialTabWasEmpty = !!initialTab._veryPossiblyEmpty;
gBrowser.selectedTab = initialTab;
this.moveTabToWorkspace(initialTab, this.activeWorkspace);
gBrowser.moveTabTo(initialTab, {
if (
this._initialTab &&
!(this._initialTab._shouldRemove && this._initialTab._veryPossiblyEmpty)
) {
initialTabWasEmpty = !!this._initialTab._veryPossiblyEmpty;
gBrowser.selectedTab = this._initialTab;
this.moveTabToWorkspace(this._initialTab, this.activeWorkspace);
gBrowser.moveTabTo(this._initialTab, {
forceUngrouped: true,
tabIndex: 0,
});
@@ -1983,41 +1985,26 @@ class nsZenWorkspaces {
);
const offset = -(newWorkspaceIndex - elementWorkspaceIndex) * 100;
const newTransform = `translateX(${offset}%)`;
// Only animate the workspace that is coming in, to avoid having multiple workspaces
// animating off-screen at the same time which can cause performance issues. With an off
// set of 1 or -1, so we animate the current workspace and the next one.
const goingLeft = newWorkspaceIndex < previousWorkspaceIndex;
const willBeVisible =
(goingLeft &&
elementWorkspaceIndex >= newWorkspaceIndex &&
elementWorkspaceIndex <= previousWorkspaceIndex) ||
(!goingLeft &&
elementWorkspaceIndex <= newWorkspaceIndex &&
elementWorkspaceIndex >= previousWorkspaceIndex);
if (shouldAnimate) {
if (!willBeVisible) {
element.style.transform = newTransform;
} else {
const existingPaddingTop = element.style.paddingTop;
animations.push(
gZenUIManager.motion.animate(
element,
{
transform: existingTransform
? [existingTransform, newTransform]
: newTransform,
paddingTop: existingTransform
? [existingPaddingTop, existingPaddingTop]
: existingPaddingTop,
},
{
type: "spring",
bounce: 0,
duration: kGlobalAnimationDuration,
}
)
);
}
const existingPaddingTop = element.style.paddingTop;
animations.push(
gZenUIManager.motion.animate(
element,
{
transform: existingTransform
? [existingTransform, newTransform]
: newTransform,
paddingTop: existingTransform
? [existingPaddingTop, existingPaddingTop]
: existingPaddingTop,
},
{
type: "spring",
bounce: 0,
duration: kGlobalAnimationDuration,
}
)
);
}
element.active = offset === 0;
if (offset === 0) {

View File

@@ -20,7 +20,22 @@ export class ZenSpacesSwipe {
};
constructor() {
this.#attachWorkspaceSwipeGestures(gNavToolbox);
const elements = [
gNavToolbox,
// Event handlers do not work on elements inside shadow DOM
// so we need to attach them directly.
document
.getElementById("tabbrowser-arrowscrollbox")
?.shadowRoot?.querySelector("scrollbox"),
];
for (const element of elements) {
if (!element) {
continue;
}
this._attachWorkspaceSwipeGestures(element);
}
this._popupOpenHandler = this._popupOpenHandler.bind(this);
}
@@ -35,7 +50,7 @@ export class ZenSpacesSwipe {
);
}
#attachWorkspaceSwipeGestures(element) {
_attachWorkspaceSwipeGestures(element) {
element.addEventListener(
"MozSwipeGestureMayStart",
this._handleSwipeMayStart.bind(this),

View File

@@ -11,6 +11,7 @@
align-items: center;
display: flex;
font-size: x-small;
margin: 0 3px;
padding: 0;
position: relative;
@@ -346,12 +347,8 @@ zen-workspace {
overflow-y: auto;
}
:root[swipe-gesture] &{
pointer-events: none;
&::part(scrollbox) {
scrollbar-width: none;
}
:root[swipe-gesture] &::part(scrollbox) {
scrollbar-width: none;
}
&[overflowing] {

View File

@@ -969,14 +969,19 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
};
_oppositeSide(side) {
const OPPOSITE_SIDES = {
top: "bottom",
bottom: "top",
left: "right",
right: "left",
};
return OPPOSITE_SIDES[side];
if (side === "top") {
return "bottom";
}
if (side === "bottom") {
return "top";
}
if (side === "left") {
return "right";
}
if (side === "right") {
return "left";
}
return undefined;
}
calculateHoverSide(x, y, elementRect) {

View File

@@ -335,7 +335,7 @@
#tabbrowser-tabs:not([movingtab])
&:active:not(:has(.tab-content > image:active)) {
scale: var(--zen-active-tab-scale);
rotate: 0.01deg; /* Subtle rotation to trigger GPU acceleration and prevent blurriness */
rotate: 0.02deg; /* Subtle rotation to trigger GPU acceleration and prevent blurriness */
}
:root:not([zen-renaming-tab="true"])
@@ -1326,7 +1326,6 @@
padding: 0;
outline: none !important;
border-radius: 0;
width: 100%;
}
/* ==========================================================================

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

View File

@@ -5,8 +5,8 @@
"binaryName": "zen",
"version": {
"product": "firefox",
"version": "150.0.3",
"candidate": "150.0.3",
"version": "150.0.2",
"candidate": "150.0.2",
"candidateBuild": 1
},
"buildOptions": {
@@ -20,7 +20,7 @@
"brandShortName": "Zen",
"brandFullName": "Zen Browser",
"release": {
"displayVersion": "1.19.13b",
"displayVersion": "1.19.12b",
"github": {
"repo": "zen-browser/desktop"
},

View File

@@ -107,9 +107,9 @@ use std::env;
use std::fs;
use std::path::PathBuf;
const STATIC_PREFS: &str = "../engine/modules/libpref/init/zen-static-prefs.inc";
const FIREFOX_PREFS: &str = "../engine/browser/app/profile/firefox.js";
const DYNAMIC_PREFS: &str = "../engine/browser/app/profile/zen.js";
const STATIC_PREFS: &str = "modules/libpref/init/zen-static-prefs.inc";
const FIREFOX_PREFS: &str = "browser/app/profile/firefox.js";
const DYNAMIC_PREFS: &str = "browser/app/profile/zen.js";
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Preference {
@@ -124,12 +124,6 @@ struct Preference {
sticky: Option<bool>,
}
fn get_config_path() -> PathBuf {
let mut path = env::current_dir().expect("Failed to get current directory");
path.push("prefs");
path
}
fn ordered_prefs(mut prefs: Vec<Preference>) -> Vec<Preference> {
// Sort preferences by name
prefs.sort_by(|a, b| a.name.cmp(&b.name));
@@ -153,11 +147,10 @@ fn get_prefs_files_recursively(dir: &PathBuf, files: &mut Vec<PathBuf>) {
}
}
fn load_preferences() -> Vec<Preference> {
fn load_preferences(prefs_path: &PathBuf) -> Vec<Preference> {
let mut prefs = Vec::new();
let config_path = get_config_path();
let mut pref_files = Vec::new();
get_prefs_files_recursively(&config_path, &mut pref_files);
get_prefs_files_recursively(&prefs_path, &mut pref_files);
for file_path in pref_files {
let content = fs::read_to_string(&file_path).expect("Failed to read file");
let mut parsed_prefs: Vec<Preference> =
@@ -263,14 +256,9 @@ fn get_value(pref: &Preference) -> String {
}
}
fn write_preferences(prefs: &[Preference]) {
let config_path = get_config_path();
if !config_path.exists() {
fs::create_dir_all(&config_path).expect("Failed to create prefs directory");
}
let static_prefs_path = config_path.join(STATIC_PREFS);
let dynamic_prefs_path = config_path.join(DYNAMIC_PREFS);
fn write_preferences(engine_path: &PathBuf, prefs: &[Preference]) {
let static_prefs_path = engine_path.join(STATIC_PREFS);
let dynamic_prefs_path = engine_path.join(DYNAMIC_PREFS);
println!(
"Writing preferences to:\n Static: {}\n Dynamic: {}",
static_prefs_path.display(),
@@ -300,10 +288,10 @@ fn write_preferences(prefs: &[Preference]) {
fs::write(&dynamic_prefs_path, dynamic_content).expect("Failed to write dynamic prefs");
}
fn prepare_zen_prefs() {
fn prepare_zen_prefs(engine_path: &PathBuf) {
// Add `#include zen.js` to the bottom of the firefox.js file if it doesn't exist
let line = "#include zen.js";
let firefox_prefs_path = get_config_path().join(FIREFOX_PREFS);
let firefox_prefs_path = engine_path.join(FIREFOX_PREFS);
if let Ok(mut content) = fs::read_to_string(&firefox_prefs_path) {
if !content.contains(line) {
content.push_str(format!("\n{}\n", line).as_str());
@@ -351,15 +339,19 @@ fn expand_pref_values(prefs: &mut [Preference]) {
fn main() {
let args: Vec<String> = env::args().collect();
let root_path = if args.len() > 1 {
let prefs_path = if args.len() > 1 {
PathBuf::from(&args[1])
} else {
env::current_dir().expect("Failed to get current directory")
PathBuf::from("prefs")
};
let engine_path = if args.len() > 2 {
PathBuf::from(&args[2])
} else {
PathBuf::from("engine")
};
env::set_current_dir(&root_path).expect("Failed to change directory");
prepare_zen_prefs();
let mut preferences = load_preferences();
prepare_zen_prefs(&engine_path);
let mut preferences = load_preferences(&prefs_path);
expand_pref_values(&mut preferences);
write_preferences(&preferences);
write_preferences(&engine_path, &preferences);
}