<commit_before>/* 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/. */

// VERSION 1.3.54

// Used to scroll groups automatically, for instance when dragging a tab over a group's overflown edges.
this.Synthesizer = {
	_utils: null,
	get utils() {
		if(!this._utils) {
			this._utils = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
		}
		return this._utils;
	},

	flipWheel: function(e) {
		try {
			e.preventDefault();
			e.stopPropagation();
			e.stopImmediatePropagation();

			// This doesn't work a lot of time, I don't fully understand why, it seems like it's because the synthesized event
			// fires before our cancelled event has finished, which interferes with the synthesized event actually acting;
			// it does fire and is caught by the handlers though, it just doesn't scroll the element.
			//this.utils.sendWheelEvent(e.clientX, e.clientY, e.deltaY, e.deltaX, e.deltaZ, window.WheelEvent.DOM_DELTA_LINE, 0, e.deltaY, e.deltaX, 0);

			// So instead we're "scrolling manually"... Ugh...

			// Which direction are we scrolling?
			let delta = e.deltaY;
			if(!delta) { return; }
			delta = (delta < 0) ? -1 : 1;

			let el = UI.groupSelector;

			// If the group at the edge is already fully visible, show the next one.
			let shift = UICache.groupSelectorSize;

			// What's visible at the edge of the box? If it's only a partial group, show it fully.
			if(LTR) {
				if(delta > 0) {
					if(el.scrollLeft == el.scrollLeftMax) { return; }
					let fullWidth = (el.clientWidth + el.scrollLeft);
					let partial = fullWidth % UICache.groupSelectorSize;
					shift = UICache.groupSelectorSize - partial;
				}
				else {
					if(el.scrollLeft == el.scrollLeftMin) { return; }
					let fullWidth = el.scrollLeft;
					let partial = fullWidth % UICache.groupSelectorSize;
					if(partial) {
						shift = partial;
					}
				}
			}
			else {
				if(delta > 0) {
					if(el.scrollLeft == el.scrollLeftMin) { return; }
					let fullWidth = (el.clientWidth - el.scrollLeft);
					let partial = fullWidth % UICache.groupSelectorSize;
					shift = UICache.groupSelectorSize - partial;
				}
				else {
					if(el.scrollLeft == el.scrollLeftMax) { return; }
					let fullWidth = 0 - el.scrollLeft;
					let partial = fullWidth % UICache.groupSelectorSize;
					if(partial) {
						shift = partial;
					}
				}
				delta = 0 - delta;
			}

			shift *= delta;
			el.scrollLeft += shift;
		}
		// We really only care about not blocking anything else that is supposed to run.
		catch(ex) { Cu.reportError(ex); }
	},

	scroll: function(mouse, deltaX, deltaY) {
		try {
			this.utils.sendWheelEvent(mouse.x, mouse.y, deltaX, deltaY, 0, window.WheelEvent.DOM_DELTA_LINE, 0, deltaX, deltaY, 0);
		}
		// We really only care about not blocking anything else that is supposed to run.
		catch(ex) { Cu.reportError(ex); }
	}
};

this.Keys = { meta: false };

