RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs (19621B)
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 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 PlacesUIUtils: "moz-src:///browser/components/places/PlacesUIUtils.sys.mjs", 11 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 12 SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", 13 SessionWindowUI: "resource:///modules/sessionstore/SessionWindowUI.sys.mjs", 14 }); 15 16 ChromeUtils.defineLazyGetter(lazy, "l10n", () => { 17 return new Localization(["browser/recentlyClosed.ftl"], true); 18 }); 19 20 XPCOMUtils.defineLazyPreferenceGetter( 21 lazy, 22 "closedTabsFromAllWindowsEnabled", 23 "browser.sessionstore.closedTabsFromAllWindows" 24 ); 25 26 XPCOMUtils.defineLazyPreferenceGetter( 27 lazy, 28 "closedTabsFromClosedWindowsEnabled", 29 "browser.sessionstore.closedTabsFromClosedWindows" 30 ); 31 32 /** 33 * @returns {Map<string, TabGroupStateData>} 34 * Map of closed tab groups keyed by tab group ID 35 */ 36 function getClosedTabGroupsById() { 37 const closedTabGroups = lazy.SessionStore.getClosedTabGroups(); 38 const closedTabGroupsById = new Map(); 39 closedTabGroups.forEach(tabGroup => 40 closedTabGroupsById.set(tabGroup.id, tabGroup) 41 ); 42 return closedTabGroupsById; 43 } 44 45 export var RecentlyClosedTabsAndWindowsMenuUtils = { 46 /** 47 * Builds up a document fragment of UI items for the recently closed tabs. 48 * 49 * @param {Window} aWindow 50 * The window that the tabs were closed in. 51 * @param {"menuitem"|"toolbarbutton"} aTagName 52 * The tag name that will be used when creating the UI items. 53 * @returns {DocumentFragment} A document fragment with UI items for each recently closed tab. 54 */ 55 getTabsFragment(aWindow, aTagName) { 56 let doc = aWindow.document; 57 const isPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(aWindow); 58 const fragment = doc.createDocumentFragment(); 59 let isEmpty = true; 60 61 if ( 62 lazy.SessionStore.getClosedTabCount({ 63 sourceWindow: aWindow, 64 }) 65 ) { 66 isEmpty = false; 67 68 const browserWindows = lazy.closedTabsFromAllWindowsEnabled 69 ? lazy.SessionStore.getWindows(aWindow) 70 : [aWindow]; 71 const closedTabSets = []; 72 for (const win of browserWindows) { 73 closedTabSets.push(lazy.SessionStore.getClosedTabDataForWindow(win)); 74 } 75 76 if ( 77 !isPrivate && 78 lazy.closedTabsFromClosedWindowsEnabled && 79 lazy.SessionStore.getClosedTabCountFromClosedWindows() 80 ) { 81 closedTabSets.push( 82 lazy.SessionStore.getClosedTabDataFromClosedWindows() 83 ); 84 } 85 86 const closedTabGroupsById = getClosedTabGroupsById(); 87 88 let currentGroupId = null; 89 90 closedTabSets.forEach(tabSet => { 91 tabSet.forEach((tab, index) => { 92 let groupId = tab.closedInTabGroupId; 93 if (groupId && closedTabGroupsById.has(groupId)) { 94 if (groupId != currentGroupId) { 95 // This is the first tab in a new group. Push all the tabs into the menu. 96 // Note that the calls to the createTabGroup methods below use the 97 // tab itself as a closed data source, since it will always contain 98 // one of either sourceClosedId or sourceWindowId. 99 if (aTagName == "menuitem") { 100 createTabGroupSubmenu( 101 closedTabGroupsById.get(groupId), 102 index, 103 tab, 104 doc, 105 fragment 106 ); 107 } else { 108 createTabGroupSubpanel( 109 closedTabGroupsById.get(groupId), 110 index, 111 tab, 112 doc, 113 fragment 114 ); 115 } 116 117 currentGroupId = groupId; 118 } else { 119 // We have already seen this group. Ignore. 120 } 121 } else { 122 createEntry(aTagName, false, index, tab, doc, tab.title, fragment); 123 currentGroupId = null; 124 } 125 }); 126 }); 127 } 128 129 if (!isEmpty) { 130 createRestoreAllEntry( 131 doc, 132 fragment, 133 false, 134 aTagName == "menuitem" 135 ? "recently-closed-menu-reopen-all-tabs" 136 : "recently-closed-panel-reopen-all-tabs", 137 aTagName 138 ); 139 } 140 return fragment; 141 }, 142 143 /** 144 * Builds up a document fragment of UI items for the recently closed windows. 145 * 146 * @param {Window} aWindow 147 * A window that can be used to create the elements and document fragment. 148 * @param {"menuitem"|"toolbarbutton"} aTagName 149 * The tag name that will be used when creating the UI items. 150 * @returns {DocumentFragment} A document fragment with UI items for each recently closed window. 151 */ 152 getWindowsFragment(aWindow, aTagName) { 153 let closedWindowData = lazy.SessionStore.getClosedWindowData(); 154 let doc = aWindow.document; 155 let fragment = doc.createDocumentFragment(); 156 if (closedWindowData.length) { 157 for (let i = 0; i < closedWindowData.length; i++) { 158 const { selected, tabs, title } = closedWindowData[i]; 159 const selectedTab = tabs[selected - 1]; 160 if (selectedTab) { 161 const menuLabel = lazy.l10n.formatValueSync( 162 "recently-closed-undo-close-window-label", 163 { tabCount: tabs.length - 1, winTitle: title } 164 ); 165 createEntry(aTagName, true, i, selectedTab, doc, menuLabel, fragment); 166 } 167 } 168 169 createRestoreAllEntry( 170 doc, 171 fragment, 172 true, 173 aTagName == "menuitem" 174 ? "recently-closed-menu-reopen-all-windows" 175 : "recently-closed-panel-reopen-all-windows", 176 aTagName 177 ); 178 } 179 return fragment; 180 }, 181 182 /** 183 * Handle a command event to re-open all closed tabs 184 * 185 * @param aEvent 186 * The command event when the user clicks the restore all menu item 187 */ 188 onRestoreAllTabsCommand(aEvent) { 189 const currentWindow = aEvent.target.ownerGlobal; 190 const browserWindows = lazy.closedTabsFromAllWindowsEnabled 191 ? lazy.SessionStore.getWindows(currentWindow) 192 : [currentWindow]; 193 const closedTabGroupsById = getClosedTabGroupsById(); 194 195 const undoAllInTabData = function (tabData, tabMethod, tabGroupMethod) { 196 while (tabData.length) { 197 let currentTabGroupId = tabData[0].state.groupId; 198 199 if (currentTabGroupId && closedTabGroupsById.has(currentTabGroupId)) { 200 let currentTabGroup = closedTabGroupsById.get(currentTabGroupId); 201 let splicedTabs = tabData.splice(0, currentTabGroup.tabs.length); 202 tabGroupMethod(splicedTabs); 203 } else { 204 let splicedTabs = tabData.splice(0, 1); 205 tabMethod(splicedTabs[0]); 206 } 207 } 208 }; 209 210 for (const sourceWindow of browserWindows) { 211 let tabData = lazy.SessionStore.getClosedTabDataForWindow(sourceWindow); 212 213 undoAllInTabData( 214 tabData, 215 _tabs => { 216 lazy.SessionStore.undoCloseTab(sourceWindow, 0, currentWindow); 217 }, 218 tabs => { 219 lazy.SessionStore.undoCloseTabGroup( 220 sourceWindow, 221 tabs[0].state.groupId, 222 currentWindow 223 ); 224 } 225 ); 226 } 227 if (lazy.closedTabsFromClosedWindowsEnabled) { 228 let tabData = lazy.SessionStore.getClosedTabDataFromClosedWindows(); 229 230 undoAllInTabData( 231 tabData, 232 tab => { 233 lazy.SessionStore.undoCloseTabFromClosedWindow( 234 { sourceClosedId: tab.sourceClosedId }, 235 tab.closedId, 236 currentWindow 237 ); 238 }, 239 tabs => { 240 lazy.SessionStore.undoCloseTabGroup( 241 { sourceClosedId: tabs[0].sourceClosedId }, 242 tabs[0].state.groupId, 243 currentWindow 244 ); 245 } 246 ); 247 } 248 }, 249 250 /** 251 * Handle a command event to re-open all closed windows 252 * 253 * @param aEvent 254 * The command event when the user clicks the restore all menu item 255 */ 256 onRestoreAllWindowsCommand() { 257 const closedData = lazy.SessionStore.getClosedWindowData(); 258 for (const { closedId } of closedData) { 259 lazy.SessionStore.undoCloseById(closedId); 260 } 261 }, 262 263 /** 264 * Re-open a closed tab and put it to the end of the tab strip. 265 * Used for a middle click. 266 * 267 * @param aEvent 268 * The event when the user clicks the menu item 269 */ 270 _undoCloseMiddleClick(aEvent) { 271 if (aEvent.button != 1) { 272 return; 273 } 274 if (aEvent.originalTarget.hasAttribute("source-closed-id")) { 275 lazy.SessionStore.undoClosedTabFromClosedWindow( 276 { 277 sourceClosedId: 278 aEvent.originalTarget.getAttribute("source-closed-id"), 279 }, 280 aEvent.originalTarget.getAttribute("value") 281 ); 282 } else { 283 lazy.SessionWindowUI.undoCloseTab( 284 aEvent.view, 285 aEvent.originalTarget.getAttribute("value"), 286 aEvent.originalTarget.getAttribute("source-window-id") 287 ); 288 } 289 aEvent.view.gBrowser.moveTabToEnd(); 290 let ancestorPanel = aEvent.target.closest("panel"); 291 if (ancestorPanel) { 292 ancestorPanel.hidePopup(); 293 } 294 }, 295 }; 296 297 /** 298 * @param {Element} element 299 * @param {TabGroupStateData} tabGroup 300 */ 301 function setTabGroupColorProperties(element, tabGroup) { 302 element.style.setProperty( 303 "--tab-group-color", 304 `var(--tab-group-color-${tabGroup.color})` 305 ); 306 element.style.setProperty( 307 "--tab-group-color-invert", 308 `var(--tab-group-color-${tabGroup.color}-invert)` 309 ); 310 element.style.setProperty( 311 "--tab-group-color-pale", 312 `var(--tab-group-color-${tabGroup.color}-pale)` 313 ); 314 } 315 316 /** 317 * Creates a `menuitem` for the tab group that will expand to a newly 318 * created submenu of the tab group's tab contents when selected. 319 * 320 * @param {TabGroupStateData} aTabGroup 321 * Session store state for the closed tab group. 322 * @param {number} aIndex 323 * The index of the first tab in the tab group, relative to the tab strip. 324 * @param {{sourceClosedId: number}|{sourceWindowId: string}} aSource 325 * An object that can be resolved to a closed data source. 326 * @param {Document} aDocument 327 * A document object that can be used to create the entry. 328 * @param {DocumentFragment} aFragment 329 * The DOM fragment that the created entry will be in. 330 */ 331 function createTabGroupSubmenu( 332 aTabGroup, 333 aIndex, 334 aSource, 335 aDocument, 336 aFragment 337 ) { 338 let element = aDocument.createXULElement("menu"); 339 if (aTabGroup.name) { 340 element.setAttribute("label", aTabGroup.name); 341 } else { 342 aDocument.l10n.setAttributes(element, "tab-context-unnamed-group"); 343 } 344 345 element.classList.add("menu-iconic", "tab-group-icon"); 346 setTabGroupColorProperties(element, aTabGroup); 347 348 let menuPopup = aDocument.createXULElement("menupopup"); 349 350 aTabGroup.tabs.forEach(tab => { 351 createEntry( 352 "menuitem", 353 false, 354 aIndex, 355 tab, 356 aDocument, 357 tab.title, 358 menuPopup 359 ); 360 aIndex++; 361 }); 362 363 menuPopup.appendChild(aDocument.createXULElement("menuseparator")); 364 365 let reopenTabGroupItem = aDocument.createXULElement("menuitem"); 366 aDocument.l10n.setAttributes( 367 reopenTabGroupItem, 368 "tab-context-reopen-tab-group" 369 ); 370 reopenTabGroupItem.addEventListener("command", () => { 371 lazy.SessionStore.undoCloseTabGroup(aSource, aTabGroup.id); 372 }); 373 menuPopup.appendChild(reopenTabGroupItem); 374 375 element.appendChild(menuPopup); 376 aFragment.appendChild(element); 377 } 378 379 /** 380 * Creates a `toolbarbutton` for the tab group that will navigate to a newly 381 * created subpanel of the tab group's tab contents when selected. 382 * 383 * @param {TabGroupStateData} aTabGroup 384 * Session store state for the closed tab group. 385 * @param {number} aIndex 386 * The index of the first tab in the tab group, relative to the tab strip. 387 * @param {{sourceClosedId: number}|{sourceWindowId: string}} aSource 388 * An object that can be resolved to a closed data source. 389 * @param {Document} aDocument 390 * A document object that can be used to create the entry. 391 * @param {DocumentFragment} aFragment 392 * The DOM fragment that the created entry will be in. 393 */ 394 function createTabGroupSubpanel( 395 aTabGroup, 396 aIndex, 397 aSource, 398 aDocument, 399 aFragment 400 ) { 401 let element = aDocument.createXULElement("toolbarbutton"); 402 if (aTabGroup.name) { 403 element.setAttribute("label", aTabGroup.name); 404 } else { 405 aDocument.l10n.setAttributes(element, "tab-context-unnamed-group"); 406 } 407 408 element.classList.add( 409 "subviewbutton", 410 "subviewbutton-iconic", 411 "subviewbutton-nav", 412 "tab-group-icon" 413 ); 414 element.setAttribute("closemenu", "none"); 415 setTabGroupColorProperties(element, aTabGroup); 416 417 const panelviewId = `closed-tabs-tab-group-${aTabGroup.id}`; 418 let panelview = aDocument.getElementById(panelviewId); 419 420 if (panelview) { 421 // panelviews get moved around the DOM by PanelMultiView, so if it still 422 // exists, remove it so we can rebuild a new panelview 423 panelview.remove(); 424 } 425 426 panelview = aDocument.createXULElement("panelview"); 427 panelview.id = panelviewId; 428 let panelBody = aDocument.createXULElement("vbox"); 429 panelBody.className = "panel-subview-body"; 430 431 aTabGroup.tabs.forEach(tab => { 432 createEntry( 433 "toolbarbutton", 434 false, 435 aIndex, 436 tab, 437 aDocument, 438 tab.title, 439 panelBody 440 ); 441 aIndex++; 442 }); 443 444 panelview.appendChild(panelBody); 445 panelview.appendChild(aDocument.createXULElement("toolbarseparator")); 446 447 let reopenTabGroupItem = aDocument.createXULElement("toolbarbutton"); 448 aDocument.l10n.setAttributes( 449 reopenTabGroupItem, 450 "tab-context-reopen-tab-group" 451 ); 452 reopenTabGroupItem.classList.add( 453 "reopentabgroupitem", 454 "subviewbutton", 455 "panel-subview-footer-button" 456 ); 457 reopenTabGroupItem.addEventListener("command", () => { 458 lazy.SessionStore.undoCloseTabGroup(aSource, aTabGroup.id); 459 }); 460 461 panelview.appendChild(reopenTabGroupItem); 462 463 element.addEventListener("command", () => { 464 aDocument.ownerGlobal.PanelUI.showSubView(panelview.id, element); 465 }); 466 467 aFragment.appendChild(panelview); 468 aFragment.appendChild(element); 469 } 470 471 /** 472 * Create a UI entry for a recently closed tab, tab group, or window. 473 * 474 * @param {"menuitem"|"toolbarbutton"} aTagName 475 * the tag name that will be used when creating the UI entry 476 * @param {boolean} aIsWindowsFragment 477 * whether or not this entry will represent a closed window 478 * @param {number} aIndex 479 * the index of the closed tab 480 * @param {TabStateData} aClosedTab 481 * the closed tab 482 * @param {Document} aDocument 483 * a document that can be used to create the entry 484 * @param {string} aMenuLabel 485 * the label the created entry will have 486 * @param {DocumentFragment} aFragment 487 * the fragment the created entry will be in 488 */ 489 function createEntry( 490 aTagName, 491 aIsWindowsFragment, 492 aIndex, 493 aClosedTab, 494 aDocument, 495 aMenuLabel, 496 aFragment 497 ) { 498 let element = aDocument.createXULElement(aTagName); 499 500 element.setAttribute("label", aMenuLabel); 501 if (aClosedTab.image) { 502 const iconURL = lazy.PlacesUIUtils.getImageURL(aClosedTab.image); 503 element.setAttribute("image", ChromeUtils.encodeURIForSrcset(iconURL)); 504 } 505 506 if (aIsWindowsFragment) { 507 element.addEventListener("command", () => 508 lazy.SessionWindowUI.undoCloseWindow(aIndex) 509 ); 510 } else if (typeof aClosedTab.sourceClosedId == "number") { 511 // sourceClosedId is used to look up the closed window to remove it when the tab is restored 512 let sourceClosedId = aClosedTab.sourceClosedId; 513 element.setAttribute("source-closed-id", sourceClosedId); 514 element.setAttribute("value", aClosedTab.closedId); 515 element.addEventListener( 516 "command", 517 () => { 518 lazy.SessionStore.undoClosedTabFromClosedWindow( 519 { sourceClosedId }, 520 aClosedTab.closedId 521 ); 522 }, 523 { once: true } 524 ); 525 } else { 526 // sourceWindowId is used to look up the closed tab entry to remove it when it is restored 527 let sourceWindowId = aClosedTab.sourceWindowId; 528 element.setAttribute("value", aIndex); 529 element.setAttribute("source-window-id", sourceWindowId); 530 element.addEventListener("command", event => 531 lazy.SessionWindowUI.undoCloseTab( 532 event.target.ownerGlobal, 533 aIndex, 534 sourceWindowId 535 ) 536 ); 537 } 538 539 if (aTagName == "menuitem") { 540 element.setAttribute( 541 "class", 542 "menuitem-iconic bookmark-item menuitem-with-favicon" 543 ); 544 } else if (aTagName == "toolbarbutton") { 545 element.setAttribute( 546 "class", 547 "subviewbutton subviewbutton-iconic bookmark-item" 548 ); 549 } 550 551 // Set the targetURI attribute so it will be shown in tooltip. 552 // SessionStore uses one-based indexes, so we need to normalize them. 553 let tabData; 554 tabData = aIsWindowsFragment ? aClosedTab : aClosedTab.state; 555 let activeIndex = (tabData.index || tabData.entries.length) - 1; 556 if (activeIndex >= 0 && tabData.entries[activeIndex]) { 557 element.setAttribute("targetURI", tabData.entries[activeIndex].url); 558 } 559 560 // Windows don't open in new tabs and menuitems dispatch command events on 561 // middle click, so we only need to manually handle middle clicks for 562 // toolbarbuttons. 563 if (!aIsWindowsFragment && aTagName != "menuitem") { 564 element.addEventListener( 565 "click", 566 RecentlyClosedTabsAndWindowsMenuUtils._undoCloseMiddleClick 567 ); 568 } 569 570 if (aIndex == 0) { 571 element.setAttribute( 572 "key", 573 aIsWindowsFragment 574 ? "key_undoCloseWindow" 575 : "key_restoreLastClosedTabOrWindowOrSession" 576 ); 577 } 578 579 aFragment.appendChild(element); 580 } 581 582 /** 583 * Create an entry to restore all closed windows or tabs. 584 * For menus, adds a menu separator and a menu item. 585 * For toolbar panels, adds a toolbar button only and expects 586 * CustomizableWidgets.sys.mjs to add its own separator elsewhere in the DOM 587 * 588 * @param {Document} aDocument 589 * a document that can be used to create the entry 590 * @param {DocumentFragment} aFragment 591 * the fragment the created entry will be in 592 * @param {boolean} aIsWindowsFragment 593 * whether or not this entry will represent a closed window 594 * @param {string} aRestoreAllLabel 595 * which localizable string to use for the entry 596 * @param {"menuitem"|"toolbarbutton"} aTagName 597 * the tag name that will be used when creating the UI entry 598 */ 599 function createRestoreAllEntry( 600 aDocument, 601 aFragment, 602 aIsWindowsFragment, 603 aRestoreAllLabel, 604 aTagName 605 ) { 606 let restoreAllElements = aDocument.createXULElement(aTagName); 607 restoreAllElements.classList.add("restoreallitem"); 608 609 if (aTagName == "toolbarbutton") { 610 restoreAllElements.classList.add( 611 "subviewbutton", 612 "panel-subview-footer-button" 613 ); 614 } 615 616 // We cannot use aDocument.l10n.setAttributes because the menubar label is not 617 // updated in time and displays a blank string (see Bug 1691553). 618 restoreAllElements.setAttribute( 619 "label", 620 lazy.l10n.formatValueSync(aRestoreAllLabel) 621 ); 622 623 restoreAllElements.addEventListener( 624 "command", 625 aIsWindowsFragment 626 ? RecentlyClosedTabsAndWindowsMenuUtils.onRestoreAllWindowsCommand 627 : RecentlyClosedTabsAndWindowsMenuUtils.onRestoreAllTabsCommand 628 ); 629 630 if (aTagName == "menuitem") { 631 aFragment.appendChild(aDocument.createXULElement("menuseparator")); 632 } 633 634 aFragment.appendChild(restoreAllElements); 635 }