|
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 const Cc = Components.classes; |
|
6 const Ci = Components.interfaces; |
|
7 const Cu = Components.utils; |
|
8 |
|
9 var gStateObject; |
|
10 var gTreeData; |
|
11 |
|
12 // Page initialization |
|
13 |
|
14 window.onload = function() { |
|
15 // the crashed session state is kept inside a textbox so that SessionStore picks it up |
|
16 // (for when the tab is closed or the session crashes right again) |
|
17 var sessionData = document.getElementById("sessionData"); |
|
18 if (!sessionData.value) { |
|
19 document.getElementById("errorTryAgain").disabled = true; |
|
20 return; |
|
21 } |
|
22 |
|
23 // remove unneeded braces (added for compatibility with Firefox 2.0 and 3.0) |
|
24 if (sessionData.value.charAt(0) == '(') |
|
25 sessionData.value = sessionData.value.slice(1, -1); |
|
26 try { |
|
27 gStateObject = JSON.parse(sessionData.value); |
|
28 } |
|
29 catch (exJSON) { |
|
30 var s = new Cu.Sandbox("about:blank", {sandboxName: 'aboutSessionRestore'}); |
|
31 gStateObject = Cu.evalInSandbox("(" + sessionData.value + ")", s); |
|
32 // If we couldn't parse the string with JSON.parse originally, make sure |
|
33 // that the value in the textbox will be parsable. |
|
34 sessionData.value = JSON.stringify(gStateObject); |
|
35 } |
|
36 |
|
37 // make sure the data is tracked to be restored in case of a subsequent crash |
|
38 var event = document.createEvent("UIEvents"); |
|
39 event.initUIEvent("input", true, true, window, 0); |
|
40 sessionData.dispatchEvent(event); |
|
41 |
|
42 initTreeView(); |
|
43 |
|
44 document.getElementById("errorTryAgain").focus(); |
|
45 }; |
|
46 |
|
47 function initTreeView() { |
|
48 var tabList = document.getElementById("tabList"); |
|
49 var winLabel = tabList.getAttribute("_window_label"); |
|
50 |
|
51 gTreeData = []; |
|
52 gStateObject.windows.forEach(function(aWinData, aIx) { |
|
53 var winState = { |
|
54 label: winLabel.replace("%S", (aIx + 1)), |
|
55 open: true, |
|
56 checked: true, |
|
57 ix: aIx |
|
58 }; |
|
59 winState.tabs = aWinData.tabs.map(function(aTabData) { |
|
60 var entry = aTabData.entries[aTabData.index - 1] || { url: "about:blank" }; |
|
61 var iconURL = aTabData.attributes && aTabData.attributes.image || null; |
|
62 // don't initiate a connection just to fetch a favicon (see bug 462863) |
|
63 if (/^https?:/.test(iconURL)) |
|
64 iconURL = "moz-anno:favicon:" + iconURL; |
|
65 return { |
|
66 label: entry.title || entry.url, |
|
67 checked: true, |
|
68 src: iconURL, |
|
69 parent: winState |
|
70 }; |
|
71 }); |
|
72 gTreeData.push(winState); |
|
73 for each (var tab in winState.tabs) |
|
74 gTreeData.push(tab); |
|
75 }, this); |
|
76 |
|
77 tabList.view = treeView; |
|
78 tabList.view.selection.select(0); |
|
79 } |
|
80 |
|
81 // User actions |
|
82 |
|
83 function restoreSession() { |
|
84 document.getElementById("errorTryAgain").disabled = true; |
|
85 |
|
86 // remove all unselected tabs from the state before restoring it |
|
87 var ix = gStateObject.windows.length - 1; |
|
88 for (var t = gTreeData.length - 1; t >= 0; t--) { |
|
89 if (treeView.isContainer(t)) { |
|
90 if (gTreeData[t].checked === 0) |
|
91 // this window will be restored partially |
|
92 gStateObject.windows[ix].tabs = |
|
93 gStateObject.windows[ix].tabs.filter(function(aTabData, aIx) |
|
94 gTreeData[t].tabs[aIx].checked); |
|
95 else if (!gTreeData[t].checked) |
|
96 // this window won't be restored at all |
|
97 gStateObject.windows.splice(ix, 1); |
|
98 ix--; |
|
99 } |
|
100 } |
|
101 var stateString = JSON.stringify(gStateObject); |
|
102 |
|
103 var ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); |
|
104 var top = getBrowserWindow(); |
|
105 |
|
106 // if there's only this page open, reuse the window for restoring the session |
|
107 if (top.gBrowser.tabs.length == 1) { |
|
108 ss.setWindowState(top, stateString, true); |
|
109 return; |
|
110 } |
|
111 |
|
112 // restore the session into a new window and close the current tab |
|
113 var newWindow = top.openDialog(top.location, "_blank", "chrome,dialog=no,all"); |
|
114 newWindow.addEventListener("load", function() { |
|
115 newWindow.removeEventListener("load", arguments.callee, true); |
|
116 ss.setWindowState(newWindow, stateString, true); |
|
117 |
|
118 var tabbrowser = top.gBrowser; |
|
119 var tabIndex = tabbrowser.getBrowserIndexForDocument(document); |
|
120 tabbrowser.removeTab(tabbrowser.tabs[tabIndex]); |
|
121 }, true); |
|
122 } |
|
123 |
|
124 function startNewSession() { |
|
125 var prefBranch = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); |
|
126 if (prefBranch.getIntPref("browser.startup.page") == 0) |
|
127 getBrowserWindow().gBrowser.loadURI("about:blank"); |
|
128 else |
|
129 getBrowserWindow().BrowserHome(); |
|
130 } |
|
131 |
|
132 function onListClick(aEvent) { |
|
133 // don't react to right-clicks |
|
134 if (aEvent.button == 2) |
|
135 return; |
|
136 |
|
137 var row = {}, col = {}; |
|
138 treeView.treeBox.getCellAt(aEvent.clientX, aEvent.clientY, row, col, {}); |
|
139 if (col.value) { |
|
140 // Restore this specific tab in the same window for middle/double/accel clicking |
|
141 // on a tab's title. |
|
142 #ifdef XP_MACOSX |
|
143 let accelKey = aEvent.metaKey; |
|
144 #else |
|
145 let accelKey = aEvent.ctrlKey; |
|
146 #endif |
|
147 if ((aEvent.button == 1 || aEvent.button == 0 && aEvent.detail == 2 || accelKey) && |
|
148 col.value.id == "title" && |
|
149 !treeView.isContainer(row.value)) { |
|
150 restoreSingleTab(row.value, aEvent.shiftKey); |
|
151 aEvent.stopPropagation(); |
|
152 } |
|
153 else if (col.value.id == "restore") |
|
154 toggleRowChecked(row.value); |
|
155 } |
|
156 } |
|
157 |
|
158 function onListKeyDown(aEvent) { |
|
159 switch (aEvent.keyCode) |
|
160 { |
|
161 case KeyEvent.DOM_VK_SPACE: |
|
162 toggleRowChecked(document.getElementById("tabList").currentIndex); |
|
163 break; |
|
164 case KeyEvent.DOM_VK_RETURN: |
|
165 var ix = document.getElementById("tabList").currentIndex; |
|
166 if (aEvent.ctrlKey && !treeView.isContainer(ix)) |
|
167 restoreSingleTab(ix, aEvent.shiftKey); |
|
168 break; |
|
169 } |
|
170 } |
|
171 |
|
172 // Helper functions |
|
173 |
|
174 function getBrowserWindow() { |
|
175 return window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation) |
|
176 .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem |
|
177 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); |
|
178 } |
|
179 |
|
180 function toggleRowChecked(aIx) { |
|
181 var item = gTreeData[aIx]; |
|
182 item.checked = !item.checked; |
|
183 treeView.treeBox.invalidateRow(aIx); |
|
184 |
|
185 function isChecked(aItem) aItem.checked; |
|
186 |
|
187 if (treeView.isContainer(aIx)) { |
|
188 // (un)check all tabs of this window as well |
|
189 for each (var tab in item.tabs) { |
|
190 tab.checked = item.checked; |
|
191 treeView.treeBox.invalidateRow(gTreeData.indexOf(tab)); |
|
192 } |
|
193 } |
|
194 else { |
|
195 // update the window's checkmark as well (0 means "partially checked") |
|
196 item.parent.checked = item.parent.tabs.every(isChecked) ? true : |
|
197 item.parent.tabs.some(isChecked) ? 0 : false; |
|
198 treeView.treeBox.invalidateRow(gTreeData.indexOf(item.parent)); |
|
199 } |
|
200 |
|
201 document.getElementById("errorTryAgain").disabled = !gTreeData.some(isChecked); |
|
202 } |
|
203 |
|
204 function restoreSingleTab(aIx, aShifted) { |
|
205 var tabbrowser = getBrowserWindow().gBrowser; |
|
206 var newTab = tabbrowser.addTab(); |
|
207 var item = gTreeData[aIx]; |
|
208 |
|
209 var ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); |
|
210 var tabState = gStateObject.windows[item.parent.ix] |
|
211 .tabs[aIx - gTreeData.indexOf(item.parent) - 1]; |
|
212 // ensure tab would be visible on the tabstrip. |
|
213 tabState.hidden = false; |
|
214 ss.setTabState(newTab, JSON.stringify(tabState)); |
|
215 |
|
216 // respect the preference as to whether to select the tab (the Shift key inverses) |
|
217 var prefBranch = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); |
|
218 if (prefBranch.getBoolPref("browser.tabs.loadInBackground") != !aShifted) |
|
219 tabbrowser.selectedTab = newTab; |
|
220 } |
|
221 |
|
222 // Tree controller |
|
223 |
|
224 var treeView = { |
|
225 treeBox: null, |
|
226 selection: null, |
|
227 |
|
228 get rowCount() { return gTreeData.length; }, |
|
229 setTree: function(treeBox) { this.treeBox = treeBox; }, |
|
230 getCellText: function(idx, column) { return gTreeData[idx].label; }, |
|
231 isContainer: function(idx) { return "open" in gTreeData[idx]; }, |
|
232 getCellValue: function(idx, column){ return gTreeData[idx].checked; }, |
|
233 isContainerOpen: function(idx) { return gTreeData[idx].open; }, |
|
234 isContainerEmpty: function(idx) { return false; }, |
|
235 isSeparator: function(idx) { return false; }, |
|
236 isSorted: function() { return false; }, |
|
237 isEditable: function(idx, column) { return false; }, |
|
238 canDrop: function(idx, orientation, dt) { return false; }, |
|
239 getLevel: function(idx) { return this.isContainer(idx) ? 0 : 1; }, |
|
240 |
|
241 getParentIndex: function(idx) { |
|
242 if (!this.isContainer(idx)) |
|
243 for (var t = idx - 1; t >= 0 ; t--) |
|
244 if (this.isContainer(t)) |
|
245 return t; |
|
246 return -1; |
|
247 }, |
|
248 |
|
249 hasNextSibling: function(idx, after) { |
|
250 var thisLevel = this.getLevel(idx); |
|
251 for (var t = after + 1; t < gTreeData.length; t++) |
|
252 if (this.getLevel(t) <= thisLevel) |
|
253 return this.getLevel(t) == thisLevel; |
|
254 return false; |
|
255 }, |
|
256 |
|
257 toggleOpenState: function(idx) { |
|
258 if (!this.isContainer(idx)) |
|
259 return; |
|
260 var item = gTreeData[idx]; |
|
261 if (item.open) { |
|
262 // remove this window's tab rows from the view |
|
263 var thisLevel = this.getLevel(idx); |
|
264 for (var t = idx + 1; t < gTreeData.length && this.getLevel(t) > thisLevel; t++); |
|
265 var deletecount = t - idx - 1; |
|
266 gTreeData.splice(idx + 1, deletecount); |
|
267 this.treeBox.rowCountChanged(idx + 1, -deletecount); |
|
268 } |
|
269 else { |
|
270 // add this window's tab rows to the view |
|
271 var toinsert = gTreeData[idx].tabs; |
|
272 for (var i = 0; i < toinsert.length; i++) |
|
273 gTreeData.splice(idx + i + 1, 0, toinsert[i]); |
|
274 this.treeBox.rowCountChanged(idx + 1, toinsert.length); |
|
275 } |
|
276 item.open = !item.open; |
|
277 this.treeBox.invalidateRow(idx); |
|
278 }, |
|
279 |
|
280 getCellProperties: function(idx, column) { |
|
281 if (column.id == "restore" && this.isContainer(idx) && gTreeData[idx].checked === 0) |
|
282 return "partial"; |
|
283 if (column.id == "title") |
|
284 return this.getImageSrc(idx, column) ? "icon" : "noicon"; |
|
285 |
|
286 return ""; |
|
287 }, |
|
288 |
|
289 getRowProperties: function(idx) { |
|
290 var winState = gTreeData[idx].parent || gTreeData[idx]; |
|
291 if (winState.ix % 2 != 0) |
|
292 return "alternate"; |
|
293 |
|
294 return ""; |
|
295 }, |
|
296 |
|
297 getImageSrc: function(idx, column) { |
|
298 if (column.id == "title") |
|
299 return gTreeData[idx].src || null; |
|
300 return null; |
|
301 }, |
|
302 |
|
303 getProgressMode : function(idx, column) { }, |
|
304 cycleHeader: function(column) { }, |
|
305 cycleCell: function(idx, column) { }, |
|
306 selectionChanged: function() { }, |
|
307 performAction: function(action) { }, |
|
308 performActionOnCell: function(action, index, column) { }, |
|
309 getColumnProperties: function(column) { return ""; } |
|
310 }; |