Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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(); |