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