/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { AppConstants } = ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); ChromeUtils.defineESModuleGetters(this, { SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", }); var gStateObject; var gTreeData; var gTreeInitialized = false; // Page initialization window.onload = function () { let toggleTabs = document.getElementById("tabsToggle"); if (toggleTabs) { let tabList = document.getElementById("tabList"); let toggleHiddenTabs = () => { toggleTabs.classList.toggle("tabs-hidden"); tabList.hidden = toggleTabs.classList.contains("tabs-hidden"); initTreeView(); }; toggleTabs.onclick = toggleHiddenTabs; } // wire up click handlers for the radio buttons if they exist. for (let radioId of ["radioRestoreAll", "radioRestoreChoose"]) { let button = document.getElementById(radioId); if (button) { button.addEventListener("click", updateTabListVisibility); } } var tabListTree = document.getElementById("tabList"); tabListTree.addEventListener("click", onListClick); tabListTree.addEventListener("keydown", onListKeyDown); var errorCancelButton = document.getElementById("errorCancel"); // aboutSessionRestore.js is included aboutSessionRestore.xhtml // and aboutWelcomeBack.xhtml, but the latter does not have an // errorCancel button. if (errorCancelButton) { errorCancelButton.addEventListener("command", startNewSession); } var errorTryAgainButton = document.getElementById("errorTryAgain"); errorTryAgainButton.addEventListener("command", restoreSession); // the crashed session state is kept inside a textbox so that SessionStore picks it up // (for when the tab is closed or the session crashes right again) var sessionData = document.getElementById("sessionData"); if (!sessionData.value) { errorTryAgainButton.disabled = true; return; } gStateObject = JSON.parse(sessionData.value); // make sure the data is tracked to be restored in case of a subsequent crash var event = document.createEvent("UIEvents"); event.initUIEvent("input", true, true, window, 0); sessionData.dispatchEvent(event); initTreeView(); errorTryAgainButton.focus({ focusVisible: false }); }; function isTreeViewVisible() { return !document.getElementById("tabList").hidden; } async function initTreeView() { if (gTreeInitialized || !isTreeViewVisible()) { return; } var tabList = document.getElementById("tabList"); let l10nIds = []; for ( let labelIndex = 0; labelIndex < gStateObject.windows.length; labelIndex++ ) { l10nIds.push({ id: "restore-page-window-label", args: { windowNumber: labelIndex + 1 }, }); } let winLabels = await document.l10n.formatValues(l10nIds); gTreeData = []; gStateObject.windows.forEach(function (aWinData, aIx) { var winState = { label: winLabels[aIx], open: true, checked: true, ix: aIx, }; winState.tabs = aWinData.tabs.map(function (aTabData) { var entry = aTabData.entries[aTabData.index - 1] || { url: "about:blank", }; var iconURL = aTabData.image || null; // don't initiate a connection just to fetch a favicon (see bug 462863) if (/^https?:/.test(iconURL)) { iconURL = "moz-anno:favicon:" + iconURL; } return { label: entry.title || entry.url, checked: true, src: iconURL, parent: winState, }; }); gTreeData.push(winState); for (let tab of winState.tabs) { gTreeData.push(tab); } }, this); tabList.view = treeView; tabList.view.selection.select(0); gTreeInitialized = true; } // User actions function updateTabListVisibility() { document.getElementById("tabList").hidden = !document.getElementById("radioRestoreChoose").checked; initTreeView(); } function restoreSession() { Services.obs.notifyObservers(null, "sessionstore-initiating-manual-restore"); document.getElementById("errorTryAgain").disabled = true; if (isTreeViewVisible()) { if (!gTreeData.some(aItem => aItem.checked)) { // This should only be possible when we have no "cancel" button, and thus // the "Restore session" button always remains enabled. In that case and // when nothing is selected, we just want a new session. startNewSession(); return; } // remove all unselected tabs from the state before restoring it var ix = gStateObject.windows.length - 1; for (var t = gTreeData.length - 1; t >= 0; t--) { if (treeView.isContainer(t)) { if (gTreeData[t].checked === 0) { // this window will be restored partially gStateObject.windows[ix].tabs = gStateObject.windows[ix].tabs.filter( (aTabData, aIx) => gTreeData[t].tabs[aIx].checked ); } else if (!gTreeData[t].checked) { // this window won't be restored at all gStateObject.windows.splice(ix, 1); } ix--; } } } var stateString = JSON.stringify(gStateObject); var top = getBrowserWindow(); // if there's only this page open, reuse the window for restoring the session if (top.gBrowser.tabs.length == 1) { SessionStore.setWindowState(top, stateString, true); return; } // restore the session into a new window and close the current tab var newWindow = top.openDialog( top.location, "_blank", "chrome,dialog=no,all" ); Services.obs.addObserver(function observe(win, topic) { if (win != newWindow) { return; } Services.obs.removeObserver(observe, topic); SessionStore.setWindowState(newWindow, stateString, true); let tabbrowser = top.gBrowser; let browser = window.docShell.chromeEventHandler; let tab = tabbrowser.getTabForBrowser(browser); tabbrowser.removeTab(tab); }, "browser-delayed-startup-finished"); } function startNewSession() { if (Services.prefs.getIntPref("browser.startup.page") == 0) { getBrowserWindow().gBrowser.loadURI(Services.io.newURI("about:blank"), { triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( {} ), }); } else { getBrowserWindow().BrowserHome(); } } function onListClick(aEvent) { // don't react to right-clicks if (aEvent.button == 2) { return; } var cell = treeView.treeBox.getCellAt(aEvent.clientX, aEvent.clientY); if (cell.col) { // Restore this specific tab in the same window for middle/double/accel clicking // on a tab's title. let accelKey = AppConstants.platform == "macosx" ? aEvent.metaKey : aEvent.ctrlKey; if ( (aEvent.button == 1 || (aEvent.button == 0 && aEvent.detail == 2) || accelKey) && cell.col.id == "title" && !treeView.isContainer(cell.row) ) { restoreSingleTab(cell.row, aEvent.shiftKey); aEvent.stopPropagation(); } else if (cell.col.id == "restore") { toggleRowChecked(cell.row); } } } function onListKeyDown(aEvent) { switch (aEvent.keyCode) { case KeyEvent.DOM_VK_SPACE: toggleRowChecked(document.getElementById("tabList").currentIndex); // Prevent page from scrolling on the space key. aEvent.preventDefault(); break; case KeyEvent.DOM_VK_RETURN: var ix = document.getElementById("tabList").currentIndex; if (aEvent.ctrlKey && !treeView.isContainer(ix)) { restoreSingleTab(ix, aEvent.shiftKey); } break; } } // Helper functions function getBrowserWindow() { return window.browsingContext.topChromeWindow; } function toggleRowChecked(aIx) { function isChecked(aItem) { return aItem.checked; } var item = gTreeData[aIx]; item.checked = !item.checked; treeView.treeBox.invalidateRow(aIx); if (treeView.isContainer(aIx)) { // (un)check all tabs of this window as well for (let tab of item.tabs) { tab.checked = item.checked; treeView.treeBox.invalidateRow(gTreeData.indexOf(tab)); } } else { // Update the window's checkmark as well (0 means "partially checked"). let state = false; if (item.parent.tabs.every(isChecked)) { state = true; } else if (item.parent.tabs.some(isChecked)) { state = 0; } item.parent.checked = state; treeView.treeBox.invalidateRow(gTreeData.indexOf(item.parent)); } // we only disable the button when there's no cancel button. if (document.getElementById("errorCancel")) { document.getElementById("errorTryAgain").disabled = !gTreeData.some(isChecked); } } function restoreSingleTab(aIx, aShifted) { var tabbrowser = getBrowserWindow().gBrowser; var newTab = tabbrowser.addWebTab(); var item = gTreeData[aIx]; var tabState = gStateObject.windows[item.parent.ix].tabs[ aIx - gTreeData.indexOf(item.parent) - 1 ]; // ensure tab would be visible on the tabstrip. tabState.hidden = false; SessionStore.setTabState(newTab, JSON.stringify(tabState)); // respect the preference as to whether to select the tab (the Shift key inverses) if ( Services.prefs.getBoolPref("browser.tabs.loadInBackground") != !aShifted ) { tabbrowser.selectedTab = newTab; } } // Tree controller var treeView = { treeBox: null, selection: null, get rowCount() { return gTreeData.length; }, setTree(treeBox) { this.treeBox = treeBox; }, getCellText(idx, column) { return gTreeData[idx].label; }, isContainer(idx) { return "open" in gTreeData[idx]; }, getCellValue(idx, column) { return gTreeData[idx].checked; }, isContainerOpen(idx) { return gTreeData[idx].open; }, isContainerEmpty(idx) { return false; }, isSeparator(idx) { return false; }, isSorted() { return false; }, isEditable(idx, column) { return false; }, canDrop(idx, orientation, dt) { return false; }, getLevel(idx) { return this.isContainer(idx) ? 0 : 1; }, getParentIndex(idx) { if (!this.isContainer(idx)) { for (var t = idx - 1; t >= 0; t--) { if (this.isContainer(t)) { return t; } } } return -1; }, hasNextSibling(idx, after) { var thisLevel = this.getLevel(idx); for (var t = after + 1; t < gTreeData.length; t++) { if (this.getLevel(t) <= thisLevel) { return this.getLevel(t) == thisLevel; } } return false; }, toggleOpenState(idx) { if (!this.isContainer(idx)) { return; } var item = gTreeData[idx]; if (item.open) { // remove this window's tab rows from the view var thisLevel = this.getLevel(idx); /* eslint-disable no-empty */ for ( var t = idx + 1; t < gTreeData.length && this.getLevel(t) > thisLevel; t++ ) {} /* eslint-disable no-empty */ var deletecount = t - idx - 1; gTreeData.splice(idx + 1, deletecount); this.treeBox.rowCountChanged(idx + 1, -deletecount); } else { // add this window's tab rows to the view var toinsert = gTreeData[idx].tabs; for (var i = 0; i < toinsert.length; i++) { gTreeData.splice(idx + i + 1, 0, toinsert[i]); } this.treeBox.rowCountChanged(idx + 1, toinsert.length); } item.open = !item.open; this.treeBox.invalidateRow(idx); }, getCellProperties(idx, column) { if ( column.id == "restore" && this.isContainer(idx) && gTreeData[idx].checked === 0 ) { return "partial"; } if (column.id == "title") { return this.getImageSrc(idx, column) ? "icon" : "noicon"; } return ""; }, getRowProperties(idx) { var winState = gTreeData[idx].parent || gTreeData[idx]; if (winState.ix % 2 != 0) { return "alternate"; } return ""; }, getImageSrc(idx, column) { if (column.id == "title") { return gTreeData[idx].src || null; } return null; }, cycleHeader(column) {}, cycleCell(idx, column) {}, selectionChanged() {}, getColumnProperties(column) { return ""; }, };