diff options
Diffstat (limited to 'browser/components/sessionstore/content')
3 files changed, 542 insertions, 0 deletions
diff --git a/browser/components/sessionstore/content/aboutSessionRestore.js b/browser/components/sessionstore/content/aboutSessionRestore.js new file mode 100644 index 0000000000..321bb9d0db --- /dev/null +++ b/browser/components/sessionstore/content/aboutSessionRestore.js @@ -0,0 +1,460 @@ +/* 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; + } + + // pages used by this script may have a link that needs to be updated to + // the in-product link. + let anchor = document.getElementById("linkMoreTroubleshooting"); + if (anchor) { + let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL"); + anchor.setAttribute("href", baseURL + "troubleshooting"); + } + + // 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("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 ""; + }, +}; diff --git a/browser/components/sessionstore/content/aboutSessionRestore.xhtml b/browser/components/sessionstore/content/aboutSessionRestore.xhtml new file mode 100644 index 0000000000..05538be5d9 --- /dev/null +++ b/browser/components/sessionstore/content/aboutSessionRestore.xhtml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +# 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/. +--> +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> + %htmlDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <head> + <meta http-equiv="Content-Security-Policy" content="default-src chrome: resource:; img-src chrome: resource: data:; object-src 'none'" /> + <meta name="color-scheme" content="light dark"/> + <title data-l10n-id="restore-page-tab-title"></title> + <link rel="stylesheet" href="chrome://global/skin/in-content/info-pages.css" type="text/css" media="all"/> + <link rel="stylesheet" href="chrome://browser/skin/aboutSessionRestore.css" type="text/css" media="all"/> + <link rel="icon" href="chrome://global/skin/icons/info.svg"/> + <link rel="localization" href="browser/aboutSessionRestore.ftl"/> + <link rel="localization" href="branding/brand.ftl"/> + <script src="chrome://browser/content/aboutSessionRestore.js"/> + </head> + + <body> + + <div class="container tab-list-tree-container"> + <div class="description-wrapper"> + <div class="title"> + <h1 class="title-text" data-l10n-id="restore-page-error-title"></h1> + </div> + <div class="description"> + <p data-l10n-id="restore-page-problem-desc"></p> + <p data-l10n-id="restore-page-try-this"></p> + </div> + <button id="tabsToggle" class="tabs-hidden"> + <span id="showTabs" data-l10n-id="restore-page-show-tabs"></span> + <span id="hideTabs" data-l10n-id="restore-page-hide-tabs"></span> + </button> + </div> + <xul:tree id="tabList" seltype="single" hidecolumnpicker="true" hidden="true"> + <xul:treecols> + <xul:treecol cycler="true" id="restore" type="checkbox" data-l10n-id="restore-page-restore-header"/> + <xul:splitter class="tree-splitter"/> + <xul:treecol primary="true" id="title" data-l10n-id="restore-page-list-header" flex="1"/> + </xul:treecols> + <xul:treechildren flex="1"/> + </xul:tree> + <div class="button-container"> +#ifdef XP_UNIX + <xul:button id="errorCancel" + data-l10n-id="restore-page-close-button"/> + <xul:button class="primary" + id="errorTryAgain" + data-l10n-id="restore-page-try-again-button"/> +#else + <xul:button class="primary" + id="errorTryAgain" + data-l10n-id="restore-page-try-again-button"/> + <xul:button id="errorCancel" + data-l10n-id="restore-page-close-button"/> +#endif + </div> + <!-- holds the session data for when the tab is closed --> + <input type="text" id="sessionData" hidden="true"/> + </div> + + </body> +</html> diff --git a/browser/components/sessionstore/content/content-sessionStore.js b/browser/components/sessionstore/content/content-sessionStore.js new file mode 100644 index 0000000000..a4bdea0bdc --- /dev/null +++ b/browser/components/sessionstore/content/content-sessionStore.js @@ -0,0 +1,13 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +"use strict"; + +const { ContentSessionStore } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/ContentSessionStore.sys.mjs" +); + +void new ContentSessionStore(this); |