// Class: UI - Singleton top-level UI manager.
this.UI = {
	// True if the Tab View UI frame has been initialized.
	_frameInitialized: false,

	// Stores the page bounds.
	_pageBounds: null,

	// Set when selecting a pinned tab from search, but without leaving tab view.
	_dontHideTabView: false,

	// If true, a select tab has just been closed in TabView.
	_closedSelectedTabInTabView: false,

	// If true, a closed tab has just been restored.
	restoredClosedTab: false,

	// Tracks whether we're currently in the process of showing/hiding the tabview.
	_isChangingVisibility: false,

	// Keeps track of the <GroupItem>s which their tab items' tabs have been moved and re-orders the tab items when switching to TabView.
	_reorderTabItemsOnShow: new Set(),

	// Keeps track of the <GroupItem>s which their tab items have been moved in TabView UI and re-orders the tabs when switcing back to main browser.
	_reorderTabsOnHide: new Set(),

	// Keeps track of which xul:tab we are currently on. Used to facilitate zooming down from a previous tab.
	_currentTab: null,

	// If the UI is in the middle of an operation, this is the max amount of milliseconds to wait between input events before we no longer consider the operation interactive.
	_maxInteractiveWait: 250,
	lastMoveTime: 0,

	// Tells whether the storage is currently busy or not.
	_storageBusy: false,

	// Tells wether the parent window is about to close
	isDOMWindowClosing: false,

	// Used to keep track of allowed browser keys.
	_browserKeys: null,

	// Browser keys that shouldn't follow through when user is typing anywhere.
	_browserKeysNotInInput: null,

	// Used to keep track of the last opened tab.
	_lastOpenedTab: null,

	// Used to keep track of the tab strip smooth scroll value.
	_originalSmoothScroll: null,

	// Sensitivity in pixels of the area reactive to dragging a tab that will cause the group to scroll.
	scrollAreaSize: 15,

	// has the user clicked the close button in the notice in this window already
	_noticeDismissed: false,

	// To enable context menus on a delay when entering tab view, so it doesn't conflict with FireGestures.
	_contextMenusEnabled: false,

	// For the thumbnails ratio, set every time we enter tab view and the window is resized while in tab view.
	_viewportRatio: 1,

	get classic() { return Prefs.displayMode == 'classic'; },
	get grid() { return Prefs.displayMode == 'grid'; },
	get single() { return Prefs.displayMode == 'single'; },

	get groupSelector() { return $('groupSelector'); },

	get sessionRestoreNotice() { return $('sessionRestoreNotice'); },
	get sessionRestoreNoticeClose() { return $('sessionRestoreNoticeClose'); },
	get sessionRestoreAutoChanged() { return $('sessionRestoreAutoChanged'); },
	get sessionRestorePrivate() { return $('sessionRestorePrivate'); },

	get exitBtn() { return $("exit-button"); },
	get optionsBtn() { return $("optionsbutton"); },
	get helpBtn() { return $("helpbutton"); },
	get singleBtn() { return $("singlebutton"); },
	get gridBtn() { return $("gridbutton"); },
	get classicBtn() { return $("classicbutton"); },
	get gridNewGroupBtn() { return $("gridNewGroup"); },
	get singleNewGroupBtn() { return $("singleNewGroup"); },

	_els: Cc["@mozilla.org/eventlistenerservice;1"].getService(Ci.nsIEventListenerService),

	// Called when a web page is about to show a modal dialog.
	receiveMessage: function(m) {
		if(!this.isTabViewVisible()) { return; }

		let tab = gBrowser.getTabForBrowser(m.target);
		if(!tab) { return; }

		// When TabView is visible, we need to call onTabSelect to make sure that TabView is hidden and that the correct group is activated.
		// When a modal dialog is shown for currently selected tab the onTabSelect event handler is not called, so we need to do it.
		if(tab.selected && this._currentTab == tab) {
			this.onTabSelect(tab);
		}
	},

	handleEvent: function(e) {
		let tab = e.target;

		switch(e.type) {
			// ___ setup event listener to save canvas images
			case 'SSWindowClosing':
				this.storageClosing();
				break;

			case 'SSWindowStateBusy':
				this.storageBusy();
				break;

			case 'SSWindowStateReady':
				this.storageReady();
				break;

			case 'resize':
				this._resize();
				break;

			case 'keyup':
				if(!e.metaKey) {
					Keys.meta = false;
				}
				break;

			case 'keypress':
				if(e.metaKey) {
					Keys.meta = true;
				}
				this._onKeypress(e);
				break;

			case 'mousedown': {
				// target == GroupItems.workSpace
				let focused = $$(":focus");
				if(focused.length > 0) {
					for(let element of focused) {
						// don't fire blur event if the same input element is clicked.
						if(e.target != element && this.isTextField(element)) {
							element.blur();
						}
					}
				}
				if(this.classic && e.originalTarget == GroupItems.workSpace && e.button == 0 && e.detail == 1) {
					this._createGroupItemOnDrag(e);
				}
				break;
			}

			case 'dblclick': {
				if(e.originalTarget != GroupItems.workSpace) { return; }

				// Create a group with one tab on double click
				let w = TabItems.tabWidth;
				let h = TabItems.tabHeight;
				let y = e.clientY - Math.floor(h/2);
				let x = e.clientX - Math.floor(w/2);
				let box = new Rect(x, y, w, h);
				box.inset(-30, -30);

				let opts = { immediately: true, bounds: box };
				let groupItem = new GroupItem([], opts);
				groupItem.newTab();
				break;
			}

			case 'click':
				switch(e.target) {
					case this.sessionRestoreNoticeClose:
						this.sessionRestoreNotice.hidden = true;
						this._noticeDismissed = true;
						break;

					case this.sessionRestoreNotice:
						// If Session Manager is enabled, we don't dare mess with its preferences.
						// Changing its settings is better done through SM's own options window.
						if(pageWatch.SM) {
							gSessionManager.openOptions();
							break;
						}

						this.goToPreferences({ jumpto: 'sessionRestore' });
						break;

					case this.exitBtn:
						this.exit();
						this.blurAll();
						break;

					case this.optionsBtn:
						this.goToPreferences();
						break;

					case this.helpBtn:
						this.goToPreferences({ pane: 'paneHowTo' });
						break;

					case this.classicBtn:
						Prefs.displayMode = 'classic';
						break;

					case this.gridBtn:
						Prefs.displayMode = 'grid';
						break;

					case this.singleBtn:
						Prefs.displayMode = 'single';
						break;

					case this.singleNewGroupBtn:
						this.setActive(GroupItems.newGroup());

						// When creating a new group through the top plus button, it should stay visible.
						this.scrollSelectorsToEnd();
						break;

					case this.gridNewGroupBtn:
						this.setActive(GroupItems.newGroup());
						break;
				}
				break;

			case 'dragover':
				if(DraggingTab) {
					if(e.target == GroupItems.workSpace || e.target == this.gridNewGroupBtn) {
						DraggingTab.canDrop(e, e.target);
					}
					else if(e.target == this.groupSelector || e.target.parentNode == this.groupSelector) {
						if(e.target.parentNode == this.groupSelector) {
							DraggingTab.canDrop(e, e.target);
						}
						UI.scrollAreaWhileDragging(e, this.groupSelector);
					}
				}
				else if(DraggingGroup && this.grid && document.body.classList.contains('groups-overflowing')) {
					UI.scrollAreaWhileDragging(e, GroupItems.workSpace);
				}
				else if(DraggingGroupSelector && (e.target == this.groupSelector || e.target.parentNode == this.groupSelector)) {
					DraggingGroupSelector.canDrop(e);
					UI.scrollAreaWhileDragging(e, this.groupSelector);
				}
				break;

			case 'wheel':
				// When scrolling the group selector area, we want it to always go sideways.
				if(!e.deltaX) {
					// Sometimes this works, sometimes it doesn't... I don't know...
					Synthesizer.flipWheel(e);
				}
				break;

			case 'contextmenu':
				if(!this._contextMenusEnabled) { break; }

				// When right-clicking a group title, we're actually right-clicking its shield if the cursor isn't already there.
				// So we focus it now and pretend like it was already focused.
				if(e.target.classList.contains('title-shield')) {
					e.target._item.focusTitle();
					gTabView.openInputContextMenu(e);
				}
				// On input fields we should show the input context menu (duh).
				else if(this.isTextField(e.target)) {
					gTabView.openInputContextMenu(e);
				}
				else {
					// There's nothing to do if we're not in main groups view.
					if(Search.inSearch || GroupOptionsUI.activeOptions) { break; }

					let tab = null;
					let group = null;
					let node = null;

					// User could have pressed the context menu button in the keyboard, in which case the tab should be the currently active tab or group.
					if(!e.button) {
						let item = this.getActiveTab() || PinnedItems._activeItem || GroupItems.getActiveGroupItem();
						if(item) {
							if(!item.isAGroupItem) {
								tab = item.tab;
							} else {
								group = item;
							}
							node = item.container;
						}
					}
					else {
						node = e.target;
						while(node) {
							if(node.isAnAppItem) {
								tab = node.tab;
								break;
							}
							else if(node._item) {
								if(node._item.isATabItem) {
									tab = node._item.tab;
									break;
								}
								else if(node._item.isAGroupItem) {
									group = node._item;
									break;
								}
							}
							else if(node.classList && node.classList.contains('expandedTray')) {
								group = node._groupItem;
								break;
							}
							node = node.parentNode;
						}
					}

					if(tab) {
						// Pause rendering of thumbs until the menu is shown, to speed things up.
						TabItems.pausePainting();

						// Make this the active group item and update the tab bar accordingly immediately,
						// since some of the context menu's items will need to act on an updated tab bar.
						this.updateShownTabs(tab);
						this.reorderTabsBasedOnTabItemOrder();
						gTabView.openTabContextMenu(e, tab, node);

						// We can continue painting thumbs once this is all done.
						TabItems.resumePainting();
					}
					// We right-clicked a group.
					else if(group) {
						gTabView.openGroupContextMenu(e, group);
					}
				}
				break;

			case 'command':
				switch(e.target) {
					case gTabView.groupContextNewTab:
						gTabView.groupContextMenu._anchorItem.newTab();
						break;
				}
				break;

			case 'TabOpen':
				if(!tab.pinned && this.isTabViewVisible() && !this._storageBusyCount) {
					this._lastOpenedTab = tab;
				}
				break;

			case 'TabClose': {
				if(this.isTabViewVisible()) {
					// just closed the selected tab in the TabView interface.
					if(this._currentTab == tab) {
						this._closedSelectedTabInTabView = true;
					}
					break;
				}

				// If we're currently in the process of session store update, we don't want to go to the Tab View UI.
				if(this._storageBusy) { return; }

				// do this only if not closing the last tab
				if(Tabs.length <= 1) { return; }

				let groupItem = GroupItems.getActiveGroupItem();
				if(!groupItem) { return; } // This shouldn't happen under normal circumstances though.

				// We could have closed the last pinned tab as well.
				if(Tabs.numPinned) {
					// Open a new tab in the current group if closing the last pinned (and visible) tab.
					if(tab.pinned && Tabs.numPinned == 1 && !groupItem.children.length) {
						gTabView.onCloseLastTab(tab);
					}

					// Otherwise do nothing, at the very least the next pinned tab will be selected.
					return;
				}

				// Only open a new tab there when you close the last tab of a groupItem.
				if(groupItem.children.length == 1 && groupItem.children[0].tab == tab) {
					// When a blank tab is active while restoring a closed tab the blank tab gets removed.
					// The active group is not closed as this is where the restored tab goes. So do not open another new tab.
					if(!tab._tabViewTabItem || !tab._tabViewTabItem.isRemovedAfterRestore) {
						gTabView.onCloseLastTab(tab);
					}
				}
				break;
			}
			case 'TabMove':
				if(!tab.pinned && GroupItems.size) {
					let groupItem = tab._tabViewTabItem && tab._tabViewTabItem.parent;
					if(!groupItem) {
						groupItem = GroupItems.getActiveGroupItem();
					}
					if(groupItem) {
						if(!this.isTabViewVisible() || this._isChangingVisibility) {
							this.setReorderTabItemsOnShow(groupItem);
						} else {
							groupItem.reorderTabItemsBasedOnTabOrder();
						}
					}
				}
				break;

			case 'TabSelect':
				this.onTabSelect(tab);
				break;

			case 'TabPinned':
				TabItems.handleTabPin(tab);
				break;

			case 'TabUnpinned': {
				TabItems.handleTabUnpin(tab);

				let groupItem = tab._tabViewTabItem.parent;
				if(groupItem) {
					this.setReorderTabItemsOnShow(groupItem);
				}
				break;
			}
		}
	},

	observe: function(aSubject, aTopic, aData) {
		switch(aTopic) {
			case 'nsPref:changed':
				switch(aSubject) {
					case 'showTabOnUpdates':
						this._noticeDismissed = false;
						this.checkSessionRestore();
						break;

					case 'displayMode':
						this.toggleMode();
						break;

					case 'stackTabs':
						for(let groupItem of GroupItems) {
							if(groupItem.isStacked || groupItem.overflowing) {
								groupItem.arrange();
							}
						}
						break;

					case 'showGroupThumbs':
						this.toggleGroupThumbs();

						// When toggling this preference, we want to reaarange the groups, and ensure the group selector shows the placholder title if necessary.
						if(this.single) {
							this._resize(true);
							for(let groupItem of GroupItems) {
								groupItem.setTitle(groupItem.getTitle());
								groupItem.updateThumb();
							}
						}
						break;

					case 'gridDynamicSize':
						if(this.grid) {
							GroupItems.arrange();
						}
						break;

					case 'showTabCounter':
						document.body.classList[(Prefs.showTabCounter) ? 'add' : 'remove']('showTabCounter');
						break;

					case 'forceBrightText':
						this.attrWatcher();
						break;
				}
				break;

			case objName+'-darktheme-changed':
				this.useDarkTheme();
				break;

			case 'pageWatch-change':
				this.checkSessionRestore();
				break;
		}
	},

	handleSubscription: function(name, info) {
		switch(name) {
			case 'close':
				// info == tabItem
				this._onActiveTabClosed(info);
				break;
		}
	},

	attrWatcher(obj, attr) {
		let lwtheme = !Prefs.forceBrightText && (!gWindow.DevEdition || !gWindow.DevEdition.isThemeCurrentlyApplied) && trueAttribute(gWindow.document.documentElement, 'lwtheme');
		toggleAttribute(document.body, 'lwtheme', lwtheme);
		toggleAttribute(this.groupSelector, 'brighttext', lwtheme && trueAttribute(gWindow[objName].$('nav-bar'), 'brighttext'));
	},

	// Must be called after the object is created.
	init: function() {
		try {
			if(Storage.readWindowBusyState(gWindow)) {
				this.storageBusy();
			}

			let data = Storage.readUIData(gWindow) || {};
			this.storageSanity(data);
			this._pageBounds = data.pageBounds;

			// In case we end up saving UI data before a Search is run, we shouldn't just lose the save search position.
			Search._position = data.searchPosition;

			// Some things depend on the different FF versions.
			toggleAttribute(document.body, 'FF48', Services.vc.compare(Services.appinfo.version, "48.0a1") >= 0);
			toggleAttribute(document.body, 'FF50', Services.vc.compare(Services.appinfo.version, "50.0a1") >= 0);

			// Try to adapt to different lwthemes.
			Watchers.addAttributeWatcher(gWindow.document.documentElement, 'lwtheme', this, false, false);
			Watchers.addAttributeWatcher(gWindow[objName].$('nav-bar'), 'brighttext', this, false, false);
			Prefs.listen('forceBrightText', this);
			this.attrWatcher();

			Observers.add(this, objName+'-darktheme-changed');
			this.useDarkTheme();

			// ___ search
			Search.init();

			// ___ currentTab
			let selectedTab = Tabs.selected;
			this._currentTab = (selectedTab != gTabView._closedLastVisibleTab) ? selectedTab : null;

			Listeners.add(this.exitBtn, 'click', this);
			Listeners.add(this.optionsBtn, 'click', this);
			Listeners.add(this.helpBtn, 'click', this);
			Listeners.add(this.singleBtn, 'click', this);
			Listeners.add(this.gridBtn, 'click', this);
			Listeners.add(this.classicBtn, 'click', this);
			Listeners.add(this.gridNewGroupBtn, 'click', this);
			Listeners.add(this.singleNewGroupBtn, 'click', this);
			Listeners.add(gTabView.groupContextNewTab, 'command', this);

			// When you click on the background/empty part of TabView, we create a new groupItem.
			Listeners.add(GroupItems.workSpace, 'mousedown', this);
			Listeners.add(GroupItems.workSpace, 'dblclick', this);
			Listeners.add(GroupItems.workSpace, 'dragover', this);
			Listeners.add(this.groupSelector, 'wheel', this, true);
			Listeners.add(this.groupSelector, 'dragover', this);

			Messenger.listenWindow(gWindow, "DOMWillOpenModalDialog", this);

			// Initialize the UI with the correct mode from the start, the groups will take care of themselves as they're added.
			Prefs.listen('stackTabs', this);
			Prefs.listen('displayMode', this);
			Prefs.listen('showGroupThumbs', this);
			Prefs.listen('gridDynamicSize', this);
			Prefs.listen('showTabCounter', this);
			document.body.classList.add(Prefs.displayMode);
			if(Prefs.showGroupThumbs) {
				document.body.classList.add('showGroupThumbs');
			}
			if(Prefs.showTabCounter) {
				document.body.classList.add('showTabCounter');
			}

			// ___ setup key handlers
			this._setupBrowserKeys();
			Listeners.add(window, 'keyup', this);
			Listeners.add(window, 'keypress', this, true);
			Listeners.add(window, 'contextmenu', this);

			Listeners.add(gWindow, "SSWindowStateBusy", this);
			Listeners.add(gWindow, "SSWindowStateReady", this);
			Listeners.add(gWindow, "SSWindowClosing", this);

			// ___ add tab action handlers
			Tabs.listen("TabOpen", this);
			Tabs.listen("TabClose", this);
			Tabs.listen("TabMove", this);
			Tabs.listen("TabSelect", this);
			Tabs.listen("TabPinned", this);
			Tabs.listen("TabUnpinned", this);

			FavIcons.init();
			GroupItems.init();
			GroupItems.pauseArrange();
			let hasGroupItemsData = GroupItems.load();
			PinnedItems.init();
			TabItems.pausePainting();
			TabItems.init();

			if(!hasGroupItemsData) {
				this.reset();
			}

			// ___ resizing
			if(this._pageBounds) {
				this._resize(true);
			} else {
				this._pageBounds = this.getPageBounds(true);
			}
			Listeners.add(window, 'resize', this);

			// ___ load frame script
			Messenger.loadInWindow(gWindow, 'TabView');

			pageWatch.register(this);
			Listeners.add(this.sessionRestoreNotice, 'click', this);

			// ___ Done
			this._frameInitialized = true;
			this._save();

			// fire an iframe initialized event so everyone knows tab view is initialized.
			dispatch(window, { type: "tabviewframeinitialized", cancelable: false });
		}
		catch(ex) {
			Cu.reportError(ex);
		}
		finally {
			// There's no point in having ridiculously large slots. We only need to maintain the relative slot differences between the groups.
			GroupItems.normalizeSlots();
		}
	},

	// Should be called when window is unloaded.
	uninit: function() {
		Listeners.remove(window, 'keyup', this);
		Listeners.remove(window, 'keypress', this, true);
		Listeners.remove(window, 'contextmenu', this);
		Listeners.remove(window, 'resize', this);
		Listeners.remove(gWindow, "SSWindowClosing", this);
		Listeners.remove(gWindow, "SSWindowStateBusy", this);
		Listeners.remove(gWindow, "SSWindowStateReady", this);
		Listeners.remove(this.sessionRestoreNotice, 'click', this);

		Listeners.remove(GroupItems.workSpace, 'mousedown', this);
		Listeners.remove(GroupItems.workSpace, 'dblclick', this);
		Listeners.remove(GroupItems.workSpace, 'dragover', this);
		Listeners.remove(this.groupSelector, 'wheel', this, true);
		Listeners.remove(this.groupSelector, 'dragover', this);

		Listeners.remove(this.exitBtn, 'click', this);
		Listeners.remove(this.optionsBtn, 'click', this);
		Listeners.remove(this.helpBtn, 'click', this);
		Listeners.remove(this.singleBtn, 'click', this);
		Listeners.remove(this.gridBtn, 'click', this);
		Listeners.remove(this.classicBtn, 'click', this);
		Listeners.remove(this.gridNewGroupBtn, 'click', this);
		Listeners.remove(this.singleNewGroupBtn, 'click', this);
		Listeners.remove(gTabView.groupContextNewTab, 'command', this);

		pageWatch.unregister(this);

		Messenger.unlistenWindow(gWindow, "DOMWillOpenModalDialog", this);
		Messenger.unloadFromWindow(gWindow, 'TabView');

		Watchers.removeAttributeWatcher(gWindow.document.documentElement, 'lwtheme', this, false, false);
		Watchers.removeAttributeWatcher(gWindow[objName].$('nav-bar'), 'brighttext', this, false, false);
		Prefs.unlisten('forceBrightText', this);

		Observers.remove(this, objName+'-darktheme-changed');

		// additional clean up
		GroupOptionsUI.hide();
		TabItems.uninit();
		PinnedItems.uninit();
		GroupItems.uninit();
		Search.uninit();
		FavIcons.uninit();

		Tabs.unlisten("TabOpen", this);
		Tabs.unlisten("TabClose", this);
		Tabs.unlisten("TabMove", this);
		Tabs.unlisten("TabSelect", this);
		Tabs.unlisten("TabPinned", this);
		Tabs.unlisten("TabUnpinned", this);

		Prefs.unlisten('stackTabs', this);
		Prefs.unlisten('displayMode', this);
		Prefs.unlisten('showGroupThumbs', this);
		Prefs.unlisten('gridDynamicSize', this);
		Prefs.unlisten('showTabCounter', this);

		this._currentTab = null;
		this._pageBounds = null;
		this._reorderTabItemsOnShow = new Set();
		this._reorderTabsOnHide = new Set();
		this._frameInitialized = false;
	},

	useDarkTheme: function() {
		toggleAttribute(document.body, 'darktheme', brightText.useDarkTheme());
	},

	goToPreferences: function(aOptions) {
		gTabView.goToPreferences(aOptions);

		// we can't very well see the preferences if we're still in tabview
		this.goToTab(Tabs.selected);
	},

	// Resets the Panorama view to have just one group with all tabs
	reset: function() {
		// (TMP) Reconnect tabs could have been disconnected in GroupItems.reconstitute.
		TabItems.resumeReconnecting();

		// Close any currently open groups to reset our main object.
		for(let group of GroupItems) {
			group.close();
		}
		GroupItems.nextID = 1;

		let padding = Trenches.defaultRadius;
		let welcomeWidth = 300;
		let pageBounds = GroupItems.getSafeWindowBounds();

		// ___ make a fresh groupItem
		let bounds = new Rect(pageBounds);
		bounds.width = Math.min(bounds.width * 0.667, pageBounds.width - (welcomeWidth + padding));
		bounds.height *= 0.667;
		if(RTL) {
			bounds.left = pageBounds.left + welcomeWidth + (2 * padding);
		}

		let options = { bounds, immediately: true };
		let groupItem = new GroupItem([], options);
		for(let tab of Tabs.notPinned) {
			if(!tab._tabViewTabItem) { continue; }

			let item = tab._tabViewTabItem;
			// To keep in sync with TMP's session manager requirements.
			if(gWindow.Tabmix) {
				item._reconnected = true;
			}
			groupItem.add(item);
		}
		this.setActive(groupItem);
	},

	// Blurs any currently focused element
	blurAll: function() {
		for(let element of $$(":focus")) {
			element.blur();
		}
	},

	// toggle between the various workspace modes available
	toggleMode: function() {
		if(document.body.classList.contains(Prefs.displayMode)) { return; }

		document.body.classList.remove('classic');
		document.body.classList.remove('grid');
		document.body.classList.remove('single');

		document.body.classList.add(Prefs.displayMode);
		this.toggleGroupThumbs();
		this._resize(true);

		try {
			GroupItems.pauseArrange();
			TabItems.pausePainting();

			// Any hidden (closed) groups are removed from view when toggling between modes.
			GroupItems.removeHiddenGroups();

			// Re-build necessary groups info (sizes, positions and stuff)
			GroupItems.load();

			// Make sure groups that were never positioned (created in grid mode) don't overlap others.
			if(UI.classic) {
				GroupItems.resnap();
			}
		}
		catch(ex) {
			Cu.reportError(ex);
		}
		finally {
			GroupItems.resumeArrange();
			TabItems.resumePainting();
		}
	},

	toggleGroupThumbs: function() {
		// don't bother
		if(!this.single) { return; }

		if(Prefs.showGroupThumbs) {
			document.body.classList.add('showGroupThumbs');
		} else {
			document.body.classList.remove('showGroupThumbs');
		}
	},

	// Returns true if the last interaction was long enough ago to consider the UI idle.
	// Used to determine whether interactivity would be sacrificed if the CPU was to become busy.
	isIdle: function() {
		let time = Date.now();
		return (time - this.lastMoveTime) > this._maxInteractiveWait;
	},

	// Returns the currently active tab as a <TabItem>
	getActiveTab: function() {
		return this._activeTab;
	},

	// Sets the currently active tab. The idea of a focused tab is useful for keyboard navigation and returning to the last zoomed-in tab.
	// Hitting return/esc brings you to the focused tab, and using the arrow keys lets you navigate between open tabs.
	// Parameters:
	//  - Takes a <TabItem>
	_setActiveTab: function(tabItem) {
		if(tabItem == this._activeTab) { return; }

		if(this._activeTab) {
			this._activeTab.makeDeactive();
			this._activeTab.removeSubscriber("close", this);
		}

		this._activeTab = tabItem;

		if(this._activeTab) {
			this._activeTab.addSubscriber("close", this);
			this._activeTab.makeActive();

			// Make sure the pinned items reflect this change as well.
			// It won't actually select anything, rather it will unselect an item if it already is selected.
			PinnedItems.makeActive(tabItem.tab);

			// When setting a new active tab (i.e. when closing the previous active tab) TabView loses focus, probably because the physical tab gets it when it's "selected".
			// This prevents the keyboard from working correctly unless we refocus our TabView.
			this.focusTabView();
		}
	},

	// Handles when the currently active tab gets closed.
	// Parameters:
	//  - the <TabItem> that is closed
	_onActiveTabClosed: function(tabItem) {
		if(this._activeTab == tabItem) {
			this._setActiveTab(null);
		}
	},

	// Sets the active tab item or group item
	// Parameters:
	// options
	//  dontSetActiveTabInGroup bool for not setting active tab in group
	setActive: function(item, options = {}) {
		if(item.isATabItem) {
			if(item.parent) {
				GroupItems.setActiveGroupItem(item.parent);
			}
			if(!options.dontSetActiveTabInGroup) {
				this._setActiveTab(item);
			}
		} else {
			GroupItems.setActiveGroupItem(item);
			if(!options.dontSetActiveTabInGroup) {
				let activeTab = item.getActiveTab();
				this._setActiveTab(activeTab);
			}
		}
	},

	// Sets the active tab to 'null'.
	clearActiveTab: function() {
		this._setActiveTab(null);
	},

	// Returns true if the TabView UI is currently shown.
	isTabViewVisible: function() {
		return gTabViewDeck.selectedPanel == gTabViewFrame;
	},

	focusTabView: function() {
		Timers.init('focusTabView', () => {
			if(this.isTabViewVisible() && !this._isChangingVisibility) {
				window.focus();
			}
		}, 0);
	},

	// To close the current tab, as commanded by the keyboard shortcut.
	closeActiveTab: function() {
		if(this._activeTab) {
			this._activeTab.closedManually = true;
			this._activeTab.close();
		}
	},

	// The following is adapted from the equivalent gBrowser.moveActiveTab* methods.
	moveActiveTab: function(aWhere) {
		if(!this._activeTab) { return; }

		switch(aWhere) {
			case "forward":
			case "backward": {
				let sibling = this._activeTab.tab;
				let x = (aWhere == "forward") ? "next" : "previous";
				while(sibling && sibling[x+"Sibling"]) {
					sibling = sibling[x+"Sibling"];
					if(!sibling.hidden) {
						this._moveActiveTab(sibling._tPos);
						break;
					}
				}
				break;
			}
			case "tostart":
				if(this._activeTab.tab._tPos > 0) {
					this._moveActiveTab(0);
				}
				break;

			case "toend": {
				let last = Tabs.length -1;
				if(this._activeTab.tab._tPos < last) {
					this._moveActiveTab(last);
				}
				break;
			}
		}
	},

	_moveActiveTab: function(pos) {
		gBrowser.moveTabTo(this._activeTab.tab, pos);
		this._activeTab.parent.reorderTabItemsBasedOnTabOrder();
	},

	// If the active tab's group is overflowing, scroll it until the tab is visible.
	showActiveTab: function() {
		let tabItem = this.getActiveTab();
		if(!tabItem) { return; }

		// There's no need to do anything, the active tab item is already visible for sure.
		if(!tabItem.parent.overflowing && !tabItem.parent.noThumbs) { return; }

		tabItem.container.scrollIntoView();
	},

	// Returns a <Rect> defining the area of the page <Item>s should stay within.
	getPageBounds: function(real) {
		if(!real && this._pageBounds) {
			let rect = new Rect(this._pageBounds);
			rect.realTop = this._pageBounds.realTop;
			rect.realLeft = this._pageBounds.realLeft;
			return rect;
		}

		let top = 0;
		let left = 0;
		let width = window.innerWidth;
		let height = window.innerHeight;
		width -= UICache.actionsWidth;
		if(UI.single) {
			let offset = UICache.groupSelectorSize + UICache.groupBorderWidth;
			height -= offset;
			top += offset;
		}
		width = Math.max(100, Math.floor(width));<commit_msg>fix for RTL layouts (which aren't actually used yet)<commit_after>/* 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/. */

// VERSION 1.3.55

// Used to scroll groups automatically, for instance when dragging a tab over a group's overflown edges.
this.Synthesizer = {
	_utils: null,
	get utils() {
		if(!this._utils) {
			this._utils = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
		}
		return this._utils;
	},

	flipWheel: function(e) {
		try {
			e.preventDefault();
			e.stopPropagation();
			e.stopImmediatePropagation();

			// This doesn't work a lot of time, I don't fully understand why, it seems like it's because the synthesized event
			// fires before our cancelled event has finished, which interferes with the synthesized event actually acting;
			// it does fire and is caught by the handlers though, it just doesn't scroll the element.
			//this.utils.sendWheelEvent(e.clientX, e.clientY, e.deltaY, e.deltaX, e.deltaZ, window.WheelEvent.DOM_DELTA_LINE, 0, e.deltaY, e.deltaX, 0);

			// So instead we're "scrolling manually"... Ugh...

			// Which direction are we scrolling?
			let delta = e.deltaY;
			if(!delta) { return; }
			delta = (delta < 0) ? -1 : 1;

			let el = UI.groupSelector;

			// If the group at the edge is already fully visible, show the next one.
			let shift = UICache.groupSelectorSize;

			// What's visible at the edge of the box? If it's only a partial group, show it fully.
			if(LTR) {
				if(delta > 0) {
					if(el.scrollLeft == el.scrollLeftMax) { return; }
					let fullWidth = (el.clientWidth + el.scrollLeft);
					let partial = fullWidth % UICache.groupSelectorSize;
					shift = UICache.groupSelectorSize - partial;
				}
				else {
					if(el.scrollLeft == el.scrollLeftMin) { return; }
					let fullWidth = el.scrollLeft;
					let partial = fullWidth % UICache.groupSelectorSize;
					if(partial) {
						shift = partial;
					}
				}
			}
			else {
				if(delta > 0) {
					if(el.scrollLeft == el.scrollLeftMin) { return; }
					let fullWidth = (el.clientWidth - el.scrollLeft);
					let partial = fullWidth % UICache.groupSelectorSize;
					shift = UICache.groupSelectorSize - partial;
				}
				else {
					if(el.scrollLeft == el.scrollLeftMax) { return; }
					let fullWidth = 0 - el.scrollLeft;
					let partial = fullWidth % UICache.groupSelectorSize;
					if(partial) {
						shift = partial;
					}
				}
				delta = 0 - delta;
			}

			shift *= delta;
			el.scrollLeft += shift;
		}
		// We really only care about not blocking anything else that is supposed to run.
		catch(ex) { Cu.reportError(ex); }
	},

	scroll: function(mouse, deltaX, deltaY) {
		try {
			this.utils.sendWheelEvent(mouse.x, mouse.y, deltaX, deltaY, 0, window.WheelEvent.DOM_DELTA_LINE, 0, deltaX, deltaY, 0);
		}
		// We really only care about not blocking anything else that is supposed to run.
		catch(ex) { Cu.reportError(ex); }
	}
};

this.Keys = { meta: false };

// Class: UI - Singleton top-level UI manager.
this.UI = {
	// True if the Tab View UI frame has been initialized.
	_frameInitialized: false,

	// Stores the page bounds.
	_pageBounds: null,

	// Set when selecting a pinned tab from search, but without leaving tab view.
	_dontHideTabView: false,

	// If true, a select tab has just been closed in TabView.
	_closedSelectedTabInTabView: false,

	// If true, a closed tab has just been restored.
	restoredClosedTab: false,

	// Tracks whether we're currently in the process of showing/hiding the tabview.
	_isChangingVisibility: false,

	// Keeps track of the <GroupItem>s which their tab items' tabs have been moved and re-orders the tab items when switching to TabView.
	_reorderTabItemsOnShow: new Set(),

	// Keeps track of the <GroupItem>s which their tab items have been moved in TabView UI and re-orders the tabs when switcing back to main browser.
	_reorderTabsOnHide: new Set(),

	// Keeps track of which xul:tab we are currently on. Used to facilitate zooming down from a previous tab.
	_currentTab: null,

	// If the UI is in the middle of an operation, this is the max amount of milliseconds to wait between input events before we no longer consider the operation interactive.
	_maxInteractiveWait: 250,
	lastMoveTime: 0,

	// Tells whether the storage is currently busy or not.
	_storageBusy: false,

	// Tells wether the parent window is about to close
	isDOMWindowClosing: false,

	// Used to keep track of allowed browser keys.
	_browserKeys: null,

	// Browser keys that shouldn't follow through when user is typing anywhere.
	_browserKeysNotInInput: null,

	// Used to keep track of the last opened tab.
	_lastOpenedTab: null,

	// Used to keep track of the tab strip smooth scroll value.
	_originalSmoothScroll: null,

	// Sensitivity in pixels of the area reactive to dragging a tab that will cause the group to scroll.
	scrollAreaSize: 15,

	// has the user clicked the close button in the notice in this window already
	_noticeDismissed: false,

	// To enable context menus on a delay when entering tab view, so it doesn't conflict with FireGestures.
	_contextMenusEnabled: false,

	// For the thumbnails ratio, set every time we enter tab view and the window is resized while in tab view.
	_viewportRatio: 1,

	get classic() { return Prefs.displayMode == 'classic'; },
	get grid() { return Prefs.displayMode == 'grid'; },
	get single() { return Prefs.displayMode == 'single'; },

	get groupSelector() { return $('groupSelector'); },

	get sessionRestoreNotice() { return $('sessionRestoreNotice'); },
	get sessionRestoreNoticeClose() { return $('sessionRestoreNoticeClose'); },
	get sessionRestoreAutoChanged() { return $('sessionRestoreAutoChanged'); },
	get sessionRestorePrivate() { return $('sessionRestorePrivate'); },

	get exitBtn() { return $("exit-button"); },
	get optionsBtn() { return $("optionsbutton"); },
	get helpBtn() { return $("helpbutton"); },
	get singleBtn() { return $("singlebutton"); },
	get gridBtn() { return $("gridbutton"); },
	get classicBtn() { return $("classicbutton"); },
	get gridNewGroupBtn() { return $("gridNewGroup"); },
	get singleNewGroupBtn() { return $("singleNewGroup"); },

	_els: Cc["@mozilla.org/eventlistenerservice;1"].getService(Ci.nsIEventListenerService),

	// Called when a web page is about to show a modal dialog.
	receiveMessage: function(m) {
		if(!this.isTabViewVisible()) { return; }

		let tab = gBrowser.getTabForBrowser(m.target);
		if(!tab) { return; }

		// When TabView is visible, we need to call onTabSelect to make sure that TabView is hidden and that the correct group is activated.
		// When a modal dialog is shown for currently selected tab the onTabSelect event handler is not called, so we need to do it.
		if(tab.selected && this._currentTab == tab) {
			this.onTabSelect(tab);
		}
	},

	handleEvent: function(e) {
		let tab = e.target;

		switch(e.type) {
			// ___ setup event listener to save canvas images
			case 'SSWindowClosing':
				this.storageClosing();
				break;

			case 'SSWindowStateBusy':
				this.storageBusy();
				break;

			case 'SSWindowStateReady':
				this.storageReady();
				break;

			case 'resize':
				this._resize();
				break;

			case 'keyup':
				if(!e.metaKey) {
					Keys.meta = false;
				}
				break;

			case 'keypress':
				if(e.metaKey) {
					Keys.meta = true;
				}
				this._onKeypress(e);
				break;

			case 'mousedown': {
				// target == GroupItems.workSpace
				let focused = $$(":focus");
				if(focused.length > 0) {
					for(let element of focused) {
						// don't fire blur event if the same input element is clicked.
						if(e.target != element && this.isTextField(element)) {
							element.blur();
						}
					}
				}
				if(this.classic && e.originalTarget == GroupItems.workSpace && e.button == 0 && e.detail == 1) {
					this._createGroupItemOnDrag(e);
				}
				break;
			}

			case 'dblclick': {
				if(e.originalTarget != GroupItems.workSpace) { return; }

				// Create a group with one tab on double click
				let w = TabItems.tabWidth;
				let h = TabItems.tabHeight;
				let y = e.clientY - Math.floor(h/2);
				let x = e.clientX - Math.floor(w/2);
				let box = new Rect(x, y, w, h);
				box.inset(-30, -30);

				let opts = { immediately: true, bounds: box };
				let groupItem = new GroupItem([], opts);
				groupItem.newTab();
				break;
			}

			case 'click':
				switch(e.target) {
					case this.sessionRestoreNoticeClose:
						this.sessionRestoreNotice.hidden = true;
						this._noticeDismissed = true;
						break;

					case this.sessionRestoreNotice:
						// If Session Manager is enabled, we don't dare mess with its preferences.
						// Changing its settings is better done through SM's own options window.
						if(pageWatch.SM) {
							gSessionManager.openOptions();
							break;
						}

						this.goToPreferences({ jumpto: 'sessionRestore' });
						break;

					case this.exitBtn:
						this.exit();
						this.blurAll();
						break;

					case this.optionsBtn:
						this.goToPreferences();
						break;

					case this.helpBtn:
						this.goToPreferences({ pane: 'paneHowTo' });
						break;

					case this.classicBtn:
						Prefs.displayMode = 'classic';
						break;

					case this.gridBtn:
						Prefs.displayMode = 'grid';
						break;

					case this.singleBtn:
						Prefs.displayMode = 'single';
						break;

					case this.singleNewGroupBtn:
						this.setActive(GroupItems.newGroup());

						// When creating a new group through the top plus button, it should stay visible.
						this.scrollSelectorsToEnd();
						break;

					case this.gridNewGroupBtn:
						this.setActive(GroupItems.newGroup());
						break;
				}
				break;

			case 'dragover':
				if(DraggingTab) {
					if(e.target == GroupItems.workSpace || e.target == this.gridNewGroupBtn) {
						DraggingTab.canDrop(e, e.target);
					}
					else if(e.target == this.groupSelector || e.target.parentNode == this.groupSelector) {
						if(e.target.parentNode == this.groupSelector) {
							DraggingTab.canDrop(e, e.target);
						}
						UI.scrollAreaWhileDragging(e, this.groupSelector);
					}
				}
				else if(DraggingGroup && this.grid && document.body.classList.contains('groups-overflowing')) {
					UI.scrollAreaWhileDragging(e, GroupItems.workSpace);
				}
				else if(DraggingGroupSelector && (e.target == this.groupSelector || e.target.parentNode == this.groupSelector)) {
					DraggingGroupSelector.canDrop(e);
					UI.scrollAreaWhileDragging(e, this.groupSelector);
				}
				break;

			case 'wheel':
				// When scrolling the group selector area, we want it to always go sideways.
				if(!e.deltaX) {
					// Sometimes this works, sometimes it doesn't... I don't know...
					Synthesizer.flipWheel(e);
				}
				break;

			case 'contextmenu':
				if(!this._contextMenusEnabled) { break; }

				// When right-clicking a group title, we're actually right-clicking its shield if the cursor isn't already there.
				// So we focus it now and pretend like it was already focused.
				if(e.target.classList.contains('title-shield')) {
					e.target._item.focusTitle();
					gTabView.openInputContextMenu(e);
				}
				// On input fields we should show the input context menu (duh).
				else if(this.isTextField(e.target)) {
					gTabView.openInputContextMenu(e);
				}
				else {
					// There's nothing to do if we're not in main groups view.
					if(Search.inSearch || GroupOptionsUI.activeOptions) { break; }

					let tab = null;
					let group = null;
					let node = null;

					// User could have pressed the context menu button in the keyboard, in which case the tab should be the currently active tab or group.
					if(!e.button) {
						let item = this.getActiveTab() || PinnedItems._activeItem || GroupItems.getActiveGroupItem();
						if(item) {
							if(!item.isAGroupItem) {
								tab = item.tab;
							} else {
								group = item;
							}
							node = item.container;
						}
					}
					else {
						node = e.target;
						while(node) {
							if(node.isAnAppItem) {
								tab = node.tab;
								break;
							}
							else if(node._item) {
								if(node._item.isATabItem) {
									tab = node._item.tab;
									break;
								}
								else if(node._item.isAGroupItem) {
									group = node._item;
									break;
								}
							}
							else if(node.classList && node.classList.contains('expandedTray')) {
								group = node._groupItem;
								break;
							}
							node = node.parentNode;
						}
					}

					if(tab) {
						// Pause rendering of thumbs until the menu is shown, to speed things up.
						TabItems.pausePainting();

						// Make this the active group item and update the tab bar accordingly immediately,
						// since some of the context menu's items will need to act on an updated tab bar.
						this.updateShownTabs(tab);
						this.reorderTabsBasedOnTabItemOrder();
						gTabView.openTabContextMenu(e, tab, node);

						// We can continue painting thumbs once this is all done.
						TabItems.resumePainting();
					}
					// We right-clicked a group.
					else if(group) {
						gTabView.openGroupContextMenu(e, group);
					}
				}
				break;

			case 'command':
				switch(e.target) {
					case gTabView.groupContextNewTab:
						gTabView.groupContextMenu._anchorItem.newTab();
						break;
				}
				break;

			case 'TabOpen':
				if(!tab.pinned && this.isTabViewVisible() && !this._storageBusyCount) {
					this._lastOpenedTab = tab;
				}
				break;

			case 'TabClose': {
				if(this.isTabViewVisible()) {
					// just closed the selected tab in the TabView interface.
					if(this._currentTab == tab) {
						this._closedSelectedTabInTabView = true;
					}
					break;
				}

				// If we're currently in the process of session store update, we don't want to go to the Tab View UI.
				if(this._storageBusy) { return; }

				// do this only if not closing the last tab
				if(Tabs.length <= 1) { return; }

				let groupItem = GroupItems.getActiveGroupItem();
				if(!groupItem) { return; } // This shouldn't happen under normal circumstances though.

				// We could have closed the last pinned tab as well.
				if(Tabs.numPinned) {
					// Open a new tab in the current group if closing the last pinned (and visible) tab.
					if(tab.pinned && Tabs.numPinned == 1 && !groupItem.children.length) {
						gTabView.onCloseLastTab(tab);
					}

					// Otherwise do nothing, at the very least the next pinned tab will be selected.
					return;
				}

				// Only open a new tab there when you close the last tab of a groupItem.
				if(groupItem.children.length == 1 && groupItem.children[0].tab == tab) {
					// When a blank tab is active while restoring a closed tab the blank tab gets removed.
					// The active group is not closed as this is where the restored tab goes. So do not open another new tab.
					if(!tab._tabViewTabItem || !tab._tabViewTabItem.isRemovedAfterRestore) {
						gTabView.onCloseLastTab(tab);
					}
				}
				break;
			}
			case 'TabMove':
				if(!tab.pinned && GroupItems.size) {
					let groupItem = tab._tabViewTabItem && tab._tabViewTabItem.parent;
					if(!groupItem) {
						groupItem = GroupItems.getActiveGroupItem();
					}
					if(groupItem) {
						if(!this.isTabViewVisible() || this._isChangingVisibility) {
							this.setReorderTabItemsOnShow(groupItem);
						} else {
							groupItem.reorderTabItemsBasedOnTabOrder();
						}
					}
				}
				break;

			case 'TabSelect':
				this.onTabSelect(tab);
				break;

			case 'TabPinned':
				TabItems.handleTabPin(tab);
				break;

			case 'TabUnpinned': {
				TabItems.handleTabUnpin(tab);

				let groupItem = tab._tabViewTabItem.parent;
				if(groupItem) {
					this.setReorderTabItemsOnShow(groupItem);
				}
				break;
			}
		}
	},

	observe: function(aSubject, aTopic, aData) {
		switch(aTopic) {
			case 'nsPref:changed':
				switch(aSubject) {
					case 'showTabOnUpdates':
						this._noticeDismissed = false;
						this.checkSessionRestore();
						break;

					case 'displayMode':
						this.toggleMode();
						break;

					case 'stackTabs':
						for(let groupItem of GroupItems) {
							if(groupItem.isStacked || groupItem.overflowing) {
								groupItem.arrange();
							}
						}
						break;

					case 'showGroupThumbs':
						this.toggleGroupThumbs();

						// When toggling this preference, we want to reaarange the groups, and ensure the group selector shows the placholder title if necessary.
						if(this.single) {
							this._resize(true);
							for(let groupItem of GroupItems) {
								groupItem.setTitle(groupItem.getTitle());
								groupItem.updateThumb();
							}
						}
						break;

					case 'gridDynamicSize':
						if(this.grid) {
							GroupItems.arrange();
						}
						break;

					case 'showTabCounter':
						document.body.classList[(Prefs.showTabCounter) ? 'add' : 'remove']('showTabCounter');
						break;

					case 'forceBrightText':
						this.attrWatcher();
						break;
				}
				break;

			case objName+'-darktheme-changed':
				this.useDarkTheme();
				break;

			case 'pageWatch-change':
				this.checkSessionRestore();
				break;
		}
	},

	handleSubscription: function(name, info) {
		switch(name) {
			case 'close':
				// info == tabItem
				this._onActiveTabClosed(info);
				break;
		}
	},

	attrWatcher(obj, attr) {
		let lwtheme = !Prefs.forceBrightText && (!gWindow.DevEdition || !gWindow.DevEdition.isThemeCurrentlyApplied) && trueAttribute(gWindow.document.documentElement, 'lwtheme');
		toggleAttribute(document.body, 'lwtheme', lwtheme);
		toggleAttribute(this.groupSelector, 'brighttext', lwtheme && trueAttribute(gWindow[objName].$('nav-bar'), 'brighttext'));
	},

	// Must be called after the object is created.
	init: function() {
		try {
			if(Storage.readWindowBusyState(gWindow)) {
				this.storageBusy();
			}

			let data = Storage.readUIData(gWindow) || {};
			this.storageSanity(data);
			this._pageBounds = data.pageBounds;

			// In case we end up saving UI data before a Search is run, we shouldn't just lose the save search position.
			Search._position = data.searchPosition;

			// Some things depend on the different FF versions.
			toggleAttribute(document.body, 'FF48', Services.vc.compare(Services.appinfo.version, "48.0a1") >= 0);
			toggleAttribute(document.body, 'FF50', Services.vc.compare(Services.appinfo.version, "50.0a1") >= 0);

			// Try to adapt to different lwthemes.
			Watchers.addAttributeWatcher(gWindow.document.documentElement, 'lwtheme', this, false, false);
			Watchers.addAttributeWatcher(gWindow[objName].$('nav-bar'), 'brighttext', this, false, false);
			Prefs.listen('forceBrightText', this);
			this.attrWatcher();

			Observers.add(this, objName+'-darktheme-changed');
			this.useDarkTheme();

			// ___ search
			Search.init();

			// ___ currentTab
			let selectedTab = Tabs.selected;
			this._currentTab = (selectedTab != gTabView._closedLastVisibleTab) ? selectedTab : null;

			Listeners.add(this.exitBtn, 'click', this);
			Listeners.add(this.optionsBtn, 'click', this);
			Listeners.add(this.helpBtn, 'click', this);
			Listeners.add(this.singleBtn, 'click', this);
			Listeners.add(this.gridBtn, 'click', this);
			Listeners.add(this.classicBtn, 'click', this);
			Listeners.add(this.gridNewGroupBtn, 'click', this);
			Listeners.add(this.singleNewGroupBtn, 'click', this);
			Listeners.add(gTabView.groupContextNewTab, 'command', this);

			// When you click on the background/empty part of TabView, we create a new groupItem.
			Listeners.add(GroupItems.workSpace, 'mousedown', this);
			Listeners.add(GroupItems.workSpace, 'dblclick', this);
			Listeners.add(GroupItems.workSpace, 'dragover', this);
			Listeners.add(this.groupSelector, 'wheel', this, true);
			Listeners.add(this.groupSelector, 'dragover', this);

			Messenger.listenWindow(gWindow, "DOMWillOpenModalDialog", this);

			// Initialize the UI with the correct mode from the start, the groups will take care of themselves as they're added.
			Prefs.listen('stackTabs', this);
			Prefs.listen('displayMode', this);
			Prefs.listen('showGroupThumbs', this);
			Prefs.listen('gridDynamicSize', this);
			Prefs.listen('showTabCounter', this);
			document.body.classList.add(Prefs.displayMode);
			if(Prefs.showGroupThumbs) {
				document.body.classList.add('showGroupThumbs');
			}
			if(Prefs.showTabCounter) {
				document.body.classList.add('showTabCounter');
			}

			// ___ setup key handlers
			this._setupBrowserKeys();
			Listeners.add(window, 'keyup', this);
			Listeners.add(window, 'keypress', this, true);
			Listeners.add(window, 'contextmenu', this);

			Listeners.add(gWindow, "SSWindowStateBusy", this);
			Listeners.add(gWindow, "SSWindowStateReady", this);
			Listeners.add(gWindow, "SSWindowClosing", this);

			// ___ add tab action handlers
			Tabs.listen("TabOpen", this);
			Tabs.listen("TabClose", this);
			Tabs.listen("TabMove", this);
			Tabs.listen("TabSelect", this);
			Tabs.listen("TabPinned", this);
			Tabs.listen("TabUnpinned", this);

			FavIcons.init();
			GroupItems.init();
			GroupItems.pauseArrange();
			let hasGroupItemsData = GroupItems.load();
			PinnedItems.init();
			TabItems.pausePainting();
			TabItems.init();

			if(!hasGroupItemsData) {
				this.reset();
			}

			// ___ resizing
			if(this._pageBounds) {
				this._resize(true);
			} else {
				this._pageBounds = this.getPageBounds(true);
			}
			Listeners.add(window, 'resize', this);

			// ___ load frame script
			Messenger.loadInWindow(gWindow, 'TabView');

			pageWatch.register(this);
			Listeners.add(this.sessionRestoreNotice, 'click', this);

			// ___ Done
			this._frameInitialized = true;
			this._save();

			// fire an iframe initialized event so everyone knows tab view is initialized.
			dispatch(window, { type: "tabviewframeinitialized", cancelable: false });
		}
		catch(ex) {
			Cu.reportError(ex);
		}
		finally {
			// There's no point in having ridiculously large slots. We only need to maintain the relative slot differences between the groups.
			GroupItems.normalizeSlots();
		}
	},

	// Should be called when window is unloaded.
	uninit: function() {
		Listeners.remove(window, 'keyup', this);
		Listeners.remove(window, 'keypress', this, true);
		Listeners.remove(window, 'contextmenu', this);
		Listeners.remove(window, 'resize', this);
		Listeners.remove(gWindow, "SSWindowClosing", this);
		Listeners.remove(gWindow, "SSWindowStateBusy", this);
		Listeners.remove(gWindow, "SSWindowStateReady", this);
		Listeners.remove(this.sessionRestoreNotice, 'click', this);

		Listeners.remove(GroupItems.workSpace, 'mousedown', this);
		Listeners.remove(GroupItems.workSpace, 'dblclick', this);
		Listeners.remove(GroupItems.workSpace, 'dragover', this);
		Listeners.remove(this.groupSelector, 'wheel', this, true);
		Listeners.remove(this.groupSelector, 'dragover', this);

		Listeners.remove(this.exitBtn, 'click', this);
		Listeners.remove(this.optionsBtn, 'click', this);
		Listeners.remove(this.helpBtn, 'click', this);
		Listeners.remove(this.singleBtn, 'click', this);
		Listeners.remove(this.gridBtn, 'click', this);
		Listeners.remove(this.classicBtn, 'click', this);
		Listeners.remove(this.gridNewGroupBtn, 'click', this);
		Listeners.remove(this.singleNewGroupBtn, 'click', this);
		Listeners.remove(gTabView.groupContextNewTab, 'command', this);

		pageWatch.unregister(this);

		Messenger.unlistenWindow(gWindow, "DOMWillOpenModalDialog", this);
		Messenger.unloadFromWindow(gWindow, 'TabView');

		Watchers.removeAttributeWatcher(gWindow.document.documentElement, 'lwtheme', this, false, false);
		Watchers.removeAttributeWatcher(gWindow[objName].$('nav-bar'), 'brighttext', this, false, false);
		Prefs.unlisten('forceBrightText', this);

		Observers.remove(this, objName+'-darktheme-changed');

		// additional clean up
		GroupOptionsUI.hide();
		TabItems.uninit();
		PinnedItems.uninit();
		GroupItems.uninit();
		Search.uninit();
		FavIcons.uninit();

		Tabs.unlisten("TabOpen", this);
		Tabs.unlisten("TabClose", this);
		Tabs.unlisten("TabMove", this);
		Tabs.unlisten("TabSelect", this);
		Tabs.unlisten("TabPinned", this);
		Tabs.unlisten("TabUnpinned", this);

		Prefs.unlisten('stackTabs', this);
		Prefs.unlisten('displayMode', this);
		Prefs.unlisten('showGroupThumbs', this);
		Prefs.unlisten('gridDynamicSize', this);
		Prefs.unlisten('showTabCounter', this);

		this._currentTab = null;
		this._pageBounds = null;
		this._reorderTabItemsOnShow = new Set();
		this._reorderTabsOnHide = new Set();
		this._frameInitialized = false;
	},

	useDarkTheme: function() {
		toggleAttribute(document.body, 'darktheme', brightText.useDarkTheme());
	},

	goToPreferences: function(aOptions) {
		gTabView.goToPreferences(aOptions);

		// we can't very well see the preferences if we're still in tabview
		this.goToTab(Tabs.selected);
	},

	// Resets the Panorama view to have just one group with all tabs
	reset: function() {
		// (TMP) Reconnect tabs could have been disconnected in GroupItems.reconstitute.
		TabItems.resumeReconnecting();

		// Close any currently open groups to reset our main object.
		for(let group of GroupItems) {
			group.close();
		}
		GroupItems.nextID = 1;

		let padding = Trenches.defaultRadius;
		let welcomeWidth = 300;
		let pageBounds = GroupItems.getSafeWindowBounds();

		// ___ make a fresh groupItem
		let bounds = new Rect(pageBounds);
		bounds.width = Math.min(bounds.width * 0.667, pageBounds.width - (welcomeWidth + padding));
		bounds.height *= 0.667;
		if(RTL) {
			bounds.left = pageBounds.left + welcomeWidth + (2 * padding);
		}

		let options = { bounds, immediately: true };
		let groupItem = new GroupItem([], options);
		for(let tab of Tabs.notPinned) {
			if(!tab._tabViewTabItem) { continue; }

			let item = tab._tabViewTabItem;
			// To keep in sync with TMP's session manager requirements.
			if(gWindow.Tabmix) {
				item._reconnected = true;
			}
			groupItem.add(item);
		}
		this.setActive(groupItem);
	},

	// Blurs any currently focused element
	blurAll: function() {
		for(let element of $$(":focus")) {
			element.blur();
		}
	},

	// toggle between the various workspace modes available
	toggleMode: function() {
		if(document.body.classList.contains(Prefs.displayMode)) { return; }

		document.body.classList.remove('classic');
		document.body.classList.remove('grid');
		document.body.classList.remove('single');

		document.body.classList.add(Prefs.displayMode);
		this.toggleGroupThumbs();
		this._resize(true);

		try {
			GroupItems.pauseArrange();
			TabItems.pausePainting();

			// Any hidden (closed) groups are removed from view when toggling between modes.
			GroupItems.removeHiddenGroups();

			// Re-build necessary groups info (sizes, positions and stuff)
			GroupItems.load();

			// Make sure groups that were never positioned (created in grid mode) don't overlap others.
			if(UI.classic) {
				GroupItems.resnap();
			}
		}
		catch(ex) {
			Cu.reportError(ex);
		}
		finally {
			GroupItems.resumeArrange();
			TabItems.resumePainting();
		}
	},

	toggleGroupThumbs: function() {
		// don't bother
		if(!this.single) { return; }

		if(Prefs.showGroupThumbs) {
			document.body.classList.add('showGroupThumbs');
		} else {
			document.body.classList.remove('showGroupThumbs');
		}
	},

	// Returns true if the last interaction was long enough ago to consider the UI idle.
	// Used to determine whether interactivity would be sacrificed if the CPU was to become busy.
	isIdle: function() {
		let time = Date.now();
		return (time - this.lastMoveTime) > this._maxInteractiveWait;
	},

	// Returns the currently active tab as a <TabItem>
	getActiveTab: function() {
		return this._activeTab;
	},

	// Sets the currently active tab. The idea of a focused tab is useful for keyboard navigation and returning to the last zoomed-in tab.
	// Hitting return/esc brings you to the focused tab, and using the arrow keys lets you navigate between open tabs.
	// Parameters:
	//  - Takes a <TabItem>
	_setActiveTab: function(tabItem) {
		if(tabItem == this._activeTab) { return; }

		if(this._activeTab) {
			this._activeTab.makeDeactive();
			this._activeTab.removeSubscriber("close", this);
		}

		this._activeTab = tabItem;

		if(this._activeTab) {
			this._activeTab.addSubscriber("close", this);
			this._activeTab.makeActive();

			// Make sure the pinned items reflect this change as well.
			// It won't actually select anything, rather it will unselect an item if it already is selected.
			PinnedItems.makeActive(tabItem.tab);

			// When setting a new active tab (i.e. when closing the previous active tab) TabView loses focus, probably because the physical tab gets it when it's "selected".
			// This prevents the keyboard from working correctly unless we refocus our TabView.
			this.focusTabView();
		}
	},

	// Handles when the currently active tab gets closed.
	// Parameters:
	//  - the <TabItem> that is closed
	_onActiveTabClosed: function(tabItem) {
		if(this._activeTab == tabItem) {
			this._setActiveTab(null);
		}
	},

	// Sets the active tab item or group item
	// Parameters:
	// options
	//  dontSetActiveTabInGroup bool for not setting active tab in group
	setActive: function(item, options = {}) {
		if(item.isATabItem) {
			if(item.parent) {
				GroupItems.setActiveGroupItem(item.parent);
			}
			if(!options.dontSetActiveTabInGroup) {
				this._setActiveTab(item);
			}
		} else {
			GroupItems.setActiveGroupItem(item);
			if(!options.dontSetActiveTabInGroup) {
				let activeTab = item.getActiveTab();
				this._setActiveTab(activeTab);
			}
		}
	},

	// Sets the active tab to 'null'.
	clearActiveTab: function() {
		this._setActiveTab(null);
	},

	// Returns true if the TabView UI is currently shown.
	isTabViewVisible: function() {
		return gTabViewDeck.selectedPanel == gTabViewFrame;
	},

	focusTabView: function() {
		Timers.init('focusTabView', () => {
			if(this.isTabViewVisible() && !this._isChangingVisibility) {
				window.focus();
			}
		}, 0);
	},

	// To close the current tab, as commanded by the keyboard shortcut.
	closeActiveTab: function() {
		if(this._activeTab) {
			this._activeTab.closedManually = true;
			this._activeTab.close();
		}
	},

	// The following is adapted from the equivalent gBrowser.moveActiveTab* methods.
	moveActiveTab: function(aWhere) {
		if(!this._activeTab) { return; }

		switch(aWhere) {
			case "forward":
			case "backward": {
				let sibling = this._activeTab.tab;
				let x = (aWhere == "forward") ? "next" : "previous";
				while(sibling && sibling[x+"Sibling"]) {
					sibling = sibling[x+"Sibling"];
					if(!sibling.hidden) {
						this._moveActiveTab(sibling._tPos);
						break;
					}
				}
				break;
			}
			case "tostart":
				if(this._activeTab.tab._tPos > 0) {
					this._moveActiveTab(0);
				}
				break;

			case "toend": {
				let last = Tabs.length -1;
				if(this._activeTab.tab._tPos < last) {
					this._moveActiveTab(last);
				}
				break;
			}
		}
	},

	_moveActiveTab: function(pos) {
		gBrowser.moveTabTo(this._activeTab.tab, pos);
		this._activeTab.parent.reorderTabItemsBasedOnTabOrder();
	},

	// If the active tab's group is overflowing, scroll it until the tab is visible.
	showActiveTab: function() {
		let tabItem = this.getActiveTab();
		if(!tabItem) { return; }

		// There's no need to do anything, the active tab item is already visible for sure.
		if(!tabItem.parent.overflowing && !tabItem.parent.noThumbs) { return; }

		tabItem.container.scrollIntoView();
	},

	// Returns a <Rect> defining the area of the page <Item>s should stay within.
	getPageBounds: function(real) {
		if(!real && this._pageBounds) {
			let rect = new Rect(this._pageBounds);
			rect.realTop = this._pageBounds.realTop;
			rect.realLeft = this._pageBounds.realLeft;
			return rect;
		}

		let top = 0;
		let left = 0;
		let width = window.innerWidth;
		let height = window.innerHeight;
		width -= UICache.actionsWidth;

		if(RTL) {
			left += UICache.actionsWidth;
		}

		if(UI.single) {
			let offset = UICache.groupSelectorSize + UICache.groupBorderWidth;
			height -= offset;
			top += offset;
		}
		width = Math.max(100, Math.floor(width));
{'fraction_non_alphanumeric': 0.12202835332606325, 'fraction_numerical': 0.0023991275899672847, 'mean_word_length': 3.276833899660204, 'pattern_counts': {'":': 12, '<': 23, '<?xml version=': 0, '>': 33, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '626325', 'n_tokens_mistral': 24593, 'n_tokens_neox': 19977, 'n_words': 5382}