From 9ce963f1cd88f4a2198d785e399cb2b833a47686 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Sat, 13 Sep 2025 01:35:38 +0200 Subject: [PATCH] feat: New urlbar actions, c=common, p=#10340 --- prefs/zen-urlbar.yaml | 5 + .../base/content/zen-assets.jar.inc.mn | 1 - .../components/BrowserGlue-sys-mjs.patch | 4 +- src/zen/@types/lib.gecko.darwin.d.ts | 3 + src/zen/@types/lib.gecko.dom.d.ts | 3 + src/zen/@types/lib.gecko.glean.d.ts | 3 + src/zen/@types/lib.gecko.linux.d.ts | 3 + src/zen/@types/lib.gecko.modules.d.ts | 3 + src/zen/@types/lib.gecko.nsresult.d.ts | 3 + src/zen/@types/lib.gecko.services.d.ts | 3 + src/zen/@types/lib.gecko.tweaks.d.ts | 3 + src/zen/@types/lib.gecko.win32.d.ts | 3 + src/zen/@types/lib.gecko.xpcom.d.ts | 3 + src/zen/@types/lib.gecko.xpidl.d.ts | 3 + src/zen/common/ZenUIManager.mjs | 8 + ...UIMigration.mjs => ZenUIMigration.sys.mjs} | 0 src/zen/common/moz.build | 1 + src/zen/common/styles/zen-urlbar.css | 45 ++- src/zen/images/favicons/calendar.svg | 5 + src/zen/images/favicons/discord.svg | 5 + src/zen/images/favicons/figma.svg | 5 + src/zen/images/favicons/github.svg | 5 + src/zen/images/favicons/notion.svg | 5 + src/zen/images/favicons/obsidian.svg | 5 + src/zen/images/favicons/reddit.svg | 5 + src/zen/images/favicons/slack.svg | 5 + src/zen/images/favicons/trello.svg | 5 + src/zen/images/favicons/x.svg | 5 + src/zen/kbs/ZenKeyboardShortcuts.mjs | 65 +++- src/zen/moz.build | 2 +- src/zen/tests/moz.build | 1 + src/zen/tests/ub-actions/browser.toml | 5 + .../ub-actions/browser_ub_actions_search.js | 23 ++ src/zen/urlbar/ZenUBActionsProvider.sys.mjs | 277 ++++++++++++++++++ src/zen/urlbar/ZenUBGlobalActions.sys.mjs | 17 ++ src/zen/urlbar/ZenUBProvider.sys.mjs | 16 + src/zen/urlbar/moz.build | 9 + 37 files changed, 541 insertions(+), 21 deletions(-) rename src/zen/common/{ZenUIMigration.mjs => ZenUIMigration.sys.mjs} (100%) create mode 100644 src/zen/tests/ub-actions/browser.toml create mode 100644 src/zen/tests/ub-actions/browser_ub_actions_search.js create mode 100644 src/zen/urlbar/ZenUBActionsProvider.sys.mjs create mode 100644 src/zen/urlbar/ZenUBGlobalActions.sys.mjs create mode 100644 src/zen/urlbar/ZenUBProvider.sys.mjs create mode 100644 src/zen/urlbar/moz.build diff --git a/prefs/zen-urlbar.yaml b/prefs/zen-urlbar.yaml index 9e6b435f3..b9b0533d4 100644 --- a/prefs/zen-urlbar.yaml +++ b/prefs/zen-urlbar.yaml @@ -22,3 +22,8 @@ - name: zen.urlbar.enable-overrides value: false + +# Mark: Zen suggestions controls + +- name: zen.urlbar.suggestions.quick-actions + value: true diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn index 7fe689ff0..67e089715 100644 --- a/src/browser/base/content/zen-assets.jar.inc.mn +++ b/src/browser/base/content/zen-assets.jar.inc.mn @@ -8,7 +8,6 @@ content/browser/zen-sets.js (../../zen/common/zen-sets.js) content/browser/ZenUIManager.mjs (../../zen/common/ZenUIManager.mjs) content/browser/zen-components/ZenActorsManager.mjs (../../zen/common/ZenActorsManager.mjs) - content/browser/zen-components/ZenUIMigration.mjs (../../zen/common/ZenUIMigration.mjs) content/browser/zen-components/ZenCommonUtils.mjs (../../zen/common/ZenCommonUtils.mjs) content/browser/zen-components/ZenSessionStore.mjs (../../zen/common/ZenSessionStore.mjs) content/browser/zen-components/ZenEmojisData.min.mjs (../../zen/common/emojis/ZenEmojisData.min.mjs) diff --git a/src/browser/components/BrowserGlue-sys-mjs.patch b/src/browser/components/BrowserGlue-sys-mjs.patch index ba2d411ef..b7d6408ac 100644 --- a/src/browser/components/BrowserGlue-sys-mjs.patch +++ b/src/browser/components/BrowserGlue-sys-mjs.patch @@ -1,12 +1,12 @@ diff --git a/browser/components/BrowserGlue.sys.mjs b/browser/components/BrowserGlue.sys.mjs -index eae3ed9518ad9ce2103bb912963465c1b10ac050..51859cd542bc69f524d891a67e725c30d4cf14bb 100644 +index eae3ed9518ad9ce2103bb912963465c1b10ac050..ccbb04cd36fd8fd63fd8c9ebd0b51f0a5966829c 100644 --- a/browser/components/BrowserGlue.sys.mjs +++ b/browser/components/BrowserGlue.sys.mjs @@ -8,6 +8,7 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { -+ gZenUIMigration: "chrome://browser/content/zen-components/ZenUIMigration.mjs", ++ gZenUIMigration: "resource:///modules/ZenUIMigration.sys.mjs", AboutHomeStartupCache: "resource:///modules/AboutHomeStartupCache.sys.mjs", AWToolbarButton: "resource:///modules/aboutwelcome/AWToolbarUtils.sys.mjs", ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs", diff --git a/src/zen/@types/lib.gecko.darwin.d.ts b/src/zen/@types/lib.gecko.darwin.d.ts index 400ad03f7..9fbe0954c 100644 --- a/src/zen/@types/lib.gecko.darwin.d.ts +++ b/src/zen/@types/lib.gecko.darwin.d.ts @@ -1,3 +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/. /** * NOTE: Do not modify this file by hand. * Content was generated from source XPCOM .idl files. diff --git a/src/zen/@types/lib.gecko.dom.d.ts b/src/zen/@types/lib.gecko.dom.d.ts index 024b28296..765ef7ca1 100644 --- a/src/zen/@types/lib.gecko.dom.d.ts +++ b/src/zen/@types/lib.gecko.dom.d.ts @@ -1,3 +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/. /** * NOTE: Do not modify this file by hand. * Content was generated from source .webidl files. diff --git a/src/zen/@types/lib.gecko.glean.d.ts b/src/zen/@types/lib.gecko.glean.d.ts index 522b87c05..fc46b3b26 100644 --- a/src/zen/@types/lib.gecko.glean.d.ts +++ b/src/zen/@types/lib.gecko.glean.d.ts @@ -1,3 +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/. /** * NOTE: Do not modify this file by hand. * Content was generated from glean .yaml files. diff --git a/src/zen/@types/lib.gecko.linux.d.ts b/src/zen/@types/lib.gecko.linux.d.ts index 78f933974..d38d49d38 100644 --- a/src/zen/@types/lib.gecko.linux.d.ts +++ b/src/zen/@types/lib.gecko.linux.d.ts @@ -1,3 +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/. /** * NOTE: Do not modify this file by hand. * Content was generated from source XPCOM .idl files. diff --git a/src/zen/@types/lib.gecko.modules.d.ts b/src/zen/@types/lib.gecko.modules.d.ts index dc9bafaab..39e78034a 100644 --- a/src/zen/@types/lib.gecko.modules.d.ts +++ b/src/zen/@types/lib.gecko.modules.d.ts @@ -1,3 +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/. /** * NOTE: Do not modify this file by hand. * Content was generated by running "mach ts paths". diff --git a/src/zen/@types/lib.gecko.nsresult.d.ts b/src/zen/@types/lib.gecko.nsresult.d.ts index 7c810658d..615709a0d 100644 --- a/src/zen/@types/lib.gecko.nsresult.d.ts +++ b/src/zen/@types/lib.gecko.nsresult.d.ts @@ -1,3 +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/. /** * NOTE: Do not modify this file by hand. * Content was generated from xpc.msg and error_list.json. diff --git a/src/zen/@types/lib.gecko.services.d.ts b/src/zen/@types/lib.gecko.services.d.ts index 149327c68..35c3182ff 100644 --- a/src/zen/@types/lib.gecko.services.d.ts +++ b/src/zen/@types/lib.gecko.services.d.ts @@ -1,3 +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/. /** * NOTE: Do not modify this file by hand. * Content was generated from services.json. diff --git a/src/zen/@types/lib.gecko.tweaks.d.ts b/src/zen/@types/lib.gecko.tweaks.d.ts index d2474244d..3f3392bf2 100644 --- a/src/zen/@types/lib.gecko.tweaks.d.ts +++ b/src/zen/@types/lib.gecko.tweaks.d.ts @@ -1,3 +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/. /** * Gecko generic/specialized adjustments for xpcom and webidl types. */ diff --git a/src/zen/@types/lib.gecko.win32.d.ts b/src/zen/@types/lib.gecko.win32.d.ts index a9f74a6a9..2a8cddafd 100644 --- a/src/zen/@types/lib.gecko.win32.d.ts +++ b/src/zen/@types/lib.gecko.win32.d.ts @@ -1,3 +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/. /** * NOTE: Do not modify this file by hand. * Content was generated from source XPCOM .idl files. diff --git a/src/zen/@types/lib.gecko.xpcom.d.ts b/src/zen/@types/lib.gecko.xpcom.d.ts index 723b6ea97..3786f6fbc 100644 --- a/src/zen/@types/lib.gecko.xpcom.d.ts +++ b/src/zen/@types/lib.gecko.xpcom.d.ts @@ -1,3 +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/. /** * NOTE: Do not modify this file by hand. * Content was generated from source XPCOM .idl files. diff --git a/src/zen/@types/lib.gecko.xpidl.d.ts b/src/zen/@types/lib.gecko.xpidl.d.ts index 97e0a04c9..7ab726f19 100644 --- a/src/zen/@types/lib.gecko.xpidl.d.ts +++ b/src/zen/@types/lib.gecko.xpidl.d.ts @@ -1,3 +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/. /** * Gecko XPIDL base types. */ diff --git a/src/zen/common/ZenUIManager.mjs b/src/zen/common/ZenUIManager.mjs index b61f74794..365e13fee 100644 --- a/src/zen/common/ZenUIManager.mjs +++ b/src/zen/common/ZenUIManager.mjs @@ -75,6 +75,14 @@ var gZenUIManager = { this._initCreateNewPopup(); this._debloatContextMenus(); + this._initOmniboxCommands(); + }, + + _initOmniboxCommands() { + const { registerZenUrlbarProviders } = ChromeUtils.importESModule( + 'resource:///modules/ZenUBProvider.sys.mjs' + ); + registerZenUrlbarProviders(); }, _debloatContextMenus() { diff --git a/src/zen/common/ZenUIMigration.mjs b/src/zen/common/ZenUIMigration.sys.mjs similarity index 100% rename from src/zen/common/ZenUIMigration.mjs rename to src/zen/common/ZenUIMigration.sys.mjs diff --git a/src/zen/common/moz.build b/src/zen/common/moz.build index 86c57726c..2de29514c 100644 --- a/src/zen/common/moz.build +++ b/src/zen/common/moz.build @@ -4,4 +4,5 @@ EXTRA_JS_MODULES += [ "ZenCustomizableUI.sys.mjs", + "ZenUIMigration.sys.mjs", ] diff --git a/src/zen/common/styles/zen-urlbar.css b/src/zen/common/styles/zen-urlbar.css index 7b71af908..fe49658f8 100644 --- a/src/zen/common/styles/zen-urlbar.css +++ b/src/zen/common/styles/zen-urlbar.css @@ -61,8 +61,11 @@ } #urlbar:not([breakout-extend='true']) { - :root:not([supress-primary-adjustment]) & .urlbar-background { + & .urlbar-background { transition: background-color 0.15s ease; + :root[supress-primary-adjustment] & { + transition: none !important; + } } &:hover .urlbar-background { @@ -559,6 +562,22 @@ button.popup-notification-dropmarker { border-top: none !important; } +.urlbarView-shortcutContent { + border-radius: 4px; + padding: 6px 8px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + margin-left: auto; + margin-top: auto; + margin-bottom: auto; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1); + + &:empty { + display: none !important; + } +} + .urlbarView-row[has-action]:is([type='switchtab'], [type='remotetab'], [type='clipboard']) { & .urlbarView-action:last-child { margin-left: auto !important; @@ -616,15 +635,14 @@ button.popup-notification-dropmarker { --urlbarView-item-inline-padding: 8px; --urlbarView-item-block-padding: 10px; - &:hover { - .urlbarView-favicon, - & { - background-color: color-mix( - in srgb, - var(--zen-branding-bg-reverse) 5%, - transparent 95% - ) !important; - } + &:hover.urlbarView-favicon, + &:hover, + & .urlbarView-shortcutContent { + background-color: color-mix( + in srgb, + var(--zen-branding-bg-reverse) 5%, + transparent 95% + ) !important; } &[selected] { @@ -643,7 +661,12 @@ button.popup-notification-dropmarker { fill: black !important; } - & .urlbarView-favicon { + & .urlbarView-shortcutContent { + color: black !important; + } + + & .urlbarView-favicon, + & .urlbarView-shortcutContent { background-color: rgba(255, 255, 255, 0.9) !important; } } diff --git a/src/zen/images/favicons/calendar.svg b/src/zen/images/favicons/calendar.svg index fd4edd73a..15e7f3ba7 100644 --- a/src/zen/images/favicons/calendar.svg +++ b/src/zen/images/favicons/calendar.svg @@ -1,3 +1,8 @@ + diff --git a/src/zen/images/favicons/discord.svg b/src/zen/images/favicons/discord.svg index 5b5e3df23..bb16ea0c6 100644 --- a/src/zen/images/favicons/discord.svg +++ b/src/zen/images/favicons/discord.svg @@ -1,3 +1,8 @@ + diff --git a/src/zen/images/favicons/figma.svg b/src/zen/images/favicons/figma.svg index ed566dcc1..5f400059b 100644 --- a/src/zen/images/favicons/figma.svg +++ b/src/zen/images/favicons/figma.svg @@ -1,3 +1,8 @@ + diff --git a/src/zen/images/favicons/github.svg b/src/zen/images/favicons/github.svg index 9b39d72a5..242dd5c86 100644 --- a/src/zen/images/favicons/github.svg +++ b/src/zen/images/favicons/github.svg @@ -1 +1,6 @@ + \ No newline at end of file diff --git a/src/zen/images/favicons/notion.svg b/src/zen/images/favicons/notion.svg index 1da179e0b..f5876ace0 100644 --- a/src/zen/images/favicons/notion.svg +++ b/src/zen/images/favicons/notion.svg @@ -1,3 +1,8 @@ + diff --git a/src/zen/images/favicons/obsidian.svg b/src/zen/images/favicons/obsidian.svg index 02b119b1c..b8349e998 100644 --- a/src/zen/images/favicons/obsidian.svg +++ b/src/zen/images/favicons/obsidian.svg @@ -1,3 +1,8 @@ + diff --git a/src/zen/images/favicons/reddit.svg b/src/zen/images/favicons/reddit.svg index fcb08b5e6..95447b8e1 100644 --- a/src/zen/images/favicons/reddit.svg +++ b/src/zen/images/favicons/reddit.svg @@ -1,3 +1,8 @@ + diff --git a/src/zen/images/favicons/slack.svg b/src/zen/images/favicons/slack.svg index 2cdecb2a9..2d370e471 100644 --- a/src/zen/images/favicons/slack.svg +++ b/src/zen/images/favicons/slack.svg @@ -1,3 +1,8 @@ + diff --git a/src/zen/images/favicons/trello.svg b/src/zen/images/favicons/trello.svg index 4411e5026..ca092ed8f 100644 --- a/src/zen/images/favicons/trello.svg +++ b/src/zen/images/favicons/trello.svg @@ -1,3 +1,8 @@ + diff --git a/src/zen/images/favicons/x.svg b/src/zen/images/favicons/x.svg index 966787626..e6afe972e 100644 --- a/src/zen/images/favicons/x.svg +++ b/src/zen/images/favicons/x.svg @@ -1,3 +1,8 @@ + diff --git a/src/zen/kbs/ZenKeyboardShortcuts.mjs b/src/zen/kbs/ZenKeyboardShortcuts.mjs index 819e05ed7..8d309e357 100644 --- a/src/zen/kbs/ZenKeyboardShortcuts.mjs +++ b/src/zen/kbs/ZenKeyboardShortcuts.mjs @@ -194,20 +194,26 @@ class nsKeyShortcutModifiers { toUserString() { let str = ''; + const separation = AppConstants.platform == 'macosx' ? ' ' : '+'; if (this.#control && !this.#accel) { - str += 'Ctrl+'; + str += AppConstants.platform == 'macosx' ? '⌃' : 'Ctrl'; + str += separation; } if (this.#alt) { - str += AppConstants.platform == 'macosx' ? 'Option+' : 'Alt+'; + str += AppConstants.platform == 'macosx' ? '⌥' : 'Alt'; + str += separation; } if (this.#shift) { - str += 'Shift+'; + str += '⇧'; + str += separation; } if (this.#meta) { - str += AppConstants.platform == 'macosx' ? 'Cmd+' : 'Win+'; + str += AppConstants.platform == 'macosx' ? '⌘' : 'Win'; + str += separation; } if (this.#accel) { - str += AppConstants.platform == 'macosx' ? 'Cmd+' : 'Ctrl+'; + str += AppConstants.platform == 'macosx' ? '⌘' : 'Ctrl'; + str += separation; } return str; } @@ -546,7 +552,32 @@ class KeyShortcut { // Get the key from the value for (let [key, value] of Object.entries(KEYCODE_MAP)) { if (value == this.#keycode) { - str += key.toLowerCase(); + const normalizedKey = key.toLowerCase(); + switch (normalizedKey) { + case 'arrowleft': + str += '←'; + break; + case 'arrowright': + str += '→'; + break; + case 'arrowup': + str += '↑'; + break; + case 'arrowdown': + str += '↓'; + break; + case 'escape': + str += AppConstants.platform == 'macosx' ? '⎋' : 'Esc'; + break; + case 'enter': + str += AppConstants.platform == 'macosx' ? '↩' : 'Enter'; + break; + case 'space': + str += AppConstants.platform == 'macosx' ? '␣' : 'Space'; + break; + default: + str += normalizedKey; + } break; } } @@ -1315,6 +1346,28 @@ var gZenKeyboardShortcutsManager = { return false; }, + + getShortcutFromCommand(command) { + for (let targetShortcut of this._currentShortcutList) { + if (targetShortcut.getAction() == command) { + return targetShortcut; + } + } + return null; + }, + + /** + * Get the shortcut as a display format for a given action/command. + * @param {string} command The action/command to search for + * @returns {string|null} The shortcut as a string or null if not found + */ + getShortcutDisplayFromCommand(command) { + const shortcut = this.getShortcutFromCommand(command); + if (shortcut) { + return shortcut.toUserString(); + } + return null; + }, }; document.addEventListener( diff --git a/src/zen/moz.build b/src/zen/moz.build index 1840323d7..56782122f 100644 --- a/src/zen/moz.build +++ b/src/zen/moz.build @@ -1,4 +1,3 @@ -# # 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/. @@ -8,5 +7,6 @@ DIRS += [ "glance", "mods", "tests", + "urlbar", "toolkit", ] diff --git a/src/zen/tests/moz.build b/src/zen/tests/moz.build index 06a9b9604..fd6101bce 100644 --- a/src/zen/tests/moz.build +++ b/src/zen/tests/moz.build @@ -10,6 +10,7 @@ BROWSER_CHROME_MANIFESTS += [ "pinned/browser.toml", "split_view/browser.toml", "tabs/browser.toml", + "ub-actions/browser.toml", "urlbar/browser.toml", "welcome/browser.toml", "workspaces/browser.toml", diff --git a/src/zen/tests/ub-actions/browser.toml b/src/zen/tests/ub-actions/browser.toml new file mode 100644 index 000000000..c3b3936c2 --- /dev/null +++ b/src/zen/tests/ub-actions/browser.toml @@ -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/. + +["browser_ub_actions_search.js"] \ No newline at end of file diff --git a/src/zen/tests/ub-actions/browser_ub_actions_search.js b/src/zen/tests/ub-actions/browser_ub_actions_search.js new file mode 100644 index 000000000..372598f0b --- /dev/null +++ b/src/zen/tests/ub-actions/browser_ub_actions_search.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +ChromeUtils.defineESModuleGetters(this, { + globalActions: 'resource:///modules/ZenUBGlobalActions.sys.mjs', + UrlbarTestUtils: 'resource://testing-common/UrlbarTestUtils.sys.mjs', +}); + +add_task(async function test_Ub_Actions_Search() { + for (const action of globalActions) { + const label = action.label; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: label, + }); + let { result } = await UrlbarTestUtils.getRowAt(window, 1); + Assert.equal(result.providerName, 'ZenUrlbarProviderGlobalActions'); + Assert.equal(result.payload.title, label); + } +}); diff --git a/src/zen/urlbar/ZenUBActionsProvider.sys.mjs b/src/zen/urlbar/ZenUBActionsProvider.sys.mjs new file mode 100644 index 000000000..d1cef4e36 --- /dev/null +++ b/src/zen/urlbar/ZenUBActionsProvider.sys.mjs @@ -0,0 +1,277 @@ +/* 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 { XPCOMUtils } from 'resource://gre/modules/XPCOMUtils.sys.mjs'; +import { UrlbarProvider, UrlbarUtils } from 'resource:///modules/UrlbarUtils.sys.mjs'; +import { globalActions } from 'resource:///modules/ZenUBGlobalActions.sys.mjs'; + +const lazy = {}; + +const DYNAMIC_TYPE_NAME = 'zen-actions'; + +// The suggestion index of the actions row within the urlbar results. +const MAX_RECENT_ACTIONS = 5; +const MINIMUM_QUERY_SCORE = 93; + +const EN_LOCALE_MATCH = /^en(-.*)$/; + +ChromeUtils.defineESModuleGetters(lazy, { + UrlbarResult: 'resource:///modules/UrlbarResult.sys.mjs', + UrlbarTokenizer: 'resource:///modules/UrlbarTokenizer.sys.mjs', + QueryScorer: 'resource:///modules/UrlbarProviderInterventions.sys.mjs', + BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs', +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + 'enabledPref', + 'zen.urlbar.suggestions.quick-actions', + true +); + +/** + * A provider that lets the user view all available global actions for a query. + */ +export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { + get name() { + return 'ZenUrlbarProviderGlobalActions'; + } + + /** + * @returns {Values} + */ + get type() { + return UrlbarUtils.PROVIDER_TYPE.PROFILE; + } + + /** + * Whether this provider should be invoked for the given context. + * If this method returns false, the providers manager won't start a query + * with this provider, to save on resources. + * + * @param {UrlbarQueryContext} queryContext The query context object + */ + async isActive(queryContext) { + return ( + lazy.enabledPref && + queryContext.searchString && + queryContext.searchString.length < UrlbarUtils.MAX_TEXT_LENGTH && + queryContext.searchString.length > 2 && + !lazy.UrlbarTokenizer.REGEXP_LIKE_PROTOCOL.test(queryContext.searchString) && + EN_LOCALE_MATCH.test(Services.locale.appLocaleAsBCP47) + ); + } + + /** + * @returns All the available global actions. + */ + get #availableActions() { + return globalActions.filter((a) => + typeof a.isAvailable === 'function' ? a.isAvailable() : true + ); + } + + /** + * Starts a search query amongst the available global actions. + * + * @param {string} queryContext The query context object + */ + #findMatchingActions(query) { + const actions = this.#availableActions; + let results = []; + for (let action of actions) { + const label = action.label; + const score = this.#calculateFuzzyScore(label, query); + if (score > MINIMUM_QUERY_SCORE) { + results.push({ + score, + action, + }); + } + } + results.sort((a, b) => b.score - a.score); + return results.slice(0, MAX_RECENT_ACTIONS).map((r) => r.action); + } + + /** + * A VS Code-style fuzzy scoring algorithm. + * @param {string} target The string to score against. + * @param {string} query The user's search query. + * @returns {number} A score representing the match quality. + * + * @credits Thanks a lot @BibekBhusal0 on GitHub for this implementation! + */ + #calculateFuzzyScore(target, query) { + if (!target || !query) return 0; + const targetLower = target.toLowerCase(); + const queryLower = query.toLowerCase(); + const targetLen = target.length; + const queryLen = query.length; + if (queryLen > targetLen) return 0; + if (queryLen === 0) return 0; + // 1. Exact match gets the highest score. + if (targetLower === queryLower) { + return 200; + } + // 2. Exact prefix matches are heavily prioritized. + if (targetLower.startsWith(queryLower)) { + return 100 + queryLen; + } + // 3. Exact abbreviation (e.g., 'tcm' for 'Toggle Compact Mode') + const initials = targetLower + .split(/[\s-_]+/) + .map((word) => word[0]) + .join(''); + if (initials === queryLower) { + return 90 + queryLen; + } + let score = 0; + let queryIndex = 0; + let lastMatchIndex = -1; + let consecutiveMatches = 0; + for (let targetIndex = 0; targetIndex < targetLen; targetIndex++) { + if (queryIndex < queryLen && targetLower[targetIndex] === queryLower[queryIndex]) { + let bonus = 10; + // Bonus for matching at the beginning of a word + if (targetIndex === 0 || [' ', '-', '_'].includes(targetLower[targetIndex - 1])) { + bonus += 15; + } + // Bonus for consecutive matches + if (lastMatchIndex === targetIndex - 1) { + consecutiveMatches++; + bonus += 20 * consecutiveMatches; + } else { + consecutiveMatches = 0; + } + // Penalty for distance from the last match + if (lastMatchIndex !== -1) { + const distance = targetIndex - lastMatchIndex; + bonus -= Math.min(distance - 1, 10); // Cap penalty + } + score += bonus; + lastMatchIndex = targetIndex; + queryIndex++; + } + } + return queryIndex === queryLen ? score : 0; + } + + async startQuery(queryContext, addCallback) { + const query = queryContext.searchString.trim().toLowerCase(); + if (!query) { + return; + } + + const actionsResults = this.#findMatchingActions(query); + if (!actionsResults.length) { + return; + } + + const ownerGlobal = lazy.BrowserWindowTracker.getTopWindow(); + for (const action of actionsResults) { + const [payload, payloadHighlights] = lazy.UrlbarResult.payloadAndSimpleHighlights([], { + suggestion: action.label, + title: action.label, + query: queryContext.searchString, + zenCommand: action.command, + dynamicType: DYNAMIC_TYPE_NAME, + icon: action.icon || 'chrome://browser/skin/trending.svg', + shortcutContent: ownerGlobal.gZenKeyboardShortcutsManager.getShortcutDisplayFromCommand( + action.command + ), + }); + + let result = new lazy.UrlbarResult( + UrlbarUtils.RESULT_TYPE.DYNAMIC, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + payload, + payloadHighlights + ); + if (action.suggestedIndex) { + result.suggestedIndex = action.suggestedIndex; + } + addCallback(this, result); + } + } + + /** + * Gets the provider's priority. + * + * @returns {number} The provider's priority for the given query. + */ + getPriority() { + return 0; + } + + /** + * This is called only for dynamic result types, when the urlbar view updates + * the view of one of the results of the provider. It should return an object + * describing the view update. + * + * @param {UrlbarResult} result The result whose view will be updated. + * @returns {object} An object describing the view update. + */ + getViewUpdate(result) { + return { + icon: { + attributes: { + src: result.payload.icon || 'chrome://browser/skin/trending.svg', + }, + }, + titleStrong: { + textContent: result.payload.title, + attributes: { dir: 'ltr' }, + }, + shortcutContent: { + textContent: result.payload.shortcutContent || '', + }, + }; + } + + getViewTemplate() { + return { + attributes: { + selectable: true, + }, + children: [ + { + name: 'icon', + tag: 'img', + classList: ['urlbarView-favicon'], + }, + { + name: 'title', + tag: 'span', + classList: ['urlbarView-title'], + children: [ + { + name: 'titleStrong', + tag: 'strong', + }, + ], + }, + { + name: 'shortcutContent', + tag: 'span', + classList: ['urlbarView-shortcutContent'], + }, + ], + }; + } + + onEngagement(queryContext, controller, details) { + const result = details.result; + const payload = result.payload; + const command = payload.zenCommand; + if (!command) { + return; + } + const ownerGlobal = details.element.ownerGlobal; + const commandToRun = ownerGlobal.document.getElementById(command); + if (commandToRun) { + ownerGlobal.gBrowser.selectedBrowser.focus(); + commandToRun.doCommand(); + } + } +} diff --git a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs new file mode 100644 index 000000000..737162308 --- /dev/null +++ b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs @@ -0,0 +1,17 @@ +/* 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/. */ + +export const globalActions = [ + { + label: 'Toggle Compact Mode', + command: 'cmd_zenCompactModeToggle', + icon: 'chrome://browser/skin/zen-icons/sidebar.svg', + }, + { + label: 'Open Theme Picker', + command: 'cmd_zenOpenZenThemePicker', + icon: 'chrome://browser/skin/zen-icons/edit-theme.svg', + suggestedIndex: 4, + }, +]; diff --git a/src/zen/urlbar/ZenUBProvider.sys.mjs b/src/zen/urlbar/ZenUBProvider.sys.mjs new file mode 100644 index 000000000..414c98725 --- /dev/null +++ b/src/zen/urlbar/ZenUBProvider.sys.mjs @@ -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/. */ + +import { UrlbarProvidersManager } from 'resource:///modules/UrlbarProvidersManager.sys.mjs'; + +const providers = {}; +ChromeUtils.defineESModuleGetters(providers, { + ZenUrlbarProviderGlobalActions: 'resource:///modules/ZenUBActionsProvider.sys.mjs', +}); + +export function registerZenUrlbarProviders() { + for (let provider of Object.values(providers)) { + UrlbarProvidersManager.registerProvider(new provider()); + } +} diff --git a/src/zen/urlbar/moz.build b/src/zen/urlbar/moz.build new file mode 100644 index 000000000..306a7c08b --- /dev/null +++ b/src/zen/urlbar/moz.build @@ -0,0 +1,9 @@ +# 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 += [ + "ZenUBActionsProvider.sys.mjs", + "ZenUBGlobalActions.sys.mjs", + "ZenUBProvider.sys.mjs", +]