michael@0: // This Source Code Form is subject to the terms of the Mozilla Public michael@0: // License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: // file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["UITour"]; michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", michael@0: "resource://gre/modules/LightweightThemeManager.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils", michael@0: "resource://gre/modules/PermissionsUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", michael@0: "resource:///modules/CustomizableUI.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", michael@0: "resource://gre/modules/UITelemetry.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry", michael@0: "resource:///modules/BrowserUITelemetry.jsm"); michael@0: michael@0: michael@0: const UITOUR_PERMISSION = "uitour"; michael@0: const PREF_PERM_BRANCH = "browser.uitour."; michael@0: const PREF_SEENPAGEIDS = "browser.uitour.seenPageIDs"; michael@0: const MAX_BUTTONS = 4; michael@0: michael@0: const BUCKET_NAME = "UITour"; michael@0: const BUCKET_TIMESTEPS = [ michael@0: 1 * 60 * 1000, // Until 1 minute after tab is closed/inactive. michael@0: 3 * 60 * 1000, // Until 3 minutes after tab is closed/inactive. michael@0: 10 * 60 * 1000, // Until 10 minutes after tab is closed/inactive. michael@0: 60 * 60 * 1000, // Until 1 hour after tab is closed/inactive. michael@0: ]; michael@0: michael@0: // Time after which seen Page IDs expire. michael@0: const SEENPAGEID_EXPIRY = 8 * 7 * 24 * 60 * 60 * 1000; // 8 weeks. michael@0: michael@0: michael@0: this.UITour = { michael@0: url: null, michael@0: seenPageIDs: null, michael@0: pageIDSourceTabs: new WeakMap(), michael@0: pageIDSourceWindows: new WeakMap(), michael@0: /* Map from browser windows to a set of tabs in which a tour is open */ michael@0: originTabs: new WeakMap(), michael@0: /* Map from browser windows to a set of pinned tabs opened by (a) tour(s) */ michael@0: pinnedTabs: new WeakMap(), michael@0: urlbarCapture: new WeakMap(), michael@0: appMenuOpenForAnnotation: new Set(), michael@0: availableTargetsCache: new WeakMap(), michael@0: michael@0: _detachingTab: false, michael@0: _annotationPanelMutationObservers: new WeakMap(), michael@0: _queuedEvents: [], michael@0: _pendingDoc: null, michael@0: michael@0: highlightEffects: ["random", "wobble", "zoom", "color"], michael@0: targets: new Map([ michael@0: ["accountStatus", { michael@0: query: (aDocument) => { michael@0: let statusButton = aDocument.getElementById("PanelUI-fxa-status"); michael@0: return aDocument.getAnonymousElementByAttribute(statusButton, michael@0: "class", michael@0: "toolbarbutton-icon"); michael@0: }, michael@0: widgetName: "PanelUI-fxa-status", michael@0: }], michael@0: ["addons", {query: "#add-ons-button"}], michael@0: ["appMenu", { michael@0: addTargetListener: (aDocument, aCallback) => { michael@0: let panelPopup = aDocument.getElementById("PanelUI-popup"); michael@0: panelPopup.addEventListener("popupshown", aCallback); michael@0: }, michael@0: query: "#PanelUI-button", michael@0: removeTargetListener: (aDocument, aCallback) => { michael@0: let panelPopup = aDocument.getElementById("PanelUI-popup"); michael@0: panelPopup.removeEventListener("popupshown", aCallback); michael@0: }, michael@0: }], michael@0: ["backForward", { michael@0: query: "#back-button", michael@0: widgetName: "urlbar-container", michael@0: }], michael@0: ["bookmarks", {query: "#bookmarks-menu-button"}], michael@0: ["customize", { michael@0: query: (aDocument) => { michael@0: let customizeButton = aDocument.getElementById("PanelUI-customize"); michael@0: return aDocument.getAnonymousElementByAttribute(customizeButton, michael@0: "class", michael@0: "toolbarbutton-icon"); michael@0: }, michael@0: widgetName: "PanelUI-customize", michael@0: }], michael@0: ["help", {query: "#PanelUI-help"}], michael@0: ["home", {query: "#home-button"}], michael@0: ["quit", {query: "#PanelUI-quit"}], michael@0: ["search", { michael@0: query: "#searchbar", michael@0: widgetName: "search-container", michael@0: }], michael@0: ["searchProvider", { michael@0: query: (aDocument) => { michael@0: let searchbar = aDocument.getElementById("searchbar"); michael@0: return aDocument.getAnonymousElementByAttribute(searchbar, michael@0: "anonid", michael@0: "searchbar-engine-button"); michael@0: }, michael@0: widgetName: "search-container", michael@0: }], michael@0: ["selectedTabIcon", { michael@0: query: (aDocument) => { michael@0: let selectedtab = aDocument.defaultView.gBrowser.selectedTab; michael@0: let element = aDocument.getAnonymousElementByAttribute(selectedtab, michael@0: "anonid", michael@0: "tab-icon-image"); michael@0: if (!element || !UITour.isElementVisible(element)) { michael@0: return null; michael@0: } michael@0: return element; michael@0: }, michael@0: }], michael@0: ["urlbar", { michael@0: query: "#urlbar", michael@0: widgetName: "urlbar-container", michael@0: }], michael@0: ]), michael@0: michael@0: init: function() { michael@0: // Lazy getter is initialized here so it can be replicated any time michael@0: // in a test. michael@0: delete this.seenPageIDs; michael@0: Object.defineProperty(this, "seenPageIDs", { michael@0: get: this.restoreSeenPageIDs.bind(this), michael@0: configurable: true, michael@0: }); michael@0: michael@0: delete this.url; michael@0: XPCOMUtils.defineLazyGetter(this, "url", function () { michael@0: return Services.urlFormatter.formatURLPref("browser.uitour.url"); michael@0: }); michael@0: michael@0: // Clear the availableTargetsCache on widget changes. michael@0: let listenerMethods = [ michael@0: "onWidgetAdded", michael@0: "onWidgetMoved", michael@0: "onWidgetRemoved", michael@0: "onWidgetReset", michael@0: "onAreaReset", michael@0: ]; michael@0: CustomizableUI.addListener(listenerMethods.reduce((listener, method) => { michael@0: listener[method] = () => this.availableTargetsCache.clear(); michael@0: return listener; michael@0: }, {})); michael@0: }, michael@0: michael@0: restoreSeenPageIDs: function() { michael@0: delete this.seenPageIDs; michael@0: michael@0: if (UITelemetry.enabled) { michael@0: let dateThreshold = Date.now() - SEENPAGEID_EXPIRY; michael@0: michael@0: try { michael@0: let data = Services.prefs.getCharPref(PREF_SEENPAGEIDS); michael@0: data = new Map(JSON.parse(data)); michael@0: michael@0: for (let [pageID, details] of data) { michael@0: michael@0: if (typeof pageID != "string" || michael@0: typeof details != "object" || michael@0: typeof details.lastSeen != "number" || michael@0: details.lastSeen < dateThreshold) { michael@0: michael@0: data.delete(pageID); michael@0: } michael@0: } michael@0: michael@0: this.seenPageIDs = data; michael@0: } catch (e) {} michael@0: } michael@0: michael@0: if (!this.seenPageIDs) michael@0: this.seenPageIDs = new Map(); michael@0: michael@0: this.persistSeenIDs(); michael@0: michael@0: return this.seenPageIDs; michael@0: }, michael@0: michael@0: addSeenPageID: function(aPageID) { michael@0: if (!UITelemetry.enabled) michael@0: return; michael@0: michael@0: this.seenPageIDs.set(aPageID, { michael@0: lastSeen: Date.now(), michael@0: }); michael@0: michael@0: this.persistSeenIDs(); michael@0: }, michael@0: michael@0: persistSeenIDs: function() { michael@0: if (this.seenPageIDs.size === 0) { michael@0: Services.prefs.clearUserPref(PREF_SEENPAGEIDS); michael@0: return; michael@0: } michael@0: michael@0: Services.prefs.setCharPref(PREF_SEENPAGEIDS, michael@0: JSON.stringify([...this.seenPageIDs])); michael@0: }, michael@0: michael@0: onPageEvent: function(aEvent) { michael@0: let contentDocument = null; michael@0: if (aEvent.target instanceof Ci.nsIDOMHTMLDocument) michael@0: contentDocument = aEvent.target; michael@0: else if (aEvent.target instanceof Ci.nsIDOMHTMLElement) michael@0: contentDocument = aEvent.target.ownerDocument; michael@0: else michael@0: return false; michael@0: michael@0: // Ignore events if they're not from a trusted origin. michael@0: if (!this.ensureTrustedOrigin(contentDocument)) michael@0: return false; michael@0: michael@0: if (typeof aEvent.detail != "object") michael@0: return false; michael@0: michael@0: let action = aEvent.detail.action; michael@0: if (typeof action != "string" || !action) michael@0: return false; michael@0: michael@0: let data = aEvent.detail.data; michael@0: if (typeof data != "object") michael@0: return false; michael@0: michael@0: let window = this.getChromeWindow(contentDocument); michael@0: // Do this before bailing if there's no tab, so later we can pick up the pieces: michael@0: window.gBrowser.tabContainer.addEventListener("TabSelect", this); michael@0: let tab = window.gBrowser._getTabForContentWindow(contentDocument.defaultView); michael@0: if (!tab) { michael@0: // This should only happen while detaching a tab: michael@0: if (this._detachingTab) { michael@0: this._queuedEvents.push(aEvent); michael@0: this._pendingDoc = Cu.getWeakReference(contentDocument); michael@0: return; michael@0: } michael@0: Cu.reportError("Discarding tabless UITour event (" + action + ") while not detaching a tab." + michael@0: "This shouldn't happen!"); michael@0: return; michael@0: } michael@0: michael@0: switch (action) { michael@0: case "registerPageID": { michael@0: // This is only relevant if Telemtry is enabled. michael@0: if (!UITelemetry.enabled) michael@0: break; michael@0: michael@0: // We don't want to allow BrowserUITelemetry.BUCKET_SEPARATOR in the michael@0: // pageID, as it could make parsing the telemetry bucket name difficult. michael@0: if (typeof data.pageID == "string" && michael@0: !data.pageID.contains(BrowserUITelemetry.BUCKET_SEPARATOR)) { michael@0: this.addSeenPageID(data.pageID); michael@0: michael@0: // Store tabs and windows separately so we don't need to loop over all michael@0: // tabs when a window is closed. michael@0: this.pageIDSourceTabs.set(tab, data.pageID); michael@0: this.pageIDSourceWindows.set(window, data.pageID); michael@0: michael@0: this.setTelemetryBucket(data.pageID); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case "showHighlight": { michael@0: let targetPromise = this.getTarget(window, data.target); michael@0: targetPromise.then(target => { michael@0: if (!target.node) { michael@0: Cu.reportError("UITour: Target could not be resolved: " + data.target); michael@0: return; michael@0: } michael@0: let effect = undefined; michael@0: if (this.highlightEffects.indexOf(data.effect) !== -1) { michael@0: effect = data.effect; michael@0: } michael@0: this.showHighlight(target, effect); michael@0: }).then(null, Cu.reportError); michael@0: break; michael@0: } michael@0: michael@0: case "hideHighlight": { michael@0: this.hideHighlight(window); michael@0: break; michael@0: } michael@0: michael@0: case "showInfo": { michael@0: let targetPromise = this.getTarget(window, data.target, true); michael@0: targetPromise.then(target => { michael@0: if (!target.node) { michael@0: Cu.reportError("UITour: Target could not be resolved: " + data.target); michael@0: return; michael@0: } michael@0: michael@0: let iconURL = null; michael@0: if (typeof data.icon == "string") michael@0: iconURL = this.resolveURL(contentDocument, data.icon); michael@0: michael@0: let buttons = []; michael@0: if (Array.isArray(data.buttons) && data.buttons.length > 0) { michael@0: for (let buttonData of data.buttons) { michael@0: if (typeof buttonData == "object" && michael@0: typeof buttonData.label == "string" && michael@0: typeof buttonData.callbackID == "string") { michael@0: let button = { michael@0: label: buttonData.label, michael@0: callbackID: buttonData.callbackID, michael@0: }; michael@0: michael@0: if (typeof buttonData.icon == "string") michael@0: button.iconURL = this.resolveURL(contentDocument, buttonData.icon); michael@0: michael@0: if (typeof buttonData.style == "string") michael@0: button.style = buttonData.style; michael@0: michael@0: buttons.push(button); michael@0: michael@0: if (buttons.length == MAX_BUTTONS) michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: let infoOptions = {}; michael@0: michael@0: if (typeof data.closeButtonCallbackID == "string") michael@0: infoOptions.closeButtonCallbackID = data.closeButtonCallbackID; michael@0: if (typeof data.targetCallbackID == "string") michael@0: infoOptions.targetCallbackID = data.targetCallbackID; michael@0: michael@0: this.showInfo(contentDocument, target, data.title, data.text, iconURL, buttons, infoOptions); michael@0: }).then(null, Cu.reportError); michael@0: break; michael@0: } michael@0: michael@0: case "hideInfo": { michael@0: this.hideInfo(window); michael@0: break; michael@0: } michael@0: michael@0: case "previewTheme": { michael@0: this.previewTheme(data.theme); michael@0: break; michael@0: } michael@0: michael@0: case "resetTheme": { michael@0: this.resetTheme(); michael@0: break; michael@0: } michael@0: michael@0: case "addPinnedTab": { michael@0: this.ensurePinnedTab(window, true); michael@0: break; michael@0: } michael@0: michael@0: case "removePinnedTab": { michael@0: this.removePinnedTab(window); michael@0: break; michael@0: } michael@0: michael@0: case "showMenu": { michael@0: this.showMenu(window, data.name); michael@0: break; michael@0: } michael@0: michael@0: case "hideMenu": { michael@0: this.hideMenu(window, data.name); michael@0: break; michael@0: } michael@0: michael@0: case "startUrlbarCapture": { michael@0: if (typeof data.text != "string" || !data.text || michael@0: typeof data.url != "string" || !data.url) { michael@0: return false; michael@0: } michael@0: michael@0: let uri = null; michael@0: try { michael@0: uri = Services.io.newURI(data.url, null, null); michael@0: } catch (e) { michael@0: return false; michael@0: } michael@0: michael@0: let secman = Services.scriptSecurityManager; michael@0: let principal = contentDocument.nodePrincipal; michael@0: let flags = secman.DISALLOW_INHERIT_PRINCIPAL; michael@0: try { michael@0: secman.checkLoadURIWithPrincipal(principal, uri, flags); michael@0: } catch (e) { michael@0: return false; michael@0: } michael@0: michael@0: this.startUrlbarCapture(window, data.text, data.url); michael@0: break; michael@0: } michael@0: michael@0: case "endUrlbarCapture": { michael@0: this.endUrlbarCapture(window); michael@0: break; michael@0: } michael@0: michael@0: case "getConfiguration": { michael@0: if (typeof data.configuration != "string") { michael@0: return false; michael@0: } michael@0: michael@0: this.getConfiguration(contentDocument, data.configuration, data.callbackID); michael@0: break; michael@0: } michael@0: michael@0: case "showFirefoxAccounts": { michael@0: // 'signup' is the only action that makes sense currently, so we don't michael@0: // accept arbitrary actions just to be safe... michael@0: // We want to replace the current tab. michael@0: contentDocument.location.href = "about:accounts?action=signup"; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (!this.originTabs.has(window)) michael@0: this.originTabs.set(window, new Set()); michael@0: michael@0: this.originTabs.get(window).add(tab); michael@0: tab.addEventListener("TabClose", this); michael@0: tab.addEventListener("TabBecomingWindow", this); michael@0: window.addEventListener("SSWindowClosing", this); michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: handleEvent: function(aEvent) { michael@0: switch (aEvent.type) { michael@0: case "pagehide": { michael@0: let window = this.getChromeWindow(aEvent.target); michael@0: this.teardownTour(window); michael@0: break; michael@0: } michael@0: michael@0: case "TabBecomingWindow": michael@0: this._detachingTab = true; michael@0: // Fall through michael@0: case "TabClose": { michael@0: let tab = aEvent.target; michael@0: if (this.pageIDSourceTabs.has(tab)) { michael@0: let pageID = this.pageIDSourceTabs.get(tab); michael@0: michael@0: // Delete this from the window cache, so if the window is closed we michael@0: // don't expire this page ID twice. michael@0: let window = tab.ownerDocument.defaultView; michael@0: if (this.pageIDSourceWindows.get(window) == pageID) michael@0: this.pageIDSourceWindows.delete(window); michael@0: michael@0: this.setExpiringTelemetryBucket(pageID, "closed"); michael@0: } michael@0: michael@0: let window = tab.ownerDocument.defaultView; michael@0: this.teardownTour(window); michael@0: break; michael@0: } michael@0: michael@0: case "TabSelect": { michael@0: if (aEvent.detail && aEvent.detail.previousTab) { michael@0: let previousTab = aEvent.detail.previousTab; michael@0: michael@0: if (this.pageIDSourceTabs.has(previousTab)) { michael@0: let pageID = this.pageIDSourceTabs.get(previousTab); michael@0: this.setExpiringTelemetryBucket(pageID, "inactive"); michael@0: } michael@0: } michael@0: michael@0: let window = aEvent.target.ownerDocument.defaultView; michael@0: let selectedTab = window.gBrowser.selectedTab; michael@0: let pinnedTab = this.pinnedTabs.get(window); michael@0: if (pinnedTab && pinnedTab.tab == selectedTab) michael@0: break; michael@0: let originTabs = this.originTabs.get(window); michael@0: if (originTabs && originTabs.has(selectedTab)) michael@0: break; michael@0: michael@0: let pendingDoc; michael@0: if (this._detachingTab && this._pendingDoc && (pendingDoc = this._pendingDoc.get())) { michael@0: if (selectedTab.linkedBrowser.contentDocument == pendingDoc) { michael@0: if (!this.originTabs.get(window)) { michael@0: this.originTabs.set(window, new Set()); michael@0: } michael@0: this.originTabs.get(window).add(selectedTab); michael@0: this.pendingDoc = null; michael@0: this._detachingTab = false; michael@0: while (this._queuedEvents.length) { michael@0: try { michael@0: this.onPageEvent(this._queuedEvents.shift()); michael@0: } catch (ex) { michael@0: Cu.reportError(ex); michael@0: } michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: michael@0: this.teardownTour(window); michael@0: break; michael@0: } michael@0: michael@0: case "SSWindowClosing": { michael@0: let window = aEvent.target; michael@0: if (this.pageIDSourceWindows.has(window)) { michael@0: let pageID = this.pageIDSourceWindows.get(window); michael@0: this.setExpiringTelemetryBucket(pageID, "closed"); michael@0: } michael@0: michael@0: this.teardownTour(window, true); michael@0: break; michael@0: } michael@0: michael@0: case "input": { michael@0: if (aEvent.target.id == "urlbar") { michael@0: let window = aEvent.target.ownerDocument.defaultView; michael@0: this.handleUrlbarInput(window); michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: setTelemetryBucket: function(aPageID) { michael@0: let bucket = BUCKET_NAME + BrowserUITelemetry.BUCKET_SEPARATOR + aPageID; michael@0: BrowserUITelemetry.setBucket(bucket); michael@0: }, michael@0: michael@0: setExpiringTelemetryBucket: function(aPageID, aType) { michael@0: let bucket = BUCKET_NAME + BrowserUITelemetry.BUCKET_SEPARATOR + aPageID + michael@0: BrowserUITelemetry.BUCKET_SEPARATOR + aType; michael@0: michael@0: BrowserUITelemetry.setExpiringBucket(bucket, michael@0: BUCKET_TIMESTEPS); michael@0: }, michael@0: michael@0: // This is registered with UITelemetry by BrowserUITelemetry, so that UITour michael@0: // can remain lazy-loaded on-demand. michael@0: getTelemetry: function() { michael@0: return { michael@0: seenPageIDs: [...this.seenPageIDs.keys()], michael@0: }; michael@0: }, michael@0: michael@0: teardownTour: function(aWindow, aWindowClosing = false) { michael@0: aWindow.gBrowser.tabContainer.removeEventListener("TabSelect", this); michael@0: aWindow.PanelUI.panel.removeEventListener("popuphiding", this.hidePanelAnnotations); michael@0: aWindow.PanelUI.panel.removeEventListener("ViewShowing", this.hidePanelAnnotations); michael@0: aWindow.removeEventListener("SSWindowClosing", this); michael@0: michael@0: let originTabs = this.originTabs.get(aWindow); michael@0: if (originTabs) { michael@0: for (let tab of originTabs) { michael@0: tab.removeEventListener("TabClose", this); michael@0: tab.removeEventListener("TabBecomingWindow", this); michael@0: } michael@0: } michael@0: this.originTabs.delete(aWindow); michael@0: michael@0: if (!aWindowClosing) { michael@0: this.hideHighlight(aWindow); michael@0: this.hideInfo(aWindow); michael@0: // Ensure the menu panel is hidden before calling recreatePopup so popup events occur. michael@0: this.hideMenu(aWindow, "appMenu"); michael@0: } michael@0: michael@0: this.endUrlbarCapture(aWindow); michael@0: this.removePinnedTab(aWindow); michael@0: this.resetTheme(); michael@0: }, michael@0: michael@0: getChromeWindow: function(aContentDocument) { michael@0: return aContentDocument.defaultView michael@0: .window michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebNavigation) michael@0: .QueryInterface(Ci.nsIDocShellTreeItem) michael@0: .rootTreeItem michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindow) michael@0: .wrappedJSObject; michael@0: }, michael@0: michael@0: importPermissions: function() { michael@0: try { michael@0: PermissionsUtils.importFromPrefs(PREF_PERM_BRANCH, UITOUR_PERMISSION); michael@0: } catch (e) { michael@0: Cu.reportError(e); michael@0: } michael@0: }, michael@0: michael@0: ensureTrustedOrigin: function(aDocument) { michael@0: if (aDocument.defaultView.top != aDocument.defaultView) michael@0: return false; michael@0: michael@0: let uri = aDocument.documentURIObject; michael@0: michael@0: if (uri.schemeIs("chrome")) michael@0: return true; michael@0: michael@0: if (!this.isSafeScheme(uri)) michael@0: return false; michael@0: michael@0: this.importPermissions(); michael@0: let permission = Services.perms.testPermission(uri, UITOUR_PERMISSION); michael@0: return permission == Services.perms.ALLOW_ACTION; michael@0: }, michael@0: michael@0: isSafeScheme: function(aURI) { michael@0: let allowedSchemes = new Set(["https"]); michael@0: if (!Services.prefs.getBoolPref("browser.uitour.requireSecure")) michael@0: allowedSchemes.add("http"); michael@0: michael@0: if (!allowedSchemes.has(aURI.scheme)) michael@0: return false; michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: resolveURL: function(aDocument, aURL) { michael@0: try { michael@0: let uri = Services.io.newURI(aURL, null, aDocument.documentURIObject); michael@0: michael@0: if (!this.isSafeScheme(uri)) michael@0: return null; michael@0: michael@0: return uri.spec; michael@0: } catch (e) {} michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: sendPageCallback: function(aDocument, aCallbackID, aData = {}) { michael@0: let detail = Cu.createObjectIn(aDocument.defaultView); michael@0: detail.data = Cu.createObjectIn(detail); michael@0: michael@0: for (let key of Object.keys(aData)) michael@0: detail.data[key] = aData[key]; michael@0: michael@0: Cu.makeObjectPropsNormal(detail.data); michael@0: Cu.makeObjectPropsNormal(detail); michael@0: michael@0: detail.callbackID = aCallbackID; michael@0: michael@0: let event = new aDocument.defaultView.CustomEvent("mozUITourResponse", { michael@0: bubbles: true, michael@0: detail: detail michael@0: }); michael@0: michael@0: aDocument.dispatchEvent(event); michael@0: }, michael@0: michael@0: isElementVisible: function(aElement) { michael@0: let targetStyle = aElement.ownerDocument.defaultView.getComputedStyle(aElement); michael@0: return (targetStyle.display != "none" && targetStyle.visibility == "visible"); michael@0: }, michael@0: michael@0: getTarget: function(aWindow, aTargetName, aSticky = false) { michael@0: let deferred = Promise.defer(); michael@0: if (typeof aTargetName != "string" || !aTargetName) { michael@0: deferred.reject("Invalid target name specified"); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: if (aTargetName == "pinnedTab") { michael@0: deferred.resolve({ michael@0: targetName: aTargetName, michael@0: node: this.ensurePinnedTab(aWindow, aSticky) michael@0: }); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: let targetObject = this.targets.get(aTargetName); michael@0: if (!targetObject) { michael@0: deferred.reject("The specified target name is not in the allowed set"); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: let targetQuery = targetObject.query; michael@0: aWindow.PanelUI.ensureReady().then(() => { michael@0: let node; michael@0: if (typeof targetQuery == "function") { michael@0: try { michael@0: node = targetQuery(aWindow.document); michael@0: } catch (ex) { michael@0: node = null; michael@0: } michael@0: } else { michael@0: node = aWindow.document.querySelector(targetQuery); michael@0: } michael@0: michael@0: deferred.resolve({ michael@0: addTargetListener: targetObject.addTargetListener, michael@0: node: node, michael@0: removeTargetListener: targetObject.removeTargetListener, michael@0: targetName: aTargetName, michael@0: widgetName: targetObject.widgetName, michael@0: }); michael@0: }).then(null, Cu.reportError); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: targetIsInAppMenu: function(aTarget) { michael@0: let placement = CustomizableUI.getPlacementOfWidget(aTarget.widgetName || aTarget.node.id); michael@0: if (placement && placement.area == CustomizableUI.AREA_PANEL) { michael@0: return true; michael@0: } michael@0: michael@0: let targetElement = aTarget.node; michael@0: // Use the widget for filtering if it exists since the target may be the icon inside. michael@0: if (aTarget.widgetName) { michael@0: targetElement = aTarget.node.ownerDocument.getElementById(aTarget.widgetName); michael@0: } michael@0: michael@0: // Handle the non-customizable buttons at the bottom of the menu which aren't proper widgets. michael@0: return targetElement.id.startsWith("PanelUI-") michael@0: && targetElement.id != "PanelUI-button"; michael@0: }, michael@0: michael@0: /** michael@0: * Called before opening or after closing a highlight or info panel to see if michael@0: * we need to open or close the appMenu to see the annotation's anchor. michael@0: */ michael@0: _setAppMenuStateForAnnotation: function(aWindow, aAnnotationType, aShouldOpenForHighlight, aCallback = null) { michael@0: // If the panel is in the desired state, we're done. michael@0: let panelIsOpen = aWindow.PanelUI.panel.state != "closed"; michael@0: if (aShouldOpenForHighlight == panelIsOpen) { michael@0: if (aCallback) michael@0: aCallback(); michael@0: return; michael@0: } michael@0: michael@0: // Don't close the menu if it wasn't opened by us (e.g. via showmenu instead). michael@0: if (!aShouldOpenForHighlight && !this.appMenuOpenForAnnotation.has(aAnnotationType)) { michael@0: if (aCallback) michael@0: aCallback(); michael@0: return; michael@0: } michael@0: michael@0: if (aShouldOpenForHighlight) { michael@0: this.appMenuOpenForAnnotation.add(aAnnotationType); michael@0: } else { michael@0: this.appMenuOpenForAnnotation.delete(aAnnotationType); michael@0: } michael@0: michael@0: // Actually show or hide the menu michael@0: if (this.appMenuOpenForAnnotation.size) { michael@0: this.showMenu(aWindow, "appMenu", aCallback); michael@0: } else { michael@0: this.hideMenu(aWindow, "appMenu"); michael@0: if (aCallback) michael@0: aCallback(); michael@0: } michael@0: michael@0: }, michael@0: michael@0: previewTheme: function(aTheme) { michael@0: let origin = Services.prefs.getCharPref("browser.uitour.themeOrigin"); michael@0: let data = LightweightThemeManager.parseTheme(aTheme, origin); michael@0: if (data) michael@0: LightweightThemeManager.previewTheme(data); michael@0: }, michael@0: michael@0: resetTheme: function() { michael@0: LightweightThemeManager.resetPreview(); michael@0: }, michael@0: michael@0: ensurePinnedTab: function(aWindow, aSticky = false) { michael@0: let tabInfo = this.pinnedTabs.get(aWindow); michael@0: michael@0: if (tabInfo) { michael@0: tabInfo.sticky = tabInfo.sticky || aSticky; michael@0: } else { michael@0: let url = Services.urlFormatter.formatURLPref("browser.uitour.pinnedTabUrl"); michael@0: michael@0: let tab = aWindow.gBrowser.addTab(url); michael@0: aWindow.gBrowser.pinTab(tab); michael@0: tab.addEventListener("TabClose", () => { michael@0: this.pinnedTabs.delete(aWindow); michael@0: }); michael@0: michael@0: tabInfo = { michael@0: tab: tab, michael@0: sticky: aSticky michael@0: }; michael@0: this.pinnedTabs.set(aWindow, tabInfo); michael@0: } michael@0: michael@0: return tabInfo.tab; michael@0: }, michael@0: michael@0: removePinnedTab: function(aWindow) { michael@0: let tabInfo = this.pinnedTabs.get(aWindow); michael@0: if (tabInfo) michael@0: aWindow.gBrowser.removeTab(tabInfo.tab); michael@0: }, michael@0: michael@0: /** michael@0: * @param aTarget The element to highlight. michael@0: * @param aEffect (optional) The effect to use from UITour.highlightEffects or "none". michael@0: * @see UITour.highlightEffects michael@0: */ michael@0: showHighlight: function(aTarget, aEffect = "none") { michael@0: function showHighlightPanel(aTargetEl) { michael@0: let highlighter = aTargetEl.ownerDocument.getElementById("UITourHighlight"); michael@0: michael@0: let effect = aEffect; michael@0: if (effect == "random") { michael@0: // Exclude "random" from the randomly selected effects. michael@0: let randomEffect = 1 + Math.floor(Math.random() * (this.highlightEffects.length - 1)); michael@0: if (randomEffect == this.highlightEffects.length) michael@0: randomEffect--; // On the order of 1 in 2^62 chance of this happening. michael@0: effect = this.highlightEffects[randomEffect]; michael@0: } michael@0: // Toggle the effect attribute to "none" and flush layout before setting it so the effect plays. michael@0: highlighter.setAttribute("active", "none"); michael@0: aTargetEl.ownerDocument.defaultView.getComputedStyle(highlighter).animationName; michael@0: highlighter.setAttribute("active", effect); michael@0: highlighter.parentElement.setAttribute("targetName", aTarget.targetName); michael@0: highlighter.parentElement.hidden = false; michael@0: michael@0: let targetRect = aTargetEl.getBoundingClientRect(); michael@0: let highlightHeight = targetRect.height; michael@0: let highlightWidth = targetRect.width; michael@0: let minDimension = Math.min(highlightHeight, highlightWidth); michael@0: let maxDimension = Math.max(highlightHeight, highlightWidth); michael@0: michael@0: // If the dimensions are within 200% of each other (to include the bookmarks button), michael@0: // make the highlight a circle with the largest dimension as the diameter. michael@0: if (maxDimension / minDimension <= 3.0) { michael@0: highlightHeight = highlightWidth = maxDimension; michael@0: highlighter.style.borderRadius = "100%"; michael@0: } else { michael@0: highlighter.style.borderRadius = ""; michael@0: } michael@0: michael@0: highlighter.style.height = highlightHeight + "px"; michael@0: highlighter.style.width = highlightWidth + "px"; michael@0: michael@0: // Close a previous highlight so we can relocate the panel. michael@0: if (highlighter.parentElement.state == "open") { michael@0: highlighter.parentElement.hidePopup(); michael@0: } michael@0: /* The "overlap" position anchors from the top-left but we want to centre highlights at their michael@0: minimum size. */ michael@0: let highlightWindow = aTargetEl.ownerDocument.defaultView; michael@0: let containerStyle = highlightWindow.getComputedStyle(highlighter.parentElement); michael@0: let paddingTopPx = 0 - parseFloat(containerStyle.paddingTop); michael@0: let paddingLeftPx = 0 - parseFloat(containerStyle.paddingLeft); michael@0: let highlightStyle = highlightWindow.getComputedStyle(highlighter); michael@0: let highlightHeightWithMin = Math.max(highlightHeight, parseFloat(highlightStyle.minHeight)); michael@0: let highlightWidthWithMin = Math.max(highlightWidth, parseFloat(highlightStyle.minWidth)); michael@0: let offsetX = paddingTopPx michael@0: - (Math.max(0, highlightWidthWithMin - targetRect.width) / 2); michael@0: let offsetY = paddingLeftPx michael@0: - (Math.max(0, highlightHeightWithMin - targetRect.height) / 2); michael@0: michael@0: this._addAnnotationPanelMutationObserver(highlighter.parentElement); michael@0: highlighter.parentElement.openPopup(aTargetEl, "overlap", offsetX, offsetY); michael@0: } michael@0: michael@0: // Prevent showing a panel at an undefined position. michael@0: if (!this.isElementVisible(aTarget.node)) michael@0: return; michael@0: michael@0: this._setAppMenuStateForAnnotation(aTarget.node.ownerDocument.defaultView, "highlight", michael@0: this.targetIsInAppMenu(aTarget), michael@0: showHighlightPanel.bind(this, aTarget.node)); michael@0: }, michael@0: michael@0: hideHighlight: function(aWindow) { michael@0: let tabData = this.pinnedTabs.get(aWindow); michael@0: if (tabData && !tabData.sticky) michael@0: this.removePinnedTab(aWindow); michael@0: michael@0: let highlighter = aWindow.document.getElementById("UITourHighlight"); michael@0: this._removeAnnotationPanelMutationObserver(highlighter.parentElement); michael@0: highlighter.parentElement.hidePopup(); michael@0: highlighter.removeAttribute("active"); michael@0: michael@0: this._setAppMenuStateForAnnotation(aWindow, "highlight", false); michael@0: }, michael@0: michael@0: /** michael@0: * Show an info panel. michael@0: * michael@0: * @param {Document} aContentDocument michael@0: * @param {Node} aAnchor michael@0: * @param {String} [aTitle=""] michael@0: * @param {String} [aDescription=""] michael@0: * @param {String} [aIconURL=""] michael@0: * @param {Object[]} [aButtons=[]] michael@0: * @param {Object} [aOptions={}] michael@0: * @param {String} [aOptions.closeButtonCallbackID] michael@0: */ michael@0: showInfo: function(aContentDocument, aAnchor, aTitle = "", aDescription = "", aIconURL = "", michael@0: aButtons = [], aOptions = {}) { michael@0: function showInfoPanel(aAnchorEl) { michael@0: aAnchorEl.focus(); michael@0: michael@0: let document = aAnchorEl.ownerDocument; michael@0: let tooltip = document.getElementById("UITourTooltip"); michael@0: let tooltipTitle = document.getElementById("UITourTooltipTitle"); michael@0: let tooltipDesc = document.getElementById("UITourTooltipDescription"); michael@0: let tooltipIcon = document.getElementById("UITourTooltipIcon"); michael@0: let tooltipButtons = document.getElementById("UITourTooltipButtons"); michael@0: michael@0: if (tooltip.state == "open") { michael@0: tooltip.hidePopup(); michael@0: } michael@0: michael@0: tooltipTitle.textContent = aTitle || ""; michael@0: tooltipDesc.textContent = aDescription || ""; michael@0: tooltipIcon.src = aIconURL || ""; michael@0: tooltipIcon.hidden = !aIconURL; michael@0: michael@0: while (tooltipButtons.firstChild) michael@0: tooltipButtons.firstChild.remove(); michael@0: michael@0: for (let button of aButtons) { michael@0: let el = document.createElement("button"); michael@0: el.setAttribute("label", button.label); michael@0: if (button.iconURL) michael@0: el.setAttribute("image", button.iconURL); michael@0: michael@0: if (button.style == "link") michael@0: el.setAttribute("class", "button-link"); michael@0: michael@0: if (button.style == "primary") michael@0: el.setAttribute("class", "button-primary"); michael@0: michael@0: let callbackID = button.callbackID; michael@0: el.addEventListener("command", event => { michael@0: tooltip.hidePopup(); michael@0: this.sendPageCallback(aContentDocument, callbackID); michael@0: }); michael@0: michael@0: tooltipButtons.appendChild(el); michael@0: } michael@0: michael@0: tooltipButtons.hidden = !aButtons.length; michael@0: michael@0: let tooltipClose = document.getElementById("UITourTooltipClose"); michael@0: let closeButtonCallback = (event) => { michael@0: this.hideInfo(document.defaultView); michael@0: if (aOptions && aOptions.closeButtonCallbackID) michael@0: this.sendPageCallback(aContentDocument, aOptions.closeButtonCallbackID); michael@0: }; michael@0: tooltipClose.addEventListener("command", closeButtonCallback); michael@0: michael@0: let targetCallback = (event) => { michael@0: let details = { michael@0: target: aAnchor.targetName, michael@0: type: event.type, michael@0: }; michael@0: this.sendPageCallback(aContentDocument, aOptions.targetCallbackID, details); michael@0: }; michael@0: if (aOptions.targetCallbackID && aAnchor.addTargetListener) { michael@0: aAnchor.addTargetListener(document, targetCallback); michael@0: } michael@0: michael@0: tooltip.addEventListener("popuphiding", function tooltipHiding(event) { michael@0: tooltip.removeEventListener("popuphiding", tooltipHiding); michael@0: tooltipClose.removeEventListener("command", closeButtonCallback); michael@0: if (aOptions.targetCallbackID && aAnchor.removeTargetListener) { michael@0: aAnchor.removeTargetListener(document, targetCallback); michael@0: } michael@0: }); michael@0: michael@0: tooltip.setAttribute("targetName", aAnchor.targetName); michael@0: tooltip.hidden = false; michael@0: let alignment = "bottomcenter topright"; michael@0: this._addAnnotationPanelMutationObserver(tooltip); michael@0: tooltip.openPopup(aAnchorEl, alignment); michael@0: } michael@0: michael@0: // Prevent showing a panel at an undefined position. michael@0: if (!this.isElementVisible(aAnchor.node)) michael@0: return; michael@0: michael@0: this._setAppMenuStateForAnnotation(aAnchor.node.ownerDocument.defaultView, "info", michael@0: this.targetIsInAppMenu(aAnchor), michael@0: showInfoPanel.bind(this, aAnchor.node)); michael@0: }, michael@0: michael@0: hideInfo: function(aWindow) { michael@0: let document = aWindow.document; michael@0: michael@0: let tooltip = document.getElementById("UITourTooltip"); michael@0: this._removeAnnotationPanelMutationObserver(tooltip); michael@0: tooltip.hidePopup(); michael@0: this._setAppMenuStateForAnnotation(aWindow, "info", false); michael@0: michael@0: let tooltipButtons = document.getElementById("UITourTooltipButtons"); michael@0: while (tooltipButtons.firstChild) michael@0: tooltipButtons.firstChild.remove(); michael@0: }, michael@0: michael@0: showMenu: function(aWindow, aMenuName, aOpenCallback = null) { michael@0: function openMenuButton(aID) { michael@0: let menuBtn = aWindow.document.getElementById(aID); michael@0: if (!menuBtn || !menuBtn.boxObject) { michael@0: aOpenCallback(); michael@0: return; michael@0: } michael@0: if (aOpenCallback) michael@0: menuBtn.addEventListener("popupshown", onPopupShown); michael@0: menuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(true); michael@0: } michael@0: function onPopupShown(event) { michael@0: this.removeEventListener("popupshown", onPopupShown); michael@0: aOpenCallback(event); michael@0: } michael@0: michael@0: if (aMenuName == "appMenu") { michael@0: aWindow.PanelUI.panel.setAttribute("noautohide", "true"); michael@0: // If the popup is already opened, don't recreate the widget as it may cause a flicker. michael@0: if (aWindow.PanelUI.panel.state != "open") { michael@0: this.recreatePopup(aWindow.PanelUI.panel); michael@0: } michael@0: aWindow.PanelUI.panel.addEventListener("popuphiding", this.hidePanelAnnotations); michael@0: aWindow.PanelUI.panel.addEventListener("ViewShowing", this.hidePanelAnnotations); michael@0: if (aOpenCallback) { michael@0: aWindow.PanelUI.panel.addEventListener("popupshown", onPopupShown); michael@0: } michael@0: aWindow.PanelUI.show(); michael@0: } else if (aMenuName == "bookmarks") { michael@0: openMenuButton("bookmarks-menu-button"); michael@0: } michael@0: }, michael@0: michael@0: hideMenu: function(aWindow, aMenuName) { michael@0: function closeMenuButton(aID) { michael@0: let menuBtn = aWindow.document.getElementById(aID); michael@0: if (menuBtn && menuBtn.boxObject) michael@0: menuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(false); michael@0: } michael@0: michael@0: if (aMenuName == "appMenu") { michael@0: aWindow.PanelUI.panel.removeAttribute("noautohide"); michael@0: aWindow.PanelUI.hide(); michael@0: this.recreatePopup(aWindow.PanelUI.panel); michael@0: } else if (aMenuName == "bookmarks") { michael@0: closeMenuButton("bookmarks-menu-button"); michael@0: } michael@0: }, michael@0: michael@0: hidePanelAnnotations: function(aEvent) { michael@0: let win = aEvent.target.ownerDocument.defaultView; michael@0: let annotationElements = new Map([ michael@0: // [annotationElement (panel), method to hide the annotation] michael@0: [win.document.getElementById("UITourHighlightContainer"), UITour.hideHighlight.bind(UITour)], michael@0: [win.document.getElementById("UITourTooltip"), UITour.hideInfo.bind(UITour)], michael@0: ]); michael@0: annotationElements.forEach((hideMethod, annotationElement) => { michael@0: if (annotationElement.state != "closed") { michael@0: let targetName = annotationElement.getAttribute("targetName"); michael@0: UITour.getTarget(win, targetName).then((aTarget) => { michael@0: // Since getTarget is async, we need to make sure that the target hasn't michael@0: // changed since it may have just moved to somewhere outside of the app menu. michael@0: if (annotationElement.getAttribute("targetName") != aTarget.targetName || michael@0: annotationElement.state == "closed" || michael@0: !UITour.targetIsInAppMenu(aTarget)) { michael@0: return; michael@0: } michael@0: hideMethod(win); michael@0: }).then(null, Cu.reportError); michael@0: } michael@0: }); michael@0: UITour.appMenuOpenForAnnotation.clear(); michael@0: }, michael@0: michael@0: recreatePopup: function(aPanel) { michael@0: // After changing popup attributes that relate to how the native widget is created michael@0: // (e.g. @noautohide) we need to re-create the frame/widget for it to take effect. michael@0: if (aPanel.hidden) { michael@0: // If the panel is already hidden, we don't need to recreate it but flush michael@0: // in case someone just hid it. michael@0: aPanel.clientWidth; // flush michael@0: return; michael@0: } michael@0: aPanel.hidden = true; michael@0: aPanel.clientWidth; // flush michael@0: aPanel.hidden = false; michael@0: }, michael@0: michael@0: startUrlbarCapture: function(aWindow, aExpectedText, aUrl) { michael@0: let urlbar = aWindow.document.getElementById("urlbar"); michael@0: this.urlbarCapture.set(aWindow, { michael@0: expected: aExpectedText.toLocaleLowerCase(), michael@0: url: aUrl michael@0: }); michael@0: urlbar.addEventListener("input", this); michael@0: }, michael@0: michael@0: endUrlbarCapture: function(aWindow) { michael@0: let urlbar = aWindow.document.getElementById("urlbar"); michael@0: urlbar.removeEventListener("input", this); michael@0: this.urlbarCapture.delete(aWindow); michael@0: }, michael@0: michael@0: handleUrlbarInput: function(aWindow) { michael@0: if (!this.urlbarCapture.has(aWindow)) michael@0: return; michael@0: michael@0: let urlbar = aWindow.document.getElementById("urlbar"); michael@0: michael@0: let {expected, url} = this.urlbarCapture.get(aWindow); michael@0: michael@0: if (urlbar.value.toLocaleLowerCase().localeCompare(expected) != 0) michael@0: return; michael@0: michael@0: urlbar.handleRevert(); michael@0: michael@0: let tab = aWindow.gBrowser.addTab(url, { michael@0: owner: aWindow.gBrowser.selectedTab, michael@0: relatedToCurrent: true michael@0: }); michael@0: aWindow.gBrowser.selectedTab = tab; michael@0: }, michael@0: michael@0: getConfiguration: function(aContentDocument, aConfiguration, aCallbackID) { michael@0: switch (aConfiguration) { michael@0: case "availableTargets": michael@0: this.getAvailableTargets(aContentDocument, aCallbackID); michael@0: break; michael@0: case "sync": michael@0: this.sendPageCallback(aContentDocument, aCallbackID, { michael@0: setup: Services.prefs.prefHasUserValue("services.sync.username"), michael@0: }); michael@0: break; michael@0: default: michael@0: Cu.reportError("getConfiguration: Unknown configuration requested: " + aConfiguration); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: getAvailableTargets: function(aContentDocument, aCallbackID) { michael@0: let window = this.getChromeWindow(aContentDocument); michael@0: let data = this.availableTargetsCache.get(window); michael@0: if (data) { michael@0: this.sendPageCallback(aContentDocument, aCallbackID, data); michael@0: return; michael@0: } michael@0: michael@0: let promises = []; michael@0: for (let targetName of this.targets.keys()) { michael@0: promises.push(this.getTarget(window, targetName)); michael@0: } michael@0: Promise.all(promises).then((targetObjects) => { michael@0: let targetNames = [ michael@0: "pinnedTab", michael@0: ]; michael@0: for (let targetObject of targetObjects) { michael@0: if (targetObject.node) michael@0: targetNames.push(targetObject.targetName); michael@0: } michael@0: let data = { michael@0: targets: targetNames, michael@0: }; michael@0: this.availableTargetsCache.set(window, data); michael@0: this.sendPageCallback(aContentDocument, aCallbackID, data); michael@0: }, (err) => { michael@0: Cu.reportError(err); michael@0: this.sendPageCallback(aContentDocument, aCallbackID, { michael@0: targets: [], michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: _addAnnotationPanelMutationObserver: function(aPanelEl) { michael@0: #ifdef XP_LINUX michael@0: let observer = this._annotationPanelMutationObservers.get(aPanelEl); michael@0: if (observer) { michael@0: return; michael@0: } michael@0: let win = aPanelEl.ownerDocument.defaultView; michael@0: observer = new win.MutationObserver(this._annotationMutationCallback); michael@0: this._annotationPanelMutationObservers.set(aPanelEl, observer); michael@0: let observerOptions = { michael@0: attributeFilter: ["height", "width"], michael@0: attributes: true, michael@0: }; michael@0: observer.observe(aPanelEl, observerOptions); michael@0: #endif michael@0: }, michael@0: michael@0: _removeAnnotationPanelMutationObserver: function(aPanelEl) { michael@0: #ifdef XP_LINUX michael@0: let observer = this._annotationPanelMutationObservers.get(aPanelEl); michael@0: if (observer) { michael@0: observer.disconnect(); michael@0: this._annotationPanelMutationObservers.delete(aPanelEl); michael@0: } michael@0: #endif michael@0: }, michael@0: michael@0: /** michael@0: * Workaround for Ubuntu panel craziness in bug 970788 where incorrect sizes get passed to michael@0: * nsXULPopupManager::PopupResized and lead to incorrect width and height attributes getting michael@0: * set on the panel. michael@0: */ michael@0: _annotationMutationCallback: function(aMutations) { michael@0: for (let mutation of aMutations) { michael@0: // Remove both attributes at once and ignore remaining mutations to be proccessed. michael@0: mutation.target.removeAttribute("width"); michael@0: mutation.target.removeAttribute("height"); michael@0: return; michael@0: } michael@0: }, michael@0: }; michael@0: michael@0: this.UITour.init();