aboutSessionRestore.js (12228B)
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 "use strict"; 6 7 const { AppConstants } = ChromeUtils.importESModule( 8 "resource://gre/modules/AppConstants.sys.mjs" 9 ); 10 ChromeUtils.defineESModuleGetters(this, { 11 PlacesUIUtils: "moz-src:///browser/components/places/PlacesUIUtils.sys.mjs", 12 SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", 13 }); 14 15 var gStateObject; 16 var gTreeData; 17 var gTreeInitialized = false; 18 19 // Page initialization 20 21 window.onload = function () { 22 let toggleTabs = document.getElementById("tabsToggle"); 23 if (toggleTabs) { 24 let tabList = document.getElementById("tabList"); 25 26 let toggleHiddenTabs = () => { 27 toggleTabs.classList.toggle("tabs-hidden"); 28 tabList.hidden = toggleTabs.classList.contains("tabs-hidden"); 29 initTreeView(); 30 }; 31 toggleTabs.onclick = toggleHiddenTabs; 32 } 33 34 // wire up click handlers for the radio buttons if they exist. 35 for (let radioId of ["radioRestoreAll", "radioRestoreChoose"]) { 36 let button = document.getElementById(radioId); 37 if (button) { 38 button.addEventListener("click", updateTabListVisibility); 39 } 40 } 41 42 var tabListTree = document.getElementById("tabList"); 43 tabListTree.addEventListener("click", onListClick); 44 tabListTree.addEventListener("keydown", onListKeyDown); 45 46 var errorCancelButton = document.getElementById("errorCancel"); 47 // aboutSessionRestore.js is included aboutSessionRestore.xhtml 48 // and aboutWelcomeBack.xhtml, but the latter does not have an 49 // errorCancel button. 50 if (errorCancelButton) { 51 errorCancelButton.addEventListener("command", startNewSession); 52 } 53 54 var errorTryAgainButton = document.getElementById("errorTryAgain"); 55 errorTryAgainButton.addEventListener("command", restoreSession); 56 57 // the crashed session state is kept inside a textbox so that SessionStore picks it up 58 // (for when the tab is closed or the session crashes right again) 59 var sessionData = document.getElementById("sessionData"); 60 if (!sessionData.value) { 61 errorTryAgainButton.disabled = true; 62 return; 63 } 64 65 gStateObject = JSON.parse(sessionData.value); 66 67 // make sure the data is tracked to be restored in case of a subsequent crash 68 var event = document.createEvent("UIEvents"); 69 event.initUIEvent("input", true, true, window, 0); 70 sessionData.dispatchEvent(event); 71 72 initTreeView(); 73 74 errorTryAgainButton.focus({ focusVisible: false }); 75 }; 76 77 function isTreeViewVisible() { 78 return !document.getElementById("tabList").hidden; 79 } 80 81 async function initTreeView() { 82 if (gTreeInitialized || !isTreeViewVisible()) { 83 return; 84 } 85 86 var tabList = document.getElementById("tabList"); 87 let l10nIds = []; 88 for ( 89 let labelIndex = 0; 90 labelIndex < gStateObject.windows.length; 91 labelIndex++ 92 ) { 93 l10nIds.push({ 94 id: "restore-page-window-label", 95 args: { windowNumber: labelIndex + 1 }, 96 }); 97 } 98 let winLabels = await document.l10n.formatValues(l10nIds); 99 gTreeData = []; 100 gStateObject.windows.forEach(function (aWinData, aIx) { 101 var winState = { 102 label: winLabels[aIx], 103 open: true, 104 checked: true, 105 ix: aIx, 106 }; 107 winState.tabs = aWinData.tabs.map(function (aTabData) { 108 var entry = aTabData.entries[aTabData.index - 1] || { 109 url: "about:blank", 110 }; 111 // don't initiate a connection just to fetch a favicon (see bug 462863) 112 return { 113 label: entry.title || entry.url, 114 checked: true, 115 src: PlacesUIUtils.getImageURL(aTabData.image), 116 parent: winState, 117 }; 118 }); 119 gTreeData.push(winState); 120 for (let tab of winState.tabs) { 121 gTreeData.push(tab); 122 } 123 }, this); 124 125 tabList.view = treeView; 126 tabList.view.selection.select(0); 127 gTreeInitialized = true; 128 } 129 130 // User actions 131 function updateTabListVisibility() { 132 document.getElementById("tabList").hidden = 133 !document.getElementById("radioRestoreChoose").checked; 134 initTreeView(); 135 } 136 137 function restoreSession() { 138 Services.obs.notifyObservers(null, "sessionstore-initiating-manual-restore"); 139 document.getElementById("errorTryAgain").disabled = true; 140 141 if (isTreeViewVisible()) { 142 if (!gTreeData.some(aItem => aItem.checked)) { 143 // This should only be possible when we have no "cancel" button, and thus 144 // the "Restore session" button always remains enabled. In that case and 145 // when nothing is selected, we just want a new session. 146 startNewSession(); 147 return; 148 } 149 150 // remove all unselected tabs from the state before restoring it 151 var ix = gStateObject.windows.length - 1; 152 for (var t = gTreeData.length - 1; t >= 0; t--) { 153 if (treeView.isContainer(t)) { 154 if (gTreeData[t].checked === 0) { 155 // this window will be restored partially 156 gStateObject.windows[ix].tabs = gStateObject.windows[ix].tabs.filter( 157 (aTabData, aIx) => gTreeData[t].tabs[aIx].checked 158 ); 159 } else if (!gTreeData[t].checked) { 160 // this window won't be restored at all 161 gStateObject.windows.splice(ix, 1); 162 } 163 ix--; 164 } 165 } 166 } 167 var stateString = JSON.stringify(gStateObject); 168 169 var top = getBrowserWindow(); 170 171 // if there's only this page open, reuse the window for restoring the session 172 if (top.gBrowser.tabs.length == 1) { 173 SessionStore.setWindowState(top, stateString, true); 174 return; 175 } 176 177 // restore the session into a new window and close the current tab 178 var newWindow = top.openDialog( 179 top.location, 180 "_blank", 181 "chrome,dialog=no,all" 182 ); 183 184 Services.obs.addObserver(function observe(win, topic) { 185 if (win != newWindow) { 186 return; 187 } 188 189 Services.obs.removeObserver(observe, topic); 190 SessionStore.setWindowState(newWindow, stateString, true); 191 192 let tabbrowser = top.gBrowser; 193 let browser = window.docShell.chromeEventHandler; 194 let tab = tabbrowser.getTabForBrowser(browser); 195 tabbrowser.removeTab(tab); 196 }, "browser-delayed-startup-finished"); 197 } 198 199 function startNewSession() { 200 if (Services.prefs.getIntPref("browser.startup.page") == 0) { 201 getBrowserWindow().gBrowser.loadURI(Services.io.newURI("about:blank"), { 202 triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( 203 {} 204 ), 205 }); 206 } else { 207 getBrowserWindow().BrowserCommands.home(); 208 } 209 } 210 211 function onListClick(aEvent) { 212 // don't react to right-clicks 213 if (aEvent.button == 2) { 214 return; 215 } 216 217 var cell = treeView.treeBox.getCellAt(aEvent.clientX, aEvent.clientY); 218 if (cell.col) { 219 // Restore this specific tab in the same window for middle/double/accel clicking 220 // on a tab's title. 221 let accelKey = 222 AppConstants.platform == "macosx" ? aEvent.metaKey : aEvent.ctrlKey; 223 if ( 224 (aEvent.button == 1 || 225 (aEvent.button == 0 && aEvent.detail == 2) || 226 accelKey) && 227 cell.col.id == "title" && 228 !treeView.isContainer(cell.row) 229 ) { 230 restoreSingleTab(cell.row, aEvent.shiftKey); 231 aEvent.stopPropagation(); 232 } else if (cell.col.id == "restore") { 233 toggleRowChecked(cell.row); 234 } 235 } 236 } 237 238 function onListKeyDown(aEvent) { 239 switch (aEvent.keyCode) { 240 case KeyEvent.DOM_VK_SPACE: 241 toggleRowChecked(document.getElementById("tabList").currentIndex); 242 // Prevent page from scrolling on the space key. 243 aEvent.preventDefault(); 244 break; 245 case KeyEvent.DOM_VK_RETURN: 246 var ix = document.getElementById("tabList").currentIndex; 247 if (aEvent.ctrlKey && !treeView.isContainer(ix)) { 248 restoreSingleTab(ix, aEvent.shiftKey); 249 } 250 break; 251 } 252 } 253 254 // Helper functions 255 256 function getBrowserWindow() { 257 return window.browsingContext.topChromeWindow; 258 } 259 260 function toggleRowChecked(aIx) { 261 function isChecked(aItem) { 262 return aItem.checked; 263 } 264 265 var item = gTreeData[aIx]; 266 item.checked = !item.checked; 267 treeView.treeBox.invalidateRow(aIx); 268 269 if (treeView.isContainer(aIx)) { 270 // (un)check all tabs of this window as well 271 for (let tab of item.tabs) { 272 tab.checked = item.checked; 273 treeView.treeBox.invalidateRow(gTreeData.indexOf(tab)); 274 } 275 } else { 276 // Update the window's checkmark as well (0 means "partially checked"). 277 let state = false; 278 if (item.parent.tabs.every(isChecked)) { 279 state = true; 280 } else if (item.parent.tabs.some(isChecked)) { 281 state = 0; 282 } 283 item.parent.checked = state; 284 285 treeView.treeBox.invalidateRow(gTreeData.indexOf(item.parent)); 286 } 287 288 // we only disable the button when there's no cancel button. 289 if (document.getElementById("errorCancel")) { 290 document.getElementById("errorTryAgain").disabled = 291 !gTreeData.some(isChecked); 292 } 293 } 294 295 function restoreSingleTab(aIx, aShifted) { 296 var tabbrowser = getBrowserWindow().gBrowser; 297 var newTab = tabbrowser.addWebTab(); 298 var item = gTreeData[aIx]; 299 300 var tabState = 301 gStateObject.windows[item.parent.ix].tabs[ 302 aIx - gTreeData.indexOf(item.parent) - 1 303 ]; 304 // ensure tab would be visible on the tabstrip. 305 tabState.hidden = false; 306 SessionStore.setTabState(newTab, JSON.stringify(tabState)); 307 308 // respect the preference as to whether to select the tab (the Shift key inverses) 309 if ( 310 Services.prefs.getBoolPref("browser.tabs.loadInBackground") != !aShifted 311 ) { 312 tabbrowser.selectedTab = newTab; 313 } 314 } 315 316 // Tree controller 317 318 var treeView = { 319 treeBox: null, 320 selection: null, 321 322 get rowCount() { 323 return gTreeData.length; 324 }, 325 setTree(treeBox) { 326 this.treeBox = treeBox; 327 }, 328 getCellText(idx) { 329 return gTreeData[idx].label; 330 }, 331 isContainer(idx) { 332 return "open" in gTreeData[idx]; 333 }, 334 getCellValue(idx) { 335 return gTreeData[idx].checked; 336 }, 337 isContainerOpen(idx) { 338 return gTreeData[idx].open; 339 }, 340 isContainerEmpty() { 341 return false; 342 }, 343 isSeparator() { 344 return false; 345 }, 346 isSorted() { 347 return false; 348 }, 349 isEditable() { 350 return false; 351 }, 352 canDrop() { 353 return false; 354 }, 355 getLevel(idx) { 356 return this.isContainer(idx) ? 0 : 1; 357 }, 358 359 getParentIndex(idx) { 360 if (!this.isContainer(idx)) { 361 for (var t = idx - 1; t >= 0; t--) { 362 if (this.isContainer(t)) { 363 return t; 364 } 365 } 366 } 367 return -1; 368 }, 369 370 hasNextSibling(idx, after) { 371 var thisLevel = this.getLevel(idx); 372 for (var t = after + 1; t < gTreeData.length; t++) { 373 if (this.getLevel(t) <= thisLevel) { 374 return this.getLevel(t) == thisLevel; 375 } 376 } 377 return false; 378 }, 379 380 toggleOpenState(idx) { 381 if (!this.isContainer(idx)) { 382 return; 383 } 384 var item = gTreeData[idx]; 385 if (item.open) { 386 // remove this window's tab rows from the view 387 var thisLevel = this.getLevel(idx); 388 /* eslint-disable no-empty */ 389 for ( 390 var t = idx + 1; 391 t < gTreeData.length && this.getLevel(t) > thisLevel; 392 t++ 393 ) {} 394 /* eslint-disable no-empty */ 395 var deletecount = t - idx - 1; 396 gTreeData.splice(idx + 1, deletecount); 397 this.treeBox.rowCountChanged(idx + 1, -deletecount); 398 } else { 399 // add this window's tab rows to the view 400 var toinsert = gTreeData[idx].tabs; 401 for (var i = 0; i < toinsert.length; i++) { 402 gTreeData.splice(idx + i + 1, 0, toinsert[i]); 403 } 404 this.treeBox.rowCountChanged(idx + 1, toinsert.length); 405 } 406 item.open = !item.open; 407 this.treeBox.invalidateRow(idx); 408 }, 409 410 getCellProperties(idx, column) { 411 if ( 412 column.id == "restore" && 413 this.isContainer(idx) && 414 gTreeData[idx].checked === 0 415 ) { 416 return "partial"; 417 } 418 if (column.id == "title") { 419 return this.getImageSrc(idx, column) ? "icon" : "noicon"; 420 } 421 422 return ""; 423 }, 424 425 getRowProperties(idx) { 426 var winState = gTreeData[idx].parent || gTreeData[idx]; 427 if (winState.ix % 2 != 0) { 428 return "alternate"; 429 } 430 431 return ""; 432 }, 433 434 getImageSrc(idx, column) { 435 if (column.id == "title") { 436 return gTreeData[idx].src || null; 437 } 438 return null; 439 }, 440 441 cycleHeader() {}, 442 cycleCell() {}, 443 selectionChanged() {}, 444 getColumnProperties() { 445 return ""; 446 }, 447 };