tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 2dc482d626a741522102784f60d34414fc3e8454
parent 36dac419b762704e686566261b1eb323b4fe84a0
Author: Jonathan Sudiaman <jsudiaman@mozilla.com>
Date:   Thu,  4 Dec 2025 14:57:20 +0000

Bug 1986986 - Add a non-removable urlbar indicator for split view r=tabbrowser-reviewers,desktop-theme-reviewers,urlbar-reviewers,fluent-reviewers,bolsson,nsharpley,jteow

- Refactor split-view-footer menu into a reusable popup in main-popupset.
- Add split view icon.
- Leverage activate/deactivate handlers to control button visibility.

Differential Revision: https://phabricator.services.mozilla.com/D273225

Diffstat:
Mbrowser/base/content/main-popupset.inc.xhtml | 10++++++++++
Mbrowser/base/content/navigator-toolbox.inc.xhtml | 7+++++++
Mbrowser/base/content/navigator-toolbox.js | 18++++++++++++++++--
Mbrowser/components/tabbrowser/content/split-view-footer.js | 19++++++++-----------
Mbrowser/components/tabbrowser/content/tabbrowser.js | 13+++++++++++--
Mbrowser/components/tabbrowser/content/tabsplitview.js | 36++++++++++++++++++++++++++++++++----
Mbrowser/components/tabbrowser/test/browser/tabs/browser_tab_splitview.js | 29+++++++++++++++++++++++++++++
Mbrowser/components/tabbrowser/test/browser/tabs/browser_tab_splitview_footer.js | 8++++++--
Mbrowser/locales/en-US/browser/browser.ftl | 4++++
Abrowser/themes/shared/icons/split-view-left-16.svg | 5+++++
Abrowser/themes/shared/icons/split-view-right-16.svg | 5+++++
Mbrowser/themes/shared/jar.inc.mn | 2++
Mbrowser/themes/shared/urlbar-searchbar.css | 20+++++++++++++++++++-
13 files changed, 154 insertions(+), 22 deletions(-)

