Bug 1605894 reduce the proliferation of DefaultLoopbackTone to only AudioStreamFlowin...
[gecko.git] / browser / modules / PageActions.sys.mjs
bloba067bb76c3f5d37af113383a332c930ea7c09db9
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/. */
5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
7 const lazy = {};
9 ChromeUtils.defineESModuleGetters(lazy, {
10   AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
11   BinarySearch: "resource://gre/modules/BinarySearch.sys.mjs",
12   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
13   setTimeout: "resource://gre/modules/Timer.sys.mjs",
14 });
16 XPCOMUtils.defineLazyModuleGetters(lazy, {
17   ASRouter: "resource://activity-stream/lib/ASRouter.jsm",
18 });
20 const ACTION_ID_BOOKMARK = "bookmark";
21 const ACTION_ID_BUILT_IN_SEPARATOR = "builtInSeparator";
22 const ACTION_ID_TRANSIENT_SEPARATOR = "transientSeparator";
24 const PREF_PERSISTED_ACTIONS = "browser.pageActions.persistedActions";
25 const PERSISTED_ACTIONS_CURRENT_VERSION = 1;
27 // Escapes the given raw URL string, and returns an equivalent CSS url()
28 // value for it.
29 function escapeCSSURL(url) {
30   return `url("${url.replace(/[\\\s"]/g, encodeURIComponent)}")`;
33 export var PageActions = {
34   /**
35    * Initializes PageActions.
36    *
37    * @param {boolean} addShutdownBlocker
38    *   This param exists only for tests.  Normally the default value of true
39    *   must be used.
40    */
41   init(addShutdownBlocker = true) {
42     this._initBuiltInActions();
44     let callbacks = this._deferredAddActionCalls;
45     delete this._deferredAddActionCalls;
47     this._loadPersistedActions();
49     // Register the built-in actions, which are defined below in this file.
50     for (let options of gBuiltInActions) {
51       if (!this.actionForID(options.id)) {
52         this._registerAction(new Action(options));
53       }
54     }
56     // Now place them all in each window.  Instead of splitting the register and
57     // place steps, we could simply call addAction, which does both, but doing
58     // it this way means that all windows initially place their actions in the
59     // urlbar the same way -- placeAllActions -- regardless of whether they're
60     // open when this method is called or opened later.
61     for (let bpa of allBrowserPageActions()) {
62       bpa.placeAllActionsInUrlbar();
63     }
65     // These callbacks are deferred until init happens and all built-in actions
66     // are added.
67     while (callbacks && callbacks.length) {
68       callbacks.shift()();
69     }
71     if (addShutdownBlocker) {
72       // Purge removed actions from persisted state on shutdown.  The point is
73       // not to do it on Action.remove().  That way actions that are removed and
74       // re-added while the app is running will have their urlbar placement and
75       // other state remembered and restored.  This happens for upgraded and
76       // downgraded extensions, for example.
77       lazy.AsyncShutdown.profileBeforeChange.addBlocker(
78         "PageActions: purging unregistered actions from cache",
79         () => this._purgeUnregisteredPersistedActions()
80       );
81     }
82   },
84   _deferredAddActionCalls: [],
86   /**
87    * A list of all Action objects, not in any particular order.  Not live.
88    * (array of Action objects)
89    */
90   get actions() {
91     let lists = [
92       this._builtInActions,
93       this._nonBuiltInActions,
94       this._transientActions,
95     ];
96     return lists.reduce((memo, list) => memo.concat(list), []);
97   },
99   /**
100    * The list of Action objects that should appear in the panel for a given
101    * window, sorted in the order in which they appear.  If there are both
102    * built-in and non-built-in actions, then the list will include the separator
103    * between the two.  The list is not live.  (array of Action objects)
104    *
105    * @param  browserWindow (DOM window, required)
106    *         This window's actions will be returned.
107    * @return (array of PageAction.Action objects) The actions currently in the
108    *         given window's panel.
109    */
110   actionsInPanel(browserWindow) {
111     function filter(action) {
112       return action.shouldShowInPanel(browserWindow);
113     }
114     let actions = this._builtInActions.filter(filter);
115     let nonBuiltInActions = this._nonBuiltInActions.filter(filter);
116     if (nonBuiltInActions.length) {
117       if (actions.length) {
118         actions.push(
119           new Action({
120             id: ACTION_ID_BUILT_IN_SEPARATOR,
121             _isSeparator: true,
122           })
123         );
124       }
125       actions.push(...nonBuiltInActions);
126     }
127     let transientActions = this._transientActions.filter(filter);
128     if (transientActions.length) {
129       if (actions.length) {
130         actions.push(
131           new Action({
132             id: ACTION_ID_TRANSIENT_SEPARATOR,
133             _isSeparator: true,
134           })
135         );
136       }
137       actions.push(...transientActions);
138     }
139     return actions;
140   },
142   /**
143    * The list of actions currently in the urlbar, sorted in the order in which
144    * they appear.  Not live.
145    *
146    * @param  browserWindow (DOM window, required)
147    *         This window's actions will be returned.
148    * @return (array of PageAction.Action objects) The actions currently in the
149    *         given window's urlbar.
150    */
151   actionsInUrlbar(browserWindow) {
152     // Remember that IDs in idsInUrlbar may belong to actions that aren't
153     // currently registered.
154     return this._persistedActions.idsInUrlbar.reduce((actions, id) => {
155       let action = this.actionForID(id);
156       if (action && action.shouldShowInUrlbar(browserWindow)) {
157         actions.push(action);
158       }
159       return actions;
160     }, []);
161   },
163   /**
164    * Gets an action.
165    *
166    * @param  id (string, required)
167    *         The ID of the action to get.
168    * @return The Action object, or null if none.
169    */
170   actionForID(id) {
171     return this._actionsByID.get(id);
172   },
174   /**
175    * Registers an action.
176    *
177    * Actions are registered by their IDs.  An error is thrown if an action with
178    * the given ID has already been added.  Use actionForID() before calling this
179    * method if necessary.
180    *
181    * Be sure to call remove() on the action if the lifetime of the code that
182    * owns it is shorter than the browser's -- if it lives in an extension, for
183    * example.
184    *
185    * @param  action (Action, required)
186    *         The Action object to register.
187    * @return The given Action.
188    */
189   addAction(action) {
190     if (this._deferredAddActionCalls) {
191       // init() hasn't been called yet.  Defer all additions until it's called,
192       // at which time _deferredAddActionCalls will be deleted.
193       this._deferredAddActionCalls.push(() => this.addAction(action));
194       return action;
195     }
196     this._registerAction(action);
197     for (let bpa of allBrowserPageActions()) {
198       bpa.placeAction(action);
199     }
200     return action;
201   },
203   _registerAction(action) {
204     if (this.actionForID(action.id)) {
205       throw new Error(`Action with ID '${action.id}' already added`);
206     }
207     this._actionsByID.set(action.id, action);
209     // Insert the action into the appropriate list, either _builtInActions or
210     // _nonBuiltInActions.
212     // Keep in mind that _insertBeforeActionID may be present but null, which
213     // means the action should be appended to the built-ins.
214     if ("__insertBeforeActionID" in action) {
215       // A "semi-built-in" action, probably an action from an extension
216       // bundled with the browser.  Right now we simply assume that no other
217       // consumers will use _insertBeforeActionID.
218       let index = !action.__insertBeforeActionID
219         ? -1
220         : this._builtInActions.findIndex(a => {
221             return a.id == action.__insertBeforeActionID;
222           });
223       if (index < 0) {
224         // Append the action (excluding transient actions).
225         index = this._builtInActions.filter(a => !a.__transient).length;
226       }
227       this._builtInActions.splice(index, 0, action);
228     } else if (action.__transient) {
229       // A transient action.
230       this._transientActions.push(action);
231     } else if (action._isBuiltIn) {
232       // A built-in action. These are mostly added on init before all other
233       // actions, one after the other. Extension actions load later and should
234       // be at the end, so just push onto the array.
235       this._builtInActions.push(action);
236     } else {
237       // A non-built-in action, like a non-bundled extension potentially.
238       // Keep this list sorted by title.
239       let index = lazy.BinarySearch.insertionIndexOf(
240         (a1, a2) => {
241           return a1.getTitle().localeCompare(a2.getTitle());
242         },
243         this._nonBuiltInActions,
244         action
245       );
246       this._nonBuiltInActions.splice(index, 0, action);
247     }
249     let isNew = !this._persistedActions.ids.includes(action.id);
250     if (isNew) {
251       // The action is new.  Store it in the persisted actions.
252       this._persistedActions.ids.push(action.id);
253     }
255     // Actions are always pinned to the urlbar, except for panel separators.
256     action._pinnedToUrlbar = !action.__isSeparator;
257     this._updateIDsPinnedToUrlbarForAction(action);
258   },
260   _updateIDsPinnedToUrlbarForAction(action) {
261     let index = this._persistedActions.idsInUrlbar.indexOf(action.id);
262     if (action.pinnedToUrlbar) {
263       if (index < 0) {
264         index =
265           action.id == ACTION_ID_BOOKMARK
266             ? -1
267             : this._persistedActions.idsInUrlbar.indexOf(ACTION_ID_BOOKMARK);
268         if (index < 0) {
269           index = this._persistedActions.idsInUrlbar.length;
270         }
271         this._persistedActions.idsInUrlbar.splice(index, 0, action.id);
272       }
273     } else if (index >= 0) {
274       this._persistedActions.idsInUrlbar.splice(index, 1);
275     }
276     this._storePersistedActions();
277   },
279   // These keep track of currently registered actions.
280   _builtInActions: [],
281   _nonBuiltInActions: [],
282   _transientActions: [],
283   _actionsByID: new Map(),
285   /**
286    * Call this when an action is removed.
287    *
288    * @param  action (Action object, required)
289    *         The action that was removed.
290    */
291   onActionRemoved(action) {
292     if (!this.actionForID(action.id)) {
293       // The action isn't registered (yet).  Not an error.
294       return;
295     }
297     this._actionsByID.delete(action.id);
298     let lists = [
299       this._builtInActions,
300       this._nonBuiltInActions,
301       this._transientActions,
302     ];
303     for (let list of lists) {
304       let index = list.findIndex(a => a.id == action.id);
305       if (index >= 0) {
306         list.splice(index, 1);
307         break;
308       }
309     }
311     for (let bpa of allBrowserPageActions()) {
312       bpa.removeAction(action);
313     }
314   },
316   /**
317    * Call this when an action's pinnedToUrlbar property changes.
318    *
319    * @param  action (Action object, required)
320    *         The action whose pinnedToUrlbar property changed.
321    */
322   onActionToggledPinnedToUrlbar(action) {
323     if (!this.actionForID(action.id)) {
324       // This may be called before the action has been added.
325       return;
326     }
327     this._updateIDsPinnedToUrlbarForAction(action);
328     for (let bpa of allBrowserPageActions()) {
329       bpa.placeActionInUrlbar(action);
330     }
331   },
333   // For tests.  See Bug 1413692.
334   _reset() {
335     PageActions._purgeUnregisteredPersistedActions();
336     PageActions._builtInActions = [];
337     PageActions._nonBuiltInActions = [];
338     PageActions._transientActions = [];
339     PageActions._actionsByID = new Map();
340   },
342   _storePersistedActions() {
343     let json = JSON.stringify(this._persistedActions);
344     Services.prefs.setStringPref(PREF_PERSISTED_ACTIONS, json);
345   },
347   _loadPersistedActions() {
348     let actions;
349     try {
350       let json = Services.prefs.getStringPref(PREF_PERSISTED_ACTIONS);
351       actions = this._migratePersistedActions(JSON.parse(json));
352     } catch (ex) {}
354     // Handle migrating to and from Proton.  We want to gracefully handle
355     // downgrades from Proton, and since Proton is controlled by a pref, we also
356     // don't want to assume that a downgrade is possible only by downgrading the
357     // app.  That makes it hard to use the normal migration approach of creating
358     // a new persisted actions version, so we handle Proton migration specially.
359     // We try-catch it separately from the earlier _migratePersistedActions call
360     // because it should not be short-circuited when the pref load or usual
361     // migration fails.
362     try {
363       actions = this._migratePersistedActionsProton(actions);
364     } catch (ex) {}
366     // If `actions` is still not defined, then this._persistedActions will
367     // remain its default value.
368     if (actions) {
369       this._persistedActions = actions;
370     }
371   },
373   _purgeUnregisteredPersistedActions() {
374     // Remove all action IDs from persisted state that do not correspond to
375     // currently registered actions.
376     for (let name of ["ids", "idsInUrlbar"]) {
377       this._persistedActions[name] = this._persistedActions[name].filter(id => {
378         return this.actionForID(id);
379       });
380     }
381     this._storePersistedActions();
382   },
384   _migratePersistedActions(actions) {
385     // Start with actions.version and migrate one version at a time, all the way
386     // up to the current version.
387     for (
388       let version = actions.version || 0;
389       version < PERSISTED_ACTIONS_CURRENT_VERSION;
390       version++
391     ) {
392       let methodName = `_migratePersistedActionsTo${version + 1}`;
393       actions = this[methodName](actions);
394       actions.version = version + 1;
395     }
396     return actions;
397   },
399   _migratePersistedActionsTo1(actions) {
400     // The `ids` object is a mapping: action ID => true.  Convert it to an array
401     // to save space in the prefs.
402     let ids = [];
403     for (let id in actions.ids) {
404       ids.push(id);
405     }
406     // Move the bookmark ID to the end of idsInUrlbar.  The bookmark action
407     // should always remain at the end of the urlbar, if present.
408     let bookmarkIndex = actions.idsInUrlbar.indexOf(ACTION_ID_BOOKMARK);
409     if (bookmarkIndex >= 0) {
410       actions.idsInUrlbar.splice(bookmarkIndex, 1);
411       actions.idsInUrlbar.push(ACTION_ID_BOOKMARK);
412     }
413     return {
414       ids,
415       idsInUrlbar: actions.idsInUrlbar,
416     };
417   },
419   _migratePersistedActionsProton(actions) {
420     if (actions?.idsInUrlbarPreProton) {
421       // continue with Proton
422     } else if (actions) {
423       // upgrade to Proton
424       actions.idsInUrlbarPreProton = [...(actions.idsInUrlbar || [])];
425     } else {
426       // new profile with Proton
427       actions = {
428         ids: [],
429         idsInUrlbar: [],
430         idsInUrlbarPreProton: [],
431         version: PERSISTED_ACTIONS_CURRENT_VERSION,
432       };
433     }
434     return actions;
435   },
437   /**
438    * Send an ASRouter trigger to possibly show messaging related to the page
439    * action that was placed in the urlbar.
440    *
441    * @param {Element} buttonNode The page action button node.
442    */
443   sendPlacedInUrlbarTrigger(buttonNode) {
444     lazy.setTimeout(async () => {
445       await lazy.ASRouter.initialized;
446       let win = buttonNode?.ownerGlobal;
447       if (!win || buttonNode.hidden) {
448         return;
449       }
450       await lazy.ASRouter.sendTriggerMessage({
451         browser: win.gBrowser.selectedBrowser,
452         id: "pageActionInUrlbar",
453         context: { pageAction: buttonNode.id },
454       });
455     }, 500);
456   },
458   // This keeps track of all actions, even those that are not currently
459   // registered because they have been removed, so long as
460   // _purgeUnregisteredPersistedActions has not been called.
461   _persistedActions: {
462     version: PERSISTED_ACTIONS_CURRENT_VERSION,
463     // action IDs that have ever been seen and not removed, order not important
464     ids: [],
465     // action IDs ordered by position in urlbar
466     idsInUrlbar: [],
467   },
471  * A single page action.
473  * Each action can have both per-browser-window state and global state.
474  * Per-window state takes precedence over global state.  This is reflected in
475  * the title, tooltip, disabled, and icon properties.  Each of these properties
476  * has a getter method and setter method that takes a browser window.  Pass null
477  * to get the action's global state.  Pass a browser window to get the per-
478  * window state.  However, if you pass a window and the action has no state for
479  * that window, then the global state will be returned.
481  * `options` is a required object with the following properties.  Regarding the
482  * properties discussed in the previous paragraph, the values in `options` set
483  * global state.
485  * @param id (string, required)
486  *        The action's ID.  Treat this like the ID of a DOM node.
487  * @param title (string, optional)
488  *        The action's title. It is optional for built in actions.
489  * @param anchorIDOverride (string, optional)
490  *        Pass a string to override the node to which the action's activated-
491  *        action panel is anchored.
492  * @param disabled (bool, optional)
493  *        Pass true to cause the action to be disabled initially in all browser
494  *        windows.  False by default.
495  * @param extensionID (string, optional)
496  *        If the action lives in an extension, pass its ID.
497  * @param iconURL (string or object, optional)
498  *        The URL string of the action's icon.  Usually you want to specify an
499  *        icon in CSS, but this option is useful if that would be a pain for
500  *        some reason.  You can also pass an object that maps pixel sizes to
501  *        URLs, like { 16: url16, 32: url32 }.  The best size for the user's
502  *        screen will be used.
503  * @param isBadged (bool, optional)
504  *        If true, the toolbarbutton for this action will get a
505  *        "badged" attribute.
506  * @param onBeforePlacedInWindow (function, optional)
507  *        Called before the action is placed in the window:
508  *        onBeforePlacedInWindow(window)
509  *        * window: The window that the action will be placed in.
510  * @param onCommand (function, optional)
511  *        Called when the action is clicked, but only if it has neither a
512  *        subview nor an iframe:
513  *        onCommand(event, buttonNode)
514  *        * event: The triggering event.
515  *        * buttonNode: The button node that was clicked.
516  * @param onIframeHiding (function, optional)
517  *        Called when the action's iframe is hiding:
518  *        onIframeHiding(iframeNode, parentPanelNode)
519  *        * iframeNode: The iframe.
520  *        * parentPanelNode: The panel node in which the iframe is shown.
521  * @param onIframeHidden (function, optional)
522  *        Called when the action's iframe is hidden:
523  *        onIframeHidden(iframeNode, parentPanelNode)
524  *        * iframeNode: The iframe.
525  *        * parentPanelNode: The panel node in which the iframe is shown.
526  * @param onIframeShowing (function, optional)
527  *        Called when the action's iframe is showing to the user:
528  *        onIframeShowing(iframeNode, parentPanelNode)
529  *        * iframeNode: The iframe.
530  *        * parentPanelNode: The panel node in which the iframe is shown.
531  * @param onLocationChange (function, optional)
532  *        Called after tab switch or when the current <browser>'s location
533  *        changes:
534  *        onLocationChange(browserWindow)
535  *        * browserWindow: The browser window containing the tab switch or
536  *          changed <browser>.
537  * @param onPlacedInPanel (function, optional)
538  *        Called when the action is added to the page action panel in a browser
539  *        window:
540  *        onPlacedInPanel(buttonNode)
541  *        * buttonNode: The action's node in the page action panel.
542  * @param onPlacedInUrlbar (function, optional)
543  *        Called when the action is added to the urlbar in a browser window:
544  *        onPlacedInUrlbar(buttonNode)
545  *        * buttonNode: The action's node in the urlbar.
546  * @param onRemovedFromWindow (function, optional)
547  *        Called after the action is removed from a browser window:
548  *        onRemovedFromWindow(browserWindow)
549  *        * browserWindow: The browser window that the action was removed from.
550  * @param onShowingInPanel (function, optional)
551  *        Called when a browser window's page action panel is showing:
552  *        onShowingInPanel(buttonNode)
553  *        * buttonNode: The action's node in the page action panel.
554  * @param onSubviewPlaced (function, optional)
555  *        Called when the action's subview is added to its parent panel in a
556  *        browser window:
557  *        onSubviewPlaced(panelViewNode)
558  *        * panelViewNode: The subview's panelview node.
559  * @param onSubviewShowing (function, optional)
560  *        Called when the action's subview is showing in a browser window:
561  *        onSubviewShowing(panelViewNode)
562  *        * panelViewNode: The subview's panelview node.
563  * @param pinnedToUrlbar (bool, optional)
564  *        Pass true to pin the action to the urlbar.  An action is shown in the
565  *        urlbar if it's pinned and not disabled.  False by default.
566  * @param tooltip (string, optional)
567  *        The action's button tooltip text.
568  * @param urlbarIDOverride (string, optional)
569  *        Usually the ID of the action's button in the urlbar will be generated
570  *        automatically.  Pass a string for this property to override that with
571  *        your own ID.
572  * @param wantsIframe (bool, optional)
573  *        Pass true to make an action that shows an iframe in a panel when
574  *        clicked.
575  * @param wantsSubview (bool, optional)
576  *        Pass true to make an action that shows a panel subview when clicked.
577  * @param disablePrivateBrowsing (bool, optional)
578  *        Pass true to prevent the action from showing in a private browsing window.
579  */
580 function Action(options) {
581   setProperties(this, options, {
582     id: true,
583     title: false,
584     anchorIDOverride: false,
585     disabled: false,
586     extensionID: false,
587     iconURL: false,
588     isBadged: false,
589     labelForHistogram: false,
590     onBeforePlacedInWindow: false,
591     onCommand: false,
592     onIframeHiding: false,
593     onIframeHidden: false,
594     onIframeShowing: false,
595     onLocationChange: false,
596     onPlacedInPanel: false,
597     onPlacedInUrlbar: false,
598     onRemovedFromWindow: false,
599     onShowingInPanel: false,
600     onSubviewPlaced: false,
601     onSubviewShowing: false,
602     onPinToUrlbarToggled: false,
603     pinnedToUrlbar: false,
604     tooltip: false,
605     urlbarIDOverride: false,
606     wantsIframe: false,
607     wantsSubview: false,
608     disablePrivateBrowsing: false,
610     // private
612     // (string, optional)
613     // The ID of another action before which to insert this new action in the
614     // panel.
615     _insertBeforeActionID: false,
617     // (bool, optional)
618     // True if this isn't really an action but a separator to be shown in the
619     // page action panel.
620     _isSeparator: false,
622     // (bool, optional)
623     // Transient actions have a couple of special properties: (1) They stick to
624     // the bottom of the panel, and (2) they're hidden in the panel when they're
625     // disabled.  Other than that they behave like other actions.
626     _transient: false,
628     // (bool, optional)
629     // True if the action's urlbar button is defined in markup.  In that case, a
630     // node with the action's urlbar node ID should already exist in the DOM
631     // (either the auto-generated ID or urlbarIDOverride).  That node will be
632     // shown when the action is added to the urlbar and hidden when the action
633     // is removed from the urlbar.
634     _urlbarNodeInMarkup: false,
635   });
637   /**
638    * A cache of the pre-computed CSS variable values for a given icon
639    * URLs object, as passed to _createIconProperties.
640    */
641   this._iconProperties = new WeakMap();
643   /**
644    * The global values for the action properties.
645    */
646   this._globalProps = {
647     disabled: this._disabled,
648     iconURL: this._iconURL,
649     iconProps: this._createIconProperties(this._iconURL),
650     title: this._title,
651     tooltip: this._tooltip,
652     wantsSubview: this._wantsSubview,
653   };
655   /**
656    * A mapping of window-specific action property objects, each of which
657    * derives from the _globalProps object.
658    */
659   this._windowProps = new WeakMap();
662 Action.prototype = {
663   /**
664    * The ID of the action's parent extension (string)
665    */
666   get extensionID() {
667     return this._extensionID;
668   },
670   /**
671    * The action's ID (string)
672    */
673   get id() {
674     return this._id;
675   },
677   get disablePrivateBrowsing() {
678     return !!this._disablePrivateBrowsing;
679   },
681   /**
682    * Verifies that the action can be shown in a private window.  For
683    * extensions, verifies the extension has access to the window.
684    */
685   canShowInWindow(browserWindow) {
686     if (this._extensionID) {
687       let policy = WebExtensionPolicy.getByID(this._extensionID);
688       if (!policy.canAccessWindow(browserWindow)) {
689         return false;
690       }
691     }
692     return !(
693       this.disablePrivateBrowsing &&
694       lazy.PrivateBrowsingUtils.isWindowPrivate(browserWindow)
695     );
696   },
698   /**
699    * True if the action is pinned to the urlbar.  The action is shown in the
700    * urlbar if it's pinned and not disabled.  (bool)
701    */
702   get pinnedToUrlbar() {
703     return this._pinnedToUrlbar || false;
704   },
705   set pinnedToUrlbar(shown) {
706     if (this.pinnedToUrlbar != shown) {
707       this._pinnedToUrlbar = shown;
708       PageActions.onActionToggledPinnedToUrlbar(this);
709       this.onPinToUrlbarToggled();
710     }
711   },
713   /**
714    * The action's disabled state (bool)
715    */
716   getDisabled(browserWindow = null) {
717     return !!this._getProperties(browserWindow).disabled;
718   },
719   setDisabled(value, browserWindow = null) {
720     return this._setProperty("disabled", !!value, browserWindow);
721   },
723   /**
724    * The action's icon URL string, or an object mapping sizes to URL strings
725    * (string or object)
726    */
727   getIconURL(browserWindow = null) {
728     return this._getProperties(browserWindow).iconURL;
729   },
730   setIconURL(value, browserWindow = null) {
731     let props = this._getProperties(browserWindow, !!browserWindow);
732     props.iconURL = value;
733     props.iconProps = this._createIconProperties(value);
735     this._updateProperty("iconURL", props.iconProps, browserWindow);
736     return value;
737   },
739   /**
740    * The set of CSS variables which define the action's icons in various
741    * sizes. This is generated automatically from the iconURL property.
742    */
743   getIconProperties(browserWindow = null) {
744     return this._getProperties(browserWindow).iconProps;
745   },
747   _createIconProperties(urls) {
748     if (urls && typeof urls == "object") {
749       let props = this._iconProperties.get(urls);
750       if (!props) {
751         props = Object.freeze({
752           "--pageAction-image": `image-set(
753             ${escapeCSSURL(this._iconURLForSize(urls, 16))},
754             ${escapeCSSURL(this._iconURLForSize(urls, 32))} 2x
755           )`,
756         });
757         this._iconProperties.set(urls, props);
758       }
759       return props;
760     }
762     let cssURL = urls ? escapeCSSURL(urls) : null;
763     return Object.freeze({
764       "--pageAction-image": cssURL,
765     });
766   },
768   /**
769    * The action's title (string). Note, built in actions will
770    * not have a title property.
771    */
772   getTitle(browserWindow = null) {
773     return this._getProperties(browserWindow).title;
774   },
775   setTitle(value, browserWindow = null) {
776     return this._setProperty("title", value, browserWindow);
777   },
779   /**
780    * The action's tooltip (string)
781    */
782   getTooltip(browserWindow = null) {
783     return this._getProperties(browserWindow).tooltip;
784   },
785   setTooltip(value, browserWindow = null) {
786     return this._setProperty("tooltip", value, browserWindow);
787   },
789   /**
790    * Whether the action wants a subview (bool)
791    */
792   getWantsSubview(browserWindow = null) {
793     return !!this._getProperties(browserWindow).wantsSubview;
794   },
795   setWantsSubview(value, browserWindow = null) {
796     return this._setProperty("wantsSubview", !!value, browserWindow);
797   },
799   /**
800    * Sets a property, optionally for a particular browser window.
801    *
802    * @param  name (string, required)
803    *         The (non-underscored) name of the property.
804    * @param  value
805    *         The value.
806    * @param  browserWindow (DOM window, optional)
807    *         If given, then the property will be set in this window's state, not
808    *         globally.
809    */
810   _setProperty(name, value, browserWindow) {
811     let props = this._getProperties(browserWindow, !!browserWindow);
812     props[name] = value;
814     this._updateProperty(name, value, browserWindow);
815     return value;
816   },
818   _updateProperty(name, value, browserWindow) {
819     // This may be called before the action has been added.
820     if (PageActions.actionForID(this.id)) {
821       for (let bpa of allBrowserPageActions(browserWindow)) {
822         bpa.updateAction(this, name, { value });
823       }
824     }
825   },
827   /**
828    * Returns the properties object for the given window, if it exists,
829    * or the global properties object if no window-specific properties
830    * exist.
831    *
832    * @param {Window?} window
833    *        The window for which to return the properties object, or
834    *        null to return the global properties object.
835    * @param {bool} [forceWindowSpecific = false]
836    *        If true, always returns a window-specific properties object.
837    *        If a properties object does not exist for the given window,
838    *        one is created and cached.
839    * @returns {object}
840    */
841   _getProperties(window, forceWindowSpecific = false) {
842     let props = window && this._windowProps.get(window);
844     if (!props && forceWindowSpecific) {
845       props = Object.create(this._globalProps);
846       this._windowProps.set(window, props);
847     }
849     return props || this._globalProps;
850   },
852   /**
853    * Override for the ID of the action's activated-action panel anchor (string)
854    */
855   get anchorIDOverride() {
856     return this._anchorIDOverride;
857   },
859   /**
860    * Override for the ID of the action's urlbar node (string)
861    */
862   get urlbarIDOverride() {
863     return this._urlbarIDOverride;
864   },
866   /**
867    * True if the action is shown in an iframe (bool)
868    */
869   get wantsIframe() {
870     return this._wantsIframe || false;
871   },
873   get isBadged() {
874     return this._isBadged || false;
875   },
877   get labelForHistogram() {
878     // The histogram label value has a length limit of 20 and restricted to a
879     // pattern. See MAX_LABEL_LENGTH and CPP_IDENTIFIER_PATTERN in
880     // toolkit/components/telemetry/parse_histograms.py
881     return (
882       this._labelForHistogram ||
883       this._id.replace(/_\w{1}/g, match => match[1].toUpperCase()).substr(0, 20)
884     );
885   },
887   /**
888    * Selects the best matching icon from the given URLs object for the
889    * given preferred size.
890    *
891    * @param {object} urls
892    *        An object containing square icons of various sizes. The name
893    *        of each property is its width, and the value is its image URL.
894    * @param {integer} peferredSize
895    *        The preferred icon width. The most appropriate icon in the
896    *        urls object will be chosen to match that size. An exact
897    *        match will be preferred, followed by an icon exactly double
898    *        the size, followed by the smallest icon larger than the
899    *        preferred size, followed by the largest available icon.
900    * @returns {string}
901    *        The chosen icon URL.
902    */
903   _iconURLForSize(urls, preferredSize) {
904     // This case is copied from ExtensionParent.jsm so that our image logic is
905     // the same, so that WebExtensions page action tests that deal with icons
906     // pass.
907     let bestSize = null;
908     if (urls[preferredSize]) {
909       bestSize = preferredSize;
910     } else if (urls[2 * preferredSize]) {
911       bestSize = 2 * preferredSize;
912     } else {
913       let sizes = Object.keys(urls)
914         .map(key => parseInt(key, 10))
915         .sort((a, b) => a - b);
916       bestSize =
917         sizes.find(candidate => candidate > preferredSize) || sizes.pop();
918     }
919     return urls[bestSize];
920   },
922   /**
923    * Performs the command for an action.  If the action has an onCommand
924    * handler, then it's called.  If the action has a subview or iframe, then a
925    * panel is opened, displaying the subview or iframe.
926    *
927    * @param  browserWindow (DOM window, required)
928    *         The browser window in which to perform the action.
929    */
930   doCommand(browserWindow) {
931     browserPageActions(browserWindow).doCommandForAction(this);
932   },
934   /**
935    * Call this when before placing the action in the window.
936    *
937    * @param  browserWindow (DOM window, required)
938    *         The browser window the action will be placed in.
939    */
940   onBeforePlacedInWindow(browserWindow) {
941     if (this._onBeforePlacedInWindow) {
942       this._onBeforePlacedInWindow(browserWindow);
943     }
944   },
946   /**
947    * Call this when the user activates the action.
948    *
949    * @param  event (DOM event, required)
950    *         The triggering event.
951    * @param  buttonNode (DOM node, required)
952    *         The action's panel or urlbar button node that was clicked.
953    */
954   onCommand(event, buttonNode) {
955     if (this._onCommand) {
956       this._onCommand(event, buttonNode);
957     }
958   },
960   /**
961    * Call this when the action's iframe is hiding.
962    *
963    * @param  iframeNode (DOM node, required)
964    *         The iframe that's hiding.
965    * @param  parentPanelNode (DOM node, required)
966    *         The panel in which the iframe is hiding.
967    */
968   onIframeHiding(iframeNode, parentPanelNode) {
969     if (this._onIframeHiding) {
970       this._onIframeHiding(iframeNode, parentPanelNode);
971     }
972   },
974   /**
975    * Call this when the action's iframe is hidden.
976    *
977    * @param  iframeNode (DOM node, required)
978    *         The iframe that's being hidden.
979    * @param  parentPanelNode (DOM node, required)
980    *         The panel in which the iframe is hidden.
981    */
982   onIframeHidden(iframeNode, parentPanelNode) {
983     if (this._onIframeHidden) {
984       this._onIframeHidden(iframeNode, parentPanelNode);
985     }
986   },
988   /**
989    * Call this when the action's iframe is showing.
990    *
991    * @param  iframeNode (DOM node, required)
992    *         The iframe that's being shown.
993    * @param  parentPanelNode (DOM node, required)
994    *         The panel in which the iframe is shown.
995    */
996   onIframeShowing(iframeNode, parentPanelNode) {
997     if (this._onIframeShowing) {
998       this._onIframeShowing(iframeNode, parentPanelNode);
999     }
1000   },
1002   /**
1003    * Call this on tab switch or when the current <browser>'s location changes.
1004    *
1005    * @param  browserWindow (DOM window, required)
1006    *         The browser window containing the tab switch or changed <browser>.
1007    */
1008   onLocationChange(browserWindow) {
1009     if (this._onLocationChange) {
1010       this._onLocationChange(browserWindow);
1011     }
1012   },
1014   /**
1015    * Call this when a DOM node for the action is added to the page action panel.
1016    *
1017    * @param  buttonNode (DOM node, required)
1018    *         The action's panel button node.
1019    */
1020   onPlacedInPanel(buttonNode) {
1021     if (this._onPlacedInPanel) {
1022       this._onPlacedInPanel(buttonNode);
1023     }
1024   },
1026   /**
1027    * Call this when a DOM node for the action is added to the urlbar.
1028    *
1029    * @param  buttonNode (DOM node, required)
1030    *         The action's urlbar button node.
1031    */
1032   onPlacedInUrlbar(buttonNode) {
1033     if (this._onPlacedInUrlbar) {
1034       this._onPlacedInUrlbar(buttonNode);
1035     }
1036   },
1038   /**
1039    * Call this when the DOM nodes for the action are removed from a browser
1040    * window.
1041    *
1042    * @param  browserWindow (DOM window, required)
1043    *         The browser window the action was removed from.
1044    */
1045   onRemovedFromWindow(browserWindow) {
1046     if (this._onRemovedFromWindow) {
1047       this._onRemovedFromWindow(browserWindow);
1048     }
1049   },
1051   /**
1052    * Call this when the action's button is shown in the page action panel.
1053    *
1054    * @param  buttonNode (DOM node, required)
1055    *         The action's panel button node.
1056    */
1057   onShowingInPanel(buttonNode) {
1058     if (this._onShowingInPanel) {
1059       this._onShowingInPanel(buttonNode);
1060     }
1061   },
1063   /**
1064    * Call this when a panelview node for the action's subview is added to the
1065    * DOM.
1066    *
1067    * @param  panelViewNode (DOM node, required)
1068    *         The subview's panelview node.
1069    */
1070   onSubviewPlaced(panelViewNode) {
1071     if (this._onSubviewPlaced) {
1072       this._onSubviewPlaced(panelViewNode);
1073     }
1074   },
1076   /**
1077    * Call this when a panelview node for the action's subview is showing.
1078    *
1079    * @param  panelViewNode (DOM node, required)
1080    *         The subview's panelview node.
1081    */
1082   onSubviewShowing(panelViewNode) {
1083     if (this._onSubviewShowing) {
1084       this._onSubviewShowing(panelViewNode);
1085     }
1086   },
1087   /**
1088    * Call this when an icon in the url is pinned or unpinned.
1089    */
1090   onPinToUrlbarToggled() {
1091     if (this._onPinToUrlbarToggled) {
1092       this._onPinToUrlbarToggled();
1093     }
1094   },
1096   /**
1097    * Removes the action's DOM nodes from all browser windows.
1098    *
1099    * PageActions will remember the action's urlbar placement, if any, after this
1100    * method is called until app shutdown.  If the action is not added again
1101    * before shutdown, then PageActions will discard the placement, and the next
1102    * time the action is added, its placement will be reset.
1103    */
1104   remove() {
1105     PageActions.onActionRemoved(this);
1106   },
1108   /**
1109    * Returns whether the action should be shown in a given window's panel.
1110    *
1111    * @param  browserWindow (DOM window, required)
1112    *         The window.
1113    * @return True if the action should be shown and false otherwise.  Actions
1114    *         are always shown in the panel unless they're both transient and
1115    *         disabled.
1116    */
1117   shouldShowInPanel(browserWindow) {
1118     // When Proton is enabled, the extension page actions should behave similarly
1119     // to a transient action, and be hidden from the urlbar overflow menu if they
1120     // are disabled (as in the urlbar when the overflow menu isn't available)
1121     //
1122     // TODO(Bug 1704139): as a follow up we may look into just set on all
1123     // extensions pageActions `_transient: true`, at least once we sunset
1124     // the proton preference and we don't need the pre-Proton behavior anymore,
1125     // and remove this special case.
1126     const isProtonExtensionAction = this.extensionID;
1128     return (
1129       (!(this.__transient || isProtonExtensionAction) ||
1130         !this.getDisabled(browserWindow)) &&
1131       this.canShowInWindow(browserWindow)
1132     );
1133   },
1135   /**
1136    * Returns whether the action should be shown in a given window's urlbar.
1137    *
1138    * @param  browserWindow (DOM window, required)
1139    *         The window.
1140    * @return True if the action should be shown and false otherwise.  The action
1141    *         should be shown if it's both pinned and not disabled.
1142    */
1143   shouldShowInUrlbar(browserWindow) {
1144     return (
1145       this.pinnedToUrlbar &&
1146       !this.getDisabled(browserWindow) &&
1147       this.canShowInWindow(browserWindow)
1148     );
1149   },
1151   get _isBuiltIn() {
1152     let builtInIDs = ["screenshots_mozilla_org"].concat(
1153       gBuiltInActions.filter(a => !a.__isSeparator).map(a => a.id)
1154     );
1155     return builtInIDs.includes(this.id);
1156   },
1158   get _isMozillaAction() {
1159     return this._isBuiltIn || this.id == "webcompat-reporter_mozilla_org";
1160   },
1163 PageActions.Action = Action;
1165 PageActions.ACTION_ID_BUILT_IN_SEPARATOR = ACTION_ID_BUILT_IN_SEPARATOR;
1166 PageActions.ACTION_ID_TRANSIENT_SEPARATOR = ACTION_ID_TRANSIENT_SEPARATOR;
1168 // These are only necessary so that the test can use them.
1169 PageActions.ACTION_ID_BOOKMARK = ACTION_ID_BOOKMARK;
1170 PageActions.PREF_PERSISTED_ACTIONS = PREF_PERSISTED_ACTIONS;
1172 // Sorted in the order in which they should appear in the page action panel.
1173 // Does not include the page actions of extensions bundled with the browser.
1174 // They're added by the relevant extension code.
1175 // NOTE: If you add items to this list (or system add-on actions that we
1176 // want to keep track of), make sure to also update Histograms.json for the
1177 // new actions.
1178 var gBuiltInActions;
1180 PageActions._initBuiltInActions = function () {
1181   gBuiltInActions = [
1182     // bookmark
1183     {
1184       id: ACTION_ID_BOOKMARK,
1185       urlbarIDOverride: "star-button-box",
1186       _urlbarNodeInMarkup: true,
1187       pinnedToUrlbar: true,
1188       onShowingInPanel(buttonNode) {
1189         browserPageActions(buttonNode).bookmark.onShowingInPanel(buttonNode);
1190       },
1191       onCommand(event, buttonNode) {
1192         browserPageActions(buttonNode).bookmark.onCommand(event, buttonNode);
1193       },
1194     },
1195   ];
1199  * Gets a BrowserPageActions object in a browser window.
1201  * @param  obj
1202  *         Either a DOM node or a browser window.
1203  * @return The BrowserPageActions object in the browser window related to the
1204  *         given object.
1205  */
1206 function browserPageActions(obj) {
1207   if (obj.BrowserPageActions) {
1208     return obj.BrowserPageActions;
1209   }
1210   return obj.ownerGlobal.BrowserPageActions;
1214  * A generator function for all open browser windows.
1216  * @param browserWindow (DOM window, optional)
1217  *        If given, then only this window will be yielded.  That may sound
1218  *        pointless, but it can make callers nicer to write since they don't
1219  *        need two separate cases, one where a window is given and another where
1220  *        it isn't.
1221  */
1222 function* allBrowserWindows(browserWindow = null) {
1223   if (browserWindow) {
1224     yield browserWindow;
1225     return;
1226   }
1227   yield* Services.wm.getEnumerator("navigator:browser");
1231  * A generator function for BrowserPageActions objects in all open windows.
1233  * @param browserWindow (DOM window, optional)
1234  *        If given, then the BrowserPageActions for only this window will be
1235  *        yielded.
1236  */
1237 function* allBrowserPageActions(browserWindow = null) {
1238   for (let win of allBrowserWindows(browserWindow)) {
1239     yield browserPageActions(win);
1240   }
1244  * A simple function that sets properties on a given object while doing basic
1245  * required-properties checking.  If a required property isn't specified in the
1246  * given options object, or if the options object has properties that aren't in
1247  * the given schema, then an error is thrown.
1249  * @param  obj
1250  *         The object to set properties on.
1251  * @param  options
1252  *         An options object supplied by the consumer.
1253  * @param  schema
1254  *         An object a property for each required and optional property.  The
1255  *         keys are property names; the value of a key is a bool that is true if
1256  *         the property is required.
1257  */
1258 function setProperties(obj, options, schema) {
1259   for (let name in schema) {
1260     let required = schema[name];
1261     if (required && !(name in options)) {
1262       throw new Error(`'${name}' must be specified`);
1263     }
1264     let nameInObj = "_" + name;
1265     if (name[0] == "_") {
1266       // The property is "private".  If it's defined in the options, then define
1267       // it on obj exactly as it's defined on options.
1268       if (name in options) {
1269         obj[nameInObj] = options[name];
1270       }
1271     } else {
1272       // The property is "public".  Make sure the property is defined on obj.
1273       obj[nameInObj] = options[name] || null;
1274     }
1275   }
1276   for (let name in options) {
1277     if (!(name in schema)) {
1278       throw new Error(`Unrecognized option '${name}'`);
1279     }
1280   }