forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			447 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			447 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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, {
 | |
|   PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs",
 | |
|   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",
 | |
|       };
 | |
|       // don't initiate a connection just to fetch a favicon (see bug 462863)
 | |
|       return {
 | |
|         label: entry.title || entry.url,
 | |
|         checked: true,
 | |
|         src: PlacesUIUtils.getImageURL(aTabData.image),
 | |
|         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 "";
 | |
|   },
 | |
| };
 | 