diff --git a/browser/base/content/main-popupset.inc.xhtml b/browser/base/content/main-popupset.inc.xhtml @@ -416,6 +416,16 @@ id="sidebar-synced-tabs-context-copy-link"/> </menupopup> + <menupopup id="split-view-menu"> + <menuitem command="splitViewCmd_separateTabs" + data-l10n-id="split-view-menuitem-separate-tabs"/> + <menuitem command="splitViewCmd_reverseTabs" + data-l10n-id="split-view-menuitem-reverse-tabs"/> + <menuseparator/> + <menuitem command="splitViewCmd_closeTabs" + data-l10n-id="split-view-menuitem-close-both-tabs"/> + </menupopup> + <menupopup id="toolbar-context-menu"> <menuitem id="toolbar-context-manage-extension" data-lazy-l10n-id="toolbar-context-menu-manage-extension" diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml @@ -389,6 +389,13 @@ <toolbarbutton id="urlbar-zoom-button" tooltip="dynamic-shortcut-tooltip" hidden="true"/> + <hbox id="split-view-button" + class="urlbar-page-action" + role="button" + data-l10n-id="urlbar-split-view-button" + hidden="true"> + <image class="urlbar-icon" id="split-view-button-icon" /> + </hbox> <hbox id="pageActionButton" class="urlbar-page-action" role="button" diff --git a/browser/base/content/navigator-toolbox.js b/browser/base/content/navigator-toolbox.js @@ -195,7 +195,8 @@ document.addEventListener( #tracking-protection-icon-container, #identity-icon-box, #identity-permission-box, - #translations-button + #translations-button, + #split-view-button `); if (!element) { return; @@ -284,6 +285,12 @@ document.addEventListener( FullPageTranslationsPanel.open(event); break; + case "split-view-button": + if (isLeftClick) { + gBrowser.openSplitViewMenu(element); + } + break; + default: throw new Error(`Missing case for #${element.id}`); } @@ -311,7 +318,8 @@ document.addEventListener( #downloads-button, #fxa-toolbar-menu-button, #unified-extensions-button, - #library-button + #library-button, + #split-view-button `); if (!element) { return; @@ -397,6 +405,12 @@ document.addEventListener( PanelUI.showSubView("appMenu-libraryView", element, event); break; + case "split-view-button": + if (isLikeLeftClick) { + gBrowser.openSplitViewMenu(element); + } + break; + default: throw new Error(`Missing case for #${element.id}`); } diff --git a/browser/components/tabbrowser/content/split-view-footer.js b/browser/components/tabbrowser/content/split-view-footer.js @@ -61,17 +61,8 @@ </hbox> <html:img class="split-view-icon" hidden="" role="presentation"/> <html:span class="split-view-uri"></html:span> - <toolbarbutton type="menu" image="chrome://global/skin/icons/more.svg"> - <menupopup class="split-view-footer-menu"> - <menuitem command="splitViewCmd_separateTabs" - data-l10n-id="split-view-menuitem-separate-tabs"/> - <menuitem command="splitViewCmd_reverseTabs" - data-l10n-id="split-view-menuitem-reverse-tabs"/> - <menuseparator/> - <menuitem command="splitViewCmd_closeTabs" - data-l10n-id="split-view-menuitem-close-both-tabs"/> - </menupopup> - </toolbarbutton> + <toolbarbutton image="chrome://global/skin/icons/more.svg" + data-l10n-id="urlbar-split-view-button" /> `; connectedCallback() { @@ -83,6 +74,7 @@ this.securityElement = this.querySelector(".split-view-security-warning"); this.iconElement = this.querySelector(".split-view-icon"); this.uriElement = this.querySelector(".split-view-uri"); + this.menuButtonElement = this.querySelector("toolbarbutton"); // Ensure these elements are up-to-date, as this info may have been set // prior to inserting this element into the DOM. @@ -90,6 +82,8 @@ this.#updateIconElement(); this.#updateUriElement(); + this.menuButtonElement.addEventListener("command", this); + this.#initialized = true; } @@ -99,6 +93,9 @@ handleEvent(e) { switch (e.type) { + case "command": + gBrowser.openSplitViewMenu(this.menuButtonElement); + break; case "TabAttrModified": for (const attribute of e.detail.changed) { this.#handleTabAttrModified(attribute); diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js @@ -388,6 +388,10 @@ /** @type {MozTabSplitViewWrapper} */ #activeSplitView = null; + get activeSplitView() { + return this.#activeSplitView; + } + /** * List of browsers which are currently in an active Split View. * @@ -3309,6 +3313,11 @@ } } + openSplitViewMenu(anchorElement) { + const menu = document.getElementById("split-view-menu"); + menu.openPopup(anchorElement, "after_start"); + } + /** * @param {string} id * @param {string} color @@ -7796,10 +7805,10 @@ break; } case "TabSplitViewActivate": - this.#activeSplitView = aEvent.originalTarget; + this.#activeSplitView = aEvent.detail.splitview; break; case "TabSplitViewDeactivate": - if (this.#activeSplitView === aEvent.originalTarget) { + if (this.#activeSplitView === aEvent.detail.splitview) { this.#activeSplitView = null; } break; diff --git a/browser/components/tabbrowser/content/tabsplitview.js b/browser/components/tabbrowser/content/tabsplitview.js @@ -7,6 +7,31 @@ // This is loaded into chrome windows with the subscript loader. Wrap in // a block to prevent accidentally leaking globals onto `window`. { + ChromeUtils.defineESModuleGetters(this, { + DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", + }); + + /** + * A shared task which updates the urlbar indicator whenever: + * - A split view is activated or deactivated. + * - The active tab of a split view changes. + * - The order of tabs in a split view changes. + * + * @type {DeferredTask} + */ + const updateUrlbarButton = new DeferredTask(() => { + const { activeSplitView, selectedTab } = gBrowser; + const button = document.getElementById("split-view-button"); + if (activeSplitView) { + const activeIndex = activeSplitView.tabs.indexOf(selectedTab); + button.hidden = false; + button.setAttribute("data-active-index", activeIndex); + } else { + button.hidden = true; + button.removeAttribute("data-active-index"); + } + }, 0); + class MozTabSplitViewWrapper extends MozXULElement { /** @type {MutationObserver} */ #tabChangeObserver; @@ -121,9 +146,10 @@ */ #activate() { gBrowser.showSplitViewPanels(this.#tabs); - this.dispatchEvent( + updateUrlbarButton.arm(); + this.container.dispatchEvent( new CustomEvent("TabSplitViewActivate", { - detail: { tabs: this.#tabs }, + detail: { tabs: this.#tabs, splitview: this }, bubbles: true, }) ); @@ -134,9 +160,10 @@ */ #deactivate() { gBrowser.hideSplitViewPanels(this.#tabs); - this.dispatchEvent( + updateUrlbarButton.arm(); + this.container.dispatchEvent( new CustomEvent("TabSplitViewDeactivate", { - detail: { tabs: this.#tabs }, + detail: { tabs: this.#tabs, splitview: this }, bubbles: true, }) ); @@ -194,6 +221,7 @@ gBrowser.moveTabBefore(secondTab, firstTab); this.#tabs = [secondTab, firstTab]; gBrowser.showSplitViewPanels(this.#tabs); + updateUrlbarButton.arm(); } /** diff --git a/browser/components/tabbrowser/test/browser/tabs/browser_tab_splitview.js b/browser/components/tabbrowser/test/browser/tabs/browser_tab_splitview.js @@ -15,6 +15,8 @@ registerCleanupFunction(async function () { ); }); +const urlbarButton = document.getElementById("split-view-button"); + async function addTabAndLoadBrowser() { const tab = BrowserTestUtils.addTab(gBrowser, "https://example.com"); await BrowserTestUtils.browserLoaded(tab.linkedBrowser); @@ -126,6 +128,18 @@ add_task(async function test_split_view_panels() { for (const tab of splitView.tabs) { await checkSplitViewPanelVisible(tab, true); } + await BrowserTestUtils.waitForMutationCondition( + urlbarButton, + { attributes: true, attributeFilter: ["hidden"] }, + () => BrowserTestUtils.isVisible(urlbarButton) + ); + + info("Open split view menu."); + const menu = document.getElementById("split-view-menu"); + const promiseMenuShown = BrowserTestUtils.waitForPopupEvent(menu, "shown"); + EventUtils.synthesizeMouseAtCenter(urlbarButton, {}); + await promiseMenuShown; + menu.hidePopup(); info("Select tabs using tab panels."); await SimpleTest.promiseFocus(tab1.linkedBrowser); @@ -134,6 +148,11 @@ add_task(async function test_split_view_panels() { panel.classList.contains("deck-selected"), "First panel is selected." ); + await BrowserTestUtils.waitForMutationCondition( + urlbarButton, + { attributes: true, attributeFilter: ["data-active-index"] }, + () => urlbarButton.dataset.activeIndex == "0" + ); await SimpleTest.promiseFocus(tab2.linkedBrowser); panel = document.getElementById(tab2.linkedPanel); @@ -141,6 +160,11 @@ add_task(async function test_split_view_panels() { panel.classList.contains("deck-selected"), "Second panel is selected." ); + await BrowserTestUtils.waitForMutationCondition( + urlbarButton, + { attributes: true }, + () => urlbarButton.dataset.activeIndex == "1" + ); info("Switch to a non-split view tab."); await BrowserTestUtils.switchTab(gBrowser, originalTab); @@ -158,6 +182,11 @@ add_task(async function test_split_view_panels() { splitView.unsplitTabs(); await checkSplitViewPanelVisible(tab1, false); await checkSplitViewPanelVisible(tab2, false); + await BrowserTestUtils.waitForMutationCondition( + urlbarButton, + { attributes: true }, + () => BrowserTestUtils.isHidden(urlbarButton) + ); BrowserTestUtils.removeTab(tab1); BrowserTestUtils.removeTab(tab2); diff --git a/browser/components/tabbrowser/test/browser/tabs/browser_tab_splitview_footer.js b/browser/components/tabbrowser/test/browser/tabs/browser_tab_splitview_footer.js @@ -42,9 +42,13 @@ async function setupSplitView() { } async function activateCommand(panel, command) { - const footerMenu = panel.querySelector(".split-view-footer-menu"); + const footerMenu = document.getElementById("split-view-menu"); const promiseShown = BrowserTestUtils.waitForPopupEvent(footerMenu, "shown"); - footerMenu.openPopup(); + const { menuButtonElement } = panel.querySelector("split-view-footer"); + // Only the urlbar menu is focusable, not the footer menu. + AccessibilityUtils.setEnv({ focusableRule: false }); + EventUtils.synthesizeMouseAtCenter(menuButtonElement, {}); + AccessibilityUtils.resetEnv(); await promiseShown; const item = footerMenu.querySelector(`menuitem[command="${command}"]`); footerMenu.activateItem(item); diff --git a/browser/locales/en-US/browser/browser.ftl b/browser/locales/en-US/browser/browser.ftl @@ -181,6 +181,10 @@ urlbar-star-edit-bookmark = urlbar-star-add-bookmark = .tooltiptext = Bookmark this page ({ $shortcut }) +urlbar-split-view-button = + .tooltiptext = Split view + .aria-label = Split view + ## Page Action Context Menu page-action-manage-extension2 = diff --git a/browser/themes/shared/icons/split-view-left-16.svg b/browser/themes/shared/icons/split-view-left-16.svg @@ -0,0 +1,4 @@ +<!-- 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/. --> + <svg width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity" xmlns="http://www.w3.org/2000/svg"><path d="M8.75 15h-1.5v-1H2c-1.103 0-2-.897-2-2V4c0-1.103.897-2 2-2h5.25V1h1.5v14zM14 2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-3.75v-1.5H14a.5.5 0 0 0 .5-.5V4a.5.5 0 0 0-.5-.5h-3.75V2H14z" /></svg> +\ No newline at end of file diff --git a/browser/themes/shared/icons/split-view-right-16.svg b/browser/themes/shared/icons/split-view-right-16.svg @@ -0,0 +1,4 @@ +<!-- 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/. --> + <svg width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity" xmlns="http://www.w3.org/2000/svg"><path d="M8.75 2H14c1.103 0 2 .897 2 2v8c0 1.103-.897 2-2 2H8.75v1h-1.5V1h1.5v1zM5.75 3.5H2a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h3.75V14H2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3.75v1.5z" /></svg> +\ No newline at end of file diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn @@ -236,6 +236,8 @@ skin/classic/browser/sidebars.svg (../shared/icons/sidebars.svg) skin/classic/browser/sidebars-right.svg (../shared/icons/sidebars-right.svg) skin/classic/browser/sort.svg (../shared/icons/sort.svg) + skin/classic/browser/split-view-left-16.svg (../shared/icons/split-view-left-16.svg) + skin/classic/browser/split-view-right-16.svg (../shared/icons/split-view-right-16.svg) skin/classic/browser/stop-to-reload.svg (../shared/icons/stop-to-reload.svg) skin/classic/browser/subtract-circle-fill.svg (../shared/icons/subtract-circle-fill.svg) skin/classic/browser/success-animation.svg (../shared/icons/success-animation.svg) diff --git a/browser/themes/shared/urlbar-searchbar.css b/browser/themes/shared/urlbar-searchbar.css @@ -48,7 +48,7 @@ #urlbar:not([actiontype="switchtab"]) > .urlbar-input-container > #urlbar-label-box > #urlbar-label-switchtab, #urlbar:not([actiontype="extension"]) > .urlbar-input-container > #urlbar-label-box > #urlbar-label-extension, .urlbar-input-container[pageproxystate="invalid"] > #page-action-buttons > .urlbar-page-action, -#identity-box.chromeUI ~ #page-action-buttons > .urlbar-page-action:not(#star-button-box), +#identity-box.chromeUI ~ #page-action-buttons > .urlbar-page-action:not(#star-button-box, #split-view-button), #urlbar[usertyping] > .urlbar-input-container > #page-action-buttons > #urlbar-zoom-button, .urlbar:is(:not([usertyping]), :not([focused])) > .urlbar-input-container > .urlbar-go-button, .urlbar-revert-button-container { @@ -535,6 +535,24 @@ height: 16px; } +#split-view-button { + &[data-active-index="0"] > #split-view-button-icon { + list-style-image: url("chrome://browser/skin/split-view-left-16.svg"); + } + &[data-active-index="1"] > #split-view-button-icon { + list-style-image: url("chrome://browser/skin/split-view-right-16.svg"); + } + + &:dir(rtl) { + &[data-active-index="0"] > #split-view-button-icon { + list-style-image: url("chrome://browser/skin/split-view-right-16.svg"); + } + &[data-active-index="1"] > #split-view-button-icon { + list-style-image: url("chrome://browser/skin/split-view-left-16.svg"); + } + } +} + #pageAction-panel-bookmark, #star-button { list-style-image: url("chrome://browser/skin/bookmark-hollow.svg");