 |
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 let TabView = {
6 _deck: null,
7 _iframe: null,
8 _window: null,
9 _initialized: false,
10 _browserKeyHandlerInitialized: false,
11 _closedLastVisibleTabBeforeFrameInitialized: false,
12 _isFrameLoading: false,
13 _initFrameCallbacks: [],
14 PREF_BRANCH: "browser.panorama.",
15 PREF_FIRST_RUN: "browser.panorama.experienced_first_run",
16 PREF_STARTUP_PAGE: "browser.startup.page",
17 PREF_RESTORE_ENABLED_ONCE: "browser.panorama.session_restore_enabled_once",
18 GROUPS_IDENTIFIER: "tabview-groups",
19 VISIBILITY_IDENTIFIER: "tabview-visibility",
20
21 // ----------
22 get windowTitle() {
23 delete this.windowTitle;
24 let brandBundle = document.getElementById("bundle_brand");
25 let brandShortName = brandBundle.getString("brandShortName");
26 let title = gNavigatorBundle.getFormattedString("tabview.title", [brandShortName]);
27 return this.windowTitle = title;
28 },
29
30 // ----------
31 get firstUseExperienced() {
32 let pref = this.PREF_FIRST_RUN;
33 if (Services.prefs.prefHasUserValue(pref))
34 return Services.prefs.getBoolPref(pref);
35
36 return false;
37 },
38
39 // ----------
40 set firstUseExperienced(val) {
41 Services.prefs.setBoolPref(this.PREF_FIRST_RUN, val);
42 },
43
44 // ----------
45 get sessionRestoreEnabledOnce() {
46 let pref = this.PREF_RESTORE_ENABLED_ONCE;
47 if (Services.prefs.prefHasUserValue(pref))
48 return Services.prefs.getBoolPref(pref);
49
50 return false;
51 },
52
53 // ----------
54 set sessionRestoreEnabledOnce(val) {
55 Services.prefs.setBoolPref(this.PREF_RESTORE_ENABLED_ONCE, val);
56 },
57
58 // ----------
59 init: function TabView_init() {
60 // disable the ToggleTabView command for popup windows
61 goSetCommandEnabled("Browser:ToggleTabView", window.toolbar.visible);
62 if (!window.toolbar.visible)
63 return;
64
65 if (this._initialized)
66 return;
67
68 if (this.firstUseExperienced) {
69 // ___ visibility
70
71 let data = SessionStore.getWindowValue(window, this.VISIBILITY_IDENTIFIER);
72 if (data && data == "true") {
73 this.show();
74 } else {
75 try {
76 data = SessionStore.getWindowValue(window, this.GROUPS_IDENTIFIER);
77 if (data) {
78 let parsedData = JSON.parse(data);
79 this.updateGroupNumberBroadcaster(parsedData.totalNumber || 1);
80 }
81 } catch (e) { }
82
83 let self = this;
84 // if a tab is changed from hidden to unhidden and the iframe is not
85 // initialized, load the iframe and setup the tab.
86 this._tabShowEventListener = function(event) {
87 if (!self._window)
88 self._initFrame(function() {
89 self._window.UI.onTabSelect(gBrowser.selectedTab);
90 if (self._closedLastVisibleTabBeforeFrameInitialized) {
91 self._closedLastVisibleTabBeforeFrameInitialized = false;
92 self._window.UI.showTabView(false);
93 }
94 });
95 };
96 this._tabCloseEventListener = function(event) {
97 if (!self._window && gBrowser.visibleTabs.length == 0)
98 self._closedLastVisibleTabBeforeFrameInitialized = true;
99 };
100 gBrowser.tabContainer.addEventListener(
101 "TabShow", this._tabShowEventListener, false);
102 gBrowser.tabContainer.addEventListener(
103 "TabClose", this._tabCloseEventListener, false);
104
105 if (this._tabBrowserHasHiddenTabs()) {
106 this._setBrowserKeyHandlers();
107 } else {
108 // for restoring last session and undoing recently closed window
109 this._SSWindowStateReadyListener = function (event) {
110 if (this._tabBrowserHasHiddenTabs())
111 this._setBrowserKeyHandlers();
112 }.bind(this);
113 window.addEventListener(
114 "SSWindowStateReady", this._SSWindowStateReadyListener, false);
115 }
116 }
117 }
118
119 Services.prefs.addObserver(this.PREF_BRANCH, this, false);
120
121 this._initialized = true;
122 },
123
124 // ----------
125 // Observes topic changes.
126 observe: function TabView_observe(subject, topic, data) {
127 if (data == this.PREF_FIRST_RUN && this.firstUseExperienced) {
128 this._addToolbarButton();
129 this.enableSessionRestore();
130 }
131 },
132
133 // ----------
134 // Uninitializes TabView.
135 uninit: function TabView_uninit() {
136 if (!this._initialized)
137 return;
138
139 Services.prefs.removeObserver(this.PREF_BRANCH, this);
140
141 if (this._tabShowEventListener)
142 gBrowser.tabContainer.removeEventListener(
143 "TabShow", this._tabShowEventListener, false);
144
145 if (this._tabCloseEventListener)
146 gBrowser.tabContainer.removeEventListener(
147 "TabClose", this._tabCloseEventListener, false);
148
149 if (this._SSWindowStateReadyListener)
150 window.removeEventListener(
151 "SSWindowStateReady", this._SSWindowStateReadyListener, false);
152
153 this._initialized = false;
154
155 if (this._window) {
156 this._window = null;
157 }
158
159 if (this._iframe) {
160 this._iframe.remove();
161 this._iframe = null;
162 }
163 },
164
165 // ----------
166 // Creates the frame and calls the callback once it's loaded.
167 // If the frame already exists, calls the callback immediately.
168 _initFrame: function TabView__initFrame(callback) {
169 let hasCallback = typeof callback == "function";
170
171 // prevent frame to be initialized for popup windows
172 if (!window.toolbar.visible)
173 return;
174
175 if (this._window) {
176 if (hasCallback)
177 callback();
178 return;
179 }
180
181 if (hasCallback)
182 this._initFrameCallbacks.push(callback);
183
184 if (this._isFrameLoading)
185 return;
186
187 this._isFrameLoading = true;
188
189 TelemetryStopwatch.start("PANORAMA_INITIALIZATION_TIME_MS");
190
191 // ___ find the deck
192 this._deck = document.getElementById("tab-view-deck");
193
194 // ___ create the frame
195 this._iframe = document.createElement("iframe");
196 this._iframe.id = "tab-view";
197 this._iframe.setAttribute("transparent", "true");
198 this._iframe.setAttribute("tooltip", "tab-view-tooltip");
199 this._iframe.flex = 1;
200
201 let self = this;
202
203 window.addEventListener("tabviewframeinitialized", function onInit() {
204 window.removeEventListener("tabviewframeinitialized", onInit, false);
205
206 TelemetryStopwatch.finish("PANORAMA_INITIALIZATION_TIME_MS");
207
208 self._isFrameLoading = false;
209 self._window = self._iframe.contentWindow;
210 self._setBrowserKeyHandlers();
211
212 if (self._tabShowEventListener) {
213 gBrowser.tabContainer.removeEventListener(
214 "TabShow", self._tabShowEventListener, false);
215 self._tabShowEventListener = null;
216 }
217 if (self._tabCloseEventListener) {
218 gBrowser.tabContainer.removeEventListener(
219 "TabClose", self._tabCloseEventListener, false);
220 self._tabCloseEventListener = null;
221 }
222 if (self._SSWindowStateReadyListener) {
223 window.removeEventListener(
224 "SSWindowStateReady", self._SSWindowStateReadyListener, false);
225 self._SSWindowStateReadyListener = null;
226 }
227
228 self._initFrameCallbacks.forEach(function (cb) cb());
229 self._initFrameCallbacks = [];
230 }, false);
231
232 this._iframe.setAttribute("src", "chrome://browser/content/tabview.html");
233 this._deck.appendChild(this._iframe);
234
235 // ___ create tooltip
236 let tooltip = document.createElement("tooltip");
237 tooltip.id = "tab-view-tooltip";
238 tooltip.setAttribute("onpopupshowing", "return TabView.fillInTooltip(document.tooltipNode);");
239 document.getElementById("mainPopupSet").appendChild(tooltip);
240 },
241
242 // ----------
243 getContentWindow: function TabView_getContentWindow() {
244 return this._window;
245 },
246
247 // ----------
248 isVisible: function TabView_isVisible() {
249 return (this._deck ? this._deck.selectedPanel == this._iframe : false);
250 },
251
252 // ----------
253 show: function TabView_show() {
254 if (this.isVisible())
255 return;
256
257 let self = this;
258 this._initFrame(function() {
259 self._window.UI.showTabView(true);
260 });
261 },
262
263 // ----------
264 hide: function TabView_hide() {
265 if (this.isVisible() && this._window) {
266 this._window.UI.exit();
267 }
268 },
269
270 // ----------
271 toggle: function TabView_toggle() {
272 if (this.isVisible())
273 this.hide();
274 else
275 this.show();
276 },
277
278 // ----------
279 _tabBrowserHasHiddenTabs: function TabView_tabBrowserHasHiddenTabs() {
280 return (gBrowser.tabs.length - gBrowser.visibleTabs.length) > 0;
281 },
282
283 // ----------
284 updateContextMenu: function TabView_updateContextMenu(tab, popup) {
285 let separator = document.getElementById("context_tabViewNamedGroups");
286 let isEmpty = true;
287
288 while (popup.firstChild && popup.firstChild != separator)
289 popup.removeChild(popup.firstChild);
290
291 let self = this;
292 this._initFrame(function() {
293 let activeGroup = tab._tabViewTabItem.parent;
294 let groupItems = self._window.GroupItems.groupItems;
295
296 groupItems.forEach(function(groupItem) {
297 // if group has title, it's not hidden and there is no active group or
298 // the active group id doesn't match the group id, a group menu item
299 // would be added.
300 if (!groupItem.hidden &&
301 (groupItem.getTitle().trim() || groupItem.getChildren().length) &&
302 (!activeGroup || activeGroup.id != groupItem.id)) {
303 let menuItem = self._createGroupMenuItem(groupItem);
304 popup.insertBefore(menuItem, separator);
305 isEmpty = false;
306 }
307 });
308 separator.hidden = isEmpty;
309 });
310 },
311
312 // ----------
313 _createGroupMenuItem: function TabView__createGroupMenuItem(groupItem) {
314 let menuItem = document.createElement("menuitem");
315 let title = groupItem.getTitle();
316
317 if (!title.trim()) {
318 let topChildLabel = groupItem.getTopChild().tab.label;
319 let childNum = groupItem.getChildren().length;
320
321 if (childNum > 1) {
322 let num = childNum - 1;
323 title =
324 gNavigatorBundle.getString("tabview.moveToUnnamedGroup.label");
325 title = PluralForm.get(num, title).replace("#1", topChildLabel).replace("#2", num);
326 } else {
327 title = topChildLabel;
328 }
329 }
330
331 menuItem.setAttribute("label", title);
332 menuItem.setAttribute("tooltiptext", title);
333 menuItem.setAttribute("crop", "center");
334 menuItem.setAttribute("class", "tabview-menuitem");
335 menuItem.setAttribute(
336 "oncommand",
337 "TabView.moveTabTo(TabContextMenu.contextTab,'" + groupItem.id + "')");
338
339 return menuItem;
340 },
341
342 // ----------
343 moveTabTo: function TabView_moveTabTo(tab, groupItemId) {
344 if (this._window) {
345 this._window.GroupItems.moveTabToGroupItem(tab, groupItemId);
346 } else {
347 let self = this;
348 this._initFrame(function() {
349 self._window.GroupItems.moveTabToGroupItem(tab, groupItemId);
350 });
351 }
352 },
353
354 // ----------
355 // Adds new key commands to the browser, for invoking the Tab Candy UI
356 // and for switching between groups of tabs when outside of the Tab Candy UI.
357 _setBrowserKeyHandlers: function TabView__setBrowserKeyHandlers() {
358 if (this._browserKeyHandlerInitialized)
359 return;
360
361 this._browserKeyHandlerInitialized = true;
362
363 let self = this;
364 window.addEventListener("keypress", function(event) {
365 if (self.isVisible() || !self._tabBrowserHasHiddenTabs())
366 return;
367
368 let charCode = event.charCode;
369 // Control (+ Shift) + `
370 if (event.ctrlKey && !event.metaKey && !event.altKey &&
371 (charCode == 96 || charCode == 126)) {
372 event.stopPropagation();
373 event.preventDefault();
374
375 self._initFrame(function() {
376 let groupItems = self._window.GroupItems;
377 let tabItem = groupItems.getNextGroupItemTab(event.shiftKey);
378 if (!tabItem)
379 return;
380
381 if (gBrowser.selectedTab.pinned)
382 groupItems.updateActiveGroupItemAndTabBar(tabItem, {dontSetActiveTabInGroup: true});
383 else
384 gBrowser.selectedTab = tabItem.tab;
385 });
386 }
387 }, true);
388 },
389
390 // ----------
391 // Prepares the tab view for undo close tab.
392 prepareUndoCloseTab: function TabView_prepareUndoCloseTab(blankTabToRemove) {
393 if (this._window) {
394 this._window.UI.restoredClosedTab = true;
395
396 if (blankTabToRemove && blankTabToRemove._tabViewTabItem)
397 blankTabToRemove._tabViewTabItem.isRemovedAfterRestore = true;
398 }
399 },
400
401 // ----------
402 // Cleans up the tab view after undo close tab.
403 afterUndoCloseTab: function TabView_afterUndoCloseTab() {
404 if (this._window)
405 this._window.UI.restoredClosedTab = false;
406 },
407
408 // ----------
409 // On move to group pop showing.
410 moveToGroupPopupShowing: function TabView_moveToGroupPopupShowing(event) {
411 // Update the context menu only if Panorama was already initialized or if
412 // there are hidden tabs.
413 let numHiddenTabs = gBrowser.tabs.length - gBrowser.visibleTabs.length;
414 if (this._window || numHiddenTabs > 0)
415 this.updateContextMenu(TabContextMenu.contextTab, event.target);
416 },
417
418 // ----------
419 // Function: _addToolbarButton
420 // Adds the TabView button to the TabsToolbar.
421 _addToolbarButton: function TabView__addToolbarButton() {
422 let buttonId = "tabview-button";
423
424 if (CustomizableUI.getPlacementOfWidget(buttonId))
425 return;
426
427 let allTabsBtnPlacement = CustomizableUI.getPlacementOfWidget("alltabs-button");
428 // allTabsBtnPlacement can never be null because the button isn't removable
429 let desiredPosition = allTabsBtnPlacement.position + 1;
430 CustomizableUI.addWidgetToArea(buttonId, "TabsToolbar", desiredPosition);
431 // NB: this is for backwards compatibility, and should be removed by
432 // https://bugzilla.mozilla.org/show_bug.cgi?id=976041
433 document.persist("TabsToolbar", "currentset");
434 },
435
436 // ----------
437 // Function: updateGroupNumberBroadcaster
438 // Updates the group number broadcaster.
439 updateGroupNumberBroadcaster: function TabView_updateGroupNumberBroadcaster(number) {
440 let groupsNumber = document.getElementById("tabviewGroupsNumber");
441 groupsNumber.setAttribute("groups", number);
442 },
443
444 // ----------
445 // Function: enableSessionRestore
446 // Enables automatic session restore when the browser is started. Does
447 // nothing if we already did that once in the past.
448 enableSessionRestore: function TabView_enableSessionRestore() {
449 if (!this._window || !this.firstUseExperienced)
450 return;
451
452 // do nothing if we already enabled session restore once
453 if (this.sessionRestoreEnabledOnce)
454 return;
455
456 this.sessionRestoreEnabledOnce = true;
457
458 // enable session restore if necessary
459 if (Services.prefs.getIntPref(this.PREF_STARTUP_PAGE) != 3) {
460 Services.prefs.setIntPref(this.PREF_STARTUP_PAGE, 3);
461
462 // show banner
463 this._window.UI.notifySessionRestoreEnabled();
464 }
465 },
466
467 // ----------
468 // Function: fillInTooltip
469 // Fills in the tooltip text.
470 fillInTooltip: function fillInTooltip(tipElement) {
471 let retVal = false;
472 let titleText = null;
473 let direction = tipElement.ownerDocument.dir;
474
475 while (!titleText && tipElement) {
476 if (tipElement.nodeType == Node.ELEMENT_NODE)
477 titleText = tipElement.getAttribute("title");
478 tipElement = tipElement.parentNode;
479 }
480 let tipNode = document.getElementById("tab-view-tooltip");
481 tipNode.style.direction = direction;
482
483 if (titleText) {
484 tipNode.setAttribute("label", titleText);
485 retVal = true;
486 }
487
488 return retVal;
489 }
490 };
491