CustomizableWidgets.sys.mjs (21670B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 6 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 7 import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"; 8 9 const lazy = {}; 10 11 ChromeUtils.defineESModuleGetters(lazy, { 12 CustomizableUI: 13 "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs", 14 LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", 15 PanelMultiView: 16 "moz-src:///browser/components/customizableui/PanelMultiView.sys.mjs", 17 RecentlyClosedTabsAndWindowsMenuUtils: 18 "resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs", 19 Sanitizer: "resource:///modules/Sanitizer.sys.mjs", 20 SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", 21 ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs", 22 }); 23 24 const kPrefCustomizationDebug = "browser.uiCustomization.debug"; 25 26 ChromeUtils.defineLazyGetter(lazy, "log", () => { 27 let { ConsoleAPI } = ChromeUtils.importESModule( 28 "resource://gre/modules/Console.sys.mjs" 29 ); 30 let debug = Services.prefs.getBoolPref(kPrefCustomizationDebug, false); 31 let consoleOptions = { 32 maxLogLevel: debug ? "all" : "log", 33 prefix: "CustomizableWidgets", 34 }; 35 return new ConsoleAPI(consoleOptions); 36 }); 37 38 XPCOMUtils.defineLazyPreferenceGetter( 39 lazy, 40 "sidebarRevampEnabled", 41 "sidebar.revamp", 42 false 43 ); 44 45 /** 46 * A helper method to synchronize aNode's DOM attributes with the properties and 47 * values in aAttrs. If aNode has an attribute that is false-y in aAttrs, 48 * then this attribute is removed. 49 * 50 * If aAttrs includes "shortcutId", the value is never set on aNode, but is 51 * instead used when setting the "label" or "tooltiptext" attributes to include 52 * the shortcut key combo. shortcutId should refer to the ID of the XUL <key> 53 * element that acts as the shortcut. 54 * 55 * @param {Element} aNode 56 * The element to change the attributes of. 57 * @param {object} aAttrs 58 * A set of key-value pairs where the key is set as the attribute name, and 59 * the value is set as the attribute value. 60 */ 61 function setAttributes(aNode, aAttrs) { 62 let doc = aNode.ownerDocument; 63 for (let [name, value] of Object.entries(aAttrs)) { 64 if (!value) { 65 if (aNode.hasAttribute(name)) { 66 aNode.removeAttribute(name); 67 } 68 } else { 69 if (name == "shortcutId") { 70 continue; 71 } 72 if (name == "label" || name == "tooltiptext") { 73 let stringId = typeof value == "string" ? value : name; 74 let additionalArgs = []; 75 if (aAttrs.shortcutId) { 76 let shortcut = doc.getElementById(aAttrs.shortcutId); 77 if (shortcut) { 78 additionalArgs.push(lazy.ShortcutUtils.prettifyShortcut(shortcut)); 79 } 80 } 81 value = lazy.CustomizableUI.getLocalizedProperty( 82 { id: aAttrs.id }, 83 stringId, 84 additionalArgs 85 ); 86 } 87 aNode.setAttribute(name, value); 88 } 89 } 90 } 91 92 /** 93 * The array of built-in CustomizableUICreateWidgetProperties that are 94 * registered as widgets upon browser start. 95 * 96 * @type {CustomizableUICreateWidgetProperties[]} 97 */ 98 export const CustomizableWidgets = [ 99 { 100 id: "history-panelmenu", 101 type: "view", 102 viewId: "PanelUI-history", 103 shortcutId: "key_gotoHistory", 104 tooltiptext: "history-panelmenu.tooltiptext2", 105 recentlyClosedTabsPanel: "appMenu-library-recentlyClosedTabs", 106 recentlyClosedWindowsPanel: "appMenu-library-recentlyClosedWindows", 107 handleEvent(event) { 108 switch (event.type) { 109 case "PanelMultiViewHidden": 110 this.onPanelMultiViewHidden(event); 111 break; 112 case "ViewShowing": 113 this.onSubViewShowing(event); 114 break; 115 case "unload": 116 this.onWindowUnload(event); 117 break; 118 case "command": { 119 let { target } = event; 120 let { PanelUI, PlacesCommandHook } = target.ownerGlobal; 121 if (target.id == "appMenuRecentlyClosedTabs") { 122 PanelUI.showSubView(this.recentlyClosedTabsPanel, target); 123 } else if (target.id == "appMenuRecentlyClosedWindows") { 124 PanelUI.showSubView(this.recentlyClosedWindowsPanel, target); 125 } else if (target.id == "appMenuSearchHistory") { 126 PlacesCommandHook.searchHistory(); 127 } 128 break; 129 } 130 default: 131 throw new Error(`Unsupported event for '${this.id}'`); 132 } 133 }, 134 onViewShowing(event) { 135 if (this._panelMenuView) { 136 return; 137 } 138 139 let panelview = event.target; 140 let document = panelview.ownerDocument; 141 let window = document.defaultView; 142 const closedTabCount = lazy.SessionStore.getClosedTabCount(); 143 144 lazy.PanelMultiView.getViewNode( 145 document, 146 "appMenuRecentlyClosedTabs" 147 ).disabled = closedTabCount == 0; 148 lazy.PanelMultiView.getViewNode( 149 document, 150 "appMenuRecentlyClosedWindows" 151 ).disabled = lazy.SessionStore.getClosedWindowCount(window) == 0; 152 153 lazy.PanelMultiView.getViewNode( 154 document, 155 "appMenu-restoreSession" 156 ).hidden = !lazy.SessionStore.canRestoreLastSession; 157 158 // We restrict the amount of results to 42. Not 50, but 42. Why? Because 42. 159 let query = 160 "place:queryType=" + 161 Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY + 162 "&sort=" + 163 Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING + 164 "&maxResults=42&excludeQueries=1"; 165 166 this._panelMenuView = new window.PlacesPanelview( 167 query, 168 document.getElementById("appMenu_historyMenu"), 169 panelview 170 ); 171 // When either of these sub-subviews show, populate them with recently closed 172 // objects data. 173 lazy.PanelMultiView.getViewNode( 174 document, 175 this.recentlyClosedTabsPanel 176 ).addEventListener("ViewShowing", this); 177 lazy.PanelMultiView.getViewNode( 178 document, 179 this.recentlyClosedWindowsPanel 180 ).addEventListener("ViewShowing", this); 181 // When the popup is hidden (thus the panelmultiview node as well), make 182 // sure to stop listening to PlacesDatabase updates. 183 panelview.panelMultiView.addEventListener("PanelMultiViewHidden", this); 184 panelview.addEventListener("command", this); 185 window.addEventListener("unload", this); 186 }, 187 onViewHiding() { 188 lazy.log.debug("History view is being hidden!"); 189 }, 190 onPanelMultiViewHidden(event) { 191 let panelMultiView = event.target; 192 let document = panelMultiView.ownerDocument; 193 if (this._panelMenuView) { 194 this._panelMenuView.uninit(); 195 delete this._panelMenuView; 196 lazy.PanelMultiView.getViewNode( 197 document, 198 this.recentlyClosedTabsPanel 199 ).removeEventListener("ViewShowing", this); 200 lazy.PanelMultiView.getViewNode( 201 document, 202 this.recentlyClosedWindowsPanel 203 ).removeEventListener("ViewShowing", this); 204 lazy.PanelMultiView.getViewNode( 205 document, 206 this.viewId 207 ).removeEventListener("command", this); 208 } 209 panelMultiView.removeEventListener("PanelMultiViewHidden", this); 210 }, 211 onWindowUnload() { 212 if (this._panelMenuView) { 213 delete this._panelMenuView; 214 } 215 }, 216 onSubViewShowing(event) { 217 let panelview = event.target; 218 let document = event.target.ownerDocument; 219 let window = document.defaultView; 220 221 this._panelMenuView.clearAllContents(panelview); 222 223 const utils = lazy.RecentlyClosedTabsAndWindowsMenuUtils; 224 const fragment = 225 panelview.id == this.recentlyClosedTabsPanel 226 ? utils.getTabsFragment(window, "toolbarbutton") 227 : utils.getWindowsFragment(window, "toolbarbutton"); 228 let elementCount = fragment.childElementCount; 229 this._panelMenuView._setEmptyPopupStatus(panelview, !elementCount); 230 if (!elementCount) { 231 return; 232 } 233 234 let body = document.createXULElement("vbox"); 235 body.className = "panel-subview-body"; 236 body.appendChild(fragment); 237 let separator = document.createXULElement("toolbarseparator"); 238 let footer; 239 while (--elementCount >= 0) { 240 let element = body.children[elementCount]; 241 if (element.tagName != "toolbarbutton") { 242 continue; 243 } 244 lazy.CustomizableUI.addShortcut(element); 245 if (element.classList.contains("restoreallitem")) { 246 footer = element; 247 } 248 } 249 panelview.appendChild(body); 250 panelview.appendChild(separator); 251 panelview.appendChild(footer); 252 }, 253 }, 254 { 255 id: "save-page-button", 256 l10nId: "toolbar-button-save-page", 257 shortcutId: "key_savePage", 258 onCreated(aNode) { 259 aNode.setAttribute("command", "Browser:SavePage"); 260 }, 261 }, 262 { 263 id: "print-button", 264 l10nId: "navbar-print", 265 shortcutId: "printKb", 266 keepBroadcastAttributesWhenCustomizing: true, 267 onCreated(aNode) { 268 aNode.setAttribute("command", "cmd_printPreviewToggle"); 269 }, 270 }, 271 { 272 id: "find-button", 273 shortcutId: "key_find", 274 tooltiptext: "find-button.tooltiptext3", 275 onCommand(aEvent) { 276 let win = aEvent.target.ownerGlobal; 277 if (win.gLazyFindCommand) { 278 win.gLazyFindCommand("onFindCommand"); 279 } 280 }, 281 }, 282 { 283 id: "open-file-button", 284 l10nId: "toolbar-button-open-file", 285 shortcutId: "openFileKb", 286 onCreated(aNode) { 287 aNode.setAttribute("command", "Browser:OpenFile"); 288 }, 289 }, 290 { 291 id: "sidebar-button", 292 l10nId: "show-sidebars", 293 defaultArea: "nav-bar", 294 _introducedByPref: "sidebar.revamp", 295 onCommand(aEvent) { 296 const { SidebarController } = aEvent.target.ownerGlobal; 297 if (lazy.sidebarRevampEnabled) { 298 SidebarController.handleToolbarButtonClick(); 299 } else { 300 SidebarController.toggle(); 301 } 302 }, 303 onCreated(aNode) { 304 if (lazy.sidebarRevampEnabled) { 305 const { SidebarController } = aNode.ownerGlobal; 306 SidebarController.updateToolbarButton(aNode); 307 aNode.setAttribute("overflows", "false"); 308 // Show the toolbar button badge by setting the badged attribute. 309 // This activates badge styling by adding feature-callout class to the toolbarbutton-badge element. 310 aNode.setAttribute("badged", true); 311 } else { 312 // Add an observer so the button is checked while the sidebar is open 313 let doc = aNode.ownerDocument; 314 let obChecked = doc.createXULElement("observes"); 315 obChecked.setAttribute("element", "sidebar-box"); 316 obChecked.setAttribute("attribute", "checked"); 317 let obPosition = doc.createXULElement("observes"); 318 obPosition.setAttribute("element", "sidebar-box"); 319 obPosition.setAttribute("attribute", "positionend"); 320 aNode.appendChild(obChecked); 321 aNode.appendChild(obPosition); 322 } 323 }, 324 }, 325 { 326 id: "zoom-controls", 327 type: "custom", 328 tooltiptext: "zoom-controls.tooltiptext2", 329 onBuild(aDocument) { 330 let buttons = [ 331 { 332 id: "zoom-out-button", 333 command: "cmd_fullZoomReduce", 334 label: true, 335 closemenu: "none", 336 tooltiptext: "tooltiptext2", 337 shortcutId: "key_fullZoomReduce", 338 class: "toolbarbutton-1 toolbarbutton-combined", 339 }, 340 { 341 id: "zoom-reset-button", 342 command: "cmd_fullZoomReset", 343 closemenu: "none", 344 tooltiptext: "tooltiptext2", 345 shortcutId: "key_fullZoomReset", 346 class: "toolbarbutton-1 toolbarbutton-combined", 347 }, 348 { 349 id: "zoom-in-button", 350 command: "cmd_fullZoomEnlarge", 351 closemenu: "none", 352 label: true, 353 tooltiptext: "tooltiptext2", 354 shortcutId: "key_fullZoomEnlarge", 355 class: "toolbarbutton-1 toolbarbutton-combined", 356 }, 357 ]; 358 359 let node = aDocument.createXULElement("toolbaritem"); 360 node.setAttribute("id", "zoom-controls"); 361 node.setAttribute( 362 "label", 363 lazy.CustomizableUI.getLocalizedProperty(this, "label") 364 ); 365 node.setAttribute( 366 "title", 367 lazy.CustomizableUI.getLocalizedProperty(this, "tooltiptext") 368 ); 369 // Set this as an attribute in addition to the property to make sure we can style correctly. 370 node.setAttribute("removable", "true"); 371 node.classList.add("chromeclass-toolbar-additional"); 372 node.classList.add("toolbaritem-combined-buttons"); 373 374 buttons.forEach(function (aButton, aIndex) { 375 if (aIndex != 0) { 376 node.appendChild(aDocument.createXULElement("separator")); 377 } 378 let btnNode = aDocument.createXULElement("toolbarbutton"); 379 setAttributes(btnNode, aButton); 380 node.appendChild(btnNode); 381 }); 382 return node; 383 }, 384 }, 385 { 386 id: "edit-controls", 387 type: "custom", 388 tooltiptext: "edit-controls.tooltiptext2", 389 onBuild(aDocument) { 390 let buttons = [ 391 { 392 id: "cut-button", 393 command: "cmd_cut", 394 label: true, 395 tooltiptext: "tooltiptext2", 396 shortcutId: "key_cut", 397 class: "toolbarbutton-1 toolbarbutton-combined", 398 }, 399 { 400 id: "copy-button", 401 command: "cmd_copy", 402 label: true, 403 tooltiptext: "tooltiptext2", 404 shortcutId: "key_copy", 405 class: "toolbarbutton-1 toolbarbutton-combined", 406 }, 407 { 408 id: "paste-button", 409 command: "cmd_paste", 410 label: true, 411 tooltiptext: "tooltiptext2", 412 shortcutId: "key_paste", 413 class: "toolbarbutton-1 toolbarbutton-combined", 414 }, 415 ]; 416 417 let node = aDocument.createXULElement("toolbaritem"); 418 node.setAttribute("id", "edit-controls"); 419 node.setAttribute( 420 "label", 421 lazy.CustomizableUI.getLocalizedProperty(this, "label") 422 ); 423 node.setAttribute( 424 "title", 425 lazy.CustomizableUI.getLocalizedProperty(this, "tooltiptext") 426 ); 427 // Set this as an attribute in addition to the property to make sure we can style correctly. 428 node.setAttribute("removable", "true"); 429 node.classList.add("chromeclass-toolbar-additional"); 430 node.classList.add("toolbaritem-combined-buttons"); 431 432 buttons.forEach(function (aButton, aIndex) { 433 if (aIndex != 0) { 434 node.appendChild(aDocument.createXULElement("separator")); 435 } 436 let btnNode = aDocument.createXULElement("toolbarbutton"); 437 setAttributes(btnNode, aButton); 438 node.appendChild(btnNode); 439 }); 440 441 let listener = { 442 onWidgetInstanceRemoved: (aWidgetId, aDoc) => { 443 if (aWidgetId != this.id || aDoc != aDocument) { 444 return; 445 } 446 lazy.CustomizableUI.removeListener(listener); 447 }, 448 onWidgetOverflow(aWidgetNode) { 449 if (aWidgetNode == node) { 450 node.ownerGlobal.updateEditUIVisibility(); 451 } 452 }, 453 onWidgetUnderflow(aWidgetNode) { 454 if (aWidgetNode == node) { 455 node.ownerGlobal.updateEditUIVisibility(); 456 } 457 }, 458 }; 459 lazy.CustomizableUI.addListener(listener); 460 461 return node; 462 }, 463 }, 464 { 465 id: "characterencoding-button", 466 l10nId: "repair-text-encoding-button", 467 onCommand(aEvent) { 468 aEvent.view.BrowserCommands.forceEncodingDetection(); 469 }, 470 }, 471 { 472 id: "email-link-button", 473 l10nId: "toolbar-button-email-link", 474 onCommand(aEvent) { 475 let win = aEvent.view; 476 win.MailIntegration.sendLinkForBrowser(win.gBrowser.selectedBrowser); 477 }, 478 }, 479 { 480 id: "logins-button", 481 l10nId: "toolbar-button-logins", 482 onCommand(aEvent) { 483 let window = aEvent.view; 484 lazy.LoginHelper.openPasswordManager(window, { entryPoint: "Toolbar" }); 485 }, 486 }, 487 ]; 488 489 if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) { 490 CustomizableWidgets.push({ 491 id: "sync-button", 492 l10nId: "toolbar-button-synced-tabs", 493 type: "view", 494 viewId: "PanelUI-remotetabs", 495 onViewShowing(aEvent) { 496 let panelview = aEvent.target; 497 let doc = panelview.ownerDocument; 498 499 let syncNowBtn = panelview.querySelector(".syncnow-label"); 500 let l10nId = syncNowBtn.getAttribute( 501 panelview.ownerGlobal.gSync._isCurrentlySyncing 502 ? "syncing-data-l10n-id" 503 : "sync-now-data-l10n-id" 504 ); 505 doc.l10n.setAttributes(syncNowBtn, l10nId); 506 507 let SyncedTabsPanelList = doc.defaultView.SyncedTabsPanelList; 508 panelview.syncedTabsPanelList = new SyncedTabsPanelList( 509 panelview, 510 lazy.PanelMultiView.getViewNode(doc, "PanelUI-remotetabs-deck"), 511 lazy.PanelMultiView.getViewNode(doc, "PanelUI-remotetabs-tabslist") 512 ); 513 panelview.addEventListener("command", this); 514 let syncNowButton = lazy.PanelMultiView.getViewNode( 515 aEvent.target.ownerDocument, 516 "PanelUI-remotetabs-syncnow" 517 ); 518 syncNowButton.addEventListener("mouseover", this); 519 }, 520 onViewHiding(aEvent) { 521 let panelview = aEvent.target; 522 panelview.syncedTabsPanelList.destroy(); 523 panelview.syncedTabsPanelList = null; 524 panelview.removeEventListener("command", this); 525 let syncNowButton = lazy.PanelMultiView.getViewNode( 526 aEvent.target.ownerDocument, 527 "PanelUI-remotetabs-syncnow" 528 ); 529 syncNowButton.removeEventListener("mouseover", this); 530 }, 531 handleEvent(aEvent) { 532 let button = aEvent.target; 533 let { gSync } = button.ownerGlobal; 534 switch (aEvent.type) { 535 case "mouseover": 536 gSync.refreshSyncButtonsTooltip(); 537 break; 538 case "command": { 539 switch (button.id) { 540 case "PanelUI-remotetabs-syncnow": 541 gSync.doSync(); 542 break; 543 case "PanelUI-remotetabs-view-managedevices": 544 gSync.openDevicesManagementPage("syncedtabs-menupanel"); 545 break; 546 case "PanelUI-remotetabs-tabsdisabledpane-button": 547 case "PanelUI-remotetabs-setupsync-button": 548 case "PanelUI-remotetabs-syncdisabled-button": 549 case "PanelUI-remotetabs-reauthsync-button": 550 case "PanelUI-remotetabs-unverified-button": 551 gSync.openPrefs("synced-tabs"); 552 break; 553 case "PanelUI-remotetabs-connect-device-button": 554 gSync.openConnectAnotherDevice("synced-tabs"); 555 break; 556 } 557 } 558 } 559 }, 560 }); 561 } 562 563 let preferencesButton = { 564 id: "preferences-button", 565 l10nId: "toolbar-settings-button", 566 onCommand(aEvent) { 567 let win = aEvent.target.ownerGlobal; 568 win.openPreferences(undefined); 569 }, 570 }; 571 if (AppConstants.platform == "macosx") { 572 preferencesButton.shortcutId = "key_preferencesCmdMac"; 573 } 574 CustomizableWidgets.push(preferencesButton); 575 576 if (Services.prefs.getBoolPref("privacy.panicButton.enabled")) { 577 CustomizableWidgets.push({ 578 id: "panic-button", 579 type: "view", 580 viewId: "PanelUI-panicView", 581 582 forgetButtonCalled(aEvent) { 583 let doc = aEvent.target.ownerDocument; 584 let group = doc.getElementById("PanelUI-panic-timeSpan"); 585 let itemsToClear = [ 586 "cookies", 587 "history", 588 "openWindows", 589 "formdata", 590 "sessions", 591 "cache", 592 "downloads", 593 "offlineApps", 594 ]; 595 let newWindowPrivateState = PrivateBrowsingUtils.isWindowPrivate( 596 doc.defaultView 597 ) 598 ? "private" 599 : "non-private"; 600 let promise = lazy.Sanitizer.sanitize(itemsToClear, { 601 ignoreTimespan: false, 602 range: lazy.Sanitizer.getClearRange(+group.value), 603 privateStateForNewWindow: newWindowPrivateState, 604 }); 605 promise.then(function () { 606 let otherWindow = Services.wm.getMostRecentWindow("navigator:browser"); 607 if (otherWindow.closed) { 608 console.error("Got a closed window!"); 609 } 610 if (otherWindow.PanicButtonNotifier) { 611 otherWindow.PanicButtonNotifier.notify(); 612 } else { 613 otherWindow.PanicButtonNotifierShouldNotify = true; 614 } 615 }); 616 }, 617 handleEvent(aEvent) { 618 switch (aEvent.type) { 619 case "command": 620 this.forgetButtonCalled(aEvent); 621 break; 622 } 623 }, 624 onViewShowing(aEvent) { 625 let win = aEvent.target.ownerGlobal; 626 let doc = win.document; 627 let eventBlocker = null; 628 eventBlocker = doc.l10n.translateElements([aEvent.target]); 629 630 let forgetButton = aEvent.target.querySelector( 631 "#PanelUI-panic-view-button" 632 ); 633 let group = doc.getElementById("PanelUI-panic-timeSpan"); 634 group.selectedItem = doc.getElementById("PanelUI-panic-5min"); 635 forgetButton.addEventListener("command", this); 636 637 if (eventBlocker) { 638 aEvent.detail.addBlocker(eventBlocker); 639 } 640 }, 641 onViewHiding(aEvent) { 642 let forgetButton = aEvent.target.querySelector( 643 "#PanelUI-panic-view-button" 644 ); 645 forgetButton.removeEventListener("command", this); 646 }, 647 }); 648 } 649 650 if (PrivateBrowsingUtils.enabled) { 651 CustomizableWidgets.push({ 652 id: "privatebrowsing-button", 653 l10nId: "toolbar-button-new-private-window", 654 shortcutId: "key_privatebrowsing", 655 onCommand(e) { 656 let win = e.target.ownerGlobal; 657 win.OpenBrowserWindow({ private: true }); 658 }, 659 }); 660 }