browser/modules/UITour.jsm

Fri, 16 Jan 2015 18:13:44 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 18:13:44 +0100
branch
TOR_BUG_9701
changeset 14
925c144e1f1f
permissions
-rw-r--r--

Integrate suggestion from review to improve consistency with existing code.

michael@0 1 // This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 // License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 this.EXPORTED_SYMBOLS = ["UITour"];
michael@0 8
michael@0 9 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
michael@0 10
michael@0 11 Cu.import("resource://gre/modules/Services.jsm");
michael@0 12 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 13 Cu.import("resource://gre/modules/Promise.jsm");
michael@0 14
michael@0 15 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
michael@0 16 "resource://gre/modules/LightweightThemeManager.jsm");
michael@0 17 XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils",
michael@0 18 "resource://gre/modules/PermissionsUtils.jsm");
michael@0 19 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
michael@0 20 "resource:///modules/CustomizableUI.jsm");
michael@0 21 XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
michael@0 22 "resource://gre/modules/UITelemetry.jsm");
michael@0 23 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
michael@0 24 "resource:///modules/BrowserUITelemetry.jsm");
michael@0 25
michael@0 26
michael@0 27 const UITOUR_PERMISSION = "uitour";
michael@0 28 const PREF_PERM_BRANCH = "browser.uitour.";
michael@0 29 const PREF_SEENPAGEIDS = "browser.uitour.seenPageIDs";
michael@0 30 const MAX_BUTTONS = 4;
michael@0 31
michael@0 32 const BUCKET_NAME = "UITour";
michael@0 33 const BUCKET_TIMESTEPS = [
michael@0 34 1 * 60 * 1000, // Until 1 minute after tab is closed/inactive.
michael@0 35 3 * 60 * 1000, // Until 3 minutes after tab is closed/inactive.
michael@0 36 10 * 60 * 1000, // Until 10 minutes after tab is closed/inactive.
michael@0 37 60 * 60 * 1000, // Until 1 hour after tab is closed/inactive.
michael@0 38 ];
michael@0 39
michael@0 40 // Time after which seen Page IDs expire.
michael@0 41 const SEENPAGEID_EXPIRY = 8 * 7 * 24 * 60 * 60 * 1000; // 8 weeks.
michael@0 42
michael@0 43
michael@0 44 this.UITour = {
michael@0 45 url: null,
michael@0 46 seenPageIDs: null,
michael@0 47 pageIDSourceTabs: new WeakMap(),
michael@0 48 pageIDSourceWindows: new WeakMap(),
michael@0 49 /* Map from browser windows to a set of tabs in which a tour is open */
michael@0 50 originTabs: new WeakMap(),
michael@0 51 /* Map from browser windows to a set of pinned tabs opened by (a) tour(s) */
michael@0 52 pinnedTabs: new WeakMap(),
michael@0 53 urlbarCapture: new WeakMap(),
michael@0 54 appMenuOpenForAnnotation: new Set(),
michael@0 55 availableTargetsCache: new WeakMap(),
michael@0 56
michael@0 57 _detachingTab: false,
michael@0 58 _annotationPanelMutationObservers: new WeakMap(),
michael@0 59 _queuedEvents: [],
michael@0 60 _pendingDoc: null,
michael@0 61
michael@0 62 highlightEffects: ["random", "wobble", "zoom", "color"],
michael@0 63 targets: new Map([
michael@0 64 ["accountStatus", {
michael@0 65 query: (aDocument) => {
michael@0 66 let statusButton = aDocument.getElementById("PanelUI-fxa-status");
michael@0 67 return aDocument.getAnonymousElementByAttribute(statusButton,
michael@0 68 "class",
michael@0 69 "toolbarbutton-icon");
michael@0 70 },
michael@0 71 widgetName: "PanelUI-fxa-status",
michael@0 72 }],
michael@0 73 ["addons", {query: "#add-ons-button"}],
michael@0 74 ["appMenu", {
michael@0 75 addTargetListener: (aDocument, aCallback) => {
michael@0 76 let panelPopup = aDocument.getElementById("PanelUI-popup");
michael@0 77 panelPopup.addEventListener("popupshown", aCallback);
michael@0 78 },
michael@0 79 query: "#PanelUI-button",
michael@0 80 removeTargetListener: (aDocument, aCallback) => {
michael@0 81 let panelPopup = aDocument.getElementById("PanelUI-popup");
michael@0 82 panelPopup.removeEventListener("popupshown", aCallback);
michael@0 83 },
michael@0 84 }],
michael@0 85 ["backForward", {
michael@0 86 query: "#back-button",
michael@0 87 widgetName: "urlbar-container",
michael@0 88 }],
michael@0 89 ["bookmarks", {query: "#bookmarks-menu-button"}],
michael@0 90 ["customize", {
michael@0 91 query: (aDocument) => {
michael@0 92 let customizeButton = aDocument.getElementById("PanelUI-customize");
michael@0 93 return aDocument.getAnonymousElementByAttribute(customizeButton,
michael@0 94 "class",
michael@0 95 "toolbarbutton-icon");
michael@0 96 },
michael@0 97 widgetName: "PanelUI-customize",
michael@0 98 }],
michael@0 99 ["help", {query: "#PanelUI-help"}],
michael@0 100 ["home", {query: "#home-button"}],
michael@0 101 ["quit", {query: "#PanelUI-quit"}],
michael@0 102 ["search", {
michael@0 103 query: "#searchbar",
michael@0 104 widgetName: "search-container",
michael@0 105 }],
michael@0 106 ["searchProvider", {
michael@0 107 query: (aDocument) => {
michael@0 108 let searchbar = aDocument.getElementById("searchbar");
michael@0 109 return aDocument.getAnonymousElementByAttribute(searchbar,
michael@0 110 "anonid",
michael@0 111 "searchbar-engine-button");
michael@0 112 },
michael@0 113 widgetName: "search-container",
michael@0 114 }],
michael@0 115 ["selectedTabIcon", {
michael@0 116 query: (aDocument) => {
michael@0 117 let selectedtab = aDocument.defaultView.gBrowser.selectedTab;
michael@0 118 let element = aDocument.getAnonymousElementByAttribute(selectedtab,
michael@0 119 "anonid",
michael@0 120 "tab-icon-image");
michael@0 121 if (!element || !UITour.isElementVisible(element)) {
michael@0 122 return null;
michael@0 123 }
michael@0 124 return element;
michael@0 125 },
michael@0 126 }],
michael@0 127 ["urlbar", {
michael@0 128 query: "#urlbar",
michael@0 129 widgetName: "urlbar-container",
michael@0 130 }],
michael@0 131 ]),
michael@0 132
michael@0 133 init: function() {
michael@0 134 // Lazy getter is initialized here so it can be replicated any time
michael@0 135 // in a test.
michael@0 136 delete this.seenPageIDs;
michael@0 137 Object.defineProperty(this, "seenPageIDs", {
michael@0 138 get: this.restoreSeenPageIDs.bind(this),
michael@0 139 configurable: true,
michael@0 140 });
michael@0 141
michael@0 142 delete this.url;
michael@0 143 XPCOMUtils.defineLazyGetter(this, "url", function () {
michael@0 144 return Services.urlFormatter.formatURLPref("browser.uitour.url");
michael@0 145 });
michael@0 146
michael@0 147 // Clear the availableTargetsCache on widget changes.
michael@0 148 let listenerMethods = [
michael@0 149 "onWidgetAdded",
michael@0 150 "onWidgetMoved",
michael@0 151 "onWidgetRemoved",
michael@0 152 "onWidgetReset",
michael@0 153 "onAreaReset",
michael@0 154 ];
michael@0 155 CustomizableUI.addListener(listenerMethods.reduce((listener, method) => {
michael@0 156 listener[method] = () => this.availableTargetsCache.clear();
michael@0 157 return listener;
michael@0 158 }, {}));
michael@0 159 },
michael@0 160
michael@0 161 restoreSeenPageIDs: function() {
michael@0 162 delete this.seenPageIDs;
michael@0 163
michael@0 164 if (UITelemetry.enabled) {
michael@0 165 let dateThreshold = Date.now() - SEENPAGEID_EXPIRY;
michael@0 166
michael@0 167 try {
michael@0 168 let data = Services.prefs.getCharPref(PREF_SEENPAGEIDS);
michael@0 169 data = new Map(JSON.parse(data));
michael@0 170
michael@0 171 for (let [pageID, details] of data) {
michael@0 172
michael@0 173 if (typeof pageID != "string" ||
michael@0 174 typeof details != "object" ||
michael@0 175 typeof details.lastSeen != "number" ||
michael@0 176 details.lastSeen < dateThreshold) {
michael@0 177
michael@0 178 data.delete(pageID);
michael@0 179 }
michael@0 180 }
michael@0 181
michael@0 182 this.seenPageIDs = data;
michael@0 183 } catch (e) {}
michael@0 184 }
michael@0 185
michael@0 186 if (!this.seenPageIDs)
michael@0 187 this.seenPageIDs = new Map();
michael@0 188
michael@0 189 this.persistSeenIDs();
michael@0 190
michael@0 191 return this.seenPageIDs;
michael@0 192 },
michael@0 193
michael@0 194 addSeenPageID: function(aPageID) {
michael@0 195 if (!UITelemetry.enabled)
michael@0 196 return;
michael@0 197
michael@0 198 this.seenPageIDs.set(aPageID, {
michael@0 199 lastSeen: Date.now(),
michael@0 200 });
michael@0 201
michael@0 202 this.persistSeenIDs();
michael@0 203 },
michael@0 204
michael@0 205 persistSeenIDs: function() {
michael@0 206 if (this.seenPageIDs.size === 0) {
michael@0 207 Services.prefs.clearUserPref(PREF_SEENPAGEIDS);
michael@0 208 return;
michael@0 209 }
michael@0 210
michael@0 211 Services.prefs.setCharPref(PREF_SEENPAGEIDS,
michael@0 212 JSON.stringify([...this.seenPageIDs]));
michael@0 213 },
michael@0 214
michael@0 215 onPageEvent: function(aEvent) {
michael@0 216 let contentDocument = null;
michael@0 217 if (aEvent.target instanceof Ci.nsIDOMHTMLDocument)
michael@0 218 contentDocument = aEvent.target;
michael@0 219 else if (aEvent.target instanceof Ci.nsIDOMHTMLElement)
michael@0 220 contentDocument = aEvent.target.ownerDocument;
michael@0 221 else
michael@0 222 return false;
michael@0 223
michael@0 224 // Ignore events if they're not from a trusted origin.
michael@0 225 if (!this.ensureTrustedOrigin(contentDocument))
michael@0 226 return false;
michael@0 227
michael@0 228 if (typeof aEvent.detail != "object")
michael@0 229 return false;
michael@0 230
michael@0 231 let action = aEvent.detail.action;
michael@0 232 if (typeof action != "string" || !action)
michael@0 233 return false;
michael@0 234
michael@0 235 let data = aEvent.detail.data;
michael@0 236 if (typeof data != "object")
michael@0 237 return false;
michael@0 238
michael@0 239 let window = this.getChromeWindow(contentDocument);
michael@0 240 // Do this before bailing if there's no tab, so later we can pick up the pieces:
michael@0 241 window.gBrowser.tabContainer.addEventListener("TabSelect", this);
michael@0 242 let tab = window.gBrowser._getTabForContentWindow(contentDocument.defaultView);
michael@0 243 if (!tab) {
michael@0 244 // This should only happen while detaching a tab:
michael@0 245 if (this._detachingTab) {
michael@0 246 this._queuedEvents.push(aEvent);
michael@0 247 this._pendingDoc = Cu.getWeakReference(contentDocument);
michael@0 248 return;
michael@0 249 }
michael@0 250 Cu.reportError("Discarding tabless UITour event (" + action + ") while not detaching a tab." +
michael@0 251 "This shouldn't happen!");
michael@0 252 return;
michael@0 253 }
michael@0 254
michael@0 255 switch (action) {
michael@0 256 case "registerPageID": {
michael@0 257 // This is only relevant if Telemtry is enabled.
michael@0 258 if (!UITelemetry.enabled)
michael@0 259 break;
michael@0 260
michael@0 261 // We don't want to allow BrowserUITelemetry.BUCKET_SEPARATOR in the
michael@0 262 // pageID, as it could make parsing the telemetry bucket name difficult.
michael@0 263 if (typeof data.pageID == "string" &&
michael@0 264 !data.pageID.contains(BrowserUITelemetry.BUCKET_SEPARATOR)) {
michael@0 265 this.addSeenPageID(data.pageID);
michael@0 266
michael@0 267 // Store tabs and windows separately so we don't need to loop over all
michael@0 268 // tabs when a window is closed.
michael@0 269 this.pageIDSourceTabs.set(tab, data.pageID);
michael@0 270 this.pageIDSourceWindows.set(window, data.pageID);
michael@0 271
michael@0 272 this.setTelemetryBucket(data.pageID);
michael@0 273 }
michael@0 274 break;
michael@0 275 }
michael@0 276
michael@0 277 case "showHighlight": {
michael@0 278 let targetPromise = this.getTarget(window, data.target);
michael@0 279 targetPromise.then(target => {
michael@0 280 if (!target.node) {
michael@0 281 Cu.reportError("UITour: Target could not be resolved: " + data.target);
michael@0 282 return;
michael@0 283 }
michael@0 284 let effect = undefined;
michael@0 285 if (this.highlightEffects.indexOf(data.effect) !== -1) {
michael@0 286 effect = data.effect;
michael@0 287 }
michael@0 288 this.showHighlight(target, effect);
michael@0 289 }).then(null, Cu.reportError);
michael@0 290 break;
michael@0 291 }
michael@0 292
michael@0 293 case "hideHighlight": {
michael@0 294 this.hideHighlight(window);
michael@0 295 break;
michael@0 296 }
michael@0 297
michael@0 298 case "showInfo": {
michael@0 299 let targetPromise = this.getTarget(window, data.target, true);
michael@0 300 targetPromise.then(target => {
michael@0 301 if (!target.node) {
michael@0 302 Cu.reportError("UITour: Target could not be resolved: " + data.target);
michael@0 303 return;
michael@0 304 }
michael@0 305
michael@0 306 let iconURL = null;
michael@0 307 if (typeof data.icon == "string")
michael@0 308 iconURL = this.resolveURL(contentDocument, data.icon);
michael@0 309
michael@0 310 let buttons = [];
michael@0 311 if (Array.isArray(data.buttons) && data.buttons.length > 0) {
michael@0 312 for (let buttonData of data.buttons) {
michael@0 313 if (typeof buttonData == "object" &&
michael@0 314 typeof buttonData.label == "string" &&
michael@0 315 typeof buttonData.callbackID == "string") {
michael@0 316 let button = {
michael@0 317 label: buttonData.label,
michael@0 318 callbackID: buttonData.callbackID,
michael@0 319 };
michael@0 320
michael@0 321 if (typeof buttonData.icon == "string")
michael@0 322 button.iconURL = this.resolveURL(contentDocument, buttonData.icon);
michael@0 323
michael@0 324 if (typeof buttonData.style == "string")
michael@0 325 button.style = buttonData.style;
michael@0 326
michael@0 327 buttons.push(button);
michael@0 328
michael@0 329 if (buttons.length == MAX_BUTTONS)
michael@0 330 break;
michael@0 331 }
michael@0 332 }
michael@0 333 }
michael@0 334
michael@0 335 let infoOptions = {};
michael@0 336
michael@0 337 if (typeof data.closeButtonCallbackID == "string")
michael@0 338 infoOptions.closeButtonCallbackID = data.closeButtonCallbackID;
michael@0 339 if (typeof data.targetCallbackID == "string")
michael@0 340 infoOptions.targetCallbackID = data.targetCallbackID;
michael@0 341
michael@0 342 this.showInfo(contentDocument, target, data.title, data.text, iconURL, buttons, infoOptions);
michael@0 343 }).then(null, Cu.reportError);
michael@0 344 break;
michael@0 345 }
michael@0 346
michael@0 347 case "hideInfo": {
michael@0 348 this.hideInfo(window);
michael@0 349 break;
michael@0 350 }
michael@0 351
michael@0 352 case "previewTheme": {
michael@0 353 this.previewTheme(data.theme);
michael@0 354 break;
michael@0 355 }
michael@0 356
michael@0 357 case "resetTheme": {
michael@0 358 this.resetTheme();
michael@0 359 break;
michael@0 360 }
michael@0 361
michael@0 362 case "addPinnedTab": {
michael@0 363 this.ensurePinnedTab(window, true);
michael@0 364 break;
michael@0 365 }
michael@0 366
michael@0 367 case "removePinnedTab": {
michael@0 368 this.removePinnedTab(window);
michael@0 369 break;
michael@0 370 }
michael@0 371
michael@0 372 case "showMenu": {
michael@0 373 this.showMenu(window, data.name);
michael@0 374 break;
michael@0 375 }
michael@0 376
michael@0 377 case "hideMenu": {
michael@0 378 this.hideMenu(window, data.name);
michael@0 379 break;
michael@0 380 }
michael@0 381
michael@0 382 case "startUrlbarCapture": {
michael@0 383 if (typeof data.text != "string" || !data.text ||
michael@0 384 typeof data.url != "string" || !data.url) {
michael@0 385 return false;
michael@0 386 }
michael@0 387
michael@0 388 let uri = null;
michael@0 389 try {
michael@0 390 uri = Services.io.newURI(data.url, null, null);
michael@0 391 } catch (e) {
michael@0 392 return false;
michael@0 393 }
michael@0 394
michael@0 395 let secman = Services.scriptSecurityManager;
michael@0 396 let principal = contentDocument.nodePrincipal;
michael@0 397 let flags = secman.DISALLOW_INHERIT_PRINCIPAL;
michael@0 398 try {
michael@0 399 secman.checkLoadURIWithPrincipal(principal, uri, flags);
michael@0 400 } catch (e) {
michael@0 401 return false;
michael@0 402 }
michael@0 403
michael@0 404 this.startUrlbarCapture(window, data.text, data.url);
michael@0 405 break;
michael@0 406 }
michael@0 407
michael@0 408 case "endUrlbarCapture": {
michael@0 409 this.endUrlbarCapture(window);
michael@0 410 break;
michael@0 411 }
michael@0 412
michael@0 413 case "getConfiguration": {
michael@0 414 if (typeof data.configuration != "string") {
michael@0 415 return false;
michael@0 416 }
michael@0 417
michael@0 418 this.getConfiguration(contentDocument, data.configuration, data.callbackID);
michael@0 419 break;
michael@0 420 }
michael@0 421
michael@0 422 case "showFirefoxAccounts": {
michael@0 423 // 'signup' is the only action that makes sense currently, so we don't
michael@0 424 // accept arbitrary actions just to be safe...
michael@0 425 // We want to replace the current tab.
michael@0 426 contentDocument.location.href = "about:accounts?action=signup";
michael@0 427 break;
michael@0 428 }
michael@0 429 }
michael@0 430
michael@0 431 if (!this.originTabs.has(window))
michael@0 432 this.originTabs.set(window, new Set());
michael@0 433
michael@0 434 this.originTabs.get(window).add(tab);
michael@0 435 tab.addEventListener("TabClose", this);
michael@0 436 tab.addEventListener("TabBecomingWindow", this);
michael@0 437 window.addEventListener("SSWindowClosing", this);
michael@0 438
michael@0 439 return true;
michael@0 440 },
michael@0 441
michael@0 442 handleEvent: function(aEvent) {
michael@0 443 switch (aEvent.type) {
michael@0 444 case "pagehide": {
michael@0 445 let window = this.getChromeWindow(aEvent.target);
michael@0 446 this.teardownTour(window);
michael@0 447 break;
michael@0 448 }
michael@0 449
michael@0 450 case "TabBecomingWindow":
michael@0 451 this._detachingTab = true;
michael@0 452 // Fall through
michael@0 453 case "TabClose": {
michael@0 454 let tab = aEvent.target;
michael@0 455 if (this.pageIDSourceTabs.has(tab)) {
michael@0 456 let pageID = this.pageIDSourceTabs.get(tab);
michael@0 457
michael@0 458 // Delete this from the window cache, so if the window is closed we
michael@0 459 // don't expire this page ID twice.
michael@0 460 let window = tab.ownerDocument.defaultView;
michael@0 461 if (this.pageIDSourceWindows.get(window) == pageID)
michael@0 462 this.pageIDSourceWindows.delete(window);
michael@0 463
michael@0 464 this.setExpiringTelemetryBucket(pageID, "closed");
michael@0 465 }
michael@0 466
michael@0 467 let window = tab.ownerDocument.defaultView;
michael@0 468 this.teardownTour(window);
michael@0 469 break;
michael@0 470 }
michael@0 471
michael@0 472 case "TabSelect": {
michael@0 473 if (aEvent.detail && aEvent.detail.previousTab) {
michael@0 474 let previousTab = aEvent.detail.previousTab;
michael@0 475
michael@0 476 if (this.pageIDSourceTabs.has(previousTab)) {
michael@0 477 let pageID = this.pageIDSourceTabs.get(previousTab);
michael@0 478 this.setExpiringTelemetryBucket(pageID, "inactive");
michael@0 479 }
michael@0 480 }
michael@0 481
michael@0 482 let window = aEvent.target.ownerDocument.defaultView;
michael@0 483 let selectedTab = window.gBrowser.selectedTab;
michael@0 484 let pinnedTab = this.pinnedTabs.get(window);
michael@0 485 if (pinnedTab && pinnedTab.tab == selectedTab)
michael@0 486 break;
michael@0 487 let originTabs = this.originTabs.get(window);
michael@0 488 if (originTabs && originTabs.has(selectedTab))
michael@0 489 break;
michael@0 490
michael@0 491 let pendingDoc;
michael@0 492 if (this._detachingTab && this._pendingDoc && (pendingDoc = this._pendingDoc.get())) {
michael@0 493 if (selectedTab.linkedBrowser.contentDocument == pendingDoc) {
michael@0 494 if (!this.originTabs.get(window)) {
michael@0 495 this.originTabs.set(window, new Set());
michael@0 496 }
michael@0 497 this.originTabs.get(window).add(selectedTab);
michael@0 498 this.pendingDoc = null;
michael@0 499 this._detachingTab = false;
michael@0 500 while (this._queuedEvents.length) {
michael@0 501 try {
michael@0 502 this.onPageEvent(this._queuedEvents.shift());
michael@0 503 } catch (ex) {
michael@0 504 Cu.reportError(ex);
michael@0 505 }
michael@0 506 }
michael@0 507 break;
michael@0 508 }
michael@0 509 }
michael@0 510
michael@0 511 this.teardownTour(window);
michael@0 512 break;
michael@0 513 }
michael@0 514
michael@0 515 case "SSWindowClosing": {
michael@0 516 let window = aEvent.target;
michael@0 517 if (this.pageIDSourceWindows.has(window)) {
michael@0 518 let pageID = this.pageIDSourceWindows.get(window);
michael@0 519 this.setExpiringTelemetryBucket(pageID, "closed");
michael@0 520 }
michael@0 521
michael@0 522 this.teardownTour(window, true);
michael@0 523 break;
michael@0 524 }
michael@0 525
michael@0 526 case "input": {
michael@0 527 if (aEvent.target.id == "urlbar") {
michael@0 528 let window = aEvent.target.ownerDocument.defaultView;
michael@0 529 this.handleUrlbarInput(window);
michael@0 530 }
michael@0 531 break;
michael@0 532 }
michael@0 533 }
michael@0 534 },
michael@0 535
michael@0 536 setTelemetryBucket: function(aPageID) {
michael@0 537 let bucket = BUCKET_NAME + BrowserUITelemetry.BUCKET_SEPARATOR + aPageID;
michael@0 538 BrowserUITelemetry.setBucket(bucket);
michael@0 539 },
michael@0 540
michael@0 541 setExpiringTelemetryBucket: function(aPageID, aType) {
michael@0 542 let bucket = BUCKET_NAME + BrowserUITelemetry.BUCKET_SEPARATOR + aPageID +
michael@0 543 BrowserUITelemetry.BUCKET_SEPARATOR + aType;
michael@0 544
michael@0 545 BrowserUITelemetry.setExpiringBucket(bucket,
michael@0 546 BUCKET_TIMESTEPS);
michael@0 547 },
michael@0 548
michael@0 549 // This is registered with UITelemetry by BrowserUITelemetry, so that UITour
michael@0 550 // can remain lazy-loaded on-demand.
michael@0 551 getTelemetry: function() {
michael@0 552 return {
michael@0 553 seenPageIDs: [...this.seenPageIDs.keys()],
michael@0 554 };
michael@0 555 },
michael@0 556
michael@0 557 teardownTour: function(aWindow, aWindowClosing = false) {
michael@0 558 aWindow.gBrowser.tabContainer.removeEventListener("TabSelect", this);
michael@0 559 aWindow.PanelUI.panel.removeEventListener("popuphiding", this.hidePanelAnnotations);
michael@0 560 aWindow.PanelUI.panel.removeEventListener("ViewShowing", this.hidePanelAnnotations);
michael@0 561 aWindow.removeEventListener("SSWindowClosing", this);
michael@0 562
michael@0 563 let originTabs = this.originTabs.get(aWindow);
michael@0 564 if (originTabs) {
michael@0 565 for (let tab of originTabs) {
michael@0 566 tab.removeEventListener("TabClose", this);
michael@0 567 tab.removeEventListener("TabBecomingWindow", this);
michael@0 568 }
michael@0 569 }
michael@0 570 this.originTabs.delete(aWindow);
michael@0 571
michael@0 572 if (!aWindowClosing) {
michael@0 573 this.hideHighlight(aWindow);
michael@0 574 this.hideInfo(aWindow);
michael@0 575 // Ensure the menu panel is hidden before calling recreatePopup so popup events occur.
michael@0 576 this.hideMenu(aWindow, "appMenu");
michael@0 577 }
michael@0 578
michael@0 579 this.endUrlbarCapture(aWindow);
michael@0 580 this.removePinnedTab(aWindow);
michael@0 581 this.resetTheme();
michael@0 582 },
michael@0 583
michael@0 584 getChromeWindow: function(aContentDocument) {
michael@0 585 return aContentDocument.defaultView
michael@0 586 .window
michael@0 587 .QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 588 .getInterface(Ci.nsIWebNavigation)
michael@0 589 .QueryInterface(Ci.nsIDocShellTreeItem)
michael@0 590 .rootTreeItem
michael@0 591 .QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 592 .getInterface(Ci.nsIDOMWindow)
michael@0 593 .wrappedJSObject;
michael@0 594 },
michael@0 595
michael@0 596 importPermissions: function() {
michael@0 597 try {
michael@0 598 PermissionsUtils.importFromPrefs(PREF_PERM_BRANCH, UITOUR_PERMISSION);
michael@0 599 } catch (e) {
michael@0 600 Cu.reportError(e);
michael@0 601 }
michael@0 602 },
michael@0 603
michael@0 604 ensureTrustedOrigin: function(aDocument) {
michael@0 605 if (aDocument.defaultView.top != aDocument.defaultView)
michael@0 606 return false;
michael@0 607
michael@0 608 let uri = aDocument.documentURIObject;
michael@0 609
michael@0 610 if (uri.schemeIs("chrome"))
michael@0 611 return true;
michael@0 612
michael@0 613 if (!this.isSafeScheme(uri))
michael@0 614 return false;
michael@0 615
michael@0 616 this.importPermissions();
michael@0 617 let permission = Services.perms.testPermission(uri, UITOUR_PERMISSION);
michael@0 618 return permission == Services.perms.ALLOW_ACTION;
michael@0 619 },
michael@0 620
michael@0 621 isSafeScheme: function(aURI) {
michael@0 622 let allowedSchemes = new Set(["https"]);
michael@0 623 if (!Services.prefs.getBoolPref("browser.uitour.requireSecure"))
michael@0 624 allowedSchemes.add("http");
michael@0 625
michael@0 626 if (!allowedSchemes.has(aURI.scheme))
michael@0 627 return false;
michael@0 628
michael@0 629 return true;
michael@0 630 },
michael@0 631
michael@0 632 resolveURL: function(aDocument, aURL) {
michael@0 633 try {
michael@0 634 let uri = Services.io.newURI(aURL, null, aDocument.documentURIObject);
michael@0 635
michael@0 636 if (!this.isSafeScheme(uri))
michael@0 637 return null;
michael@0 638
michael@0 639 return uri.spec;
michael@0 640 } catch (e) {}
michael@0 641
michael@0 642 return null;
michael@0 643 },
michael@0 644
michael@0 645 sendPageCallback: function(aDocument, aCallbackID, aData = {}) {
michael@0 646 let detail = Cu.createObjectIn(aDocument.defaultView);
michael@0 647 detail.data = Cu.createObjectIn(detail);
michael@0 648
michael@0 649 for (let key of Object.keys(aData))
michael@0 650 detail.data[key] = aData[key];
michael@0 651
michael@0 652 Cu.makeObjectPropsNormal(detail.data);
michael@0 653 Cu.makeObjectPropsNormal(detail);
michael@0 654
michael@0 655 detail.callbackID = aCallbackID;
michael@0 656
michael@0 657 let event = new aDocument.defaultView.CustomEvent("mozUITourResponse", {
michael@0 658 bubbles: true,
michael@0 659 detail: detail
michael@0 660 });
michael@0 661
michael@0 662 aDocument.dispatchEvent(event);
michael@0 663 },
michael@0 664
michael@0 665 isElementVisible: function(aElement) {
michael@0 666 let targetStyle = aElement.ownerDocument.defaultView.getComputedStyle(aElement);
michael@0 667 return (targetStyle.display != "none" && targetStyle.visibility == "visible");
michael@0 668 },
michael@0 669
michael@0 670 getTarget: function(aWindow, aTargetName, aSticky = false) {
michael@0 671 let deferred = Promise.defer();
michael@0 672 if (typeof aTargetName != "string" || !aTargetName) {
michael@0 673 deferred.reject("Invalid target name specified");
michael@0 674 return deferred.promise;
michael@0 675 }
michael@0 676
michael@0 677 if (aTargetName == "pinnedTab") {
michael@0 678 deferred.resolve({
michael@0 679 targetName: aTargetName,
michael@0 680 node: this.ensurePinnedTab(aWindow, aSticky)
michael@0 681 });
michael@0 682 return deferred.promise;
michael@0 683 }
michael@0 684
michael@0 685 let targetObject = this.targets.get(aTargetName);
michael@0 686 if (!targetObject) {
michael@0 687 deferred.reject("The specified target name is not in the allowed set");
michael@0 688 return deferred.promise;
michael@0 689 }
michael@0 690
michael@0 691 let targetQuery = targetObject.query;
michael@0 692 aWindow.PanelUI.ensureReady().then(() => {
michael@0 693 let node;
michael@0 694 if (typeof targetQuery == "function") {
michael@0 695 try {
michael@0 696 node = targetQuery(aWindow.document);
michael@0 697 } catch (ex) {
michael@0 698 node = null;
michael@0 699 }
michael@0 700 } else {
michael@0 701 node = aWindow.document.querySelector(targetQuery);
michael@0 702 }
michael@0 703
michael@0 704 deferred.resolve({
michael@0 705 addTargetListener: targetObject.addTargetListener,
michael@0 706 node: node,
michael@0 707 removeTargetListener: targetObject.removeTargetListener,
michael@0 708 targetName: aTargetName,
michael@0 709 widgetName: targetObject.widgetName,
michael@0 710 });
michael@0 711 }).then(null, Cu.reportError);
michael@0 712 return deferred.promise;
michael@0 713 },
michael@0 714
michael@0 715 targetIsInAppMenu: function(aTarget) {
michael@0 716 let placement = CustomizableUI.getPlacementOfWidget(aTarget.widgetName || aTarget.node.id);
michael@0 717 if (placement && placement.area == CustomizableUI.AREA_PANEL) {
michael@0 718 return true;
michael@0 719 }
michael@0 720
michael@0 721 let targetElement = aTarget.node;
michael@0 722 // Use the widget for filtering if it exists since the target may be the icon inside.
michael@0 723 if (aTarget.widgetName) {
michael@0 724 targetElement = aTarget.node.ownerDocument.getElementById(aTarget.widgetName);
michael@0 725 }
michael@0 726
michael@0 727 // Handle the non-customizable buttons at the bottom of the menu which aren't proper widgets.
michael@0 728 return targetElement.id.startsWith("PanelUI-")
michael@0 729 && targetElement.id != "PanelUI-button";
michael@0 730 },
michael@0 731
michael@0 732 /**
michael@0 733 * Called before opening or after closing a highlight or info panel to see if
michael@0 734 * we need to open or close the appMenu to see the annotation's anchor.
michael@0 735 */
michael@0 736 _setAppMenuStateForAnnotation: function(aWindow, aAnnotationType, aShouldOpenForHighlight, aCallback = null) {
michael@0 737 // If the panel is in the desired state, we're done.
michael@0 738 let panelIsOpen = aWindow.PanelUI.panel.state != "closed";
michael@0 739 if (aShouldOpenForHighlight == panelIsOpen) {
michael@0 740 if (aCallback)
michael@0 741 aCallback();
michael@0 742 return;
michael@0 743 }
michael@0 744
michael@0 745 // Don't close the menu if it wasn't opened by us (e.g. via showmenu instead).
michael@0 746 if (!aShouldOpenForHighlight && !this.appMenuOpenForAnnotation.has(aAnnotationType)) {
michael@0 747 if (aCallback)
michael@0 748 aCallback();
michael@0 749 return;
michael@0 750 }
michael@0 751
michael@0 752 if (aShouldOpenForHighlight) {
michael@0 753 this.appMenuOpenForAnnotation.add(aAnnotationType);
michael@0 754 } else {
michael@0 755 this.appMenuOpenForAnnotation.delete(aAnnotationType);
michael@0 756 }
michael@0 757
michael@0 758 // Actually show or hide the menu
michael@0 759 if (this.appMenuOpenForAnnotation.size) {
michael@0 760 this.showMenu(aWindow, "appMenu", aCallback);
michael@0 761 } else {
michael@0 762 this.hideMenu(aWindow, "appMenu");
michael@0 763 if (aCallback)
michael@0 764 aCallback();
michael@0 765 }
michael@0 766
michael@0 767 },
michael@0 768
michael@0 769 previewTheme: function(aTheme) {
michael@0 770 let origin = Services.prefs.getCharPref("browser.uitour.themeOrigin");
michael@0 771 let data = LightweightThemeManager.parseTheme(aTheme, origin);
michael@0 772 if (data)
michael@0 773 LightweightThemeManager.previewTheme(data);
michael@0 774 },
michael@0 775
michael@0 776 resetTheme: function() {
michael@0 777 LightweightThemeManager.resetPreview();
michael@0 778 },
michael@0 779
michael@0 780 ensurePinnedTab: function(aWindow, aSticky = false) {
michael@0 781 let tabInfo = this.pinnedTabs.get(aWindow);
michael@0 782
michael@0 783 if (tabInfo) {
michael@0 784 tabInfo.sticky = tabInfo.sticky || aSticky;
michael@0 785 } else {
michael@0 786 let url = Services.urlFormatter.formatURLPref("browser.uitour.pinnedTabUrl");
michael@0 787
michael@0 788 let tab = aWindow.gBrowser.addTab(url);
michael@0 789 aWindow.gBrowser.pinTab(tab);
michael@0 790 tab.addEventListener("TabClose", () => {
michael@0 791 this.pinnedTabs.delete(aWindow);
michael@0 792 });
michael@0 793
michael@0 794 tabInfo = {
michael@0 795 tab: tab,
michael@0 796 sticky: aSticky
michael@0 797 };
michael@0 798 this.pinnedTabs.set(aWindow, tabInfo);
michael@0 799 }
michael@0 800
michael@0 801 return tabInfo.tab;
michael@0 802 },
michael@0 803
michael@0 804 removePinnedTab: function(aWindow) {
michael@0 805 let tabInfo = this.pinnedTabs.get(aWindow);
michael@0 806 if (tabInfo)
michael@0 807 aWindow.gBrowser.removeTab(tabInfo.tab);
michael@0 808 },
michael@0 809
michael@0 810 /**
michael@0 811 * @param aTarget The element to highlight.
michael@0 812 * @param aEffect (optional) The effect to use from UITour.highlightEffects or "none".
michael@0 813 * @see UITour.highlightEffects
michael@0 814 */
michael@0 815 showHighlight: function(aTarget, aEffect = "none") {
michael@0 816 function showHighlightPanel(aTargetEl) {
michael@0 817 let highlighter = aTargetEl.ownerDocument.getElementById("UITourHighlight");
michael@0 818
michael@0 819 let effect = aEffect;
michael@0 820 if (effect == "random") {
michael@0 821 // Exclude "random" from the randomly selected effects.
michael@0 822 let randomEffect = 1 + Math.floor(Math.random() * (this.highlightEffects.length - 1));
michael@0 823 if (randomEffect == this.highlightEffects.length)
michael@0 824 randomEffect--; // On the order of 1 in 2^62 chance of this happening.
michael@0 825 effect = this.highlightEffects[randomEffect];
michael@0 826 }
michael@0 827 // Toggle the effect attribute to "none" and flush layout before setting it so the effect plays.
michael@0 828 highlighter.setAttribute("active", "none");
michael@0 829 aTargetEl.ownerDocument.defaultView.getComputedStyle(highlighter).animationName;
michael@0 830 highlighter.setAttribute("active", effect);
michael@0 831 highlighter.parentElement.setAttribute("targetName", aTarget.targetName);
michael@0 832 highlighter.parentElement.hidden = false;
michael@0 833
michael@0 834 let targetRect = aTargetEl.getBoundingClientRect();
michael@0 835 let highlightHeight = targetRect.height;
michael@0 836 let highlightWidth = targetRect.width;
michael@0 837 let minDimension = Math.min(highlightHeight, highlightWidth);
michael@0 838 let maxDimension = Math.max(highlightHeight, highlightWidth);
michael@0 839
michael@0 840 // If the dimensions are within 200% of each other (to include the bookmarks button),
michael@0 841 // make the highlight a circle with the largest dimension as the diameter.
michael@0 842 if (maxDimension / minDimension <= 3.0) {
michael@0 843 highlightHeight = highlightWidth = maxDimension;
michael@0 844 highlighter.style.borderRadius = "100%";
michael@0 845 } else {
michael@0 846 highlighter.style.borderRadius = "";
michael@0 847 }
michael@0 848
michael@0 849 highlighter.style.height = highlightHeight + "px";
michael@0 850 highlighter.style.width = highlightWidth + "px";
michael@0 851
michael@0 852 // Close a previous highlight so we can relocate the panel.
michael@0 853 if (highlighter.parentElement.state == "open") {
michael@0 854 highlighter.parentElement.hidePopup();
michael@0 855 }
michael@0 856 /* The "overlap" position anchors from the top-left but we want to centre highlights at their
michael@0 857 minimum size. */
michael@0 858 let highlightWindow = aTargetEl.ownerDocument.defaultView;
michael@0 859 let containerStyle = highlightWindow.getComputedStyle(highlighter.parentElement);
michael@0 860 let paddingTopPx = 0 - parseFloat(containerStyle.paddingTop);
michael@0 861 let paddingLeftPx = 0 - parseFloat(containerStyle.paddingLeft);
michael@0 862 let highlightStyle = highlightWindow.getComputedStyle(highlighter);
michael@0 863 let highlightHeightWithMin = Math.max(highlightHeight, parseFloat(highlightStyle.minHeight));
michael@0 864 let highlightWidthWithMin = Math.max(highlightWidth, parseFloat(highlightStyle.minWidth));
michael@0 865 let offsetX = paddingTopPx
michael@0 866 - (Math.max(0, highlightWidthWithMin - targetRect.width) / 2);
michael@0 867 let offsetY = paddingLeftPx
michael@0 868 - (Math.max(0, highlightHeightWithMin - targetRect.height) / 2);
michael@0 869
michael@0 870 this._addAnnotationPanelMutationObserver(highlighter.parentElement);
michael@0 871 highlighter.parentElement.openPopup(aTargetEl, "overlap", offsetX, offsetY);
michael@0 872 }
michael@0 873
michael@0 874 // Prevent showing a panel at an undefined position.
michael@0 875 if (!this.isElementVisible(aTarget.node))
michael@0 876 return;
michael@0 877
michael@0 878 this._setAppMenuStateForAnnotation(aTarget.node.ownerDocument.defaultView, "highlight",
michael@0 879 this.targetIsInAppMenu(aTarget),
michael@0 880 showHighlightPanel.bind(this, aTarget.node));
michael@0 881 },
michael@0 882
michael@0 883 hideHighlight: function(aWindow) {
michael@0 884 let tabData = this.pinnedTabs.get(aWindow);
michael@0 885 if (tabData && !tabData.sticky)
michael@0 886 this.removePinnedTab(aWindow);
michael@0 887
michael@0 888 let highlighter = aWindow.document.getElementById("UITourHighlight");
michael@0 889 this._removeAnnotationPanelMutationObserver(highlighter.parentElement);
michael@0 890 highlighter.parentElement.hidePopup();
michael@0 891 highlighter.removeAttribute("active");
michael@0 892
michael@0 893 this._setAppMenuStateForAnnotation(aWindow, "highlight", false);
michael@0 894 },
michael@0 895
michael@0 896 /**
michael@0 897 * Show an info panel.
michael@0 898 *
michael@0 899 * @param {Document} aContentDocument
michael@0 900 * @param {Node} aAnchor
michael@0 901 * @param {String} [aTitle=""]
michael@0 902 * @param {String} [aDescription=""]
michael@0 903 * @param {String} [aIconURL=""]
michael@0 904 * @param {Object[]} [aButtons=[]]
michael@0 905 * @param {Object} [aOptions={}]
michael@0 906 * @param {String} [aOptions.closeButtonCallbackID]
michael@0 907 */
michael@0 908 showInfo: function(aContentDocument, aAnchor, aTitle = "", aDescription = "", aIconURL = "",
michael@0 909 aButtons = [], aOptions = {}) {
michael@0 910 function showInfoPanel(aAnchorEl) {
michael@0 911 aAnchorEl.focus();
michael@0 912
michael@0 913 let document = aAnchorEl.ownerDocument;
michael@0 914 let tooltip = document.getElementById("UITourTooltip");
michael@0 915 let tooltipTitle = document.getElementById("UITourTooltipTitle");
michael@0 916 let tooltipDesc = document.getElementById("UITourTooltipDescription");
michael@0 917 let tooltipIcon = document.getElementById("UITourTooltipIcon");
michael@0 918 let tooltipButtons = document.getElementById("UITourTooltipButtons");
michael@0 919
michael@0 920 if (tooltip.state == "open") {
michael@0 921 tooltip.hidePopup();
michael@0 922 }
michael@0 923
michael@0 924 tooltipTitle.textContent = aTitle || "";
michael@0 925 tooltipDesc.textContent = aDescription || "";
michael@0 926 tooltipIcon.src = aIconURL || "";
michael@0 927 tooltipIcon.hidden = !aIconURL;
michael@0 928
michael@0 929 while (tooltipButtons.firstChild)
michael@0 930 tooltipButtons.firstChild.remove();
michael@0 931
michael@0 932 for (let button of aButtons) {
michael@0 933 let el = document.createElement("button");
michael@0 934 el.setAttribute("label", button.label);
michael@0 935 if (button.iconURL)
michael@0 936 el.setAttribute("image", button.iconURL);
michael@0 937
michael@0 938 if (button.style == "link")
michael@0 939 el.setAttribute("class", "button-link");
michael@0 940
michael@0 941 if (button.style == "primary")
michael@0 942 el.setAttribute("class", "button-primary");
michael@0 943
michael@0 944 let callbackID = button.callbackID;
michael@0 945 el.addEventListener("command", event => {
michael@0 946 tooltip.hidePopup();
michael@0 947 this.sendPageCallback(aContentDocument, callbackID);
michael@0 948 });
michael@0 949
michael@0 950 tooltipButtons.appendChild(el);
michael@0 951 }
michael@0 952
michael@0 953 tooltipButtons.hidden = !aButtons.length;
michael@0 954
michael@0 955 let tooltipClose = document.getElementById("UITourTooltipClose");
michael@0 956 let closeButtonCallback = (event) => {
michael@0 957 this.hideInfo(document.defaultView);
michael@0 958 if (aOptions && aOptions.closeButtonCallbackID)
michael@0 959 this.sendPageCallback(aContentDocument, aOptions.closeButtonCallbackID);
michael@0 960 };
michael@0 961 tooltipClose.addEventListener("command", closeButtonCallback);
michael@0 962
michael@0 963 let targetCallback = (event) => {
michael@0 964 let details = {
michael@0 965 target: aAnchor.targetName,
michael@0 966 type: event.type,
michael@0 967 };
michael@0 968 this.sendPageCallback(aContentDocument, aOptions.targetCallbackID, details);
michael@0 969 };
michael@0 970 if (aOptions.targetCallbackID && aAnchor.addTargetListener) {
michael@0 971 aAnchor.addTargetListener(document, targetCallback);
michael@0 972 }
michael@0 973
michael@0 974 tooltip.addEventListener("popuphiding", function tooltipHiding(event) {
michael@0 975 tooltip.removeEventListener("popuphiding", tooltipHiding);
michael@0 976 tooltipClose.removeEventListener("command", closeButtonCallback);
michael@0 977 if (aOptions.targetCallbackID && aAnchor.removeTargetListener) {
michael@0 978 aAnchor.removeTargetListener(document, targetCallback);
michael@0 979 }
michael@0 980 });
michael@0 981
michael@0 982 tooltip.setAttribute("targetName", aAnchor.targetName);
michael@0 983 tooltip.hidden = false;
michael@0 984 let alignment = "bottomcenter topright";
michael@0 985 this._addAnnotationPanelMutationObserver(tooltip);
michael@0 986 tooltip.openPopup(aAnchorEl, alignment);
michael@0 987 }
michael@0 988
michael@0 989 // Prevent showing a panel at an undefined position.
michael@0 990 if (!this.isElementVisible(aAnchor.node))
michael@0 991 return;
michael@0 992
michael@0 993 this._setAppMenuStateForAnnotation(aAnchor.node.ownerDocument.defaultView, "info",
michael@0 994 this.targetIsInAppMenu(aAnchor),
michael@0 995 showInfoPanel.bind(this, aAnchor.node));
michael@0 996 },
michael@0 997
michael@0 998 hideInfo: function(aWindow) {
michael@0 999 let document = aWindow.document;
michael@0 1000
michael@0 1001 let tooltip = document.getElementById("UITourTooltip");
michael@0 1002 this._removeAnnotationPanelMutationObserver(tooltip);
michael@0 1003 tooltip.hidePopup();
michael@0 1004 this._setAppMenuStateForAnnotation(aWindow, "info", false);
michael@0 1005
michael@0 1006 let tooltipButtons = document.getElementById("UITourTooltipButtons");
michael@0 1007 while (tooltipButtons.firstChild)
michael@0 1008 tooltipButtons.firstChild.remove();
michael@0 1009 },
michael@0 1010
michael@0 1011 showMenu: function(aWindow, aMenuName, aOpenCallback = null) {
michael@0 1012 function openMenuButton(aID) {
michael@0 1013 let menuBtn = aWindow.document.getElementById(aID);
michael@0 1014 if (!menuBtn || !menuBtn.boxObject) {
michael@0 1015 aOpenCallback();
michael@0 1016 return;
michael@0 1017 }
michael@0 1018 if (aOpenCallback)
michael@0 1019 menuBtn.addEventListener("popupshown", onPopupShown);
michael@0 1020 menuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(true);
michael@0 1021 }
michael@0 1022 function onPopupShown(event) {
michael@0 1023 this.removeEventListener("popupshown", onPopupShown);
michael@0 1024 aOpenCallback(event);
michael@0 1025 }
michael@0 1026
michael@0 1027 if (aMenuName == "appMenu") {
michael@0 1028 aWindow.PanelUI.panel.setAttribute("noautohide", "true");
michael@0 1029 // If the popup is already opened, don't recreate the widget as it may cause a flicker.
michael@0 1030 if (aWindow.PanelUI.panel.state != "open") {
michael@0 1031 this.recreatePopup(aWindow.PanelUI.panel);
michael@0 1032 }
michael@0 1033 aWindow.PanelUI.panel.addEventListener("popuphiding", this.hidePanelAnnotations);
michael@0 1034 aWindow.PanelUI.panel.addEventListener("ViewShowing", this.hidePanelAnnotations);
michael@0 1035 if (aOpenCallback) {
michael@0 1036 aWindow.PanelUI.panel.addEventListener("popupshown", onPopupShown);
michael@0 1037 }
michael@0 1038 aWindow.PanelUI.show();
michael@0 1039 } else if (aMenuName == "bookmarks") {
michael@0 1040 openMenuButton("bookmarks-menu-button");
michael@0 1041 }
michael@0 1042 },
michael@0 1043
michael@0 1044 hideMenu: function(aWindow, aMenuName) {
michael@0 1045 function closeMenuButton(aID) {
michael@0 1046 let menuBtn = aWindow.document.getElementById(aID);
michael@0 1047 if (menuBtn && menuBtn.boxObject)
michael@0 1048 menuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(false);
michael@0 1049 }
michael@0 1050
michael@0 1051 if (aMenuName == "appMenu") {
michael@0 1052 aWindow.PanelUI.panel.removeAttribute("noautohide");
michael@0 1053 aWindow.PanelUI.hide();
michael@0 1054 this.recreatePopup(aWindow.PanelUI.panel);
michael@0 1055 } else if (aMenuName == "bookmarks") {
michael@0 1056 closeMenuButton("bookmarks-menu-button");
michael@0 1057 }
michael@0 1058 },
michael@0 1059
michael@0 1060 hidePanelAnnotations: function(aEvent) {
michael@0 1061 let win = aEvent.target.ownerDocument.defaultView;
michael@0 1062 let annotationElements = new Map([
michael@0 1063 // [annotationElement (panel), method to hide the annotation]
michael@0 1064 [win.document.getElementById("UITourHighlightContainer"), UITour.hideHighlight.bind(UITour)],
michael@0 1065 [win.document.getElementById("UITourTooltip"), UITour.hideInfo.bind(UITour)],
michael@0 1066 ]);
michael@0 1067 annotationElements.forEach((hideMethod, annotationElement) => {
michael@0 1068 if (annotationElement.state != "closed") {
michael@0 1069 let targetName = annotationElement.getAttribute("targetName");
michael@0 1070 UITour.getTarget(win, targetName).then((aTarget) => {
michael@0 1071 // Since getTarget is async, we need to make sure that the target hasn't
michael@0 1072 // changed since it may have just moved to somewhere outside of the app menu.
michael@0 1073 if (annotationElement.getAttribute("targetName") != aTarget.targetName ||
michael@0 1074 annotationElement.state == "closed" ||
michael@0 1075 !UITour.targetIsInAppMenu(aTarget)) {
michael@0 1076 return;
michael@0 1077 }
michael@0 1078 hideMethod(win);
michael@0 1079 }).then(null, Cu.reportError);
michael@0 1080 }
michael@0 1081 });
michael@0 1082 UITour.appMenuOpenForAnnotation.clear();
michael@0 1083 },
michael@0 1084
michael@0 1085 recreatePopup: function(aPanel) {
michael@0 1086 // After changing popup attributes that relate to how the native widget is created
michael@0 1087 // (e.g. @noautohide) we need to re-create the frame/widget for it to take effect.
michael@0 1088 if (aPanel.hidden) {
michael@0 1089 // If the panel is already hidden, we don't need to recreate it but flush
michael@0 1090 // in case someone just hid it.
michael@0 1091 aPanel.clientWidth; // flush
michael@0 1092 return;
michael@0 1093 }
michael@0 1094 aPanel.hidden = true;
michael@0 1095 aPanel.clientWidth; // flush
michael@0 1096 aPanel.hidden = false;
michael@0 1097 },
michael@0 1098
michael@0 1099 startUrlbarCapture: function(aWindow, aExpectedText, aUrl) {
michael@0 1100 let urlbar = aWindow.document.getElementById("urlbar");
michael@0 1101 this.urlbarCapture.set(aWindow, {
michael@0 1102 expected: aExpectedText.toLocaleLowerCase(),
michael@0 1103 url: aUrl
michael@0 1104 });
michael@0 1105 urlbar.addEventListener("input", this);
michael@0 1106 },
michael@0 1107
michael@0 1108 endUrlbarCapture: function(aWindow) {
michael@0 1109 let urlbar = aWindow.document.getElementById("urlbar");
michael@0 1110 urlbar.removeEventListener("input", this);
michael@0 1111 this.urlbarCapture.delete(aWindow);
michael@0 1112 },
michael@0 1113
michael@0 1114 handleUrlbarInput: function(aWindow) {
michael@0 1115 if (!this.urlbarCapture.has(aWindow))
michael@0 1116 return;
michael@0 1117
michael@0 1118 let urlbar = aWindow.document.getElementById("urlbar");
michael@0 1119
michael@0 1120 let {expected, url} = this.urlbarCapture.get(aWindow);
michael@0 1121
michael@0 1122 if (urlbar.value.toLocaleLowerCase().localeCompare(expected) != 0)
michael@0 1123 return;
michael@0 1124
michael@0 1125 urlbar.handleRevert();
michael@0 1126
michael@0 1127 let tab = aWindow.gBrowser.addTab(url, {
michael@0 1128 owner: aWindow.gBrowser.selectedTab,
michael@0 1129 relatedToCurrent: true
michael@0 1130 });
michael@0 1131 aWindow.gBrowser.selectedTab = tab;
michael@0 1132 },
michael@0 1133
michael@0 1134 getConfiguration: function(aContentDocument, aConfiguration, aCallbackID) {
michael@0 1135 switch (aConfiguration) {
michael@0 1136 case "availableTargets":
michael@0 1137 this.getAvailableTargets(aContentDocument, aCallbackID);
michael@0 1138 break;
michael@0 1139 case "sync":
michael@0 1140 this.sendPageCallback(aContentDocument, aCallbackID, {
michael@0 1141 setup: Services.prefs.prefHasUserValue("services.sync.username"),
michael@0 1142 });
michael@0 1143 break;
michael@0 1144 default:
michael@0 1145 Cu.reportError("getConfiguration: Unknown configuration requested: " + aConfiguration);
michael@0 1146 break;
michael@0 1147 }
michael@0 1148 },
michael@0 1149
michael@0 1150 getAvailableTargets: function(aContentDocument, aCallbackID) {
michael@0 1151 let window = this.getChromeWindow(aContentDocument);
michael@0 1152 let data = this.availableTargetsCache.get(window);
michael@0 1153 if (data) {
michael@0 1154 this.sendPageCallback(aContentDocument, aCallbackID, data);
michael@0 1155 return;
michael@0 1156 }
michael@0 1157
michael@0 1158 let promises = [];
michael@0 1159 for (let targetName of this.targets.keys()) {
michael@0 1160 promises.push(this.getTarget(window, targetName));
michael@0 1161 }
michael@0 1162 Promise.all(promises).then((targetObjects) => {
michael@0 1163 let targetNames = [
michael@0 1164 "pinnedTab",
michael@0 1165 ];
michael@0 1166 for (let targetObject of targetObjects) {
michael@0 1167 if (targetObject.node)
michael@0 1168 targetNames.push(targetObject.targetName);
michael@0 1169 }
michael@0 1170 let data = {
michael@0 1171 targets: targetNames,
michael@0 1172 };
michael@0 1173 this.availableTargetsCache.set(window, data);
michael@0 1174 this.sendPageCallback(aContentDocument, aCallbackID, data);
michael@0 1175 }, (err) => {
michael@0 1176 Cu.reportError(err);
michael@0 1177 this.sendPageCallback(aContentDocument, aCallbackID, {
michael@0 1178 targets: [],
michael@0 1179 });
michael@0 1180 });
michael@0 1181 },
michael@0 1182
michael@0 1183 _addAnnotationPanelMutationObserver: function(aPanelEl) {
michael@0 1184 #ifdef XP_LINUX
michael@0 1185 let observer = this._annotationPanelMutationObservers.get(aPanelEl);
michael@0 1186 if (observer) {
michael@0 1187 return;
michael@0 1188 }
michael@0 1189 let win = aPanelEl.ownerDocument.defaultView;
michael@0 1190 observer = new win.MutationObserver(this._annotationMutationCallback);
michael@0 1191 this._annotationPanelMutationObservers.set(aPanelEl, observer);
michael@0 1192 let observerOptions = {
michael@0 1193 attributeFilter: ["height", "width"],
michael@0 1194 attributes: true,
michael@0 1195 };
michael@0 1196 observer.observe(aPanelEl, observerOptions);
michael@0 1197 #endif
michael@0 1198 },
michael@0 1199
michael@0 1200 _removeAnnotationPanelMutationObserver: function(aPanelEl) {
michael@0 1201 #ifdef XP_LINUX
michael@0 1202 let observer = this._annotationPanelMutationObservers.get(aPanelEl);
michael@0 1203 if (observer) {
michael@0 1204 observer.disconnect();
michael@0 1205 this._annotationPanelMutationObservers.delete(aPanelEl);
michael@0 1206 }
michael@0 1207 #endif
michael@0 1208 },
michael@0 1209
michael@0 1210 /**
michael@0 1211 * Workaround for Ubuntu panel craziness in bug 970788 where incorrect sizes get passed to
michael@0 1212 * nsXULPopupManager::PopupResized and lead to incorrect width and height attributes getting
michael@0 1213 * set on the panel.
michael@0 1214 */
michael@0 1215 _annotationMutationCallback: function(aMutations) {
michael@0 1216 for (let mutation of aMutations) {
michael@0 1217 // Remove both attributes at once and ignore remaining mutations to be proccessed.
michael@0 1218 mutation.target.removeAttribute("width");
michael@0 1219 mutation.target.removeAttribute("height");
michael@0 1220 return;
michael@0 1221 }
michael@0 1222 },
michael@0 1223 };
michael@0 1224
michael@0 1225 this.UITour.init();

mercurial