Bug 1413574 - Hide disabled page actions and offer a "Manage Extension" command for actions in extensions. r?Gijs draft
authorDrew Willcoxon <adw@mozilla.com>
Fri, 10 Nov 2017 11:06:02 -0500
changeset 696393 150254fededaa3cc4a3560cf884998a4e0f07123
parent 695283 b9d434643cd7052612cecda9e83aa0139c3c29ad
child 739847 944a632e43e3fdd28b20cd7d868b6d5ea4f2d34e
push id88701
push userdwillcoxon@mozilla.com
push dateFri, 10 Nov 2017 16:06:29 +0000
reviewersGijs
bugs1413574
milestone58.0a1
Bug 1413574 - Hide disabled page actions and offer a "Manage Extension" command for actions in extensions. r?Gijs MozReview-Commit-ID: HJpu9Jfi2bB
browser/base/content/browser-pageActions.js
browser/base/content/browser.css
browser/base/content/browser.xul
browser/base/content/test/urlbar/browser_page_action_menu.js
browser/components/extensions/ext-pageAction.js
browser/components/extensions/test/xpcshell/test_ext_pageAction_shutdown.js
browser/components/uitour/test/browser_UITour_availableTargets.js
browser/extensions/pocket/bootstrap.js
browser/locales/en-US/chrome/browser/browser.dtd
browser/modules/PageActions.jsm
browser/modules/test/browser/browser_PageActions.js
toolkit/components/telemetry/Histograms.json
--- a/browser/base/content/browser-pageActions.js
+++ b/browser/base/content/browser-pageActions.js
@@ -43,34 +43,16 @@ var BrowserPageActions = {
     return this.mainViewBodyNode = this.mainViewNode.querySelector(".panel-subview-body");
   },
 
   /**
    * Inits.  Call to init.
    */
   init() {
     this.placeAllActions();
-
-    // Add a click listener to #page-action-buttons for blocking clicks on
-    // disabled actions in the urlbar.  Normally we'd do this by setting
-    // `pointer-events: none` in the CSS, but that also blocks context menu
-    // events, and we want the context menu even on disabled actions so that
-    // they can be removed from the urlbar.
-    this.mainButtonNode.parentNode.addEventListener("click", event => {
-      if (event.button == 2) {
-        // Let context-clicks be handled normally.
-        return;
-      }
-      let node = event.originalTarget;
-      let action = this.actionForNode(node);
-      if (action && action.getDisabled(window)) {
-        event.preventDefault();
-        event.stopPropagation();
-      }
-    }, true);
   },
 
   /**
    * Places all registered actions.
    */
   placeAllActions() {
     // Place actions in the panel.  Notify of onBeforePlacedInWindow too.
     for (let action of PageActions.actions) {
@@ -81,17 +63,17 @@ var BrowserPageActions = {
     // Place actions in the urlbar.  Do this in reverse order.  The reason is
     // subtle.  If there were no urlbar nodes already in markup (like the
     // bookmark star button), then doing this in forward order would be fine.
     // Forward order means that the insert-before relationship is always broken:
     // there's never a next-sibling node before which to insert a new node, so
     // node.insertBefore() is always passed null, and nodes are always appended.
     // That will break the position of nodes that should be inserted before
     // nodes that are in markup, which in turn can break other nodes.
-    let actionsInUrlbar = PageActions.actionsInUrlbar;
+    let actionsInUrlbar = PageActions.actionsInUrlbar(window);
     for (let i = actionsInUrlbar.length - 1; i >= 0; i--) {
       let action = actionsInUrlbar[i];
       this.placeActionInUrlbar(action);
     }
   },
 
   /**
    * Adds or removes as necessary DOM nodes for the given action.
@@ -107,31 +89,29 @@ var BrowserPageActions = {
 
   /**
    * Adds or removes as necessary DOM nodes for the action in the panel.
    *
    * @param  action (PageActions.Action, required)
    *         The action to place.
    */
   placeActionInPanel(action) {
-    let insertBeforeID = PageActions.nextActionIDInPanel(action);
     let id = this.panelButtonNodeIDForActionID(action.id);
     let node = document.getElementById(id);
     if (!node) {
       let panelViewNode;
       [node, panelViewNode] = this._makePanelButtonNodeForAction(action);
       node.id = id;
-      let insertBeforeNode = null;
-      if (insertBeforeID) {
-        let insertBeforeNodeID =
-          this.panelButtonNodeIDForActionID(insertBeforeID);
-        insertBeforeNode = document.getElementById(insertBeforeNodeID);
-      }
+      let insertBeforeID = PageActions.nextActionIDInPanel(action);
+      let insertBeforeNode =
+        insertBeforeID ? this.panelButtonNodeForActionID(insertBeforeID) :
+        null;
       this.mainViewBodyNode.insertBefore(node, insertBeforeNode);
       this.updateAction(action);
+      this._updateActionDisabledInPanel(action);
       action.onPlacedInPanel(node);
       if (panelViewNode) {
         action.subview.onPlaced(panelViewNode);
       }
     }
   },
 
   _makePanelButtonNodeForAction(action) {
@@ -351,21 +331,20 @@ var BrowserPageActions = {
 
   /**
    * Adds or removes as necessary a DOM node for the given action in the urlbar.
    *
    * @param  action (PageActions.Action, required)
    *         The action to place.
    */
   placeActionInUrlbar(action) {
-    let insertBeforeID = PageActions.nextActionIDInUrlbar(action);
     let id = this.urlbarButtonNodeIDForActionID(action.id);
     let node = document.getElementById(id);
 
-    if (!action.shownInUrlbar) {
+    if (!action.shouldShowInUrlbar(window)) {
       if (node) {
         if (action.__urlbarNodeInMarkup) {
           node.hidden = true;
         } else {
           node.remove();
         }
       }
       return;
@@ -377,49 +356,42 @@ var BrowserPageActions = {
       node.hidden = false;
     } else if (!node) {
       newlyPlaced = true;
       node = this._makeUrlbarButtonNode(action);
       node.id = id;
     }
 
     if (newlyPlaced) {
-      let parentNode = this.mainButtonNode.parentNode;
-      let insertBeforeNode = null;
-      if (insertBeforeID) {
-        let insertBeforeNodeID =
-          this.urlbarButtonNodeIDForActionID(insertBeforeID);
-        insertBeforeNode = document.getElementById(insertBeforeNodeID);
-      }
-      parentNode.insertBefore(node, insertBeforeNode);
+      let insertBeforeID = PageActions.nextActionIDInUrlbar(window, action);
+      let insertBeforeNode =
+        insertBeforeID ? this.urlbarButtonNodeForActionID(insertBeforeID) :
+        null;
+      this.mainButtonNode.parentNode.insertBefore(node, insertBeforeNode);
       this.updateAction(action);
       action.onPlacedInUrlbar(node);
 
       // urlbar buttons should always have tooltips, so if the node doesn't have
       // one, then as a last resort use the label of the corresponding panel
       // button.  Why not set tooltiptext to action.title when the node is
       // created?  Because the consumer may set a title dynamically.
       if (!node.hasAttribute("tooltiptext")) {
-        let panelNodeID = this.panelButtonNodeIDForActionID(action.id);
-        let panelNode = document.getElementById(panelNodeID);
+        let panelNode = this.panelButtonNodeForActionID(action.id);
         if (panelNode) {
           node.setAttribute("tooltiptext", panelNode.getAttribute("label"));
         }
       }
     }
   },
 
   _makeUrlbarButtonNode(action) {
     let buttonNode = document.createElement("image");
     buttonNode.classList.add("urlbar-icon", "urlbar-page-action");
     buttonNode.setAttribute("actionid", action.id);
     buttonNode.setAttribute("role", "button");
-    buttonNode.addEventListener("contextmenu", event => {
-      BrowserPageActions.onContextMenu(event);
-    });
     if (action.nodeAttributes) {
       for (let name in action.nodeAttributes) {
         buttonNode.setAttribute(name, action.nodeAttributes[name]);
       }
     }
     buttonNode.addEventListener("click", event => {
       this.doCommandForAction(action, event, buttonNode);
     });
@@ -434,18 +406,17 @@ var BrowserPageActions = {
    */
   removeAction(action) {
     this._removeActionFromPanel(action);
     this._removeActionFromUrlbar(action);
     action.onRemovedFromWindow(window);
   },
 
   _removeActionFromPanel(action) {
-    let id = this.panelButtonNodeIDForActionID(action.id);
-    let node = document.getElementById(id);
+    let node = this.panelButtonNodeForActionID(action.id);
     if (node) {
       node.remove();
     }
     if (action.subview) {
       let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false);
       let panelViewNode = document.getElementById(panelViewNodeID);
       if (panelViewNode) {
         panelViewNode.remove();
@@ -461,127 +432,120 @@ var BrowserPageActions = {
       );
       if (separator) {
         separator.remove();
       }
     }
   },
 
   _removeActionFromUrlbar(action) {
-    let id = this.urlbarButtonNodeIDForActionID(action.id);
-    let node = document.getElementById(id);
+    let node = this.urlbarButtonNodeForActionID(action.id);
     if (node) {
       node.remove();
     }
   },
 
   /**
    * Updates the DOM nodes of an action to reflect either a changed property or
    * all properties.
    *
    * @param  action (PageActions.Action, required)
    *         The action to update.
-   * @param  nameToUpdate (string, optional)
-   *         The property's name.  If not given, then DOM nodes will be updated
-   *         to reflect the current values of all properties.
+   * @param  propertyName (string, optional)
+   *         The name of the property to update.  If not given, then DOM nodes
+   *         will be updated to reflect the current values of all properties.
    */
-  updateAction(action, nameToUpdate = null) {
-    let names = nameToUpdate ? [nameToUpdate] : [
-      "disabled",
+  updateAction(action, propertyName = null) {
+    let propertyNames = propertyName ? [propertyName] : [
       "iconURL",
       "title",
       "tooltip",
     ];
-    for (let name of names) {
+    for (let name of propertyNames) {
       let upper = name[0].toUpperCase() + name.substr(1);
       this[`_updateAction${upper}`](action);
     }
   },
 
   _updateActionDisabled(action) {
-    let nodeIDs = [
-      this.panelButtonNodeIDForActionID(action.id),
-      this.urlbarButtonNodeIDForActionID(action.id),
-    ];
-    for (let nodeID of nodeIDs) {
-      let node = document.getElementById(nodeID);
-      if (node) {
-        if (action.getDisabled(window)) {
-          node.setAttribute("disabled", "true");
-        } else {
-          node.removeAttribute("disabled");
-        }
+    this._updateActionDisabledInPanel(action);
+    this.placeActionInUrlbar(action);
+  },
+
+  _updateActionDisabledInPanel(action) {
+    let panelButton = this.panelButtonNodeForActionID(action.id);
+    if (panelButton) {
+      if (action.getDisabled(window)) {
+        panelButton.setAttribute("disabled", "true");
+      } else {
+        panelButton.removeAttribute("disabled");
       }
     }
   },
 
   _updateActionIconURL(action) {
-    let nodeIDs = [
-      this.panelButtonNodeIDForActionID(action.id),
-      this.urlbarButtonNodeIDForActionID(action.id),
-    ];
-    for (let nodeID of nodeIDs) {
-      let node = document.getElementById(nodeID);
-      if (node) {
-        for (let size of [16, 32]) {
-          let url = action.iconURLForSize(size, window);
-          let prop = `--pageAction-image-${size}px`;
-          if (url) {
-            node.style.setProperty(prop, `url("${url}")`);
-          } else {
-            node.style.removeProperty(prop);
-          }
+    let nodes = [
+      this.panelButtonNodeForActionID(action.id),
+      this.urlbarButtonNodeForActionID(action.id),
+    ].filter(n => !!n);
+    for (let node of nodes) {
+      for (let size of [16, 32]) {
+        let url = action.iconURLForSize(size, window);
+        let prop = `--pageAction-image-${size}px`;
+        if (url) {
+          node.style.setProperty(prop, `url("${url}")`);
+        } else {
+          node.style.removeProperty(prop);
         }
       }
     }
   },
 
   _updateActionTitle(action) {
     let title = action.getTitle(window);
     if (!title) {
       // `title` is a required action property, but the bookmark action's is an
       // empty string since its actual title is set via
       // BookmarkingUI.updateBookmarkPageMenuItem().  The purpose of this early
       // return is to ignore that empty title.
       return;
     }
-    let attrNamesByNodeIDFnName = {
-      panelButtonNodeIDForActionID: "label",
-      urlbarButtonNodeIDForActionID: "aria-label",
+    let attrNamesByNodeFnName = {
+      panelButtonNodeForActionID: "label",
+      urlbarButtonNodeForActionID: "aria-label",
     };
-    for (let [fnName, attrName] of Object.entries(attrNamesByNodeIDFnName)) {
-      let nodeID = this[fnName](action.id);
-      let node = document.getElementById(nodeID);
+    for (let [fnName, attrName] of Object.entries(attrNamesByNodeFnName)) {
+      let node = this[fnName](action.id);
       if (node) {
         node.setAttribute(attrName, title);
       }
     }
     // tooltiptext falls back to the title, so update it, too.
     this._updateActionTooltip(action);
   },
 
   _updateActionTooltip(action) {
-    let node = document.getElementById(
-      this.urlbarButtonNodeIDForActionID(action.id)
-    );
+    let node = this.urlbarButtonNodeForActionID(action.id);
     if (node) {
       let tooltip = action.getTooltip(window) || action.getTitle(window);
       node.setAttribute("tooltiptext", tooltip);
     }
   },
 
   doCommandForAction(action, event, buttonNode) {
     if (event && event.type == "click" && event.button != 0) {
       return;
     }
     PageActions.logTelemetry("used", action, buttonNode);
     // If we're in the panel, open a subview inside the panel:
     // Note that we can't use this.panelNode.contains(buttonNode) here
-    // because of XBL boundaries breaking ELement.contains.
-    if (action.subview && buttonNode && buttonNode.closest("panel") == this.panelNode) {
+    // because of XBL boundaries breaking Element.contains.
+    if (action.subview &&
+        buttonNode &&
+        buttonNode.closest("panel") == this.panelNode) {
       let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false);
       let panelViewNode = document.getElementById(panelViewNodeID);
       action.subview.onShowing(panelViewNode);
       this.multiViewNode.showSubView(panelViewNode, buttonNode);
       return;
     }
     // Otherwise, hide the main popup in case it was open:
     this.panelNode.hidePopup();
@@ -623,27 +587,49 @@ var BrowserPageActions = {
         actionID = this._actionIDForNodeID(n.id);
         action = PageActions.actionForID(actionID);
       }
     }
     return action;
   },
 
   /**
+   * The given action's top-level button in the main panel.
+   *
+   * @param  actionID (string, required)
+   *         The action ID.
+   * @return (DOM node) The action's button in the main panel.
+   */
+  panelButtonNodeForActionID(actionID) {
+    return document.getElementById(this.panelButtonNodeIDForActionID(actionID));
+  },
+
+  /**
    * The ID of the given action's top-level button in the main panel.
    *
    * @param  actionID (string, required)
    *         The action ID.
    * @return (string) The ID of the action's button in the main panel.
    */
   panelButtonNodeIDForActionID(actionID) {
     return `pageAction-panel-${actionID}`;
   },
 
   /**
+   * The given action's button in the urlbar.
+   *
+   * @param  actionID (string, required)
+   *         The action ID.
+   * @return (DOM node) The action's urlbar button node.
+   */
+  urlbarButtonNodeForActionID(actionID) {
+    return document.getElementById(this.urlbarButtonNodeIDForActionID(actionID));
+  },
+
+  /**
    * The ID of the given action's button in the urlbar.
    *
    * @param  actionID (string, required)
    *         The action ID.
    * @return (string) The ID of the action's urlbar button node.
    */
   urlbarButtonNodeIDForActionID(actionID) {
     let action = PageActions.actionForID(actionID);
@@ -717,86 +703,95 @@ var BrowserPageActions = {
    * Show the page action panel
    *
    * @param  event (DOM event, optional)
    *         The event that triggers showing the panel. (such as a mouse click,
    *         if the user clicked something to open the panel)
    */
   showPanel(event = null) {
     for (let action of PageActions.actions) {
-      let buttonNodeID = this.panelButtonNodeIDForActionID(action.id);
-      let buttonNode = document.getElementById(buttonNodeID);
+      let buttonNode = this.panelButtonNodeForActionID(action.id);
       action.onShowingInPanel(buttonNode);
     }
 
     this.panelNode.hidden = false;
     this.panelNode.addEventListener("popuphiding", () => {
       this.mainButtonNode.removeAttribute("open");
     }, {once: true});
     this.mainButtonNode.setAttribute("open", "true");
     this.panelNode.openPopup(this.mainButtonNode, {
       position: "bottomcenter topright",
       triggerEvent: event,
     });
   },
 
   /**
-   * Call this on the contextmenu event.  Note that this is called before
-   * onContextMenuShowing.
-   *
-   * @param  event (DOM event, required)
-   *         The contextmenu event.
-   */
-  onContextMenu(event) {
-    let node = event.originalTarget;
-    this._contextAction = this.actionForNode(node);
-    // Don't show the menu if there's no action where the user clicked!
-    if (!this._contextAction) {
-      event.preventDefault();
-    }
-  },
-
-  /**
    * Call this on the context menu's popupshowing event.
    *
    * @param  event (DOM event, required)
    *         The popupshowing event.
    * @param  popup (DOM node, required)
    *         The context menu popup DOM node.
    */
   onContextMenuShowing(event, popup) {
     if (event.target != popup) {
       return;
     }
-    // Right now there's only one item in the context menu, to toggle the
-    // context action's shown-in-urlbar state.  Update it now.
-    let toggleItem = popup.firstChild;
-    let toggleItemLabel = null;
-    if (this._contextAction) {
-      toggleItem.disabled = false;
-      if (this._contextAction.shownInUrlbar) {
-        toggleItemLabel = toggleItem.getAttribute("remove-label");
-      }
+
+    this._contextAction = this.actionForNode(popup.triggerNode);
+    if (!this._contextAction) {
+      event.preventDefault();
+      return;
     }
-    if (!toggleItemLabel) {
-      toggleItemLabel = toggleItem.getAttribute("add-label");
+
+    let state;
+    if (this._contextAction._isBuiltIn) {
+      state =
+        this._contextAction.pinnedToUrlbar ?
+        "builtInPinned" :
+        "builtInUnpinned";
+    } else {
+      state =
+        this._contextAction.pinnedToUrlbar ?
+        "extensionPinned" :
+        "extensionUnpinned";
     }
-    toggleItem.label = toggleItemLabel;
+    popup.setAttribute("state", state);
   },
 
   /**
-   * Call this from the context menu's toggle menu item.
+   * Call this from the menu item in the context menu that toggles pinning.
    */
-  toggleShownInUrlbarForContextAction() {
+  togglePinningForContextAction() {
     if (!this._contextAction) {
       return;
     }
-    let telemetryType = this._contextAction.shownInUrlbar ? "removed" : "added";
-    PageActions.logTelemetry(telemetryType, this._contextAction);
-    this._contextAction.shownInUrlbar = !this._contextAction.shownInUrlbar;
+    let action = this._contextAction;
+    this._contextAction = null;
+
+    let telemetryType = action.pinnedToUrlbar ? "removed" : "added";
+    PageActions.logTelemetry(telemetryType, action);
+
+    action.pinnedToUrlbar = !action.pinnedToUrlbar;
+  },
+
+  /**
+   * Call this from the menu item in the context menu that opens about:addons.
+   */
+  openAboutAddonsForContextAction() {
+    if (!this._contextAction) {
+      return;
+    }
+    let action = this._contextAction;
+    this._contextAction = null;
+
+    PageActions.logTelemetry("managed", action);
+
+    let viewID = "addons://detail/" + encodeURIComponent(action.extensionID);
+    window.BrowserOpenAddonsMgr(viewID);
   },
 
   _contextAction: null,
 
   /**
    * Titles for a few of the built-in actions are defined in DTD, but the
    * actions are created in JS.  So what we do is for each title, set an
    * attribute in markup on the main page action panel whose value is the DTD
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -1415,16 +1415,27 @@ toolbarpaletteitem[place="palette"][hidd
   .pageAction-panel-button > .toolbarbutton-icon {
     list-style-image: var(--pageAction-image-32px, inherit);
   }
   .urlbar-page-action {
     list-style-image: var(--pageAction-image-32px, inherit);
   }
 }
 
+/* Page action context menu */
+#pageActionContextMenu > .pageActionContextMenuItem {
+  visibility: collapse;
+}
+#pageActionContextMenu[state=builtInPinned] > .pageActionContextMenuItem.builtInPinned,
+#pageActionContextMenu[state=builtInUnpinned] > .pageActionContextMenuItem.builtInUnpinned,
+#pageActionContextMenu[state=extensionPinned] > .pageActionContextMenuItem.extensionPinned,
+#pageActionContextMenu[state=extensionUnpinned] > .pageActionContextMenuItem.extensionUnpinned {
+  visibility: visible;
+}
+
 /* WebExtension Sidebars */
 #sidebar-box[sidebarcommand$="-sidebar-action"] > #sidebar-header > #sidebar-switcher-target > #sidebar-icon {
   list-style-image: var(--webextension-menuitem-image, inherit);
   -moz-context-properties: fill;
   fill: currentColor;
   width: 16px;
   height: 16px;
 }
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -417,17 +417,16 @@
            emailLink-title="&emailPageCmd.label;"
            sendToDevice-title="&pageAction.sendTabToDevice.label;"
            sendToDevice-notReadyTitle="&sendToDevice.syncNotReady.label;">
       <photonpanelmultiview id="pageActionPanelMultiView"
                             mainViewId="pageActionPanelMainView"
                             viewCacheId="appMenu-viewCache">
         <panelview id="pageActionPanelMainView"
                    context="pageActionContextMenu"
-                   oncontextmenu="BrowserPageActions.onContextMenu(event);"
                    class="PanelUI-subView">
           <vbox class="panel-subview-body"/>
         </panelview>
       </photonpanelmultiview>
     </panel>
     <panel id="pageActionFeedback"
            role="alert"
            type="arrow"
@@ -441,21 +440,32 @@
       <hbox id="pageActionFeedbackAnimatableBox">
         <image id="pageActionFeedbackAnimatableImage"/>
       </hbox>
       <label id="pageActionFeedbackMessage"/>
     </panel>
 
     <menupopup id="pageActionContextMenu"
                onpopupshowing="BrowserPageActions.onContextMenuShowing(event, this);">
-      <menuitem id="pageActionContextMenu-toggleUrlbar"
-                add-label="&pageAction.addToUrlbar.label;"
-                remove-label="&pageAction.removeFromUrlbar.label;"
+      <menuitem class="pageActionContextMenuItem builtInUnpinned"
                 label="&pageAction.addToUrlbar.label;"
-                oncommand="BrowserPageActions.toggleShownInUrlbarForContextAction();"/>
+                oncommand="BrowserPageActions.togglePinningForContextAction();"/>
+      <menuitem class="pageActionContextMenuItem builtInPinned"
+                label="&pageAction.removeFromUrlbar.label;"
+                oncommand="BrowserPageActions.togglePinningForContextAction();"/>
+      <menuitem class="pageActionContextMenuItem extensionUnpinned"
+                label="&pageAction.allowInUrlbar.label;"
+                oncommand="BrowserPageActions.togglePinningForContextAction();"/>
+      <menuitem class="pageActionContextMenuItem extensionPinned"
+                label="&pageAction.disallowInUrlbar.label;"
+                oncommand="BrowserPageActions.togglePinningForContextAction();"/>
+      <menuseparator class="pageActionContextMenuItem extensionPinned extensionUnpinned"/>
+      <menuitem class="pageActionContextMenuItem extensionPinned extensionUnpinned"
+                label="&pageAction.manageExtension.label;"
+                oncommand="BrowserPageActions.openAboutAddonsForContextAction();"/>
     </menupopup>
 
     <!-- Bookmarks and history tooltip -->
     <tooltip id="bhTooltip"/>
 
     <tooltip id="tabbrowser-tab-tooltip" onpopupshowing="gBrowser.createTooltip(event);"/>
 
     <tooltip id="back-button-tooltip">
@@ -854,19 +864,17 @@
                   <label id="identity-icon-label" class="plain" flex="1"/>
                   <label id="identity-icon-country-label" class="plain"/>
                 </hbox>
               </box>
               <box id="urlbar-display-box" align="center">
                 <label id="switchtab" class="urlbar-display urlbar-display-switchtab" value="&urlbar.switchToTab.label;"/>
                 <label id="extension" class="urlbar-display urlbar-display-extension" value="&urlbar.extension.label;"/>
               </box>
-              <hbox id="page-action-buttons"
-                    context="pageActionContextMenu"
-                    oncontextmenu="BrowserPageActions.onContextMenu(event);">
+              <hbox id="page-action-buttons" context="pageActionContextMenu">
                 <hbox id="userContext-icons" hidden="true">
                   <label id="userContext-label"/>
                   <image id="userContext-indicator"/>
                 </hbox>
                 <image id="reader-mode-button"
                        class="urlbar-icon urlbar-page-action"
                        role="button"
                        hidden="true"
--- a/browser/base/content/test/urlbar/browser_page_action_menu.js
+++ b/browser/base/content/test/urlbar/browser_page_action_menu.js
@@ -152,18 +152,18 @@ add_task(async function copyURLFromPanel
 
 add_task(async function copyURLFromURLBar() {
   // Open an actionable page so that the main page action button appears.  (It
   // does not appear on about:blank for example.)
   let url = "http://example.com/";
   await BrowserTestUtils.withNewTab(url, async () => {
     // Add action to URL bar.
     let action = PageActions._builtInActions.find(a => a.id == "copyURL");
-    action.shownInUrlbar = true;
-    registerCleanupFunction(() => action.shownInUrlbar = false);
+    action.pinnedToUrlbar = true;
+    registerCleanupFunction(() => action.pinnedToUrlbar = false);
 
     let copyURLButton =
       document.getElementById("pageAction-urlbar-copyURL");
     let feedbackShownPromise = promisePanelShown("pageActionFeedback");
     EventUtils.synthesizeMouseAtCenter(copyURLButton, {});
 
     await feedbackShownPromise;
     let panel = document.getElementById("pageActionFeedback");
@@ -539,17 +539,17 @@ add_task(async function sendToDevice_inU
 
     let cleanUp = () => {
       sandbox.restore();
     };
     registerCleanupFunction(cleanUp);
 
     // Add Send to Device to the urlbar.
     let action = PageActions.actionForID("sendToDevice");
-    action.shownInUrlbar = true;
+    action.pinnedToUrlbar = true;
 
     // Click it to open its panel.
     let urlbarButton = document.getElementById(
       BrowserPageActions.urlbarButtonNodeIDForActionID(action.id)
     );
     Assert.ok(!urlbarButton.disabled);
     let panelPromise =
       promisePanelShown(BrowserPageActions._activatedActionPanelID);
@@ -617,17 +617,17 @@ add_task(async function sendToDevice_inU
     Assert.equal(
       BrowserPageActionFeedback.panelNode.anchorNode.id,
       urlbarButton.id
     );
     info("Waiting for the Sent! notification panel to close");
     await promisePanelHidden(BrowserPageActionFeedback.panelNode.id);
 
     // Remove Send to Device from the urlbar.
-    action.shownInUrlbar = false;
+    action.pinnedToUrlbar = false;
 
     cleanUp();
   });
 });
 
 add_task(async function contextMenu() {
   // Open an actionable page so that the main page action button appears.
   let url = "http://example.com/";
@@ -637,24 +637,24 @@ add_task(async function contextMenu() {
     let bookmarkButton = document.getElementById("pageAction-panel-bookmark");
     let contextMenuPromise = promisePanelShown("pageActionContextMenu");
     EventUtils.synthesizeMouseAtCenter(bookmarkButton, {
       type: "contextmenu",
       button: 2,
     });
     await contextMenuPromise;
 
-    // The context menu should show "Remove from Address Bar".  Click it.
-    let contextMenuNode = document.getElementById("pageActionContextMenu");
-    Assert.equal(contextMenuNode.childNodes.length, 1,
+    // The context menu should show the "remove" item.  Click it.
+    let menuItems = collectContextMenuItems();
+    Assert.equal(menuItems.length, 1,
                  "Context menu has one child");
-    Assert.equal(contextMenuNode.childNodes[0].label, "Remove from Address Bar",
+    Assert.equal(menuItems[0].label, "Remove from Address Bar",
                  "Context menu is in the 'remove' state");
     contextMenuPromise = promisePanelHidden("pageActionContextMenu");
-    EventUtils.synthesizeMouseAtCenter(contextMenuNode.childNodes[0], {});
+    EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
     await contextMenuPromise;
 
     // The action should be removed from the urlbar.  In this case, the bookmark
     // star, the node in the urlbar should be hidden.
     let starButtonBox = document.getElementById("star-button-box");
     await BrowserTestUtils.waitForCondition(() => {
       return starButtonBox.hidden;
     }, "Waiting for star button to become hidden");
@@ -663,67 +663,71 @@ add_task(async function contextMenu() {
     // panel remains open.)
     contextMenuPromise = promisePanelShown("pageActionContextMenu");
     EventUtils.synthesizeMouseAtCenter(bookmarkButton, {
       type: "contextmenu",
       button: 2,
     });
     await contextMenuPromise;
 
-    // The context menu should show "Add to Address Bar".  Click it.
-    Assert.equal(contextMenuNode.childNodes.length, 1,
+    // The context menu should show the "add" item.  Click it.
+    menuItems = collectContextMenuItems();
+    Assert.equal(menuItems.length, 1,
                  "Context menu has one child");
-    Assert.equal(contextMenuNode.childNodes[0].label, "Add to Address Bar",
+    Assert.equal(menuItems[0].label, "Add to Address Bar",
                  "Context menu is in the 'add' state");
     contextMenuPromise = promisePanelHidden("pageActionContextMenu");
-    EventUtils.synthesizeMouseAtCenter(contextMenuNode.childNodes[0], {});
+    EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
     await contextMenuPromise;
 
     // The action should be added to the urlbar.
     await BrowserTestUtils.waitForCondition(() => {
       return !starButtonBox.hidden;
     }, "Waiting for star button to become unhidden");
 
     // Open the context menu on the bookmark star in the urlbar.
     contextMenuPromise = promisePanelShown("pageActionContextMenu");
     EventUtils.synthesizeMouseAtCenter(starButtonBox, {
       type: "contextmenu",
       button: 2,
     });
     await contextMenuPromise;
 
-    // The context menu should show "Remove from Address Bar".  Click it.
-    Assert.equal(contextMenuNode.childNodes.length, 1,
+    // The context menu should show the "remove" item.  Click it.
+    menuItems = collectContextMenuItems();
+    Assert.equal(menuItems.length, 1,
                  "Context menu has one child");
-    Assert.equal(contextMenuNode.childNodes[0].label, "Remove from Address Bar",
+    Assert.equal(menuItems[0].label, "Remove from Address Bar",
                  "Context menu is in the 'remove' state");
     contextMenuPromise = promisePanelHidden("pageActionContextMenu");
-    EventUtils.synthesizeMouseAtCenter(contextMenuNode.childNodes[0], {});
+    EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
     await contextMenuPromise;
 
     // The action should be removed from the urlbar.
     await BrowserTestUtils.waitForCondition(() => {
       return starButtonBox.hidden;
     }, "Waiting for star button to become hidden");
 
     // Finally, add the bookmark star back to the urlbar so that other tests
     // that rely on it are OK.
     await promisePageActionPanelOpen();
     contextMenuPromise = promisePanelShown("pageActionContextMenu");
     EventUtils.synthesizeMouseAtCenter(bookmarkButton, {
       type: "contextmenu",
       button: 2,
     });
     await contextMenuPromise;
-    Assert.equal(contextMenuNode.childNodes.length, 1,
+
+    menuItems = collectContextMenuItems();
+    Assert.equal(menuItems.length, 1,
                  "Context menu has one child");
-    Assert.equal(contextMenuNode.childNodes[0].label, "Add to Address Bar",
+    Assert.equal(menuItems[0].label, "Add to Address Bar",
                  "Context menu is in the 'add' state");
     contextMenuPromise = promisePanelHidden("pageActionContextMenu");
-    EventUtils.synthesizeMouseAtCenter(contextMenuNode.childNodes[0], {});
+    EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
     await contextMenuPromise;
     await BrowserTestUtils.waitForCondition(() => {
       return !starButtonBox.hidden;
     }, "Waiting for star button to become unhidden");
   });
 
   // urlbar tests that run after this one can break if the mouse is left over
   // the area where the urlbar popup appears, which seems to happen due to the
@@ -770,8 +774,15 @@ function checkSendToDeviceItems(expected
         if (name == "label") {
           attrVal = attrVal.normalize("NFKC"); // There's a bug with …
         }
         Assert.equal(attrVal, expected.attrs[name]);
       }
     }
   }
 }
+
+function collectContextMenuItems() {
+  let contextMenu = document.getElementById("pageActionContextMenu");
+  return Array.filter(contextMenu.childNodes, node => {
+    return window.getComputedStyle(node).visibility == "visible";
+  });
+}
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -61,19 +61,20 @@ this.pageAction = class extends Extensio
 
     this.defaults.icon = await StartupCache.get(
       extension, ["pageAction", "default_icon"],
       () => IconDetails.normalize({path: options.default_icon}, extension));
 
     if (!this.browserPageAction) {
       this.browserPageAction = PageActions.addAction(new PageActions.Action({
         id: widgetId,
+        extensionID: extension.id,
         title: this.defaults.title,
         iconURL: this.getIconData(this.defaults.icon),
-        shownInUrlbar: true,
+        pinnedToUrlbar: true,
         disabled: true,
         onCommand: (event, buttonNode) => {
           this.handleClick(event.target.ownerGlobal);
         },
         onBeforePlacedInWindow: browserWindow => {
           if (this.extension.hasPermission("menus") ||
               this.extension.hasPermission("contextMenus")) {
             browserWindow.document.addEventListener("popupshowing", this);
--- a/browser/components/extensions/test/xpcshell/test_ext_pageAction_shutdown.js
+++ b/browser/components/extensions/test/xpcshell/test_ext_pageAction_shutdown.js
@@ -19,17 +19,17 @@ createAppInfo("xpcshell@tests.mozilla.or
 // This is copied and pasted from ExtensionPopups.jsm.  It's used as the
 // PageActions action ID.  See ext-pageAction.js.
 function makeWidgetId(id) {
   id = id.toLowerCase();
   // FIXME: This allows for collisions.
   return id.replace(/[^a-z0-9_-]/g, "_");
 }
 
-// Tests that the shownInUrlbar property of the PageActions.Action object
+// Tests that the pinnedToUrlbar property of the PageActions.Action object
 // backing the extension's page action persists across app restarts.
 add_task(async function testAppShutdown() {
   let extensionData = {
     useAddonManager: "permanent",
     manifest: {
       page_action: {
         default_title: "test_ext_pageAction_shutdown.js",
         browser_style: false,
@@ -39,41 +39,41 @@ add_task(async function testAppShutdown(
 
   // Simulate starting up the app.
   PageActions.init();
   await promiseStartupManager();
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   await extension.startup();
 
-  // Get the PageAction.Action object.  Its shownInUrlbar should have been
+  // Get the PageAction.Action object.  Its pinnedToUrlbar should have been
   // initialized to true in ext-pageAction.js, when it's created.
   let actionID = makeWidgetId(extension.id);
   let action = PageActions.actionForID(actionID);
-  Assert.equal(action.shownInUrlbar, true);
+  Assert.equal(action.pinnedToUrlbar, true);
 
   // Simulate restarting the app without first unloading the extension.
   await promiseShutdownManager();
   PageActions._reset();
   await promiseStartupManager();
   await extension.awaitStartup();
 
-  // Get the action.  Its shownInUrlbar should remain true.
+  // Get the action.  Its pinnedToUrlbar should remain true.
   action = PageActions.actionForID(actionID);
-  Assert.equal(action.shownInUrlbar, true);
+  Assert.equal(action.pinnedToUrlbar, true);
 
-  // Now set its shownInUrlbar to false.
-  action.shownInUrlbar = false;
+  // Now set its pinnedToUrlbar to false.
+  action.pinnedToUrlbar = false;
 
   // Simulate restarting the app again without first unloading the extension.
   await promiseShutdownManager();
   PageActions._reset();
   await promiseStartupManager();
   await extension.awaitStartup();
 
-  // Get the action.  Its shownInUrlbar should remain false.
+  // Get the action.  Its pinnedToUrlbar should remain false.
   action = PageActions.actionForID(actionID);
-  Assert.equal(action.shownInUrlbar, false);
+  Assert.equal(action.pinnedToUrlbar, false);
 
   // Now unload the extension and quit the app.
   await extension.unload();
   await promiseShutdownManager();
 });
--- a/browser/components/uitour/test/browser_UITour_availableTargets.js
+++ b/browser/components/uitour/test/browser_UITour_availableTargets.js
@@ -131,27 +131,27 @@ async function assertTargetNode(targetNa
   let target = await UITour.getTarget(window, targetName);
   is(target.node.id, expectedNodeId, "UITour should get the right target node");
 }
 
 var pageActionsHelper = {
   setActionsUrlbarState(inUrlbar) {
     this._originalStates = [];
     PageActions._actionsByID.forEach(action => {
-      this._originalStates.push([ action, action.shownInUrlbar ]);
-      action.shownInUrlbar = inUrlbar;
+      this._originalStates.push([ action, action.pinnedToUrlbar ]);
+      action.pinnedToUrlbar = inUrlbar;
     });
   },
 
   restoreActionsUrlbarState() {
     if (!this._originalStates) {
       return;
     }
     for (let [ action, originalState] of this._originalStates) {
-      action.shownInUrlbar = originalState;
+      action.pinnedToUrlbar = originalState;
     }
     this._originalStates = null;
   }
 };
 
 function ensureScreenshotsEnabled() {
   SpecialPowers.pushPrefEnv({ set: [
     [ "extensions.screenshots.disabled", false ]
--- a/browser/extensions/pocket/bootstrap.js
+++ b/browser/extensions/pocket/bootstrap.js
@@ -89,17 +89,17 @@ var PocketPageAction = {
 
   init() {
     let id = "pocket";
     this.pageAction = PageActions.actionForID(id);
     if (!this.pageAction) {
       this.pageAction = PageActions.addAction(new PageActions.Action({
         id,
         title: gPocketBundle.GetStringFromName("saveToPocketCmd.label"),
-        shownInUrlbar: true,
+        pinnedToUrlbar: true,
         wantsIframe: true,
         urlbarIDOverride: "pocket-button-box",
         anchorIDOverride: "pocket-button",
         _insertBeforeActionID: PageActions.ACTION_ID_BOOKMARK_SEPARATOR,
         _urlbarNodeInMarkup: true,
         onBeforePlacedInWindow(window) {
           let doc = window.document;
 
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -956,16 +956,19 @@ you can use these alternative items. Oth
 <!ENTITY updateRestart.acceptButton.accesskey "R">
 <!ENTITY updateRestart.cancelButton.label "Not Now">
 <!ENTITY updateRestart.cancelButton.accesskey "N">
 <!ENTITY updateRestart.panelUI.label2 "Restart to update &brandShorterName;">
 
 <!ENTITY pageActionButton.tooltip "Page actions">
 <!ENTITY pageAction.addToUrlbar.label "Add to Address Bar">
 <!ENTITY pageAction.removeFromUrlbar.label "Remove from Address Bar">
+<!ENTITY pageAction.allowInUrlbar.label "Show in Address Bar">
+<!ENTITY pageAction.disallowInUrlbar.label "Don’t Show in Address Bar">
+<!ENTITY pageAction.manageExtension.label "Manage Extension…">
 
 <!ENTITY pageAction.sendTabToDevice.label "Send Tab to Device">
 <!ENTITY sendToDevice.syncNotReady.label "Syncing Devices…">
 
 <!ENTITY libraryButton.tooltip "View history, saved bookmarks, and more">
 
 <!-- LOCALIZATION NOTE: (accessibilityIndicator.tooltip): This is used to
      display a tooltip for accessibility indicator in toolbar/tabbar. It is also
--- a/browser/modules/PageActions.jsm
+++ b/browser/modules/PageActions.jsm
@@ -114,25 +114,30 @@ this.PageActions = {
   /**
    * The list of non-built-in actions.  Not live.  (array of Action objects)
    */
   get nonBuiltInActions() {
     return this._nonBuiltInActions.slice();
   },
 
   /**
-   * The list of actions in the urlbar, sorted in the order in which they should
-   * appear there.  Not live.  (array of Action objects)
+   * The list of actions currently in the urlbar, sorted in the order in which
+   * they appear.  Not live.
+   *
+   * @param  browserWindow (DOM window, required)
+   *         This window's actions will be returned.
+   * @return (array of PageAction.Action objects) The actions currently in the
+   *         given window's urlbar.
    */
-  get actionsInUrlbar() {
+  actionsInUrlbar(browserWindow) {
     // Remember that IDs in idsInUrlbar may belong to actions that aren't
     // currently registered.
     return this._persistedActions.idsInUrlbar.reduce((actions, id) => {
       let action = this.actionForID(id);
-      if (action) {
+      if (action && action.shouldShowInUrlbar(browserWindow)) {
         actions.push(action);
       }
       return actions;
     }, []);
   },
 
   /**
    * Gets an action.
@@ -223,40 +228,39 @@ this.PageActions = {
       // Keep this list sorted by title.
       let index = BinarySearch.insertionIndexOf((a1, a2) => {
         return a1.getTitle().localeCompare(a2.getTitle());
       }, this._nonBuiltInActions, action);
       this._nonBuiltInActions.splice(index, 0, action);
     }
 
     if (this._persistedActions.ids.includes(action.id)) {
-      // The action has been seen before.  Override its shownInUrlbar value
+      // The action has been seen before.  Override its pinnedToUrlbar value
       // with the persisted value.  Set the private version of that property
-      // so that onActionToggledShownInUrlbar isn't called, which happens when
+      // so that onActionToggledPinnedToUrlbar isn't called, which happens when
       // the public version is set.
-      action._shownInUrlbar =
+      action._pinnedToUrlbar =
         this._persistedActions.idsInUrlbar.includes(action.id);
     } else {
       // The action is new.  Store it in the persisted actions.
       this._persistedActions.ids.push(action.id);
-      this._updateIDsInUrlbarForAction(action);
+      this._updateIDsPinnedToUrlbarForAction(action);
     }
   },
 
-  _updateIDsInUrlbarForAction(action) {
+  _updateIDsPinnedToUrlbarForAction(action) {
     let index = this._persistedActions.idsInUrlbar.indexOf(action.id);
-    if (action.shownInUrlbar) {
+    if (action.pinnedToUrlbar) {
       if (index < 0) {
-        let nextID = this.nextActionIDInUrlbar(action.id);
-        let nextIndex =
-          nextID ? this._persistedActions.idsInUrlbar.indexOf(nextID) : -1;
-        if (nextIndex < 0) {
-          nextIndex = this._persistedActions.idsInUrlbar.length;
+        index = action.id == ACTION_ID_BOOKMARK ? -1 :
+                this._persistedActions.idsInUrlbar.indexOf(ACTION_ID_BOOKMARK);
+        if (index < 0) {
+          index = this._persistedActions.idsInUrlbar.length;
         }
-        this._persistedActions.idsInUrlbar.splice(nextIndex, 0, action.id);
+        this._persistedActions.idsInUrlbar.splice(index, 0, action.id);
       }
     } else if (index >= 0) {
       this._persistedActions.idsInUrlbar.splice(index, 1);
     }
     this._storePersistedActions();
   },
 
   // These keep track of currently registered actions.
@@ -268,23 +272,23 @@ this.PageActions = {
    * Returns the ID of the action before which the given action should be
    * inserted in the urlbar.
    *
    * @param  action (Action object, required)
    *         The action you're inserting.
    * @return The ID of the reference action, or null if your action should be
    *         appended.
    */
-  nextActionIDInUrlbar(action) {
+  nextActionIDInUrlbar(browserWindow, action) {
     // Actions in the urlbar are always inserted before the bookmark action,
     // which always comes last if it's present.
     if (action.id == ACTION_ID_BOOKMARK) {
       return null;
     }
-    let id = this._nextActionID(action, this.actionsInUrlbar);
+    let id = this._nextActionID(action, this.actionsInUrlbar(browserWindow));
     return id || ACTION_ID_BOOKMARK;
   },
 
   /**
    * Returns the ID of the action before which the given action should be
    * inserted in the panel.
    *
    * @param  action (Action object, required)
@@ -349,44 +353,42 @@ this.PageActions = {
     }
 
     for (let bpa of allBrowserPageActions()) {
       bpa.removeAction(action);
     }
   },
 
   /**
-   * Call this when an action's shownInUrlbar property changes.
+   * Call this when an action's pinnedToUrlbar property changes.
    *
    * @param  action (Action object, required)
-   *         The action whose shownInUrlbar property changed.
+   *         The action whose pinnedToUrlbar property changed.
    */
-  onActionToggledShownInUrlbar(action) {
+  onActionToggledPinnedToUrlbar(action) {
     if (!this.actionForID(action.id)) {
       // This may be called before the action has been added.
       return;
     }
-    this._updateIDsInUrlbarForAction(action);
+    this._updateIDsPinnedToUrlbarForAction(action);
     for (let bpa of allBrowserPageActions()) {
       bpa.placeActionInUrlbar(action);
     }
   },
 
   logTelemetry(type, action, node = null) {
-    const kAllowedLabels = ["pocket", "screenshots", "webcompat"].concat(
-      gBuiltInActions.filter(a => !a.__isSeparator).map(a => a.id)
-    );
-
     if (type == "used") {
-      type = (node && node.closest("#urlbar-container")) ? "urlbar_used" : "panel_used";
+      type =
+        node && node.closest("#urlbar-container") ? "urlbar_used" :
+        "panel_used";
     }
     let histogramID = "FX_PAGE_ACTION_" + type.toUpperCase();
     try {
       let histogram = Services.telemetry.getHistogramById(histogramID);
-      if (kAllowedLabels.includes(action.labelForHistogram)) {
+      if (action._isBuiltIn) {
         histogram.add(action.labelForHistogram);
       } else {
         histogram.add("other");
       }
     } catch (ex) {
       Cu.reportError(ex);
     }
   },
@@ -487,16 +489,18 @@ this.PageActions = {
  * @param title (string, required)
  *        The action's title.
  * @param anchorIDOverride (string, optional)
  *        Pass a string to override the node to which the action's activated-
  *        action panel is anchored.
  * @param disabled (bool, optional)
  *        Pass true to cause the action to be disabled initially in all browser
  *        windows.  False by default.
+ * @param extensionID (string, optional)
+ *        If the action lives in an extension, pass its ID.
  * @param iconURL (string or object, optional)
  *        The URL string of the action's icon.  Usually you want to specify an
  *        icon in CSS, but this option is useful if that would be a pain for
  *        some reason.  You can also pass an object that maps pixel sizes to
  *        URLs, like { 16: url16, 32: url32 }.  The best size for the user's
  *        screen will be used.
  * @param nodeAttributes (object, optional)
  *        An object of name-value pairs.  Each pair will be added as an
@@ -544,19 +548,19 @@ this.PageActions = {
  * @param onRemovedFromWindow (function, optional)
  *        Called after the action is removed from a browser window:
  *        onRemovedFromWindow(browserWindow)
  *        * browserWindow: The browser window that the action was removed from.
  * @param onShowingInPanel (function, optional)
  *        Called when a browser window's page action panel is showing:
  *        onShowingInPanel(buttonNode)
  *        * buttonNode: The action's node in the page action panel.
- * @param shownInUrlbar (bool, optional)
- *        Pass true to show the action in the urlbar, false otherwise.  False by
- *        default.
+ * @param pinnedToUrlbar (bool, optional)
+ *        Pass true to pin the action to the urlbar.  An action is shown in the
+ *        urlbar if it's pinned and not disabled.  False by default.
  * @param subview (object, optional)
  *        An options object suitable for passing to the Subview constructor, if
  *        you'd like the action to have a subview.  See the subview constructor
  *        for info on this object's properties.
  * @param tooltip (string, optional)
  *        The action's button tooltip text.
  * @param urlbarIDOverride (string, optional)
  *        Usually the ID of the action's button in the urlbar will be generated
@@ -567,30 +571,31 @@ this.PageActions = {
  *        clicked.
  */
 function Action(options) {
   setProperties(this, options, {
     id: true,
     title: !options._isSeparator,
     anchorIDOverride: false,
     disabled: false,
+    extensionID: false,
     iconURL: false,
     labelForHistogram: false,
     nodeAttributes: false,
     onBeforePlacedInWindow: false,
     onCommand: false,
     onIframeHiding: false,
     onIframeHidden: false,
     onIframeShown: false,
     onLocationChange: false,
     onPlacedInPanel: false,
     onPlacedInUrlbar: false,
     onRemovedFromWindow: false,
     onShowingInPanel: false,
-    shownInUrlbar: false,
+    pinnedToUrlbar: false,
     subview: false,
     tooltip: false,
     urlbarIDOverride: false,
     wantsIframe: false,
 
     // private
 
     // (string, optional)
@@ -613,42 +618,50 @@ function Action(options) {
   });
   if (this._subview) {
     this._subview = new Subview(options.subview);
   }
 }
 
 Action.prototype = {
   /**
+   * The ID of the action's parent extension (string, nullable)
+   */
+  get extensionID() {
+    return this._extensionID;
+  },
+
+  /**
    * The action's ID (string, nonnull)
    */
   get id() {
     return this._id;
   },
 
   /**
    * Attribute name => value mapping to set on nodes created for this action
    * (object, nullable)
    */
   get nodeAttributes() {
     return this._nodeAttributes;
   },
 
   /**
-   * True if the action is shown in the urlbar (bool, nonnull)
+   * True if the action is pinned to the urlbar.  The action is shown in the
+   * urlbar if it's pinned and not disabled.  (bool, nonnull)
    */
-  get shownInUrlbar() {
-    return this._shownInUrlbar || false;
+  get pinnedToUrlbar() {
+    return this._pinnedToUrlbar || false;
   },
-  set shownInUrlbar(shown) {
-    if (this.shownInUrlbar != shown) {
-      this._shownInUrlbar = shown;
-      PageActions.onActionToggledShownInUrlbar(this);
+  set pinnedToUrlbar(shown) {
+    if (this.pinnedToUrlbar != shown) {
+      this._pinnedToUrlbar = shown;
+      PageActions.onActionToggledPinnedToUrlbar(this);
     }
-    return this.shownInUrlbar;
+    return this.pinnedToUrlbar;
   },
 
   /**
    * The action's disabled state (bool, nonnull)
    */
   getDisabled(browserWindow = null) {
     return !!this._getProperty("disabled", browserWindow);
   },
@@ -967,17 +980,38 @@ Action.prototype = {
    *
    * PageActions will remember the action's urlbar placement, if any, after this
    * method is called until app shutdown.  If the action is not added again
    * before shutdown, then PageActions will discard the placement, and the next
    * time the action is added, its placement will be reset.
    */
   remove() {
     PageActions.onActionRemoved(this);
-  }
+  },
+
+  /**
+   * Returns whether the action should be shown in a given window's urlbar.
+   *
+   * @param  browserWindow (DOM window, required)
+   *         The window.
+   * @return True if the action should be shown and false otherwise.  The action
+   *         should be shown if it's both pinned and not disabled.
+   */
+  shouldShowInUrlbar(browserWindow) {
+    return this.pinnedToUrlbar && !this.getDisabled(browserWindow);
+  },
+
+  get _isBuiltIn() {
+    let builtInIDs = [
+      "pocket",
+      "screenshots",
+      "webcompat-reporter-button",
+    ].concat(gBuiltInActions.filter(a => !a.__isSeparator).map(a => a.id));
+    return builtInIDs.includes(this.id);
+  },
 };
 
 this.PageActions.Action = Action;
 
 
 /**
  * A Subview represents a PanelUI panelview that your actions can show.
  * `options` is a required object with the following properties.
@@ -1141,17 +1175,17 @@ var gBuiltInActions = [
   // bookmark
   {
     id: ACTION_ID_BOOKMARK,
     urlbarIDOverride: "star-button-box",
     _urlbarNodeInMarkup: true,
     // The title is set in browser-pageActions.js by calling
     // BookmarkingUI.updateBookmarkPageMenuItem().
     title: "",
-    shownInUrlbar: true,
+    pinnedToUrlbar: true,
     nodeAttributes: {
       observes: "bookmarkThisPageBroadcaster",
     },
     onShowingInPanel(buttonNode) {
       browserPageActions(buttonNode).bookmark.onShowingInPanel(buttonNode);
     },
     onCommand(event, buttonNode) {
       browserPageActions(buttonNode).bookmark.onCommand(event, buttonNode);
--- a/browser/modules/test/browser/browser_PageActions.js
+++ b/browser/modules/test/browser/browser_PageActions.js
@@ -1,9 +1,8 @@
-/* eslint-disable mozilla/no-arbitrary-setTimeout */
 "use strict";
 
 // This is a test for PageActions.jsm, specifically the generalized parts that
 // add and remove page actions and toggle them in the urlbar.  This does not
 // test the built-in page actions; browser_page_action_menu.js does that.
 
 // Initialization.  Must run first.
 add_task(async function init() {
@@ -72,20 +71,24 @@ add_task(async function simple() {
       Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
       Assert.equal(buttonNode.id, panelButtonID, "buttonNode.id");
     },
   }));
 
   Assert.equal(action.getIconURL(), iconURL, "iconURL");
   Assert.equal(action.id, id, "id");
   Assert.deepEqual(action.nodeAttributes, nodeAttributes, "nodeAttributes");
-  Assert.equal(action.shownInUrlbar, false, "shownInUrlbar");
+  Assert.equal(action.pinnedToUrlbar, false, "pinnedToUrlbar");
   Assert.equal(action.subview, null, "subview");
+  Assert.equal(action.getDisabled(), false, "disabled");
+  Assert.equal(action.getDisabled(window), false, "disabled in window");
   Assert.equal(action.getTitle(), title, "title");
+  Assert.equal(action.getTitle(window), title, "title in window");
   Assert.equal(action.getTooltip(), tooltip, "tooltip");
+  Assert.equal(action.getTooltip(window), tooltip, "tooltip in window");
   Assert.equal(action.urlbarIDOverride, null, "urlbarIDOverride");
   Assert.equal(action.wantsIframe, false, "wantsIframe");
 
   Assert.ok(!("__insertBeforeActionID" in action), "__insertBeforeActionID");
   Assert.ok(!("__isSeparator" in action), "__isSeparator");
   Assert.ok(!("__urlbarNodeInMarkup" in action), "__urlbarNodeInMarkup");
 
   Assert.equal(onPlacedInPanelCallCount, 1,
@@ -146,17 +149,17 @@ add_task(async function simple() {
   Assert.equal(onShowingInPanelCallCount, 1,
                "onShowingInPanelCallCount should be inc'ed");
   onCommandExpectedButtonID = panelButtonID;
   EventUtils.synthesizeMouseAtCenter(panelButtonNode, {});
   await promisePageActionPanelHidden();
   Assert.equal(onCommandCallCount, 1, "onCommandCallCount should be inc'ed");
 
   // Show the action's button in the urlbar.
-  action.shownInUrlbar = true;
+  action.pinnedToUrlbar = true;
   Assert.equal(onPlacedInUrlbarCallCount, 1,
                "onPlacedInUrlbarCallCount should be inc'ed");
   urlbarButtonNode = document.getElementById(urlbarButtonID);
   Assert.notEqual(urlbarButtonNode, null, "urlbarButtonNode");
   for (let name in action.nodeAttributes) {
     Assert.ok(urlbarButtonNode.hasAttribute(name), name,
               "Has attribute: " + name);
     Assert.equal(urlbarButtonNode.getAttribute(name),
@@ -167,29 +170,45 @@ add_task(async function simple() {
   // The button should have been inserted before the bookmark star.
   Assert.notEqual(urlbarButtonNode.nextSibling, null, "Should be a next node");
   Assert.equal(
     urlbarButtonNode.nextSibling.id,
     PageActions.actionForID(PageActions.ACTION_ID_BOOKMARK).urlbarIDOverride,
     "Next node should be the bookmark star"
   );
 
+  // Disable the action.  The button in the urlbar should be removed, and the
+  // button in the panel should be disabled.
+  action.setDisabled(true);
+  urlbarButtonNode = document.getElementById(urlbarButtonID);
+  Assert.equal(urlbarButtonNode, null, "urlbar button should be removed");
+  Assert.equal(panelButtonNode.disabled, true,
+               "panel button should be disabled");
+
+  // Enable the action.  The button in the urlbar should be added back, and the
+  // button in the panel should be enabled.
+  action.setDisabled(false);
+  urlbarButtonNode = document.getElementById(urlbarButtonID);
+  Assert.notEqual(urlbarButtonNode, null, "urlbar button should be added back");
+  Assert.equal(panelButtonNode.disabled, false,
+               "panel button should not be disabled");
+
   // Click the urlbar button.
   onCommandExpectedButtonID = urlbarButtonID;
   EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
   Assert.equal(onCommandCallCount, 2, "onCommandCallCount should be inc'ed");
 
   // Set a new title.
   let newTitle = title + " new title";
   action.setTitle(newTitle);
   Assert.equal(action.getTitle(), newTitle, "New title");
   Assert.equal(panelButtonNode.getAttribute("label"), action.getTitle(),
                "New label");
 
-  // Now that shownInUrlbar has been toggled, make sure that it sticks across
+  // Now that pinnedToUrlbar has been toggled, make sure that it sticks across
   // app restarts.  Simulate that by "unregistering" the action (not by removing
   // it, which is more permanent) and then registering it again.
 
   // unregister
   PageActions._actionsByID.delete(action.id);
   let index = PageActions._nonBuiltInActions.findIndex(a => a.id == action.id);
   Assert.ok(index >= 0, "Action should be in _nonBuiltInActions to begin with");
   PageActions._nonBuiltInActions.splice(index, 1);
@@ -197,20 +216,20 @@ add_task(async function simple() {
   // register again
   PageActions._registerAction(action);
 
   // check relevant properties
   Assert.ok(PageActions._persistedActions.ids.includes(action.id),
             "PageActions should have 'seen' the action");
   Assert.ok(PageActions._persistedActions.idsInUrlbar.includes(action.id),
             "idsInUrlbar should still include the action");
-  Assert.ok(action.shownInUrlbar,
-            "shownInUrlbar should still be true");
-  Assert.ok(action._shownInUrlbar,
-            "_shownInUrlbar should still be true, for good measure");
+  Assert.ok(action.pinnedToUrlbar,
+            "pinnedToUrlbar should still be true");
+  Assert.ok(action._pinnedToUrlbar,
+            "_pinnedToUrlbar should still be true, for good measure");
 
   // Remove the action.
   action.remove();
   panelButtonNode = document.getElementById(panelButtonID);
   Assert.equal(panelButtonNode, null, "panelButtonNode");
   urlbarButtonNode = document.getElementById(urlbarButtonID);
   Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
 
@@ -296,17 +315,17 @@ add_task(async function withSubview() {
         break;
       }
     }
   };
 
   let action = PageActions.addAction(new PageActions.Action({
     iconURL: "chrome://browser/skin/mail.svg",
     id,
-    shownInUrlbar: true,
+    pinnedToUrlbar: true,
     subview,
     title: "Test subview",
     onCommand(event, buttonNode) {
       onActionCommandCallCount++;
     },
     onPlacedInPanel(buttonNode) {
       onActionPlacedInPanelCallCount++;
       Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
@@ -439,17 +458,17 @@ add_task(async function withIframe() {
   let onIframeShownCount = 0;
 
   let panelButtonID = BrowserPageActions.panelButtonNodeIDForActionID(id);
   let urlbarButtonID = BrowserPageActions.urlbarButtonNodeIDForActionID(id);
 
   let action = PageActions.addAction(new PageActions.Action({
     iconURL: "chrome://browser/skin/mail.svg",
     id,
-    shownInUrlbar: true,
+    pinnedToUrlbar: true,
     title: "Test iframe",
     wantsIframe: true,
     onCommand(event, buttonNode) {
       onCommandCallCount++;
     },
     onIframeShown(iframeNode, panelNode) {
       onIframeShownCount++;
       Assert.ok(iframeNode, "iframeNode should be non-null: " + iframeNode);
@@ -525,17 +544,17 @@ add_task(async function withIframe() {
   // action's urlbar button.
   aaPanel = document.getElementById(BrowserPageActions._activatedActionPanelID);
   Assert.notEqual(aaPanel, null, "aaPanel");
   Assert.equal(aaPanel.anchorNode.id, urlbarButtonID, "aaPanel.anchorNode.id");
   EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
   await promisePanelHidden(BrowserPageActions._activatedActionPanelID);
 
   // Hide the action's button in the urlbar.
-  action.shownInUrlbar = false;
+  action.pinnedToUrlbar = false;
   urlbarButtonNode = document.getElementById(urlbarButtonID);
   Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
 
   // Open the panel, click the action's button.
   await promisePageActionPanelOpen();
   EventUtils.synthesizeMouseAtCenter(panelButtonNode, {});
   await promisePanelShown(BrowserPageActions._activatedActionPanelID);
   Assert.equal(onCommandCallCount, 0, "onCommandCallCount should remain 0");
@@ -825,32 +844,32 @@ add_task(async function nonBuiltFirst() 
 
 // Makes sure that urlbar nodes appear in the correct order in a new window.
 add_task(async function urlbarOrderNewWindow() {
   // Make some new actions.
   let actions = [0, 1, 2].map(i => {
     return PageActions.addAction(new PageActions.Action({
       id: `test-urlbarOrderNewWindow-${i}`,
       title: `Test urlbarOrderNewWindow ${i}`,
-      shownInUrlbar: true,
+      pinnedToUrlbar: true,
     }));
   });
 
   // Make sure PageActions knows they're inserted before the bookmark action in
   // the urlbar.
   Assert.deepEqual(
     PageActions._persistedActions.idsInUrlbar.slice(
       PageActions._persistedActions.idsInUrlbar.length - (actions.length + 1)
     ),
     actions.map(a => a.id).concat([PageActions.ACTION_ID_BOOKMARK]),
     "PageActions._persistedActions.idsInUrlbar has new actions inserted"
   );
   Assert.deepEqual(
-    PageActions.actionsInUrlbar.slice(
-      PageActions.actionsInUrlbar.length - (actions.length + 1)
+    PageActions.actionsInUrlbar(window).slice(
+      PageActions.actionsInUrlbar(window).length - (actions.length + 1)
     ).map(a => a.id),
     actions.map(a => a.id).concat([PageActions.ACTION_ID_BOOKMARK]),
     "PageActions.actionsInUrlbar has new actions inserted"
   );
 
   // Reach into _persistedActions to move the new actions to the front of the
   // urlbar, same as if the user moved them.  That way we can test that insert-
   // before IDs are correctly non-null when the urlbar nodes are inserted in the
@@ -929,35 +948,35 @@ add_task(async function migrate1() {
     JSON.stringify(persisted)
   );
 
   // Migrate.
   PageActions._loadPersistedActions();
 
   Assert.equal(PageActions._persistedActions.version, 1, "Correct version");
 
-  // Need to set copyURL's _shownInUrlbar.  It won't be set since it's false by
+  // Need to set copyURL's _pinnedToUrlbar.  It won't be set since it's false by
   // default and we reached directly into persisted storage above.
-  PageActions.actionForID("copyURL")._shownInUrlbar = true;
+  PageActions.actionForID("copyURL")._pinnedToUrlbar = true;
 
   // expected order
   let orderedIDs = [
     "pocket",
     "copyURL",
     PageActions.ACTION_ID_BOOKMARK,
   ];
 
   // Check the ordering.
   Assert.deepEqual(
     PageActions._persistedActions.idsInUrlbar,
     orderedIDs,
     "PageActions._persistedActions.idsInUrlbar has right order"
   );
   Assert.deepEqual(
-    PageActions.actionsInUrlbar.map(a => a.id),
+    PageActions.actionsInUrlbar(window).map(a => a.id),
     orderedIDs,
     "PageActions.actionsInUrlbar has right order"
   );
 
   // Open a new window.
   let win = await BrowserTestUtils.openNewBrowserWindow();
   await BrowserTestUtils.openNewForegroundTab({
     gBrowser: win.gBrowser,
@@ -977,31 +996,33 @@ add_task(async function migrate1() {
     actualUrlbarNodeIDs,
     orderedIDs.map(id => win.BrowserPageActions.urlbarButtonNodeIDForActionID(id)),
     "Expected actions in new window's urlbar"
   );
 
   // Done, clean up.
   await BrowserTestUtils.closeWindow(win);
   Services.prefs.clearUserPref(PageActions.PREF_PERSISTED_ACTIONS);
-  PageActions.actionForID("copyURL").shownInUrlbar = false;
+  PageActions.actionForID("copyURL").pinnedToUrlbar = false;
 });
 
 
 // Opens a new browser window and makes sure per-window state works right.
 add_task(async function perWindowState() {
   // Add a test action.
   let title = "Test perWindowState";
   let action = PageActions.addAction(new PageActions.Action({
     iconURL: "chrome://browser/skin/mail.svg",
     id: "test-perWindowState",
-    shownInUrlbar: true,
+    pinnedToUrlbar: true,
     title,
   }));
 
+  let actionsInUrlbar = PageActions.actionsInUrlbar(window);
+
   // Open a new browser window and load an actionable page so that the action
   // shows up in it.
   let newWindow = await BrowserTestUtils.openNewBrowserWindow();
   await BrowserTestUtils.openNewForegroundTab({
     gBrowser: newWindow.gBrowser,
     url: "http://example.com/",
   });
 
@@ -1038,53 +1059,100 @@ add_task(async function perWindowState()
   // same in the old window.
   let panelButtonNode1 = document.getElementById(panelButtonID);
   Assert.equal(panelButtonNode1.getAttribute("label"), newGlobalTitle,
                "Panel button label in old window");
   let panelButtonNode2 = newWindow.document.getElementById(panelButtonID);
   Assert.equal(panelButtonNode2.getAttribute("label"), newPerWinTitle,
                "Panel button label in new window");
 
+  // Disable the action in the new window.
+  action.setDisabled(true, newWindow);
+  Assert.equal(action.getDisabled(), false,
+               "Disabled: global should remain false");
+  Assert.equal(action.getDisabled(window), false,
+               "Disabled: old window should remain false");
+  Assert.equal(action.getDisabled(newWindow), true,
+               "Disabled: new window should be true");
+
+  // Check PageActions.actionsInUrlbar for each window.
+  Assert.deepEqual(
+    PageActions.actionsInUrlbar(window).map(a => a.id),
+    actionsInUrlbar.map(a => a.id),
+    "PageActions.actionsInUrlbar: old window should have all actions in urlbar"
+  );
+  Assert.deepEqual(
+    PageActions.actionsInUrlbar(newWindow).map(a => a.id),
+    actionsInUrlbar.map(a => a.id).filter(id => id != action.id),
+    "PageActions.actionsInUrlbar: new window should have all actions in urlbar except the test action"
+  );
+
+  // Check the urlbar nodes for the old window.
+  let actualUrlbarNodeIDs = [];
+  for (let node = BrowserPageActions.mainButtonNode.nextSibling;
+       node;
+       node = node.nextSibling) {
+    actualUrlbarNodeIDs.push(node.id);
+  }
+  Assert.deepEqual(
+    actualUrlbarNodeIDs,
+    actionsInUrlbar.map(a => BrowserPageActions.urlbarButtonNodeIDForActionID(a.id)),
+    "Old window should have all nodes in urlbar"
+  );
+
+  // Check the urlbar nodes for the new window.
+  actualUrlbarNodeIDs = [];
+  for (let node = newWindow.BrowserPageActions.mainButtonNode.nextSibling;
+       node;
+       node = node.nextSibling) {
+    actualUrlbarNodeIDs.push(node.id);
+  }
+  Assert.deepEqual(
+    actualUrlbarNodeIDs,
+    actionsInUrlbar.filter(a => a.id != action.id).map(a => BrowserPageActions.urlbarButtonNodeIDForActionID(a.id)),
+    "New window should have all nodes in urlbar except for the test action's"
+  );
+
   // Done, clean up.
   await BrowserTestUtils.closeWindow(newWindow);
   action.remove();
 });
 
 
 // Adds an action, changes its placement in the urlbar to something non-default,
 // removes the action, and then adds it back.  Since the action was removed and
 // re-added without restarting the app (or more accurately without calling
 // PageActions._purgeUnregisteredPersistedActions), the action should remain in
 // persisted state and retain its last placement in the urlbar.
 add_task(async function removeRetainState() {
   // Get the list of actions initially in the urlbar.
-  let initialActionsInUrlbar = PageActions.actionsInUrlbar;
+  let initialActionsInUrlbar = PageActions.actionsInUrlbar(window);
   Assert.ok(initialActionsInUrlbar.length > 0,
             "This test expects there to be at least one action in the urlbar initially (like the bookmark star)");
 
   // Add a test action.
   let id = "test-removeRetainState";
   let testAction = PageActions.addAction(new PageActions.Action({
     id,
     title: "Test removeRetainState",
   }));
 
   // Show its button in the urlbar.
-  testAction.shownInUrlbar = true;
+  testAction.pinnedToUrlbar = true;
 
-  // "Move" the test action to the front of the urlbar by toggling shownInUrlbar
-  // for all the other actions in the urlbar.
+  // "Move" the test action to the front of the urlbar by toggling
+  // pinnedToUrlbar for all the other actions in the urlbar.
   for (let action of initialActionsInUrlbar) {
-    action.shownInUrlbar = false;
-    action.shownInUrlbar = true;
+    action.pinnedToUrlbar = false;
+    action.pinnedToUrlbar = true;
   }
 
   // Check the actions in PageActions.actionsInUrlbar.
   Assert.deepEqual(
-    PageActions.actionsInUrlbar.map(a => a.id),
+    PageActions.actionsInUrlbar(window).map(a => a.id),
     [testAction].concat(initialActionsInUrlbar).map(a => a.id),
     "PageActions.actionsInUrlbar should be in expected order: testAction followed by all initial actions"
   );
 
   // Check the nodes in the urlbar.
   let actualUrlbarNodeIDs = [];
   for (let node = BrowserPageActions.mainButtonNode.nextSibling;
        node;
@@ -1097,17 +1165,17 @@ add_task(async function removeRetainStat
     "urlbar nodes should be in expected order: testAction followed by all initial actions"
   );
 
   // Remove the test action.
   testAction.remove();
 
   // Check the actions in PageActions.actionsInUrlbar.
   Assert.deepEqual(
-    PageActions.actionsInUrlbar.map(a => a.id),
+    PageActions.actionsInUrlbar(window).map(a => a.id),
     initialActionsInUrlbar.map(a => a.id),
     "PageActions.actionsInUrlbar should be in expected order after removing test action: all initial actions"
   );
 
   // Check the nodes in the urlbar.
   actualUrlbarNodeIDs = [];
   for (let node = BrowserPageActions.mainButtonNode.nextSibling;
        node;
@@ -1122,21 +1190,21 @@ add_task(async function removeRetainStat
 
   // Add the test action again.
   testAction = PageActions.addAction(new PageActions.Action({
     id,
     title: "Test removeRetainState",
   }));
 
   // Show its button in the urlbar again.
-  testAction.shownInUrlbar = true;
+  testAction.pinnedToUrlbar = true;
 
   // Check the actions in PageActions.actionsInUrlbar.
   Assert.deepEqual(
-    PageActions.actionsInUrlbar.map(a => a.id),
+    PageActions.actionsInUrlbar(window).map(a => a.id),
     [testAction].concat(initialActionsInUrlbar).map(a => a.id),
     "PageActions.actionsInUrlbar should be in expected order after re-adding test action: testAction followed by all initial actions"
   );
 
   // Check the nodes in the urlbar.
   actualUrlbarNodeIDs = [];
   for (let node = BrowserPageActions.mainButtonNode.nextSibling;
        node;
@@ -1149,16 +1217,210 @@ add_task(async function removeRetainStat
     "urlbar nodes should be in expected order after re-adding test action: testAction followed by all initial actions"
   );
 
   // Done, clean up.
   testAction.remove();
 });
 
 
+// Opens the context menu on a non-built-in action.  (The context menu for
+// built-in actions is tested in browser_page_action_menu.js.)
+add_task(async function contextMenu() {
+  // Add a test action.
+  let action = PageActions.addAction(new PageActions.Action({
+    id: "test-contextMenu",
+    title: "Test contextMenu",
+    pinnedToUrlbar: true,
+  }));
+
+  // Open the panel and then open the context menu on the action's item.
+  await promisePageActionPanelOpen();
+  let panelButton = BrowserPageActions.panelButtonNodeForActionID(action.id);
+  let contextMenuPromise = promisePanelShown("pageActionContextMenu");
+  EventUtils.synthesizeMouseAtCenter(panelButton, {
+    type: "contextmenu",
+    button: 2,
+  });
+  await contextMenuPromise;
+
+  // The context menu should show the "don't show" item and the "manage" item.
+  // Click the "don't show" item.
+  let menuItems = collectContextMenuItems();
+  Assert.equal(menuItems.length, 3,
+               "Context menu has 3 children");
+  Assert.equal(menuItems[0].label, "Don\u2019t Show in Address Bar",
+               "Context menu is in the 'don't show' state");
+  Assert.equal(menuItems[1].localName, "menuseparator",
+               "menuseparator is present");
+  Assert.equal(menuItems[2].label, "Manage Extension\u2026",
+               "'Manage' item is present");
+  contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+  EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
+  await contextMenuPromise;
+
+  // The action should be removed from the urlbar.
+  await BrowserTestUtils.waitForCondition(() => {
+    return !BrowserPageActions.urlbarButtonNodeForActionID(action.id);
+  }, "Waiting for urlbar button to be removed");
+
+  // Open the context menu again on the action's button in the panel.  (The
+  // panel should still be open.)
+  contextMenuPromise = promisePanelShown("pageActionContextMenu");
+  EventUtils.synthesizeMouseAtCenter(panelButton, {
+    type: "contextmenu",
+    button: 2,
+  });
+  await contextMenuPromise;
+
+  // The context menu should show the "show" item and the "manage" item.  Click
+  // the "show" item.
+  menuItems = collectContextMenuItems();
+  Assert.equal(menuItems.length, 3,
+               "Context menu has 3 children");
+  Assert.equal(menuItems[0].label, "Show in Address Bar",
+               "Context menu is in the 'show' state");
+  Assert.equal(menuItems[1].localName, "menuseparator",
+               "menuseparator is present");
+  Assert.equal(menuItems[2].label, "Manage Extension\u2026",
+               "'Manage' item is present");
+  contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+  EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
+  await contextMenuPromise;
+
+  // The action should be added back to the urlbar.
+  await BrowserTestUtils.waitForCondition(() => {
+    return BrowserPageActions.urlbarButtonNodeForActionID(action.id);
+  }, "Waiting for urlbar button to be added back");
+
+  // Open the context menu again on the action's button in the panel.  (The
+  // panel should still be open.)
+  contextMenuPromise = promisePanelShown("pageActionContextMenu");
+  EventUtils.synthesizeMouseAtCenter(panelButton, {
+    type: "contextmenu",
+    button: 2,
+  });
+  await contextMenuPromise;
+
+  // The context menu should show the "don't show" item and the "manage" item.
+  // Click the "manage" item.  about:addons should open.
+  menuItems = collectContextMenuItems();
+  Assert.equal(menuItems.length, 3,
+               "Context menu has 3 children");
+  Assert.equal(menuItems[0].label, "Don\u2019t Show in Address Bar",
+               "Context menu is in the 'don't show' state");
+  Assert.equal(menuItems[1].localName, "menuseparator",
+               "menuseparator is present");
+  Assert.equal(menuItems[2].label, "Manage Extension\u2026",
+               "'Manage' item is present");
+  contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+  let aboutAddonsPromise =
+    BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
+  EventUtils.synthesizeMouseAtCenter(menuItems[2], {});
+  let values = await Promise.all([aboutAddonsPromise, contextMenuPromise]);
+  let aboutAddonsTab = values[0];
+  await BrowserTestUtils.removeTab(aboutAddonsTab);
+
+  // Open the context menu on the action's urlbar button.
+  let urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(action.id);
+  contextMenuPromise = promisePanelShown("pageActionContextMenu");
+  EventUtils.synthesizeMouseAtCenter(urlbarButton, {
+    type: "contextmenu",
+    button: 2,
+  });
+  await contextMenuPromise;
+
+  // The context menu should show the "don't show" item and the "manage" item.
+  // Click the "don't show" item.
+  menuItems = collectContextMenuItems();
+  Assert.equal(menuItems.length, 3,
+               "Context menu has 3 children");
+  Assert.equal(menuItems[0].label, "Don\u2019t Show in Address Bar",
+               "Context menu is in the 'don't show' state");
+  Assert.equal(menuItems[1].localName, "menuseparator",
+               "menuseparator is present");
+  Assert.equal(menuItems[2].label, "Manage Extension\u2026",
+               "'Manage' item is present");
+  contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+  EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
+  await contextMenuPromise;
+
+  // The action should be removed from the urlbar.
+  await BrowserTestUtils.waitForCondition(() => {
+    return !BrowserPageActions.urlbarButtonNodeForActionID(action.id);
+  }, "Waiting for urlbar button to be removed");
+
+  // Open the panel and then open the context menu on the action's item.
+  await promisePageActionPanelOpen();
+  contextMenuPromise = promisePanelShown("pageActionContextMenu");
+  EventUtils.synthesizeMouseAtCenter(panelButton, {
+    type: "contextmenu",
+    button: 2,
+  });
+  await contextMenuPromise;
+
+  // The context menu should show the "show" item and the "manage" item.  Click
+  // the "show" item.
+  menuItems = collectContextMenuItems();
+  Assert.equal(menuItems.length, 3,
+               "Context menu has 3 children");
+  Assert.equal(menuItems[0].label, "Show in Address Bar",
+               "Context menu is in the 'show' state");
+  Assert.equal(menuItems[1].localName, "menuseparator",
+               "menuseparator is present");
+  Assert.equal(menuItems[2].label, "Manage Extension\u2026",
+               "'Manage' item is present");
+  contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+  EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
+  await contextMenuPromise;
+
+  // The action should be added back to the urlbar.
+  await BrowserTestUtils.waitForCondition(() => {
+    return BrowserPageActions.urlbarButtonNodeForActionID(action.id);
+  }, "Waiting for urlbar button to be added back");
+
+  // Open the context menu on the action's urlbar button.
+  urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(action.id);
+  contextMenuPromise = promisePanelShown("pageActionContextMenu");
+  EventUtils.synthesizeMouseAtCenter(urlbarButton, {
+    type: "contextmenu",
+    button: 2,
+  });
+  await contextMenuPromise;
+
+  // The context menu should show the "don't show" item and the "manage" item.
+  // Click the "manage" item.  about:addons should open.
+  menuItems = collectContextMenuItems();
+  Assert.equal(menuItems.length, 3,
+               "Context menu has 3 children");
+  Assert.equal(menuItems[0].label, "Don\u2019t Show in Address Bar",
+               "Context menu is in the 'don't show' state");
+  Assert.equal(menuItems[1].localName, "menuseparator",
+               "menuseparator is present");
+  Assert.equal(menuItems[2].label, "Manage Extension\u2026",
+               "'Manage' item is present");
+  contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+  aboutAddonsPromise =
+    BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
+  EventUtils.synthesizeMouseAtCenter(menuItems[2], {});
+  values = await Promise.all([aboutAddonsPromise, contextMenuPromise]);
+  aboutAddonsTab = values[0];
+  await BrowserTestUtils.removeTab(aboutAddonsTab);
+
+  // Done, clean up.
+  action.remove();
+
+  // urlbar tests that run after this one can break if the mouse is left over
+  // the area where the urlbar popup appears, which seems to happen due to the
+  // above synthesized mouse events.  Move it over the urlbar.
+  EventUtils.synthesizeMouseAtCenter(gURLBar, { type: "mousemove" });
+  gURLBar.focus();
+});
+
+
 function promisePageActionPanelOpen() {
   let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
                   .getInterface(Ci.nsIDOMWindowUtils);
   return BrowserTestUtils.waitForCondition(() => {
     // Wait for the main page action button to become visible.  It's hidden for
     // some URIs, so depending on when this is called, it may not yet be quite
     // visible.  It's up to the caller to make sure it will be visible.
     info("Waiting for main page action button to have non-0 size");
@@ -1230,8 +1492,15 @@ function promisePageActionViewChildrenVi
       let bounds = dwu.getBoundsWithoutFlushing(childNode);
       if (bounds.width > 0 && bounds.height > 0) {
         return true;
       }
     }
     return false;
   });
 }
+
+function collectContextMenuItems() {
+  let contextMenu = document.getElementById("pageActionContextMenu");
+  return Array.filter(contextMenu.childNodes, node => {
+    return window.getComputedStyle(node).visibility == "visible";
+  });
+}
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -6596,16 +6596,26 @@
     "alert_emails": ["gijs@mozilla.com"],
     "bug_numbers": [1393843],
     "expires_in_version": "60",
     "kind": "categorical",
     "labels": ["bookmark", "pocket", "screenshots", "webcompat", "copyURL", "emailLink",
                "sendToDevice", "other"],
     "description": "Count how many times people remove items from the url bar"
   },
+  "FX_PAGE_ACTION_MANAGED": {
+    "record_in_processes": ["main"],
+    "alert_emails": ["gijs@mozilla.com"],
+    "bug_numbers": [1393843],
+    "expires_in_version": "60",
+    "kind": "categorical",
+    "labels": ["bookmark", "pocket", "screenshots", "webcompat", "copyURL", "emailLink",
+               "sendToDevice", "other"],
+    "description": "Count how many times people manage extensions via their actions in the url bar"
+  },
   "FX_PAGE_ACTION_URLBAR_USED": {
     "record_in_processes": ["main"],
     "alert_emails": ["gijs@mozilla.com"],
     "bug_numbers": [1393843],
     "expires_in_version": "60",
     "kind": "categorical",
     "labels": ["bookmark", "pocket", "screenshots", "webcompat", "copyURL", "emailLink",
                "sendToDevice", "other"],