From 4011ad75572aede3e383b5d855c62b854112920d Mon Sep 17 00:00:00 2001 From: aminomancer <33384265+aminomancer@users.noreply.github.com> Date: Thu, 16 Jun 2022 06:24:41 -0700 Subject: [PATCH] 3.1.9: Update several scripts. Add styles for the statusbar webrtc indicator. Switch to tab-size 2 (refactor all CSS and JS files) --- JS/aboutCfg.uc.js | 150 +- JS/animateContextMenus.uc.js | 124 +- JS/appMenuAboutConfigButton.uc.js | 28 +- JS/atoolboxButton.uc.js | 990 ++-- JS/autoHideNavbarSupport.uc.js | 239 +- JS/autocompletePopupStyler.uc.js | 83 +- JS/bookmarksMenuAndButtonShortcuts.uc.js | 298 +- JS/bookmarksPopupShadowRoot.uc.js | 224 +- JS/clearDownloadsButton.uc.js | 152 +- JS/copyCurrentUrlHotkey.uc.js | 151 +- JS/customHintProvider.uc.js | 395 +- JS/debugExtensionInToolbarContextMenu.uc.js | 70 +- JS/downloadsDeleteFileCommand.uc.js | 204 +- JS/enterInUrlbarToRefresh.uc.js | 252 +- JS/extensionOptionsPanel.uc.js | 1754 +++---- JS/extensionStylesheetLoader.uc.js | 197 +- JS/eyedropperButton.uc.js | 6 +- JS/findbarMods.uc.js | 1028 ++-- JS/fixTitlebarTooltips.uc.js | 15 +- JS/floatingSidebarResizer.uc.js | 234 +- JS/fluentRevealNavbar.uc.js | 425 +- JS/fluentRevealTabs.uc.js | 574 +- JS/fullscreenHotkey.uc.js | 33 +- ...kingProtectionIconOnCustomNewTabPage.uc.js | 225 +- JS/letCtrlWClosePinnedTabs.uc.js | 97 +- JS/miscMods.uc.js | 779 +-- JS/navbarToolbarButtonSlider.uc.js | 1492 +++--- JS/oneClickOneOffSearchButtons.uc.js | 531 +- JS/openBookmarkInContainerTab.uc.js | 601 +-- JS/openBookmarksHistoryEtcInNewTabs.uc.js | 17 +- JS/openLinkInUnloadedTab.uc.js | 818 +-- JS/osDetector.uc.js | 15 +- JS/pinTabHotkey.uc.js | 28 +- JS/privateTabs.uc.js | 1168 ++--- JS/privateWindowHomepage.uc.js | 50 +- JS/recentlyClosedTabsContextMenu.uc.js | 199 +- JS/removeSearchEngineAliasFormatting.uc.js | 70 +- ...orePreProtonLibraryButton-standalone.uc.js | 334 +- JS/restorePreProtonLibraryButton.uc.js | 322 +- ...estorePreProtonStarButton-standalone.uc.js | 227 +- JS/restorePreProtonStarButton.uc.js | 219 +- JS/screenshotPageActionButton.uc.js | 262 +- JS/scrollingOneOffs.uc.js | 289 +- JS/searchSelectionShortcut.uc.js | 433 +- JS/showSelectedSidebarInSwitcherPanel.uc.js | 88 +- JS/tabAnimation.uc.js | 85 +- JS/tabContextMenuNavigation.uc.js | 351 +- JS/tabLoadingSpinner.uc.js | 57 +- JS/tabThumbnailTooltip.uc.js | 380 +- JS/tabTooltipNavButtons.uc.js | 1597 +++--- JS/toggleMenubarHotkey.uc.js | 88 +- JS/tooltipShadowSupport.uc.js | 25 +- JS/trackingProtectionMiddleClickToggle.uc.js | 150 +- JS/unreadTabMods.uc.js | 351 +- JS/updateBannerLabels.uc.js | 144 +- JS/updateNotificationSlayer.uc.js | 269 +- JS/urlbarContainerColor.uc.js | 93 +- JS/urlbarMods.uc.js | 320 +- JS/urlbarMouseWheelScroll.uc.js | 178 +- JS/urlbarNotificationIconsOpenStatus.uc.js | 79 +- JS/urlbarViewScrollSelect.uc.js | 187 +- JS/userChromeAgentAuthorSheetLoader.uc.js | 103 +- JS/userChromeDevtoolsSheetLoader.uc.js | 168 +- JS/verticalTabsPane.uc.js | 56 +- JS/windowDragHotkey.uc.js | 121 +- README.md | 6 +- custom-chrome.css | 2 +- resources/aboutconfig/config.css | 44 +- resources/in-content/custom-content.css | 14 +- resources/in-content/devtools.css | 979 ++-- resources/in-content/downloads.css | 851 +-- resources/in-content/ext-bitwarden.css | 223 +- resources/in-content/ext-containers.css | 2035 ++++---- resources/in-content/ext-darkreader.css | 455 +- resources/in-content/ext-firefoxnotes.css | 635 ++- resources/in-content/ext-mdm.css | 130 +- resources/in-content/ext-nordvpn.css | 475 +- resources/in-content/ext-privatebookmarks.css | 619 +-- resources/in-content/ext-simpletranslate.css | 315 +- resources/in-content/ext-sortbookmarks.css | 430 +- resources/in-content/ext-tabnotes.css | 18 +- resources/in-content/ext-ublock.css | 278 - resources/in-content/library.css | 133 +- resources/in-content/menus.css | 485 +- resources/in-content/misc-content.css | 2 +- resources/in-content/picture-in-picture.css | 136 +- resources/in-content/root.css | 110 +- resources/in-content/site-github.css | 124 +- resources/in-content/site-google.css | 22 +- resources/in-content/site-mozilla.css | 160 +- resources/in-content/site-reddit.css | 44 +- resources/in-content/site-subscene.css | 96 +- resources/in-content/site-wiki.css | 4618 +++++++++-------- resources/in-content/site-youtube.css | 24 +- resources/in-content/system.css | 1711 +++--- resources/layout/XMLPrettyPrint.css | 58 +- resources/layout/arrowscrollbox.css | 38 +- .../contentaccessible/ImageDocument.css | 20 +- .../layout/contentaccessible/plaintext.css | 32 +- .../layout/contentaccessible/viewsource.css | 166 +- resources/layout/uc-low-globals.css | 26 +- resources/layout/uc-nsRules.au.css | 605 ++- resources/notifications/statusbar/camera.png | Bin 0 -> 292 bytes resources/notifications/statusbar/camera.svg | 6 + .../notifications/statusbar/microphone.png | Bin 0 -> 379 bytes .../notifications/statusbar/microphone.svg | 7 + resources/notifications/statusbar/screen.png | Bin 0 -> 234 bytes resources/notifications/statusbar/screen.svg | 7 + uc-app-menu.css | 691 +-- uc-bookmarks.css | 736 +-- uc-context-menu-icons.css | 662 ++- uc-context-menus.css | 576 +- uc-ctrl-tab.css | 122 +- uc-extensions.css | 980 ++-- uc-findbar.css | 415 +- uc-fullscreen.css | 153 +- uc-globals.css | 897 ++-- uc-misc.css | 867 ++-- uc-navbar.css | 990 ++-- uc-panels.css | 1516 +++--- uc-popups.css | 3217 ++++++------ uc-search-mode-icons.css | 91 +- uc-search-one-offs.css | 336 +- uc-sidebar.css | 630 +-- uc-tabs-bar.css | 904 ++-- uc-tabs.css | 918 ++-- uc-urlbar-results.css | 940 ++-- uc-urlbar.css | 1352 ++--- uc-variables.css | 408 +- userChrome.ag.css | 809 +-- userChrome.au.css | 3895 +++++++------- userChrome.css | 7 +- 132 files changed, 29839 insertions(+), 29104 deletions(-) create mode 100644 resources/notifications/statusbar/camera.png create mode 100644 resources/notifications/statusbar/camera.svg create mode 100644 resources/notifications/statusbar/microphone.png create mode 100644 resources/notifications/statusbar/microphone.svg create mode 100644 resources/notifications/statusbar/screen.png create mode 100644 resources/notifications/statusbar/screen.svg diff --git a/JS/aboutCfg.uc.js b/JS/aboutCfg.uc.js index 445ea383..5106ba35 100644 --- a/JS/aboutCfg.uc.js +++ b/JS/aboutCfg.uc.js @@ -10,19 +10,21 @@ // user configuration const config = { - // the value to put after "about:" — if this is "cfg" then the final URl will be "about:cfg". if - // you use this and my appMenuAboutConfigButton.uc.js script, and you want to change this - // address for whatever reason, be sure to edit the urlOverride setting in that script so it - // says "about:your-new-address" - address: "cfg", + // the value to put after "about:" — if this is "cfg" then the final URl will + // be "about:cfg". if you use this and my appMenuAboutConfigButton.uc.js + // script, and you want to change this address for whatever reason, be sure to + // edit the urlOverride setting in that script so it says + // "about:your-new-address" + address: "cfg", - // the script tries to automatically find earthlng's aboutconfig URL, e.g. - // "chrome://userchrome/content/aboutconfig/config.xhtml" if you followed the instructions on my - // repo for making it compatible with fx-autoconfig. alternatively, it should also be able to - // find the URL if you use earthlng's autoconfig loader or xiaoxiaoflood's, and didn't modify - // anything. if it's unable to find the URL for your particular setup, please find it yourself - // and paste it here, *inside the quotes* - pathOverride: "", + // the script tries to automatically find earthlng's aboutconfig URL, e.g. + // "chrome://userchrome/content/aboutconfig/config.xhtml" if you followed the + // instructions on my repo for making it compatible with fx-autoconfig. + // alternatively, it should also be able to find the URL if you use earthlng's + // autoconfig loader or xiaoxiaoflood's, and didn't modify anything. if it's + // unable to find the URL for your particular setup, please find it yourself + // and paste it here, *inside the quotes* + pathOverride: "", }; let { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); @@ -30,49 +32,45 @@ let { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Compo let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); function findAboutConfig() { - if (config.pathOverride) return config.pathOverride; - let dir = Services.dirsvc.get("UChrm", Ci.nsIFile); - let appendFn = (nm) => dir.append(nm); + if (config.pathOverride) return config.pathOverride; + let dir = Services.dirsvc.get("UChrm", Ci.nsIFile); + let appendFn = nm => dir.append(nm); - // fx-autoconfig - ["resources", "aboutconfig", "config.xhtml"].forEach(appendFn); - if (dir.exists()) return "chrome://userchrome/content/aboutconfig/config.xhtml"; + // fx-autoconfig + ["resources", "aboutconfig", "config.xhtml"].forEach(appendFn); + if (dir.exists()) return "chrome://userchrome/content/aboutconfig/config.xhtml"; - // earthlng's loader - dir = Services.dirsvc.get("UChrm", Ci.nsIFile); - ["utils", "aboutconfig", "config.xhtml"].forEach(appendFn); - if (dir.exists()) return "chrome://userchromejs/content/aboutconfig/config.xhtml"; + // earthlng's loader + dir = Services.dirsvc.get("UChrm", Ci.nsIFile); + ["utils", "aboutconfig", "config.xhtml"].forEach(appendFn); + if (dir.exists()) return "chrome://userchromejs/content/aboutconfig/config.xhtml"; - // xiaoxiaoflood's loader - dir = Services.dirsvc.get("UChrm", Ci.nsIFile); - ["utils", "aboutconfig", "aboutconfig.xhtml"].forEach(appendFn); - if (dir.exists()) return "chrome://userchromejs/content/aboutconfig/aboutconfig.xhtml"; + // xiaoxiaoflood's loader + dir = Services.dirsvc.get("UChrm", Ci.nsIFile); + ["utils", "aboutconfig", "aboutconfig.xhtml"].forEach(appendFn); + if (dir.exists()) return "chrome://userchromejs/content/aboutconfig/aboutconfig.xhtml"; - // no about:config replacement found - return false; + // no about:config replacement found + return false; } -// generate a unique ID on every app launch. protection against the very unlikely possibility that a -// future update adds a component with the same class ID, which would break the script. +// generate a unique ID on every app launch. protection against the very +// unlikely possibility that a future update adds a component with the same +// class ID, which would break the script. function generateFreeCID() { - let uuid = Components.ID( - Cc["@mozilla.org/uuid-generator;1"] - .getService(Ci.nsIUUIDGenerator) - .generateUUID() - .toString() + let uuid = Components.ID( + Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString() + ); + // I can't tell whether generateUUID is guaranteed to produce a unique ID, or + // just a random ID. so I add this loop to regenerate it in the extremely + // unlikely (or potentially impossible) event that the UUID is already + // registered as a CID. + while (registrar.isCIDRegistered(uuid)) { + uuid = Components.ID( + Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString() ); - // I can't tell whether generateUUID is guaranteed to produce a unique ID, or just a random ID. - // so I add this loop to regenerate it in the extremely unlikely (or potentially impossible) - // event that the UUID is already registered as a CID. - while (registrar.isCIDRegistered(uuid)) { - uuid = Components.ID( - Cc["@mozilla.org/uuid-generator;1"] - .getService(Ci.nsIUUIDGenerator) - .generateUUID() - .toString() - ); - } - return uuid; + } + return uuid; } function VintageAboutConfig() {} @@ -80,40 +78,40 @@ function VintageAboutConfig() {} let urlString = findAboutConfig(); VintageAboutConfig.prototype = { - get uri() { - if (!urlString) return null; - return this._uri || (this._uri = Services.io.newURI(urlString)); - }, - newChannel: function (_uri, loadInfo) { - const ch = Services.io.newChannelFromURIWithLoadInfo(this.uri, loadInfo); - ch.owner = Services.scriptSecurityManager.getSystemPrincipal(); - return ch; - }, - getURIFlags: function (_uri) { - return Ci.nsIAboutModule.ALLOW_SCRIPT | Ci.nsIAboutModule.IS_SECURE_CHROME_UI; - }, - getChromeURI: function (_uri) { - return this.uri; - }, - QueryInterface: ChromeUtils.generateQI(["nsIAboutModule"]), + get uri() { + if (!urlString) return null; + return this._uri || (this._uri = Services.io.newURI(urlString)); + }, + newChannel: function (_uri, loadInfo) { + const ch = Services.io.newChannelFromURIWithLoadInfo(this.uri, loadInfo); + ch.owner = Services.scriptSecurityManager.getSystemPrincipal(); + return ch; + }, + getURIFlags: function (_uri) { + return Ci.nsIAboutModule.ALLOW_SCRIPT | Ci.nsIAboutModule.IS_SECURE_CHROME_UI; + }, + getChromeURI: function (_uri) { + return this.uri; + }, + QueryInterface: ChromeUtils.generateQI(["nsIAboutModule"]), }; var AboutModuleFactory = { - createInstance(aOuter, aIID) { - if (aOuter) { - throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION); - } - return new VintageAboutConfig().QueryInterface(aIID); - }, - QueryInterface: ChromeUtils.generateQI(["nsIFactory"]), + createInstance(aOuter, aIID) { + if (aOuter) { + throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION); + } + return new VintageAboutConfig().QueryInterface(aIID); + }, + QueryInterface: ChromeUtils.generateQI(["nsIFactory"]), }; if (urlString) - registrar.registerFactory( - generateFreeCID(), - `about:${config.address}`, - `@mozilla.org/network/protocol/about;1?what=${config.address}`, - AboutModuleFactory - ); + registrar.registerFactory( + generateFreeCID(), + `about:${config.address}`, + `@mozilla.org/network/protocol/about;1?what=${config.address}`, + AboutModuleFactory + ); let EXPORTED_SYMBOLS = []; diff --git a/JS/animateContextMenus.uc.js b/JS/animateContextMenus.uc.js index 2293b433..57e7b93f 100644 --- a/JS/animateContextMenus.uc.js +++ b/JS/animateContextMenus.uc.js @@ -3,75 +3,81 @@ // @version 1.0.1 // @author aminomancer // @homepage https://github.com/aminomancer/uc.css.js -// @description Give all context menus the same opening animation that panel popups like the app menu have — the menu slides down 70px and fades in opacity at the same time. It's a cool effect that doesn't trigger a reflow since it uses transform, but it does repaint the menu, so I wouldn't recommend using this on weak hardware. +// @description Give all context menus the same opening animation that panel +// popups like the app menu have — the menu slides down 70px and fades in +// opacity at the same time. It's a cool effect that doesn't trigger a reflow +// since it uses transform, but it does repaint the menu, so I wouldn't +// recommend using this on weak hardware. // @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. // @include * // ==/UserScript== class AnimateContextMenus { - constructor() { - document.documentElement.setAttribute("animate-menupopups", true); - addEventListener("popupshowing", this); - addEventListener("popupshown", this); - addEventListener("popuphidden", this); - let css = ` -:root[animate-menupopups] :not(menulist) > menupopup:not([position], [type="arrow"], [animate="false"]) { - opacity: 0; - transform: translateY(-70px) scaleX(0.95) scaleY(0.5); - transform-origin: top; - transition-property: transform, opacity; - transition-duration: 0.18s, 0.18s; - transition-timing-function: - var(--animation-easing-function, cubic-bezier(.07, .95, 0, 1)), ease-out; - transform-style: flat; - backface-visibility: hidden; + constructor() { + document.documentElement.setAttribute("animate-menupopups", true); + addEventListener("popupshowing", this); + addEventListener("popupshown", this); + addEventListener("popuphidden", this); + let css = `:root[animate-menupopups] + :not(menulist) + > menupopup:not([position], [type="arrow"], [animate="false"]) { + opacity: 0; + transform: translateY(-70px) scaleX(0.95) scaleY(0.5); + transform-origin: top; + transition-property: transform, opacity; + transition-duration: 0.18s, 0.18s; + transition-timing-function: var(--animation-easing-function, cubic-bezier(0.07, 0.95, 0, 1)), + ease-out; + transform-style: flat; + backface-visibility: hidden; } -:root[animate-menupopups] :not(menulist) > menupopup:not([position], [type="arrow"])[animate][animate="open"] { - opacity: 1.0; - transition-duration: 0.18s, 0.18s; - transform: none !important; - transition-timing-function: - var(--animation-easing-function, cubic-bezier(.07, .95, 0, 1)), ease-in-out; +:root[animate-menupopups] + :not(menulist) + > menupopup:not([position], [type="arrow"])[animate][animate="open"] { + opacity: 1; + transition-duration: 0.18s, 0.18s; + transform: none !important; + transition-timing-function: var(--animation-easing-function, cubic-bezier(0.07, 0.95, 0, 1)), + ease-in-out; } -:root[animate-menupopups] :not(menulist) > menupopup:not([position], [type="arrow"])[animate][animate="cancel"] { - transform: none; +:root[animate-menupopups] + :not(menulist) + > menupopup:not([position], [type="arrow"])[animate][animate="cancel"] { + transform: none; } :root[animate-menupopups] :not(menulist) > menupopup:not([position], [type="arrow"])[animating] { - pointer-events: none; -} -`; - const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - let sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService( - Ci.nsIStyleSheetService - ); - let uri = Services.io.newURI("data:text/css;charset=UTF=8," + encodeURIComponent(css)); - if (!sss.sheetRegistered(uri, sss.AUTHOR_SHEET)) - sss.loadAndRegisterSheet(uri, sss.AUTHOR_SHEET); - } - handleEvent(e) { - if (e.target.tagName !== "menupopup") return; - if (e.target.hasAttribute("position")) return; - if (e.target.getAttribute("type") == "arrow") return; - if (e.target.parentElement) if (e.target.parentElement.tagName == "menulist") return; - if ( - e.target.shadowRoot && - e.target.shadowRoot.firstElementChild.classList.contains("panel-arrowcontainer") - ) - return; - this[`on_${e.type}`](e); - } - on_popupshowing(e) { - if (e.target.getAttribute("animate") != "false") { - e.target.setAttribute("animate", "open"); - e.target.setAttribute("animating", "true"); - } - } - on_popupshown(e) { - e.target.removeAttribute("animating"); - } - on_popuphidden(e) { - if (e.target.getAttribute("animate") != "false") e.target.removeAttribute("animate"); + pointer-events: none; +}`; + const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + let sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService); + let uri = Services.io.newURI("data:text/css;charset=UTF=8," + encodeURIComponent(css)); + if (!sss.sheetRegistered(uri, sss.AUTHOR_SHEET)) + sss.loadAndRegisterSheet(uri, sss.AUTHOR_SHEET); + } + handleEvent(e) { + if (e.target.tagName !== "menupopup") return; + if (e.target.hasAttribute("position")) return; + if (e.target.getAttribute("type") == "arrow") return; + if (e.target.parentElement) if (e.target.parentElement.tagName == "menulist") return; + if ( + e.target.shadowRoot && + e.target.shadowRoot.firstElementChild.classList.contains("panel-arrowcontainer") + ) + return; + this[`on_${e.type}`](e); + } + on_popupshowing(e) { + if (e.target.getAttribute("animate") != "false") { + e.target.setAttribute("animate", "open"); + e.target.setAttribute("animating", "true"); } + } + on_popupshown(e) { + e.target.removeAttribute("animating"); + } + on_popuphidden(e) { + if (e.target.getAttribute("animate") != "false") e.target.removeAttribute("animate"); + } } new AnimateContextMenus(); diff --git a/JS/appMenuAboutConfigButton.uc.js b/JS/appMenuAboutConfigButton.uc.js index ecf32b86..a295adbc 100644 --- a/JS/appMenuAboutConfigButton.uc.js +++ b/JS/appMenuAboutConfigButton.uc.js @@ -24,20 +24,20 @@ // user configuration const config = { urlOverride: "", - /* the script tries to automatically find earthlng's aboutconfig URL, - and if it can't be found, uses the built-in about:config URL instead. if - it's unable to find the URL for your particular setup, or if you just - want to use the vanilla about:config page, replace this empty string - with your preferred URL, in quotes. if you want to use my about:cfg - script that registers earthlng's aboutconfig page to about:cfg, and you - want the about:config button to take you to about:cfg, then leave this - empty. it will automatically use about:cfg if the script exists. if - about:cfg doesn't work for you then change the pathOverride in *that* - script instead of setting urlOverride in this one. if you changed the - address (the "cfg" string) in that script, you'll need to use - urlOverride here if you want the button to direct to earthlng's - aboutconfig page. so if for example you changed the address to "config2" - then change urlOverride above to "about:config2" */ + /* the script tries to automatically find earthlng's aboutconfig URL, and if + it can't be found, uses the built-in about:config URL instead. if it's + unable to find the URL for your particular setup, or if you just want to + use the vanilla about:config page, replace this empty string with your + preferred URL, in quotes. if you want to use my about:cfg script that + registers earthlng's aboutconfig page to about:cfg, and you want the + about:config button to take you to about:cfg, then leave this empty. it + will automatically use about:cfg if the script exists. if about:cfg + doesn't work for you then change the pathOverride in *that* script + instead of setting urlOverride in this one. if you changed the address + (the "cfg" string) in that script, you'll need to use urlOverride here + if you want the button to direct to earthlng's aboutconfig page. so if + for example you changed the address to "config2" then change urlOverride + above to "about:config2" */ }; let { interfaces: Ci, manager: Cm } = Components; diff --git a/JS/atoolboxButton.uc.js b/JS/atoolboxButton.uc.js index c41bb697..d80d6253 100644 --- a/JS/atoolboxButton.uc.js +++ b/JS/atoolboxButton.uc.js @@ -1,499 +1,491 @@ -// ==UserScript== -// @name Toolbox Button -// @version 1.2.6 -// @author aminomancer -// @homepage https://github.com/aminomancer/uc.css.js -// @description Adds a new toolbar button that 1) opens the content toolbox on left click; -// 2) opens the browser toolbox on right click; 3) toggles "Popup Auto-Hide" on middle click. -// Left click will open the toolbox for the active tab, or close it if it's already open. Right click -// will open the elevated browser toolbox if it's not already open. If it is already open, then -// instead of trying to start a new process and spawning an irritating dialog, it'll just show a -// brief notification saying the toolbox is already open. The button also shows a badge while a -// toolbox window is open. Middle click will toggle the preference for popup auto-hide: -// "ui.popup.disable_autohide". This does the same thing as the "Disable Popup Auto-Hide" option in -// the menu at the top right of the browser toolbox, prevents popups from closing so you can debug -// them. If you want to change which mouse buttons execute which functions, search for -// "userChrome.toolboxButton.mouseConfig" in about:config. Change the 0, 1, and 2 values. 0 = left -// click, 1 = middle, and 2 = right. By default, when you open a browser toolbox window, the script -// will disable popup auto-hide, and then re-enable it when you close the toolbox. I find that I -// usually want popup auto-hide disabled when I'm using the toolbox, and never want it disabled when -// I'm not using the toolbox, so I made it automatic, instead of having to right click and then -// immediately middle click every time. If you don't like this automatic feature, you can turn it -// off by setting "userChrome.toolboxButton.popupAutohide.toggle-on-toolbox-launch" to false in -// about:config. When you middle click, the button will show a notification telling you the current -// status of popup auto-hide, e.g. "Holding popups open." This is just so that people who use the -// feature a lot won't lose track of whether it's on or off, and won't need to open a popup and try -// to close it to test it. (The toolbar button also changes appearance while popup auto-hide is -// disabled. It becomes blue like the downloads button and the icon changes into a popup icon. This -// change is animated, as long as the user doesn't have reduced motion enabled) All of these -// notifications use the native confirmation hint custom element, since it looks nice. That's the -// one that appears when you save a bookmark, #confirmation-hint. So you can customize them with -// that selector. -// @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. -// ==/UserScript== - -// Modify these strings for easy localization. I tried to use built-in strings for this so it would -// automatically localize itself, but I found that every reference to the "Browser Toolbox" -// throughout the entire firefox UI is derived from a single message in a single localization file, -// which doesn't follow the standard format. It can only be parsed by the devtools' own special l10n -// module, which itself can only be imported by a CJS module. Requiring CJS just for a button seems -// ridiculous, plus there really aren't any localized strings that work for these confirmation -// messages anyway, or even the tooltip. So if your UI language isn't English you can modify all the -// strings created by this script in the following object: -const toolboxButtonL10n = { - // Confirmation hint. You receive this message when you right click the toolbox button, but a - // toolbox process for the window is already open. You can only have one toolbox open - // per-window. So if I have 3 windows open, and I right-click the toolbox button in window 1, - // then it'll launch a browser toolbox for window 1. If I then right-click the toolbox button in - // window 2, it'll launch a browser toolbox for window 2. But if I go back to window 1 and - // right-click the toolbox button a second time, it will do nothing except show a brief - // confirmation hint to explain the lack of action. - alreadyOpenMsg: "Browser Toolbox is already open.", - // Confirmation hint. This appears when you first middle-click the toolbox button. It signifies - // that popups are being kept open. That is, "popup auto-hide" has been temporarily disabled. - holdingOpenMsg: "Holding popups open.", - // Confirmation hint. This appears when you middle-click the toolbox button a second time, - // toggling "popup auto-hide" back on, thereby allowing popups to close on their own. - lettingCloseMsg: "Letting popups close.", - menuBundle: Services.strings.createBundle("chrome://devtools/locale/menus.properties"), - toolboxBundle: Services.strings.createBundle("chrome://devtools/locale/toolbox.properties"), - getString(name, where) { - return this[`${where}Bundle`].GetStringFromName(name); - }, - get contentLabel() { - return ( - this._contentLabel || - (this._contentLabel = this.getString("browserContentToolboxMenu.label", "menu")) - ); - }, - get browserLabel() { - return ( - this._browserLabel || - (this._browserLabel = this.getString("browserToolboxMenu.label", "menu")) - ); - }, - get autoHideLabel() { - return ( - this._autoHideLabel || - (this._autoHideLabel = this.getString( - "toolbox.meatballMenu.noautohide.label", - "toolbox" - )) - ); - }, - get defaultLabel() { - return this.contentLabel; - }, - get defaultTooltip() { - return this.defaultLabel; - }, -}; - -(() => { - let toolboxLauncher = ChromeUtils.import( - "resource://devtools/client/framework/browser-toolbox/Launcher.jsm" - ).BrowserToolboxLauncher; - - if ( - /^chrome:\/\/browser\/content\/browser.(xul||xhtml)$/i.test(location) && - !CustomizableUI.getPlacementOfWidget("toolbox-button", true) - ) - CustomizableUI.createWidget({ - id: "toolbox-button", - type: "custom", - defaultArea: CustomizableUI.AREA_NAVBAR, - label: toolboxButtonL10n.defaultLabel, - removable: true, - overflows: true, - tooltiptext: toolboxButtonL10n.defaultTooltip, - onBuild: function (aDoc) { - let CustomHint = { - _timerID: null, - - /** - * Shows a transient, non-interactive confirmation hint anchored to an element, - * usually used in response to a user action to reaffirm that it was successful - * and potentially provide extra context. - * - * @param anchor (DOM node, required) - * The anchor for the panel. A value of null will anchor to the - * viewpoint (see options.x below) - * @param message (string, required) - * The message to be shown. - * @param options (object, optional) - * An object with any number of the following optional properties: - * - event (DOM event): The event that triggered the feedback. - * - hideArrow (boolean): Optionally hide the arrow. - * - hideCheck (boolean): Optionally hide the checkmark. - * - description (string): If provided, show a more detailed - * description/subtitle with the passed text. - * - duration (numeric): How long the hint should stick around, in - * milliseconds. Default is 1500 — 1.5 seconds. - * - position (string): One of a number of strings representing how the anchor point of the popup - * is aligned relative to the anchor point of the anchor node. - * Possible values for position are: - * before_start, before_end, after_start, after_end, - * start_before, start_after, end_before, end_after, - * overlap, after_pointer - * For example, after_start means the anchor node's bottom left corner will - * be aligned with the popup node's top left corner. overlap means their - * top left corners will be lined up exactly, so they will overlap. - * - x (number): Horizontal offset in pixels, relative to the anchor. - * If no anchor is provided, relative to the viewport. - * - y (number): Vertical offset in pixels, relative to the anchor. Negative - * values may also be used to move to the left and upwards respectively. - * Unanchored popups may be created by supplying null as the - * anchor node. An unanchored popup appears at the position - * specified by x and y, relative to the viewport of the document - * containing the popup node. (ignoring the anchor parameter) - * - */ - show(anchor, message, options = {}) { - this._reset(); - - this._message.textContent = message; - - if (options.description) { - this._description.textContent = options.description; - this._description.hidden = false; - this._panel.classList.add("with-description"); - } else { - this._description.hidden = true; - this._panel.classList.remove("with-description"); - } - - if (options.hideArrow) { - this._panel.setAttribute("hidearrow", "true"); - } - - if (options.hideCheck) { - this._animationBox.setAttribute("hidden", "true"); - this._panel.setAttribute("data-message-id", "hideCheckHint"); - } else this._panel.setAttribute("data-message-id", "checkmarkHint"); - - const DURATION = options.duration || 1500; - this._panel.addEventListener( - "popupshown", - () => { - this._animationBox.setAttribute("animate", "true"); - this._timerID = setTimeout(() => { - this._panel.hidePopup(true); - this._animationBox.removeAttribute("hidden"); - }, DURATION + 120); - }, - { once: true } - ); - - this._panel.addEventListener( - "popuphidden", - () => { - // reset the timerId in case our timeout wasn't the cause of the popup being hidden - this._reset(); - }, - { once: true } - ); - - let { position, x, y } = options; - this._panel.openPopup(null, { position, triggerEvent: options.event }); - this._panel.moveToAnchor(anchor, position, x, y); - }, - - _reset() { - if (this._timerID) { - clearTimeout(this._timerID); - this._timerID = null; - this._animationBox.removeAttribute("hidden"); - } - if (this.__panel) { - this._panel.removeAttribute("hidearrow"); - this._animationBox.removeAttribute("animate"); - this._panel.removeAttribute("data-message-id"); - this._panel.hidePopup(); - } - }, - - get _panel() { - this._ensurePanel(); - return this.__panel; - }, - - get _animationBox() { - this._ensurePanel(); - delete this._animationBox; - return (this._animationBox = aDoc.getElementById( - "confirmation-hint-checkmark-animation-container" - )); - }, - - get _message() { - this._ensurePanel(); - delete this._message; - return (this._message = aDoc.getElementById("confirmation-hint-message")); - }, - - get _description() { - this._ensurePanel(); - delete this._description; - return (this._description = aDoc.getElementById( - "confirmation-hint-description" - )); - }, - - _ensurePanel() { - if (!this.__panel) { - // hook into the built-in confirmation hint element - let wrapper = aDoc.getElementById("confirmation-hint-wrapper"); - wrapper?.replaceWith(wrapper.content); - this.__panel = aDoc.getElementById("confirmation-hint"); - ConfirmationHint.__panel = aDoc.getElementById("confirmation-hint"); - } - }, - }; - - let toolbarbutton = aDoc.createXULElement("toolbarbutton"); - let badgeStack = aDoc.createXULElement("stack"); - let icon = aDoc.createXULElement("image"); - let label = aDoc.createXULElement("label"); - let badgeLabel = aDoc.createElement("label"); - for (const [key, val] of Object.entries({ - class: "toolbarbutton-1 chromeclass-toolbar-additional", - badged: true, - label: toolboxButtonL10n.defaultLabel, - id: "toolbox-button", - role: "button", - icon: "toolbox", - removable: true, - overflows: true, - tooltiptext: toolboxButtonL10n.defaultTooltip, - })) - toolbarbutton.setAttribute(key, val); - - toolbarbutton.appendChild(badgeStack); - badgeStack.after(label); - badgeStack.appendChild(icon); - icon.after(badgeLabel); - badgeStack.setAttribute("class", "toolbarbutton-badge-stack"); - icon.setAttribute("class", "toolbarbutton-icon"); - badgeLabel.setAttribute("class", "toolbarbutton-badge"); - for (const [key, val] of Object.entries({ - class: "toolbarbutton-text", - crop: "right", - flex: "1", - value: toolboxButtonL10n.defaultLabel, - })) - label.setAttribute(key, val); - - let prefSvc = Services.prefs; - let obSvc = Services.obs; - let toolboxBranch = "userChrome.toolboxButton"; - let autoHide = "ui.popup.disable_autohide"; - let autoTogglePopups = - "userChrome.toolboxButton.popupAutohide.toggle-on-toolbox-launch"; - let mouseConfig = "userChrome.toolboxButton.mouseConfig"; - - let onClick = function (e) { - let { button } = e; - if (e.getModifierState("Accel")) { - if (button == 2) return; - if (button == 0 && AppConstants.platform == "macosx") button = 2; - } - switch (button) { - case this.mouseConfig.contentToolbox: - // toggle the content toolbox - aDoc.defaultView.key_toggleToolbox.click(); - break; - case this.mouseConfig.browserToolbox: - toolboxLauncher.getBrowserToolboxSessionState() // check if a browser toolbox window is already open - ? CustomHint.show(toolbarbutton, toolboxButtonL10n.alreadyOpenMsg, { - event: e, - hideCheck: true, - }) // if so, just show a hint that it's already open - : aDoc.defaultView.key_browserToolbox.click(); // if not, launch a new one - break; - case this.mouseConfig.popupHide: - CustomHint.show( - toolbarbutton, - toolboxButtonL10n[ - this.popupAutoHide ? "lettingCloseMsg" : "holdingOpenMsg" - ], - { event: e, hideCheck: this.popupAutoHide } - ); - // toggle the pref - prefSvc.setBoolPref(autoHide, !this.popupAutoHide); - // animate the icon transformation - this.triggerAnimation(); - break; - default: - return; - } - e.preventDefault(); - }; - - if (AppConstants.platform === "macosx") { - toolbarbutton.onmousedown = onClick; - toolbarbutton.onclick = (e) => { - if (e.getModifierState("Accel")) return; - e.preventDefault(); - }; - } else toolbarbutton.onclick = onClick; - - toolbarbutton.triggerAnimation = function () { - this.addEventListener( - "animationend", - () => { - this.removeAttribute("animate"); - }, - { once: true } - ); - this.setAttribute("animate", "true"); - }; - - function getPref(root, pref) { - switch (root.getPrefType(pref)) { - case root.PREF_BOOL: - return root.getBoolPref(pref); - case root.PREF_INT: - return root.getIntPref(pref); - case root.PREF_STRING: - return root.getStringPref(pref); - default: - return null; - } - } - - function prefObserver(sub, _top, pref) { - let value = getPref(sub, pref); - switch (pref) { - case autoHide: - if (value === null) value = false; - toolbarbutton.popupAutoHide = value; - if (value) { - // change icon src to popup icon - toolbarbutton.setAttribute("icon", "autohide"); - // highlight color - icon.style.fill = "var(--toolbarbutton-icon-fill-attention)"; - } else { - // change icon src to toolbox icon - toolbarbutton.setAttribute("icon", "toolbox"); - // un-highlight color - icon.style.removeProperty("fill"); - } - break; - case autoTogglePopups: - if (value === null) value = true; - toolbarbutton.autoTogglePopups = value; - break; - case mouseConfig: - if (value === null) - value = { - "contentToolbox": 0, - "browserToolbox": 2, - "popupHide": 1, - }; - toolbarbutton.mouseConfig = JSON.parse(value); - toolbarbutton.setStrings(); - break; - } - } - - /** - * listen for toolboxes opening and closing - */ - function toolboxObserver(sub, _top, _data) { - // whether a toolbox is open - let state = toolboxLauncher.getBrowserToolboxSessionState(); - // set toolbar button's badge content - badgeLabel.textContent = state ? 1 : ""; - // if toolbox is open and autohide is not already enabled, enable it - if (sub === "initial-load" || !toolbarbutton.autoTogglePopups) return; - if (state && !toolbarbutton.popupAutoHide) prefSvc.setBoolPref(autoHide, true); - // if toolbox just closed and autohide is not already disabled, disable it - else if (!state && toolbarbutton.popupAutoHide) - prefSvc.setBoolPref(autoHide, false); - } - - toolbarbutton.setStrings = function () { - let hotkey, labelString; - for (const [key, val] of Object.entries(toolbarbutton.mouseConfig)) - if (val === 0) - switch (key) { - case "contentToolbox": - labelString = toolboxButtonL10n.contentLabel; - hotkey = aDoc.defaultView.key_toggleToolbox; - break; - case "browserToolbox": - labelString = toolboxButtonL10n.browserLabel; - hotkey = aDoc.defaultView.key_browserToolbox; - break; - case "popupHide": - labelString = toolboxButtonL10n.autoHideLabel; - break; - } - let shortcut = hotkey ? ` (${ShortcutUtils.prettifyShortcut(hotkey)})` : ""; - toolbarbutton.label = labelString; - label.value = labelString; - toolbarbutton.tooltipText = `${labelString}${shortcut}`; - }; - - /** - * remove this window's observers when the window closes, since observers are global - */ - function uninit() { - prefSvc.removeObserver(autoHide, prefObserver); - prefSvc.removeObserver(toolboxBranch, prefObserver); - obSvc.removeObserver(toolboxObserver, "devtools:loader:destroy"); - obSvc.removeObserver(toolboxObserver, "devtools-thread-ready"); - window.removeEventListener("unload", uninit, false); - } - - function toolboxInit() { - prefObserver(prefSvc, null, autoHide); - prefObserver(prefSvc, null, autoTogglePopups); - prefObserver(prefSvc, null, mouseConfig); - toolboxObserver("initial-load"); - } - - if (!prefSvc.prefHasUserValue(autoTogglePopups)) - prefSvc.setBoolPref(autoTogglePopups, true); - if (!prefSvc.prefHasUserValue(mouseConfig)) - prefSvc.setStringPref( - mouseConfig, - `{"contentToolbox": 0, "browserToolbox": 2, "popupHide": 1}` - ); - window.addEventListener("unload", uninit, false); - prefSvc.addObserver(autoHide, prefObserver); - prefSvc.addObserver(toolboxBranch, prefObserver); - obSvc.addObserver(toolboxObserver, "devtools:loader:destroy"); - obSvc.addObserver(toolboxObserver, "devtools-thread-ready"); - if (gBrowserInit.delayedStartupFinished) { - toolboxInit(); - } else { - let delayedListener2 = (subject, topic) => { - if (topic == "browser-delayed-startup-finished" && subject == window) { - obSvc.removeObserver(delayedListener2, topic); - toolboxInit(); - } - }; - obSvc.addObserver(delayedListener2, "browser-delayed-startup-finished"); - } - return toolbarbutton; - }, - }); - - let styleSvc = Cc["@mozilla.org/content/style-sheet-service;1"].getService( - Ci.nsIStyleSheetService - ); - let toolboxCSS = `.toolbarbutton-1#toolbox-button{-moz-box-align:center;}.toolbarbutton-1#toolbox-button .toolbarbutton-badge-stack{-moz-box-pack:center;}.toolbarbutton-1#toolbox-button .toolbarbutton-icon{height:16px;width:16px;transition:fill 50ms ease-in-out 0s;}.toolbarbutton-1#toolbox-button{list-style-image:url('data:image/svg+xml;utf8,');}.toolbarbutton-1#toolbox-button[icon="autohide"]{list-style-image:url('data:image/svg+xml;utf8,');}@media (prefers-reduced-motion:no-preference){.toolbarbutton-1#toolbox-button[animate] .toolbarbutton-icon{animation-name:toolboxButtonPulse;animation-duration:200ms;animation-iteration-count:1;animation-timing-function:ease-in-out}}@keyframes toolboxButtonPulse{from{transform:scale(1)}40%{transform:scale(.7)}to{transform:scale(1)}}#confirmation-hint[data-message-id="hideCheckHint"] #confirmation-hint-message{margin-inline:0;}`; - let styleURI = makeURI("data:text/css;charset=UTF=8," + encodeURIComponent(toolboxCSS)); - if (!styleSvc.sheetRegistered(styleURI, styleSvc.AUTHOR_SHEET)) - styleSvc.loadAndRegisterSheet(styleURI, styleSvc.AUTHOR_SHEET); - - let observer = new MutationObserver(() => { - if (document.getElementById("key_toggleToolbox")) { - CustomizableUI.getWidget("toolbox-button").forWindow(window).node.setStrings(); - observer.disconnect(); - observer = null; - } - }); - observer.observe(document.body, { childList: true }); -})(); +// ==UserScript== +// @name Toolbox Button +// @version 1.2.6 +// @author aminomancer +// @homepage https://github.com/aminomancer/uc.css.js +// @description Adds a new toolbar button that 1) opens the content toolbox on left click; +// 2) opens the browser toolbox on right click; 3) toggles "Popup Auto-Hide" on middle click. +// Left click will open the toolbox for the active tab, or close it if it's already open. Right click +// will open the elevated browser toolbox if it's not already open. If it is already open, then +// instead of trying to start a new process and spawning an irritating dialog, it'll just show a +// brief notification saying the toolbox is already open. The button also shows a badge while a +// toolbox window is open. Middle click will toggle the preference for popup auto-hide: +// "ui.popup.disable_autohide". This does the same thing as the "Disable Popup Auto-Hide" option in +// the menu at the top right of the browser toolbox, prevents popups from closing so you can debug +// them. If you want to change which mouse buttons execute which functions, search for +// "userChrome.toolboxButton.mouseConfig" in about:config. Change the 0, 1, and 2 values. 0 = left +// click, 1 = middle, and 2 = right. By default, when you open a browser toolbox window, the script +// will disable popup auto-hide, and then re-enable it when you close the toolbox. I find that I +// usually want popup auto-hide disabled when I'm using the toolbox, and never want it disabled when +// I'm not using the toolbox, so I made it automatic, instead of having to right click and then +// immediately middle click every time. If you don't like this automatic feature, you can turn it +// off by setting "userChrome.toolboxButton.popupAutohide.toggle-on-toolbox-launch" to false in +// about:config. When you middle click, the button will show a notification telling you the current +// status of popup auto-hide, e.g. "Holding popups open." This is just so that people who use the +// feature a lot won't lose track of whether it's on or off, and won't need to open a popup and try +// to close it to test it. (The toolbar button also changes appearance while popup auto-hide is +// disabled. It becomes blue like the downloads button and the icon changes into a popup icon. This +// change is animated, as long as the user doesn't have reduced motion enabled) All of these +// notifications use the native confirmation hint custom element, since it looks nice. That's the +// one that appears when you save a bookmark, #confirmation-hint. So you can customize them with +// that selector. +// @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. +// ==/UserScript== + +// Modify these strings for easy localization. I tried to use built-in strings +// for this so it would automatically localize itself, but I found that every +// reference to the "Browser Toolbox" throughout the entire firefox UI is +// derived from a single message in a single localization file, which doesn't +// follow the standard format. It can only be parsed by the devtools' own +// special l10n module, which itself can only be imported by a CJS module. +// Requiring CJS just for a button seems ridiculous, plus there really aren't +// any localized strings that work for these confirmation messages anyway, or +// even the tooltip. So if your UI language isn't English you can modify all the +// strings created by this script in the following object: +const toolboxButtonL10n = { + // Confirmation hint. You receive this message when you right click the + // toolbox button, but a toolbox process for the window is already open. You + // can only have one toolbox open per-window. So if I have 3 windows open, and + // I right-click the toolbox button in window 1, then it'll launch a browser + // toolbox for window 1. If I then right-click the toolbox button in window 2, + // it'll launch a browser toolbox for window 2. But if I go back to window 1 + // and right-click the toolbox button a second time, it will do nothing except + // show a brief confirmation hint to explain the lack of action. + alreadyOpenMsg: "Browser Toolbox is already open.", + // Confirmation hint. This appears when you first middle-click the toolbox + // button. It signifies that popups are being kept open. That is, "popup + // auto-hide" has been temporarily disabled. + holdingOpenMsg: "Holding popups open.", + // Confirmation hint. This appears when you middle-click the toolbox button a + // second time, toggling "popup auto-hide" back on, thereby allowing popups to + // close on their own. + lettingCloseMsg: "Letting popups close.", + menuBundle: Services.strings.createBundle("chrome://devtools/locale/menus.properties"), + toolboxBundle: Services.strings.createBundle("chrome://devtools/locale/toolbox.properties"), + getString(name, where) { + return this[`${where}Bundle`].GetStringFromName(name); + }, + get contentLabel() { + return ( + this._contentLabel || + (this._contentLabel = this.getString("browserContentToolboxMenu.label", "menu")) + ); + }, + get browserLabel() { + return ( + this._browserLabel || + (this._browserLabel = this.getString("browserToolboxMenu.label", "menu")) + ); + }, + get autoHideLabel() { + return ( + this._autoHideLabel || + (this._autoHideLabel = this.getString("toolbox.meatballMenu.noautohide.label", "toolbox")) + ); + }, + get defaultLabel() { + return this.contentLabel; + }, + get defaultTooltip() { + return this.defaultLabel; + }, +}; + +(() => { + let toolboxLauncher = ChromeUtils.import( + "resource://devtools/client/framework/browser-toolbox/Launcher.jsm" + ).BrowserToolboxLauncher; + + if ( + /^chrome:\/\/browser\/content\/browser.(xul||xhtml)$/i.test(location) && + !CustomizableUI.getPlacementOfWidget("toolbox-button", true) + ) + CustomizableUI.createWidget({ + id: "toolbox-button", + type: "custom", + defaultArea: CustomizableUI.AREA_NAVBAR, + label: toolboxButtonL10n.defaultLabel, + removable: true, + overflows: true, + tooltiptext: toolboxButtonL10n.defaultTooltip, + onBuild: function (aDoc) { + let CustomHint = { + _timerID: null, + + /** + * Shows a transient, non-interactive confirmation hint anchored to an + * element, usually used in response to a user action to reaffirm that + * it was successful and potentially provide extra context. + * + * @param anchor (DOM node, required) + * The anchor for the panel. A value of null will anchor to the + * viewpoint (see options.x below) + * @param message (string, required) + * The message to be shown. + * @param options (object, optional) + * An object with any number of the following optional properties: + * - event (DOM event): The event that triggered the feedback. + * - hideArrow (boolean): Optionally hide the arrow. + * - hideCheck (boolean): Optionally hide the checkmark. + * - description (string): If provided, show a more detailed + * description/subtitle with the passed text. + * - duration (numeric): How long the hint should stick around, in + * milliseconds. Default is 1500 — 1.5 seconds. + * - position (string): One of a number of strings representing how the anchor point of the popup + * is aligned relative to the anchor point of the anchor node. + * Possible values for position are: + * before_start, before_end, after_start, after_end, + * start_before, start_after, end_before, end_after, + * overlap, after_pointer + * For example, after_start means the anchor node's bottom left corner will + * be aligned with the popup node's top left corner. overlap means their + * top left corners will be lined up exactly, so they will overlap. + * - x (number): Horizontal offset in pixels, relative to the anchor. + * If no anchor is provided, relative to the viewport. + * - y (number): Vertical offset in pixels, relative to the anchor. Negative + * values may also be used to move to the left and upwards respectively. + * Unanchored popups may be created by supplying null as the + * anchor node. An unanchored popup appears at the position + * specified by x and y, relative to the viewport of the document + * containing the popup node. (ignoring the anchor parameter) + * + */ + show(anchor, message, options = {}) { + this._reset(); + + this._message.textContent = message; + + if (options.description) { + this._description.textContent = options.description; + this._description.hidden = false; + this._panel.classList.add("with-description"); + } else { + this._description.hidden = true; + this._panel.classList.remove("with-description"); + } + + if (options.hideArrow) { + this._panel.setAttribute("hidearrow", "true"); + } + + if (options.hideCheck) { + this._animationBox.setAttribute("hidden", "true"); + this._panel.setAttribute("data-message-id", "hideCheckHint"); + } else this._panel.setAttribute("data-message-id", "checkmarkHint"); + + const DURATION = options.duration || 1500; + this._panel.addEventListener( + "popupshown", + () => { + this._animationBox.setAttribute("animate", "true"); + this._timerID = setTimeout(() => { + this._panel.hidePopup(true); + this._animationBox.removeAttribute("hidden"); + }, DURATION + 120); + }, + { once: true } + ); + + this._panel.addEventListener( + "popuphidden", + () => { + // reset the timerId in case our timeout wasn't the cause of the popup being hidden + this._reset(); + }, + { once: true } + ); + + let { position, x, y } = options; + this._panel.openPopup(null, { position, triggerEvent: options.event }); + this._panel.moveToAnchor(anchor, position, x, y); + }, + + _reset() { + if (this._timerID) { + clearTimeout(this._timerID); + this._timerID = null; + this._animationBox.removeAttribute("hidden"); + } + if (this.__panel) { + this._panel.removeAttribute("hidearrow"); + this._animationBox.removeAttribute("animate"); + this._panel.removeAttribute("data-message-id"); + this._panel.hidePopup(); + } + }, + + get _panel() { + this._ensurePanel(); + return this.__panel; + }, + + get _animationBox() { + this._ensurePanel(); + delete this._animationBox; + return (this._animationBox = aDoc.getElementById( + "confirmation-hint-checkmark-animation-container" + )); + }, + + get _message() { + this._ensurePanel(); + delete this._message; + return (this._message = aDoc.getElementById("confirmation-hint-message")); + }, + + get _description() { + this._ensurePanel(); + delete this._description; + return (this._description = aDoc.getElementById("confirmation-hint-description")); + }, + + _ensurePanel() { + if (!this.__panel) { + // hook into the built-in confirmation hint element + let wrapper = aDoc.getElementById("confirmation-hint-wrapper"); + wrapper?.replaceWith(wrapper.content); + this.__panel = aDoc.getElementById("confirmation-hint"); + ConfirmationHint.__panel = aDoc.getElementById("confirmation-hint"); + } + }, + }; + + let toolbarbutton = aDoc.createXULElement("toolbarbutton"); + let badgeStack = aDoc.createXULElement("stack"); + let icon = aDoc.createXULElement("image"); + let label = aDoc.createXULElement("label"); + let badgeLabel = aDoc.createElement("label"); + for (const [key, val] of Object.entries({ + class: "toolbarbutton-1 chromeclass-toolbar-additional", + badged: true, + label: toolboxButtonL10n.defaultLabel, + id: "toolbox-button", + role: "button", + icon: "toolbox", + removable: true, + overflows: true, + tooltiptext: toolboxButtonL10n.defaultTooltip, + })) + toolbarbutton.setAttribute(key, val); + + toolbarbutton.appendChild(badgeStack); + badgeStack.after(label); + badgeStack.appendChild(icon); + icon.after(badgeLabel); + badgeStack.setAttribute("class", "toolbarbutton-badge-stack"); + icon.setAttribute("class", "toolbarbutton-icon"); + badgeLabel.setAttribute("class", "toolbarbutton-badge"); + for (const [key, val] of Object.entries({ + class: "toolbarbutton-text", + crop: "right", + flex: "1", + value: toolboxButtonL10n.defaultLabel, + })) + label.setAttribute(key, val); + + let prefSvc = Services.prefs; + let obSvc = Services.obs; + let toolboxBranch = "userChrome.toolboxButton"; + let autoHide = "ui.popup.disable_autohide"; + let autoTogglePopups = "userChrome.toolboxButton.popupAutohide.toggle-on-toolbox-launch"; + let mouseConfig = "userChrome.toolboxButton.mouseConfig"; + + let onClick = function (e) { + let { button } = e; + if (e.getModifierState("Accel")) { + if (button == 2) return; + if (button == 0 && AppConstants.platform == "macosx") button = 2; + } + switch (button) { + case this.mouseConfig.contentToolbox: + // toggle the content toolbox + aDoc.defaultView.key_toggleToolbox.click(); + break; + case this.mouseConfig.browserToolbox: + toolboxLauncher.getBrowserToolboxSessionState() // check if a browser toolbox window is already open + ? CustomHint.show(toolbarbutton, toolboxButtonL10n.alreadyOpenMsg, { + event: e, + hideCheck: true, + }) // if so, just show a hint that it's already open + : aDoc.defaultView.key_browserToolbox.click(); // if not, launch a new one + break; + case this.mouseConfig.popupHide: + CustomHint.show( + toolbarbutton, + toolboxButtonL10n[this.popupAutoHide ? "lettingCloseMsg" : "holdingOpenMsg"], + { event: e, hideCheck: this.popupAutoHide } + ); + // toggle the pref + prefSvc.setBoolPref(autoHide, !this.popupAutoHide); + // animate the icon transformation + this.triggerAnimation(); + break; + default: + return; + } + e.preventDefault(); + }; + + if (AppConstants.platform === "macosx") { + toolbarbutton.onmousedown = onClick; + toolbarbutton.onclick = e => { + if (e.getModifierState("Accel")) return; + e.preventDefault(); + }; + } else toolbarbutton.onclick = onClick; + + toolbarbutton.triggerAnimation = function () { + this.addEventListener( + "animationend", + () => { + this.removeAttribute("animate"); + }, + { once: true } + ); + this.setAttribute("animate", "true"); + }; + + function getPref(root, pref) { + switch (root.getPrefType(pref)) { + case root.PREF_BOOL: + return root.getBoolPref(pref); + case root.PREF_INT: + return root.getIntPref(pref); + case root.PREF_STRING: + return root.getStringPref(pref); + default: + return null; + } + } + + function prefObserver(sub, _top, pref) { + let value = getPref(sub, pref); + switch (pref) { + case autoHide: + if (value === null) value = false; + toolbarbutton.popupAutoHide = value; + if (value) { + // change icon src to popup icon + toolbarbutton.setAttribute("icon", "autohide"); + // highlight color + icon.style.fill = "var(--toolbarbutton-icon-fill-attention)"; + } else { + // change icon src to toolbox icon + toolbarbutton.setAttribute("icon", "toolbox"); + // un-highlight color + icon.style.removeProperty("fill"); + } + break; + case autoTogglePopups: + if (value === null) value = true; + toolbarbutton.autoTogglePopups = value; + break; + case mouseConfig: + if (value === null) + value = { + "contentToolbox": 0, + "browserToolbox": 2, + "popupHide": 1, + }; + toolbarbutton.mouseConfig = JSON.parse(value); + toolbarbutton.setStrings(); + break; + } + } + + // listen for toolboxes opening and closing + function toolboxObserver(sub, _top, _data) { + // whether a toolbox is open + let state = toolboxLauncher.getBrowserToolboxSessionState(); + // set toolbar button's badge content + badgeLabel.textContent = state ? 1 : ""; + // if toolbox is open and autohide is not already enabled, enable it + if (sub === "initial-load" || !toolbarbutton.autoTogglePopups) return; + if (state && !toolbarbutton.popupAutoHide) prefSvc.setBoolPref(autoHide, true); + // if toolbox just closed and autohide is not already disabled, disable it + else if (!state && toolbarbutton.popupAutoHide) prefSvc.setBoolPref(autoHide, false); + } + + toolbarbutton.setStrings = function () { + let hotkey, labelString; + for (const [key, val] of Object.entries(toolbarbutton.mouseConfig)) + if (val === 0) + switch (key) { + case "contentToolbox": + labelString = toolboxButtonL10n.contentLabel; + hotkey = aDoc.defaultView.key_toggleToolbox; + break; + case "browserToolbox": + labelString = toolboxButtonL10n.browserLabel; + hotkey = aDoc.defaultView.key_browserToolbox; + break; + case "popupHide": + labelString = toolboxButtonL10n.autoHideLabel; + break; + } + let shortcut = hotkey ? ` (${ShortcutUtils.prettifyShortcut(hotkey)})` : ""; + toolbarbutton.label = labelString; + label.value = labelString; + toolbarbutton.tooltipText = `${labelString}${shortcut}`; + }; + + // remove this window's observers when the window closes, since observers are global + function uninit() { + prefSvc.removeObserver(autoHide, prefObserver); + prefSvc.removeObserver(toolboxBranch, prefObserver); + obSvc.removeObserver(toolboxObserver, "devtools:loader:destroy"); + obSvc.removeObserver(toolboxObserver, "devtools-thread-ready"); + window.removeEventListener("unload", uninit, false); + } + + function toolboxInit() { + prefObserver(prefSvc, null, autoHide); + prefObserver(prefSvc, null, autoTogglePopups); + prefObserver(prefSvc, null, mouseConfig); + toolboxObserver("initial-load"); + } + + if (!prefSvc.prefHasUserValue(autoTogglePopups)) + prefSvc.setBoolPref(autoTogglePopups, true); + if (!prefSvc.prefHasUserValue(mouseConfig)) + prefSvc.setStringPref( + mouseConfig, + `{"contentToolbox": 0, "browserToolbox": 2, "popupHide": 1}` + ); + window.addEventListener("unload", uninit, false); + prefSvc.addObserver(autoHide, prefObserver); + prefSvc.addObserver(toolboxBranch, prefObserver); + obSvc.addObserver(toolboxObserver, "devtools:loader:destroy"); + obSvc.addObserver(toolboxObserver, "devtools-thread-ready"); + if (gBrowserInit.delayedStartupFinished) { + toolboxInit(); + } else { + let delayedListener2 = (subject, topic) => { + if (topic == "browser-delayed-startup-finished" && subject == window) { + obSvc.removeObserver(delayedListener2, topic); + toolboxInit(); + } + }; + obSvc.addObserver(delayedListener2, "browser-delayed-startup-finished"); + } + return toolbarbutton; + }, + }); + + let styleSvc = Cc["@mozilla.org/content/style-sheet-service;1"].getService( + Ci.nsIStyleSheetService + ); + let toolboxCSS = `.toolbarbutton-1#toolbox-button{-moz-box-align:center;}.toolbarbutton-1#toolbox-button .toolbarbutton-badge-stack{-moz-box-pack:center;}.toolbarbutton-1#toolbox-button .toolbarbutton-icon{height:16px;width:16px;transition:fill 50ms ease-in-out 0s;}.toolbarbutton-1#toolbox-button{list-style-image:url('data:image/svg+xml;utf8,');}.toolbarbutton-1#toolbox-button[icon="autohide"]{list-style-image:url('data:image/svg+xml;utf8,');}@media (prefers-reduced-motion:no-preference){.toolbarbutton-1#toolbox-button[animate] .toolbarbutton-icon{animation-name:toolboxButtonPulse;animation-duration:200ms;animation-iteration-count:1;animation-timing-function:ease-in-out}}@keyframes toolboxButtonPulse{from{transform:scale(1)}40%{transform:scale(.7)}to{transform:scale(1)}}#confirmation-hint[data-message-id="hideCheckHint"] #confirmation-hint-message{margin-inline:0;}`; + let styleURI = makeURI("data:text/css;charset=UTF=8," + encodeURIComponent(toolboxCSS)); + if (!styleSvc.sheetRegistered(styleURI, styleSvc.AUTHOR_SHEET)) + styleSvc.loadAndRegisterSheet(styleURI, styleSvc.AUTHOR_SHEET); + + let observer = new MutationObserver(() => { + if (document.getElementById("key_toggleToolbox")) { + CustomizableUI.getWidget("toolbox-button").forWindow(window).node.setStrings(); + observer.disconnect(); + observer = null; + } + }); + observer.observe(document.body, { childList: true }); +})(); diff --git a/JS/autoHideNavbarSupport.uc.js b/JS/autoHideNavbarSupport.uc.js index 56a06427..5afade78 100644 --- a/JS/autoHideNavbarSupport.uc.js +++ b/JS/autoHideNavbarSupport.uc.js @@ -3,133 +3,144 @@ // @version 1.2.0 // @author aminomancer // @homepage https://github.com/aminomancer -// @description In fullscreen, the navbar hides automatically when you're not using it. But it doesn't have a very smooth animation, and there are certain situations where the navbar should be visible but isn't. This sets up its own logic to allow CSS transitions to cover the animation, and allows you to show the navbar only when hovering/focusing the navbar, or when a popup is opened that is anchored to something on the navbar, e.g. an extension popup. Also allows hiding the bookmarks toolbar under the same circumstances, fullscreen or not. You can use this for any toolbar, whether in fullscreen or not. duskFox just uses it for the bookmarks/personal toolbar, as well as for the navbar while in fullscreen, but your CSS can use it under any circumstances with popup-status="true". My preferred CSS transitions are in the stylesheets on my repo (see uc-fullscreen.css) but you can also do your own thing with selectors like box[popup-status="true"] > #navigator-toolbox > whatever +// @description In fullscreen, the navbar hides automatically when you're not +// using it. But it doesn't have a very smooth animation, and there are certain +// situations where the navbar should be visible but isn't. This sets up its own +// logic to allow CSS transitions to cover the animation, and allows you to show +// the navbar only when hovering/focusing the navbar, or when a popup is opened +// that is anchored to something on the navbar, e.g. an extension popup. Also +// allows hiding the bookmarks toolbar under the same circumstances, fullscreen +// or not. You can use this for any toolbar, whether in fullscreen or not. +// duskFox just uses it for the bookmarks/personal toolbar, as well as for the +// navbar while in fullscreen, but your CSS can use it under any circumstances +// with popup-status="true". My preferred CSS transitions are in the stylesheets +// on my repo (see uc-fullscreen.css) but you can also do your own thing with +// selectors like box[popup-status="true"] > #navigator-toolbox > whatever // @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. // ==/UserScript== class AutoHideHandler { - constructor() { - let autohidePref = "browser.fullscreen.autohide"; - this.observe(Services.prefs, "nsPref:read", autohidePref); - Services.prefs.addObserver(autohidePref, this); - for (let ev of ["popupshowing", "popuphiding"]) { - this.mainPopupSet.addEventListener(ev, this, true); - gNavToolbox.addEventListener(ev, this, true); - } - // onViewOpen and onViewClose - gURLBar.controller.addQueryListener(this); - for (let topic of ["urlbar-focus", "urlbar-blur"]) { - Services.obs.addObserver(this, topic); - } + constructor() { + let autohidePref = "browser.fullscreen.autohide"; + this.observe(Services.prefs, "nsPref:read", autohidePref); + Services.prefs.addObserver(autohidePref, this); + for (let ev of ["popupshowing", "popuphiding"]) { + this.mainPopupSet.addEventListener(ev, this, true); + gNavToolbox.addEventListener(ev, this, true); } - get navBlock() { - return this._navBlock || (this._navBlock = gNavToolbox.parentElement); + // onViewOpen and onViewClose + gURLBar.controller.addQueryListener(this); + for (let topic of ["urlbar-focus", "urlbar-blur"]) { + Services.obs.addObserver(this, topic); } - get mainPopupSet() { - return this._mainPopupSet || (this._mainPopupSet = document.getElementById("mainPopupSet")); + } + get navBlock() { + return this._navBlock || (this._navBlock = gNavToolbox.parentElement); + } + get mainPopupSet() { + return this._mainPopupSet || (this._mainPopupSet = document.getElementById("mainPopupSet")); + } + get backButton() { + return this._backButton || (this._backButton = document.getElementById("back-button")); + } + get fwdButton() { + return this._fwdButton || (this._fwdButton = document.getElementById("forward-button")); + } + getPref(root, pref, def) { + switch (root.getPrefType(pref)) { + case root.PREF_BOOL: + return root.getBoolPref(pref, def); + case root.PREF_INT: + return root.getIntPref(pref, def); + case root.PREF_STRING: + return root.getStringPref(pref, def); + default: + return null; } - get backButton() { - return this._backButton || (this._backButton = document.getElementById("back-button")); - } - get fwdButton() { - return this._fwdButton || (this._fwdButton = document.getElementById("forward-button")); - } - getPref(root, pref, def) { - switch (root.getPrefType(pref)) { - case root.PREF_BOOL: - return root.getBoolPref(pref, def); - case root.PREF_INT: - return root.getIntPref(pref, def); - case root.PREF_STRING: - return root.getStringPref(pref, def); - default: - return null; - } - } - observe(sub, topic, data) { - switch (topic) { - case "urlbar-focus": - case "urlbar-blur": - this._onUrlbarViewEvent(); - break; - case "nsPref:changed": - case "nsPref:read": - this._onPrefChanged(sub, data); - break; - default: - } - } - _onPrefChanged(sub, pref) { - switch (pref) { - case "browser.fullscreen.autohide": - let value = this.getPref(sub, pref, true); - if (value) document.documentElement.setAttribute("fullscreen-autohide", value); - else document.documentElement.removeAttribute("fullscreen-autohide"); - break; - default: - } - } - handleEvent(event) { - let targ = event.originalTarget; - if (targ.tagName === "tooltip") return; - switch (targ.id) { - case "contentAreaContextMenu": - case "sidebarMenu-popup": - case "ctrlTab-panel": - case "SyncedTabsSidebarContext": - case "SyncedTabsSidebarTabsFilterContext": - case "urlbar-scheme": - case "urlbar-input": - case "urlbar-label-box": - case "urlbar-search-mode-indicator": - case "pageActionContextMenu": - case "confirmation-hint": - return; - case "backForwardMenu": - if (this.backButton.disabled && this.fwdButton.disabled) return; - case "": - if (targ.hasAttribute("menu-api")) return; - } - if (targ.getAttribute("nopreventnavboxhide")) return; - let popNode = targ.triggerNode; - if ( - targ.className === "urlbarView" || - (event.target.parentElement.tagName === "menu" && !targ.closest("menubar")) - ) - return; - switch (event.type) { - case "popupshowing": - this.navBlock.setAttribute("popup-status", true); - break; - case "popuphiding": - if (popNode?.closest("panel") || popNode?.closest("menupopup")) return; - let panel = targ.closest("panel"); - if (targ !== panel && panel?.getAttribute("panelopen")) return; - this.navBlock.removeAttribute("popup-status"); - break; - } - } - onViewOpen() { + } + observe(sub, topic, data) { + switch (topic) { + case "urlbar-focus": + case "urlbar-blur": this._onUrlbarViewEvent(); + break; + case "nsPref:changed": + case "nsPref:read": + this._onPrefChanged(sub, data); + break; + default: } - onViewClose() { - this._onUrlbarViewEvent(); + } + _onPrefChanged(sub, pref) { + switch (pref) { + case "browser.fullscreen.autohide": + let value = this.getPref(sub, pref, true); + if (value) document.documentElement.setAttribute("fullscreen-autohide", value); + else document.documentElement.removeAttribute("fullscreen-autohide"); + break; + default: } - _onUrlbarViewEvent() { - if (gURLBar.view.isOpen || gURLBar.focused) - this.navBlock.setAttribute("urlbar-status", true); - else this.navBlock.removeAttribute("urlbar-status"); + } + handleEvent(event) { + let targ = event.originalTarget; + if (targ.tagName === "tooltip") return; + switch (targ.id) { + case "contentAreaContextMenu": + case "sidebarMenu-popup": + case "ctrlTab-panel": + case "SyncedTabsSidebarContext": + case "SyncedTabsSidebarTabsFilterContext": + case "urlbar-scheme": + case "urlbar-input": + case "urlbar-label-box": + case "urlbar-search-mode-indicator": + case "pageActionContextMenu": + case "confirmation-hint": + return; + case "backForwardMenu": + if (this.backButton.disabled && this.fwdButton.disabled) return; + case "": + if (targ.hasAttribute("menu-api")) return; } + if (targ.getAttribute("nopreventnavboxhide")) return; + let popNode = targ.triggerNode; + if ( + targ.className === "urlbarView" || + (event.target.parentElement.tagName === "menu" && !targ.closest("menubar")) + ) + return; + switch (event.type) { + case "popupshowing": + this.navBlock.setAttribute("popup-status", true); + break; + case "popuphiding": + if (popNode?.closest("panel") || popNode?.closest("menupopup")) return; + let panel = targ.closest("panel"); + if (targ !== panel && panel?.getAttribute("panelopen")) return; + this.navBlock.removeAttribute("popup-status"); + break; + } + } + onViewOpen() { + this._onUrlbarViewEvent(); + } + onViewClose() { + this._onUrlbarViewEvent(); + } + _onUrlbarViewEvent() { + if (gURLBar.view.isOpen || gURLBar.focused) this.navBlock.setAttribute("urlbar-status", true); + else this.navBlock.removeAttribute("urlbar-status"); + } } if (gBrowserInit.delayedStartupFinished) { - window.navbarAutoHide = new AutoHideHandler(); + window.navbarAutoHide = new AutoHideHandler(); } else { - let delayedListener = (subject, topic) => { - if (topic == "browser-delayed-startup-finished" && subject == window) { - Services.obs.removeObserver(delayedListener, topic); - window.navbarAutoHide = new AutoHideHandler(); - } - }; - Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); + let delayedListener = (subject, topic) => { + if (topic == "browser-delayed-startup-finished" && subject == window) { + Services.obs.removeObserver(delayedListener, topic); + window.navbarAutoHide = new AutoHideHandler(); + } + }; + Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); } diff --git a/JS/autocompletePopupStyler.uc.js b/JS/autocompletePopupStyler.uc.js index 687fa946..edecb5a5 100644 --- a/JS/autocompletePopupStyler.uc.js +++ b/JS/autocompletePopupStyler.uc.js @@ -3,46 +3,57 @@ // @version 1.0 // @author aminomancer // @homepage https://github.com/aminomancer/uc.css.js -// @description This mini-script adds an attribute to #PopupAutoComplete when it's opened on a panel in the chrome UI, rather than opened on an input field in the content area. The reason for this is that my style gives panels and menupopups the same background color. So without this, if the autocomplete popup opened on a panel (for example the password update notification popup) it would end up blending in with the panel which doesn't look great. When it opens inside the content area, we want it to keep its normal background color, var(--arrowpanel-background). But when it opens in a panel, we want to give it a brighter background color, var(--autocomplete-background). This is implemented in uc-popups.css by this rule: panel#PopupAutoComplete[type="autocomplete-richlistbox"][anchored-on-panel]{background-color: var(--autocomplete-background) !important;} +// @description This mini-script adds an attribute to #PopupAutoComplete when +// it's opened on a panel in the chrome UI, rather than opened on an input field +// in the content area. The reason for this is that my style gives panels and +// menupopups the same background color. So without this, if the autocomplete +// popup opened on a panel (for example the password update notification popup) +// it would end up blending in with the panel which doesn't look great. When it +// opens inside the content area, we want it to keep its normal background +// color, var(--arrowpanel-background). But when it opens in a panel, we want to +// give it a brighter background color, var(--autocomplete-background). This is +// implemented in uc-popups.css by this rule: +// panel#PopupAutoComplete[type="autocomplete-richlistbox"][anchored-on-panel] { +// background-color: var(--autocomplete-background) !important; +// } // @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. // ==/UserScript== (function () { - class AutocompletePopupStyler { - constructor() { - this.autocomplete.addEventListener("popupshowing", this); - } - handleEvent(_e) { - this.autocomplete.toggleAttribute("anchored-on-panel", this.sameBG); - } - get sameBG() { - if (!this.autocomplete.anchorNode) return false; - return ( - getComputedStyle(this.panelShadowContent).backgroundColor === - getComputedStyle(this.autocomplete).backgroundColor - ); - } - get autocomplete() { - return ( - this._autocomplete || - (this._autocomplete = document.getElementById("PopupAutoComplete")) - ); - } - get panelShadowContent() { - return this.autocomplete.anchorNode - ?.closest("panel") - .shadowRoot.querySelector(`[part="content"]`); - } + class AutocompletePopupStyler { + constructor() { + this.autocomplete.addEventListener("popupshowing", this); } - - if (gBrowserInit.delayedStartupFinished) new AutocompletePopupStyler(); - else { - let delayedListener = (subject, topic) => { - if (topic == "browser-delayed-startup-finished" && subject == window) { - Services.obs.removeObserver(delayedListener, topic); - new AutocompletePopupStyler(); - } - }; - Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); + handleEvent(_e) { + this.autocomplete.toggleAttribute("anchored-on-panel", this.sameBG); + } + get sameBG() { + if (!this.autocomplete.anchorNode) return false; + return ( + getComputedStyle(this.panelShadowContent).backgroundColor === + getComputedStyle(this.autocomplete).backgroundColor + ); + } + get autocomplete() { + return ( + this._autocomplete || (this._autocomplete = document.getElementById("PopupAutoComplete")) + ); } + get panelShadowContent() { + return this.autocomplete.anchorNode + ?.closest("panel") + .shadowRoot.querySelector(`[part="content"]`); + } + } + + if (gBrowserInit.delayedStartupFinished) new AutocompletePopupStyler(); + else { + let delayedListener = (subject, topic) => { + if (topic == "browser-delayed-startup-finished" && subject == window) { + Services.obs.removeObserver(delayedListener, topic); + new AutocompletePopupStyler(); + } + }; + Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); + } })(); diff --git a/JS/bookmarksMenuAndButtonShortcuts.uc.js b/JS/bookmarksMenuAndButtonShortcuts.uc.js index a6438b82..bacf3f34 100644 --- a/JS/bookmarksMenuAndButtonShortcuts.uc.js +++ b/JS/bookmarksMenuAndButtonShortcuts.uc.js @@ -3,158 +3,164 @@ // @version 1.3.1 // @author aminomancer // @homepage https://github.com/aminomancer/uc.css.js -// @description Adds some shortcuts for bookmarking pages. First, middle-clicking the bookmarks or library toolbar button will bookmark the current tab, or un-bookmark it if it's already bookmarked. Second, a menu item is added to the bookmarks toolbar button's popup, which bookmarks the current tab, or, if the page is already bookmarked, opens the bookmark editor popup. These are added primarily so that bookmarks can be added or removed with a single click, and can still be quickly added even if the bookmark page action is hidden for whatever reason. Third, another menu item is added to replicate the "Search bookmarks" button in the app menu's bookmarks panel. Clicking it will open the urlbar in bookmarks search mode. +// @description Adds some shortcuts for bookmarking pages. First, +// middle-clicking the bookmarks or library toolbar button will bookmark the +// current tab, or un-bookmark it if it's already bookmarked. Second, a menu +// item is added to the bookmarks toolbar button's popup, which bookmarks the +// current tab, or, if the page is already bookmarked, opens the bookmark editor +// popup. These are added primarily so that bookmarks can be added or removed +// with a single click, and can still be quickly added even if the bookmark page +// action is hidden for whatever reason. Third, another menu item is added to +// replicate the "Search bookmarks" button in the app menu's bookmarks panel. +// Clicking it will open the urlbar in bookmarks search mode. // @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. // ==/UserScript== const ucBookmarksShortcuts = { - create(aDoc, tag, props, isHTML = false) { - let el = isHTML ? aDoc.createElement(tag) : aDoc.createXULElement(tag); - for (let prop in props) el.setAttribute(prop, props[prop]); - return el; - }, - async bookmarkClick(e) { - if (e.button !== 1 || e.target.tagName !== "toolbarbutton") return; - let bm = await PlacesUtils.bookmarks.fetch({ url: new URL(BookmarkingUI._uri.spec) }); - bm ? PlacesTransactions.Remove(bm.guid).transact() : this.starCmd(); - e.preventDefault(); - e.stopPropagation(); - }, - async starCmd() { - if (!BookmarkingUI._pendingUpdate) { - if (!!BookmarkingUI.starAnimBox && !(BookmarkingUI._itemGuids.size > 0)) { - BrowserUIUtils.setToolbarButtonHeightProperty(BookmarkingUI.star); - document - .getElementById("star-button-animatable-box") - .addEventListener( - "animationend", - () => BookmarkingUI.star.removeAttribute("animate"), - { once: true } - ); - BookmarkingUI.star.setAttribute("animate", "true"); - } - let browser = gBrowser.selectedBrowser; - let url = new URL(browser.currentURI.spec); - let parentGuid = await PlacesUIUtils.defaultParentGuid; - let info = { url, parentGuid }; - let charset = null; - let isErrorPage = false; - if (browser.documentURI) - isErrorPage = /^about:(neterror|certerror|blocked)/.test(browser.documentURI.spec); - try { - if (isErrorPage) { - let entry = await PlacesUtils.history.fetch(browser.currentURI); - if (entry) info.title = entry.title; - } else info.title = browser.contentTitle; - info.title = info.title || url.href; - charset = browser.characterSet; - } catch (e) {} - info.guid = await PlacesTransactions.NewBookmark(info).transact(); - if (charset) PlacesUIUtils.setCharsetForPage(url, charset, window); - gURLBar.handleRevert(); - StarUI.showConfirmation(); - } - }, - addMenuitems(popup) { - let doc = popup.ownerDocument; - this.bookmarkTab = doc.createXULElement("menuitem"); - this.bookmarkTab = this.create(doc, "menuitem", { - id: "BMB_bookmarkThisPage", - label: "", - class: "menuitem-iconic subviewbutton", - onclick: "ucBookmarksShortcuts.updateMenuItem()", - oncommand: "BookmarkingUI.onStarCommand(event);", - key: "addBookmarkAsKb", - image: "chrome://browser/skin/bookmark-hollow.svg", - }); - popup.insertBefore(this.bookmarkTab, popup.firstElementChild); - popup.addEventListener("popupshowing", this.updateMenuItem, false); - this.bookmarkTab.setAttribute("data-l10n-id", "bookmarks-current-tab"); - this.searchBookmarks = popup.querySelector("#BMB_viewBookmarksSidebar").after( - this.create(doc, "menuitem", { - id: "BMB_searchBookmarks", - class: "menuitem-iconic subviewbutton", - "data-l10n-id": "bookmarks-search", - oncommand: "PlacesCommandHook.searchBookmarks();", - image: "chrome://global/skin/icons/search-glass.svg", - }) - ); - }, - onLocationChange(browser, _prog, _req, location, _flags) { - if (browser !== gBrowser.selectedBrowser) return; - this.updateMenuItem(null, location); - }, - handlePlacesEvents(events) { - for (let e of events) if (e.url && e.url == BookmarkingUI._uri?.spec) this.updateMenuItem(); - }, - async updateMenuItem(_e, location) { - let uri; - let menuitem = ucBookmarksShortcuts.bookmarkTab; - if (location) uri = new URL(location?.spec); - if (BookmarkingUI._uri) uri = new URL(BookmarkingUI._uri.spec); - if (!uri) return; - let isStarred = await PlacesUtils.bookmarks.fetch({ url: uri }); - if ("l10n" in menuitem.ownerDocument && menuitem.ownerDocument.l10n) - menuitem.ownerDocument.l10n.setAttributes( - menuitem, - isStarred ? "bookmarks-bookmark-edit-panel" : "bookmarks-current-tab" - ); - menuitem.setAttribute( - "image", - isStarred - ? "chrome://browser/skin/bookmark.svg" - : "chrome://browser/skin/bookmark-hollow.svg" - ); - }, - init() { - let { node } = CustomizableUI.getWidget("bookmarks-menu-button")?.forWindow(window); - // delete these two lines if you don't want the confirmation hint to show when you bookmark a page. - Services.prefs.setIntPref("browser.bookmarks.editDialog.confirmationHintShowCount", 0); - Services.prefs.lockPref("browser.bookmarks.editDialog.confirmationHintShowCount"); - BookmarkingUI.button.setAttribute("onclick", "ucBookmarksShortcuts.bookmarkClick(event)"); - CustomizableUI.getWidget("library-button") - .forWindow(window) - .node?.setAttribute("onclick", "ucBookmarksShortcuts.bookmarkClick(event)"); - this.addMenuitems(node.querySelector("#BMB_bookmarksPopup")); - gBrowser.addTabsProgressListener(this); - PlacesUtils.bookmarks.addObserver(this); - PlacesUtils.observers.addListener( - ["bookmark-added", "bookmark-removed"], - this.handlePlacesEvents.bind(this) - ); - // set the "positionend" attribute on the view bookmarks sidebar menuitem. - // this way we can swap between the left/right sidebar icons based on which side the sidebar is on, - // like the sidebar toolbar widget does. - let sidebarItem = node.querySelector("#BMB_viewBookmarksSidebar"); - if (sidebarItem) - sidebarItem.appendChild( - this.create(document, "observes", { - "element": "sidebar-box", - "attribute": "positionend", - }) - ); - // show the URL and keyword fields in the edit bookmark panel - eval( - `StarUI.showEditBookmarkPopup = async function ` + - StarUI.showEditBookmarkPopup - .toSource() - .replace(/^\(/, "") - .replace(/\)$/, "") - .replace(/async showEditBookmarkPopup/, "") - .replace(/async function\s*/, "") - .replace(/\[\"location\", \"keyword\"\]/, "[]") - ); - }, - QueryInterface: ChromeUtils.generateQI(["nsINavBookmarkObserver"]), + create(aDoc, tag, props, isHTML = false) { + let el = isHTML ? aDoc.createElement(tag) : aDoc.createXULElement(tag); + for (let prop in props) el.setAttribute(prop, props[prop]); + return el; + }, + async bookmarkClick(e) { + if (e.button !== 1 || e.target.tagName !== "toolbarbutton") return; + let bm = await PlacesUtils.bookmarks.fetch({ url: new URL(BookmarkingUI._uri.spec) }); + bm ? PlacesTransactions.Remove(bm.guid).transact() : this.starCmd(); + e.preventDefault(); + e.stopPropagation(); + }, + async starCmd() { + if (!BookmarkingUI._pendingUpdate) { + if (!!BookmarkingUI.starAnimBox && !(BookmarkingUI._itemGuids.size > 0)) { + BrowserUIUtils.setToolbarButtonHeightProperty(BookmarkingUI.star); + document + .getElementById("star-button-animatable-box") + .addEventListener("animationend", () => BookmarkingUI.star.removeAttribute("animate"), { + once: true, + }); + BookmarkingUI.star.setAttribute("animate", "true"); + } + let browser = gBrowser.selectedBrowser; + let url = new URL(browser.currentURI.spec); + let parentGuid = await PlacesUIUtils.defaultParentGuid; + let info = { url, parentGuid }; + let charset = null; + let isErrorPage = false; + if (browser.documentURI) + isErrorPage = /^about:(neterror|certerror|blocked)/.test(browser.documentURI.spec); + try { + if (isErrorPage) { + let entry = await PlacesUtils.history.fetch(browser.currentURI); + if (entry) info.title = entry.title; + } else info.title = browser.contentTitle; + info.title = info.title || url.href; + charset = browser.characterSet; + } catch (e) {} + info.guid = await PlacesTransactions.NewBookmark(info).transact(); + if (charset) PlacesUIUtils.setCharsetForPage(url, charset, window); + gURLBar.handleRevert(); + StarUI.showConfirmation(); + } + }, + addMenuitems(popup) { + let doc = popup.ownerDocument; + this.bookmarkTab = doc.createXULElement("menuitem"); + this.bookmarkTab = this.create(doc, "menuitem", { + id: "BMB_bookmarkThisPage", + label: "", + class: "menuitem-iconic subviewbutton", + onclick: "ucBookmarksShortcuts.updateMenuItem()", + oncommand: "BookmarkingUI.onStarCommand(event);", + key: "addBookmarkAsKb", + image: "chrome://browser/skin/bookmark-hollow.svg", + }); + popup.insertBefore(this.bookmarkTab, popup.firstElementChild); + popup.addEventListener("popupshowing", this.updateMenuItem, false); + this.bookmarkTab.setAttribute("data-l10n-id", "bookmarks-current-tab"); + this.searchBookmarks = popup.querySelector("#BMB_viewBookmarksSidebar").after( + this.create(doc, "menuitem", { + id: "BMB_searchBookmarks", + class: "menuitem-iconic subviewbutton", + "data-l10n-id": "bookmarks-search", + oncommand: "PlacesCommandHook.searchBookmarks();", + image: "chrome://global/skin/icons/search-glass.svg", + }) + ); + }, + onLocationChange(browser, _prog, _req, location, _flags) { + if (browser !== gBrowser.selectedBrowser) return; + this.updateMenuItem(null, location); + }, + handlePlacesEvents(events) { + for (let e of events) if (e.url && e.url == BookmarkingUI._uri?.spec) this.updateMenuItem(); + }, + async updateMenuItem(_e, location) { + let uri; + let menuitem = ucBookmarksShortcuts.bookmarkTab; + if (location) uri = new URL(location?.spec); + if (BookmarkingUI._uri) uri = new URL(BookmarkingUI._uri.spec); + if (!uri) return; + let isStarred = await PlacesUtils.bookmarks.fetch({ url: uri }); + if ("l10n" in menuitem.ownerDocument && menuitem.ownerDocument.l10n) + menuitem.ownerDocument.l10n.setAttributes( + menuitem, + isStarred ? "bookmarks-bookmark-edit-panel" : "bookmarks-current-tab" + ); + menuitem.setAttribute( + "image", + isStarred ? "chrome://browser/skin/bookmark.svg" : "chrome://browser/skin/bookmark-hollow.svg" + ); + }, + init() { + let { node } = CustomizableUI.getWidget("bookmarks-menu-button")?.forWindow(window); + // delete these two lines if you don't want the confirmation hint to show + // when you bookmark a page. + Services.prefs.setIntPref("browser.bookmarks.editDialog.confirmationHintShowCount", 0); + Services.prefs.lockPref("browser.bookmarks.editDialog.confirmationHintShowCount"); + BookmarkingUI.button.setAttribute("onclick", "ucBookmarksShortcuts.bookmarkClick(event)"); + CustomizableUI.getWidget("library-button") + .forWindow(window) + .node?.setAttribute("onclick", "ucBookmarksShortcuts.bookmarkClick(event)"); + this.addMenuitems(node.querySelector("#BMB_bookmarksPopup")); + gBrowser.addTabsProgressListener(this); + PlacesUtils.bookmarks.addObserver(this); + PlacesUtils.observers.addListener( + ["bookmark-added", "bookmark-removed"], + this.handlePlacesEvents.bind(this) + ); + // set the "positionend" attribute on the view bookmarks sidebar menuitem. + // this way we can swap between the left/right sidebar icons based on which + // side the sidebar is on, like the sidebar toolbar widget does. + let sidebarItem = node.querySelector("#BMB_viewBookmarksSidebar"); + if (sidebarItem) + sidebarItem.appendChild( + this.create(document, "observes", { + "element": "sidebar-box", + "attribute": "positionend", + }) + ); + // show the URL and keyword fields in the edit bookmark panel + eval( + `StarUI.showEditBookmarkPopup = async function ` + + StarUI.showEditBookmarkPopup + .toSource() + .replace(/^\(/, "") + .replace(/\)$/, "") + .replace(/async showEditBookmarkPopup/, "") + .replace(/async function\s*/, "") + .replace(/\[\"location\", \"keyword\"\]/, "[]") + ); + }, + QueryInterface: ChromeUtils.generateQI(["nsINavBookmarkObserver"]), }; if (gBrowserInit.delayedStartupFinished) ucBookmarksShortcuts.init(); else { - let delayedListener = (subject, topic) => { - if (topic == "browser-delayed-startup-finished" && subject == window) { - Services.obs.removeObserver(delayedListener, topic); - ucBookmarksShortcuts.init(); - } - }; - Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); + let delayedListener = (subject, topic) => { + if (topic == "browser-delayed-startup-finished" && subject == window) { + Services.obs.removeObserver(delayedListener, topic); + ucBookmarksShortcuts.init(); + } + }; + Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); } diff --git a/JS/bookmarksPopupShadowRoot.uc.js b/JS/bookmarksPopupShadowRoot.uc.js index 0aba26e2..6f75c001 100644 --- a/JS/bookmarksPopupShadowRoot.uc.js +++ b/JS/bookmarksPopupShadowRoot.uc.js @@ -1,107 +1,117 @@ -// ==UserScript== -// @name Bookmarks Popup Mods -// @version 1.2 -// @author aminomancer -// @homepage https://github.com/aminomancer/uc.css.js -// @description Implement smooth scrolling for all bookmarks popups that are tall enough to scroll. Add special click functions to their scroll buttons — hovering a scroll button will scroll at a constant rate, as normal. (though faster than vanilla) But clicking a scroll button will immediately jump to the top/bottom of the list. This no longer styles the scroll buttons, since I now style all arrowscrollbox scrollbuttons equally with resources/layout/arrowscrollbox.css. To do that requires chrome.manifest, line 6. This replaces the built-in arrowscrollbox.css with my version that makes the scrollbuttons look a lot prettier. If you want to customize them, just edit arrowscrollbox.css. This script still adds the custom classes though, in case you want to use them to style the elements in userChrome.css. -// @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. -// @include main -// ==/UserScript== - -const bookmarksPopupShadowRoot = { - handleEvent(e) { - if (!e.target.getAttribute("placespopup") || !e.target.scrollBox) return; - if (!e.target.getAttribute("uc-init")) - setTimeout(() => { - this.checkPopups(e.target); - }, 0); - let scrollbox = e.target.scrollBox.scrollbox; - let height = window.screen.availHeight; - let cls = e.target.scrollBox.parentElement?.classList; - if (scrollbox.scrollTopMax < height && scrollbox.clientHeight < height) - cls.add("BMBsmallContentBox"); - else cls.remove("BMBsmallContentBox"); - }, - - checkPopups(popup) { - popup.setAttribute("uc-init", true); - this.setUpScroll(popup); - }, - - setUpScroll(popup) { - popup.scrollBox.parentElement.classList.add("BMB-special-innerbox"); - popup.scrollBox.smoothScroll = true; - popup.scrollBox._scrollIncrement = 150; - popup.scrollBox._scrollButtonUp.classList.add("BMB-special-scrollbutton-up"); - popup.scrollBox._scrollButtonDown.classList.add("BMB-special-scrollbutton-down"); - popup.scrollBox._onButtonMouseOver = function _onButtonMouseOver(index) { - if (this._ensureElementIsVisibleAnimationFrame || this._arrowScrollAnim.requestHandle) - return; - if (this._clickToScroll) this._continueScroll(index); - else this._startScroll(index); - }; - popup.scrollBox._onButtonMouseOut = function _onButtonMouseOut() { - if (this._ensureElementIsVisibleAnimationFrame || this._arrowScrollAnim.requestHandle) - return; - if (this._clickToScroll) this._pauseScroll(); - else this._stopScroll(); - }; - popup.scrollBox._scrollButtonDown.onclick = function scrollToBottom() { - bookmarksPopupShadowRoot.scrollByIndex(popup.scrollBox, popup.children.length); - }; - popup.scrollBox._scrollButtonUp.onclick = function scrollToTop() { - bookmarksPopupShadowRoot.scrollByIndex(popup.scrollBox, -popup.children.length); - }; - }, - - scrollByIndex(box, index, aInstant) { - if (index == 0) return; - var rect = box.scrollClientRect; - var [start, end] = box.startEndProps; - var x = index > 0 ? rect[end] + 1 : rect[start] - 1; - var nextElement = box._elementFromPoint(x, index); - if (!nextElement) return; - var targetElement; - if (box.isRTLScrollbox) index *= -1; - while (index < 0 && nextElement) { - if (box._canScrollToElement(nextElement)) targetElement = nextElement; - nextElement = nextElement.previousElementSibling; - index++; - } - while (index > 0 && nextElement) { - if (box._canScrollToElement(nextElement)) targetElement = nextElement; - nextElement = nextElement.nextElementSibling; - index--; - } - if (!targetElement || !box._canScrollToElement(targetElement)) return; - box._stopScroll(); - let animFrame = window.requestAnimationFrame(() => { - targetElement.scrollIntoView({ - block: "nearest", - behavior: aInstant ? "instant" : "auto", - }); - box._ensureElementIsVisibleAnimationFrame = 0; - box._arrowScrollAnim.requestHandle = 0; - }); - box._ensureElementIsVisibleAnimationFrame = animFrame; - box._arrowScrollAnim.requestHandle = animFrame; - }, - init() { - addEventListener("popupshowing", this, true); - CustomizableUI.removeListener(this); - }, -}; - -(function () { - if (gBrowserInit.delayedStartupFinished) { - bookmarksPopupShadowRoot.init(); - } else { - let delayedListener = (subject, topic) => { - if (topic == "browser-delayed-startup-finished" && subject == window) { - Services.obs.removeObserver(delayedListener, topic); - bookmarksPopupShadowRoot.init(); - } - }; - Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); - } -})(); +// ==UserScript== +// @name Bookmarks Popup Mods +// @version 1.2 +// @author aminomancer +// @homepage https://github.com/aminomancer/uc.css.js +// @description Implement smooth scrolling for all bookmarks popups that are +// tall enough to scroll. Add special click functions to their scroll buttons — +// hovering a scroll button will scroll at a constant rate, as normal. (though +// faster than vanilla) But clicking a scroll button will immediately jump to +// the top/bottom of the list. This no longer styles the scroll buttons, since I +// now style all arrowscrollbox scrollbuttons equally with +// resources/layout/arrowscrollbox.css. To do that requires chrome.manifest, +// line 6. This replaces the built-in arrowscrollbox.css with my version that +// makes the scrollbuttons look a lot prettier. If you want to customize them, +// just edit arrowscrollbox.css. This script still adds the custom classes +// though, in case you want to use them to style the elements in userChrome.css. +// @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. +// @include main +// ==/UserScript== + +(function () { + const bookmarksPopupShadowRoot = { + handleEvent(e) { + if (!e.target.getAttribute("placespopup") || !e.target.scrollBox) return; + if (!e.target.getAttribute("uc-init")) + setTimeout(() => { + this.checkPopups(e.target); + }, 0); + let scrollbox = e.target.scrollBox.scrollbox; + let height = window.screen.availHeight; + let cls = e.target.scrollBox.parentElement?.classList; + if (scrollbox.scrollTopMax < height && scrollbox.clientHeight < height) + cls.add("BMBsmallContentBox"); + else cls.remove("BMBsmallContentBox"); + }, + + checkPopups(popup) { + popup.setAttribute("uc-init", true); + this.setUpScroll(popup); + }, + + setUpScroll(popup) { + popup.scrollBox.parentElement.classList.add("BMB-special-innerbox"); + popup.scrollBox.smoothScroll = true; + popup.scrollBox._scrollIncrement = 150; + popup.scrollBox._scrollButtonUp.classList.add("BMB-special-scrollbutton-up"); + popup.scrollBox._scrollButtonDown.classList.add("BMB-special-scrollbutton-down"); + popup.scrollBox._onButtonMouseOver = function _onButtonMouseOver(index) { + if (this._ensureElementIsVisibleAnimationFrame || this._arrowScrollAnim.requestHandle) + return; + if (this._clickToScroll) this._continueScroll(index); + else this._startScroll(index); + }; + popup.scrollBox._onButtonMouseOut = function _onButtonMouseOut() { + if (this._ensureElementIsVisibleAnimationFrame || this._arrowScrollAnim.requestHandle) + return; + if (this._clickToScroll) this._pauseScroll(); + else this._stopScroll(); + }; + popup.scrollBox._scrollButtonDown.onclick = function scrollToBottom() { + bookmarksPopupShadowRoot.scrollByIndex(popup.scrollBox, popup.children.length); + }; + popup.scrollBox._scrollButtonUp.onclick = function scrollToTop() { + bookmarksPopupShadowRoot.scrollByIndex(popup.scrollBox, -popup.children.length); + }; + }, + + scrollByIndex(box, index, aInstant) { + if (index == 0) return; + var rect = box.scrollClientRect; + var [start, end] = box.startEndProps; + var x = index > 0 ? rect[end] + 1 : rect[start] - 1; + var nextElement = box._elementFromPoint(x, index); + if (!nextElement) return; + var targetElement; + if (box.isRTLScrollbox) index *= -1; + while (index < 0 && nextElement) { + if (box._canScrollToElement(nextElement)) targetElement = nextElement; + nextElement = nextElement.previousElementSibling; + index++; + } + while (index > 0 && nextElement) { + if (box._canScrollToElement(nextElement)) targetElement = nextElement; + nextElement = nextElement.nextElementSibling; + index--; + } + if (!targetElement || !box._canScrollToElement(targetElement)) return; + box._stopScroll(); + let animFrame = window.requestAnimationFrame(() => { + targetElement.scrollIntoView({ + block: "nearest", + behavior: aInstant ? "instant" : "auto", + }); + box._ensureElementIsVisibleAnimationFrame = 0; + box._arrowScrollAnim.requestHandle = 0; + }); + box._ensureElementIsVisibleAnimationFrame = animFrame; + box._arrowScrollAnim.requestHandle = animFrame; + }, + init() { + addEventListener("popupshowing", this, true); + CustomizableUI.removeListener(this); + }, + }; + + if (gBrowserInit.delayedStartupFinished) { + bookmarksPopupShadowRoot.init(); + } else { + let delayedListener = (subject, topic) => { + if (topic == "browser-delayed-startup-finished" && subject == window) { + Services.obs.removeObserver(delayedListener, topic); + bookmarksPopupShadowRoot.init(); + } + }; + Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); + } +})(); diff --git a/JS/clearDownloadsButton.uc.js b/JS/clearDownloadsButton.uc.js index f8630f16..e9f59475 100644 --- a/JS/clearDownloadsButton.uc.js +++ b/JS/clearDownloadsButton.uc.js @@ -3,83 +3,89 @@ // @version 1.4.0 // @author aminomancer // @homepage https://github.com/aminomancer/uc.css.js -// @description Place a "Clear Downloads" button in the downloads panel, right next to the "Show all downloads" button. +// @description Place a "Clear Downloads" button in the downloads panel, +// right next to the "Show all downloads" button. // @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. // ==/UserScript== (function () { - const config = { - "Label in sentence case": true, // the "Show all downloads" button is in sentence case, but the localized "Clear Downloads" button string is from a context menu, so it's in "Title Case". if you want both in sentence case use this. I've tried to account for other alphabets or RTL languages but user will be the judge - }; - class ClearDLPanel { - constructor() { - this.makeButton(); - this.setCountHandler(); - Services.obs.addObserver(this, "downloads-panel-count-changed"); - } - async genStrings() { - this.strings = await new Localization(["browser/downloads.ftl"], true); - const messages = await this.strings.formatMessages(["downloads-cmd-clear-downloads"]); - this.label = messages[0].attributes[0].value; - this.accessKey = messages[0].attributes[1].value; - return [this.label, this.accessKey]; - } - async makeButton() { - this.clearPanelButton = document.createXULElement("button"); - let strings = await this.genStrings(); - let labelString = config["Label in sentence case"] - ? this.sentenceCase(strings[0]) - : strings[0]; - for (const [key, val] of Object.entries({ - id: "clearDownloadsPanel", - class: - DownloadsView.downloadsHistory.className || - "downloadsPanelFooterButton subviewbutton panel-subview-footer-button toolbarbutton-1", - oncommand: `goDoCommand('downloadsCmd_clearList'); DownloadsPanel.hidePanel();`, - label: labelString, - accesskey: strings[1], - flex: "1", - pack: "start", - })) - this.clearPanelButton.setAttribute(key, val); - DownloadsView.downloadsHistory.after(this.clearPanelButton); - this.clearPanelButton.hidden = !DownloadsView._visibleViewItems?.size > 0; - this.clearPanelButton - ?.closest("#downloadsFooter") - .prepend(document.createXULElement("toolbarseparator")); - this.clearPanelButton?.parentElement.setAttribute("uc-hbox", "true"); - } - sentenceCase(str) { - return str - .toLocaleLowerCase() - .replace(RTL_UI ? /.$/i : /^./i, function (letter) { - return letter.toLocaleUpperCase(); - }) - .trim(); - } - setCountHandler() { - eval( - `DownloadsView._itemCountChanged = function ${DownloadsView._itemCountChanged - .toSource() - .replace( - /hiddenCount \> 0\;\n/, - `hiddenCount > 0;\n Services.obs.notifyObservers(null, "downloads-panel-count-changed", String(count));\n` - )}` - ); - } - observe(_sub, _top, data) { - this.clearPanelButton.hidden = parseInt(data) < 1; - } + const config = { + // the "Show all downloads" button is in sentence case, but the + // localized "Clear Downloads" button string is from a context menu, so + // it's in "Title Case". if you want both in sentence case use this. + // I've tried to account for other alphabets or RTL languages but user + // will be the judge + "Label in sentence case": true, + }; + class ClearDLPanel { + constructor() { + this.makeButton(); + this.setCountHandler(); + Services.obs.addObserver(this, "downloads-panel-count-changed"); } - - if (gBrowserInit.delayedStartupFinished) new ClearDLPanel(); - else { - let delayedListener = (subject, topic) => { - if (topic == "browser-delayed-startup-finished" && subject == window) { - Services.obs.removeObserver(delayedListener, topic); - new ClearDLPanel(); - } - }; - Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); + async genStrings() { + this.strings = await new Localization(["browser/downloads.ftl"], true); + const messages = await this.strings.formatMessages(["downloads-cmd-clear-downloads"]); + this.label = messages[0].attributes[0].value; + this.accessKey = messages[0].attributes[1].value; + return [this.label, this.accessKey]; + } + async makeButton() { + this.clearPanelButton = document.createXULElement("button"); + let strings = await this.genStrings(); + let labelString = config["Label in sentence case"] + ? this.sentenceCase(strings[0]) + : strings[0]; + for (const [key, val] of Object.entries({ + id: "clearDownloadsPanel", + class: + DownloadsView.downloadsHistory.className || + "downloadsPanelFooterButton subviewbutton panel-subview-footer-button toolbarbutton-1", + oncommand: `goDoCommand('downloadsCmd_clearList'); DownloadsPanel.hidePanel();`, + label: labelString, + accesskey: strings[1], + flex: "1", + pack: "start", + })) + this.clearPanelButton.setAttribute(key, val); + DownloadsView.downloadsHistory.after(this.clearPanelButton); + this.clearPanelButton.hidden = !DownloadsView._visibleViewItems?.size > 0; + this.clearPanelButton + ?.closest("#downloadsFooter") + .prepend(document.createXULElement("toolbarseparator")); + this.clearPanelButton?.parentElement.setAttribute("uc-hbox", "true"); + } + sentenceCase(str) { + return str + .toLocaleLowerCase() + .replace(RTL_UI ? /.$/i : /^./i, function (letter) { + return letter.toLocaleUpperCase(); + }) + .trim(); } + setCountHandler() { + eval( + `DownloadsView._itemCountChanged = function ${DownloadsView._itemCountChanged + .toSource() + .replace( + /hiddenCount \> 0\;\n/, + `hiddenCount > 0;\n Services.obs.notifyObservers(null, "downloads-panel-count-changed", String(count));\n` + )}` + ); + } + observe(_sub, _top, data) { + this.clearPanelButton.hidden = parseInt(data) < 1; + } + } + + if (gBrowserInit.delayedStartupFinished) new ClearDLPanel(); + else { + let delayedListener = (subject, topic) => { + if (topic == "browser-delayed-startup-finished" && subject == window) { + Services.obs.removeObserver(delayedListener, topic); + new ClearDLPanel(); + } + }; + Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); + } })(); diff --git a/JS/copyCurrentUrlHotkey.uc.js b/JS/copyCurrentUrlHotkey.uc.js index 24233570..1eba5bd3 100644 --- a/JS/copyCurrentUrlHotkey.uc.js +++ b/JS/copyCurrentUrlHotkey.uc.js @@ -4,94 +4,91 @@ // @author aminomancer // @homepage https://github.com/aminomancer // @description Adds a new hotkey (Ctrl+Alt+C by default) that copies -// whatever is in the urlbar, even when it's not in focus. +// whatever is in the urlbar, even when it's not in focus. // @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. // ==/UserScript== class CopyCurrentURL { - static config = { - // if you have customHintProvider.uc.js, copying will open a - // confirmation hint anchored to the urlbar. - "copy confirmation hint": true, + static config = { + // if you have customHintProvider.uc.js, copying will open a confirmation + // hint anchored to the urlbar. + "copy confirmation hint": true, - // when you right-click the urlbar, the context menu has a "copy" - // command. set this to "true" to show a "Ctrl+Alt+C" hint next to - // this command, like firefox does with many other commands. the - // hint text will reflect the actual hotkey. so on macOS it will - // show "Cmd+Alt+C" and if you modify the modifiers below, it will - // show your modifiers instead. this setting isn't enabled by - // default because 1) unlike our custom hotkey, this command - // actually only copies the selection, not the full input content. - // so it's disabled if nothing is highlighted. and 2) the context - // menu is very thin due to the short names of the commands. adding - // "Ctrl+Alt+C" makes it kind of cramped. but it's easy to forget - // that hotkeys exist if they're not visually displayed anywhere, so - // you may want to enable this feature. - "context menu shortcut hint": true, + // when you right-click the urlbar, the context menu has a "copy" command. + // set this to "true" to show a "Ctrl+Alt+C" hint next to this command, like + // firefox does with many other commands. the hint text will reflect the + // actual hotkey. so on macOS it will show "Cmd+Alt+C" and if you modify the + // modifiers below, it will show your modifiers instead. this setting isn't + // enabled by default because 1) unlike our custom hotkey, this command + // actually only copies the selection, not the full input content. so it's + // disabled if nothing is highlighted. and 2) the context menu is very thin + // due to the short names of the commands. adding "Ctrl+Alt+C" makes it kind + // of cramped. but it's easy to forget that hotkeys exist if they're not + // visually displayed anywhere, so you may want to enable this feature. + "context menu shortcut hint": true, - shortcut: { - // shortcut key, combined with modifiers. - key: "C", + shortcut: { + // shortcut key, combined with modifiers. + key: "C", - // ctrl + alt or cmd + alt (use accel, it's cross-platform. it - // can be changed in about:config with ui.key.accelKey. if you - // leave the "" quotes empty, no modifier will be used. that - // means the hotkey will just be "C" which is a bad idea — only - // do that if your "key" value is something obscure like a - // function key, since this key will be active at all times and - // in almost all contexts. - modifiers: "accel alt", + // ctrl + alt or cmd + alt (use accel, it's cross-platform. it can be + // changed in about:config with ui.key.accelKey. if you leave the "" quotes + // empty, no modifier will be used. that means the hotkey will just be "C" + // which is a bad idea — only do that if your "key" value is something + // obscure like a function key, since this key will be active at all times + // and in almost all contexts. + modifiers: "accel alt", - // no need to change this. - id: "key_copyCurrentUrl", - }, - }; - constructor() { - this.showHint = !!CopyCurrentURL.config["copy confirmation hint"]; - this.hotkey = _ucUtils.registerHotkey(CopyCurrentURL.config.shortcut, (win, key) => { - if (win === window && gURLBar.value) { - this.clipboardHelper.copyString(gURLBar.value); - this.showHint && - win.CustomHint?.show(gURLBar.inputField, "Copied", { - position: "after_start", - x: 16, - }); - } - }); - if (CopyCurrentURL.config["context menu shortcut hint"]) this.shortcutHint(); + // no need to change this. + id: "key_copyCurrentUrl", + }, + }; + constructor() { + this.showHint = !!CopyCurrentURL.config["copy confirmation hint"]; + this.hotkey = _ucUtils.registerHotkey(CopyCurrentURL.config.shortcut, (win, key) => { + if (win === window && gURLBar.value) { + this.clipboardHelper.copyString(gURLBar.value); + this.showHint && + win.CustomHint?.show(gURLBar.inputField, "Copied", { + position: "after_start", + x: 16, + }); + } + }); + if (CopyCurrentURL.config["context menu shortcut hint"]) this.shortcutHint(); + } + get clipboardHelper() { + return ( + this._clipboardHelper || + (this._clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( + Ci.nsIClipboardHelper + )) + ); + } + handleEvent(_e) { + const menuitem = gURLBar + .querySelector("moz-input-box") + ?.menupopup?.querySelector(`[cmd="cmd_copy"]`); + if (menuitem) { + menuitem.setAttribute("key", CopyCurrentURL.config.shortcut.id); + gURLBar.removeEventListener("contextmenu", this); } - get clipboardHelper() { - return ( - this._clipboardHelper || - (this._clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( - Ci.nsIClipboardHelper - )) - ); - } - handleEvent(_e) { - const menuitem = gURLBar - .querySelector("moz-input-box") - ?.menupopup?.querySelector(`[cmd="cmd_copy"]`); - if (menuitem) { - menuitem.setAttribute("key", CopyCurrentURL.config.shortcut.id); - gURLBar.removeEventListener("contextmenu", this); - } - } - setupHint() { - gURLBar.addEventListener("contextmenu", this); - } - shortcutHint() { - if (gBrowserInit.delayedStartupFinished) this.setupHint(); - else { - let delayedListener = (subject, topic) => { - if (topic == "browser-delayed-startup-finished" && subject == window) { - Services.obs.removeObserver(delayedListener, topic); - this.setupHint(); - } - }; - Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); + } + setupHint() { + gURLBar.addEventListener("contextmenu", this); + } + shortcutHint() { + if (gBrowserInit.delayedStartupFinished) this.setupHint(); + else { + let delayedListener = (subject, topic) => { + if (topic == "browser-delayed-startup-finished" && subject == window) { + Services.obs.removeObserver(delayedListener, topic); + this.setupHint(); } + }; + Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); } + } } window.copyCurrentUrl = new CopyCurrentURL(); diff --git a/JS/customHintProvider.uc.js b/JS/customHintProvider.uc.js index b0ca59e4..6596a024 100644 --- a/JS/customHintProvider.uc.js +++ b/JS/customHintProvider.uc.js @@ -3,206 +3,215 @@ // @version 1.1.2 // @author aminomancer // @homepage https://github.com/aminomancer/uc.css.js -// @description A utility script for other scripts to take advantage of. Sets up a global object (on the chrome window) for showing confirmation hints with custom messages. The built-in confirmation hint component can only show a few messages built into the browser's localization system. It only accepts l10n IDs, so if your script wants to show a custom message with some specific string, it won't work. This works just like the built-in confirmation hint, and uses the built-in confirmation hint element, but it accepts an arbitrary string as a parameter. So you can open a confirmation hint with *any* message, e.g. CustomHint.show(anchorNode, "This is my custom message", {hideArrow: true, hideCheck: true, description: "Awesome.", duration: 3000}) +// @description A utility script for other scripts to take advantage of. Sets +// up a global object (on the chrome window) for showing confirmation hints with +// custom messages. The built-in confirmation hint component can only show a few +// messages built into the browser's localization system. It only accepts l10n +// IDs, so if your script wants to show a custom message with some specific +// string, it won't work. This works just like the built-in confirmation hint, +// and uses the built-in confirmation hint element, but it accepts an arbitrary +// string as a parameter. So you can open a confirmation hint with *any* +// message, e.g. CustomHint.show(anchorNode, "This is my custom message", {hideArrow: true, hideCheck: true, description: "Awesome.", duration: 3000}) // @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. // ==/UserScript== window.CustomHint = { - _timerID: null, - - /** - * Shows a transient, non-interactive confirmation hint anchored to an - * element, usually used in response to a user action to reaffirm that it was - * successful and potentially provide extra context. - * - * @param anchor (DOM node, required) - * The anchor for the panel. A value of null will anchor to the viewpoint (see options.x below) - * @param message (string, required) - * The message to be shown. - * @param options (object, optional) - * An object with any number of the following optional properties: - * - event (DOM event): The event that triggered the feedback. - * - hideArrow (boolean): Optionally hide the arrow. - * - hideCheck (boolean): Optionally hide the checkmark. - * - description (string): If provided, show a more detailed description/subtitle with the passed text. - * - duration (numeric): How long the hint should stick around, in milliseconds. - * Default is 1500 — 1.5 seconds. Pass -1 to make the hint stay open - * indefinitely, until the user clicks out of it, presses Escape, etc. - * - position (string): One of a number of strings representing how the anchor point of the popup - * is aligned relative to the anchor point of the anchor node. - * Possible values for position are: - * before_start, before_end, after_start, after_end, - * start_before, start_after, end_before, end_after, - * overlap, after_pointer - * For example, after_start means the anchor node's bottom left corner will - * be aligned with the popup node's top left corner. overlap means their - * top left corners will be lined up exactly, so they will overlap. - * - x (number): Horizontal offset in pixels, relative to the anchor. - * If no anchor is provided, relative to the viewport. - * - y (number): Vertical offset in pixels, relative to the anchor. - * Negative values may also be used to move to the left and upwards respectively. - * Unanchored popups may be created by supplying null as the anchor node. - * An unanchored popup appears at the position specified by x and y, relative to the - * viewport of the document containing the popup node. (ignoring the anchor parameter) - * - */ - show(anchor, message, options = {}) { - this._reset(); - - this._message.textContent = message; - - if (options.description) { - this._description.textContent = options.description; - this._description.hidden = false; - this._panel.classList.add("with-description"); - } else { - this._description.hidden = true; - this._panel.classList.remove("with-description"); - } - - if (options.hideArrow) { - this._panel.setAttribute("hidearrow", "true"); - } - - if (options.hideCheck) { - this._animationBox.setAttribute("hidden", "true"); - this._panel.setAttribute("data-message-id", "hideCheckHint"); - } else this._panel.setAttribute("data-message-id", "checkmarkHint"); - - const DURATION = options.duration || 1500; - this._panel.addEventListener( - "popupshown", - () => { - this._animationBox.setAttribute("animate", "true"); - this._timerID = - DURATION > 0 - ? setTimeout(() => { - this._panel.hidePopup(true); - this._animationBox.removeAttribute("hidden"); - }, DURATION + 120) - : 1; - }, - { once: true } - ); + _timerID: null, + + /** + * Shows a transient, non-interactive confirmation hint anchored to an + * element, usually used in response to a user action to reaffirm that it was + * successful and potentially provide extra context. + * + * @param anchor (DOM node, required) + * The anchor for the panel. A value of null will anchor to the viewpoint (see options.x below) + * @param message (string, required) + * The message to be shown. + * @param options (object, optional) + * An object with any number of the following optional properties: + * - event (DOM event): The event that triggered the feedback. + * - hideArrow (boolean): Optionally hide the arrow. + * - hideCheck (boolean): Optionally hide the checkmark. + * - description (string): If provided, show a more detailed description/subtitle with the passed text. + * - duration (numeric): How long the hint should stick around, in milliseconds. + * Default is 1500 — 1.5 seconds. Pass -1 to make the hint stay open + * indefinitely, until the user clicks out of it, presses Escape, etc. + * - position (string): One of a number of strings representing how the anchor point of the popup + * is aligned relative to the anchor point of the anchor node. + * Possible values for position are: + * before_start, before_end, after_start, after_end, + * start_before, start_after, end_before, end_after, + * overlap, after_pointer + * For example, after_start means the anchor node's bottom left corner will + * be aligned with the popup node's top left corner. overlap means their + * top left corners will be lined up exactly, so they will overlap. + * - x (number): Horizontal offset in pixels, relative to the anchor. + * If no anchor is provided, relative to the viewport. + * - y (number): Vertical offset in pixels, relative to the anchor. + * Negative values may also be used to move to the left and upwards respectively. + * Unanchored popups may be created by supplying null as the anchor node. + * An unanchored popup appears at the position specified by x and y, relative to the + * viewport of the document containing the popup node. (ignoring the anchor parameter) + * + */ + show(anchor, message, options = {}) { + this._reset(); + + this._message.textContent = message; + + if (options.description) { + this._description.textContent = options.description; + this._description.hidden = false; + this._panel.classList.add("with-description"); + } else { + this._description.hidden = true; + this._panel.classList.remove("with-description"); + } - this._panel.addEventListener( - "popuphidden", - () => { - // reset the timerId in case our timeout wasn't the cause of the popup being hidden - this._reset(); - }, - { once: true } - ); + if (options.hideArrow) { + this._panel.setAttribute("hidearrow", "true"); + } - let { position, x, y } = options; - this._panel.openPopup(null, { position, triggerEvent: options.event }); - this._panel.moveToAnchor(anchor, position, x, y); - }, - - _reset() { - if (this._timerID) { - clearTimeout(this._timerID); - this._timerID = null; - this._animationBox.removeAttribute("hidden"); - } - if (this.__panel) { - this._panel.removeAttribute("hidearrow"); - this._animationBox.removeAttribute("animate"); - this._panel.removeAttribute("data-message-id"); - this._panel.hidePopup(); - } - }, - - get _panel() { - this._ensurePanel(); - return this.__panel; - }, - - get _animationBox() { - this._ensurePanel(); - delete this._animationBox; - return (this._animationBox = document.getElementById( - "confirmation-hint-checkmark-animation-container" - )); - }, - - get _message() { - this._ensurePanel(); - delete this._message; - return (this._message = document.getElementById("confirmation-hint-message")); - }, - - get _description() { - this._ensurePanel(); - delete this._description; - return (this._description = document.getElementById("confirmation-hint-description")); - }, - - _ensurePanel() { - if (!this.__panel) { - // hook into the built-in confirmation hint element - let wrapper = document.getElementById("confirmation-hint-wrapper"); - wrapper?.replaceWith(wrapper.content); - this.__panel = document.getElementById("confirmation-hint"); - ConfirmationHint.__panel = document.getElementById("confirmation-hint"); - } - }, + if (options.hideCheck) { + this._animationBox.setAttribute("hidden", "true"); + this._panel.setAttribute("data-message-id", "hideCheckHint"); + } else this._panel.setAttribute("data-message-id", "checkmarkHint"); + + const DURATION = options.duration || 1500; + this._panel.addEventListener( + "popupshown", + () => { + this._animationBox.setAttribute("animate", "true"); + this._timerID = + DURATION > 0 + ? setTimeout(() => { + this._panel.hidePopup(true); + this._animationBox.removeAttribute("hidden"); + }, DURATION + 120) + : 1; + }, + { once: true } + ); + + this._panel.addEventListener( + "popuphidden", + () => { + // reset the timerId in case our timeout wasn't the cause of the popup being hidden + this._reset(); + }, + { once: true } + ); + + let { position, x, y } = options; + this._panel.openPopup(null, { position, triggerEvent: options.event }); + this._panel.moveToAnchor(anchor, position, x, y); + }, + + _reset() { + if (this._timerID) { + clearTimeout(this._timerID); + this._timerID = null; + this._animationBox.removeAttribute("hidden"); + } + if (this.__panel) { + this._panel.removeAttribute("hidearrow"); + this._animationBox.removeAttribute("animate"); + this._panel.removeAttribute("data-message-id"); + this._panel.hidePopup(); + } + }, + + get _panel() { + this._ensurePanel(); + return this.__panel; + }, + + get _animationBox() { + this._ensurePanel(); + delete this._animationBox; + return (this._animationBox = document.getElementById( + "confirmation-hint-checkmark-animation-container" + )); + }, + + get _message() { + this._ensurePanel(); + delete this._message; + return (this._message = document.getElementById("confirmation-hint-message")); + }, + + get _description() { + this._ensurePanel(); + delete this._description; + return (this._description = document.getElementById("confirmation-hint-description")); + }, + + _ensurePanel() { + if (!this.__panel) { + // hook into the built-in confirmation hint element + let wrapper = document.getElementById("confirmation-hint-wrapper"); + wrapper?.replaceWith(wrapper.content); + this.__panel = document.getElementById("confirmation-hint"); + ConfirmationHint.__panel = document.getElementById("confirmation-hint"); + } + }, }; (function () { - function init() { - // when the confirmation hint was created, openPopup worked differently. - // it didn't set the "open" attribute on the anchor directly. - // that was up to the function calling openPopup to handle. - // which worked well for the confirmation hint, since it shouldn't set the "open" attribute at all. - // the confirmation hint isn't a popup or a panel in terms of function, it's more like a tooltip. - // so it shouldn't show the [open] style on buttons it's anchored to, - // nor should it stifle scrolling while it's open. - // since setting the attribute is now built into the binary code, - // we can only stop it by using openPopupAtScreen instead of openPopup. - // this is why I tried to get mozilla to revert this change to openPopup but nobody has ever responded. - // so for now I'm just gonna fix it with a script. it will get the coordinates via javascript instead of C++ - ConfirmationHint.show = function show(anchor, messageId, options = {}) { - this._reset(); - this._message.textContent = gBrowserBundle.GetStringFromName( - `confirmationHint.${messageId}.label` - ); - if (options.showDescription) { - this._description.textContent = gBrowserBundle.GetStringFromName( - `confirmationHint.${messageId}.description` - ); - this._description.hidden = false; - this._panel.classList.add("with-description"); - } else { - this._description.hidden = true; - this._panel.classList.remove("with-description"); - } - if (options.hideArrow) this._panel.setAttribute("hidearrow", "true"); - this._panel.setAttribute("data-message-id", messageId); - const DURATION = options.showDescription ? 4000 : 1500; - this._panel.addEventListener( - "popupshown", - () => { - this._animationBox.setAttribute("animate", "true"); - this._timerID = setTimeout(() => this._panel.hidePopup(true), DURATION + 120); - }, - { once: true } - ); - this._panel.addEventListener("popuphidden", () => this._reset(), { once: true }); - - let { position, x, y } = options; - this._panel.openPopup(null, { position, triggerEvent: options.event }); - this._panel.moveToAnchor(anchor, position, x, y); - }; - } - if (gBrowserInit.delayedStartupFinished) init(); - else { - let delayedListener = (subject, topic) => { - if (topic == "browser-delayed-startup-finished" && subject == window) { - Services.obs.removeObserver(delayedListener, topic); - init(); - } - }; - Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); - } + function init() { + // when the confirmation hint was created, openPopup worked differently. it + // didn't set the "open" attribute on the anchor directly. that was up to + // the function calling openPopup to handle. which worked well for the + // confirmation hint, since it shouldn't set the "open" attribute at all. + // the confirmation hint isn't a popup or a panel in terms of function, it's + // more like a tooltip. so it shouldn't show the [open] style on buttons + // it's anchored to, nor should it stifle scrolling while it's open. since + // setting the attribute is now built into the binary code, we can only stop + // it by using openPopupAtScreen instead of openPopup. this is why I tried + // to get mozilla to revert this change to openPopup but nobody has ever + // responded. so for now I'm just gonna fix it with a script. it will get + // the coordinates via javascript instead of C++ + ConfirmationHint.show = function show(anchor, messageId, options = {}) { + this._reset(); + this._message.textContent = gBrowserBundle.GetStringFromName( + `confirmationHint.${messageId}.label` + ); + if (options.showDescription) { + this._description.textContent = gBrowserBundle.GetStringFromName( + `confirmationHint.${messageId}.description` + ); + this._description.hidden = false; + this._panel.classList.add("with-description"); + } else { + this._description.hidden = true; + this._panel.classList.remove("with-description"); + } + if (options.hideArrow) this._panel.setAttribute("hidearrow", "true"); + this._panel.setAttribute("data-message-id", messageId); + const DURATION = options.showDescription ? 4000 : 1500; + this._panel.addEventListener( + "popupshown", + () => { + this._animationBox.setAttribute("animate", "true"); + this._timerID = setTimeout(() => this._panel.hidePopup(true), DURATION + 120); + }, + { once: true } + ); + this._panel.addEventListener("popuphidden", () => this._reset(), { once: true }); + + let { position, x, y } = options; + this._panel.openPopup(null, { position, triggerEvent: options.event }); + this._panel.moveToAnchor(anchor, position, x, y); + }; + } + if (gBrowserInit.delayedStartupFinished) init(); + else { + let delayedListener = (subject, topic) => { + if (topic == "browser-delayed-startup-finished" && subject == window) { + Services.obs.removeObserver(delayedListener, topic); + init(); + } + }; + Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); + } })(); diff --git a/JS/debugExtensionInToolbarContextMenu.uc.js b/JS/debugExtensionInToolbarContextMenu.uc.js index 75478b4c..0b6ee193 100644 --- a/JS/debugExtensionInToolbarContextMenu.uc.js +++ b/JS/debugExtensionInToolbarContextMenu.uc.js @@ -3,20 +3,49 @@ // @version 1.4.0 // @author aminomancer // @homepage https://github.com/aminomancer/uc.css.js -// @description Adds a new context menu when right-clicking an add-on's button in the toolbar or urlbar, any time the "Manage Extension" and "Remove Extension" items are available. The new "Debug Extension" menu contains several items: "Extension Manifest" opens the extension's manifest directly in a new tab. Aside from reading the manifest, from there you can also view the whole contents of the extension within Firefox by removing "/manifest.json" from the URL. In the "View Documents" submenu there are 4 options for viewing, debugging and modding an addon's main HTML contents. "Browser Action" opens the extension's toolbar button popup URL (if it has one) in a regular browser window. The popup URL is whatever document it displays in its panel view, the popup that opens when you click the addon's toolbar button. This is the one you're most likely to want to modify with CSS. "Page Action" opens the extension's page action popup URL in the same manner. A page action is an icon on the right side of the urlbar whose behavior is specific to the page in the active tab. "Sidebar Action" opens the extension's sidebar document, so this would let you debug Tree Style Tab for example. "Extension Options" opens the document that the extension uses for configuration, also in a regular browser window. This could be the page that displays in its submenu on about:addons, or a separate page. "Inspect Extension" opens a devtools tab targeting the extension background. This is the same page you'd get if you opened about:debugging and clicked the "Inspect" button next to an extension. "View Source" opens the addon's .xpi archive. And, as you'd expect, "Copy ID" copies the extension's ID to your clipboard, while "Copy URL" copies the extension's base URL, so it can be used in CSS rules like @-moz-document. The menu items' labels are not localized automatically since Firefox doesn't include any similar strings. If you need to change the language or anything, modify the strings below under "static config." As usual, icons for the new menu are included in uc-context-menu-icons.css +// @description Adds a new context menu when right-clicking an add-on's +// button in the toolbar or urlbar, any time the "Manage Extension" and "Remove +// Extension" items are available. The new "Debug Extension" menu contains +// several items: "Extension Manifest" opens the extension's manifest directly +// in a new tab. Aside from reading the manifest, from there you can also view +// the whole contents of the extension within Firefox by removing +// "/manifest.json" from the URL. In the "View Documents" submenu there are 4 +// options for viewing, debugging and modding an addon's main HTML contents. +// "Browser Action" opens the extension's toolbar button popup URL (if it has +// one) in a regular browser window. The popup URL is whatever document it +// displays in its panel view, the popup that opens when you click the addon's +// toolbar button. This is the one you're most likely to want to modify with +// CSS. "Page Action" opens the extension's page action popup URL in the same +// manner. A page action is an icon on the right side of the urlbar whose +// behavior is specific to the page in the active tab. "Sidebar Action" opens +// the extension's sidebar document, so this would let you debug Tree Style Tab +// for example. "Extension Options" opens the document that the extension uses +// for configuration, also in a regular browser window. This could be the page +// that displays in its submenu on about:addons, or a separate page. "Inspect +// Extension" opens a devtools tab targeting the extension background. This is +// the same page you'd get if you opened about:debugging and clicked the +// "Inspect" button next to an extension. "View Source" opens the addon's .xpi +// archive. And, as you'd expect, "Copy ID" copies the extension's ID to your +// clipboard, while "Copy URL" copies the extension's base URL, so it can be +// used in CSS rules like @-moz-document. The menu items' labels are not +// localized automatically since Firefox doesn't include any similar strings. If +// you need to change the language or anything, modify the strings below under +// "static config." As usual, icons for the new menu are included in +// uc-context-menu-icons.css // @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. // ==/UserScript== class DebugExtension { - // you can modify the menu items' labels and access keys here, e.g. if you prefer another language. - // an access key is the letter highlighted in a menuitem's label. - // if the letter highlighted is "D" for example, and you press D on your keyboard - // while the context menu is open, it will automatically select the menu item with that access key. - // if two menu items have the same access key and are both visible, - // then instead of selecting one menu item it will just cycle between the two. - // however, the access key does not need to be a character in the label. - // if the access key isn't in the label, then instead of underlining the letter in the label, - // it will add the access key to the end of the label in parentheses. + // you can modify the menu items' labels and access keys here, e.g. if you + // prefer another language. an access key is the letter highlighted in a + // menuitem's label. if the letter highlighted is "D" for example, and you + // press D on your keyboard while the context menu is open, it will + // automatically select the menu item with that access key. if two menu items + // have the same access key and are both visible, then instead of selecting + // one menu item it will just cycle between the two. however, the access key + // does not need to be a character in the label. if the access key isn't in + // the label, then instead of underlining the letter in the label, it will add + // the access key to the end of the label in parentheses. // e.g. "Debug Extension (Q)" instead of "_D_ebug Extension". static config = { menuLabel: "Debug Extension", // menu label @@ -167,7 +196,8 @@ class DebugExtension { return menu; } /** - * make a menu item that opens a given type of page, with label & accesskey corresponding to those defined in the "config" static property + * make a menu item that opens a given type of page, with label & accesskey + * corresponding to those defined in the "config" static property * @param {string} type (which menuitem to make) * @param {object} popup (where to put the menuitem) * @returns a menuitem DOM node @@ -216,7 +246,8 @@ class DebugExtension { // click callback onCommand(event, popup, type) { let id = this.getExtensionId(popup); - let extension = WebExtensionPolicy.getByID(id).extension; // this contains information about an extension with a given ID. + // this contains information about an extension with a given ID. + let extension = WebExtensionPolicy.getByID(id).extension; // use extension's principal if it's available. let triggeringPrincipal = extension.principal; let url; @@ -239,7 +270,8 @@ class DebugExtension { break; case "Inspector": url = `about:devtools-toolbox?id=${encodeURIComponent(id)}&type=extension`; - triggeringPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); // use the system principal for about:devtools-toolbox + // use the system principal for about:devtools-toolbox + triggeringPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); break; case "ViewSource": this.openArchive(id); @@ -259,8 +291,8 @@ class DebugExtension { Services.io.newURI(url), {} ); - // whether to open in the current tab or a new tab. - // only opens in the current tab if the current tab is on the new tab page or home page. + // whether to open in the current tab or a new tab. only opens in the + // current tab if the current tab is on the new tab page or home page. let where = new RegExp(`(${BROWSER_NEW_TAB_URL}|${HomePage.get(window)})`, "i").test( gBrowser.currentURI.spec ) @@ -282,10 +314,10 @@ class DebugExtension { dir.append(id + ".xpi"); dir.launch(); } - // modify the internal functions that updates the visibility - // of the built-in "remove extension," "manage extension" items, etc. - // that's based on whether the button that was clicked is an extension or not, - // so it also updates the visibility of our menu by the same parameter. + // modify the internal functions that updates the visibility of the built-in + // "remove extension," "manage extension" items, etc. that's based on whether + // the button that was clicked is an extension or not, so it also updates the + // visibility of our menu by the same parameter. setupUpdate() { eval( `ToolbarContextMenu.updateExtension = async function ` + diff --git a/JS/downloadsDeleteFileCommand.uc.js b/JS/downloadsDeleteFileCommand.uc.js index 5a703f09..e56564a5 100644 --- a/JS/downloadsDeleteFileCommand.uc.js +++ b/JS/downloadsDeleteFileCommand.uc.js @@ -3,112 +3,120 @@ // @version 1.0.2 // @author aminomancer // @homepage https://github.com/aminomancer/uc.css.js -// @description Adds a new "Delete" menuitem when right-clicking a download in the downloads panel or the downloads manager. This will delete the downloaded file from disk. It's important since the ability to "temporarily" download files with Firefox is being removed as part of bug 1733587 to reduce the risk of data loss. When you choose to "open" a file instead of "save" it, Firefox will no longer save the file in your Temp folder, but rather in your chosen Downloads folder. So, being able to clean up these files from the context menu is a nice feature. This will most likely be released in Firefox (see bug 1745624), but I did a lot of the testing for it with an autoconfig script, so it isn't any extra work to publish this here, at least until it makes it into a release build. When you download a version of Firefox that includes the menuitem, you can just delete this script. +// @description Adds a new "Delete" menuitem when right-clicking a download +// in the downloads panel or the downloads manager. This will delete the +// downloaded file from disk. It's important since the ability to "temporarily" +// download files with Firefox is being removed as part of bug 1733587 to reduce +// the risk of data loss. When you choose to "open" a file instead of "save" it, +// Firefox will no longer save the file in your Temp folder, but rather in your +// chosen Downloads folder. So, being able to clean up these files from the +// context menu is a nice feature. This will most likely be released in Firefox +// (see bug 1745624), but I did a lot of the testing for it with an autoconfig +// script, so it isn't any extra work to publish this here, at least until it +// makes it into a release build. When you download a version of Firefox that +// includes the menuitem, you can just delete this script. // @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. // @include chrome://browser/content/places/places.xhtml // @include main // ==/UserScript== (function () { - function init() { - if (!("DownloadsViewUI" in window)) return; - // Make the command. - document.getElementById("downloadsCmd_alwaysOpenSimilarFiles").after( - _ucUtils.createElement(document, "command", { - id: "downloadsCmd_deleteFile", - oncommand: "goDoCommand('downloadsCmd_deleteFile')", - }) - ); - // Make the menuitem. - let context = document.getElementById("downloadsContextMenu"); - context.insertBefore( - _ucUtils.createElement(document, "menuitem", { - command: "downloadsCmd_deleteFile", - class: "downloadDeleteFileMenuItem", - "data-l10n-id": "text-action-delete", - }), - context.querySelector(".downloadRemoveFromHistoryMenuItem") - ); - let clearDownloads = context.querySelector( - `[data-l10n-id="downloads-cmd-clear-downloads"]` - ); - if (clearDownloads.getAttribute("accesskey") === "D") - clearDownloads.setAttribute("accesskey", "C"); + function init() { + if (!("DownloadsViewUI" in window)) return; + // Make the command. + document.getElementById("downloadsCmd_alwaysOpenSimilarFiles").after( + _ucUtils.createElement(document, "command", { + id: "downloadsCmd_deleteFile", + oncommand: "goDoCommand('downloadsCmd_deleteFile')", + }) + ); + // Make the menuitem. + let context = document.getElementById("downloadsContextMenu"); + context.insertBefore( + _ucUtils.createElement(document, "menuitem", { + command: "downloadsCmd_deleteFile", + class: "downloadDeleteFileMenuItem", + "data-l10n-id": "text-action-delete", + }), + context.querySelector(".downloadRemoveFromHistoryMenuItem") + ); + let clearDownloads = context.querySelector(`[data-l10n-id="downloads-cmd-clear-downloads"]`); + if (clearDownloads.getAttribute("accesskey") === "D") + clearDownloads.setAttribute("accesskey", "C"); - // Add the class method for the command. - if ( - !DownloadsViewUI.DownloadElementShell.prototype.hasOwnProperty( - "downloadsCmd_deleteFile" + // Add the class method for the command. + if (!DownloadsViewUI.DownloadElementShell.prototype.hasOwnProperty("downloadsCmd_deleteFile")) + DownloadsViewUI.DownloadElementShell.prototype.downloadsCmd_deleteFile = + async function downloadsCmd_deleteFile() { + let { download } = this; + let { path } = download.target; + let { succeeded } = download; + let indicator = DownloadsCommon.getIndicatorData(this.element.ownerGlobal); + // Remove the download view. + await DownloadsCommon.deleteDownload(download); + if (succeeded) { + // Temp files are made "read-only" by DownloadIntegration.downloadDone, + // so reset the permission bits to read/write. This won't be necessary + // after 1733587 since Downloads won't ever be temporary. + let info = await IOUtils.stat(path); + await IOUtils.setPermissions(path, 0o660); + await IOUtils.remove(path, { + ignoreAbsent: true, + recursive: info.type === "directory", + }); + } + if (!indicator._hasDownloads) indicator.attention = DownloadsCommon.ATTENTION_NONE; + }; + // Add a class method for the panel's class (extends the class above) to handle a special case. + if ( + "DownloadsViewItem" in window && + !DownloadsViewItem.prototype.hasOwnProperty("downloadsCmd_deleteFile") + ) { + DownloadsViewItem.prototype.downloadsCmd_deleteFile = + async function downloadsCmd_deleteFile() { + await DownloadsViewUI.DownloadElementShell.prototype.downloadsCmd_deleteFile.call(this); + // Protects against an unusual edge case where the user: + // 1) downloads a file with Firefox; + // 2) deletes the file from outside of Firefox, e.g., a file manager; + // 3) downloads the same file from the same source; + // 4) opens the downloads panel and uses the menuitem to delete one of those 2 files. + // Under those conditions, Firefox will make 2 view items even though + // there's only 1 file. Using this method will only delete the view + // item it was called on, because this instance is not aware of other + // view items with identical targets. So the remaining view item needs + // to be refreshed to hide the "Delete" option. That example only + // concerns 2 duplicate view items but you can have an arbitrary + // number, so iterate over all items... + for (let viewItem of DownloadsView._visibleViewItems.values()) { + viewItem.download.refresh().catch(Cu.reportError); + } + // Don't use DownloadsPanel.hidePanel for this method because it will remove + // the view item from the list, which is already sufficient feedback. + }; + } + // Show/hide the menuitem based on whether there's any file to delete. + if (DownloadsViewUI.updateContextMenuForElement.name === "updateContextMenuForElement") + eval( + `DownloadsViewUI.updateContextMenuForElement = function ` + + DownloadsViewUI.updateContextMenuForElement + .toSource() + .replace(/^updateContextMenuForElement/, "") + .replace( + /(let download = element\._shell\.download;)/, + `$1\n contextMenu.querySelector(".downloadDeleteFileMenuItem").hidden =\n !(download.target.exists || download.target.partFileExists);\n` ) - ) - DownloadsViewUI.DownloadElementShell.prototype.downloadsCmd_deleteFile = - async function downloadsCmd_deleteFile() { - let { download } = this; - let { path } = download.target; - let { succeeded } = download; - let indicator = DownloadsCommon.getIndicatorData(this.element.ownerGlobal); - // Remove the download view. - await DownloadsCommon.deleteDownload(download); - if (succeeded) { - // Temp files are made "read-only" by DownloadIntegration.downloadDone, so reset the permission bits to read/write. - // This won't be necessary after 1733587 since Downloads won't ever be temporary. - let info = await IOUtils.stat(path); - await IOUtils.setPermissions(path, 0o660); - await IOUtils.remove(path, { - ignoreAbsent: true, - recursive: info.type === "directory", - }); - } - if (!indicator._hasDownloads) - indicator.attention = DownloadsCommon.ATTENTION_NONE; - }; - // Add a class method for the panel's class (extends the class above) to handle a special case. - if ( - "DownloadsViewItem" in window && - !DownloadsViewItem.prototype.hasOwnProperty("downloadsCmd_deleteFile") - ) { - DownloadsViewItem.prototype.downloadsCmd_deleteFile = - async function downloadsCmd_deleteFile() { - await DownloadsViewUI.DownloadElementShell.prototype.downloadsCmd_deleteFile.call( - this - ); - // Protects against an unusual edge case where the user: - // 1) downloads a file with Firefox; 2) deletes the file from outside of Firefox, e.g., a file manager; - // 3) downloads the same file from the same source; 4) opens the downloads panel and uses the menuitem to delete one of those 2 files; - // Under those conditions, Firefox will make 2 view items even though there's only 1 file. - // Using this method will only delete the view item it was called on, because this instance is not aware of other view items with identical targets. - // So the remaining view item needs to be refreshed to hide the "Delete" option. - // That example only concerns 2 duplicate view items but you can have an arbitrary number, so iterate over all items... - for (let viewItem of DownloadsView._visibleViewItems.values()) { - viewItem.download.refresh().catch(Cu.reportError); - } - // Don't use DownloadsPanel.hidePanel for this method because it will remove - // the view item from the list, which is already sufficient feedback. - }; + ); + } + if ("gBrowserInit" in window) { + if (gBrowserInit.delayedStartupFinished) init(); + else { + let delayedListener = (subject, topic) => { + if (topic == "browser-delayed-startup-finished" && subject == window) { + Services.obs.removeObserver(delayedListener, topic); + init(); } - // Show/hide the menuitem based on whether there's any file to delete. - if (DownloadsViewUI.updateContextMenuForElement.name === "updateContextMenuForElement") - eval( - `DownloadsViewUI.updateContextMenuForElement = function ` + - DownloadsViewUI.updateContextMenuForElement - .toSource() - .replace(/^updateContextMenuForElement/, "") - .replace( - /(let download = element\._shell\.download;)/, - `$1\n contextMenu.querySelector(".downloadDeleteFileMenuItem").hidden =\n !(download.target.exists || download.target.partFileExists);\n` - ) - ); + }; + Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); } - if ("gBrowserInit" in window) { - if (gBrowserInit.delayedStartupFinished) init(); - else { - let delayedListener = (subject, topic) => { - if (topic == "browser-delayed-startup-finished" && subject == window) { - Services.obs.removeObserver(delayedListener, topic); - init(); - } - }; - Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); - } - } else init(); + } else init(); })(); diff --git a/JS/enterInUrlbarToRefresh.uc.js b/JS/enterInUrlbarToRefresh.uc.js index 46d552da..dfac46d8 100644 --- a/JS/enterInUrlbarToRefresh.uc.js +++ b/JS/enterInUrlbarToRefresh.uc.js @@ -24,140 +24,138 @@ // ==/UserScript== (function () { - function init() { - let source = gURLBar._loadURL.toSource(); - if (source.startsWith("(function")) return; - eval( - `gURLBar._loadURL = function ` + - source - .replace( - /(SitePermissions\.clearTemporaryBlockPermissions\(browser\)\;)/, - `$1\n params.isReload = true;` - ) - .replace( - /(this\.window\.openTrustedLinkIn\(url, openUILinkWhere, params\);)/, - `if (params.isReload) {\n delete params.isReload;\n this.openLinkWithForceReload(url, params);\n } else {\n $1\n }` - ) - ); - /** - * Open a url in the current tab and ensure it reloads rather than anchor navigation. - * - * @param {string} url - * The URL to open. - * @param {object} params - * The parameters related to how and where the result will be opened. - * Further supported paramters are listed in utilityOverlay.js#openUILinkIn. - */ - gURLBar.openLinkWithForceReload = function (url, params = {}) { - if (!url) { - return; - } - if (!params.triggeringPrincipal) { - params.triggeringPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); - } - params.fromChrome = params.fromChrome ?? true; + function init() { + let source = gURLBar._loadURL.toSource(); + if (source.startsWith("(function")) return; + eval( + `gURLBar._loadURL = function ` + + source + .replace( + /(SitePermissions\.clearTemporaryBlockPermissions\(browser\)\;)/, + `$1\n params.isReload = true;` + ) + .replace( + /(this\.window\.openTrustedLinkIn\(url, openUILinkWhere, params\);)/, + `if (params.isReload) {\n delete params.isReload;\n this.openLinkWithForceReload(url, params);\n } else {\n $1\n }` + ) + ); + /** + * Open a url in the current tab and ensure it reloads rather than anchor navigation. + * + * @param {string} url + * The URL to open. + * @param {object} params + * The parameters related to how and where the result will be opened. + * Further supported paramters are listed in utilityOverlay.js#openUILinkIn. + */ + gURLBar.openLinkWithForceReload = function (url, params = {}) { + if (!url) { + return; + } + if (!params.triggeringPrincipal) { + params.triggeringPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + } + params.fromChrome = params.fromChrome ?? true; - let aAllowThirdPartyFixup = params.allowThirdPartyFixup; - let aPostData = params.postData; - let aReferrerInfo = params.referrerInfo - ? params.referrerInfo - : new this.window.ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, null); - let aAllowInheritPrincipal = !!params.allowInheritPrincipal; - let aForceAllowDataURI = params.forceAllowDataURI; - let aIsPrivate = params.private; - let aAllowPopups = !!params.allowPopups; - let aUserContextId = params.userContextId; - let aIndicateErrorPageLoad = params.indicateErrorPageLoad; - let aPrincipal = params.originPrincipal; - let aStoragePrincipal = params.originStoragePrincipal; - let aTriggeringPrincipal = params.triggeringPrincipal; - let aCsp = params.csp; - let aForceAboutBlankViewerInCurrent = params.forceAboutBlankViewerInCurrent; - let aResolveOnContentBrowserReady = params.resolveOnContentBrowserCreated; + let aAllowThirdPartyFixup = params.allowThirdPartyFixup; + let aPostData = params.postData; + let aReferrerInfo = params.referrerInfo + ? params.referrerInfo + : new this.window.ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, null); + let aAllowInheritPrincipal = !!params.allowInheritPrincipal; + let aForceAllowDataURI = params.forceAllowDataURI; + let aIsPrivate = params.private; + let aAllowPopups = !!params.allowPopups; + let aUserContextId = params.userContextId; + let aIndicateErrorPageLoad = params.indicateErrorPageLoad; + let aPrincipal = params.originPrincipal; + let aStoragePrincipal = params.originStoragePrincipal; + let aTriggeringPrincipal = params.triggeringPrincipal; + let aCsp = params.csp; + let aForceAboutBlankViewerInCurrent = params.forceAboutBlankViewerInCurrent; + let aResolveOnContentBrowserReady = params.resolveOnContentBrowserCreated; - let w = this.window; + let w = this.window; - function useOAForPrincipal(principal) { - if (principal && principal.isContentPrincipal) { - let attrs = { - userContextId: aUserContextId, - privateBrowsingId: - aIsPrivate || (w && PrivateBrowsingUtils.isWindowPrivate(w)), - firstPartyDomain: principal.originAttributes.firstPartyDomain, - }; - return Services.scriptSecurityManager.principalWithOA(principal, attrs); - } - return principal; - } - aPrincipal = useOAForPrincipal(aPrincipal); - aStoragePrincipal = useOAForPrincipal(aStoragePrincipal); - aTriggeringPrincipal = useOAForPrincipal(aTriggeringPrincipal); + function useOAForPrincipal(principal) { + if (principal && principal.isContentPrincipal) { + let attrs = { + userContextId: aUserContextId, + privateBrowsingId: aIsPrivate || (w && PrivateBrowsingUtils.isWindowPrivate(w)), + firstPartyDomain: principal.originAttributes.firstPartyDomain, + }; + return Services.scriptSecurityManager.principalWithOA(principal, attrs); + } + return principal; + } + aPrincipal = useOAForPrincipal(aPrincipal); + aStoragePrincipal = useOAForPrincipal(aStoragePrincipal); + aTriggeringPrincipal = useOAForPrincipal(aTriggeringPrincipal); - w.focus(); + w.focus(); - let uriObj; - let targetBrowser = params.targetBrowser || w.gBrowser.selectedBrowser; - try { - uriObj = Services.io.newURI(url); - } catch (e) {} + let uriObj; + let targetBrowser = params.targetBrowser || w.gBrowser.selectedBrowser; + try { + uriObj = Services.io.newURI(url); + } catch (e) {} - let flags = Ci.nsIWebNavigation.LOAD_FLAGS_IS_REFRESH; - if (aAllowThirdPartyFixup) { - flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; - flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; - } - if (!aAllowInheritPrincipal) { - flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL; - } - if (aAllowPopups) { - flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_POPUPS; - } - if (aIndicateErrorPageLoad) { - flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ERROR_LOAD_CHANGES_RV; - } - if (aForceAllowDataURI) { - flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FORCE_ALLOW_DATA_URI; - } - let { URI_INHERITS_SECURITY_CONTEXT } = Ci.nsIProtocolHandler; - if ( - aForceAboutBlankViewerInCurrent && - (!uriObj || this.window.doGetProtocolFlags(uriObj) & URI_INHERITS_SECURITY_CONTEXT) - ) { - targetBrowser.createAboutBlankContentViewer(aPrincipal, aStoragePrincipal); - } + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_IS_REFRESH; + if (aAllowThirdPartyFixup) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + } + if (!aAllowInheritPrincipal) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL; + } + if (aAllowPopups) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_POPUPS; + } + if (aIndicateErrorPageLoad) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ERROR_LOAD_CHANGES_RV; + } + if (aForceAllowDataURI) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FORCE_ALLOW_DATA_URI; + } + let { URI_INHERITS_SECURITY_CONTEXT } = Ci.nsIProtocolHandler; + if ( + aForceAboutBlankViewerInCurrent && + (!uriObj || this.window.doGetProtocolFlags(uriObj) & URI_INHERITS_SECURITY_CONTEXT) + ) { + targetBrowser.createAboutBlankContentViewer(aPrincipal, aStoragePrincipal); + } - targetBrowser.loadURI(url, { - triggeringPrincipal: aTriggeringPrincipal, - csp: aCsp, - flags, - referrerInfo: aReferrerInfo, - postData: aPostData, - userContextId: aUserContextId, - }); - if (aResolveOnContentBrowserReady) { - aResolveOnContentBrowserReady(targetBrowser); - } + targetBrowser.loadURI(url, { + triggeringPrincipal: aTriggeringPrincipal, + csp: aCsp, + flags, + referrerInfo: aReferrerInfo, + postData: aPostData, + userContextId: aUserContextId, + }); + if (aResolveOnContentBrowserReady) { + aResolveOnContentBrowserReady(targetBrowser); + } - let focusUrlBar = - w.document.activeElement == w.gURLBar.inputField && w.isBlankPageURL(url); - if ( - !params.avoidBrowserFocus && - !focusUrlBar && - targetBrowser == w.gBrowser.selectedBrowser - ) { - targetBrowser.focus(); - } - }; - } + let focusUrlBar = w.document.activeElement == w.gURLBar.inputField && w.isBlankPageURL(url); + if ( + !params.avoidBrowserFocus && + !focusUrlBar && + targetBrowser == w.gBrowser.selectedBrowser + ) { + targetBrowser.focus(); + } + }; + } - if (gBrowserInit.delayedStartupFinished) init(); - else { - let delayedListener = (subject, topic) => { - if (topic == "browser-delayed-startup-finished" && subject == window) { - Services.obs.removeObserver(delayedListener, topic); - init(); - } - }; - Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); - } + if (gBrowserInit.delayedStartupFinished) init(); + else { + let delayedListener = (subject, topic) => { + if (topic == "browser-delayed-startup-finished" && subject == window) { + Services.obs.removeObserver(delayedListener, topic); + init(); + } + }; + Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); + } })(); diff --git a/JS/extensionOptionsPanel.uc.js b/JS/extensionOptionsPanel.uc.js index 06d354b1..516ad7a3 100644 --- a/JS/extensionOptionsPanel.uc.js +++ b/JS/extensionOptionsPanel.uc.js @@ -3,933 +3,941 @@ // @version 1.8.2 // @author aminomancer // @homepage https://github.com/aminomancer/uc.css.js -// @description This script creates a toolbar button that opens a popup panel where extensions can be configured, disabled, uninstalled, etc. Each extension gets its own button in the panel. Clicking an extension's button leads to a subview where you can jump to the extension's options, disable or enable the extension, uninstall it, configure automatic updates, disable/enable it in private browsing, view its source code in whatever program is associated with .xpi files, open the extension's homepage, or copy the extension's ID. The panel can also be opened from the App Menu, using the built-in "Add-ons and themes" button. Since v1.8, themes will also be listed in the panel. Hovering a theme will show a tooltip with a preview/screenshot of the theme, and clicking the theme will toggle it on or off. There are several translation and configuration options directly below. +// @description This script creates a toolbar button that opens a popup panel +// where extensions can be configured, disabled, uninstalled, etc. Each +// extension gets its own button in the panel. Clicking an extension's button +// leads to a subview where you can jump to the extension's options, disable or +// enable the extension, uninstall it, configure automatic updates, +// disable/enable it in private browsing, view its source code in whatever +// program is associated with .xpi files, open the extension's homepage, or copy +// the extension's ID. The panel can also be opened from the App Menu, using the +// built-in "Add-ons and themes" button. Since v1.8, themes will also be listed +// in the panel. Hovering a theme will show a tooltip with a preview/screenshot +// of the theme, and clicking the theme will toggle it on or off. There are +// several translation and configuration options directly below. // @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. // ==/UserScript== class ExtensionOptionsWidget { - // user configuration. change the value to the right of the colon. - static config = { - "Replace addons button": true, // this script replaces the "Add-ons & Themes" button in the app menu with an "Extensions" button that opens our new panel instead of opening about:addons. set to false if you want to leave this button alone + // user configuration. change the value to the right of the colon. + static config = { + "Replace addons button": true, // this script replaces the "Add-ons & Themes" button in the app menu with an "Extensions" button that opens our new panel instead of opening about:addons. set to false if you want to leave this button alone - "Show header": true, // set to false if you don't want the "Add-on options" title to be displayed at the top of the panel + "Show header": true, // set to false if you don't want the "Add-on options" title to be displayed at the top of the panel - "Show version": false, // show the addon version next to its name in the list + "Show version": false, // show the addon version next to its name in the list - "Show addon messages": true, // about:addons shows you when an addon has a warning or error, e.g. it's unsigned or blocked. if this is set to true, we'll show the same information in the panel + "Show addon messages": true, // about:addons shows you when an addon has a warning or error, e.g. it's unsigned or blocked. if this is set to true, we'll show the same information in the panel - "Show theme preview tooltips": true, // when hovering a theme in the panel, a preview/screenshot of the theme will be displayed in a tooltip, if possible. this depends on the add-on author. + "Show theme preview tooltips": true, // when hovering a theme in the panel, a preview/screenshot of the theme will be displayed in a tooltip, if possible. this depends on the add-on author. - "Show hidden extensions": false, // show system extensions? + "Show hidden extensions": false, // show system extensions? - "Show disabled extensions": true, // show extensions that you've disabled? + "Show disabled extensions": true, // show extensions that you've disabled? - "Show enabled extensions first": true, // show enabled extensions at the top of the list and disabled extensions at the bottom? + "Show enabled extensions first": true, // show enabled extensions at the top of the list and disabled extensions at the bottom? - "Addon ID blacklist": [], // put addon IDs in this list, separated by commas, to exclude them from the list, e.g. ["screenshots@mozilla.org", "dark-theme@mozilla.org"] + "Addon ID blacklist": [], // put addon IDs in this list, separated by commas, to exclude them from the list, e.g. ["screenshots@mozilla.org", "dark-theme@mozilla.org"] - "Icon URL": `chrome://mozapps/skin/extensions/extension.svg`, // if you want to change the button's icon for some reason, you can replace this string with any URL or data URL that leads to an image. + "Icon URL": `chrome://mozapps/skin/extensions/extension.svg`, // if you want to change the button's icon for some reason, you can replace this string with any URL or data URL that leads to an image. - // localization strings - l10n: { - "Button label": "Add-ons and themes", // what should the button's label be when it's in the overflow panel or customization palette? + // localization strings + l10n: { + "Button label": "Add-ons and themes", // what should the button's label be when it's in the overflow panel or customization palette? - "Button tooltip": "Add-ons and themes", // what should the button's tooltip be? I use sentence case since that's the convention. + "Button tooltip": "Add-ons and themes", // what should the button's tooltip be? I use sentence case since that's the convention. - "Panel title": "Add-ons and themes", // title shown at the top of the panel (when "Show header" is true) + "Panel title": "Add-ons and themes", // title shown at the top of the panel (when "Show header" is true) - "Download addons label": "Download add-ons", // label for the button that appears when you have no addons installed. + "Download addons label": "Download add-ons", // label for the button that appears when you have no addons installed. - "Addons page label": "Add-ons page", // label for the about:addons button at the bottom of the panel + "Addons page label": "Add-ons page", // label for the about:addons button at the bottom of the panel - "Addon options label": "Extension options", // labels for the addon subview buttons + "Addon options label": "Extension options", // labels for the addon subview buttons - "Manage addon label": "Manage add-on", + "Manage addon label": "Manage add-on", - "Enable addon label": "Enable", + "Enable addon label": "Enable", - "Disable addon label": "Disable", + "Disable addon label": "Disable", - "Uninstall addon label": "Uninstall", + "Uninstall addon label": "Uninstall", - "View source label": "View source", + "View source label": "View source", - "Manage shortcuts label": "Manage shortcuts", + "Manage shortcuts label": "Manage shortcuts", - "Open homepage label": "Open homepage", + "Open homepage label": "Open homepage", - "Copy ID label": "Copy ID", + "Copy ID label": "Copy ID", - "Automatic updates label": "Automatic updates:", + "Automatic updates label": "Automatic updates:", - // labels for the automatic update radio buttons - autoUpdate: { - "Default label": "Default", + // labels for the automatic update radio buttons + autoUpdate: { + "Default label": "Default", - "On label": "On", + "On label": "On", - "Off label": "Off", - }, + "Off label": "Off", + }, - "Run in private windows label": "Run in private windows:", + "Run in private windows label": "Run in private windows:", - // labels for the run in private windows radio buttons - runInPrivate: { - "Allow label": "Allow", + // labels for the run in private windows radio buttons + runInPrivate: { + "Allow label": "Allow", - "Don't allow label": "Don't allow", - }, - - // labels for addon buttons that have a warning or error, - // e.g. addon automatically disabled because it's on a blocklist or unsigned - addonMessages: { - "Blocked": "Blocked", - - "Signature required": "Signature required", - - "Incompatible": "Incompatible", - - "Unverified": "Unverified", - - "Insecure": "Insecure", - }, + "Don't allow label": "Don't allow", + }, + + // labels for addon buttons that have a warning or error, + // e.g. addon automatically disabled because it's on a blocklist or unsigned + addonMessages: { + "Blocked": "Blocked", + + "Signature required": "Signature required", + + "Incompatible": "Incompatible", + + "Unverified": "Unverified", + + "Insecure": "Insecure", + }, + }, + }; + + /** + * create a DOM node with given parameters + * @param {object} aDoc (which doc to create the element in) + * @param {string} tag (an HTML tag name, like "button" or "p") + * @param {object} props (an object containing attribute name/value pairs, + * e.g. class: ".bookmark-item") + * @param {boolean} isHTML (if true, create an HTML element. if omitted or + * false, create a XUL element. generally avoid HTML + * when modding the UI, most UI elements are actually + * XUL elements.) + * @returns the created DOM node + */ + create(aDoc, tag, props, isHTML = false) { + let el = isHTML ? aDoc.createElement(tag) : aDoc.createXULElement(tag); + for (let prop in props) el.setAttribute(prop, props[prop]); + return el; + } + + /** + * set or remove multiple attributes for a given node + * @param {object} el (a DOM node) + * @param {object} props (an object of attribute name/value pairs) + * @returns the DOM node + */ + setAttributes(el, props) { + for (let [name, value] of Object.entries(props)) + if (value) el.setAttribute(name, value); + else el.removeAttribute(name); + } + + /** + * make a valid ID for a DOM node based on an extension's ID. + * @param {string} id (an extension's ID) + * @returns an ID with crap removed so it can be used in a DOM node's ID. + */ + makeWidgetId(id) { + id = id.toLowerCase(); + return id.replace(/[^a-z0-9_-]/g, "_"); + } + + /** + * for a given addon ID, get the Extension object from the addon policy + * @param {string} id (an addon's ID) + * @returns the Extension object + */ + extensionForAddonId(id) { + let policy = WebExtensionPolicy.getByID(id); + return policy && policy.extension; + } + + /** + * find out if an addon has a valid signature + * @param {object} addon (an Addon object, retrieved by AddonManager.getAddonsByTypes) + * @returns true if signed, false if unsigned or invalid + */ + isCorrectlySigned(addon) { + // Add-ons without an "isCorrectlySigned" property are correctly signed as + // they aren't the correct type for signing. + return addon.isCorrectlySigned !== false; + } + + /** + * find out if an addon has been automatically disabled from the xpi database + * because it lacked a valid signature and user had xpinstall.signatures.required = true + * @param {object} addon (an Addon object) + * @returns true if the addon was auto-disabled + */ + isDisabledUnsigned(addon) { + let signingRequired = + addon.type == "locale" ? this.LANGPACKS_REQUIRE_SIGNING : this.REQUIRE_SIGNING; + return signingRequired && !this.isCorrectlySigned(addon); + } + + /** + * find an addon's screenshot url. prefer 680x92. + * @param {object} addon (an Addon object) + * @returns {string} url + */ + getScreenshotUrlForAddon(addon) { + if (addon.id == "default-theme@mozilla.org") + return "chrome://mozapps/content/extensions/default-theme/preview.svg"; + const builtInThemePreview = this.BuiltInThemes.previewForBuiltInThemeId(addon.id); + if (builtInThemePreview) return builtInThemePreview; + let { screenshots } = addon; + if (!screenshots || !screenshots.length) return null; + let screenshot = screenshots.find(s => s.width === 680 && s.height === 92); + if (!screenshot) screenshot = screenshots[0]; + return screenshot.url; + } + + // where panelviews are hiding when we're not looking + viewCache(doc) { + return doc.getElementById("appMenu-viewCache"); + } + + constructor() { + XPCOMUtils.defineLazyModuleGetters(this, { + ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.jsm", + BuiltInThemes: "resource:///modules/BuiltInThemes.jsm", + }); + XPCOMUtils.defineLazyGetter(this, "extBundle", function () { + return Services.strings.createBundle("chrome://global/locale/extensions.properties"); + }); + XPCOMUtils.defineLazyPreferenceGetter( + this, + "REQUIRE_SIGNING", + "xpinstall.signatures.required", + false + ); + XPCOMUtils.defineLazyPreferenceGetter( + this, + "LANGPACKS_REQUIRE_SIGNING", + "extensions.langpacks.signatures.required", + false + ); + this.viewId = "PanelUI-eom"; + this.config = ExtensionOptionsWidget.config; + let l10n = this.config.l10n; + if ( + /^chrome:\/\/browser\/content\/browser.(xul||xhtml)$/i.test(location) && + !CustomizableUI.getPlacementOfWidget("eom-button", true) + ) + CustomizableUI.createWidget({ + id: "eom-button", + viewId: this.viewId, + type: "view", + defaultArea: CustomizableUI.AREA_NAVBAR, + removable: true, + label: l10n["Button label"], + tooltiptext: l10n["Button tooltip"], + // if the button is middle-clicked, open the addons page instead of the panel + onClick: event => { + if (event.button == 1) { + event.target.ownerGlobal.BrowserOpenAddonsMgr("addons://list/extension"); + } }, - }; - - /** - * create a DOM node with given parameters - * @param {object} aDoc (which doc to create the element in) - * @param {string} tag (an HTML tag name, like "button" or "p") - * @param {object} props (an object containing attribute name/value pairs, e.g. class: ".bookmark-item") - * @param {boolean} isHTML (if true, create an HTML element. if omitted or false, create a XUL element. generally avoid HTML when modding the UI, most UI elements are actually XUL elements.) - * @returns the created DOM node - */ - create(aDoc, tag, props, isHTML = false) { - let el = isHTML ? aDoc.createElement(tag) : aDoc.createXULElement(tag); - for (let prop in props) el.setAttribute(prop, props[prop]); - return el; - } - - /** - * set or remove multiple attributes for a given node - * @param {object} el (a DOM node) - * @param {object} props (an object of attribute name/value pairs) - * @returns the DOM node - */ - setAttributes(el, props) { - for (let [name, value] of Object.entries(props)) - if (value) el.setAttribute(name, value); - else el.removeAttribute(name); - } - - /** - * make a valid ID for a DOM node based on an extension's ID. - * @param {string} id (an extension's ID) - * @returns an ID with crap removed so it can be used in a DOM node's ID. - */ - makeWidgetId(id) { - id = id.toLowerCase(); - return id.replace(/[^a-z0-9_-]/g, "_"); - } - - /** - * for a given addon ID, get the Extension object from the addon policy - * @param {string} id (an addon's ID) - * @returns the Extension object - */ - extensionForAddonId(id) { - let policy = WebExtensionPolicy.getByID(id); - return policy && policy.extension; - } - - /** - * find out if an addon has a valid signature - * @param {object} addon (an Addon object, retrieved by AddonManager.getAddonsByTypes) - * @returns true if signed, false if unsigned or invalid - */ - isCorrectlySigned(addon) { - // Add-ons without an "isCorrectlySigned" property are correctly signed as they aren't the correct type for signing. - return addon.isCorrectlySigned !== false; - } - - /** - * find out if an addon has been automatically disabled from the xpi database - * because it lacked a valid signature and user had xpinstall.signatures.required = true - * @param {object} addon (an Addon object) - * @returns true if the addon was auto-disabled - */ - isDisabledUnsigned(addon) { - let signingRequired = - addon.type == "locale" ? this.LANGPACKS_REQUIRE_SIGNING : this.REQUIRE_SIGNING; - return signingRequired && !this.isCorrectlySigned(addon); - } - - /** - * find an addon's screenshot url. prefer 680x92. - * @param {object} addon (an Addon object) - * @returns {string} url - */ - getScreenshotUrlForAddon(addon) { - if (addon.id == "default-theme@mozilla.org") - return "chrome://mozapps/content/extensions/default-theme/preview.svg"; - const builtInThemePreview = this.BuiltInThemes.previewForBuiltInThemeId(addon.id); - if (builtInThemePreview) return builtInThemePreview; - let { screenshots } = addon; - if (!screenshots || !screenshots.length) return null; - let screenshot = screenshots.find((s) => s.width === 680 && s.height === 92); - if (!screenshot) screenshot = screenshots[0]; - return screenshot.url; - } - - // where panelviews are hiding when we're not looking - viewCache(doc) { - return doc.getElementById("appMenu-viewCache"); - } + // create the panelview before the toolbar button + onBeforeCreated: aDoc => { + let eop = aDoc.defaultView.extensionOptionsPanel; + if (!eop) return; + let view = eop.create(aDoc, "panelview", { + id: eop.viewId, + class: "PanelUI-subView cui-widget-panelview", + flex: "1", + style: "min-width:30em", + }); + aDoc.getElementById("appMenu-viewCache").appendChild(view); + aDoc.defaultView.extensionOptionsPanel.panelview = view; + + if (eop.config["Show header"]) { + let header = view.appendChild( + eop.create(aDoc, "vbox", { id: "eom-mainView-panel-header" }) + ); + let heading = header.appendChild(eop.create(aDoc, "label")); + let label = heading.appendChild( + eop.create(aDoc, "html:span", { + id: "eom-mainView-panel-header-span", + role: "heading", + "aria-level": "1", + }) + ); + label.textContent = l10n["Panel title"]; + view.appendChild(aDoc.createXULElement("toolbarseparator")); + } + + view.appendChild( + eop.create(aDoc, "vbox", { + id: view.id + "-body", + class: "panel-subview-body", + }) + ); + + // create the theme preview tooltip + aDoc + .getElementById("mainPopupSet") + .appendChild( + aDoc.defaultView.MozXULElement.parseXULToFragment( + `` + ) + ); - constructor() { - XPCOMUtils.defineLazyModuleGetters(this, { - ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.jsm", - BuiltInThemes: "resource:///modules/BuiltInThemes.jsm", - }); - XPCOMUtils.defineLazyGetter(this, "extBundle", function () { - return Services.strings.createBundle("chrome://global/locale/extensions.properties"); - }); - XPCOMUtils.defineLazyPreferenceGetter( - this, - "REQUIRE_SIGNING", - "xpinstall.signatures.required", - false - ); - XPCOMUtils.defineLazyPreferenceGetter( - this, - "LANGPACKS_REQUIRE_SIGNING", - "extensions.langpacks.signatures.required", - false - ); - this.viewId = "PanelUI-eom"; - this.config = ExtensionOptionsWidget.config; - let l10n = this.config.l10n; + eop.fluentSetup(aDoc).then(() => eop.swapAddonsButton(aDoc)); + }, + // populate the panel before it's shown + onViewShowing: event => { + if (event.originalTarget === event.target.ownerGlobal.extensionOptionsPanel?.panelview) + event.target.ownerGlobal.extensionOptionsPanel.getAddonsAndPopulate(event); + }, + // delete the panel if the widget node is destroyed + onDestroyed: aDoc => { + let view = aDoc.getElementById(aDoc.defaultView.extensionOptionsPanel?.viewId); + if (view) { + aDoc.defaultView.CustomizableUI.hidePanelForNode(view); + view.remove(); + } + }, + }); + this.loadStylesheet(); // load the stylesheet + } + + // grab localized strings for the extensions button and disabled/enabled extensions headings + async fluentSetup(aDoc) { + aDoc.ownerGlobal.MozXULElement.insertFTLIfNeeded("toolkit/about/aboutAddons.ftl"); + let [extensions, themes, enabled, disabled, privateHelp] = await aDoc.l10n.formatValues([ + "addon-category-extension", + "addon-category-theme", + "extension-enabled-heading", + "extension-disabled-heading", + "addon-detail-private-browsing-help", + ]); + privateHelp = privateHelp.replace(/\s*\<.*\>$/, ""); + this.aboutAddonsStrings = { extensions, themes, enabled, disabled, privateHelp }; + } + + /** + * this script changes the built-in "Add-ons & themes" button in the app menu + * to open our new panel instead of opening about:addons + * @param {object} aDoc (the document our widget has been created within) + */ + swapAddonsButton(aDoc) { + if (!this.config["Replace addons button"]) return; + let win = aDoc.defaultView; + win.PanelUI._initialized || win.PanelUI.init(shouldSuppressPopupNotifications); + this.setAttributes( + win.PanelUI.mainView.querySelector("#appMenu-extensions-themes-button") || + win.PanelUI.mainView.querySelector("#appMenu-addons-button"), + { + command: 0, + key: 0, + shortcut: 0, + class: "subviewbutton subviewbutton-nav", + oncommand: "PanelUI.showSubView('PanelUI-eom', this);", + closemenu: "none", + } + ); + } + + /** + * grab all addons and populate the panel with them. + * @param {object} e (a ViewShowing event) + */ + async getAddonsAndPopulate(e) { + let extensions = await AddonManager.getAddonsByTypes(["extension"]); + let themes = await AddonManager.getAddonsByTypes(["theme"]); + this.populatePanelBody(e, { extensions, themes }); + } + + /** + * create everything inside the panel + * @param {object} e (a ViewShowing event - its target is the panelview node) + * @param {array} addons (an object containing arrays for different addon + * types e.g. extensions, themes) + */ + populatePanelBody(e, addons) { + let prevState; + let { extensions, themes } = addons; + let view = e?.target || this.panelview; + let win = view.ownerGlobal; + let doc = win.document; + let body = view.querySelector(".panel-subview-body"); + let l10n = this.config.l10n; + let enabledFirst = this.config["Show enabled extensions first"]; + let showVersion = this.config["Show version"]; + let showDisabled = this.config["Show disabled extensions"]; + let blackListArray = this.config["Addon ID blacklist"]; + + // clear all the panel items and subviews before rebuilding them. + while (body.hasChildNodes()) body.removeChild(body.firstChild); + [...this.viewCache(doc).children].forEach(panel => { + if (panel.id.includes("PanelUI-eom-addon-")) panel.remove(); + }); + let appMenuMultiView = win.PanelMultiView.forNode(PanelUI.multiView); + if (win.PanelMultiView.forNode(view.closest("panelmultiview")) === appMenuMultiView) + [...appMenuMultiView._viewStack.children].forEach(panel => { + if (panel.id !== view.id && panel.id.includes("PanelUI-eom-addon-")) panel.remove(); + }); + + // extensions... + let enabledSubheader = body.appendChild( + this.create(doc, "h2", { class: "subview-subheader" }, true) + ); + enabledSubheader.textContent = this.aboutAddonsStrings[showDisabled ? "enabled" : "extensions"]; + extensions + .sort((a, b) => { + // get sorted by enabled state... + let ka = (enabledFirst ? (a.isActive ? "0" : "1") : "") + a.name.toLowerCase(); + let kb = (enabledFirst ? (b.isActive ? "0" : "1") : "") + b.name.toLowerCase(); + return ka < kb ? -1 : 1; + }) + .forEach(addon => { + // then get excluded if config wills it... if ( - /^chrome:\/\/browser\/content\/browser.(xul||xhtml)$/i.test(location) && - !CustomizableUI.getPlacementOfWidget("eom-button", true) - ) - CustomizableUI.createWidget({ - id: "eom-button", - viewId: this.viewId, - type: "view", - defaultArea: CustomizableUI.AREA_NAVBAR, - removable: true, - label: l10n["Button label"], - tooltiptext: l10n["Button tooltip"], - // if the button is middle-clicked, open the addons page instead of the panel - onClick: (event) => { - if (event.button == 1) { - event.target.ownerGlobal.BrowserOpenAddonsMgr("addons://list/extension"); - } - }, - // create the panelview before the toolbar button - onBeforeCreated: (aDoc) => { - let eop = aDoc.defaultView.extensionOptionsPanel; - if (!eop) return; - let view = eop.create(aDoc, "panelview", { - id: eop.viewId, - class: "PanelUI-subView cui-widget-panelview", - flex: "1", - style: "min-width:30em", - }); - aDoc.getElementById("appMenu-viewCache").appendChild(view); - aDoc.defaultView.extensionOptionsPanel.panelview = view; - - if (eop.config["Show header"]) { - let header = view.appendChild( - eop.create(aDoc, "vbox", { id: "eom-mainView-panel-header" }) - ); - let heading = header.appendChild(eop.create(aDoc, "label")); - let label = heading.appendChild( - eop.create(aDoc, "html:span", { - id: "eom-mainView-panel-header-span", - role: "heading", - "aria-level": "1", - }) - ); - label.textContent = l10n["Panel title"]; - view.appendChild(aDoc.createXULElement("toolbarseparator")); - } - - view.appendChild( - eop.create(aDoc, "vbox", { - id: view.id + "-body", - class: "panel-subview-body", - }) - ); - - // create the theme preview tooltip - aDoc.getElementById("mainPopupSet").appendChild( - aDoc.defaultView.MozXULElement.parseXULToFragment( - `` - ) - ); - - eop.fluentSetup(aDoc).then(() => eop.swapAddonsButton(aDoc)); - }, - // populate the panel before it's shown - onViewShowing: (event) => { - if ( - event.originalTarget === - event.target.ownerGlobal.extensionOptionsPanel?.panelview - ) - event.target.ownerGlobal.extensionOptionsPanel.getAddonsAndPopulate(event); - }, - // delete the panel if the widget node is destroyed - onDestroyed: (aDoc) => { - let view = aDoc.getElementById(aDoc.defaultView.extensionOptionsPanel?.viewId); - if (view) { - aDoc.defaultView.CustomizableUI.hidePanelForNode(view); - view.remove(); - } - }, - }); - this.loadStylesheet(); // load the stylesheet - } - - // grab localized strings for the extensions button and disabled/enabled extensions headings - async fluentSetup(aDoc) { - aDoc.ownerGlobal.MozXULElement.insertFTLIfNeeded("toolkit/about/aboutAddons.ftl"); - let [extensions, themes, enabled, disabled, privateHelp] = await aDoc.l10n.formatValues([ - "addon-category-extension", - "addon-category-theme", - "extension-enabled-heading", - "extension-disabled-heading", - "addon-detail-private-browsing-help", - ]); - privateHelp = privateHelp.replace(/\s*\<.*\>$/, ""); - this.aboutAddonsStrings = { extensions, themes, enabled, disabled, privateHelp }; - } - - /** - * this script changes the built-in "Add-ons & themes" button in the app menu - * to open our new panel instead of opening about:addons - * @param {object} aDoc (the document our widget has been created within) - */ - swapAddonsButton(aDoc) { - if (!this.config["Replace addons button"]) return; - let win = aDoc.defaultView; - win.PanelUI._initialized || win.PanelUI.init(shouldSuppressPopupNotifications); - this.setAttributes( - win.PanelUI.mainView.querySelector("#appMenu-extensions-themes-button") || - win.PanelUI.mainView.querySelector("#appMenu-addons-button"), - { - command: 0, - key: 0, - shortcut: 0, - class: "subviewbutton subviewbutton-nav", - oncommand: "PanelUI.showSubView('PanelUI-eom', this);", - closemenu: "none", - } - ); - } - - /** - * grab all addons and populate the panel with them. - * @param {object} e (a ViewShowing event) - */ - async getAddonsAndPopulate(e) { - let extensions = await AddonManager.getAddonsByTypes(["extension"]); - let themes = await AddonManager.getAddonsByTypes(["theme"]); - this.populatePanelBody(e, { extensions, themes }); - } - - /** - * create everything inside the panel - * @param {object} e (a ViewShowing event - its target is the panelview node) - * @param {array} addons (an object containing arrays for different addon - * types e.g. extensions, themes) - */ - populatePanelBody(e, addons) { - let prevState; - let { extensions, themes } = addons; - let view = e?.target || this.panelview; - let win = view.ownerGlobal; - let doc = win.document; - let body = view.querySelector(".panel-subview-body"); - let l10n = this.config.l10n; - let enabledFirst = this.config["Show enabled extensions first"]; - let showVersion = this.config["Show version"]; - let showDisabled = this.config["Show disabled extensions"]; - let blackListArray = this.config["Addon ID blacklist"]; - - // clear all the panel items and subviews before rebuilding them. - while (body.hasChildNodes()) body.removeChild(body.firstChild); - [...this.viewCache(doc).children].forEach((panel) => { - if (panel.id.includes("PanelUI-eom-addon-")) panel.remove(); - }); - let appMenuMultiView = win.PanelMultiView.forNode(PanelUI.multiView); - if (win.PanelMultiView.forNode(view.closest("panelmultiview")) === appMenuMultiView) - [...appMenuMultiView._viewStack.children].forEach((panel) => { - if (panel.id !== view.id && panel.id.includes("PanelUI-eom-addon-")) panel.remove(); - }); + !blackListArray.includes(addon.id) && + (!addon.hidden || this.config["Show hidden extensions"]) && + (!addon.userDisabled || showDisabled) + ) { + // then get built into subviewbuttons and corresponding subviews... + if (showDisabled && enabledFirst && prevState && addon.isActive != prevState) { + body.appendChild(doc.createXULElement("toolbarseparator")); + let disabledSubheader = body.appendChild( + this.create(doc, "h2", { class: "subview-subheader" }, true) + ); + disabledSubheader.textContent = this.aboutAddonsStrings.disabled; + } + prevState = addon.isActive; - // extensions... - let enabledSubheader = body.appendChild( - this.create(doc, "h2", { class: "subview-subheader" }, true) - ); - enabledSubheader.textContent = - this.aboutAddonsStrings[showDisabled ? "enabled" : "extensions"]; - extensions - .sort((a, b) => { - // get sorted by enabled state... - let ka = (enabledFirst ? (a.isActive ? "0" : "1") : "") + a.name.toLowerCase(); - let kb = (enabledFirst ? (b.isActive ? "0" : "1") : "") + b.name.toLowerCase(); - return ka < kb ? -1 : 1; + let subviewbutton = body.appendChild( + this.create(doc, "toolbarbutton", { + label: addon.name + (showVersion ? " " + addon.version : ""), + class: "subviewbutton subviewbutton-iconic subviewbutton-nav eom-addon-button", + oncommand: "extensionOptionsPanel.showSubView(event, this)", + closemenu: "none", + "addon-type": "extension", + "data-extensionid": addon.id, }) - .forEach((addon) => { - // then get excluded if config wills it... - if ( - !blackListArray.includes(addon.id) && - (!addon.hidden || this.config["Show hidden extensions"]) && - (!addon.userDisabled || showDisabled) - ) { - // then get built into subviewbuttons and corresponding subviews... - if (showDisabled && enabledFirst && prevState && addon.isActive != prevState) { - body.appendChild(doc.createXULElement("toolbarseparator")); - let disabledSubheader = body.appendChild( - this.create(doc, "h2", { class: "subview-subheader" }, true) - ); - disabledSubheader.textContent = this.aboutAddonsStrings.disabled; - } - prevState = addon.isActive; - - let subviewbutton = body.appendChild( - this.create(doc, "toolbarbutton", { - label: addon.name + (showVersion ? " " + addon.version : ""), - class: "subviewbutton subviewbutton-iconic subviewbutton-nav eom-addon-button", - oncommand: "extensionOptionsPanel.showSubView(event, this)", - closemenu: "none", - "addon-type": "extension", - "data-extensionid": addon.id, - }) - ); - if (!addon.isActive) subviewbutton.classList.add("disabled"); - // set the icon using CSS variables and list-style-image so that user stylesheets can override the icon URL. - subviewbutton.style.setProperty( - "--extension-icon", - `url(${addon.iconURL || this.config["Icon URL"]})` - ); - subviewbutton._Addon = addon; - - if (this.config["Show addon messages"]) - this.setAddonMessage(doc, subviewbutton, addon); - } - }); + ); + if (!addon.isActive) subviewbutton.classList.add("disabled"); + // set the icon using CSS variables and list-style-image so that user stylesheets can override the icon URL. + subviewbutton.style.setProperty( + "--extension-icon", + `url(${addon.iconURL || this.config["Icon URL"]})` + ); + subviewbutton._Addon = addon; + + if (this.config["Show addon messages"]) this.setAddonMessage(doc, subviewbutton, addon); + } + }); - // themes... - let themesSeparator = body.appendChild(doc.createXULElement("toolbarseparator")); - let themesSubheader = body.appendChild( - this.create(doc, "h2", { class: "subview-subheader" }, true) + // themes... + let themesSeparator = body.appendChild(doc.createXULElement("toolbarseparator")); + let themesSubheader = body.appendChild( + this.create(doc, "h2", { class: "subview-subheader" }, true) + ); + themesSubheader.textContent = this.aboutAddonsStrings.themes; + themes.forEach(addon => { + if ( + !blackListArray.includes(addon.id) && + (!addon.hidden || this.config["Show hidden extensions"]) && + (!addon.userDisabled || showDisabled) + ) { + let subviewbutton = body.appendChild( + this.create(doc, "toolbarbutton", { + label: addon.name + (showVersion ? " " + addon.version : ""), + class: "subviewbutton subviewbutton-iconic eom-addon-button", + closemenu: "none", + "addon-type": "theme", + "data-extensionid": addon.id, + }) ); - themesSubheader.textContent = this.aboutAddonsStrings.themes; - themes.forEach((addon) => { - if ( - !blackListArray.includes(addon.id) && - (!addon.hidden || this.config["Show hidden extensions"]) && - (!addon.userDisabled || showDisabled) - ) { - let subviewbutton = body.appendChild( - this.create(doc, "toolbarbutton", { - label: addon.name + (showVersion ? " " + addon.version : ""), - class: "subviewbutton subviewbutton-iconic eom-addon-button", - closemenu: "none", - "addon-type": "theme", - "data-extensionid": addon.id, - }) - ); - subviewbutton.addEventListener("command", async (e) => { - await addon[addon.userDisabled ? "enable" : "disable"](); - subviewbutton.parentElement - .querySelectorAll(`.eom-addon-button[addon-type="theme"]`) - .forEach((btn) => { - btn.classList[btn._Addon?.isActive ? "remove" : "add"]("disabled"); - this.setAddonMessage(doc, btn, btn._Addon); - }); - }); - if (!addon.isActive) subviewbutton.classList.add("disabled"); - subviewbutton.style.setProperty( - "--extension-icon", - `url(${addon.iconURL || this.config["Icon URL"]})` - ); - subviewbutton._Addon = addon; - - this.setAddonMessage(doc, subviewbutton, addon); - } + subviewbutton.addEventListener("command", async e => { + await addon[addon.userDisabled ? "enable" : "disable"](); + subviewbutton.parentElement + .querySelectorAll(`.eom-addon-button[addon-type="theme"]`) + .forEach(btn => { + btn.classList[btn._Addon?.isActive ? "remove" : "add"]("disabled"); + this.setAddonMessage(doc, btn, btn._Addon); + }); }); - - // if no addons are shown, display a "Download Addons" button that leads to AMO. - let getAddonsButton = body.appendChild( - this.create(doc, "toolbarbutton", { - id: "eom-get-addons-button", - class: "subviewbutton subviewbutton-iconic", - label: l10n["Download addons label"], - image: `data:image/svg+xml;utf8,`, - oncommand: `switchToTabHavingURI(Services.urlFormatter.formatURLPref("extensions.getAddons.link.url"), true, { + if (!addon.isActive) subviewbutton.classList.add("disabled"); + subviewbutton.style.setProperty( + "--extension-icon", + `url(${addon.iconURL || this.config["Icon URL"]})` + ); + subviewbutton._Addon = addon; + + this.setAddonMessage(doc, subviewbutton, addon); + } + }); + + // if no addons are shown, display a "Download Addons" button that leads to AMO. + let getAddonsButton = body.appendChild( + this.create(doc, "toolbarbutton", { + id: "eom-get-addons-button", + class: "subviewbutton subviewbutton-iconic", + label: l10n["Download addons label"], + image: `data:image/svg+xml;utf8,`, + oncommand: `switchToTabHavingURI(Services.urlFormatter.formatURLPref("extensions.getAddons.link.url"), true, { inBackground: false, triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), });`, - }) - ); - - let hasExtensions = !!body.querySelector(`.eom-addon-button[addon-type="extension"]`); - let hasThemes = !!body.querySelector(`.eom-addon-button[addon-type="theme"]`); - getAddonsButton.hidden = hasExtensions || hasThemes; - if (!hasExtensions) { - enabledSubheader.remove(); - themesSeparator.remove(); - } - if (!hasThemes) { - themesSubheader.remove(); - themesSeparator.remove(); - } + }) + ); - // make a footer button that leads to about:addons - if (view.querySelector("#eom-allAddonsButton")) return; - view.appendChild(doc.createXULElement("toolbarseparator")); - view.appendChild( - this.create(doc, "toolbarbutton", { - label: l10n["Addons page label"], - id: "eom-allAddonsButton", - class: "subviewbutton subviewbutton-iconic panel-subview-footer-button", - image: this.config["Icon URL"], - key: "key_openAddons", - shortcut: win.ShortcutUtils.prettifyShortcut(win.key_openAddons), - oncommand: `BrowserOpenAddonsMgr("addons://list/extension")`, - }) - ); + let hasExtensions = !!body.querySelector(`.eom-addon-button[addon-type="extension"]`); + let hasThemes = !!body.querySelector(`.eom-addon-button[addon-type="theme"]`); + getAddonsButton.hidden = hasExtensions || hasThemes; + if (!hasExtensions) { + enabledSubheader.remove(); + themesSeparator.remove(); } - - /** - * for a given button made for an addon, find out if it has a message (blocked, unverified, etc.) and if so, display it - * @param {object} doc (the document we're localizing) - * @param {object} subviewbutton (an addon button in the panel) - * @param {object} addon (an Addon object) - */ - async setAddonMessage(doc, subviewbutton, addon) { - const l10n = this.config.l10n; - const { name } = addon; - const { STATE_BLOCKED, STATE_SOFTBLOCKED } = Ci.nsIBlocklistService; - const formatString = (type, args) => { - return new Promise((resolve) => { - doc.l10n - .formatMessages([{ id: `details-notification-${type}`, args }]) - .then((msg) => resolve(msg[0].value)); - }); - }; - - let message = null; - if (addon.blocklistState === STATE_BLOCKED) - message = { - label: l10n.addonMessages["Blocked"], - detail: await formatString("blocked", { name }), - type: "error", - }; - else if (this.isDisabledUnsigned(addon)) - message = { - label: l10n.addonMessages["Signature Required"], - detail: await formatString("unsigned-and-disabled", { name }), - type: "error", - }; - else if ( - !addon.isCompatible && - (AddonManager.checkCompatibility || addon.blocklistState !== STATE_SOFTBLOCKED) - ) - message = { - label: l10n.addonMessages["Incompatible"], - detail: await formatString("incompatible", { - name, - version: Services.appinfo.version, - }), - type: "warning", - }; - else if (!this.isCorrectlySigned(addon)) - message = { - label: l10n.addonMessages["Unverified"], - detail: await formatString("unsigned", { name }), - type: "warning", - }; - else if (addon.blocklistState === STATE_SOFTBLOCKED) - message = { - label: l10n.addonMessages["Insecure"], - detail: await formatString("softblocked", { name }), - type: "warning", - }; - if (addon.type === "theme" && (!message || message.type !== "error")) { - message = message ?? {}; - message.detail = ""; - message.tooltip = "eom-theme-preview-tooltip"; - message.preview = this.getScreenshotUrlForAddon(addon); - if (addon.isActive) { - message.label = null; - message.checked = true; - } - } - if (subviewbutton._addonMessage) { - subviewbutton.removeAttribute("message-type"); - subviewbutton.removeAttribute("tooltiptext"); - subviewbutton.removeAttribute("tooltip"); - subviewbutton.removeAttribute("enable-checked"); - subviewbutton.querySelector(".eom-message-label")?.remove(); - delete subviewbutton._addonMessage; - } - if (message) { - subviewbutton.setAttribute("message-type", message?.type); - subviewbutton.setAttribute("tooltiptext", message?.detail); - if (message.tooltip) subviewbutton.setAttribute("tooltip", message.tooltip); - if (message.checked) subviewbutton.setAttribute("enable-checked", true); - if (message.label) - subviewbutton.appendChild( - this.create(document, "h", { - class: "toolbarbutton-text eom-message-label", - }) - ).textContent = `(${message.label})`; - } - subviewbutton._addonMessage = message; - } - - /** - * show the subview for a given extension - * @param {object} event (a triggering command/click event) - * @param {object} anchor (the subviewbutton that was clicked — dictates the title of the subview) - */ - showSubView(event, anchor) { - if (!("_Addon" in anchor)) return; - this.buildSubView(anchor, anchor._Addon); - event.target.ownerGlobal.PanelUI?.showSubView( - "PanelUI-eom-addon-" + this.makeWidgetId(anchor._Addon.id), - anchor, - event - ); + if (!hasThemes) { + themesSubheader.remove(); + themesSeparator.remove(); } - /** - * for a given addon, build a panel subview - * @param {object} subviewbutton (the button you click to enter the subview, corresponding to the addon) - * @param {object} addon (an addon object provided by the AddonManager, with all the data we need) - */ - buildSubView(subviewbutton, addon) { - let l10n = this.config.l10n; - let win = subviewbutton.ownerGlobal; - let doc = win.document; - let view = this.viewCache(doc).appendChild( - this.create(doc, "panelview", { - id: "PanelUI-eom-addon-" + this.makeWidgetId(addon.id), // turn the extension ID into a DOM node ID - flex: "1", - class: "PanelUI-subView cui-widget-panelview", - }) - ); - - // create options button - let optionsButton = view.appendChild( - this.create(doc, "toolbarbutton", { - label: l10n["Addon options label"], - class: "subviewbutton", - }) - ); - optionsButton.addEventListener("command", (e) => this.openAddonOptions(addon, win)); - - // manage button, when no options page exists - let manageButton = view.appendChild( - this.create(doc, "toolbarbutton", { - label: l10n["Manage addon label"], - class: "subviewbutton", - }) - ); - manageButton.addEventListener("command", (e) => - win.BrowserOpenAddonsMgr("addons://detail/" + encodeURIComponent(addon.id)) - ); - - // disable button - let disableButton = view.appendChild( - this.create(doc, "toolbarbutton", { - label: addon.userDisabled - ? l10n["Enable addon label"] - : l10n["Disable addon label"], - class: "subviewbutton", - closemenu: "none", - }) - ); - disableButton.addEventListener("command", async (e) => { - if (addon.userDisabled) { - await addon.enable(); - disableButton.setAttribute("label", l10n["Disable addon label"]); - } else { - await addon.disable(); - disableButton.setAttribute("label", l10n["Enable addon label"]); - } - this.getAddonsAndPopulate(); - }); - - // uninstall button, and so on... - let uninstallButton = view.appendChild( - this.create(doc, "toolbarbutton", { - label: l10n["Uninstall addon label"], - class: "subviewbutton", - }) - ); - uninstallButton.addEventListener("command", (e) => { - if (win.Services.prompt.confirm(null, null, `Delete ${addon.name} permanently?`)) - addon.pendingOperations & win.AddonManager.PENDING_UNINSTALL - ? addon.cancelUninstall() - : addon.uninstall(); - }); + // make a footer button that leads to about:addons + if (view.querySelector("#eom-allAddonsButton")) return; + view.appendChild(doc.createXULElement("toolbarseparator")); + view.appendChild( + this.create(doc, "toolbarbutton", { + label: l10n["Addons page label"], + id: "eom-allAddonsButton", + class: "subviewbutton subviewbutton-iconic panel-subview-footer-button", + image: this.config["Icon URL"], + key: "key_openAddons", + shortcut: win.ShortcutUtils.prettifyShortcut(win.key_openAddons), + oncommand: `BrowserOpenAddonsMgr("addons://list/extension")`, + }) + ); + } + + /** + * for a given button made for an addon, find out if it has a message + * (blocked, unverified, etc.) and if so, display it + * @param {object} doc (the document we're localizing) + * @param {object} subviewbutton (an addon button in the panel) + * @param {object} addon (an Addon object) + */ + async setAddonMessage(doc, subviewbutton, addon) { + const l10n = this.config.l10n; + const { name } = addon; + const { STATE_BLOCKED, STATE_SOFTBLOCKED } = Ci.nsIBlocklistService; + const formatString = (type, args) => { + return new Promise(resolve => { + doc.l10n + .formatMessages([{ id: `details-notification-${type}`, args }]) + .then(msg => resolve(msg[0].value)); + }); + }; - // allow automatic updates radio group - let updates = view.appendChild( - this.create(doc, "hbox", { - id: "eom-allow-auto-updates", - class: "subviewbutton eom-radio-hbox", - align: "center", - }) - ); - let updatesLabel = updates.appendChild( - this.create(doc, "label", { - id: "eom-allow-auto-updates-label", - class: "toolbarbutton-text eom-radio-label", - flex: 1, - wrap: true, - }) - ); - updatesLabel.textContent = l10n["Automatic updates label"]; - let updatesGroup = updates.appendChild( - this.create(doc, "radiogroup", { - id: "eom-allow-auto-updates-group", - class: "eom-radio-group", - value: addon.applyBackgroundUpdates, - closemenu: "none", - orient: "horizontal", - "aria-labelledby": "eom-allow-auto-updates-label", - }) - ); - updatesGroup.addEventListener( - "command", - (e) => (addon.applyBackgroundUpdates = e.target.value) - ); - updatesGroup.appendChild( - this.create(doc, "radio", { - label: l10n.autoUpdate["Default label"], - class: "subviewradio", - value: 1, - }) - ); - updatesGroup.appendChild( - this.create(doc, "radio", { - label: l10n.autoUpdate["On label"], - class: "subviewradio", - value: 2, - }) - ); - updatesGroup.appendChild( - this.create(doc, "radio", { - label: l10n.autoUpdate["Off label"], - class: "subviewradio", - value: 0, - }) - ); + let message = null; + if (addon.blocklistState === STATE_BLOCKED) + message = { + label: l10n.addonMessages["Blocked"], + detail: await formatString("blocked", { name }), + type: "error", + }; + else if (this.isDisabledUnsigned(addon)) + message = { + label: l10n.addonMessages["Signature Required"], + detail: await formatString("unsigned-and-disabled", { name }), + type: "error", + }; + else if ( + !addon.isCompatible && + (AddonManager.checkCompatibility || addon.blocklistState !== STATE_SOFTBLOCKED) + ) + message = { + label: l10n.addonMessages["Incompatible"], + detail: await formatString("incompatible", { + name, + version: Services.appinfo.version, + }), + type: "warning", + }; + else if (!this.isCorrectlySigned(addon)) + message = { + label: l10n.addonMessages["Unverified"], + detail: await formatString("unsigned", { name }), + type: "warning", + }; + else if (addon.blocklistState === STATE_SOFTBLOCKED) + message = { + label: l10n.addonMessages["Insecure"], + detail: await formatString("softblocked", { name }), + type: "warning", + }; + if (addon.type === "theme" && (!message || message.type !== "error")) { + message = message ?? {}; + message.detail = ""; + message.tooltip = "eom-theme-preview-tooltip"; + message.preview = this.getScreenshotUrlForAddon(addon); + if (addon.isActive) { + message.label = null; + message.checked = true; + } + } + if (subviewbutton._addonMessage) { + subviewbutton.removeAttribute("message-type"); + subviewbutton.removeAttribute("tooltiptext"); + subviewbutton.removeAttribute("tooltip"); + subviewbutton.removeAttribute("enable-checked"); + subviewbutton.querySelector(".eom-message-label")?.remove(); + delete subviewbutton._addonMessage; + } + if (message) { + subviewbutton.setAttribute("message-type", message?.type); + subviewbutton.setAttribute("tooltiptext", message?.detail); + if (message.tooltip) subviewbutton.setAttribute("tooltip", message.tooltip); + if (message.checked) subviewbutton.setAttribute("enable-checked", true); + if (message.label) + subviewbutton.appendChild( + this.create(document, "h", { + class: "toolbarbutton-text eom-message-label", + }) + ).textContent = `(${message.label})`; + } + subviewbutton._addonMessage = message; + } + + /** + * show the subview for a given extension + * @param {object} event (a triggering command/click event) + * @param {object} anchor (the subviewbutton that was clicked — + * dictates the title of the subview) + */ + showSubView(event, anchor) { + if (!("_Addon" in anchor)) return; + this.buildSubView(anchor, anchor._Addon); + event.target.ownerGlobal.PanelUI?.showSubView( + "PanelUI-eom-addon-" + this.makeWidgetId(anchor._Addon.id), + anchor, + event + ); + } + + /** + * for a given addon, build a panel subview + * @param {object} subviewbutton (the button you click to enter the subview, + * corresponding to the addon) + * @param {object} addon (an addon object provided by the AddonManager, + * with all the data we need) + */ + buildSubView(subviewbutton, addon) { + let l10n = this.config.l10n; + let win = subviewbutton.ownerGlobal; + let doc = win.document; + let view = this.viewCache(doc).appendChild( + this.create(doc, "panelview", { + id: "PanelUI-eom-addon-" + this.makeWidgetId(addon.id), // turn the extension ID into a DOM node ID + flex: "1", + class: "PanelUI-subView cui-widget-panelview", + }) + ); - // run in private windows radio group - let setPrivateState = async (addon, node) => { - let perms = await this.ExtensionPermissions.get(addon.id); - let isAllowed = perms.permissions.includes("internal:privateBrowsingAllowed"); - node.permState = isAllowed; - node.value = isAllowed ? 1 : 0; - }; - let privateWindows = view.appendChild( - this.create(doc, "hbox", { - id: "eom-run-in-private", - class: "subviewbutton eom-radio-hbox", - align: "center", - }) - ); - let privateLabel = privateWindows.appendChild( - this.create(doc, "label", { - id: "eom-run-in-private-label", - class: "toolbarbutton-text eom-radio-label", - flex: 1, - wrap: true, - tooltiptext: this.aboutAddonsStrings.privateHelp, - }) - ); - privateLabel.textContent = l10n["Run in private windows label"]; - let privateGroup = privateWindows.appendChild( - this.create(doc, "radiogroup", { - id: "eom-run-in-private-group", - class: "eom-radio-group", - closemenu: "none", - orient: "horizontal", - "aria-labelledby": "eom-run-in-private-label", - }) - ); - privateGroup.addEventListener("command", async () => { - let extension = this.extensionForAddonId(addon.id); - await this.ExtensionPermissions[privateGroup.permState ? "remove" : "add"]( - addon.id, - { - permissions: ["internal:privateBrowsingAllowed"], - origins: [], - }, - extension - ); - setPrivateState(addon, privateGroup); - }); - privateGroup.appendChild( - this.create(doc, "radio", { - label: l10n.runInPrivate["Allow label"], - class: "subviewradio", - value: 1, - }) - ); - privateGroup.appendChild( - this.create(doc, "radio", { - label: l10n.runInPrivate["Don't allow label"], - class: "subviewradio", - value: 0, - }) - ); - setPrivateState(addon, privateGroup); + // create options button + let optionsButton = view.appendChild( + this.create(doc, "toolbarbutton", { + label: l10n["Addon options label"], + class: "subviewbutton", + }) + ); + optionsButton.addEventListener("command", e => this.openAddonOptions(addon, win)); + + // manage button, when no options page exists + let manageButton = view.appendChild( + this.create(doc, "toolbarbutton", { + label: l10n["Manage addon label"], + class: "subviewbutton", + }) + ); + manageButton.addEventListener("command", e => + win.BrowserOpenAddonsMgr("addons://detail/" + encodeURIComponent(addon.id)) + ); - // manage shortcuts - let shortcutsButton = view.appendChild( - this.create(doc, "toolbarbutton", { - label: l10n["Manage shortcuts label"], - class: "subviewbutton", - }) - ); - shortcutsButton.addEventListener("command", () => - win.BrowserOpenAddonsMgr("addons://shortcuts/shortcuts") - ); + // disable button + let disableButton = view.appendChild( + this.create(doc, "toolbarbutton", { + label: addon.userDisabled ? l10n["Enable addon label"] : l10n["Disable addon label"], + class: "subviewbutton", + closemenu: "none", + }) + ); + disableButton.addEventListener("command", async e => { + if (addon.userDisabled) { + await addon.enable(); + disableButton.setAttribute("label", l10n["Disable addon label"]); + } else { + await addon.disable(); + disableButton.setAttribute("label", l10n["Enable addon label"]); + } + this.getAddonsAndPopulate(); + }); + + // uninstall button, and so on... + let uninstallButton = view.appendChild( + this.create(doc, "toolbarbutton", { + label: l10n["Uninstall addon label"], + class: "subviewbutton", + }) + ); + uninstallButton.addEventListener("command", e => { + if (win.Services.prompt.confirm(null, null, `Delete ${addon.name} permanently?`)) + addon.pendingOperations & win.AddonManager.PENDING_UNINSTALL + ? addon.cancelUninstall() + : addon.uninstall(); + }); + + // allow automatic updates radio group + let updates = view.appendChild( + this.create(doc, "hbox", { + id: "eom-allow-auto-updates", + class: "subviewbutton eom-radio-hbox", + align: "center", + }) + ); + let updatesLabel = updates.appendChild( + this.create(doc, "label", { + id: "eom-allow-auto-updates-label", + class: "toolbarbutton-text eom-radio-label", + flex: 1, + wrap: true, + }) + ); + updatesLabel.textContent = l10n["Automatic updates label"]; + let updatesGroup = updates.appendChild( + this.create(doc, "radiogroup", { + id: "eom-allow-auto-updates-group", + class: "eom-radio-group", + value: addon.applyBackgroundUpdates, + closemenu: "none", + orient: "horizontal", + "aria-labelledby": "eom-allow-auto-updates-label", + }) + ); + updatesGroup.addEventListener("command", e => (addon.applyBackgroundUpdates = e.target.value)); + updatesGroup.appendChild( + this.create(doc, "radio", { + label: l10n.autoUpdate["Default label"], + class: "subviewradio", + value: 1, + }) + ); + updatesGroup.appendChild( + this.create(doc, "radio", { + label: l10n.autoUpdate["On label"], + class: "subviewradio", + value: 2, + }) + ); + updatesGroup.appendChild( + this.create(doc, "radio", { + label: l10n.autoUpdate["Off label"], + class: "subviewradio", + value: 0, + }) + ); - let viewSrcButton = view.appendChild( - this.create(doc, "toolbarbutton", { - label: l10n["View source label"], - class: "subviewbutton", - }) - ); - viewSrcButton.addEventListener("command", () => this.openArchive(addon)); + // run in private windows radio group + let setPrivateState = async (addon, node) => { + let perms = await this.ExtensionPermissions.get(addon.id); + let isAllowed = perms.permissions.includes("internal:privateBrowsingAllowed"); + node.permState = isAllowed; + node.value = isAllowed ? 1 : 0; + }; + let privateWindows = view.appendChild( + this.create(doc, "hbox", { + id: "eom-run-in-private", + class: "subviewbutton eom-radio-hbox", + align: "center", + }) + ); + let privateLabel = privateWindows.appendChild( + this.create(doc, "label", { + id: "eom-run-in-private-label", + class: "toolbarbutton-text eom-radio-label", + flex: 1, + wrap: true, + tooltiptext: this.aboutAddonsStrings.privateHelp, + }) + ); + privateLabel.textContent = l10n["Run in private windows label"]; + let privateGroup = privateWindows.appendChild( + this.create(doc, "radiogroup", { + id: "eom-run-in-private-group", + class: "eom-radio-group", + closemenu: "none", + orient: "horizontal", + "aria-labelledby": "eom-run-in-private-label", + }) + ); + privateGroup.addEventListener("command", async () => { + let extension = this.extensionForAddonId(addon.id); + await this.ExtensionPermissions[privateGroup.permState ? "remove" : "add"]( + addon.id, + { + permissions: ["internal:privateBrowsingAllowed"], + origins: [], + }, + extension + ); + setPrivateState(addon, privateGroup); + }); + privateGroup.appendChild( + this.create(doc, "radio", { + label: l10n.runInPrivate["Allow label"], + class: "subviewradio", + value: 1, + }) + ); + privateGroup.appendChild( + this.create(doc, "radio", { + label: l10n.runInPrivate["Don't allow label"], + class: "subviewradio", + value: 0, + }) + ); + setPrivateState(addon, privateGroup); + + // manage shortcuts + let shortcutsButton = view.appendChild( + this.create(doc, "toolbarbutton", { + label: l10n["Manage shortcuts label"], + class: "subviewbutton", + }) + ); + shortcutsButton.addEventListener("command", () => + win.BrowserOpenAddonsMgr("addons://shortcuts/shortcuts") + ); - let homePageButton = view.appendChild( - this.create(doc, "toolbarbutton", { - label: l10n["Open homepage label"], - class: "subviewbutton", - }) - ); - homePageButton.addEventListener("command", () => { - win.switchToTabHavingURI(addon.homepageURL || addon.supportURL, true, { - inBackground: false, - triggeringPrincipal: win.Services.scriptSecurityManager.getSystemPrincipal(), - }); - }); + let viewSrcButton = view.appendChild( + this.create(doc, "toolbarbutton", { + label: l10n["View source label"], + class: "subviewbutton", + }) + ); + viewSrcButton.addEventListener("command", () => this.openArchive(addon)); - let copyIdButton = view.appendChild( - this.create(doc, "toolbarbutton", { - label: l10n["Copy ID label"], - class: "subviewbutton panel-subview-footer-button", - closemenu: "none", - }) + let homePageButton = view.appendChild( + this.create(doc, "toolbarbutton", { + label: l10n["Open homepage label"], + class: "subviewbutton", + }) + ); + homePageButton.addEventListener("command", () => { + win.switchToTabHavingURI(addon.homepageURL || addon.supportURL, true, { + inBackground: false, + triggeringPrincipal: win.Services.scriptSecurityManager.getSystemPrincipal(), + }); + }); + + let copyIdButton = view.appendChild( + this.create(doc, "toolbarbutton", { + label: l10n["Copy ID label"], + class: "subviewbutton panel-subview-footer-button", + closemenu: "none", + }) + ); + copyIdButton.addEventListener("command", () => { + win.Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(win.Ci.nsIClipboardHelper) + .copyString(addon.id); + win.CustomHint?.show(copyIdButton, "Copied"); + }); + + view.addEventListener("ViewShowing", () => { + optionsButton.hidden = !addon.optionsURL; + manageButton.hidden = !!addon.optionsURL; + updates.hidden = !(addon.permissions & win.AddonManager.PERM_CAN_UPGRADE); + updatesGroup.setAttribute("value", addon.applyBackgroundUpdates); + privateWindows.hidden = !( + addon.incognito != "not_allowed" && + !!(addon.permissions & win.AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS) + ); + setPrivateState(addon, privateGroup); + shortcutsButton.hidden = !this.extensionForAddonId(addon.id)?.shortcuts?.manifestCommands + ?.size; + disableButton.setAttribute( + "label", + addon.userDisabled ? l10n["Enable addon label"] : l10n["Disable addon label"] + ); + uninstallButton.hidden = viewSrcButton.hidden = + addon.isSystem || addon.isBuiltin || addon.temporarilyInstalled; + homePageButton.hidden = !(addon.homepageURL || addon.supportURL); + }); + } + + /** + * open a given addon's options page + * @param {object} addon (an addon object) + * @param {object} win (the window from which this was invoked) + */ + openAddonOptions(addon, win) { + if (!addon.isActive || !addon.optionsURL) return; + + switch (Number(addon.optionsType)) { + case 5: + win.BrowserOpenAddonsMgr( + "addons://detail/" + win.encodeURIComponent(addon.id) + "/preferences" ); - copyIdButton.addEventListener("command", () => { - win.Cc["@mozilla.org/widget/clipboardhelper;1"] - .getService(win.Ci.nsIClipboardHelper) - .copyString(addon.id); - win.CustomHint?.show(copyIdButton, "Copied"); - }); - - view.addEventListener("ViewShowing", () => { - optionsButton.hidden = !addon.optionsURL; - manageButton.hidden = !!addon.optionsURL; - updates.hidden = !(addon.permissions & win.AddonManager.PERM_CAN_UPGRADE); - updatesGroup.setAttribute("value", addon.applyBackgroundUpdates); - privateWindows.hidden = !( - addon.incognito != "not_allowed" && - !!(addon.permissions & win.AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS) - ); - setPrivateState(addon, privateGroup); - shortcutsButton.hidden = !this.extensionForAddonId(addon.id)?.shortcuts - ?.manifestCommands?.size; - disableButton.setAttribute( - "label", - addon.userDisabled ? l10n["Enable addon label"] : l10n["Disable addon label"] - ); - uninstallButton.hidden = viewSrcButton.hidden = - addon.isSystem || addon.isBuiltin || addon.temporarilyInstalled; - homePageButton.hidden = !(addon.homepageURL || addon.supportURL); - }); - } - - /** - * open a given addon's options page - * @param {object} addon (an addon object) - * @param {object} win (the window from which this was invoked) - */ - openAddonOptions(addon, win) { - if (!addon.isActive || !addon.optionsURL) return; - - switch (Number(addon.optionsType)) { - case 5: - win.BrowserOpenAddonsMgr( - "addons://detail/" + win.encodeURIComponent(addon.id) + "/preferences" - ); - break; - case 3: - win.switchToTabHavingURI(addon.optionsURL, true); - break; - case 1: - let windows = win.Services.wm.getEnumerator(null); - while (windows.hasMoreElements()) { - let win2 = windows.getNext(); - if (win2.closed) continue; - if (win2.document.documentURI == addon.optionsURL) { - win2.focus(); - return; - } - } - let features = "chrome,titlebar,toolbar,centerscreen"; - if (win.Services.prefs.getBoolPref("browser.preferences.instantApply")) - features += ",dialog=no"; - win.openDialog(addon.optionsURL, addon.id, features); - } - } - - /** - * open a given addon's source xpi file in the user's associated program, e.g. 7-zip - * @param {object} addon (an addon object) - */ - openArchive(addon) { - let dir = Services.dirsvc.get("ProfD", Ci.nsIFile); - dir.append("extensions"); - dir.append(addon.id + ".xpi"); - dir.launch(); - } - - onTooltipShowing(e) { - let anchor = e.target.triggerNode - ? e.target.triggerNode.closest(".eom-addon-button") - : null; - let message = anchor._addonMessage; - let img = e.target.querySelector("#eom-theme-preview-canvas"); - img.src = message?.preview || ""; - if (!anchor || !message?.preview) { - e.preventDefault(); + break; + case 3: + win.switchToTabHavingURI(addon.optionsURL, true); + break; + case 1: + let windows = win.Services.wm.getEnumerator(null); + while (windows.hasMoreElements()) { + let win2 = windows.getNext(); + if (win2.closed) continue; + if (win2.document.documentURI == addon.optionsURL) { + win2.focus(); return; + } } + let features = "chrome,titlebar,toolbar,centerscreen"; + if (win.Services.prefs.getBoolPref("browser.preferences.instantApply")) + features += ",dialog=no"; + win.openDialog(addon.optionsURL, addon.id, features); } - - // generate and load a stylesheet - loadStylesheet() { - let sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService( - Ci.nsIStyleSheetService - ); - let uri = makeURI( - "data:text/css;charset=UTF=8," + - encodeURIComponent( - /*css*/ - `#eom-button { + } + + /** + * open a given addon's source xpi file in the user's associated program, e.g. 7-zip + * @param {object} addon (an addon object) + */ + openArchive(addon) { + let dir = Services.dirsvc.get("ProfD", Ci.nsIFile); + dir.append("extensions"); + dir.append(addon.id + ".xpi"); + dir.launch(); + } + + onTooltipShowing(e) { + let anchor = e.target.triggerNode ? e.target.triggerNode.closest(".eom-addon-button") : null; + let message = anchor._addonMessage; + let img = e.target.querySelector("#eom-theme-preview-canvas"); + img.src = message?.preview || ""; + if (!anchor || !message?.preview) { + e.preventDefault(); + return; + } + } + + // generate and load a stylesheet + loadStylesheet() { + let sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService); + let uri = makeURI( + "data:text/css;charset=UTF=8," + + encodeURIComponent( + /*css*/ + `#eom-button { list-style-image: url('${this.config["Icon URL"]}'); } #eom-mainView-panel-header { @@ -1035,11 +1043,11 @@ class ExtensionOptionsWidget { border: 1px solid var(--arrowpanel-border-color); background: var(--arrowpanel-border-color); }` - ) - ); - if (sss.sheetRegistered(uri, sss.AUTHOR_SHEET)) return; - sss.loadAndRegisterSheet(uri, sss.AUTHOR_SHEET); - } + ) + ); + if (sss.sheetRegistered(uri, sss.AUTHOR_SHEET)) return; + sss.loadAndRegisterSheet(uri, sss.AUTHOR_SHEET); + } } window.extensionOptionsPanel = new ExtensionOptionsWidget(); diff --git a/JS/extensionStylesheetLoader.uc.js b/JS/extensionStylesheetLoader.uc.js index 61dc2c1c..f6fa751b 100644 --- a/JS/extensionStylesheetLoader.uc.js +++ b/JS/extensionStylesheetLoader.uc.js @@ -3,21 +3,24 @@ // @version 1.0.2 // @author aminomancer // @homepage https://github.com/aminomancer -// @description Allows users to share stylesheets for webextensions without needing to edit the -// URL. This works by creating an actor in every extension browser that sets an attribute on the -// root element to expose the addon's ID to user stylesheets. This means we can use the addon's ID -// instead of @-moz-document url(). That is good because addons' URLs are randomly generated upon -// install, meaning the URLs I specify in resources/in-content/ext-*.css will not be the same as -// yours, so they will not work for you. You can also use this in combination with my -// debugExtensionInToolbarContextMenu.uc.js to add your own style rules for extension content. Once -// you have that script installed, you can right-click an addon's toolbar button > Debug Extension > -// Copy ID. Then, in userContent.css, add a rule like -// :root[uc-extension-id="example@aminomancer"]{color:red} Keep in mind, the ID is not the same as -// the URL. That's why this script is necessary in the first place. URLs are random, unique, and -// per-install. Conversely, an extension's ID is permanent and universal, but potentially not -// unique, in that two authors could potentially make extensions with the same ID. I haven't seen -// this before but it's possible, in principle. If you need to, you can find the ID by navigating to -// about:debugging#/runtime/this-firefox +// @description Allows users to share stylesheets for webextensions without +// needing to edit the URL. This works by creating an actor in every extension +// browser that sets an attribute on the root element to expose the addon's ID +// to user stylesheets. This means we can use the addon's ID instead of +// @-moz-document url(). That is good because addons' URLs are randomly +// generated upon install, meaning the URLs I specify in +// resources/in-content/ext-*.css will not be the same as yours, so they will +// not work for you. You can also use this in combination with my +// debugExtensionInToolbarContextMenu.uc.js to add your own style rules for +// extension content. Once you have that script installed, you can right-click +// an addon's toolbar button > Debug Extension > Copy ID. Then, in +// userContent.css, add a rule like :root[uc-extension-id="example@aminomancer"]{color:red} +// Keep in mind, the ID is not the same as the URL. That's why this script is +// necessary in the first place. URLs are random, unique, and per-install. +// Conversely, an extension's ID is permanent and universal, but potentially not +// unique, in that two authors could potentially make extensions with the same +// ID. I haven't seen this before but it's possible, in principle. If you need +// to, you can find the ID by navigating to about:debugging#/runtime/this-firefox // @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. // @include main // @startup extensionStylesheetLoader @@ -25,92 +28,100 @@ // ==/UserScript== class ExtensionStylesheetLoader { - constructor() { - this.setup(); + constructor() { + this.setup(); + } + async setup() { + // make a temp directory for our child file + const registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + let tempDir = Services.dirsvc.get("UChrm", Ci.nsIFile); + tempDir.append(".ExtensionStylesheetLoader"); + let { path } = tempDir; + await IOUtils.makeDirectory(path, { ignoreExisting: true, createAncestors: false }); + // hide the temp dir on windows so it doesn't get in the way of user + // activities or prevent its eventual deletion. + if (AppConstants.platform === "win") { + if ("setWindowsAttributes" in IOUtils) + await IOUtils.setWindowsAttributes(path, { hidden: true }); } - async setup() { - // make a temp directory for our child file - const registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); - let tempDir = Services.dirsvc.get("UChrm", Ci.nsIFile); - tempDir.append(".ExtensionStylesheetLoader"); - let { path } = tempDir; - await IOUtils.makeDirectory(path, { ignoreExisting: true, createAncestors: false }); - // hide the temp dir on windows so it doesn't get in the way of user activities or prevent its eventual deletion. - if (AppConstants.platform === "win") { - if ("setWindowsAttributes" in IOUtils) - await IOUtils.setWindowsAttributes(path, { hidden: true }); - } - this.tempPath = path; + this.tempPath = path; - // create a manifest file that registers a URI for chrome://uc-extensionstylesheetloader/content/ - this.manifestFile = await this.createTempFile(`content uc-extensionstylesheetloader ./`, { - name: "ucsss", - type: "manifest", - }); - this.childFile = await this.createTempFile( - `"use strict";const EXPORTED_SYMBOLS=["ExtensionStylesheetLoaderChild"];const{WebExtensionPolicy}=Cu.getGlobalForObject(Cu);class ExtensionStylesheetLoaderChild extends JSWindowActorChild{handleEvent(e){let policy=WebExtensionPolicy.getByHostname(this.document.location.hostname);if(policy&&policy.id)this.document.documentElement.setAttribute("uc-extension-id",policy.id)}}`, - { name: "ExtensionStylesheetLoaderChild", type: "jsm" } - ); + // create a manifest file that registers a URI for + // chrome://uc-extensionstylesheetloader/content/ + this.manifestFile = await this.createTempFile(`content uc-extensionstylesheetloader ./`, { + name: "ucsss", + type: "manifest", + }); + this.childFile = await this.createTempFile( + `"use strict";const EXPORTED_SYMBOLS=["ExtensionStylesheetLoaderChild"];const{WebExtensionPolicy}=Cu.getGlobalForObject(Cu);class ExtensionStylesheetLoaderChild extends JSWindowActorChild{handleEvent(e){let policy=WebExtensionPolicy.getByHostname(this.document.location.hostname);if(policy&&policy.id)this.document.documentElement.setAttribute("uc-extension-id",policy.id)}}`, + { name: "ExtensionStylesheetLoaderChild", type: "jsm" } + ); - tempDir.append(this.manifestFile.name); - if (tempDir.exists()) registrar.autoRegister(tempDir); - else return; - ChromeUtils.registerWindowActor("ExtensionStylesheetLoader", { - child: { - moduleURI: this.childFile.url, - events: { DOMDocElementInserted: {} }, - }, - allFrames: true, - matches: ["moz-extension://*/*"], - messageManagerGroups: ["browsers", "webext-browsers", "sidebars"], - }); - // listen for application quit so we can clean up the temp files. - Services.obs.addObserver(this, "quit-application"); + tempDir.append(this.manifestFile.name); + if (tempDir.exists()) registrar.autoRegister(tempDir); + else return; + ChromeUtils.registerWindowActor("ExtensionStylesheetLoader", { + child: { + moduleURI: this.childFile.url, + events: { DOMDocElementInserted: {} }, + }, + allFrames: true, + matches: ["moz-extension://*/*"], + messageManagerGroups: ["browsers", "webext-browsers", "sidebars"], + }); + // listen for application quit so we can clean up the temp files. + Services.obs.addObserver(this, "quit-application"); + } + /** + * create a file in the temp folder + * @param {string} contents (the actual file contents in UTF-8) + * @param {object} options (an optional object containing properties path or + * name. path creates a file at a specific absolute + * path. name creates a file of that name in the + * chrome/.ExtensionStylesheetLoader folder. + * if omitted, it will create + * chrome/.ExtensionStylesheetLoader/uc-temp) + * @returns {object} (an object containing the filename and + * a chrome:// URL leading to the file) + */ + async createTempFile(contents, options = {}) { + let { path = null, name = "uc-temp", type = "txt" } = options; + const uuid = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator) + .generateUUID() + .toString(); + name += "-" + uuid + "." + type; + if (!path) { + let dir = Services.dirsvc.get("UChrm", Ci.nsIFile); + dir.append(".ExtensionStylesheetLoader"); + dir.append(name); + path = dir.path; } - /** - * create a file in the temp folder - * @param {string} contents (the actual file contents in UTF-8) - * @param {object} options (an optional object containing properties path or name. path creates a file at a specific absolute path. name creates a file of that name in the chrome/.ExtensionStylesheetLoader folder. if omitted, it will create chrome/.ExtensionStylesheetLoader/uc-temp) - * @returns {object} { name, url } (an object containing the filename and a chrome:// URL leading to the file) - */ - async createTempFile(contents, options = {}) { - let { path = null, name = "uc-temp", type = "txt" } = options; - const uuid = Cc["@mozilla.org/uuid-generator;1"] - .getService(Ci.nsIUUIDGenerator) - .generateUUID() - .toString(); - name += "-" + uuid + "." + type; - if (!path) { - let dir = Services.dirsvc.get("UChrm", Ci.nsIFile); - dir.append(".ExtensionStylesheetLoader"); - dir.append(name); - path = dir.path; - } - await IOUtils.writeUTF8(path, contents); - let url = "chrome://uc-extensionstylesheetloader/content/" + name; - return { name, url }; - } - // application quit listener. clean up the temp files. - observe(subject, topic, data) { - switch (topic) { - case "quit-application": - Services.obs.removeObserver(this, "quit-application"); - this.cleanup(); - break; - default: - } - } - // remove the temp directory when firefox's main process ends - async cleanup() { - await IOUtils.remove(this.tempPath, { - ignoreAbsent: true, - recursive: true, - }); + await IOUtils.writeUTF8(path, contents); + let url = "chrome://uc-extensionstylesheetloader/content/" + name; + return { name, url }; + } + // application quit listener. clean up the temp files. + observe(subject, topic, data) { + switch (topic) { + case "quit-application": + Services.obs.removeObserver(this, "quit-application"); + this.cleanup(); + break; + default: } + } + // remove the temp directory when firefox's main process ends + async cleanup() { + await IOUtils.remove(this.tempPath, { + ignoreAbsent: true, + recursive: true, + }); + } } _ucUtils.sharedGlobal.extensionStylesheetLoader = { - _startup: () => {}, + _startup: () => {}, }; if (location.href === AppConstants.BROWSER_CHROME_URL) new ExtensionStylesheetLoader(); diff --git a/JS/eyedropperButton.uc.js b/JS/eyedropperButton.uc.js index 171b5cab..69b03870 100644 --- a/JS/eyedropperButton.uc.js +++ b/JS/eyedropperButton.uc.js @@ -3,7 +3,11 @@ // @version 1.0.1 // @author aminomancer // @homepage https://github.com/aminomancer/uc.css.js -// @description Adds a toolbar button that implements the color picker without launching the devtools. Similar to the menu item in the "More Tools" and "Tools > Browser Tools" menus, only this one can be placed directly on your toolbar. Also adds a customizable hotkey to do the same — by default, it's Ctrl+Shift+Y (or Cmd+Shift+Y on macOS) +// @description Adds a toolbar button that implements the color picker +// without launching the devtools. Similar to the menu item in the "More Tools" +// and "Tools > Browser Tools" menus, only this one can be placed directly on +// your toolbar. Also adds a customizable hotkey to do the same — by default, +// it's Ctrl+Shift+Y (or Cmd+Shift+Y on macOS) // @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. // ==/UserScript== diff --git a/JS/findbarMods.uc.js b/JS/findbarMods.uc.js index 57968cdd..e2245725 100644 --- a/JS/findbarMods.uc.js +++ b/JS/findbarMods.uc.js @@ -1,481 +1,547 @@ -// ==UserScript== -// @name Findbar Mods -// @version 1.3.0 -// @author aminomancer -// @homepage https://github.com/aminomancer -// @description 1) Make a custom context menu for the findbar that lets you permanently configure findbar-related settings. You can set "Highlight All" and "Whole Words" just like you can with the built-in checkboxes, but this also lets you choose any setting for "Match Case" and "Match Diacritics." The built-in checkboxes for these settings only let you choose between states 1 and 0, true and false. There's actually a 2 state which enables a more useful and intuitive mode. Read the notes in the "l10n" section below for more info. Additionally, most of the built-in checkboxes are only temporary. They only apply to the current browser. This can be useful, but since a context menu requires more intention to reach, its actions should be more permanent. Instead of just setting the browser state, the context menu sets the user preferences just like you could in about:config. 2) Set up a hotkey system that allows you to close the findbar by pressing Escape or Ctrl+F while the findbar is focused. Normally, Ctrl+F only opens the findbar. With this script, Ctrl+F acts more like a toggle. As normal, when the findbar is closed, Ctrl+F will open it. When the findbar is open but not focused, Ctrl+F will focus it and select all text in the input box. From there, pressing Ctrl+F once more will close it. If you're in 'find as you type' mode, ctrl+f switches to regular find mode. 3) (Optional) Miniaturize the findbar matches label and the "Match case" and "Whole words" buttons. Instead of "1 of 500 matches" this one says "1/500" and floats inside the input box. This is enabled by default by the "usingDuskfox" setting below. It's mainly intended for people who use CSS themes that make the findbar much more compact, like my theme duskFox. If you don't use one of these themes already, you can grab the relevant code from uc-findbar.css on my repo, or if you like having a big findbar, you can just set "usingDuskfox" to false below. For those interested in customizing this with CSS, the mini matches indicator can be styled with the selector .matches-indicator. It's the next sibling of the findbar input box. See uc-findbar.css in this repo for how I styled it. Specific methods used are documented in more detail in the code comments below. -// @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. -// ==/UserScript== - -class FindbarMods { - // set this to false if you're not using my theme or something similar to make the findbar much smaller. - // the miniaturized findbar isn't necessary with the default firefox layout, and requires a lot of CSS to implement. - // so setting this to false will disable the miniaturization features of the script, - // and solely implement the context menu and hotkey features. - static usingDuskfox = true; - - // firefox has no localization strings for these phrases, since they can only be configured in about:config. - // change the label and accesskey values for your language. keep the quotes. - static l10n = { - // match case popup submenu - caseInsensitive: { - label: "Case Insensitive", - accesskey: "I", - }, - caseSensitive: { - label: "Case Sensitive", - accesskey: "S", - }, - // ignore case when your search string is all lowercase; - // match case when your search string contains at least one capitalized character. - auto: { - label: "Auto", - accesskey: "A", - }, - // diacritics popup submenu - // e matches e and é, é matches é and e - matchAllDiacritics: { - label: "Match All Diacritics", - accesskey: "A", - }, - // e matches e but not é, é matches é but not e - exclusiveMatch: { - label: "Exclusive Matching", - accesskey: "E", - }, - // e matches e and é, é matches é but not e - smartMatch: { - label: "Smart Matching", - accesskey: "S", - }, - }; - /** - * create a DOM node with given parameters - * @param {object} aDoc (which document to create the element in) - * @param {string} tag (an HTML tag name, like "button" or "p") - * @param {object} props (an object containing attribute name/value pairs, e.g. class: ".bookmark-item") - * @param {boolean} isHTML (if true, create an HTML element. if omitted or false, create a XUL element. generally avoid HTML when modding the UI, most UI elements are actually XUL elements.) - * @returns the created DOM node - */ - create(aDoc, tag, props, isHTML = false) { - let el = isHTML ? aDoc.createElement(tag) : aDoc.createXULElement(tag); - for (let prop in props) el.setAttribute(prop, props[prop]); - return el; - } - constructor() { - this.buildContextMenu(); - // callback to execute for every new findbar created (each loaded tab has its own findbar) - gBrowser.tabContainer.addEventListener("TabFindInitialized", this); - addEventListener("findbaropen", this); - } - handleEvent(e) { - switch (e.type) { - case "TabFindInitialized": - this.onTabFindInitialized(e); - break; - case "findbaropen": - this.onFindbarOpen(e); - break; - case "popupshowing": - this.onPopupShowing(e); - break; - case "popuphiding": - this.onPopupHiding(e); - break; - } - } - // we want to use firefox's built-in localized strings wherever possible - async buildStrings() { - let msgs = await document.l10n.formatMessages([ - "findbar-highlight-all2", - "findbar-entire-word", - "findbar-case-sensitive", - "findbar-match-diacritics", - ]); - let attrs = msgs.map((msg) => { - msg.attributes = msg.attributes.reduce((entries, { name, value }) => { - entries[name] = value; - return entries; - }, {}); - return msg.attributes; - }); - let [highlight, entireWord, caseSense, diacritics] = attrs; - return { - highlight, - entireWord, - caseSense, - diacritics, - }; - } - async buildContextMenu() { - let l10n = FindbarMods.l10n; - // ensure the .ftl file is loaded; this will almost always execute before firefox's own findbar code does. - MozXULElement.insertFTLIfNeeded("toolkit/main-window/findbar.ftl"); - this.fluentStrings = await this.buildStrings(); - this.contextMenu = document.getElementById("mainPopupSet").appendChild( - this.create(document, "menupopup", { - id: "findbar-context-menu", - }) - ); - this.contextMenu.addEventListener("popupshowing", this); - this.contextMenu.addEventListener("popuphiding", this); - - this.contextMenu._menuitemHighlightAll = this.contextMenu.appendChild( - this.create(document, "menuitem", { - id: "findbar-menu-highlight-all", - type: "checkbox", - label: this.fluentStrings.highlight.label, - accesskey: this.fluentStrings.highlight.accesskey, - oncommand: `let node = this.parentElement.triggerNode; - if (!node) return; - let findbar = node.tagName === "findbar" ? node : node.closest("findbar"); - if (findbar) findbar.toggleHighlight(!findbar._highlightAll);`, - }) - ); - this.contextMenu._menuitemEntireWord = this.contextMenu.appendChild( - this.create(document, "menuitem", { - id: "findbar-menu-entire-word", - type: "checkbox", - label: this.fluentStrings.entireWord.label, - accesskey: this.fluentStrings.entireWord.accesskey, - oncommand: `let node = this.parentElement.triggerNode; - if (!node) return; - let findbar = node.tagName === "findbar" ? node : node.closest("findbar"); - if (findbar) findbar.toggleEntireWord(!findbar.browser.finder._entireWord);`, - }) - ); - - this.contextMenu._menuMatchCase = this.contextMenu.appendChild( - this.create(document, "menu", { - id: "findbar-menu-match-case", - label: this.fluentStrings.caseSense.label, - accesskey: this.fluentStrings.caseSense.accesskey, - }) - ); - let matchCasePopup = this.contextMenu._menuMatchCase.appendChild( - document.createXULElement("menupopup") - ); - matchCasePopup.addEventListener("popupshowing", this); - this.contextMenu._menuMatchCasePopup = matchCasePopup; - - // we make these options permanent by using the preferences service instead of MozFindbar's methods. - this.contextMenu._menuitemCaseInsensitive = matchCasePopup.appendChild( - this.create(document, "menuitem", { - id: "findbar-menu-case-insensitive", - type: "radio", - label: l10n.caseInsensitive.label, - accesskey: l10n.caseInsensitive.accesskey, - oncommand: `Services.prefs.setIntPref("accessibility.typeaheadfind.casesensitive", 0);`, - }) - ); - this.contextMenu._menuitemCaseSensitive = matchCasePopup.appendChild( - this.create(document, "menuitem", { - id: "findbar-menu-case-sensitive", - type: "radio", - label: l10n.caseSensitive.label, - accesskey: l10n.caseSensitive.accesskey, - oncommand: `Services.prefs.setIntPref("accessibility.typeaheadfind.casesensitive", 1);`, - }) - ); - this.contextMenu._menuitemCaseAuto = matchCasePopup.appendChild( - this.create(document, "menuitem", { - id: "findbar-menu-case-auto", - type: "radio", - label: l10n.auto.label, - accesskey: l10n.auto.accesskey, - oncommand: `Services.prefs.setIntPref("accessibility.typeaheadfind.casesensitive", 2);`, - }) - ); - - this.contextMenu._menuMatchDiacritics = this.contextMenu.appendChild( - this.create(document, "menu", { - id: "findbar-menu-match-diacritics", - label: this.fluentStrings.diacritics.label, - accesskey: this.fluentStrings.diacritics.accesskey, - }) - ); - let diacriticsPopup = this.contextMenu._menuMatchDiacritics.appendChild( - document.createXULElement("menupopup") - ); - diacriticsPopup.addEventListener("popupshowing", this); - this.contextMenu._menuMatchDiacriticsPopup = diacriticsPopup; - - this.contextMenu._menuitemMatchAllDiacritics = diacriticsPopup.appendChild( - this.create(document, "menuitem", { - id: "findbar-menu-match-all-diacritics", - type: "radio", - label: l10n.matchAllDiacritics.label, - accesskey: l10n.matchAllDiacritics.accesskey, - oncommand: `Services.prefs.setIntPref("findbar.matchdiacritics", 0);`, - }) - ); - this.contextMenu._menuitemExclusiveMatching = diacriticsPopup.appendChild( - this.create(document, "menuitem", { - id: "findbar-menu-exclusive-matching", - type: "radio", - label: l10n.exclusiveMatch.label, - accesskey: l10n.exclusiveMatch.accesskey, - oncommand: `Services.prefs.setIntPref("findbar.matchdiacritics", 1);`, - }) - ); - this.contextMenu._menuitemSmartMatching = diacriticsPopup.appendChild( - this.create(document, "menuitem", { - id: "findbar-menu-smart-matching", - type: "radio", - label: l10n.smartMatch.label, - accesskey: l10n.smartMatch.accesskey, - oncommand: `Services.prefs.setIntPref("findbar.matchdiacritics", 2);`, - }) - ); - } - modClassMethods() { - let findbarClass = customElements.get("findbar").prototype; - findbarClass.ucFindbarMods = this; - // make sure the new mini buttons are never hidden, - // since the position of the new matches indicator depends on the position of the buttons. - // instead of hiding them while state 2 is applied via pref, - // just disable them so they're grayed out and unclickable. - eval( - `findbarClass._updateCaseSensitivity = function ` + - findbarClass._updateCaseSensitivity - .toSource() - .replace(/_updateCaseSensitivity/, ``) - .replace(/checkbox\.hidden/, `checkbox.disabled`) - ); - eval( - `findbarClass._setEntireWord = function ` + - findbarClass._setEntireWord - .toSource() - .replace(/_setEntireWord/, ``) - .replace(/checkbox\.hidden/, `checkbox.disabled`) - ); - // override the native method that sets some findbar UI properties, e.g. switching between normal and find-as-you-type mode. - findbarClass._updateFindUI = function () { - let showMinimalUI = this.findMode != this.FIND_NORMAL; - let nodes = this.getElement("findbar-container").children; - let wrapper = this.getElement("findbar-textbox-wrapper"); - let foundMatches = this._foundMatches; - let tinyIndicator = this._tinyIndicator; - for (let node of nodes) { - if (node == wrapper || node == foundMatches) continue; - node.hidden = showMinimalUI; - } - this.getElement("find-next").hidden = this.getElement("find-previous").hidden = - showMinimalUI; - foundMatches.hidden = showMinimalUI || !foundMatches.value; - tinyIndicator.style.display = showMinimalUI ? "none" : "inline-block"; - if (showMinimalUI) this._findField.classList.add("minimal"); - else this._findField.classList.remove("minimal"); - this._updateCaseSensitivity(); - this._updateDiacriticMatching(); - this._setEntireWord(); - this._setHighlightAll(); - if (this.findMode == this.FIND_TYPEAHEAD) - this._findField.placeholder = this._fastFindStr; - else if (this.findMode == this.FIND_LINKS) - this._findField.placeholder = this._fastFindLinksStr; - else this._findField.placeholder = this._normalFindStr; - }; - // override the native on-results function so it updates both labels. - findbarClass.onMatchesCountResult = function (result) { - if (result.total !== 0) { - if (result.total == -1) { - this._foundMatches.value = PluralForm.get( - result.limit, - this.strBundle.GetStringFromName("FoundMatchesCountLimit") - ).replace("#1", result.limit); - this._tinyIndicator.textContent = `${result.limit}+`; - } else { - this._foundMatches.value = PluralForm.get( - result.total, - this.strBundle.GetStringFromName("FoundMatches") - ) - .replace("#1", result.current) - .replace("#2", result.total); - this._tinyIndicator.textContent = `${result.current}/${result.total}`; - } - this._foundMatches.hidden = false; - this._tinyIndicator.removeAttribute("empty"); // bring it back if it's not blank. - } else { - this._foundMatches.hidden = true; - this._foundMatches.value = ""; - this._tinyIndicator.textContent = " "; - this._tinyIndicator.setAttribute("empty", "true"); // hide the indicator background with CSS if it's blank. - } - }; - } - // when the context menu opens, ensure the menuitems are checked/unchecked appropriately. - onPopupShowing(e) { - let node = e.target.triggerNode; - if (!node) return; - let findbar = node.tagName === "findbar" ? node : node.closest("findbar"); - if (!findbar) return; - if (e.currentTarget !== this.contextMenu) return this.onSubmenuShowing(e, findbar); - this.contextMenu._menuitemHighlightAll.setAttribute("checked", !!findbar._highlightAll); - this.contextMenu._menuitemEntireWord.setAttribute("checked", !!findbar._entireWord); - if (findbar._quickFindTimeout) { - clearTimeout(findbar._quickFindTimeout); - findbar._quickFindTimeout = null; - findbar._updateBrowserWithState(); - } - } - onPopupHiding(e) { - if (e.target !== this.contextMenu) return; - let node = e.target.triggerNode; - if (!node) return; - let findbar = node.tagName === "findbar" ? node : node.closest("findbar"); - if (!findbar) return; - if (findbar.findMode != findbar.FIND_NORMAL) findbar._setFindCloseTimeout(); - } - // do the same with the submenus, except since they have type="radio" we don't need to uncheck anything. - // checking any of a radio menuitem's siblings will automatically uncheck it, just like a radio input. - onSubmenuShowing(e, findbar) { - if (e.target === this.contextMenu._menuMatchDiacriticsPopup) { - let diacriticsStatus = - Services.prefs.getIntPref("findbar.matchdiacritics", 0) || findbar._matchDiacritics; - let activeItem = this.contextMenu._menuMatchDiacriticsPopup.children[diacriticsStatus]; - activeItem.setAttribute("checked", true); - } - if (e.target === this.contextMenu._menuMatchCasePopup) { - let caseStatus = - Services.prefs.getIntPref("accessibility.typeaheadfind.casesensitive", 0) || - findbar._typeAheadCaseSensitive; - let activeItem = this.contextMenu._menuMatchCasePopup.children[caseStatus]; - activeItem.setAttribute("checked", true); - } - } - domSetup(findbar) { - // ensure that our new context menu is opened on right-click. - findbar.setAttribute("context", "findbar-context-menu"); - // begin moving elements and making the mini matches label. - if (FindbarMods.usingDuskfox) this.miniaturize(findbar); - } - miniaturize(findbar) { - function onKey(e) { - if (this.hasMenu() && this.open) return; - // handle arrow key focus navigation - else { - if ( - e.keyCode == KeyEvent.DOM_VK_UP || - (e.keyCode == KeyEvent.DOM_VK_LEFT && - document.defaultView.getComputedStyle(this.parentNode).direction == - "ltr") || - (e.keyCode == KeyEvent.DOM_VK_RIGHT && - document.defaultView.getComputedStyle(this.parentNode).direction == "rtl") - ) { - e.preventDefault(); - window.document.commandDispatcher.rewindFocus(); - return; - } - if ( - e.keyCode == KeyEvent.DOM_VK_DOWN || - (e.keyCode == KeyEvent.DOM_VK_RIGHT && - document.defaultView.getComputedStyle(this.parentNode).direction == - "ltr") || - (e.keyCode == KeyEvent.DOM_VK_LEFT && - document.defaultView.getComputedStyle(this.parentNode).direction == "rtl") - ) { - e.preventDefault(); - window.document.commandDispatcher.advanceFocus(); - return; - } - } - // handle access keys - if (!e.charCode || e.charCode <= 32 || e.altKey || e.ctrlKey || e.metaKey) return; - const charLower = String.fromCharCode(e.charCode).toLowerCase(); - if (this.accessKey.toLowerCase() == charLower) { - this.click(); - return; - } - // check against accesskeys of siblings and activate them if matched - for (const el of Object.values(this.parentElement.children)) - if (el.accessKey.toLowerCase() === charLower) { - el.focus(); - el.click(); - return; - } - } - // the new mini indicator that will read something like 1/27 instead of 1 of 27 matches. - findbar._tinyIndicator = this.create(document, "label", { - class: "matches-indicator", - style: "box-sizing: border-box; display: inline-block; -moz-box-align: center; margin: 0; line-height: 20px; position: absolute; font-size: 10px; right: 110px; color: hsla(0, 0%, 100%, 0.25); pointer-events: none; padding-inline-start: 20px; mask-image: linear-gradient(to right, transparent 0px, black 20px);", - empty: true, - }); - let caseSensitiveButton = findbar.querySelector(".findbar-case-sensitive"); - let entireWordButton = findbar.querySelector(".findbar-entire-word"); - // my own findbar CSS is pretty complicated. it turns the findbar into a small floating box. in vanilla firefox the findbar is a bar that covers the full width of the window and flexes the browser out of the way. mine hovers over the content area without pushing anything out of its way. I also hide a few of the less frequently used buttons. - // so my findbar is tiny but since we're adding an indicator, we might as well make the text field bigger to get something in return. the default firefox findbar is really silly, why have such a giant findbar if the text field can't stretch to fill that space? - // there's also some CSS in my stylesheets that gives the findbar a smooth transition and starting animation and compresses the buttons and stuff. the effects of this might look really weird without those rules so I'd definitely either 1) look at uc-findbar.css, or 2) set usingDuskfox to false at the top of this script. - findbar._findField.style.width = "20em"; - // we want the close button to be on the far right end of the findbar. - findbar._findField.parentNode.after(findbar.querySelector(".findbar-closebutton")); - // put it after the input box so we can use the ~ squiggly combinator - findbar._findField.after(findbar._tinyIndicator); - // move the match-case and entire-word buttons into the text field. uc-findbar.css turns these buttons into mini icons, same size as the next/previous buttons. this way we can fit everything important into one row. - findbar._tinyIndicator.after(caseSensitiveButton, entireWordButton); - - // listen for access keys, arrow keys etc. since these buttons are now inside the text area. - caseSensitiveButton.addEventListener("keypress", onKey); - entireWordButton.addEventListener("keypress", onKey); - } - // for a given findbar, move its label into the proper position. - setLabelPosition(findbar) { - let getBounds = window.windowUtils.getBoundsWithoutFlushing; - let distanceFromEdge = - getBounds(findbar).right - getBounds(findbar.querySelector(".findbar-textbox")).right; - findbar._tinyIndicator.style.right = `${distanceFromEdge + 1}px`; - } - // when a new tab is opened and the findbar somehow activated, a new findbar is born. - // so we have to manage it every time. - onTabFindInitialized(e) { - if (e.target.ownerGlobal !== window) return; - if (!this.initialized) { - this.initialized = true; - if (FindbarMods.usingDuskfox) this.modClassMethods(); - } - let findbar = e.target._findBar; - - // determine what to do when the hotkey is pressed - function exitFindBar(e) { - if (e.repeat || e.shiftKey || e.altKey) return; - if (e.code === "KeyF" && (e.ctrlKey || e.metaKey)) { - if (this.hidden) return; // if it's already hidden then let the built-in command open it. - let field = this._findField; - try { - this.findMode > 0 // if we're in 'find as you type' mode... - ? this.open(0) // switch to normal find mode. - : field.contains(document.activeElement) // if we're in normal mode already then check if the input box is focused... - ? field.selectionEnd - field.selectionStart === field.textLength // if already focused, check if all input text is selected. difference between end and start only equals length if every character is within the selection range. - ? this.close() // if there's already a selection, close the findbar. - : (field.select(), field.focus()) // if nothing is selected, select the full contents of the input box. - : (field.select(), field.focus()); // if not focused, focus and select the input box. - } catch (e) { - // i haven't seen an error here but if any of these references don't exist it probably means the built-in findbar object initialized wrong for some reason. - // in which case it's probably not open. it definitely exists though, since this event listener can't exist in the first place unless the findbar object exists. so just try opening it - this.open(0); - } - e.preventDefault(); - } - } - - this.domSetup(findbar); - // set up hotkey ctrl+F to close findbar when it's already open - findbar.addEventListener("keypress", exitFindBar, true); - } - onFindbarOpen(e) { - if (e.target.findMode == e.target.FIND_NORMAL) - setTimeout(() => e.target.ucFindbarMods.setLabelPosition(e.target), 1); - } -} - -// check that startup has finished and gBrowser is initialized before we add an event listener -if (gBrowserInit.delayedStartupFinished) new FindbarMods(); -else { - let delayedListener = (subject, topic) => { - if (topic == "browser-delayed-startup-finished" && subject == window) { - Services.obs.removeObserver(delayedListener, topic); - new FindbarMods(); - } - }; - Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); -} +// ==UserScript== +// @name Findbar Mods +// @version 1.3.1 +// @author aminomancer +// @homepage https://github.com/aminomancer +// @description 1) Make a custom context menu for the findbar that lets you +// permanently configure findbar-related settings. You can set "Highlight All" +// and "Whole Words" just like you can with the built-in checkboxes, but this +// also lets you choose any setting for "Match Case" and "Match Diacritics." The +// built-in checkboxes for these settings only let you choose between states 1 +// and 0, true and false. There's actually a 2 state which enables a more useful +// and intuitive mode. Read the notes in the "l10n" section below for more info. +// Additionally, most of the built-in checkboxes are only temporary. They only +// apply to the current browser. This can be useful, but since a context menu +// requires more intention to reach, its actions should be more permanent. +// Instead of just setting the browser state, the context menu sets the user +// preferences just like you could in about:config. 2) Set up a hotkey system +// that allows you to close the findbar by pressing Escape or Ctrl+F while the +// findbar is focused. Normally, Ctrl+F only opens the findbar. With this +// script, Ctrl+F acts more like a toggle. As normal, when the findbar is +// closed, Ctrl+F will open it. When the findbar is open but not focused, Ctrl+F +// will focus it and select all text in the input box. From there, pressing +// Ctrl+F once more will close it. If you're in 'find as you type' mode, ctrl+f +// switches to regular find mode. 3) (Optional) Miniaturize the findbar matches +// label and the "Match case" and "Whole words" buttons. Instead of "1 of 500 +// matches" this one says "1/500" and floats inside the input box. This is +// enabled by default by the "usingDuskfox" setting below. It's mainly intended +// for people who use CSS themes that make the findbar much more compact, like +// my theme duskFox. If you don't use one of these themes already, you can grab +// the relevant code from uc-findbar.css on my repo, or if you like having a big +// findbar, you can just set "usingDuskfox" to false below. For those interested +// in customizing this with CSS, the mini matches indicator can be styled with +// the selector .matches-indicator. It's the next sibling of the findbar input +// box. See uc-findbar.css in this repo for how I styled it. Specific methods +// used are documented in more detail in the code comments below. +// @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. +// ==/UserScript== + +class FindbarMods { + // set this to false if you're not using my theme or something similar to make + // the findbar much smaller. the miniaturized findbar isn't necessary with the + // default firefox layout, and requires a lot of CSS to implement. so setting + // this to false will disable the miniaturization features of the script, and + // solely implement the context menu and hotkey features. + static usingDuskfox = true; + + // firefox has no localization strings for these phrases, since they can only + // be configured in about:config. change the label and accesskey values for + // your language. keep the quotes. + static l10n = { + // match case popup submenu + caseInsensitive: { + label: "Case Insensitive", + accesskey: "I", + }, + caseSensitive: { + label: "Case Sensitive", + accesskey: "S", + }, + // ignore case when your search string is all lowercase; + // match case when your search string contains at least one capitalized character. + auto: { + label: "Auto", + accesskey: "A", + }, + // diacritics popup submenu + // e matches e and é, é matches é and e + matchAllDiacritics: { + label: "Match All Diacritics", + accesskey: "A", + }, + // e matches e but not é, é matches é but not e + exclusiveMatch: { + label: "Exclusive Matching", + accesskey: "E", + }, + // e matches e and é, é matches é but not e + smartMatch: { + label: "Smart Matching", + accesskey: "S", + }, + }; + /** + * create a DOM node with given parameters + * @param {object} aDoc (which document to create the element in) + * @param {string} tag (an HTML tag name, like "button" or "p") + * @param {object} props (an object containing attribute name/value pairs, + * e.g. class: ".bookmark-item") + * @param {boolean} isHTML (if true, create an HTML element. if omitted or + * false, create a XUL element. generally avoid HTML + * when modding the UI, most UI elements are actually + * XUL elements.) + * @returns the created DOM node + */ + create(aDoc, tag, props, isHTML = false) { + let el = isHTML ? aDoc.createElement(tag) : aDoc.createXULElement(tag); + for (let prop in props) el.setAttribute(prop, props[prop]); + return el; + } + constructor() { + this.buildContextMenu(); + // callback to execute for every new findbar created + // (each loaded tab has its own findbar) + gBrowser.tabContainer.addEventListener("TabFindInitialized", this); + addEventListener("findbaropen", this); + } + handleEvent(e) { + switch (e.type) { + case "TabFindInitialized": + this.onTabFindInitialized(e); + break; + case "findbaropen": + this.onFindbarOpen(e); + break; + case "popupshowing": + this.onPopupShowing(e); + break; + case "popuphiding": + this.onPopupHiding(e); + break; + } + } + // we want to use firefox's built-in localized strings wherever possible + async buildStrings() { + let msgs = await document.l10n.formatMessages([ + "findbar-highlight-all2", + "findbar-entire-word", + "findbar-case-sensitive", + "findbar-match-diacritics", + ]); + let attrs = msgs.map(msg => { + msg.attributes = msg.attributes.reduce((entries, { name, value }) => { + entries[name] = value; + return entries; + }, {}); + return msg.attributes; + }); + let [highlight, entireWord, caseSense, diacritics] = attrs; + return { + highlight, + entireWord, + caseSense, + diacritics, + }; + } + async buildContextMenu() { + let l10n = FindbarMods.l10n; + // ensure the .ftl file is loaded; this will almost always execute + // before firefox's own findbar code does. + MozXULElement.insertFTLIfNeeded("toolkit/main-window/findbar.ftl"); + this.fluentStrings = await this.buildStrings(); + this.contextMenu = document.getElementById("mainPopupSet").appendChild( + this.create(document, "menupopup", { + id: "findbar-context-menu", + }) + ); + this.contextMenu.addEventListener("popupshowing", this); + this.contextMenu.addEventListener("popuphiding", this); + + this.contextMenu._menuitemHighlightAll = this.contextMenu.appendChild( + this.create(document, "menuitem", { + id: "findbar-menu-highlight-all", + type: "checkbox", + label: this.fluentStrings.highlight.label, + accesskey: this.fluentStrings.highlight.accesskey, + oncommand: `let node = this.parentElement.triggerNode; + if (!node) return; + let findbar = node.tagName === "findbar" ? node : node.closest("findbar"); + if (findbar) findbar.toggleHighlight(!findbar._highlightAll);`, + }) + ); + this.contextMenu._menuitemEntireWord = this.contextMenu.appendChild( + this.create(document, "menuitem", { + id: "findbar-menu-entire-word", + type: "checkbox", + label: this.fluentStrings.entireWord.label, + accesskey: this.fluentStrings.entireWord.accesskey, + oncommand: `let node = this.parentElement.triggerNode; + if (!node) return; + let findbar = node.tagName === "findbar" ? node : node.closest("findbar"); + if (findbar) findbar.toggleEntireWord(!findbar.browser.finder._entireWord);`, + }) + ); + + this.contextMenu._menuMatchCase = this.contextMenu.appendChild( + this.create(document, "menu", { + id: "findbar-menu-match-case", + label: this.fluentStrings.caseSense.label, + accesskey: this.fluentStrings.caseSense.accesskey, + }) + ); + let matchCasePopup = this.contextMenu._menuMatchCase.appendChild( + document.createXULElement("menupopup") + ); + matchCasePopup.addEventListener("popupshowing", this); + this.contextMenu._menuMatchCasePopup = matchCasePopup; + + // we make these options permanent by using the preferences service + // instead of MozFindbar's methods. + this.contextMenu._menuitemCaseInsensitive = matchCasePopup.appendChild( + this.create(document, "menuitem", { + id: "findbar-menu-case-insensitive", + type: "radio", + label: l10n.caseInsensitive.label, + accesskey: l10n.caseInsensitive.accesskey, + oncommand: `Services.prefs.setIntPref("accessibility.typeaheadfind.casesensitive", 0);`, + }) + ); + this.contextMenu._menuitemCaseSensitive = matchCasePopup.appendChild( + this.create(document, "menuitem", { + id: "findbar-menu-case-sensitive", + type: "radio", + label: l10n.caseSensitive.label, + accesskey: l10n.caseSensitive.accesskey, + oncommand: `Services.prefs.setIntPref("accessibility.typeaheadfind.casesensitive", 1);`, + }) + ); + this.contextMenu._menuitemCaseAuto = matchCasePopup.appendChild( + this.create(document, "menuitem", { + id: "findbar-menu-case-auto", + type: "radio", + label: l10n.auto.label, + accesskey: l10n.auto.accesskey, + oncommand: `Services.prefs.setIntPref("accessibility.typeaheadfind.casesensitive", 2);`, + }) + ); + + this.contextMenu._menuMatchDiacritics = this.contextMenu.appendChild( + this.create(document, "menu", { + id: "findbar-menu-match-diacritics", + label: this.fluentStrings.diacritics.label, + accesskey: this.fluentStrings.diacritics.accesskey, + }) + ); + let diacriticsPopup = this.contextMenu._menuMatchDiacritics.appendChild( + document.createXULElement("menupopup") + ); + diacriticsPopup.addEventListener("popupshowing", this); + this.contextMenu._menuMatchDiacriticsPopup = diacriticsPopup; + + this.contextMenu._menuitemMatchAllDiacritics = diacriticsPopup.appendChild( + this.create(document, "menuitem", { + id: "findbar-menu-match-all-diacritics", + type: "radio", + label: l10n.matchAllDiacritics.label, + accesskey: l10n.matchAllDiacritics.accesskey, + oncommand: `Services.prefs.setIntPref("findbar.matchdiacritics", 0);`, + }) + ); + this.contextMenu._menuitemExclusiveMatching = diacriticsPopup.appendChild( + this.create(document, "menuitem", { + id: "findbar-menu-exclusive-matching", + type: "radio", + label: l10n.exclusiveMatch.label, + accesskey: l10n.exclusiveMatch.accesskey, + oncommand: `Services.prefs.setIntPref("findbar.matchdiacritics", 1);`, + }) + ); + this.contextMenu._menuitemSmartMatching = diacriticsPopup.appendChild( + this.create(document, "menuitem", { + id: "findbar-menu-smart-matching", + type: "radio", + label: l10n.smartMatch.label, + accesskey: l10n.smartMatch.accesskey, + oncommand: `Services.prefs.setIntPref("findbar.matchdiacritics", 2);`, + }) + ); + } + modClassMethods() { + let findbarClass = customElements.get("findbar").prototype; + findbarClass.ucFindbarMods = this; + // make sure the new mini buttons are never hidden, since the position of + // the new matches indicator depends on the position of the buttons. instead + // of hiding them while state 2 is applied via pref, just disable them so + // they're grayed out and unclickable. + eval( + `findbarClass._updateCaseSensitivity = function ` + + findbarClass._updateCaseSensitivity + .toSource() + .replace(/_updateCaseSensitivity/, ``) + .replace(/checkbox\.hidden/, `checkbox.disabled`) + ); + eval( + `findbarClass._setEntireWord = function ` + + findbarClass._setEntireWord + .toSource() + .replace(/_setEntireWord/, ``) + .replace(/checkbox\.hidden/, `checkbox.disabled`) + ); + // override the native method that sets some findbar UI properties, + // e.g. switching between normal and find-as-you-type mode. + findbarClass._updateFindUI = function () { + let showMinimalUI = this.findMode != this.FIND_NORMAL; + let nodes = this.getElement("findbar-container").children; + let wrapper = this.getElement("findbar-textbox-wrapper"); + let foundMatches = this._foundMatches; + let tinyIndicator = this._tinyIndicator; + for (let node of nodes) { + if (node == wrapper || node == foundMatches) continue; + node.hidden = showMinimalUI; + } + this.getElement("find-next").hidden = this.getElement("find-previous").hidden = showMinimalUI; + foundMatches.hidden = showMinimalUI || !foundMatches.value; + tinyIndicator.style.display = showMinimalUI ? "none" : "inline-block"; + if (showMinimalUI) this._findField.classList.add("minimal"); + else this._findField.classList.remove("minimal"); + this._updateCaseSensitivity(); + this._updateDiacriticMatching(); + this._setEntireWord(); + this._setHighlightAll(); + if (this.findMode == this.FIND_TYPEAHEAD) this._findField.placeholder = this._fastFindStr; + else if (this.findMode == this.FIND_LINKS) + this._findField.placeholder = this._fastFindLinksStr; + else this._findField.placeholder = this._normalFindStr; + }; + // override the native on-results function so it updates both labels. + findbarClass.onMatchesCountResult = function (result) { + if (result.total !== 0) { + if (result.total == -1) { + this._foundMatches.value = PluralForm.get( + result.limit, + this.strBundle.GetStringFromName("FoundMatchesCountLimit") + ).replace("#1", result.limit); + this._tinyIndicator.textContent = `${result.limit}+`; + } else { + this._foundMatches.value = PluralForm.get( + result.total, + this.strBundle.GetStringFromName("FoundMatches") + ) + .replace("#1", result.current) + .replace("#2", result.total); + this._tinyIndicator.textContent = `${result.current}/${result.total}`; + } + this._foundMatches.hidden = false; + // bring it back if it's not blank. + this._tinyIndicator.removeAttribute("empty"); + } else { + this._foundMatches.hidden = true; + this._foundMatches.value = ""; + this._tinyIndicator.textContent = " "; + // hide the indicator background with CSS if it's blank. + this._tinyIndicator.setAttribute("empty", "true"); + } + }; + } + // when the context menu opens, ensure the menuitems are checked/unchecked appropriately. + onPopupShowing(e) { + let node = e.target.triggerNode; + if (!node) return; + let findbar = node.tagName === "findbar" ? node : node.closest("findbar"); + if (!findbar) return; + if (e.currentTarget !== this.contextMenu) return this.onSubmenuShowing(e, findbar); + this.contextMenu._menuitemHighlightAll.setAttribute("checked", !!findbar._highlightAll); + this.contextMenu._menuitemEntireWord.setAttribute("checked", !!findbar._entireWord); + if (findbar._quickFindTimeout) { + clearTimeout(findbar._quickFindTimeout); + findbar._quickFindTimeout = null; + findbar._updateBrowserWithState(); + } + } + onPopupHiding(e) { + if (e.target !== this.contextMenu) return; + let node = e.target.triggerNode; + if (!node) return; + let findbar = node.tagName === "findbar" ? node : node.closest("findbar"); + if (!findbar) return; + if (findbar.findMode != findbar.FIND_NORMAL) findbar._setFindCloseTimeout(); + } + // do the same with the submenus, except since they have type="radio" we don't + // need to uncheck anything. checking any of a radio menuitem's siblings will + // automatically uncheck it, just like a radio input. + onSubmenuShowing(e, findbar) { + if (e.target === this.contextMenu._menuMatchDiacriticsPopup) { + let diacriticsStatus = + Services.prefs.getIntPref("findbar.matchdiacritics", 0) || findbar._matchDiacritics; + let activeItem = this.contextMenu._menuMatchDiacriticsPopup.children[diacriticsStatus]; + activeItem.setAttribute("checked", true); + } + if (e.target === this.contextMenu._menuMatchCasePopup) { + let caseStatus = + Services.prefs.getIntPref("accessibility.typeaheadfind.casesensitive", 0) || + findbar._typeAheadCaseSensitive; + let activeItem = this.contextMenu._menuMatchCasePopup.children[caseStatus]; + activeItem.setAttribute("checked", true); + } + } + domSetup(findbar) { + // ensure that our new context menu is opened on right-click. + findbar.setAttribute("context", "findbar-context-menu"); + // begin moving elements and making the mini matches label. + if (FindbarMods.usingDuskfox) this.miniaturize(findbar); + } + miniaturize(findbar) { + function onKey(e) { + if (this.hasMenu() && this.open) return; + // handle arrow key focus navigation + else { + if ( + e.keyCode == KeyEvent.DOM_VK_UP || + (e.keyCode == KeyEvent.DOM_VK_LEFT && + document.defaultView.getComputedStyle(this.parentNode).direction == "ltr") || + (e.keyCode == KeyEvent.DOM_VK_RIGHT && + document.defaultView.getComputedStyle(this.parentNode).direction == "rtl") + ) { + e.preventDefault(); + window.document.commandDispatcher.rewindFocus(); + return; + } + if ( + e.keyCode == KeyEvent.DOM_VK_DOWN || + (e.keyCode == KeyEvent.DOM_VK_RIGHT && + document.defaultView.getComputedStyle(this.parentNode).direction == "ltr") || + (e.keyCode == KeyEvent.DOM_VK_LEFT && + document.defaultView.getComputedStyle(this.parentNode).direction == "rtl") + ) { + e.preventDefault(); + window.document.commandDispatcher.advanceFocus(); + return; + } + } + // handle access keys + if (!e.charCode || e.charCode <= 32 || e.altKey || e.ctrlKey || e.metaKey) return; + const charLower = String.fromCharCode(e.charCode).toLowerCase(); + if (this.accessKey.toLowerCase() == charLower) { + this.click(); + return; + } + // check against accesskeys of siblings and activate them if matched + for (const el of Object.values(this.parentElement.children)) + if (el.accessKey.toLowerCase() === charLower) { + el.focus(); + el.click(); + return; + } + } + // the new mini indicator that will read something like 1/27 instead of 1 of 27 matches. + findbar._tinyIndicator = this.create(document, "label", { + class: "matches-indicator", + style: + "box-sizing: border-box; display: inline-block; -moz-box-align: center; margin: 0; line-height: 20px; position: absolute; font-size: 10px; right: 110px; color: hsla(0, 0%, 100%, 0.25); pointer-events: none; padding-inline-start: 20px; mask-image: linear-gradient(to right, transparent 0px, black 20px);", + empty: true, + }); + let caseSensitiveButton = findbar.querySelector(".findbar-case-sensitive"); + let entireWordButton = findbar.querySelector(".findbar-entire-word"); + // my own findbar CSS is pretty complicated. it turns the findbar into a + // small floating box. in vanilla firefox the findbar is a bar that covers + // the full width of the window and flexes the browser out of the way. mine + // hovers over the content area without pushing anything out of its way. I + // also hide a few of the less frequently used buttons. so my findbar is + // tiny but since we're adding an indicator, we might as well make the text + // field bigger to get something in return. the default firefox findbar is + // really silly, why have such a giant findbar if the text field can't + // stretch to fill that space? there's also some CSS in my stylesheets that + // gives the findbar a smooth transition and starting animation and + // compresses the buttons and stuff. the effects of this might look really + // weird without those rules so I'd definitely either 1) look at uc-findbar.css, + // or 2) set usingDuskfox to false at the top of this script. + findbar._findField.style.width = "20em"; + // we want the close button to be on the far right end of the findbar. + findbar._findField.parentNode.after(findbar.querySelector(".findbar-closebutton")); + // put it after the input box so we can use the ~ squiggly combinator + findbar._findField.after(findbar._tinyIndicator); + // move the match-case and entire-word buttons into the text field. + // uc-findbar.css turns these buttons into mini icons, same size as the next + // and previous buttons. this way we can fit everything important into one row. + findbar._tinyIndicator.after(caseSensitiveButton, entireWordButton); + + // listen for access keys, arrow keys etc. + // since these buttons are now inside the text area. + caseSensitiveButton.addEventListener("keypress", onKey); + entireWordButton.addEventListener("keypress", onKey); + } + // for a given findbar, move its label into the proper position. + setLabelPosition(findbar) { + let getBounds = window.windowUtils.getBoundsWithoutFlushing; + let distanceFromEdge = + getBounds(findbar).right - getBounds(findbar.querySelector(".findbar-textbox")).right; + findbar._tinyIndicator.style.right = `${distanceFromEdge + 1}px`; + } + // when a new tab is opened and the findbar somehow activated, a new findbar + // is born. so we have to manage it every time. + onTabFindInitialized(e) { + if (e.target.ownerGlobal !== window) return; + if (!this.initialized) { + this.initialized = true; + if (FindbarMods.usingDuskfox) this.modClassMethods(); + } + let findbar = e.target._findBar; + + // determine what to do when the hotkey is pressed + function exitFindBar(e) { + if (e.repeat || e.shiftKey || e.altKey) return; + if (e.code === "KeyF" && (e.ctrlKey || e.metaKey)) { + if (this.hidden) return; // if it's already hidden then let the built-in command open it. + let field = this._findField; + try { + // if we're in 'find as you type' mode... + if (this.findMode > 0) { + // switch to normal find mode. + this.open(0); + } else { + // if the findbar text field isn't focused and fully selected, then + // focus and select it. if it's already focused and selected, then + // close the findbar. + if ( + field.contains(document.activeElement) && + field.selectionEnd - field.selectionStart === field.textLength + ) { + this.close(); + } else { + field.select(); + field.focus(); + } + } + } catch (e) { + // I haven't seen an error here but if any of these references don't + // exist it probably means the built-in findbar object initialized + // wrong for some reason. in which case it's probably not open. it + // definitely exists though, since this event listener can't exist in + // the first place unless the findbar object exists. + this.open(0); + } + e.preventDefault(); + } + } + + this.domSetup(findbar); + // set up hotkey ctrl+F to close findbar when it's already open + findbar.addEventListener("keypress", exitFindBar, true); + } + onFindbarOpen(e) { + if (e.target.findMode == e.target.FIND_NORMAL) + setTimeout(() => e.target.ucFindbarMods.setLabelPosition(e.target), 1); + } +} + +// check that startup has finished and gBrowser is initialized before we add an event listener +if (gBrowserInit.delayedStartupFinished) new FindbarMods(); +else { + let delayedListener = (subject, topic) => { + if (topic == "browser-delayed-startup-finished" && subject == window) { + Services.obs.removeObserver(delayedListener, topic); + new FindbarMods(); + } + }; + Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); +} diff --git a/JS/fixTitlebarTooltips.uc.js b/JS/fixTitlebarTooltips.uc.js index d8f883dd..73fb0b1c 100644 --- a/JS/fixTitlebarTooltips.uc.js +++ b/JS/fixTitlebarTooltips.uc.js @@ -3,7 +3,20 @@ // @version 1.1.1 // @author aminomancer // @homepage https://github.com/aminomancer/uc.css.js -// @description Since bug 1718629 (https://bugzilla.mozilla.org/show_bug.cgi?id=1718629), Firefox has tried to make the titlebar buttons (window controls) function more like native controls. In doing so, it allows the OS to draw tooltips for these buttons. So it prevents itself from showing redundant tooltips. That means we can't style the titlebar buttons' tooltips, they don't obey preferences, they disappear after 5 seconds on Windows, and they don't appear at all in fullscreen mode. Personally I would not be a fan of this change even if I didn't heavily customize Firefox's tooltips, because no matter what, OS tooltips are not going to be consistent with Firefox's tooltips, for reasons I mentioned. But in any case, we can fix this issue with JavaScript. It's caused by the titlebar-btn attribute. But removing that programmatically won't work because it's parsed by a C++ component when the buttons are connected. It's already too late by the time the script is running. So we need to recreate the DOM nodes without this attribute. +// @description Since bug 1718629 (https://bugzilla.mozilla.org/show_bug.cgi?id=1718629), +// Firefox has tried to make the titlebar buttons (window controls) function +// more like native controls. In doing so, it allows the OS to draw tooltips for +// these buttons. So it prevents itself from showing redundant tooltips. That +// means we can't style the titlebar buttons' tooltips, they don't obey +// preferences, they disappear after 5 seconds on Windows, and they don't appear +// at all in fullscreen mode. Personally I would not be a fan of this change +// even if I didn't heavily customize Firefox's tooltips, because no matter +// what, OS tooltips are not going to be consistent with Firefox's tooltips, for +// reasons I mentioned. But in any case, we can fix this issue with JavaScript. +// It's caused by the titlebar-btn attribute. But removing that programmatically +// won't work because it's parsed by a C++ component when the buttons are +// connected. It's already too late by the time the script is running. So we +// need to recreate the DOM nodes without this attribute. // @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. // ==/UserScript== diff --git a/JS/floatingSidebarResizer.uc.js b/JS/floatingSidebarResizer.uc.js index 307303ff..5ff7069c 100644 --- a/JS/floatingSidebarResizer.uc.js +++ b/JS/floatingSidebarResizer.uc.js @@ -3,120 +3,150 @@ // @version 1.3.0 // @author aminomancer // @homepage https://github.com/aminomancer -// @description A floating sidebar that you can still resize. The default sidebar in firefox is nice, it can move from the left side to the right side and you can resize it. But it squeezes the browser content area out of the way. That might be desirable for some people. That way the entire contents of the page are still visible when the sidebar is open. That works well with responsive page layouts but doesn't work well with elements that try to preserve some explicit aspect ratio. It also doesn't look very aesthetic when you open the sidebar and the entire page makes this jarring transformation as everything shifts left or right to make way for the sidebar. So say your browser window is sized precisely to 16:9 dimensions. Maybe you have ocd like me and don't want to see any letterbox when you watch netflix. By default when you open the sidebar, it pushes the whole content area to the side, which changes the content area width:height ratio. So the player setup needs to resize the video element, resulting in a letterbox effect. It's easy enough to make the sidebar "float" over the content though. You can do it with pure css. The major downside is that you lose the ability to resize the sidebar. You'd have to set the width manually. That's because the native implementation of resizing relies on the old-school proprietary -moz-box spec. The space within #browser is finite and the -moz-boxes within fill that space based on some css rules. The resizing is actually handled by the separator, which is a totally independent element. So within #browser you have: content | separator | sidebar. And moving the separator defines how big the sidebar and content area are, but this only works *because* they can't occupy the same space. To make the sidebar float over the content area you need to change its display and position rules, which means the separator no longer packs right next to the sidebar. It's sort of like plucking the sidebar out of the flexbox. The separator moves all the way to the end of the screen and the content area expands to fill that space. So the separator becomes useless and we lose the ability to resize the sidebar. So the main thing this does is add a resizer to the sidebar. It doesn't make the sidebar float by itself. That's what the css files in this repo are for. +// @description A floating sidebar that you can still resize. The default +// sidebar in firefox is nice, it can move from the left side to the right side +// and you can resize it. But it squeezes the browser content area out of the +// way. That might be desirable for some people. That way the entire contents of +// the page are still visible when the sidebar is open. That works well with +// responsive page layouts but doesn't work well with elements that try to +// preserve some explicit aspect ratio. It also doesn't look very aesthetic when +// you open the sidebar and the entire page makes this jarring transformation as +// everything shifts left or right to make way for the sidebar. So say your +// browser window is sized precisely to 16:9 dimensions. Maybe you have ocd like +// me and don't want to see any letterbox when you watch netflix. By default +// when you open the sidebar, it pushes the whole content area to the side, +// which changes the content area width:height ratio. So the player setup needs +// to resize the video element, resulting in a letterbox effect. It's easy +// enough to make the sidebar "float" over the content though. You can do it +// with pure css. The major downside is that you lose the ability to resize the +// sidebar. You'd have to set the width manually. That's because the native +// implementation of resizing relies on the old-school proprietary -moz-box +// spec. The space within #browser is finite and the -moz-boxes within fill that +// space based on some css rules. The resizing is actually handled by the +// separator, which is a totally independent element. So within #browser you +// have: content | separator | sidebar. And moving the separator defines how big +// the sidebar and content area are, but this only works *because* they can't +// occupy the same space. To make the sidebar float over the content area you +// need to change its display and position rules, which means the separator no +// longer packs right next to the sidebar. It's sort of like plucking the +// sidebar out of the flexbox. The separator moves all the way to the end of the +// screen and the content area expands to fill that space. So the separator +// becomes useless and we lose the ability to resize the sidebar. So the main +// thing this does is add a resizer to the sidebar. It doesn't make the sidebar +// float by itself. That's what the css files in this repo are for. // @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. // ==/UserScript== (() => { - function startup() { - // outer box - let box = SidebarUI._box; - let prefsvc = Services.prefs; - // boolean pref: whether sidebar is on the left or right side of the browser content area. - let anchor = "sidebar.position_start"; - // string pref: we set this so we can remember user's sidebar width when rebooting or opening a new window. - let widthPref = "userChrome.floating-sidebar.width"; - // invisible little bar by which to drag the sidebar to resize it - let resizer = document.createElement("div"); - let frame = false; - let startWidth; - let startX; + function startup() { + // outer box + let box = SidebarUI._box; + let prefsvc = Services.prefs; + // boolean pref: whether sidebar is on the left or right side of the browser content area. + let anchor = "sidebar.position_start"; + // string pref: we set this so we can remember user's sidebar width when + // rebooting or opening a new window. + let widthPref = "userChrome.floating-sidebar.width"; + // invisible little bar by which to drag the sidebar to resize it + let resizer = document.createElement("div"); + let frame = false; + let startWidth; + let startX; - function initDrag(e) { - // this is not directly visible since the element has no background - // or content. this just means that while you're clicking+dragging - // the resizer, its width expands to double the size of the sidebar. - resizer.style.width = "200%"; - // we want the resizer to expand massively in both directions. - resizer.style.marginInline = "-100%"; - startX = e.screenX; - startWidth = parseInt(document.defaultView.getComputedStyle(box).width, 10); - document.documentElement.addEventListener("mousemove", doDrag, true); - document.documentElement.addEventListener("mouseup", stopDrag, false); - } - - function doDrag(e) { - // throttling, since mousemove events can fire way faster than even - // a 144Hz monitor can render frames. - if (frame) return; - frame = true; - requestAnimationFrame(() => { - if (SidebarUI._positionStart) - box.style.width = startWidth + e.screenX - startX + "px"; - else box.style.width = startWidth - e.screenX + startX + "px"; - frame = false; - }); - } - - function stopDrag(_e) { - // this is the neutral/idle size. when you're not clicking/dragging, - // it's just a 4px vertical "border" - resizer.style.width = "4px"; - // remove the -100% margin-inline rule we set while dragging. - resizer.style.removeProperty("margin-inline"); - document.documentElement.removeEventListener("mousemove", doDrag, true); - document.documentElement.removeEventListener("mouseup", stopDrag, false); - // now that we've stopped moving the mouse, permanently record the - // sidebar width so we can restore from it later. - prefsvc.setStringPref(widthPref, box.style.width); - } + function initDrag(e) { + // this is not directly visible since the element has no background or + // content. this just means that while you're clicking+dragging the + // resizer, its width expands to double the size of the sidebar. + resizer.style.width = "200%"; + // we want the resizer to expand massively in both directions. + resizer.style.marginInline = "-100%"; + startX = e.screenX; + startWidth = parseInt(document.defaultView.getComputedStyle(box).width, 10); + document.documentElement.addEventListener("mousemove", doDrag, true); + document.documentElement.addEventListener("mouseup", stopDrag, false); + } - function alignObserve(_sub, _top, pref) { - // we want the resizer to go on the left side of the sidebar when - // the sidebar is on the right side of the window, and vice versa. - if (prefsvc.getBoolPref(pref)) resizer.style.right = "0"; - else resizer.style.removeProperty("right"); - } + function doDrag(e) { + // throttling, since mousemove events can fire way faster than even + // a 144Hz monitor can render frames. + if (frame) return; + frame = true; + requestAnimationFrame(() => { + if (SidebarUI._positionStart) box.style.width = startWidth + e.screenX - startX + "px"; + else box.style.width = startWidth - e.screenX + startX + "px"; + frame = false; + }); + } - function exitSideBar(e) { - if (e.code === "Escape") { - if (e.repeat || e.shiftKey || e.altKey || e.ctrlKey || this.hidden) return; - SidebarUI.toggle(); - e.preventDefault(); - } - } + function stopDrag(_e) { + // this is the neutral/idle size. when you're not clicking/dragging, + // it's just a 4px vertical "border" + resizer.style.width = "4px"; + // remove the -100% margin-inline rule we set while dragging. + resizer.style.removeProperty("margin-inline"); + document.documentElement.removeEventListener("mousemove", doDrag, true); + document.documentElement.removeEventListener("mouseup", stopDrag, false); + // now that we've stopped moving the mouse, permanently record the + // sidebar width so we can restore from it later. + prefsvc.setStringPref(widthPref, box.style.width); + } - function domSetup() { - resizer.style.cssText = - "display: inline-block; height: 100%; position: absolute; width: 4px; cursor: ew-resize;"; - box.appendChild(resizer); - box.style.minWidth = "18em"; - box.style.maxWidth = "100%"; - try { - box.style.width = prefsvc.getStringPref(widthPref); - } catch (e) { - box.style.width = "18em"; - } - if (prefsvc.getBoolPref(anchor)) resizer.style.right = "0"; - } + function alignObserve(_sub, _top, pref) { + // we want the resizer to go on the left side of the sidebar when + // the sidebar is on the right side of the window, and vice versa. + if (prefsvc.getBoolPref(pref)) resizer.style.right = "0"; + else resizer.style.removeProperty("right"); + } - function attachListeners() { - resizer.addEventListener("mousedown", initDrag, false); - window.addEventListener("unload", uninit, false); - prefsvc.addObserver(anchor, alignObserve); - box.addEventListener("keypress", exitSideBar, true); - } + function exitSideBar(e) { + if (e.code === "Escape") { + if (e.repeat || e.shiftKey || e.altKey || e.ctrlKey || this.hidden) return; + SidebarUI.toggle(); + e.preventDefault(); + } + } - function uninit() { - window.removeEventListener("unload", uninit, false); - prefsvc.removeObserver(anchor, alignObserve); - } + function domSetup() { + resizer.style.cssText = + "display: inline-block; height: 100%; position: absolute; width: 4px; cursor: ew-resize;"; + box.appendChild(resizer); + box.style.minWidth = "18em"; + box.style.maxWidth = "100%"; + try { + box.style.width = prefsvc.getStringPref(widthPref); + } catch (e) { + box.style.width = "18em"; + } + if (prefsvc.getBoolPref(anchor)) resizer.style.right = "0"; + } - // remove old preference - prefsvc.clearUserPref("userChrome.floating-sidebar.hotkey"); - domSetup(); - attachListeners(); + function attachListeners() { + resizer.addEventListener("mousedown", initDrag, false); + window.addEventListener("unload", uninit, false); + prefsvc.addObserver(anchor, alignObserve); + box.addEventListener("keypress", exitSideBar, true); } - // wait until components are initialized so we can access SidebarUI - if (gBrowserInit.delayedStartupFinished) startup(); - else { - let delayedListener = (subject, topic) => { - if (topic == "browser-delayed-startup-finished" && subject == window) { - Services.obs.removeObserver(delayedListener, topic); - startup(); - } - }; - Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); + function uninit() { + window.removeEventListener("unload", uninit, false); + prefsvc.removeObserver(anchor, alignObserve); } + + // remove old preference + prefsvc.clearUserPref("userChrome.floating-sidebar.hotkey"); + domSetup(); + attachListeners(); + } + + // wait until components are initialized so we can access SidebarUI + if (gBrowserInit.delayedStartupFinished) startup(); + else { + let delayedListener = (subject, topic) => { + if (topic == "browser-delayed-startup-finished" && subject == window) { + Services.obs.removeObserver(delayedListener, topic); + startup(); + } + }; + Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); + } })(); diff --git a/JS/fluentRevealNavbar.uc.js b/JS/fluentRevealNavbar.uc.js index 76bd7af8..066fc965 100644 --- a/JS/fluentRevealNavbar.uc.js +++ b/JS/fluentRevealNavbar.uc.js @@ -1,201 +1,224 @@ -// ==UserScript== -// @name Fluent Reveal Navbar Buttons -// @version 1.2 -// @author aminomancer -// @homepage https://github.com/aminomancer/uc.css.js -// @description Adds a visual effect to navbar buttons similar to the spotlight gradient effect on Windows 10's start menu tiles. When hovering over or near a button, a subtle radial gradient is applied to every button in the vicinity the mouse. This is compatible with Fluent Reveal Tabs. -// @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. -// ==/UserScript== - -(function () { - class FluentRevealEffect { - // user configuration - static options = { - includeBookmarks: true, // if true, show the effect on bookmarks on the toolbar - lightColor: "hsla(224, 100%, 80%, 0.15)", // the color of the gradient. default is sort of a faint baby blue. you may prefer just white, e.g. hsla(0, 0%, 100%, 0.05) - gradientSize: 50, // how wide the radial gradient is. - clickEffect: false, // whether to show an additional light burst when clicking an element. (not recommended) - }; - - // instantiate the handler for a given window - constructor() { - this._options = FluentRevealEffect.options; - this.applyEffect(window); - document.documentElement.setAttribute("fluent-reveal-hover", true); - if (this._options.clickEffect) - document.documentElement.setAttribute("fluent-reveal-click", true); - } - - // get all the toolbar buttons in the navbar, in iterable form - get toolbarButtons() { - let buttons = Array.from(gNavToolbox.querySelectorAll(".toolbarbutton-1")); - if (this._options.includeBookmarks) - buttons = buttons.concat( - Array.from(this.placesToolbarItems.querySelectorAll(".bookmark-item")) - ); - return buttons; - } - - get placesToolbarItems() { - return ( - this._placesToolbarItems || - (this._placesToolbarItems = document.getElementById("PlacesToolbarItems")) - ); - } - - /** - * main event handler. handles all the mouse behavior. - * @param {object} e (event) - */ - handleEvent(e) { - requestAnimationFrame((time) => { - switch (e.type) { - case "scroll": - case "mousemove": - if (this._options.clickEffect && this._options.is_pressed) - this.generateEffectsForAll(e, true); - else this.generateEffectsForAll(e); - break; - - case "mousedown": - this._options.is_pressed = true; - this.generateEffectsForAll(e, true); - break; - - case "mouseup": - this._options.is_pressed = false; - this.generateEffectsForAll(e); - break; - } - }); - } - - /** - * main entry point for applying all the script behavior to an element. - * @param {object} el (a DOM node to apply the effect to) - * @param {object} options (an object containing options similar to the static options at the top of the script) - */ - applyEffect(el, options = this._options) { - let { clickEffect } = options.clickEffect === undefined ? this._options : options; - let { gradientSize } = options.gradientSize === undefined ? this._options : options; - let { lightColor } = options.lightColor === undefined ? this._options : options; - - Object.assign(this._options, { - clickEffect, - lightColor, - gradientSize, - is_pressed: false, - }); - - el.addEventListener("mousemove", this); - el.addEventListener("mouseleave", this); - el.addEventListener("scroll", this, true); - - // only set up the click effect if the option is enabled and the element doesn't already have a click effect. - if (clickEffect) { - el.addEventListener("mousedown", this); - el.addEventListener("mouseup", this); - } - } - - /** - * called individually on each toolbar button. finds the element inside the toolbar button that's supposed to have a background color (they're not all the same, some are just the icons, some are badge stacks, and some widgets have multiple buttons too) and generates a gradient for it, since gradient coordinates need to be relative to the top left corner of the element the gradient is displayed on. - * @param {object} el (a toolbar button node) - * @param {object} e (the event that triggered the painting) - * @param {boolean} click (whether the left mouse button is down) - */ - generateToolbarButtonEffect(el, e, click = false) { - let { gradientSize, lightColor } = this._options; - let isBookmark = el.id === "PlacesChevron" || el.classList.contains("bookmark-item"); - let area = isBookmark - ? el - : el.querySelector(".toolbarbutton-badge-stack") || - el.querySelector(".toolbarbutton-icon"); - let areaStyle = getComputedStyle(area); - if ( - areaStyle.display == "none" || - areaStyle.visibility == "hidden" || - areaStyle.visibility == "collapse" - ) { - if (isBookmark) return this.clearEffect(area); - else area = el.querySelector(".toolbarbutton-text"); - } - - if (el.disabled || areaStyle.pointerEvents == "none") return this.clearEffect(area); - - let x = (e.pageX || MousePosTracker._x) - this.getOffset(area).left - window.scrollX; - let y = (e.pageY || MousePosTracker._y) - this.getOffset(area).top - window.scrollY; - - let cssLightEffect = `radial-gradient(circle ${gradientSize}px at ${x}px ${y}px, ${lightColor}, rgba(255,255,255,0)), radial-gradient(circle ${70}px at ${x}px ${y}px, rgba(255,255,255,0), ${lightColor}, rgba(255,255,255,0), rgba(255,255,255,0))`; - - this.drawEffect(area, x, y, lightColor, gradientSize, click ? cssLightEffect : null); - } - - /** - * iterate over all the toolbar buttons in the navbar, generating a separate gradient for each. - * @param {object} e (the event that invoked this) - * @param {boolean} click (whether the left mouse button is down) - */ - generateEffectsForAll(e, click = false) { - this.toolbarButtons.forEach((button) => - this.generateToolbarButtonEffect(button, e, click) - ); - } - - /** - * used to calculate the x and y coordinates used in drawing the gradient - * @param {object} el (a DOM node) - * @returns {object} (an object containing top and left coordinates) - */ - getOffset(el) { - return { - top: el.getBoundingClientRect().top, - left: el.getBoundingClientRect().left, - }; - } - - /** - * finally draw the specified effect on a given element, that is, give the element an inline background-image property - * @param {object} el (a DOM node) - * @param {integer} x (x coordinate for gradient center) - * @param {integer} y (y coordinate for gradient center) - * @param {string} lightColor (any color value accepted by CSS, e.g. "#FFF", "rgba(125, 125, 125, 0.5)", or "hsla(50, 0%, 100%, 0.2)") - * @param {integer} gradientSize (how many pixels wide the gradient should be) - * @param {string} cssLightEffect (technically, any background-image value accepted by CSS, but should be a radial-gradient() function, surrounded by quotes) - */ - drawEffect(el, x, y, lightColor, gradientSize, cssLightEffect = null) { - let lightBg; - - if (cssLightEffect === null) - lightBg = `radial-gradient(circle ${gradientSize}px at ${x}px ${y}px, ${lightColor}, rgba(255,255,255,0))`; - else lightBg = cssLightEffect; - - el.style.backgroundImage = lightBg; - } - - /** - * invoked when the script tries to paint a disabled or otherwise unclickable button. (e.g. in the toolbar customization menu) - * @param {object} el (a DOM node) - */ - clearEffect(el) { - this._options.is_pressed = false; - el.style.removeProperty("background-image"); - } - } - - function init() { - window.fluentRevealNavbar = new FluentRevealEffect(); // instantiate the class on a global property to share the methods with other scripts if desired. - } - - // wait for the chrome window to finish starting up, since we need to reference gNavToolbox as soon as any mouse events are detected - if (gBrowserInit.delayedStartupFinished) init(); - else { - let delayedListener = (subject, topic) => { - if (topic == "browser-delayed-startup-finished" && subject == window) { - Services.obs.removeObserver(delayedListener, topic); - init(); - } - }; - Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); - } -})(); +// ==UserScript== +// @name Fluent Reveal Navbar Buttons +// @version 1.2 +// @author aminomancer +// @homepage https://github.com/aminomancer/uc.css.js +// @description Adds a visual effect to navbar buttons similar to the +// spotlight gradient effect on Windows 10's start menu tiles. When hovering +// over or near a button, a subtle radial gradient is applied to every button in +// the vicinity the mouse. This is compatible with Fluent Reveal Tabs. +// @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. +// ==/UserScript== + +(function () { + class FluentRevealEffect { + // user configuration + static options = { + // if true, show the effect on bookmarks on the toolbar + includeBookmarks: true, + + // the color of the gradient. default is sort of a faint baby blue. you may prefer just white, e.g. hsla(0, 0%, 100%, 0.05) + lightColor: "hsla(224, 100%, 80%, 0.15)", + + // how wide the radial gradient is. + gradientSize: 50, + + // whether to show an additional light burst when clicking an element. (not recommended) + clickEffect: false, + }; + + // instantiate the handler for a given window + constructor() { + this._options = FluentRevealEffect.options; + this.applyEffect(window); + document.documentElement.setAttribute("fluent-reveal-hover", true); + if (this._options.clickEffect) + document.documentElement.setAttribute("fluent-reveal-click", true); + } + + // get all the toolbar buttons in the navbar, in iterable form + get toolbarButtons() { + let buttons = Array.from(gNavToolbox.querySelectorAll(".toolbarbutton-1")); + if (this._options.includeBookmarks) + buttons = buttons.concat( + Array.from(this.placesToolbarItems.querySelectorAll(".bookmark-item")) + ); + return buttons; + } + + get placesToolbarItems() { + return ( + this._placesToolbarItems || + (this._placesToolbarItems = document.getElementById("PlacesToolbarItems")) + ); + } + + /** + * main event handler. handles all the mouse behavior. + * @param {object} e (event) + */ + handleEvent(e) { + requestAnimationFrame(time => { + switch (e.type) { + case "scroll": + case "mousemove": + if (this._options.clickEffect && this._options.is_pressed) + this.generateEffectsForAll(e, true); + else this.generateEffectsForAll(e); + break; + + case "mousedown": + this._options.is_pressed = true; + this.generateEffectsForAll(e, true); + break; + + case "mouseup": + this._options.is_pressed = false; + this.generateEffectsForAll(e); + break; + } + }); + } + + /** + * main entry point for applying all the script behavior to an element. + * @param {object} el (a DOM node to apply the effect to) + * @param {object} options (an object containing options similar to the + * static options at the top of the script) + */ + applyEffect(el, options = this._options) { + let { clickEffect } = options.clickEffect === undefined ? this._options : options; + let { gradientSize } = options.gradientSize === undefined ? this._options : options; + let { lightColor } = options.lightColor === undefined ? this._options : options; + + Object.assign(this._options, { + clickEffect, + lightColor, + gradientSize, + is_pressed: false, + }); + + el.addEventListener("mousemove", this); + el.addEventListener("mouseleave", this); + el.addEventListener("scroll", this, true); + + // only set up the click effect if the option is enabled and the element + // doesn't already have a click effect. + if (clickEffect) { + el.addEventListener("mousedown", this); + el.addEventListener("mouseup", this); + } + } + + /** + * called individually on each toolbar button. finds the element inside the + * toolbar button that's supposed to have a background color (they're not + * all the same, some are just the icons, some are badge stacks, and some + * widgets have multiple buttons too) and generates a gradient for it, since + * gradient coordinates need to be relative to the top left corner of the + * element the gradient is displayed on. + * @param {object} el (a toolbar button node) + * @param {object} e (the event that triggered the painting) + * @param {boolean} click (whether the left mouse button is down) + */ + generateToolbarButtonEffect(el, e, click = false) { + let { gradientSize, lightColor } = this._options; + let isBookmark = el.id === "PlacesChevron" || el.classList.contains("bookmark-item"); + let area = isBookmark + ? el + : el.querySelector(".toolbarbutton-badge-stack") || el.querySelector(".toolbarbutton-icon"); + let areaStyle = getComputedStyle(area); + if ( + areaStyle.display == "none" || + areaStyle.visibility == "hidden" || + areaStyle.visibility == "collapse" + ) { + if (isBookmark) return this.clearEffect(area); + else area = el.querySelector(".toolbarbutton-text"); + } + + if (el.disabled || areaStyle.pointerEvents == "none") return this.clearEffect(area); + + let x = (e.pageX || MousePosTracker._x) - this.getOffset(area).left - window.scrollX; + let y = (e.pageY || MousePosTracker._y) - this.getOffset(area).top - window.scrollY; + + let cssLightEffect = `radial-gradient(circle ${gradientSize}px at ${x}px ${y}px, ${lightColor}, rgba(255,255,255,0)), radial-gradient(circle ${70}px at ${x}px ${y}px, rgba(255,255,255,0), ${lightColor}, rgba(255,255,255,0), rgba(255,255,255,0))`; + + this.drawEffect(area, x, y, lightColor, gradientSize, click ? cssLightEffect : null); + } + + /** + * iterate over all the toolbar buttons in the navbar, generating a separate gradient for each. + * @param {object} e (the event that invoked this) + * @param {boolean} click (whether the left mouse button is down) + */ + generateEffectsForAll(e, click = false) { + this.toolbarButtons.forEach(button => this.generateToolbarButtonEffect(button, e, click)); + } + + /** + * used to calculate the x and y coordinates used in drawing the gradient + * @param {object} el (a DOM node) + * @returns {object} (an object containing top and left coordinates) + */ + getOffset(el) { + return { + top: el.getBoundingClientRect().top, + left: el.getBoundingClientRect().left, + }; + } + + /** + * finally draw the specified effect on a given element, that is, give the + * element an inline background-image property + * @param {object} el (a DOM node) + * @param {integer} x (x coordinate for gradient center) + * @param {integer} y (y coordinate for gradient center) + * @param {string} lightColor (any color value accepted by CSS, e.g. "#FFF", + * "rgba(125, 125, 125, 0.5)", or + * "hsla(50, 0%, 100%, 0.2)") + * @param {integer} gradientSize (how many pixels wide the gradient should be) + * @param {string} cssLightEffect (technically, any background-image value accepted + * by CSS, but should be a radial-gradient() + * function, surrounded by quotes) + */ + drawEffect(el, x, y, lightColor, gradientSize, cssLightEffect = null) { + let lightBg; + + if (cssLightEffect === null) + lightBg = `radial-gradient(circle ${gradientSize}px at ${x}px ${y}px, ${lightColor}, rgba(255,255,255,0))`; + else lightBg = cssLightEffect; + + el.style.backgroundImage = lightBg; + } + + /** + * invoked when the script tries to paint a disabled or otherwise + * unclickable button. (e.g. in the toolbar customization menu) + * @param {object} el (a DOM node) + */ + clearEffect(el) { + this._options.is_pressed = false; + el.style.removeProperty("background-image"); + } + } + + function init() { + // instantiate the class on a global property to share the methods + // with other scripts if desired. + window.fluentRevealNavbar = new FluentRevealEffect(); + } + + // wait for the chrome window to finish starting up, since we need to + // reference gNavToolbox as soon as any mouse events are detected + if (gBrowserInit.delayedStartupFinished) init(); + else { + let delayedListener = (subject, topic) => { + if (topic == "browser-delayed-startup-finished" && subject == window) { + Services.obs.removeObserver(delayedListener, topic); + init(); + } + }; + Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); + } +})(); diff --git a/JS/fluentRevealTabs.uc.js b/JS/fluentRevealTabs.uc.js index 4731fbf3..3093228f 100644 --- a/JS/fluentRevealTabs.uc.js +++ b/JS/fluentRevealTabs.uc.js @@ -1,252 +1,322 @@ -// ==UserScript== -// @name Fluent Reveal Tabs -// @version 1.1 -// @author aminomancer -// @homepage https://github.com/aminomancer/uc.css.js -// @description Adds a visual effect to tabs similar to the spotlight gradient effect on Windows 10's start menu tiles. When hovering a tab, a subtle radial gradient is applied under the mouse. Inspired by the proof of concept here: https://www.reddit.com/r/FirefoxCSS/comments/ng5lnt/proof_of_concept_legacy_edge_like_interaction/ -// @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. -// ==/UserScript== - -(function () { - class FluentRevealEffect { - // user configuration - static options = { - showOnSelectedTab: false, // whether to show the effect if the tab is selected. this doesn't look good with my theme so I set it to false. - showOnPinnedTab: false, // whether to show the effect on pinned tabs. likewise, doesn't look good with my theme but may work with yours. - lightColor: "hsla(224, 100%, 80%, 0.05)", // the color of the gradient. default is sort of a faint baby blue. you may prefer just white, e.g. hsla(0, 0%, 100%, 0.05) - gradientSize: 50, // how wide the radial gradient is. 50px looks best with my theme, but default proton tabs are larger so you may want to try 60 or even 70. - clickEffect: false, // whether to show an additional light burst when clicking a tab. I don't recommend this since it doesn't play nicely with dragging & dropping if you release while your mouse is outside the tab box. I can probably fix this issue but I don't think it's a great fit for tabs anyway. - }; - - /** - * sleep for n ms - * @param {integer} ms (how long to wait) - * @returns a promise resolved after the passed number of milliseconds - */ - static sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - - // instantiate the handler for a given window - constructor() { - this._options = FluentRevealEffect.options; - gBrowser.tabContainer.addEventListener("TabOpen", (e) => - this.applyEffect(e.target.querySelector(".tab-content"), true) - ); - gBrowser.tabs.forEach((tab) => - this.applyEffect(tab.querySelector(".tab-content"), true) - ); - } - - /** - * main event handler. handles all the mouse behavior. - * @param {object} e (event) - */ - handleEvent(e) { - let { gradientSize, lightColor, clickEffect } = e.currentTarget.fluentRevealState; // grab the colors and behavior from the event. this allows us to apply different colors/behavior to different elements and makes the script more adaptable for future expansion or user extension. - let x = e.pageX - this.getOffset(e.currentTarget).left - window.scrollX; // calculate gradient display coordinates based on mouse and element coords. - let y = e.pageY - this.getOffset(e.currentTarget).top - window.scrollY; - let cssLightEffect = `radial-gradient(circle ${gradientSize}px at ${x}px ${y}px, ${lightColor}, rgba(255,255,255,0)), radial-gradient(circle ${70}px at ${x}px ${y}px, rgba(255,255,255,0), ${lightColor}, rgba(255,255,255,0), rgba(255,255,255,0))`; // the effect is actually applied to the element by setting its background-color value to this. - - switch (e.type) { - case "mousemove": - if (this.shouldClear(e.currentTarget)) return this.clearEffect(e.currentTarget); // if the element is a tab, check if it's selected or pinned and check if the user options hide the effect on selected or pinned tabs. determines if we should avoid showing the effect on the element at the current time. - if (clickEffect && e.currentTarget.fluentRevealState.is_pressed) - // mousemove events still trigger while the element is clicked. so if the click effect is enabled and the element is pressed, we want to apply a different effect than we normally would. - this.drawEffect( - e.currentTarget, - x, - y, - lightColor, - gradientSize, - cssLightEffect - ); - else this.drawEffect(e.currentTarget, x, y, lightColor, gradientSize); // normal hover effect. - break; - - case "mouseleave": - this.clearEffect(e.currentTarget); // mouse left the element so remove the background-image property. - break; - - case "mousedown": - if (this.shouldClear(e.currentTarget)) return this.clearEffect(e.currentTarget); // again, check if it's selected or pinned - e.currentTarget.fluentRevealState.is_pressed = true; - this.drawEffect( - e.currentTarget, - x, - y, - lightColor, - gradientSize, - cssLightEffect - ); - break; - - case "mouseup": - if (this.shouldClear(e.currentTarget)) return this.clearEffect(e.currentTarget); - e.currentTarget.fluentRevealState.is_pressed = false; - this.drawEffect(e.currentTarget, x, y, lightColor, gradientSize); - break; - } - } - - /* - Reveal Effect - https://github.com/d2phap/fluent-reveal-effect - - MIT License - Copyright (c) 2018 Duong Dieu Phap - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - - /** - * main entry point for applying all the script behavior to an element. - * @param {object} element (a DOM node to apply the effect to) - * @param {boolean} isTab (pass true if applying to a child of a tab) - * @param {object} options (an object containing options similar to the static options at the top of the script) - */ - applyEffect(element, isTab = false, options = this._options) { - // you may pass an options object when calling this method, but the options object passed does not necessarily contain ALL the properties of the static options object at the top of the script. if you pass just {gradientSize, lightColor} then clickEffect would be undefined rather than true or false. undefined is falsy so it's parsed like false. but if the default (static) clickEffect option was set to true, then it should default to true when you don't pass it, not default to false. so we need to set each of these values equal to 1) the option in the passed options object if it exists, or 2) the option in the static options object. if we just said let {clickEffect, gradientSize, lightColor} = options; then any values not passed in the options object would default to false. instead we're gonna set each one individually. I haven't run into this issue before so please let me know if there's a faster/shorter way of doing this. - let { clickEffect } = options.clickEffect === undefined ? this._options : options; - let { gradientSize } = options.gradientSize === undefined ? this._options : options; - let { lightColor } = options.lightColor === undefined ? this._options : options; - - // cache the values on the element itself. this is how we can support different options for different elements, something the library doesn't support. - element.fluentRevealState = { - clickEffect, - lightColor, - gradientSize, - isTab, - is_pressed: false, - }; - - // make sure we don't add duplicate event listeners if applyEffect() is somehow called more than once on the same element. this shouldn't normally happen since the script itself only ever invokes the method when a tab is created. but if you want to mess around with the script, apply it to additional elements, this is a good safeguard against listeners piling up. - if (!element.getAttribute("fluent-reveal-hover")) { - element.setAttribute("fluent-reveal-hover", true); - element.addEventListener("mousemove", this); - element.addEventListener("mouseleave", this); - } - - // only set up the click effect if the option is enabled and the element doesn't already have a click effect. - if (clickEffect && !element.getAttribute("fluent-reveal-click")) { - element.setAttribute("fluent-reveal-click", true); - element.addEventListener("mousedown", this); - element.addEventListener("mouseup", this); - } - } - - /** - * completely remove the script behavior from a given element. isn't actually used by the script, but it's here if you ever need it for some reason. - * usage: fluentRevealFx.revertElement(gBrowser.selectedTab.querySelector(".tab-content")) - * @param {object} element (a DOM node) - */ - revertElement(element) { - // this isn't really necessary but just for the sake of completeness... - try { - delete element.fluentRevealState; // try to delete the property - } catch (e) { - element.fluentRevealState = null; // if it's undeletable (e.g. the element was sealed) then at least negate it. - } - - if (element.getAttribute("fluent-reveal-hover")) { - element.removeAttribute("fluent-reveal-hover"); - element.removeEventListener("mousemove", this); - element.removeEventListener("mouseleave", this); - } - - if (element.getAttribute("fluent-reveal-click")) { - element.removeAttribute("fluent-reveal-click"); - element.removeEventListener("mousedown", this); - element.removeEventListener("mouseup", this); - } - } - - /** - * invoked when the mouse leaves an element, or when effects would otherwise be applied to a selected/pinned tab if user options prevent it. - * @param {object} element (a DOM node) - */ - clearEffect(element) { - element.fluentRevealState.is_pressed = false; - element.style.removeProperty("background-image"); // the original library memoized the element's computed background-image on applyEffect(), and set the inline style's background-image back to the memoized background-image when clearing the effect. this would work fine if you have total control of the DOM, such as if you were using the library for a website you control. but since we're hacking a browser, we can't be using inline styles willy-nilly. if we left an inline style every time we cleared the effect, it would override firefox's internal CSS rules. it would basically mean the background-image of the element could only ever be defined by the script. that wouldn't be a problem for the script as-is, because we only apply the effect to elements that shouldn't ever have a background-image defined by CSS in the first place. so instead of doing that we just remove the inline background-image property altogether, so the element can go back to displaying whatever background-image CSS tells it to. - } - - /** - * test whether the effect should be removed/forgone on a given element because the element is a selected or pinned tab. - * @param {object} element (a DOM node) - * @returns {boolean} (true if effect should not be shown) - */ - shouldClear(element) { - if (!element.fluentRevealState.isTab) return false; // if it's not a tab then it never needs to be skipped - let tab = element.tab || element.closest("tab"); // the effect isn't actually applied to the tab itself but to .tab-content, so traverse up to the actual tab element which holds properties like selected, pinned. - return ( - (!this._options.showOnSelectedTab && tab.selected) || - (!this._options.showOnPinnedTab && tab.pinned) - ); - } - - /** - * used to calculate the x and y coordinates used in drawing the gradient - * @param {object} element (a DOM node) - * @returns {object} (an object containing top and left coordinates) - */ - getOffset(element) { - return { - top: element.getBoundingClientRect().top, - left: element.getBoundingClientRect().left, - }; - } - - /** - * finally draw the specified effect on a given element, that is, give the element an inline background-image property - * @param {object} element (a DOM node) - * @param {integer} x (x coordinate for gradient center) - * @param {integer} y (y coordinate for gradient center) - * @param {string} lightColor (any color value accepted by CSS, e.g. "#FFF", "rgba(125, 125, 125, 0.5)", or "hsla(50, 0%, 100%, 0.2)") - * @param {integer} gradientSize (how many pixels wide the gradient should be) - * @param {string} cssLightEffect (technically, any background-image value accepted by CSS, but should be a radial-gradient() function, surrounded by quotes) - */ - drawEffect(element, x, y, lightColor, gradientSize, cssLightEffect = null) { - let lightBg; - - if (cssLightEffect === null) - lightBg = `radial-gradient(circle ${gradientSize}px at ${x}px ${y}px, ${lightColor}, rgba(255,255,255,0))`; - else lightBg = cssLightEffect; - - element.style.backgroundImage = lightBg; - } - } - - function init() { - window.fluentRevealFx = new FluentRevealEffect(); // instantiate the class on a global property to share the methods with other scripts if desired. - } - - // wait for the chrome window to finish starting up. we apply the effect to tabs by modifying class methods of gBrowser.tabContainer. those modules must load before we can modify them. when startup finishes it sets delayedStartupFinished to true. so if it's already finished by the time this script executes we can just init() immediately. - if (gBrowserInit.delayedStartupFinished) init(); - else { - // otherwise, we need to hook up an observer so we can wait and be informed when startup finishes. - let delayedListener = (subject, topic) => { - // make sure we're not responding to notifications about other windows, since a different instance of this script executes separately inside each chrome window. - if (topic == "browser-delayed-startup-finished" && subject == window) { - Services.obs.removeObserver(delayedListener, topic); // remove the observer once we're done - init(); // start everything - } - }; - Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); // when the main chrome modules are initialized, the "browser-delayed-startup-finished" notification is sent to observers. so by adding an observer we'll know when this happens and can respond to it. - } -})(); +// ==UserScript== +// @name Fluent Reveal Tabs +// @version 1.1 +// @author aminomancer +// @homepage https://github.com/aminomancer/uc.css.js +// @description Adds a visual effect to tabs similar to the spotlight +// gradient effect on Windows 10's start menu tiles. When hovering a tab, a +// subtle radial gradient is applied under the mouse. Inspired by the proof of +// concept here: https://www.reddit.com/r/FirefoxCSS/comments/ng5lnt/proof_of_concept_legacy_edge_like_interaction/ +// @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. +// ==/UserScript== + +(function () { + class FluentRevealEffect { + // user configuration + static options = { + // whether to show the effect if the tab is selected. this doesn't look + // good with my theme so I set it to false. + showOnSelectedTab: false, + + // whether to show the effect on pinned tabs. likewise, doesn't look good + // with my theme but may work with yours. + showOnPinnedTab: false, + + // the color of the gradient. default is sort of a faint baby blue. + // you may prefer just white, e.g. hsla(0, 0%, 100%, 0.05) + lightColor: "hsla(224, 100%, 80%, 0.05)", + + // how wide the radial gradient is. 50px looks best with my theme, but + // default proton tabs are larger so you may want to try 60 or even 70. + gradientSize: 50, + + // whether to show an additional light burst when clicking a tab. I don't + // recommend this since it doesn't play nicely with dragging & dropping if + // you release while your mouse is outside the tab box. I can probably fix + // this issue but I don't think it's a great fit for tabs anyway. + clickEffect: false, + }; + + /** + * sleep for n ms + * @param {integer} ms (how long to wait) + * @returns a promise resolved after the passed number of milliseconds + */ + static sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + // instantiate the handler for a given window + constructor() { + this._options = FluentRevealEffect.options; + gBrowser.tabContainer.addEventListener("TabOpen", e => + this.applyEffect(e.target.querySelector(".tab-content"), true) + ); + gBrowser.tabs.forEach(tab => this.applyEffect(tab.querySelector(".tab-content"), true)); + } + + /** + * main event handler. handles all the mouse behavior. + * @param {object} e (event) + */ + handleEvent(e) { + // grab the colors and behavior from the event. this allows us to apply + // different colors/behavior to different elements and makes the script + // more adaptable for future expansion or user extension. + let { gradientSize, lightColor, clickEffect } = e.currentTarget.fluentRevealState; + // calculate gradient display coordinates based on mouse and element coords. + let x = e.pageX - this.getOffset(e.currentTarget).left - window.scrollX; + let y = e.pageY - this.getOffset(e.currentTarget).top - window.scrollY; + // the effect is actually applied to the element by setting its + // background-color value to this. + let cssLightEffect = `radial-gradient(circle ${gradientSize}px at ${x}px ${y}px, ${lightColor}, rgba(255,255,255,0)), radial-gradient(circle ${70}px at ${x}px ${y}px, rgba(255,255,255,0), ${lightColor}, rgba(255,255,255,0), rgba(255,255,255,0))`; + + switch (e.type) { + case "mousemove": + // if the element is a tab, check if it's selected or pinned and check + // if the user options hide the effect on selected or pinned tabs. + // determines if we should avoid showing the effect on the element at + // the current time. + if (this.shouldClear(e.currentTarget)) return this.clearEffect(e.currentTarget); + // mousemove events still trigger while the element is clicked. so if + // the click effect is enabled and the element is pressed, we want to + // apply a different effect than we normally would. + this.drawEffect( + e.currentTarget, + x, + y, + lightColor, + gradientSize, + clickEffect && e.currentTarget.fluentRevealState.is_pressed ? cssLightEffect : null + ); + break; + + case "mouseleave": + // mouse left the element so remove the background-image property. + this.clearEffect(e.currentTarget); + break; + + case "mousedown": + // again, check if it's selected or pinned + if (this.shouldClear(e.currentTarget)) return this.clearEffect(e.currentTarget); + e.currentTarget.fluentRevealState.is_pressed = true; + this.drawEffect(e.currentTarget, x, y, lightColor, gradientSize, cssLightEffect); + break; + + case "mouseup": + if (this.shouldClear(e.currentTarget)) return this.clearEffect(e.currentTarget); + e.currentTarget.fluentRevealState.is_pressed = false; + this.drawEffect(e.currentTarget, x, y, lightColor, gradientSize); + break; + } + } + + /** + * Reveal Effect + * https://github.com/d2phap/fluent-reveal-effect + * + * MIT License + * Copyright (c) 2018 Duong Dieu Phap + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + + /** + * main entry point for applying all the script behavior to an element. + * @param {object} element (a DOM node to apply the effect to) + * @param {boolean} isTab (pass true if applying to a child of a tab) + * @param {object} options (an object containing options similar to the + * static options at the top of the script) + */ + applyEffect(element, isTab = false, options = this._options) { + // you may pass an options object when calling this method, but the + // options object passed does not necessarily contain ALL the properties + // of the static options object at the top of the script. if you pass just + // {gradientSize, lightColor} then clickEffect would be undefined rather + // than true or false. undefined is falsy so it's parsed like false. but + // if the default (static) clickEffect option was set to true, then it + // should default to true when you don't pass it, not default to false. so + // we need to set each of these values equal to 1) the option in the + // passed options object if it exists, or 2) the option in the static + // options object. if we just said let {clickEffect, gradientSize, + // lightColor} = options; then any values not passed in the options object + // would default to false. instead we're gonna set each one individually. + // I haven't run into this issue before so please let me know if there's a + // faster/shorter way of doing this. + let { clickEffect } = options.clickEffect === undefined ? this._options : options; + let { gradientSize } = options.gradientSize === undefined ? this._options : options; + let { lightColor } = options.lightColor === undefined ? this._options : options; + + // cache the values on the element itself. this is how we can support different + // options for different elements, something the library doesn't support. + element.fluentRevealState = { + clickEffect, + lightColor, + gradientSize, + isTab, + is_pressed: false, + }; + + // make sure we don't add duplicate event listeners if applyEffect() is + // somehow called more than once on the same element. this shouldn't + // normally happen since the script itself only ever invokes the method + // when a tab is created. but if you want to mess around with the script, + // apply it to additional elements, this is a good safeguard against + // listeners piling up. + if (!element.getAttribute("fluent-reveal-hover")) { + element.setAttribute("fluent-reveal-hover", true); + element.addEventListener("mousemove", this); + element.addEventListener("mouseleave", this); + } + + // only set up the click effect if the option is enabled and the element + // doesn't already have a click effect. + if (clickEffect && !element.getAttribute("fluent-reveal-click")) { + element.setAttribute("fluent-reveal-click", true); + element.addEventListener("mousedown", this); + element.addEventListener("mouseup", this); + } + } + + /** + * completely remove the script behavior from a given element. isn't actually + * used by the script, but it's here if you ever need it for some reason. usage: + * fluentRevealFx.revertElement(gBrowser.selectedTab.querySelector(".tab-content")) + * @param {object} element (a DOM node) + */ + revertElement(element) { + // this isn't really necessary but just for the sake of completeness... + try { + // try to delete the property + delete element.fluentRevealState; + } catch (e) { + // if it's undeletable (e.g. the element was sealed) then at least negate it. + element.fluentRevealState = null; + } + + if (element.getAttribute("fluent-reveal-hover")) { + element.removeAttribute("fluent-reveal-hover"); + element.removeEventListener("mousemove", this); + element.removeEventListener("mouseleave", this); + } + + if (element.getAttribute("fluent-reveal-click")) { + element.removeAttribute("fluent-reveal-click"); + element.removeEventListener("mousedown", this); + element.removeEventListener("mouseup", this); + } + } + + /** + * invoked when the mouse leaves an element, or when effects would otherwise + * be applied to a selected/pinned tab if user options prevent it. + * @param {object} element (a DOM node) + */ + clearEffect(element) { + element.fluentRevealState.is_pressed = false; + // the original library memoized the element's computed background-image on + // applyEffect(), and set the inline style's background-image back to the + // memoized background-image when clearing the effect. this would work fine + // if you have total control of the DOM, such as if you were using the + // library for a website you control. but since we're hacking a browser, we + // can't be using inline styles willy-nilly. if we left an inline style + // every time we cleared the effect, it would override firefox's internal + // CSS rules. it would basically mean the background-image of the element + // could only ever be defined by the script. that wouldn't be a problem for + // the script as-is, because we only apply the effect to elements that + // shouldn't ever have a background-image defined by CSS in the first + // place. so instead of doing that we just remove the inline + // background-image property altogether, so the element can go back to + // displaying whatever background-image CSS tells it to. + element.style.removeProperty("background-image"); + } + + /** + * test whether the effect should be removed/forgone on a given element + * because the element is a selected or pinned tab. + * @param {object} element (a DOM node) + * @returns {boolean} (true if effect should not be shown) + */ + shouldClear(element) { + // if it's not a tab then it never needs to be skipped + if (!element.fluentRevealState.isTab) return false; + // the effect isn't actually applied to the tab itself but to + // .tab-content, so traverse up to the actual tab element which holds + // properties like selected, pinned. + let tab = element.tab || element.closest("tab"); + return ( + (!this._options.showOnSelectedTab && tab.selected) || + (!this._options.showOnPinnedTab && tab.pinned) + ); + } + + /** + * used to calculate the x and y coordinates used in drawing the gradient + * @param {object} element (a DOM node) + * @returns {object} (an object containing top and left coordinates) + */ + getOffset(element) { + return { + top: element.getBoundingClientRect().top, + left: element.getBoundingClientRect().left, + }; + } + + /** + * finally draw the specified effect on a given element, that is, give the + * element an inline background-image property + * @param {object} element (a DOM node) + * @param {integer} x (x coordinate for gradient center) + * @param {integer} y (y coordinate for gradient center) + * @param {string} lightColor (any color value accepted by CSS, e.g. "#FFF", + * "rgba(125, 125, 125, 0.5)", or + * "hsla(50, 0%, 100%, 0.2)") + * @param {integer} gradientSize (how many pixels wide the gradient should be) + * @param {string} cssLightEffect (technically, any background-image value accepted by + * CSS, but should be a radial-gradient() function, + * surrounded by quotes) + */ + drawEffect(element, x, y, lightColor, gradientSize, cssLightEffect = null) { + let lightBg; + + if (cssLightEffect === null) + lightBg = `radial-gradient(circle ${gradientSize}px at ${x}px ${y}px, ${lightColor}, rgba(255,255,255,0))`; + else lightBg = cssLightEffect; + + element.style.backgroundImage = lightBg; + } + } + + function init() { + // instantiate the class on a global property to share the methods with + // other scripts if desired. + window.fluentRevealFx = new FluentRevealEffect(); + } + + if (gBrowserInit.delayedStartupFinished) init(); + else { + let delayedListener = (subject, topic) => { + if (topic == "browser-delayed-startup-finished" && subject == window) { + Services.obs.removeObserver(delayedListener, topic); + init(); + } + }; + Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); + } +})(); diff --git a/JS/fullscreenHotkey.uc.js b/JS/fullscreenHotkey.uc.js index 69447ad8..6d58ce9c 100644 --- a/JS/fullscreenHotkey.uc.js +++ b/JS/fullscreenHotkey.uc.js @@ -1,16 +1,17 @@ -// ==UserScript== -// @name Fullscreen Hotkey -// @version 1.1 -// @author aminomancer -// @homepage https://github.com/aminomancer -// @description All this does is remap the fullscreen shortcut from F11 to Ctrl+E, since I already use F11 for other stuff. -// @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. -// ==/UserScript== - -SessionStore.promiseInitialized.then(() => { - let fullScreenKey = document.getElementById(nodeToShortcutMap["fullscreen-button"]); - fullScreenKey.removeAttribute("keycode"); - fullScreenKey.setAttribute("key", "E"); - fullScreenKey.setAttribute("modifiers", "accel"); - document.getElementById("key_search2").setAttribute("modifiers", "accel,shift"); -}); +// ==UserScript== +// @name Fullscreen Hotkey +// @version 1.1 +// @author aminomancer +// @homepage https://github.com/aminomancer +// @description All this does is remap the fullscreen shortcut from F11 to +// Ctrl+E, since I already use F11 for other stuff. +// @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. +// ==/UserScript== + +SessionStore.promiseInitialized.then(() => { + let fullScreenKey = document.getElementById(nodeToShortcutMap["fullscreen-button"]); + fullScreenKey.removeAttribute("keycode"); + fullScreenKey.setAttribute("key", "E"); + fullScreenKey.setAttribute("modifiers", "accel"); + document.getElementById("key_search2").setAttribute("modifiers", "accel,shift"); +}); diff --git a/JS/hideTrackingProtectionIconOnCustomNewTabPage.uc.js b/JS/hideTrackingProtectionIconOnCustomNewTabPage.uc.js index b533f58e..efc264b8 100644 --- a/JS/hideTrackingProtectionIconOnCustomNewTabPage.uc.js +++ b/JS/hideTrackingProtectionIconOnCustomNewTabPage.uc.js @@ -3,108 +3,139 @@ // @version 1.3.1 // @author aminomancer // @homepage https://github.com/aminomancer -// @description By default, Firefox hides the tracking protection while 1) the current tab is open to the default new tab page; or 2) the user is typing into the url bar. Hiding the icon while the user is typing is unnecessary, since although "pageproxystate" has changed, the content principal is still the same and clicking the tracking protection icon to open the popup still works. Opening the popup while pageproxystate is invalid still loads the tracking details and options for the current content URI. But hiding the icon on the new tab page is necessary, because the tracking protection icon is hidden on about:blank. If you use an extension to set a custom new tab page, you will see the tracking protection icon briefly disappear when opening a new tab, before reappearing as the custom new tab page loads. That is because about:blank loads before the custom new tab page loads. So the icon is hidden and unhidden in the span of a hundred milliseconds or so. This looks very ugly, so my stylesheet has always prevented the tracking protection icon from being hidden on any page, including about:blank. That way at least it doesn't disappear. But this isn't a great solution, because there are a number of pages for which the tracking protection icon does nothing. The protection handler can't handle internal pages, for example. Previously I just disabled pointer events on the icon when it was supposed to be hidden. But I think this script is a better solution. If this script is not installed, my theme will default to those older methods I just mentioned. But if the script is installed, it will restore the built-in behavior of hiding the tracking protection icon on internal pages, only it will also hide the icon on the user's custom new tab page. The icon will still be visible if you're on a valid webpage, (anything but about, chrome, and resource URIs) even if you begin typing in the urlbar. +// @description By default, Firefox hides the tracking protection while 1) +// the current tab is open to the default new tab page; or 2) the user is typing +// into the url bar. Hiding the icon while the user is typing is unnecessary, +// since although "pageproxystate" has changed, the content principal is still +// the same and clicking the tracking protection icon to open the popup still +// works. Opening the popup while pageproxystate is invalid still loads the +// tracking details and options for the current content URI. But hiding the icon +// on the new tab page is necessary, because the tracking protection icon is +// hidden on about:blank. If you use an extension to set a custom new tab page, +// you will see the tracking protection icon briefly disappear when opening a +// new tab, before reappearing as the custom new tab page loads. That is because +// about:blank loads before the custom new tab page loads. So the icon is hidden +// and unhidden in the span of a hundred milliseconds or so. This looks very +// ugly, so my stylesheet has always prevented the tracking protection icon from +// being hidden on any page, including about:blank. That way at least it doesn't +// disappear. But this isn't a great solution, because there are a number of +// pages for which the tracking protection icon does nothing. The protection +// handler can't handle internal pages, for example. Previously I just disabled +// pointer events on the icon when it was supposed to be hidden. But I think +// this script is a better solution. If this script is not installed, my theme +// will default to those older methods I just mentioned. But if the script is +// installed, it will restore the built-in behavior of hiding the tracking +// protection icon on internal pages, only it will also hide the icon on the +// user's custom new tab page. The icon will still be visible if you're on a +// valid webpage, (anything but about, chrome, and resource URIs) even if you +// begin typing in the urlbar. // @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. // ==/UserScript== (function () { - class HideOnNTP { - // duskFox has a preference, userChrome.urlbar.hide-bookmarks-button-on-system-pages - // which hides the urlbar's star button in certain conditions when the tracking protection icon is hidden. - // that means on the new tab page or on about:blank, there is no star button taking up space in the urlbar, - // since these pages are unlikely to ever be bookmarked. however, ctrl + D can still open the edit bookmark panel. - // when this happens, it can't be anchored to the star button since the star button is hidden. - // so it is anchored to the identity icon instead. this looks kinda silly though because it's not meant to be - // anchored so far to the left of the star button. so instead if this pref is enabled, we'll try to - // anchor it to the bookmarks menu button or the library button, if they exist on the user's toolbar. - _getAnchor(panel) { - if (this.hideBookmarks) - for (let id of ["star-button-box", "bookmarks-menu-button", "libary-button"]) { - let node = document.getElementById(id); - if (node && !node.hidden) { - let bounds = window.windowUtils.getBoundsWithoutFlushing(node); - if (bounds.height > 0 && bounds.width > 0) { - // add an attribute to the panel if it's going to be anchored to a toolbar button. - // duskFox CSS uses this to line it up better with a toolbar button. - // normally a panel is positioned -7px vertically from its anchor. - // but a panel anchored to a toolbar button is anchored -12px from its anchor. - // the reasons for this are just related to how the buttons are flexed in their container. - node.classList.contains("toolbarbutton-1") - ? panel.setAttribute("on-toolbar-button", true) - : panel.removeAttribute("on-toolbar-button"); - return node; - } - } - } - panel.removeAttribute("on-toolbar-button"); - return BookmarkingUI.anchor; - } - constructor() { - XPCOMUtils.defineLazyPreferenceGetter( - this, - "hideBookmarks", - "userChrome.urlbar.hide-bookmarks-button-on-system-pages", - false - ); - eval( - `StarUI.showEditBookmarkPopup = async function ` + - StarUI.showEditBookmarkPopup - .toSource() - .replace(/^\(/, "") - .replace(/\)$/, "") - .replace(/async showEditBookmarkPopup/, "") - .replace(/async function\s*/, "") - .replace( - /this\.panel\.openPopup\(BookmarkingUI\.anchor, \"bottomcenter topright\"\)\;/, - `this.panel.openPopup(hideOnNTP?._getAnchor(this.panel) || BookmarkingUI.anchor, \"bottomcenter topright\");` - ) - ); - // the main part of this script. hide the tracking protection icon on new tab page. - gProtectionsHandler.onLocationChange = function onLocationChange() { - let currentURL = gBrowser.currentURI.spec; - let isInitial = isInitialPage(gBrowser.currentURI); - if (this._showToastAfterRefresh) { - this._showToastAfterRefresh = false; - if ( - this._previousURI == currentURL && - this._previousOuterWindowID == gBrowser.selectedBrowser.outerWindowID - ) - this.showProtectionsPopup({ toast: true }); - } - this.hadShieldState = false; - if (currentURL.startsWith("view-source:")) - this._trackingProtectionIconContainer.setAttribute("view-source", true); - else this._trackingProtectionIconContainer.removeAttribute("view-source"); - // make the identity box unfocusable on new tab page - if (gIdentityHandler._identityIconBox) - gIdentityHandler._identityIconBox.disabled = isInitial; - // hide the TP icon on new tab page - if (!ContentBlockingAllowList.canHandle(gBrowser.selectedBrowser) || isInitial) { - this._trackingProtectionIconContainer.hidden = true; - return; - } else this._trackingProtectionIconContainer.hidden = false; - this.hasException = ContentBlockingAllowList.includes(gBrowser.selectedBrowser); - if (this._protectionsPopup) - this._protectionsPopup.toggleAttribute("hasException", this.hasException); - this.iconBox.toggleAttribute("hasException", this.hasException); - this.fingerprintersHistogramAdd("pageLoad"); - this.cryptominersHistogramAdd("pageLoad"); - this.shieldHistogramAdd(0); - }; + class HideOnNTP { + // duskFox has a preference, + // userChrome.urlbar.hide-bookmarks-button-on-system-pages + // which hides the urlbar's star button in certain conditions when the + // tracking protection icon is hidden. that means on the new tab page or on + // about:blank, there is no star button taking up space in the urlbar, since + // these pages are unlikely to ever be bookmarked. however, ctrl + D can + // still open the edit bookmark panel. when this happens, it can't be + // anchored to the star button since the star button is hidden. so it is + // anchored to the identity icon instead. this looks kinda silly though + // because it's not meant to be anchored so far to the left of the star + // button. so instead if this pref is enabled, we'll try to anchor it to the + // bookmarks menu button or the library button, if they exist on the user's + // toolbar. + _getAnchor(panel) { + if (this.hideBookmarks) + for (let id of ["star-button-box", "bookmarks-menu-button", "libary-button"]) { + let node = document.getElementById(id); + if (node && !node.hidden) { + let bounds = window.windowUtils.getBoundsWithoutFlushing(node); + if (bounds.height > 0 && bounds.width > 0) { + // add an attribute to the panel if it's going to be anchored to a + // toolbar button. duskFox CSS uses this to line it up better with + // a toolbar button. normally a panel is positioned -7px + // vertically from its anchor. but a panel anchored to a toolbar + // button is anchored -12px from its anchor. the reasons for this + // are just related to how the buttons are flexed in their container. + node.classList.contains("toolbarbutton-1") + ? panel.setAttribute("on-toolbar-button", true) + : panel.removeAttribute("on-toolbar-button"); + return node; + } + } } + panel.removeAttribute("on-toolbar-button"); + return BookmarkingUI.anchor; } - function init() { - window.hideOnNTP = new HideOnNTP(); - } - document.documentElement.setAttribute("hide-tp-icon-on-ntp", true); - if (gBrowserInit.delayedStartupFinished) init(); - else { - let delayedListener = (subject, topic) => { - if (topic == "browser-delayed-startup-finished" && subject == window) { - Services.obs.removeObserver(delayedListener, topic); - init(); - } - }; - Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); + constructor() { + XPCOMUtils.defineLazyPreferenceGetter( + this, + "hideBookmarks", + "userChrome.urlbar.hide-bookmarks-button-on-system-pages", + false + ); + eval( + `StarUI.showEditBookmarkPopup = async function ` + + StarUI.showEditBookmarkPopup + .toSource() + .replace(/^\(/, "") + .replace(/\)$/, "") + .replace(/async showEditBookmarkPopup/, "") + .replace(/async function\s*/, "") + .replace( + /this\.panel\.openPopup\(BookmarkingUI\.anchor, \"bottomcenter topright\"\)\;/, + `this.panel.openPopup(hideOnNTP?._getAnchor(this.panel) || BookmarkingUI.anchor, \"bottomcenter topright\");` + ) + ); + // the main part of this script. hide the tracking protection icon on new tab page. + gProtectionsHandler.onLocationChange = function onLocationChange() { + let currentURL = gBrowser.currentURI.spec; + let isInitial = isInitialPage(gBrowser.currentURI); + if (this._showToastAfterRefresh) { + this._showToastAfterRefresh = false; + if ( + this._previousURI == currentURL && + this._previousOuterWindowID == gBrowser.selectedBrowser.outerWindowID + ) + this.showProtectionsPopup({ toast: true }); + } + this.hadShieldState = false; + if (currentURL.startsWith("view-source:")) + this._trackingProtectionIconContainer.setAttribute("view-source", true); + else this._trackingProtectionIconContainer.removeAttribute("view-source"); + // make the identity box unfocusable on new tab page + if (gIdentityHandler._identityIconBox) + gIdentityHandler._identityIconBox.disabled = isInitial; + // hide the TP icon on new tab page + if (!ContentBlockingAllowList.canHandle(gBrowser.selectedBrowser) || isInitial) { + this._trackingProtectionIconContainer.hidden = true; + return; + } else this._trackingProtectionIconContainer.hidden = false; + this.hasException = ContentBlockingAllowList.includes(gBrowser.selectedBrowser); + if (this._protectionsPopup) + this._protectionsPopup.toggleAttribute("hasException", this.hasException); + this.iconBox.toggleAttribute("hasException", this.hasException); + this.fingerprintersHistogramAdd("pageLoad"); + this.cryptominersHistogramAdd("pageLoad"); + this.shieldHistogramAdd(0); + }; } + } + function init() { + window.hideOnNTP = new HideOnNTP(); + } + document.documentElement.setAttribute("hide-tp-icon-on-ntp", true); + if (gBrowserInit.delayedStartupFinished) init(); + else { + let delayedListener = (subject, topic) => { + if (topic == "browser-delayed-startup-finished" && subject == window) { + Services.obs.removeObserver(delayedListener, topic); + init(); + } + }; + Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); + } })(); diff --git a/JS/letCtrlWClosePinnedTabs.uc.js b/JS/letCtrlWClosePinnedTabs.uc.js index 4c0051bf..44ecb09f 100644 --- a/JS/letCtrlWClosePinnedTabs.uc.js +++ b/JS/letCtrlWClosePinnedTabs.uc.js @@ -1,45 +1,52 @@ -// ==UserScript== -// @name Let Ctrl+W Close Pinned Tabs -// @version 1.0 -// @author aminomancer -// @homepage https://github.com/aminomancer -// @description The filename should say it all, this just removes the "feature" that prevents you from closing pinned tabs with the Ctrl+W/Cmd+W shortcut. I guess this is meant to be consistent with the fact that the close button is hidden on pinned tabs by default, but it doesn't really make sense because it's a lot harder to accidentally press Ctrl+W than it is to accidentally click the close button. Firefox still lets you close tabs by middle-clicking them, which is arguably easier to do unintentionally than to press Ctrl+W. Since my theme makes pinned tabs really small, I also added a preference to hide the close button on pinned tabs. But I never find myself accidentally closing tabs with Ctrl+W so I'm disabling this little obstacle. -// @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. -// ==/UserScript== - -(() => { - function init() { - window.AminoCloseTabOrWindow = function AminoCloseTabOrWindow(event) { - // If we're not a browser window, just close the window. - if (window.location.href != AppConstants.BROWSER_CHROME_URL) { - closeWindow(true); - return; - } - - // In a multi-select context, close all selected tabs - if (gBrowser.multiSelectedTabsCount) { - gBrowser.removeMultiSelectedTabs(); - return; - } - - // If the current tab is the last one, this will close the window. - gBrowser.removeCurrentTab({ animate: true }); - }; - - document - .getElementById("cmd_close") - .setAttribute("oncommand", "AminoCloseTabOrWindow(event);"); - } - - if (gBrowserInit.delayedStartupFinished) { - init(); - } else { - let delayedListener = (subject, topic) => { - if (topic == "browser-delayed-startup-finished" && subject == window) { - Services.obs.removeObserver(delayedListener, topic); - init(); - } - }; - Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); - } -})(); +// ==UserScript== +// @name Let Ctrl+W Close Pinned Tabs +// @version 1.0 +// @author aminomancer +// @homepage https://github.com/aminomancer +// @description The filename should say it all, this just removes the +// "feature" that prevents you from closing pinned tabs with the Ctrl+W/Cmd+W +// shortcut. I guess this is meant to be consistent with the fact that the close +// button is hidden on pinned tabs by default, but it doesn't really make sense +// because it's a lot harder to accidentally press Ctrl+W than it is to +// accidentally click the close button. Firefox still lets you close tabs by +// middle-clicking them, which is arguably easier to do unintentionally than to +// press Ctrl+W. Since my theme makes pinned tabs really small, I also added a +// preference to hide the close button on pinned tabs. But I never find myself +// accidentally closing tabs with Ctrl+W so I'm disabling this little obstacle. +// @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. +// ==/UserScript== + +(() => { + function init() { + window.AminoCloseTabOrWindow = function AminoCloseTabOrWindow(event) { + // If we're not a browser window, just close the window. + if (window.location.href != AppConstants.BROWSER_CHROME_URL) { + closeWindow(true); + return; + } + + // In a multi-select context, close all selected tabs + if (gBrowser.multiSelectedTabsCount) { + gBrowser.removeMultiSelectedTabs(); + return; + } + + // If the current tab is the last one, this will close the window. + gBrowser.removeCurrentTab({ animate: true }); + }; + + document.getElementById("cmd_close").setAttribute("oncommand", "AminoCloseTabOrWindow(event);"); + } + + if (gBrowserInit.delayedStartupFinished) { + init(); + } else { + let delayedListener = (subject, topic) => { + if (topic == "browser-delayed-startup-finished" && subject == window) { + Services.obs.removeObserver(delayedListener, topic); + init(); + } + }; + Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished"); + } +})(); diff --git a/JS/miscMods.uc.js b/JS/miscMods.uc.js index f566af52..f04cd68a 100644 --- a/JS/miscMods.uc.js +++ b/JS/miscMods.uc.js @@ -1,6 +1,6 @@ // ==UserScript== // @name Misc. Mods -// @version 1.9.4 +// @version 2.0.0 // @author aminomancer // @homepage https://github.com/aminomancer/uc.css.js // @description Various tiny mods not worth making separate scripts for. Read the comments inside the script for details. @@ -8,405 +8,420 @@ // ==/UserScript== (function () { - let config = { - // by default the bookmarks toolbar unhides itself when you use the edit bookmark panel and - // select the bookmarks toolbar as the bookmark's folder. this is super annoying so I'm - // completely turning it off. - "Disable bookmarks toolbar auto show": true, + let config = { + // by default the bookmarks toolbar unhides itself when you use the edit bookmark panel and + // select the bookmarks toolbar as the bookmark's folder. this is super annoying so I'm + // completely turning it off. + "Disable bookmarks toolbar auto show": true, - // on macOS the arrow keyboard shortcuts (cmd+shift+pgup) "wrap" relative to the tab bar, so - // moving the final tab right will move it to the beginning of the tab bar. for some reason - // this is turned off on linux and windows. I'm turning it on. - "Moving tabs with arrow keys can wrap": true, + // on macOS the arrow keyboard shortcuts (cmd+shift+pgup) "wrap" relative to the tab bar, so + // moving the final tab right will move it to the beginning of the tab bar. for some reason + // this is turned off on linux and windows. I'm turning it on. + "Moving tabs with arrow keys can wrap": true, - // for some reason, when you open the downloads panel it automatically focuses the first - // element, which is the footer if you don't have any downloads. this is inconsistent with - // other panels, and a little annoying imo. it's not a big deal but one of firefox's biggest - // problems compared to other browsers is a general lack of consistency. so I think removing - // this whole behavior would probably be wise, but for now I'll just stop it from focusing - // the *footer*, but still allow it to focus the first download item if there are any. - "Stop downloads panel auto-focusing the footer button": true, + // for some reason, when you open the downloads panel it automatically focuses the first + // element, which is the footer if you don't have any downloads. this is inconsistent with + // other panels, and a little annoying imo. it's not a big deal but one of firefox's biggest + // problems compared to other browsers is a general lack of consistency. so I think removing + // this whole behavior would probably be wise, but for now I'll just stop it from focusing + // the *footer*, but still allow it to focus the first download item if there are any. + "Stop downloads panel auto-focusing the footer button": true, - // when you use the "move tab" hotkeys, e.g. Ctrl + Shift + PageUp, it only moves the active - // tab, even if you have multiple tabs selected. this is inconsistent with the keyboard - // shortcuts "move tab to end" or "move tab to start" and of course, inconsistent with the - // drag & drop behavior. this will change the hotkeys so they move all selected tabs. - "Move all selected tabs with hotkeys": true, + // when you use the "move tab" hotkeys, e.g. Ctrl + Shift + PageUp, it only moves the active + // tab, even if you have multiple tabs selected. this is inconsistent with the keyboard + // shortcuts "move tab to end" or "move tab to start" and of course, inconsistent with the + // drag & drop behavior. this will change the hotkeys so they move all selected tabs. + "Move all selected tabs with hotkeys": true, - // with browser.proton.places-tooltip.enabled, the bookmarks/history/tabs tooltip is - // improved and normally it gets anchored to the element that popped up the tooltip, i.e. - // the element you hovered. but for some reason menupopups are an exception. it does this on - // all relevant elements, including bookmarks in panels, just not on bookmarks menu popups. - // but I tested it and it works fine on menupopups so I'm removing the exception. there also - // isn't any anchoring inside sidebars, because the bookmarks/history items in the sidebar - // aren't actual DOM elements. there's just one node, the tree, and the individual items are - // drawn inside it. so they're kind of like virtual nodes. we can't anchor to them the - // normal way since they're not elements, but we can get their screen coordinates and - // constrain the tooltip popup within those coordinates. so this will implement the proton - // places tooltip behavior everywhere, rather than it being restricted to panels and the tab bar. - "Anchor bookmarks menu tooltip to bookmark": true, + // with browser.proton.places-tooltip.enabled, the bookmarks/history/tabs tooltip is + // improved and normally it gets anchored to the element that popped up the tooltip, i.e. + // the element you hovered. but for some reason menupopups are an exception. it does this on + // all relevant elements, including bookmarks in panels, just not on bookmarks menu popups. + // but I tested it and it works fine on menupopups so I'm removing the exception. there also + // isn't any anchoring inside sidebars, because the bookmarks/history items in the sidebar + // aren't actual DOM elements. there's just one node, the tree, and the individual items are + // drawn inside it. so they're kind of like virtual nodes. we can't anchor to them the + // normal way since they're not elements, but we can get their screen coordinates and + // constrain the tooltip popup within those coordinates. so this will implement the proton + // places tooltip behavior everywhere, rather than it being restricted to panels and the tab bar. + "Anchor bookmarks menu tooltip to bookmark": true, - // by default, when you hit ctrl+tab it waits 200ms before opening the panel. if you replace - // the 200 with another number, it will wait that long in milliseconds instead. - "Reduce ctrl+tab delay": 200, + // by default, when you hit ctrl+tab it waits 200ms before opening the panel. if you replace + // the 200 with another number, it will wait that long in milliseconds instead. + "Reduce ctrl+tab delay": 200, - // normally, firefox only animates the stop/reload button when it's in the main customizable - // navbar. if you enter customize mode and move the button to the tabs toolbar, menu bar, or - // personal/bookmarks toolbar, the animated transition between the stop icon to the reload - // icon disappears. the icon just instantly changes. I suspect this is done in order to - // avoid potential problems with density modes, but it doesn't seem necessary. as long as - // you provide some CSS it works fine: - // #stop-reload-button {position: relative;} - // #stop-reload-button > :is(#reload-button, #stop-button) > .toolbarbutton-animatable-box {display: block;} - // :is(#reload-button, #stop-button) > .toolbarbutton-icon {padding: var(--toolbarbutton-inner-padding) !important;} - "Allow stop/reload button to animate in other toolbars": true, + // normally, firefox only animates the stop/reload button when it's in the main customizable + // navbar. if you enter customize mode and move the button to the tabs toolbar, menu bar, or + // personal/bookmarks toolbar, the animated transition between the stop icon to the reload + // icon disappears. the icon just instantly changes. I suspect this is done in order to + // avoid potential problems with density modes, but it doesn't seem necessary. as long as + // you provide some CSS it works fine: + // #stop-reload-button {position: relative;} + // #stop-reload-button > :is(#reload-button, #stop-button) > .toolbarbutton-animatable-box {display: block;} + // :is(#reload-button, #stop-button) > .toolbarbutton-icon {padding: var(--toolbarbutton-inner-padding) !important;} + "Allow stop/reload button to animate in other toolbars": true, - // When you open a private window, it shows a little private browsing icon in the top of the - // navbar, next to the window control buttons. It doesn't have a tooltip for some reason, so - // if you don't already recognize the private browsing icon, you won't know what it means. - // This simply gives it a localized tooltip like "You're in a Private Window" in English. - // The exact string is drawn from Firefox's fluent files, so it depends on your language. - "Give the private browsing indicator a tooltip": true, + // When you open a private window, it shows a little private browsing icon in the top of the + // navbar, next to the window control buttons. It doesn't have a tooltip for some reason, so + // if you don't already recognize the private browsing icon, you won't know what it means. + // This simply gives it a localized tooltip like "You're in a Private Window" in English. + // The exact string is drawn from Firefox's fluent files, so it depends on your language. + "Give the private browsing indicator a tooltip": true, - // The location where your bookmarks are saved by default is defined in the preference - // browser.bookmarks.defaultLocation. This pref is updated every time you manually change a - // bookmark's folder in the urlbar star button's edit bookmark panel. So if you want to save - // to toolbar by default, but you just added a bookmark to a different folder with the - // panel, that different folder now becomes your default location. So the next time you go - // to add a bookmark, instead of saving it to your toolbar it'll save it to the most recent - // folder you chose in the edit bookmark panel. This can be kind of annoying if you have a - // main bookmarks folder and a bunch of smaller subfolders. So I added this option to - // eliminate this updating behavior. This will stop Firefox from automatically updating the - // preference every time you use the edit bookmark panel. Once you install the script there - // will be a new checkbox in the edit bookmark panel, once you expand the "location" - // section. If you uncheck this checkbox, Firefox will stop updating the default bookmark - // location. So whatever the default location is set to at the time you uncheck the checkbox - // will permanently remain your default location. You can still change the default location - // by modifying the preference directly or by temporarily checking that checkbox. It just - // means the default location will only automatically change when the checkbox is checked. - "Preserve your default bookmarks folder": true, + // The location where your bookmarks are saved by default is defined in the preference + // browser.bookmarks.defaultLocation. This pref is updated every time you manually change a + // bookmark's folder in the urlbar star button's edit bookmark panel. So if you want to save + // to toolbar by default, but you just added a bookmark to a different folder with the + // panel, that different folder now becomes your default location. So the next time you go + // to add a bookmark, instead of saving it to your toolbar it'll save it to the most recent + // folder you chose in the edit bookmark panel. This can be kind of annoying if you have a + // main bookmarks folder and a bunch of smaller subfolders. So I added this option to + // eliminate this updating behavior. This will stop Firefox from automatically updating the + // preference every time you use the edit bookmark panel. Once you install the script there + // will be a new checkbox in the edit bookmark panel, once you expand the "location" + // section. If you uncheck this checkbox, Firefox will stop updating the default bookmark + // location. So whatever the default location is set to at the time you uncheck the checkbox + // will permanently remain your default location. You can still change the default location + // by modifying the preference directly or by temporarily checking that checkbox. It just + // means the default location will only automatically change when the checkbox is checked. + "Preserve your default bookmarks folder": true, - // By default, the private browsing indicator is just an inert that sits next to the - // window control buttons. Hovering it reveals a tooltip, but that's it. Without any hover - // styles it seems kind of out of place. But giving something hover styles when it has no - // actual function seems like a bad idea. So instead of doing nothing, clicking the - // indicator will open a support page with info about private browsing. Better than nothing, - // and I didn't want to make it a redundant "new private window" button. - "Turn private browsing indicator into button": true, + // By default, the private browsing indicator is just an inert that sits next to the + // window control buttons. Hovering it reveals a tooltip, but that's it. Without any hover + // styles it seems kind of out of place. But giving something hover styles when it has no + // actual function seems like a bad idea. So instead of doing nothing, clicking the + // indicator will open a support page with info about private browsing. Better than nothing, + // and I didn't want to make it a redundant "new private window" button. + "Turn private browsing indicator into button": true, - // By default, the permissions popup anchors to the center of the permissions box. But this - // box can have anywhere from 1 to 20 icons visible at one time. So the permission winds up - // appearing like it's just floating in space rather than anchored to something in - // particular. This mod will change the method so that it anchors to the permission granted - // icon instead. That's the first icon in the box. So it will appear left-aligned rather - // than center aligned. - "Anchor permissions popup to granted permission icon": true, + // By default, the permissions popup anchors to the center of the permissions box. But this + // box can have anywhere from 1 to 20 icons visible at one time. So the permission winds up + // appearing like it's just floating in space rather than anchored to something in + // particular. This mod will change the method so that it anchors to the permission granted + // icon instead. That's the first icon in the box. So it will appear left-aligned rather + // than center aligned. + "Anchor permissions popup to granted permission icon": true, - // When you click and drag a tab, Firefox displays a small thumbnail preview of the tab's - // content next to your mouse cursor (provided you have `nglayout.enable_drag_images` set to - // true). This preview has a white background, so it will display as a 160x90px white - // rectangle until the thumbnail loads. If you use "dark mode" a lot, this will pretty - // consistently result in an unsightly white flash every time you click and drag a tab. - // Unfortunately the white color is set at the Canvas level so can't be overridden with CSS. - // But with JavaScript we can change the method that sets the background color. Instead of - // using a fixed color value, this setting calculates the effective value of a CSS variable, - // --in-content-bg-dark. This variable is already set by duskFox so you don't need to set it - // yourself if you use my CSS theme. If you don't, then make sure you add - // `:root{--in-content-bg-dark: #000}` to your userChrome.css, or it will fall back to white. - "Customize tab drag preview background color": true, - }; - class UCMiscMods { - constructor() { - if (config["Disable bookmarks toolbar auto show"]) - gEditItemOverlay._autoshowBookmarksToolbar = function () {}; - if (config["Moving tabs with arrow keys can wrap"]) gBrowser.arrowKeysShouldWrap = true; - if (config["Stop downloads panel auto-focusing the footer button"]) - this.stopDownloadsPanelFocus(); - if (config["Move all selected tabs with hotkeys"]) this.moveTabKeysMoveSelectedTabs(); - if (config["Anchor bookmarks menu tooltip to bookmark"]) this.anchorBookmarksTooltip(); - this.reduceCtrlTabDelay(config["Reduce ctrl+tab delay"]); - if (config["Allow stop/reload button to animate in other toolbars"]) - this.stopReloadAnimations(); - if (config["Give the private browsing indicator a tooltip"]) - this.addPrivateBrowsingTooltip(); - if (config["Preserve your default bookmarks folder"]) - this.makeDefaultBookmarkFolderPermanent(); - if (config["Turn private browsing indicator into button"]) - this.privateBrowsingIndicatorButton(); - if (config["Anchor permissions popup to granted permission icon"]) - this.anchorPermissionsPopup(); - if (config["Customize tab drag preview background color"]) this.tabDragPreview(); - this.randomTinyStuff(); - } - stopDownloadsPanelFocus() { - eval( - `DownloadsPanel._focusPanel = function ` + - DownloadsPanel._focusPanel - .toSource() - .replace(/DownloadsFooter\.focus\(\)\;/, ``) - ); - } - moveTabKeysMoveSelectedTabs() { - gBrowser.moveTabsBackward = function () { - let tabs = this.selectedTab.multiselected ? this.selectedTabs : [this.selectedTab]; - let previousTab = this.tabContainer.findNextTab(tabs[0], { - direction: -1, - filter: (tab) => !tab.hidden, - }); - for (let tab of tabs) { - if (previousTab) this.moveTabTo(tab, previousTab._tPos); - else if (this.arrowKeysShouldWrap && tab._tPos < this.browsers.length - 1) - this.moveTabTo(tab, this.browsers.length - 1); - } - }; - gBrowser.moveTabsForward = function () { - let tabs = this.selectedTab.multiselected ? this.selectedTabs : [this.selectedTab]; - let nextTab = this.tabContainer.findNextTab(tabs[tabs.length - 1], { - direction: 1, - filter: (tab) => !tab.hidden, - }); - for (let i = tabs.length - 1; i >= 0; i--) { - let tab = tabs[i]; - if (nextTab) this.moveTabTo(tab, nextTab._tPos); - else if (this.arrowKeysShouldWrap && tab._tPos > 0) this.moveTabTo(tab, 0); - } - }; - eval( - `gBrowser._handleKeyDownEvent = function ` + - gBrowser._handleKeyDownEvent - .toSource() - .replace(/moveTabBackward/, `moveTabsBackward`) - .replace(/moveTabForward/, `moveTabsForward`) - ); - } - anchorBookmarksTooltip() { - BookmarksEventHandler.fillInBHTooltip = function (aDocument, aEvent) { - var node; - var cropped = false; - var targetURI; - let tooltip = aEvent.target; - if (tooltip.triggerNode.localName == "treechildren") { - var tree = tooltip.triggerNode.parentNode; - var cell = tree.getCellAt(aEvent.clientX, aEvent.clientY); - if (cell.row == -1) return false; - node = tree.view.nodeForTreeIndex(cell.row); - cropped = tree.isCellCropped(cell.row, cell.col); - // get coordinates for the cell in a tree. - var cellCoords = tree.getCoordsForCellItem(cell.row, cell.col, "cell"); - } else { - var tooltipNode = tooltip.triggerNode; - if (tooltipNode._placesNode) node = tooltipNode._placesNode; - else targetURI = tooltipNode.getAttribute("targetURI"); - } - if (!node && !targetURI) return false; - var title = node ? node.title : tooltipNode.label; - var url; - if (targetURI || PlacesUtils.nodeIsURI(node)) url = targetURI || node.uri; - if (!cropped && !url) return false; - aEvent.target.setAttribute("position", "after_start"); - if (tooltipNode) aEvent.target.moveToAnchor(tooltipNode, "after_start"); - else if (tree && cellCoords) - // anchor the tooltip to the tree cell - aEvent.target.moveTo( - cellCoords.left + tree.screenX, - cellCoords.bottom + tree.screenY - ); - let tooltipTitle = aEvent.target.querySelector(".places-tooltip-title"); - tooltipTitle.hidden = !title || title == url; - if (!tooltipTitle.hidden) tooltipTitle.textContent = title; - let tooltipUrl = aEvent.target.querySelector(".places-tooltip-uri"); - tooltipUrl.hidden = !url; - if (!tooltipUrl.hidden) tooltipUrl.value = url; - return true; - }; - } - reduceCtrlTabDelay(delay) { - if (delay === 200) return; - ctrlTab.open = function () { - if (this.isOpen) return; - this.canvasWidth = Math.ceil((screen.availWidth * 0.85) / this.maxTabPreviews); - this.canvasHeight = Math.round(this.canvasWidth * tabPreviews.aspectRatio); - this.updatePreviews(); - this._selectedIndex = 1; - gBrowser.warmupTab(this.selected._tab); - this._timer = setTimeout(() => { - this._timer = null; - this._openPanel(); - }, delay); - }; - } - stopReloadAnimations() { - eval( - `CombinedStopReload.switchToStop = function ` + - CombinedStopReload.switchToStop - .toSource() - .replace(/switchToStop/, "") - .replace(/#nav-bar-customization-target/, `.customization-target`) - ); - eval( - `CombinedStopReload.switchToReload = function ` + - CombinedStopReload.switchToReload - .toSource() - .replace(/switchToReload/, "") - .replace(/#nav-bar-customization-target/, `.customization-target`) - ); + // By default, when you're in DOM fullscreen (e.g. you clicked the + // fullscreen button in a web video player like YouTube's) and you open + // the permissions popup somehow, Firefox exits fullscreen before + // opening the popup. This was done to prevent a weird flickering bug. + // But in my testing it doesn't seem to be necessary, at least when + // duskFox is installed. So this tiny mod just removes that behavior so + // that it opens as normal in fullscreen. + "Don't exit DOM fullscreen when opening permissions popup": true, + + // When you click and drag a tab, Firefox displays a small thumbnail preview of the tab's + // content next to your mouse cursor (provided you have `nglayout.enable_drag_images` set to + // true). This preview has a white background, so it will display as a 160x90px white + // rectangle until the thumbnail loads. If you use "dark mode" a lot, this will pretty + // consistently result in an unsightly white flash every time you click and drag a tab. + // Unfortunately the white color is set at the Canvas level so can't be overridden with CSS. + // But with JavaScript we can change the method that sets the background color. Instead of + // using a fixed color value, this setting calculates the effective value of a CSS variable, + // --in-content-bg-dark. This variable is already set by duskFox so you don't need to set it + // yourself if you use my CSS theme. If you don't, then make sure you add + // `:root{--in-content-bg-dark: #000}` to your userChrome.css, or it will fall back to white. + "Customize tab drag preview background color": true, + }; + class UCMiscMods { + constructor() { + if (config["Disable bookmarks toolbar auto show"]) + gEditItemOverlay._autoshowBookmarksToolbar = function () {}; + if (config["Moving tabs with arrow keys can wrap"]) gBrowser.arrowKeysShouldWrap = true; + if (config["Stop downloads panel auto-focusing the footer button"]) + this.stopDownloadsPanelFocus(); + if (config["Move all selected tabs with hotkeys"]) this.moveTabKeysMoveSelectedTabs(); + if (config["Anchor bookmarks menu tooltip to bookmark"]) this.anchorBookmarksTooltip(); + this.reduceCtrlTabDelay(config["Reduce ctrl+tab delay"]); + if (config["Allow stop/reload button to animate in other toolbars"]) + this.stopReloadAnimations(); + if (config["Give the private browsing indicator a tooltip"]) this.addPrivateBrowsingTooltip(); + if (config["Preserve your default bookmarks folder"]) + this.makeDefaultBookmarkFolderPermanent(); + if (config["Turn private browsing indicator into button"]) + this.privateBrowsingIndicatorButton(); + if (config["Anchor permissions popup to granted permission icon"]) + this.anchorPermissionsPopup(); + if (config["Don't exit DOM fullscreen when opening permissions popup"]) + this.permsPopupInFullscreen(); + if (config["Customize tab drag preview background color"]) this.tabDragPreview(); + this.randomTinyStuff(); + } + stopDownloadsPanelFocus() { + eval( + `DownloadsPanel._focusPanel = function ` + + DownloadsPanel._focusPanel.toSource().replace(/DownloadsFooter\.focus\(\)\;/, ``) + ); + } + moveTabKeysMoveSelectedTabs() { + gBrowser.moveTabsBackward = function () { + let tabs = this.selectedTab.multiselected ? this.selectedTabs : [this.selectedTab]; + let previousTab = this.tabContainer.findNextTab(tabs[0], { + direction: -1, + filter: tab => !tab.hidden, + }); + for (let tab of tabs) { + if (previousTab) this.moveTabTo(tab, previousTab._tPos); + else if (this.arrowKeysShouldWrap && tab._tPos < this.browsers.length - 1) + this.moveTabTo(tab, this.browsers.length - 1); } - async addPrivateBrowsingTooltip() { - this.privateL10n = await new Localization(["browser/aboutPrivateBrowsing.ftl"], true); - let l10nId = PrivateBrowsingUtils.isWindowPrivate(window) - ? "about-private-browsing-info-title" - : "about-private-browsing-not-private"; - document.querySelector(".private-browsing-indicator").tooltipText = - await this.privateL10n.formatValue([l10nId]); + }; + gBrowser.moveTabsForward = function () { + let tabs = this.selectedTab.multiselected ? this.selectedTabs : [this.selectedTab]; + let nextTab = this.tabContainer.findNextTab(tabs[tabs.length - 1], { + direction: 1, + filter: tab => !tab.hidden, + }); + for (let i = tabs.length - 1; i >= 0; i--) { + let tab = tabs[i]; + if (nextTab) this.moveTabTo(tab, nextTab._tPos); + else if (this.arrowKeysShouldWrap && tab._tPos > 0) this.moveTabTo(tab, 0); } - makeDefaultBookmarkFolderPermanent() { - let { panel } = StarUI; - let checkbox = panel.querySelector("#editBMPanel_newFolderBox").appendChild( - _ucUtils.createElement(document, "checkbox", { - id: "editBookmarkPanel_persistLastLocation", - label: "Remember last location", - accesskey: "R", - tooltiptext: - "Update the default bookmark folder when you change it. If unchecked, the folder chosen when this was checked will remain the default folder indefinitely.", - oncommand: `Services.prefs.setBoolPref("userChrome.bookmarks.editDialog.persistLastLocation", this.checked)`, - }) - ); - panel.addEventListener("popupshowing", (e) => { - if (e.target !== panel) return; - let pref = Services.prefs.getBoolPref( - "userChrome.bookmarks.editDialog.persistLastLocation", - true - ); - checkbox.checked = pref; - }); - eval( - `StarUI._storeRecentlyUsedFolder = async function ` + - StarUI._storeRecentlyUsedFolder - .toSource() - .replace(/^async \_storeRecentlyUsedFolder/, "") - .replace( - /if \(didChangeFolder\)/, - `if (didChangeFolder && Services.prefs.getBoolPref("userChrome.bookmarks.editDialog.persistLastLocation", true))` - ) - ); + }; + eval( + `gBrowser._handleKeyDownEvent = function ` + + gBrowser._handleKeyDownEvent + .toSource() + .replace(/moveTabBackward/, `moveTabsBackward`) + .replace(/moveTabForward/, `moveTabsForward`) + ); + } + anchorBookmarksTooltip() { + BookmarksEventHandler.fillInBHTooltip = function (aDocument, aEvent) { + var node; + var cropped = false; + var targetURI; + let tooltip = aEvent.target; + if (tooltip.triggerNode.localName == "treechildren") { + var tree = tooltip.triggerNode.parentNode; + var cell = tree.getCellAt(aEvent.clientX, aEvent.clientY); + if (cell.row == -1) return false; + node = tree.view.nodeForTreeIndex(cell.row); + cropped = tree.isCellCropped(cell.row, cell.col); + // get coordinates for the cell in a tree. + var cellCoords = tree.getCoordsForCellItem(cell.row, cell.col, "cell"); + } else { + var tooltipNode = tooltip.triggerNode; + if (tooltipNode._placesNode) node = tooltipNode._placesNode; + else targetURI = tooltipNode.getAttribute("targetURI"); } - privateBrowsingIndicatorButton() { - let indicator = document.querySelector(".private-browsing-indicator"); - let tooltiptext = indicator.getAttribute("tooltiptext"); - let markup = `