browser/modules/UITour.jsm

changeset 0
6474c204b198
     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();

mercurial