1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/modules/UITour.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1225 @@ 1.4 +// This Source Code Form is subject to the terms of the Mozilla Public 1.5 +// License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 +// file, You can obtain one at http://mozilla.org/MPL/2.0/. 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +this.EXPORTED_SYMBOLS = ["UITour"]; 1.11 + 1.12 +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; 1.13 + 1.14 +Cu.import("resource://gre/modules/Services.jsm"); 1.15 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.16 +Cu.import("resource://gre/modules/Promise.jsm"); 1.17 + 1.18 +XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", 1.19 + "resource://gre/modules/LightweightThemeManager.jsm"); 1.20 +XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils", 1.21 + "resource://gre/modules/PermissionsUtils.jsm"); 1.22 +XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", 1.23 + "resource:///modules/CustomizableUI.jsm"); 1.24 +XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", 1.25 + "resource://gre/modules/UITelemetry.jsm"); 1.26 +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry", 1.27 + "resource:///modules/BrowserUITelemetry.jsm"); 1.28 + 1.29 + 1.30 +const UITOUR_PERMISSION = "uitour"; 1.31 +const PREF_PERM_BRANCH = "browser.uitour."; 1.32 +const PREF_SEENPAGEIDS = "browser.uitour.seenPageIDs"; 1.33 +const MAX_BUTTONS = 4; 1.34 + 1.35 +const BUCKET_NAME = "UITour"; 1.36 +const BUCKET_TIMESTEPS = [ 1.37 + 1 * 60 * 1000, // Until 1 minute after tab is closed/inactive. 1.38 + 3 * 60 * 1000, // Until 3 minutes after tab is closed/inactive. 1.39 + 10 * 60 * 1000, // Until 10 minutes after tab is closed/inactive. 1.40 + 60 * 60 * 1000, // Until 1 hour after tab is closed/inactive. 1.41 +]; 1.42 + 1.43 +// Time after which seen Page IDs expire. 1.44 +const SEENPAGEID_EXPIRY = 8 * 7 * 24 * 60 * 60 * 1000; // 8 weeks. 1.45 + 1.46 + 1.47 +this.UITour = { 1.48 + url: null, 1.49 + seenPageIDs: null, 1.50 + pageIDSourceTabs: new WeakMap(), 1.51 + pageIDSourceWindows: new WeakMap(), 1.52 + /* Map from browser windows to a set of tabs in which a tour is open */ 1.53 + originTabs: new WeakMap(), 1.54 + /* Map from browser windows to a set of pinned tabs opened by (a) tour(s) */ 1.55 + pinnedTabs: new WeakMap(), 1.56 + urlbarCapture: new WeakMap(), 1.57 + appMenuOpenForAnnotation: new Set(), 1.58 + availableTargetsCache: new WeakMap(), 1.59 + 1.60 + _detachingTab: false, 1.61 + _annotationPanelMutationObservers: new WeakMap(), 1.62 + _queuedEvents: [], 1.63 + _pendingDoc: null, 1.64 + 1.65 + highlightEffects: ["random", "wobble", "zoom", "color"], 1.66 + targets: new Map([ 1.67 + ["accountStatus", { 1.68 + query: (aDocument) => { 1.69 + let statusButton = aDocument.getElementById("PanelUI-fxa-status"); 1.70 + return aDocument.getAnonymousElementByAttribute(statusButton, 1.71 + "class", 1.72 + "toolbarbutton-icon"); 1.73 + }, 1.74 + widgetName: "PanelUI-fxa-status", 1.75 + }], 1.76 + ["addons", {query: "#add-ons-button"}], 1.77 + ["appMenu", { 1.78 + addTargetListener: (aDocument, aCallback) => { 1.79 + let panelPopup = aDocument.getElementById("PanelUI-popup"); 1.80 + panelPopup.addEventListener("popupshown", aCallback); 1.81 + }, 1.82 + query: "#PanelUI-button", 1.83 + removeTargetListener: (aDocument, aCallback) => { 1.84 + let panelPopup = aDocument.getElementById("PanelUI-popup"); 1.85 + panelPopup.removeEventListener("popupshown", aCallback); 1.86 + }, 1.87 + }], 1.88 + ["backForward", { 1.89 + query: "#back-button", 1.90 + widgetName: "urlbar-container", 1.91 + }], 1.92 + ["bookmarks", {query: "#bookmarks-menu-button"}], 1.93 + ["customize", { 1.94 + query: (aDocument) => { 1.95 + let customizeButton = aDocument.getElementById("PanelUI-customize"); 1.96 + return aDocument.getAnonymousElementByAttribute(customizeButton, 1.97 + "class", 1.98 + "toolbarbutton-icon"); 1.99 + }, 1.100 + widgetName: "PanelUI-customize", 1.101 + }], 1.102 + ["help", {query: "#PanelUI-help"}], 1.103 + ["home", {query: "#home-button"}], 1.104 + ["quit", {query: "#PanelUI-quit"}], 1.105 + ["search", { 1.106 + query: "#searchbar", 1.107 + widgetName: "search-container", 1.108 + }], 1.109 + ["searchProvider", { 1.110 + query: (aDocument) => { 1.111 + let searchbar = aDocument.getElementById("searchbar"); 1.112 + return aDocument.getAnonymousElementByAttribute(searchbar, 1.113 + "anonid", 1.114 + "searchbar-engine-button"); 1.115 + }, 1.116 + widgetName: "search-container", 1.117 + }], 1.118 + ["selectedTabIcon", { 1.119 + query: (aDocument) => { 1.120 + let selectedtab = aDocument.defaultView.gBrowser.selectedTab; 1.121 + let element = aDocument.getAnonymousElementByAttribute(selectedtab, 1.122 + "anonid", 1.123 + "tab-icon-image"); 1.124 + if (!element || !UITour.isElementVisible(element)) { 1.125 + return null; 1.126 + } 1.127 + return element; 1.128 + }, 1.129 + }], 1.130 + ["urlbar", { 1.131 + query: "#urlbar", 1.132 + widgetName: "urlbar-container", 1.133 + }], 1.134 + ]), 1.135 + 1.136 + init: function() { 1.137 + // Lazy getter is initialized here so it can be replicated any time 1.138 + // in a test. 1.139 + delete this.seenPageIDs; 1.140 + Object.defineProperty(this, "seenPageIDs", { 1.141 + get: this.restoreSeenPageIDs.bind(this), 1.142 + configurable: true, 1.143 + }); 1.144 + 1.145 + delete this.url; 1.146 + XPCOMUtils.defineLazyGetter(this, "url", function () { 1.147 + return Services.urlFormatter.formatURLPref("browser.uitour.url"); 1.148 + }); 1.149 + 1.150 + // Clear the availableTargetsCache on widget changes. 1.151 + let listenerMethods = [ 1.152 + "onWidgetAdded", 1.153 + "onWidgetMoved", 1.154 + "onWidgetRemoved", 1.155 + "onWidgetReset", 1.156 + "onAreaReset", 1.157 + ]; 1.158 + CustomizableUI.addListener(listenerMethods.reduce((listener, method) => { 1.159 + listener[method] = () => this.availableTargetsCache.clear(); 1.160 + return listener; 1.161 + }, {})); 1.162 + }, 1.163 + 1.164 + restoreSeenPageIDs: function() { 1.165 + delete this.seenPageIDs; 1.166 + 1.167 + if (UITelemetry.enabled) { 1.168 + let dateThreshold = Date.now() - SEENPAGEID_EXPIRY; 1.169 + 1.170 + try { 1.171 + let data = Services.prefs.getCharPref(PREF_SEENPAGEIDS); 1.172 + data = new Map(JSON.parse(data)); 1.173 + 1.174 + for (let [pageID, details] of data) { 1.175 + 1.176 + if (typeof pageID != "string" || 1.177 + typeof details != "object" || 1.178 + typeof details.lastSeen != "number" || 1.179 + details.lastSeen < dateThreshold) { 1.180 + 1.181 + data.delete(pageID); 1.182 + } 1.183 + } 1.184 + 1.185 + this.seenPageIDs = data; 1.186 + } catch (e) {} 1.187 + } 1.188 + 1.189 + if (!this.seenPageIDs) 1.190 + this.seenPageIDs = new Map(); 1.191 + 1.192 + this.persistSeenIDs(); 1.193 + 1.194 + return this.seenPageIDs; 1.195 + }, 1.196 + 1.197 + addSeenPageID: function(aPageID) { 1.198 + if (!UITelemetry.enabled) 1.199 + return; 1.200 + 1.201 + this.seenPageIDs.set(aPageID, { 1.202 + lastSeen: Date.now(), 1.203 + }); 1.204 + 1.205 + this.persistSeenIDs(); 1.206 + }, 1.207 + 1.208 + persistSeenIDs: function() { 1.209 + if (this.seenPageIDs.size === 0) { 1.210 + Services.prefs.clearUserPref(PREF_SEENPAGEIDS); 1.211 + return; 1.212 + } 1.213 + 1.214 + Services.prefs.setCharPref(PREF_SEENPAGEIDS, 1.215 + JSON.stringify([...this.seenPageIDs])); 1.216 + }, 1.217 + 1.218 + onPageEvent: function(aEvent) { 1.219 + let contentDocument = null; 1.220 + if (aEvent.target instanceof Ci.nsIDOMHTMLDocument) 1.221 + contentDocument = aEvent.target; 1.222 + else if (aEvent.target instanceof Ci.nsIDOMHTMLElement) 1.223 + contentDocument = aEvent.target.ownerDocument; 1.224 + else 1.225 + return false; 1.226 + 1.227 + // Ignore events if they're not from a trusted origin. 1.228 + if (!this.ensureTrustedOrigin(contentDocument)) 1.229 + return false; 1.230 + 1.231 + if (typeof aEvent.detail != "object") 1.232 + return false; 1.233 + 1.234 + let action = aEvent.detail.action; 1.235 + if (typeof action != "string" || !action) 1.236 + return false; 1.237 + 1.238 + let data = aEvent.detail.data; 1.239 + if (typeof data != "object") 1.240 + return false; 1.241 + 1.242 + let window = this.getChromeWindow(contentDocument); 1.243 + // Do this before bailing if there's no tab, so later we can pick up the pieces: 1.244 + window.gBrowser.tabContainer.addEventListener("TabSelect", this); 1.245 + let tab = window.gBrowser._getTabForContentWindow(contentDocument.defaultView); 1.246 + if (!tab) { 1.247 + // This should only happen while detaching a tab: 1.248 + if (this._detachingTab) { 1.249 + this._queuedEvents.push(aEvent); 1.250 + this._pendingDoc = Cu.getWeakReference(contentDocument); 1.251 + return; 1.252 + } 1.253 + Cu.reportError("Discarding tabless UITour event (" + action + ") while not detaching a tab." + 1.254 + "This shouldn't happen!"); 1.255 + return; 1.256 + } 1.257 + 1.258 + switch (action) { 1.259 + case "registerPageID": { 1.260 + // This is only relevant if Telemtry is enabled. 1.261 + if (!UITelemetry.enabled) 1.262 + break; 1.263 + 1.264 + // We don't want to allow BrowserUITelemetry.BUCKET_SEPARATOR in the 1.265 + // pageID, as it could make parsing the telemetry bucket name difficult. 1.266 + if (typeof data.pageID == "string" && 1.267 + !data.pageID.contains(BrowserUITelemetry.BUCKET_SEPARATOR)) { 1.268 + this.addSeenPageID(data.pageID); 1.269 + 1.270 + // Store tabs and windows separately so we don't need to loop over all 1.271 + // tabs when a window is closed. 1.272 + this.pageIDSourceTabs.set(tab, data.pageID); 1.273 + this.pageIDSourceWindows.set(window, data.pageID); 1.274 + 1.275 + this.setTelemetryBucket(data.pageID); 1.276 + } 1.277 + break; 1.278 + } 1.279 + 1.280 + case "showHighlight": { 1.281 + let targetPromise = this.getTarget(window, data.target); 1.282 + targetPromise.then(target => { 1.283 + if (!target.node) { 1.284 + Cu.reportError("UITour: Target could not be resolved: " + data.target); 1.285 + return; 1.286 + } 1.287 + let effect = undefined; 1.288 + if (this.highlightEffects.indexOf(data.effect) !== -1) { 1.289 + effect = data.effect; 1.290 + } 1.291 + this.showHighlight(target, effect); 1.292 + }).then(null, Cu.reportError); 1.293 + break; 1.294 + } 1.295 + 1.296 + case "hideHighlight": { 1.297 + this.hideHighlight(window); 1.298 + break; 1.299 + } 1.300 + 1.301 + case "showInfo": { 1.302 + let targetPromise = this.getTarget(window, data.target, true); 1.303 + targetPromise.then(target => { 1.304 + if (!target.node) { 1.305 + Cu.reportError("UITour: Target could not be resolved: " + data.target); 1.306 + return; 1.307 + } 1.308 + 1.309 + let iconURL = null; 1.310 + if (typeof data.icon == "string") 1.311 + iconURL = this.resolveURL(contentDocument, data.icon); 1.312 + 1.313 + let buttons = []; 1.314 + if (Array.isArray(data.buttons) && data.buttons.length > 0) { 1.315 + for (let buttonData of data.buttons) { 1.316 + if (typeof buttonData == "object" && 1.317 + typeof buttonData.label == "string" && 1.318 + typeof buttonData.callbackID == "string") { 1.319 + let button = { 1.320 + label: buttonData.label, 1.321 + callbackID: buttonData.callbackID, 1.322 + }; 1.323 + 1.324 + if (typeof buttonData.icon == "string") 1.325 + button.iconURL = this.resolveURL(contentDocument, buttonData.icon); 1.326 + 1.327 + if (typeof buttonData.style == "string") 1.328 + button.style = buttonData.style; 1.329 + 1.330 + buttons.push(button); 1.331 + 1.332 + if (buttons.length == MAX_BUTTONS) 1.333 + break; 1.334 + } 1.335 + } 1.336 + } 1.337 + 1.338 + let infoOptions = {}; 1.339 + 1.340 + if (typeof data.closeButtonCallbackID == "string") 1.341 + infoOptions.closeButtonCallbackID = data.closeButtonCallbackID; 1.342 + if (typeof data.targetCallbackID == "string") 1.343 + infoOptions.targetCallbackID = data.targetCallbackID; 1.344 + 1.345 + this.showInfo(contentDocument, target, data.title, data.text, iconURL, buttons, infoOptions); 1.346 + }).then(null, Cu.reportError); 1.347 + break; 1.348 + } 1.349 + 1.350 + case "hideInfo": { 1.351 + this.hideInfo(window); 1.352 + break; 1.353 + } 1.354 + 1.355 + case "previewTheme": { 1.356 + this.previewTheme(data.theme); 1.357 + break; 1.358 + } 1.359 + 1.360 + case "resetTheme": { 1.361 + this.resetTheme(); 1.362 + break; 1.363 + } 1.364 + 1.365 + case "addPinnedTab": { 1.366 + this.ensurePinnedTab(window, true); 1.367 + break; 1.368 + } 1.369 + 1.370 + case "removePinnedTab": { 1.371 + this.removePinnedTab(window); 1.372 + break; 1.373 + } 1.374 + 1.375 + case "showMenu": { 1.376 + this.showMenu(window, data.name); 1.377 + break; 1.378 + } 1.379 + 1.380 + case "hideMenu": { 1.381 + this.hideMenu(window, data.name); 1.382 + break; 1.383 + } 1.384 + 1.385 + case "startUrlbarCapture": { 1.386 + if (typeof data.text != "string" || !data.text || 1.387 + typeof data.url != "string" || !data.url) { 1.388 + return false; 1.389 + } 1.390 + 1.391 + let uri = null; 1.392 + try { 1.393 + uri = Services.io.newURI(data.url, null, null); 1.394 + } catch (e) { 1.395 + return false; 1.396 + } 1.397 + 1.398 + let secman = Services.scriptSecurityManager; 1.399 + let principal = contentDocument.nodePrincipal; 1.400 + let flags = secman.DISALLOW_INHERIT_PRINCIPAL; 1.401 + try { 1.402 + secman.checkLoadURIWithPrincipal(principal, uri, flags); 1.403 + } catch (e) { 1.404 + return false; 1.405 + } 1.406 + 1.407 + this.startUrlbarCapture(window, data.text, data.url); 1.408 + break; 1.409 + } 1.410 + 1.411 + case "endUrlbarCapture": { 1.412 + this.endUrlbarCapture(window); 1.413 + break; 1.414 + } 1.415 + 1.416 + case "getConfiguration": { 1.417 + if (typeof data.configuration != "string") { 1.418 + return false; 1.419 + } 1.420 + 1.421 + this.getConfiguration(contentDocument, data.configuration, data.callbackID); 1.422 + break; 1.423 + } 1.424 + 1.425 + case "showFirefoxAccounts": { 1.426 + // 'signup' is the only action that makes sense currently, so we don't 1.427 + // accept arbitrary actions just to be safe... 1.428 + // We want to replace the current tab. 1.429 + contentDocument.location.href = "about:accounts?action=signup"; 1.430 + break; 1.431 + } 1.432 + } 1.433 + 1.434 + if (!this.originTabs.has(window)) 1.435 + this.originTabs.set(window, new Set()); 1.436 + 1.437 + this.originTabs.get(window).add(tab); 1.438 + tab.addEventListener("TabClose", this); 1.439 + tab.addEventListener("TabBecomingWindow", this); 1.440 + window.addEventListener("SSWindowClosing", this); 1.441 + 1.442 + return true; 1.443 + }, 1.444 + 1.445 + handleEvent: function(aEvent) { 1.446 + switch (aEvent.type) { 1.447 + case "pagehide": { 1.448 + let window = this.getChromeWindow(aEvent.target); 1.449 + this.teardownTour(window); 1.450 + break; 1.451 + } 1.452 + 1.453 + case "TabBecomingWindow": 1.454 + this._detachingTab = true; 1.455 + // Fall through 1.456 + case "TabClose": { 1.457 + let tab = aEvent.target; 1.458 + if (this.pageIDSourceTabs.has(tab)) { 1.459 + let pageID = this.pageIDSourceTabs.get(tab); 1.460 + 1.461 + // Delete this from the window cache, so if the window is closed we 1.462 + // don't expire this page ID twice. 1.463 + let window = tab.ownerDocument.defaultView; 1.464 + if (this.pageIDSourceWindows.get(window) == pageID) 1.465 + this.pageIDSourceWindows.delete(window); 1.466 + 1.467 + this.setExpiringTelemetryBucket(pageID, "closed"); 1.468 + } 1.469 + 1.470 + let window = tab.ownerDocument.defaultView; 1.471 + this.teardownTour(window); 1.472 + break; 1.473 + } 1.474 + 1.475 + case "TabSelect": { 1.476 + if (aEvent.detail && aEvent.detail.previousTab) { 1.477 + let previousTab = aEvent.detail.previousTab; 1.478 + 1.479 + if (this.pageIDSourceTabs.has(previousTab)) { 1.480 + let pageID = this.pageIDSourceTabs.get(previousTab); 1.481 + this.setExpiringTelemetryBucket(pageID, "inactive"); 1.482 + } 1.483 + } 1.484 + 1.485 + let window = aEvent.target.ownerDocument.defaultView; 1.486 + let selectedTab = window.gBrowser.selectedTab; 1.487 + let pinnedTab = this.pinnedTabs.get(window); 1.488 + if (pinnedTab && pinnedTab.tab == selectedTab) 1.489 + break; 1.490 + let originTabs = this.originTabs.get(window); 1.491 + if (originTabs && originTabs.has(selectedTab)) 1.492 + break; 1.493 + 1.494 + let pendingDoc; 1.495 + if (this._detachingTab && this._pendingDoc && (pendingDoc = this._pendingDoc.get())) { 1.496 + if (selectedTab.linkedBrowser.contentDocument == pendingDoc) { 1.497 + if (!this.originTabs.get(window)) { 1.498 + this.originTabs.set(window, new Set()); 1.499 + } 1.500 + this.originTabs.get(window).add(selectedTab); 1.501 + this.pendingDoc = null; 1.502 + this._detachingTab = false; 1.503 + while (this._queuedEvents.length) { 1.504 + try { 1.505 + this.onPageEvent(this._queuedEvents.shift()); 1.506 + } catch (ex) { 1.507 + Cu.reportError(ex); 1.508 + } 1.509 + } 1.510 + break; 1.511 + } 1.512 + } 1.513 + 1.514 + this.teardownTour(window); 1.515 + break; 1.516 + } 1.517 + 1.518 + case "SSWindowClosing": { 1.519 + let window = aEvent.target; 1.520 + if (this.pageIDSourceWindows.has(window)) { 1.521 + let pageID = this.pageIDSourceWindows.get(window); 1.522 + this.setExpiringTelemetryBucket(pageID, "closed"); 1.523 + } 1.524 + 1.525 + this.teardownTour(window, true); 1.526 + break; 1.527 + } 1.528 + 1.529 + case "input": { 1.530 + if (aEvent.target.id == "urlbar") { 1.531 + let window = aEvent.target.ownerDocument.defaultView; 1.532 + this.handleUrlbarInput(window); 1.533 + } 1.534 + break; 1.535 + } 1.536 + } 1.537 + }, 1.538 + 1.539 + setTelemetryBucket: function(aPageID) { 1.540 + let bucket = BUCKET_NAME + BrowserUITelemetry.BUCKET_SEPARATOR + aPageID; 1.541 + BrowserUITelemetry.setBucket(bucket); 1.542 + }, 1.543 + 1.544 + setExpiringTelemetryBucket: function(aPageID, aType) { 1.545 + let bucket = BUCKET_NAME + BrowserUITelemetry.BUCKET_SEPARATOR + aPageID + 1.546 + BrowserUITelemetry.BUCKET_SEPARATOR + aType; 1.547 + 1.548 + BrowserUITelemetry.setExpiringBucket(bucket, 1.549 + BUCKET_TIMESTEPS); 1.550 + }, 1.551 + 1.552 + // This is registered with UITelemetry by BrowserUITelemetry, so that UITour 1.553 + // can remain lazy-loaded on-demand. 1.554 + getTelemetry: function() { 1.555 + return { 1.556 + seenPageIDs: [...this.seenPageIDs.keys()], 1.557 + }; 1.558 + }, 1.559 + 1.560 + teardownTour: function(aWindow, aWindowClosing = false) { 1.561 + aWindow.gBrowser.tabContainer.removeEventListener("TabSelect", this); 1.562 + aWindow.PanelUI.panel.removeEventListener("popuphiding", this.hidePanelAnnotations); 1.563 + aWindow.PanelUI.panel.removeEventListener("ViewShowing", this.hidePanelAnnotations); 1.564 + aWindow.removeEventListener("SSWindowClosing", this); 1.565 + 1.566 + let originTabs = this.originTabs.get(aWindow); 1.567 + if (originTabs) { 1.568 + for (let tab of originTabs) { 1.569 + tab.removeEventListener("TabClose", this); 1.570 + tab.removeEventListener("TabBecomingWindow", this); 1.571 + } 1.572 + } 1.573 + this.originTabs.delete(aWindow); 1.574 + 1.575 + if (!aWindowClosing) { 1.576 + this.hideHighlight(aWindow); 1.577 + this.hideInfo(aWindow); 1.578 + // Ensure the menu panel is hidden before calling recreatePopup so popup events occur. 1.579 + this.hideMenu(aWindow, "appMenu"); 1.580 + } 1.581 + 1.582 + this.endUrlbarCapture(aWindow); 1.583 + this.removePinnedTab(aWindow); 1.584 + this.resetTheme(); 1.585 + }, 1.586 + 1.587 + getChromeWindow: function(aContentDocument) { 1.588 + return aContentDocument.defaultView 1.589 + .window 1.590 + .QueryInterface(Ci.nsIInterfaceRequestor) 1.591 + .getInterface(Ci.nsIWebNavigation) 1.592 + .QueryInterface(Ci.nsIDocShellTreeItem) 1.593 + .rootTreeItem 1.594 + .QueryInterface(Ci.nsIInterfaceRequestor) 1.595 + .getInterface(Ci.nsIDOMWindow) 1.596 + .wrappedJSObject; 1.597 + }, 1.598 + 1.599 + importPermissions: function() { 1.600 + try { 1.601 + PermissionsUtils.importFromPrefs(PREF_PERM_BRANCH, UITOUR_PERMISSION); 1.602 + } catch (e) { 1.603 + Cu.reportError(e); 1.604 + } 1.605 + }, 1.606 + 1.607 + ensureTrustedOrigin: function(aDocument) { 1.608 + if (aDocument.defaultView.top != aDocument.defaultView) 1.609 + return false; 1.610 + 1.611 + let uri = aDocument.documentURIObject; 1.612 + 1.613 + if (uri.schemeIs("chrome")) 1.614 + return true; 1.615 + 1.616 + if (!this.isSafeScheme(uri)) 1.617 + return false; 1.618 + 1.619 + this.importPermissions(); 1.620 + let permission = Services.perms.testPermission(uri, UITOUR_PERMISSION); 1.621 + return permission == Services.perms.ALLOW_ACTION; 1.622 + }, 1.623 + 1.624 + isSafeScheme: function(aURI) { 1.625 + let allowedSchemes = new Set(["https"]); 1.626 + if (!Services.prefs.getBoolPref("browser.uitour.requireSecure")) 1.627 + allowedSchemes.add("http"); 1.628 + 1.629 + if (!allowedSchemes.has(aURI.scheme)) 1.630 + return false; 1.631 + 1.632 + return true; 1.633 + }, 1.634 + 1.635 + resolveURL: function(aDocument, aURL) { 1.636 + try { 1.637 + let uri = Services.io.newURI(aURL, null, aDocument.documentURIObject); 1.638 + 1.639 + if (!this.isSafeScheme(uri)) 1.640 + return null; 1.641 + 1.642 + return uri.spec; 1.643 + } catch (e) {} 1.644 + 1.645 + return null; 1.646 + }, 1.647 + 1.648 + sendPageCallback: function(aDocument, aCallbackID, aData = {}) { 1.649 + let detail = Cu.createObjectIn(aDocument.defaultView); 1.650 + detail.data = Cu.createObjectIn(detail); 1.651 + 1.652 + for (let key of Object.keys(aData)) 1.653 + detail.data[key] = aData[key]; 1.654 + 1.655 + Cu.makeObjectPropsNormal(detail.data); 1.656 + Cu.makeObjectPropsNormal(detail); 1.657 + 1.658 + detail.callbackID = aCallbackID; 1.659 + 1.660 + let event = new aDocument.defaultView.CustomEvent("mozUITourResponse", { 1.661 + bubbles: true, 1.662 + detail: detail 1.663 + }); 1.664 + 1.665 + aDocument.dispatchEvent(event); 1.666 + }, 1.667 + 1.668 + isElementVisible: function(aElement) { 1.669 + let targetStyle = aElement.ownerDocument.defaultView.getComputedStyle(aElement); 1.670 + return (targetStyle.display != "none" && targetStyle.visibility == "visible"); 1.671 + }, 1.672 + 1.673 + getTarget: function(aWindow, aTargetName, aSticky = false) { 1.674 + let deferred = Promise.defer(); 1.675 + if (typeof aTargetName != "string" || !aTargetName) { 1.676 + deferred.reject("Invalid target name specified"); 1.677 + return deferred.promise; 1.678 + } 1.679 + 1.680 + if (aTargetName == "pinnedTab") { 1.681 + deferred.resolve({ 1.682 + targetName: aTargetName, 1.683 + node: this.ensurePinnedTab(aWindow, aSticky) 1.684 + }); 1.685 + return deferred.promise; 1.686 + } 1.687 + 1.688 + let targetObject = this.targets.get(aTargetName); 1.689 + if (!targetObject) { 1.690 + deferred.reject("The specified target name is not in the allowed set"); 1.691 + return deferred.promise; 1.692 + } 1.693 + 1.694 + let targetQuery = targetObject.query; 1.695 + aWindow.PanelUI.ensureReady().then(() => { 1.696 + let node; 1.697 + if (typeof targetQuery == "function") { 1.698 + try { 1.699 + node = targetQuery(aWindow.document); 1.700 + } catch (ex) { 1.701 + node = null; 1.702 + } 1.703 + } else { 1.704 + node = aWindow.document.querySelector(targetQuery); 1.705 + } 1.706 + 1.707 + deferred.resolve({ 1.708 + addTargetListener: targetObject.addTargetListener, 1.709 + node: node, 1.710 + removeTargetListener: targetObject.removeTargetListener, 1.711 + targetName: aTargetName, 1.712 + widgetName: targetObject.widgetName, 1.713 + }); 1.714 + }).then(null, Cu.reportError); 1.715 + return deferred.promise; 1.716 + }, 1.717 + 1.718 + targetIsInAppMenu: function(aTarget) { 1.719 + let placement = CustomizableUI.getPlacementOfWidget(aTarget.widgetName || aTarget.node.id); 1.720 + if (placement && placement.area == CustomizableUI.AREA_PANEL) { 1.721 + return true; 1.722 + } 1.723 + 1.724 + let targetElement = aTarget.node; 1.725 + // Use the widget for filtering if it exists since the target may be the icon inside. 1.726 + if (aTarget.widgetName) { 1.727 + targetElement = aTarget.node.ownerDocument.getElementById(aTarget.widgetName); 1.728 + } 1.729 + 1.730 + // Handle the non-customizable buttons at the bottom of the menu which aren't proper widgets. 1.731 + return targetElement.id.startsWith("PanelUI-") 1.732 + && targetElement.id != "PanelUI-button"; 1.733 + }, 1.734 + 1.735 + /** 1.736 + * Called before opening or after closing a highlight or info panel to see if 1.737 + * we need to open or close the appMenu to see the annotation's anchor. 1.738 + */ 1.739 + _setAppMenuStateForAnnotation: function(aWindow, aAnnotationType, aShouldOpenForHighlight, aCallback = null) { 1.740 + // If the panel is in the desired state, we're done. 1.741 + let panelIsOpen = aWindow.PanelUI.panel.state != "closed"; 1.742 + if (aShouldOpenForHighlight == panelIsOpen) { 1.743 + if (aCallback) 1.744 + aCallback(); 1.745 + return; 1.746 + } 1.747 + 1.748 + // Don't close the menu if it wasn't opened by us (e.g. via showmenu instead). 1.749 + if (!aShouldOpenForHighlight && !this.appMenuOpenForAnnotation.has(aAnnotationType)) { 1.750 + if (aCallback) 1.751 + aCallback(); 1.752 + return; 1.753 + } 1.754 + 1.755 + if (aShouldOpenForHighlight) { 1.756 + this.appMenuOpenForAnnotation.add(aAnnotationType); 1.757 + } else { 1.758 + this.appMenuOpenForAnnotation.delete(aAnnotationType); 1.759 + } 1.760 + 1.761 + // Actually show or hide the menu 1.762 + if (this.appMenuOpenForAnnotation.size) { 1.763 + this.showMenu(aWindow, "appMenu", aCallback); 1.764 + } else { 1.765 + this.hideMenu(aWindow, "appMenu"); 1.766 + if (aCallback) 1.767 + aCallback(); 1.768 + } 1.769 + 1.770 + }, 1.771 + 1.772 + previewTheme: function(aTheme) { 1.773 + let origin = Services.prefs.getCharPref("browser.uitour.themeOrigin"); 1.774 + let data = LightweightThemeManager.parseTheme(aTheme, origin); 1.775 + if (data) 1.776 + LightweightThemeManager.previewTheme(data); 1.777 + }, 1.778 + 1.779 + resetTheme: function() { 1.780 + LightweightThemeManager.resetPreview(); 1.781 + }, 1.782 + 1.783 + ensurePinnedTab: function(aWindow, aSticky = false) { 1.784 + let tabInfo = this.pinnedTabs.get(aWindow); 1.785 + 1.786 + if (tabInfo) { 1.787 + tabInfo.sticky = tabInfo.sticky || aSticky; 1.788 + } else { 1.789 + let url = Services.urlFormatter.formatURLPref("browser.uitour.pinnedTabUrl"); 1.790 + 1.791 + let tab = aWindow.gBrowser.addTab(url); 1.792 + aWindow.gBrowser.pinTab(tab); 1.793 + tab.addEventListener("TabClose", () => { 1.794 + this.pinnedTabs.delete(aWindow); 1.795 + }); 1.796 + 1.797 + tabInfo = { 1.798 + tab: tab, 1.799 + sticky: aSticky 1.800 + }; 1.801 + this.pinnedTabs.set(aWindow, tabInfo); 1.802 + } 1.803 + 1.804 + return tabInfo.tab; 1.805 + }, 1.806 + 1.807 + removePinnedTab: function(aWindow) { 1.808 + let tabInfo = this.pinnedTabs.get(aWindow); 1.809 + if (tabInfo) 1.810 + aWindow.gBrowser.removeTab(tabInfo.tab); 1.811 + }, 1.812 + 1.813 + /** 1.814 + * @param aTarget The element to highlight. 1.815 + * @param aEffect (optional) The effect to use from UITour.highlightEffects or "none". 1.816 + * @see UITour.highlightEffects 1.817 + */ 1.818 + showHighlight: function(aTarget, aEffect = "none") { 1.819 + function showHighlightPanel(aTargetEl) { 1.820 + let highlighter = aTargetEl.ownerDocument.getElementById("UITourHighlight"); 1.821 + 1.822 + let effect = aEffect; 1.823 + if (effect == "random") { 1.824 + // Exclude "random" from the randomly selected effects. 1.825 + let randomEffect = 1 + Math.floor(Math.random() * (this.highlightEffects.length - 1)); 1.826 + if (randomEffect == this.highlightEffects.length) 1.827 + randomEffect--; // On the order of 1 in 2^62 chance of this happening. 1.828 + effect = this.highlightEffects[randomEffect]; 1.829 + } 1.830 + // Toggle the effect attribute to "none" and flush layout before setting it so the effect plays. 1.831 + highlighter.setAttribute("active", "none"); 1.832 + aTargetEl.ownerDocument.defaultView.getComputedStyle(highlighter).animationName; 1.833 + highlighter.setAttribute("active", effect); 1.834 + highlighter.parentElement.setAttribute("targetName", aTarget.targetName); 1.835 + highlighter.parentElement.hidden = false; 1.836 + 1.837 + let targetRect = aTargetEl.getBoundingClientRect(); 1.838 + let highlightHeight = targetRect.height; 1.839 + let highlightWidth = targetRect.width; 1.840 + let minDimension = Math.min(highlightHeight, highlightWidth); 1.841 + let maxDimension = Math.max(highlightHeight, highlightWidth); 1.842 + 1.843 + // If the dimensions are within 200% of each other (to include the bookmarks button), 1.844 + // make the highlight a circle with the largest dimension as the diameter. 1.845 + if (maxDimension / minDimension <= 3.0) { 1.846 + highlightHeight = highlightWidth = maxDimension; 1.847 + highlighter.style.borderRadius = "100%"; 1.848 + } else { 1.849 + highlighter.style.borderRadius = ""; 1.850 + } 1.851 + 1.852 + highlighter.style.height = highlightHeight + "px"; 1.853 + highlighter.style.width = highlightWidth + "px"; 1.854 + 1.855 + // Close a previous highlight so we can relocate the panel. 1.856 + if (highlighter.parentElement.state == "open") { 1.857 + highlighter.parentElement.hidePopup(); 1.858 + } 1.859 + /* The "overlap" position anchors from the top-left but we want to centre highlights at their 1.860 + minimum size. */ 1.861 + let highlightWindow = aTargetEl.ownerDocument.defaultView; 1.862 + let containerStyle = highlightWindow.getComputedStyle(highlighter.parentElement); 1.863 + let paddingTopPx = 0 - parseFloat(containerStyle.paddingTop); 1.864 + let paddingLeftPx = 0 - parseFloat(containerStyle.paddingLeft); 1.865 + let highlightStyle = highlightWindow.getComputedStyle(highlighter); 1.866 + let highlightHeightWithMin = Math.max(highlightHeight, parseFloat(highlightStyle.minHeight)); 1.867 + let highlightWidthWithMin = Math.max(highlightWidth, parseFloat(highlightStyle.minWidth)); 1.868 + let offsetX = paddingTopPx 1.869 + - (Math.max(0, highlightWidthWithMin - targetRect.width) / 2); 1.870 + let offsetY = paddingLeftPx 1.871 + - (Math.max(0, highlightHeightWithMin - targetRect.height) / 2); 1.872 + 1.873 + this._addAnnotationPanelMutationObserver(highlighter.parentElement); 1.874 + highlighter.parentElement.openPopup(aTargetEl, "overlap", offsetX, offsetY); 1.875 + } 1.876 + 1.877 + // Prevent showing a panel at an undefined position. 1.878 + if (!this.isElementVisible(aTarget.node)) 1.879 + return; 1.880 + 1.881 + this._setAppMenuStateForAnnotation(aTarget.node.ownerDocument.defaultView, "highlight", 1.882 + this.targetIsInAppMenu(aTarget), 1.883 + showHighlightPanel.bind(this, aTarget.node)); 1.884 + }, 1.885 + 1.886 + hideHighlight: function(aWindow) { 1.887 + let tabData = this.pinnedTabs.get(aWindow); 1.888 + if (tabData && !tabData.sticky) 1.889 + this.removePinnedTab(aWindow); 1.890 + 1.891 + let highlighter = aWindow.document.getElementById("UITourHighlight"); 1.892 + this._removeAnnotationPanelMutationObserver(highlighter.parentElement); 1.893 + highlighter.parentElement.hidePopup(); 1.894 + highlighter.removeAttribute("active"); 1.895 + 1.896 + this._setAppMenuStateForAnnotation(aWindow, "highlight", false); 1.897 + }, 1.898 + 1.899 + /** 1.900 + * Show an info panel. 1.901 + * 1.902 + * @param {Document} aContentDocument 1.903 + * @param {Node} aAnchor 1.904 + * @param {String} [aTitle=""] 1.905 + * @param {String} [aDescription=""] 1.906 + * @param {String} [aIconURL=""] 1.907 + * @param {Object[]} [aButtons=[]] 1.908 + * @param {Object} [aOptions={}] 1.909 + * @param {String} [aOptions.closeButtonCallbackID] 1.910 + */ 1.911 + showInfo: function(aContentDocument, aAnchor, aTitle = "", aDescription = "", aIconURL = "", 1.912 + aButtons = [], aOptions = {}) { 1.913 + function showInfoPanel(aAnchorEl) { 1.914 + aAnchorEl.focus(); 1.915 + 1.916 + let document = aAnchorEl.ownerDocument; 1.917 + let tooltip = document.getElementById("UITourTooltip"); 1.918 + let tooltipTitle = document.getElementById("UITourTooltipTitle"); 1.919 + let tooltipDesc = document.getElementById("UITourTooltipDescription"); 1.920 + let tooltipIcon = document.getElementById("UITourTooltipIcon"); 1.921 + let tooltipButtons = document.getElementById("UITourTooltipButtons"); 1.922 + 1.923 + if (tooltip.state == "open") { 1.924 + tooltip.hidePopup(); 1.925 + } 1.926 + 1.927 + tooltipTitle.textContent = aTitle || ""; 1.928 + tooltipDesc.textContent = aDescription || ""; 1.929 + tooltipIcon.src = aIconURL || ""; 1.930 + tooltipIcon.hidden = !aIconURL; 1.931 + 1.932 + while (tooltipButtons.firstChild) 1.933 + tooltipButtons.firstChild.remove(); 1.934 + 1.935 + for (let button of aButtons) { 1.936 + let el = document.createElement("button"); 1.937 + el.setAttribute("label", button.label); 1.938 + if (button.iconURL) 1.939 + el.setAttribute("image", button.iconURL); 1.940 + 1.941 + if (button.style == "link") 1.942 + el.setAttribute("class", "button-link"); 1.943 + 1.944 + if (button.style == "primary") 1.945 + el.setAttribute("class", "button-primary"); 1.946 + 1.947 + let callbackID = button.callbackID; 1.948 + el.addEventListener("command", event => { 1.949 + tooltip.hidePopup(); 1.950 + this.sendPageCallback(aContentDocument, callbackID); 1.951 + }); 1.952 + 1.953 + tooltipButtons.appendChild(el); 1.954 + } 1.955 + 1.956 + tooltipButtons.hidden = !aButtons.length; 1.957 + 1.958 + let tooltipClose = document.getElementById("UITourTooltipClose"); 1.959 + let closeButtonCallback = (event) => { 1.960 + this.hideInfo(document.defaultView); 1.961 + if (aOptions && aOptions.closeButtonCallbackID) 1.962 + this.sendPageCallback(aContentDocument, aOptions.closeButtonCallbackID); 1.963 + }; 1.964 + tooltipClose.addEventListener("command", closeButtonCallback); 1.965 + 1.966 + let targetCallback = (event) => { 1.967 + let details = { 1.968 + target: aAnchor.targetName, 1.969 + type: event.type, 1.970 + }; 1.971 + this.sendPageCallback(aContentDocument, aOptions.targetCallbackID, details); 1.972 + }; 1.973 + if (aOptions.targetCallbackID && aAnchor.addTargetListener) { 1.974 + aAnchor.addTargetListener(document, targetCallback); 1.975 + } 1.976 + 1.977 + tooltip.addEventListener("popuphiding", function tooltipHiding(event) { 1.978 + tooltip.removeEventListener("popuphiding", tooltipHiding); 1.979 + tooltipClose.removeEventListener("command", closeButtonCallback); 1.980 + if (aOptions.targetCallbackID && aAnchor.removeTargetListener) { 1.981 + aAnchor.removeTargetListener(document, targetCallback); 1.982 + } 1.983 + }); 1.984 + 1.985 + tooltip.setAttribute("targetName", aAnchor.targetName); 1.986 + tooltip.hidden = false; 1.987 + let alignment = "bottomcenter topright"; 1.988 + this._addAnnotationPanelMutationObserver(tooltip); 1.989 + tooltip.openPopup(aAnchorEl, alignment); 1.990 + } 1.991 + 1.992 + // Prevent showing a panel at an undefined position. 1.993 + if (!this.isElementVisible(aAnchor.node)) 1.994 + return; 1.995 + 1.996 + this._setAppMenuStateForAnnotation(aAnchor.node.ownerDocument.defaultView, "info", 1.997 + this.targetIsInAppMenu(aAnchor), 1.998 + showInfoPanel.bind(this, aAnchor.node)); 1.999 + }, 1.1000 + 1.1001 + hideInfo: function(aWindow) { 1.1002 + let document = aWindow.document; 1.1003 + 1.1004 + let tooltip = document.getElementById("UITourTooltip"); 1.1005 + this._removeAnnotationPanelMutationObserver(tooltip); 1.1006 + tooltip.hidePopup(); 1.1007 + this._setAppMenuStateForAnnotation(aWindow, "info", false); 1.1008 + 1.1009 + let tooltipButtons = document.getElementById("UITourTooltipButtons"); 1.1010 + while (tooltipButtons.firstChild) 1.1011 + tooltipButtons.firstChild.remove(); 1.1012 + }, 1.1013 + 1.1014 + showMenu: function(aWindow, aMenuName, aOpenCallback = null) { 1.1015 + function openMenuButton(aID) { 1.1016 + let menuBtn = aWindow.document.getElementById(aID); 1.1017 + if (!menuBtn || !menuBtn.boxObject) { 1.1018 + aOpenCallback(); 1.1019 + return; 1.1020 + } 1.1021 + if (aOpenCallback) 1.1022 + menuBtn.addEventListener("popupshown", onPopupShown); 1.1023 + menuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(true); 1.1024 + } 1.1025 + function onPopupShown(event) { 1.1026 + this.removeEventListener("popupshown", onPopupShown); 1.1027 + aOpenCallback(event); 1.1028 + } 1.1029 + 1.1030 + if (aMenuName == "appMenu") { 1.1031 + aWindow.PanelUI.panel.setAttribute("noautohide", "true"); 1.1032 + // If the popup is already opened, don't recreate the widget as it may cause a flicker. 1.1033 + if (aWindow.PanelUI.panel.state != "open") { 1.1034 + this.recreatePopup(aWindow.PanelUI.panel); 1.1035 + } 1.1036 + aWindow.PanelUI.panel.addEventListener("popuphiding", this.hidePanelAnnotations); 1.1037 + aWindow.PanelUI.panel.addEventListener("ViewShowing", this.hidePanelAnnotations); 1.1038 + if (aOpenCallback) { 1.1039 + aWindow.PanelUI.panel.addEventListener("popupshown", onPopupShown); 1.1040 + } 1.1041 + aWindow.PanelUI.show(); 1.1042 + } else if (aMenuName == "bookmarks") { 1.1043 + openMenuButton("bookmarks-menu-button"); 1.1044 + } 1.1045 + }, 1.1046 + 1.1047 + hideMenu: function(aWindow, aMenuName) { 1.1048 + function closeMenuButton(aID) { 1.1049 + let menuBtn = aWindow.document.getElementById(aID); 1.1050 + if (menuBtn && menuBtn.boxObject) 1.1051 + menuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(false); 1.1052 + } 1.1053 + 1.1054 + if (aMenuName == "appMenu") { 1.1055 + aWindow.PanelUI.panel.removeAttribute("noautohide"); 1.1056 + aWindow.PanelUI.hide(); 1.1057 + this.recreatePopup(aWindow.PanelUI.panel); 1.1058 + } else if (aMenuName == "bookmarks") { 1.1059 + closeMenuButton("bookmarks-menu-button"); 1.1060 + } 1.1061 + }, 1.1062 + 1.1063 + hidePanelAnnotations: function(aEvent) { 1.1064 + let win = aEvent.target.ownerDocument.defaultView; 1.1065 + let annotationElements = new Map([ 1.1066 + // [annotationElement (panel), method to hide the annotation] 1.1067 + [win.document.getElementById("UITourHighlightContainer"), UITour.hideHighlight.bind(UITour)], 1.1068 + [win.document.getElementById("UITourTooltip"), UITour.hideInfo.bind(UITour)], 1.1069 + ]); 1.1070 + annotationElements.forEach((hideMethod, annotationElement) => { 1.1071 + if (annotationElement.state != "closed") { 1.1072 + let targetName = annotationElement.getAttribute("targetName"); 1.1073 + UITour.getTarget(win, targetName).then((aTarget) => { 1.1074 + // Since getTarget is async, we need to make sure that the target hasn't 1.1075 + // changed since it may have just moved to somewhere outside of the app menu. 1.1076 + if (annotationElement.getAttribute("targetName") != aTarget.targetName || 1.1077 + annotationElement.state == "closed" || 1.1078 + !UITour.targetIsInAppMenu(aTarget)) { 1.1079 + return; 1.1080 + } 1.1081 + hideMethod(win); 1.1082 + }).then(null, Cu.reportError); 1.1083 + } 1.1084 + }); 1.1085 + UITour.appMenuOpenForAnnotation.clear(); 1.1086 + }, 1.1087 + 1.1088 + recreatePopup: function(aPanel) { 1.1089 + // After changing popup attributes that relate to how the native widget is created 1.1090 + // (e.g. @noautohide) we need to re-create the frame/widget for it to take effect. 1.1091 + if (aPanel.hidden) { 1.1092 + // If the panel is already hidden, we don't need to recreate it but flush 1.1093 + // in case someone just hid it. 1.1094 + aPanel.clientWidth; // flush 1.1095 + return; 1.1096 + } 1.1097 + aPanel.hidden = true; 1.1098 + aPanel.clientWidth; // flush 1.1099 + aPanel.hidden = false; 1.1100 + }, 1.1101 + 1.1102 + startUrlbarCapture: function(aWindow, aExpectedText, aUrl) { 1.1103 + let urlbar = aWindow.document.getElementById("urlbar"); 1.1104 + this.urlbarCapture.set(aWindow, { 1.1105 + expected: aExpectedText.toLocaleLowerCase(), 1.1106 + url: aUrl 1.1107 + }); 1.1108 + urlbar.addEventListener("input", this); 1.1109 + }, 1.1110 + 1.1111 + endUrlbarCapture: function(aWindow) { 1.1112 + let urlbar = aWindow.document.getElementById("urlbar"); 1.1113 + urlbar.removeEventListener("input", this); 1.1114 + this.urlbarCapture.delete(aWindow); 1.1115 + }, 1.1116 + 1.1117 + handleUrlbarInput: function(aWindow) { 1.1118 + if (!this.urlbarCapture.has(aWindow)) 1.1119 + return; 1.1120 + 1.1121 + let urlbar = aWindow.document.getElementById("urlbar"); 1.1122 + 1.1123 + let {expected, url} = this.urlbarCapture.get(aWindow); 1.1124 + 1.1125 + if (urlbar.value.toLocaleLowerCase().localeCompare(expected) != 0) 1.1126 + return; 1.1127 + 1.1128 + urlbar.handleRevert(); 1.1129 + 1.1130 + let tab = aWindow.gBrowser.addTab(url, { 1.1131 + owner: aWindow.gBrowser.selectedTab, 1.1132 + relatedToCurrent: true 1.1133 + }); 1.1134 + aWindow.gBrowser.selectedTab = tab; 1.1135 + }, 1.1136 + 1.1137 + getConfiguration: function(aContentDocument, aConfiguration, aCallbackID) { 1.1138 + switch (aConfiguration) { 1.1139 + case "availableTargets": 1.1140 + this.getAvailableTargets(aContentDocument, aCallbackID); 1.1141 + break; 1.1142 + case "sync": 1.1143 + this.sendPageCallback(aContentDocument, aCallbackID, { 1.1144 + setup: Services.prefs.prefHasUserValue("services.sync.username"), 1.1145 + }); 1.1146 + break; 1.1147 + default: 1.1148 + Cu.reportError("getConfiguration: Unknown configuration requested: " + aConfiguration); 1.1149 + break; 1.1150 + } 1.1151 + }, 1.1152 + 1.1153 + getAvailableTargets: function(aContentDocument, aCallbackID) { 1.1154 + let window = this.getChromeWindow(aContentDocument); 1.1155 + let data = this.availableTargetsCache.get(window); 1.1156 + if (data) { 1.1157 + this.sendPageCallback(aContentDocument, aCallbackID, data); 1.1158 + return; 1.1159 + } 1.1160 + 1.1161 + let promises = []; 1.1162 + for (let targetName of this.targets.keys()) { 1.1163 + promises.push(this.getTarget(window, targetName)); 1.1164 + } 1.1165 + Promise.all(promises).then((targetObjects) => { 1.1166 + let targetNames = [ 1.1167 + "pinnedTab", 1.1168 + ]; 1.1169 + for (let targetObject of targetObjects) { 1.1170 + if (targetObject.node) 1.1171 + targetNames.push(targetObject.targetName); 1.1172 + } 1.1173 + let data = { 1.1174 + targets: targetNames, 1.1175 + }; 1.1176 + this.availableTargetsCache.set(window, data); 1.1177 + this.sendPageCallback(aContentDocument, aCallbackID, data); 1.1178 + }, (err) => { 1.1179 + Cu.reportError(err); 1.1180 + this.sendPageCallback(aContentDocument, aCallbackID, { 1.1181 + targets: [], 1.1182 + }); 1.1183 + }); 1.1184 + }, 1.1185 + 1.1186 + _addAnnotationPanelMutationObserver: function(aPanelEl) { 1.1187 +#ifdef XP_LINUX 1.1188 + let observer = this._annotationPanelMutationObservers.get(aPanelEl); 1.1189 + if (observer) { 1.1190 + return; 1.1191 + } 1.1192 + let win = aPanelEl.ownerDocument.defaultView; 1.1193 + observer = new win.MutationObserver(this._annotationMutationCallback); 1.1194 + this._annotationPanelMutationObservers.set(aPanelEl, observer); 1.1195 + let observerOptions = { 1.1196 + attributeFilter: ["height", "width"], 1.1197 + attributes: true, 1.1198 + }; 1.1199 + observer.observe(aPanelEl, observerOptions); 1.1200 +#endif 1.1201 + }, 1.1202 + 1.1203 + _removeAnnotationPanelMutationObserver: function(aPanelEl) { 1.1204 +#ifdef XP_LINUX 1.1205 + let observer = this._annotationPanelMutationObservers.get(aPanelEl); 1.1206 + if (observer) { 1.1207 + observer.disconnect(); 1.1208 + this._annotationPanelMutationObservers.delete(aPanelEl); 1.1209 + } 1.1210 +#endif 1.1211 + }, 1.1212 + 1.1213 +/** 1.1214 + * Workaround for Ubuntu panel craziness in bug 970788 where incorrect sizes get passed to 1.1215 + * nsXULPopupManager::PopupResized and lead to incorrect width and height attributes getting 1.1216 + * set on the panel. 1.1217 + */ 1.1218 + _annotationMutationCallback: function(aMutations) { 1.1219 + for (let mutation of aMutations) { 1.1220 + // Remove both attributes at once and ignore remaining mutations to be proccessed. 1.1221 + mutation.target.removeAttribute("width"); 1.1222 + mutation.target.removeAttribute("height"); 1.1223 + return; 1.1224 + } 1.1225 + }, 1.1226 +}; 1.1227 + 1.1228 +this.UITour.init();