diff --git a/waterfox/browser/app/profile/000-waterfox.js b/waterfox/browser/app/profile/000-waterfox.js index d757fb995b0f..8393a8ad37d2 100644 --- a/waterfox/browser/app/profile/000-waterfox.js +++ b/waterfox/browser/app/profile/000-waterfox.js @@ -238,3 +238,13 @@ pref("userContent.page.proton", true); // Need proton_color // ** Useful Options *********************************************************** // Integrated calculator at urlbar pref("browser.urlbar.suggest.calculator", true); + +// Extensibles prefs +pref("browser.tabs.duplicateTab", true); +pref("browser.tabs.copyurl", true); +pref("browser.tabs.copyallurls", false); +pref("browser.tabs.copyurl.activetab", false); +pref("browser.tabs.unloadTab", false); +pref("browser.restart_menu.showpanelmenubtn", true); +pref("browser.restart_menu.purgecache", false); +pref("browser.restart_menu.requireconfirm", true); \ No newline at end of file diff --git a/waterfox/browser/components/WaterfoxGlue.jsm b/waterfox/browser/components/WaterfoxGlue.jsm index f17f46db3ef0..411054091308 100644 --- a/waterfox/browser/components/WaterfoxGlue.jsm +++ b/waterfox/browser/components/WaterfoxGlue.jsm @@ -15,6 +15,7 @@ const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyModuleGetters(this, { ChromeManifest: "resource:///modules/ChromeManifest.jsm", Overlays: "resource:///modules/Overlays.jsm", + TabFeatures: "resource:///modules/TabFeatures.jsm", }); XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]); @@ -60,6 +61,8 @@ const WaterfoxGlue = { if (subject.URL.includes("browser.xhtml")) { const window = subject.defaultView; Overlays.load(this.startupManifest, window); + + TabFeatures.init(window); } break; } diff --git a/waterfox/browser/components/chrome.manifest b/waterfox/browser/components/chrome.manifest index e69de29bb2d1..da63cde00bdd 100644 --- a/waterfox/browser/components/chrome.manifest +++ b/waterfox/browser/components/chrome.manifest @@ -0,0 +1 @@ +overlay chrome://browser/content/browser.xhtml chrome://browser/content/overlays/tabfeatures.xhtml \ No newline at end of file diff --git a/waterfox/browser/components/moz.build b/waterfox/browser/components/moz.build index 34924ffe83e1..7f8ef2e04522 100644 --- a/waterfox/browser/components/moz.build +++ b/waterfox/browser/components/moz.build @@ -4,6 +4,10 @@ # 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/. +DIRS += [ + "tabfeatures", +] + EXTRA_JS_MODULES += [ "WaterfoxGlue.jsm", ] diff --git a/waterfox/browser/components/tabfeatures/TabFeatures.jsm b/waterfox/browser/components/tabfeatures/TabFeatures.jsm new file mode 100644 index 000000000000..dea5388e38e9 --- /dev/null +++ b/waterfox/browser/components/tabfeatures/TabFeatures.jsm @@ -0,0 +1,167 @@ +/* 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/. */ + +/* global */ + +const EXPORTED_SYMBOLS = ["TabFeatures"]; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); + +const TabFeatures = { + PREF_ACTIVETAB: "browser.tabs.copyurl.activetab", + PREF_REQUIRECONFIRM: "browser.restart_menu.requireconfirm", + PREF_PURGECACHE: "browser.restart_menu.purgecache", + + get browserBundle() { + return Services.strings.createBundle( + "chrome://extensibles/locale/extensibles.properties" + ); + }, + get brandBundle() { + return Services.strings.createBundle( + "chrome://branding/locale/brand.properties" + ); + }, + + init(window) { + window.TabFeatures = this; + this.initListeners(window); + }, + + initListeners(aWindow) { + aWindow.document + .getElementById("tabContextMenu") + ?.addEventListener("popupshowing", this.tabContext); + if (AppConstants.platform == "macosx") { + aWindow.document + .getElementById("file-menu") + ?.addEventListener("popupshowing", this.tabContext); + } else { + aWindow.document + .getElementById("appMenu-popup") + ?.addEventListener("popupshowing", this.tabContext); + } + }, + + tabContext(aEvent) { + let win = aEvent.view; + if (!win) { + win = Services.wm.getMostRecentWindow("navigator:browser"); + } + let { document } = win; + let elements = document.getElementsByClassName("tabFeature"); + for (let i = 0; i < elements.length; i++) { + let el = elements[i]; + let pref = el.getAttribute("preference"); + if (pref) { + let visible = Services.prefs.getBoolPref(pref); + el.hidden = !visible; + } + } + // Can't unload selected tab, so don't show menu item in that case + if (win.TabContextMenu.contextTab === win.gBrowser.selectedTab) { + const el = document.getElementById("context_unloadTab"); + el.hidden = true; + } + }, + + // Copies current tab url to clipboard + copyTabUrl(aUri, aWindow) { + const gClipboardHelper = Cc[ + "@mozilla.org/widget/clipboardhelper;1" + ].getService(Ci.nsIClipboardHelper); + try { + Services.prefs.getBoolPref(this.PREF_ACTIVETAB) + ? gClipboardHelper.copyString(aWindow.gBrowser.currentURI.spec) + : gClipboardHelper.copyString(aUri); + } catch (e) { + throw new Error( + "We're sorry but something has gone wrong with 'CopyTabUrl' " + e + ); + } + }, + + // Copies all tab urls to clipboard + copyAllTabUrls(aWindow) { + const gClipboardHelper = Cc[ + "@mozilla.org/widget/clipboardhelper;1" + ].getService(Ci.nsIClipboardHelper); + //Get all urls + let urlArr = this._getAllUrls(aWindow); + try { + // Enumerate all urls in to a list. + let urlList = urlArr.join("\n"); + // Send list to clipboard. + gClipboardHelper.copyString(urlList.trim()); + // Clear url list after clipboard event + urlList = ""; + } catch (e) { + throw new Error( + "We're sorry but something has gone wrong with 'copyAllTabUrls' " + e + ); + } + }, + + // Get all the tab urls into an array. + _getAllUrls(aWindow) { + // We don't want to copy about uri's + let blocklist = /^about:.*/i; + let urlArr = []; + let tabCount = aWindow.gBrowser.browsers.length; + Array(tabCount) + .fill() + .map((_, i) => { + let spec = aWindow.gBrowser.getBrowserAtIndex(i).currentURI.spec; + if (!blocklist.test(spec)) { + urlArr.push(spec); + } + }); + return urlArr; + }, + + async restartBrowser() { + try { + if (Services.prefs.getBoolPref(this.PREF_REQUIRECONFIRM)) { + // Need brand in here to be able to expand { -brand-short-name } + let l10n = new Localization([ + "branding/brand.ftl", + "browser/extensibles.ftl", + ]); + let [title, question] = ( + await l10n.formatMessages([ + { id: "restart-prompt-title" }, + { id: "restart-prompt-question" }, + ]) + ).map(({ value }) => value); + + if (Services.prompt.confirm(null, title, question)) { + // only restart if confirmation given + this._attemptRestart(); + } + } else { + this._attemptRestart(); + } + } catch (e) { + Cu.reportError( + "We're sorry but something has gone wrong with 'restartBrowser' " + e + ); + } + }, + + _attemptRestart() { + // Purge cache if required + if (Services.prefs.getBoolPref(this.PREF_PURGECACHE)) { + Services.appinfo.invalidateCachesOnRestart(); + } + + // Initiate the restart + Services.startup.quit( + Services.startup.eRestart | Services.startup.eAttemptQuit + ); + }, +}; diff --git a/waterfox/browser/components/tabfeatures/content/tabfeatures.xhtml b/waterfox/browser/components/tabfeatures/content/tabfeatures.xhtml new file mode 100644 index 000000000000..f659600e38ed --- /dev/null +++ b/waterfox/browser/components/tabfeatures/content/tabfeatures.xhtml @@ -0,0 +1,43 @@ + + +#filter substitution + + + + + + + + + + + +#ifdef XP_MACOSX + + +#else + + + + + + +#endif + diff --git a/waterfox/browser/components/tabfeatures/jar.mn b/waterfox/browser/components/tabfeatures/jar.mn new file mode 100644 index 000000000000..934732928781 --- /dev/null +++ b/waterfox/browser/components/tabfeatures/jar.mn @@ -0,0 +1,3 @@ +browser.jar: +% content browser %content/browser/ contentaccessible=yes +* content/browser/overlays/tabfeatures.xhtml (content/tabfeatures.xhtml) \ No newline at end of file diff --git a/waterfox/browser/components/tabfeatures/moz.build b/waterfox/browser/components/tabfeatures/moz.build new file mode 100644 index 000000000000..7d8bc9e33233 --- /dev/null +++ b/waterfox/browser/components/tabfeatures/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 += [ + "TabFeatures.jsm", +] + +BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"] + +JAR_MANIFESTS += ["jar.mn"] diff --git a/waterfox/browser/components/tabfeatures/test/browser/browser.ini b/waterfox/browser/components/tabfeatures/test/browser/browser.ini new file mode 100644 index 000000000000..ca12b41ae859 --- /dev/null +++ b/waterfox/browser/components/tabfeatures/test/browser/browser.ini @@ -0,0 +1,7 @@ +[DEFAULT] +tags = waterfox_tabfeatures +firefox-appdir = browser +support-files = + head.js + +[browser_tabfeatures.js] diff --git a/waterfox/browser/components/tabfeatures/test/browser/browser_tabfeatures.js b/waterfox/browser/components/tabfeatures/test/browser/browser_tabfeatures.js new file mode 100644 index 000000000000..ef524e729fd0 --- /dev/null +++ b/waterfox/browser/components/tabfeatures/test/browser/browser_tabfeatures.js @@ -0,0 +1,114 @@ +"use strict"; + +add_task(async function testCopyTabUrls() { + // Make sure elements are present + let copyTabUrl = document.getElementById("context_copyTabUrl"); + let copyAllTabUrls = document.getElementById("context_copyAllTabUrls"); + ok(copyTabUrl, "Copy tab URL is included"); + ok(copyAllTabUrls, "Copy all tab URLs is included"); + // Make sure that defaults are set correctly + await openAndCloseTabContextMenu(gBrowser.selectedTab); + is(copyTabUrl.hidden, false, "Copy tab URL visible by default"); + is(copyAllTabUrls.hidden, true, "Copy all tab URLs hidden by default"); + // Make sure changing prefs causes elements to be shown/hidden + Services.prefs.setBoolPref(COPY_URL_PREF, false); + Services.prefs.setBoolPref(COPY_ALL_URLS_PREF, false); + await openAndCloseTabContextMenu(gBrowser.selectedTab); + is(copyTabUrl.hidden, true, "Copy tab URL hidden"); + is(copyAllTabUrls.hidden, true, "Copy all tab URLs hidden"); + Services.prefs.setBoolPref(COPY_URL_PREF, true); + Services.prefs.setBoolPref(COPY_ALL_URLS_PREF, true); + await openAndCloseTabContextMenu(gBrowser.selectedTab); + is(copyTabUrl.hidden, false, "Copy tab URL visible"); + is(copyAllTabUrls.hidden, false, "Copy all tab URLs visible"); + Services.prefs.clearUserPref(COPY_URL_PREF); + Services.prefs.clearUserPref(COPY_ALL_URLS_PREF); +}); + +add_task(async function testHideDuplicateTab() { + // Setting duplicateTab pref to false should hide element in all windows + let duplicateTab = document.getElementById("context_duplicateTab"); + Services.prefs.setBoolPref(DUPLICATE_TAB_PREF, false); + await openAndCloseTabContextMenu(gBrowser.selectedTab); + is(duplicateTab.hidden, true, "Duplicate tab hidden"); + // Should fall back to default value of true, i.e. element showing + Services.prefs.clearUserPref(DUPLICATE_TAB_PREF); + // Ensure showing + await openAndCloseTabContextMenu(gBrowser.selectedTab); + is(duplicateTab.hidden, false, "Duplicate tab showing"); +}); + +add_task(async function testRestartItem() { + // Make sure element is present + let restartBrowserMenu = document.getElementById("app_restartBrowser"); + // Need to use PanelMultiView to get PanelUI elements + let restartBrowserApp = PanelMultiView.getViewNode( + document, + "appMenu-restart-button" + ); + if (OS == "macosx") { + ok(restartBrowserMenu, "Restart browser menu bar item is included"); + is(restartBrowserApp, null, "Restart browser appMenu item not included"); + await openAndCloseFileMenu(); + is( + restartBrowserMenu.hidden, + false, + "Restart browser menu bar item is visible" + ); + } else { + is( + restartBrowserMenu, + null, + "Restart browser menu bar item is not included" + ); + ok(restartBrowserApp, "Restart browser appMenu item included"); + } + // Make sure element is hidden + Services.prefs.setBoolPref(RESTART_PREF, false); + if (OS == "macosx") { + await openAndCloseFileMenu(); + is( + restartBrowserMenu.hidden, + true, + "Restart browser menu bar item is hidden" + ); + } + Services.prefs.clearUserPref(RESTART_PREF); +}); + +add_task(async function testCopyUrlFunctionality() { + let copyTabUrl = document.getElementById("context_copyTabUrl"); + let copyAllTabUrls = document.getElementById("context_copyAllTabUrls"); + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URI1); + let browser = tab.linkedBrowser; + // Test copy tab url copies URL + await openTabContextMenu(tab); + EventUtils.synthesizeMouseAtCenter(copyTabUrl, {}); + let tabURI = await pasteFromClipboard(browser); + is(tabURI, URI1); + // Test copy all tab urls + Services.prefs.setBoolPref(COPY_ALL_URLS_PREF, true); + const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, URI2); + await openTabContextMenu(tab); + EventUtils.synthesizeMouseAtCenter(copyAllTabUrls, {}); + let tabURIs = await pasteFromClipboard(browser); + is(tabURIs, URI1 + "\n" + URI2); + // Test copy active tab pref + Services.prefs.setBoolPref(COPY_ACTIVE_URL_PREF, true); + await openTabContextMenu(tab); + EventUtils.synthesizeMouseAtCenter(copyTabUrl, {}); + let activeURI = await pasteFromClipboard(browser); + // URI2 should be active, so we copy from tab1 to verify + is(activeURI, URI2); + // Then we verify that URI1 is copied when active pref is false + Services.prefs.setBoolPref(COPY_ACTIVE_URL_PREF, false); + await openTabContextMenu(tab); + EventUtils.synthesizeMouseAtCenter(copyTabUrl, {}); + activeURI = await pasteFromClipboard(browser); + is(activeURI, URI1); + // Cleanup + Services.prefs.clearUserPref(COPY_ALL_URLS_PREF); + Services.prefs.clearUserPref(COPY_ACTIVE_URL_PREF); + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); +}); diff --git a/waterfox/browser/components/tabfeatures/test/browser/head.js b/waterfox/browser/components/tabfeatures/test/browser/head.js new file mode 100644 index 000000000000..1b41a70509bc --- /dev/null +++ b/waterfox/browser/components/tabfeatures/test/browser/head.js @@ -0,0 +1,93 @@ +"use strict"; + +const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); + +var { synthesizeDrop, synthesizeMouseAtCenter } = EventUtils; + +const COPY_URL_PREF = "browser.tabs.copyurl"; +const COPY_ALL_URLS_PREF = "browser.tabs.copyallurls"; +const COPY_ACTIVE_URL_PREF = "browser.tabs.copyurl.activetab"; +const DUPLICATE_TAB_PREF = "browser.tabs.duplicateTab"; +const RESTART_PREF = "browser.restart_menu.showpanelmenubtn"; + +const URI1 = "https://test1.example.com/"; +const URI2 = "https://example.com/"; + +let OS = AppConstants.platform; +/** + * Helper for opening the toolbar context menu. + */ +async function openTabContextMenu(tab) { + info("Opening tab context menu"); + let contextMenu = document.getElementById("tabContextMenu"); + let openTabContextMenuPromise = BrowserTestUtils.waitForPopupEvent( + contextMenu, + "shown" + ); + + EventUtils.synthesizeMouseAtCenter(tab, { type: "contextmenu" }); + await openTabContextMenuPromise; + return contextMenu; +} + +async function openAndCloseTabContextMenu(tab) { + await openTabContextMenu(tab); + info("Opened tab context menu"); + await EventUtils.synthesizeKey("VK_ESCAPE", {}); + info("Closed tab context menu"); +} + +/** + * Helper for opening the file menu. + */ +async function openFileMenu() { + info("Opening file menu"); + let fileMenu = document.getElementById("file-menu"); + let openFileMenuPromise = BrowserTestUtils.waitForPopupEvent( + fileMenu, + "shown" + ); + EventUtils.synthesizeMouseAtCenter(fileMenu, {}); + await openFileMenuPromise; + return fileMenu; +} + +async function openAndCloseFileMenu() { + await openFileMenu(); + await EventUtils.synthesizeKey("VK_ESCAPE", {}); + info("Closed file menu"); +} + +/** + * Helper for opening toolbar context menu. + */ +async function openToolbarContextMenu(contextMenu, target) { + let popupshown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter(target, { type: "contextmenu" }); + await popupshown; +} + +/** + * Helper to paste from clipboard + */ + +async function pasteFromClipboard(browser) { + return SpecialPowers.spawn(browser, [], () => { + let { document } = content; + document.body.contentEditable = true; + document.body.focus(); + let pastePromise = new Promise(resolve => { + document.addEventListener( + "paste", + e => { + resolve(e.clipboardData.getData("text/plain")); + }, + { once: true } + ); + }); + document.execCommand("paste"); + return pastePromise; + }); +}