Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.

Commit 99ef23c

Browse files
committed
Bug 1948261 - Add "Pin extension to toolbar" checkbox in postinstall r=zombie,fluent-reviewers,bolsson
This adds the "Pin extension to toolbar" checkbox to the post-install doorhanger, and ensures that the checkbox has a state that reflects the actual placement of the button, including externally triggered changes. Differential Revision: https://phabricator.services.mozilla.com/D250324
1 parent 9ad8373 commit 99ef23c

File tree

5 files changed

+472
-1
lines changed

5 files changed

+472
-1
lines changed

browser/base/content/browser-addons.js

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const lazy = {};
1515
ChromeUtils.defineESModuleGetters(lazy, {
1616
AMBrowserExtensionsImport: "resource://gre/modules/AddonManager.sys.mjs",
1717
AbuseReporter: "resource://gre/modules/AbuseReporter.sys.mjs",
18+
ExtensionCommon: "resource://gre/modules/ExtensionCommon.sys.mjs",
1819
ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
1920
ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs",
2021
OriginControls: "resource://gre/modules/ExtensionPermissions.sys.mjs",
@@ -737,19 +738,104 @@ customElements.define(
737738
}
738739
);
739740

741+
class BrowserActionWidgetObserver {
742+
#connected = false;
743+
/**
744+
* @param {string} addonId The ID of the extension
745+
* @param {function()} onButtonAreaChanged Callback that is called whenever
746+
* the observer detects the presence, absence or relocation of the browser
747+
* action button for the given extension.
748+
*/
749+
constructor(addonId, onButtonAreaChanged) {
750+
this.addonId = addonId;
751+
// The expected ID of the browserAction widget. Keep in sync with
752+
// actionWidgetId logic in ext-browserAction.js.
753+
this.widgetId = `${lazy.ExtensionCommon.makeWidgetId(addonId)}-browser-action`;
754+
this.onButtonAreaChanged = onButtonAreaChanged;
755+
}
756+
757+
startObserving() {
758+
if (this.#connected) {
759+
return;
760+
}
761+
this.#connected = true;
762+
CustomizableUI.addListener(this);
763+
window.addEventListener("unload", this);
764+
}
765+
766+
stopObserving() {
767+
if (!this.#connected) {
768+
return;
769+
}
770+
this.#connected = false;
771+
CustomizableUI.removeListener(this);
772+
window.removeEventListener("unload", this);
773+
}
774+
775+
hasBrowserActionUI() {
776+
const policy = WebExtensionPolicy.getByID(this.addonId);
777+
if (!policy?.canAccessWindow(window)) {
778+
// Add-on is not an extension, or extension has not started yet. Or it
779+
// was uninstalled/disabled. Or disabled in current (private) window.
780+
return false;
781+
}
782+
if (!gUnifiedExtensions.browserActionFor(policy)) {
783+
// Does not have a browser action button.
784+
return false;
785+
}
786+
return true;
787+
}
788+
789+
onWidgetCreated(aWidgetId) {
790+
// This is triggered as soon as ext-browserAction registers the button,
791+
// shortly after hasBrowserActionUI() above can return true for the first
792+
// time since add-on installation.
793+
if (aWidgetId === this.widgetId) {
794+
this.onButtonAreaChanged();
795+
}
796+
}
797+
798+
onWidgetAdded(aWidgetId) {
799+
if (aWidgetId === this.widgetId) {
800+
this.onButtonAreaChanged();
801+
}
802+
}
803+
804+
onWidgetMoved(aWidgetId) {
805+
if (aWidgetId === this.widgetId) {
806+
this.onButtonAreaChanged();
807+
}
808+
}
809+
810+
handleEvent(event) {
811+
if (event.type === "unload") {
812+
this.stopObserving();
813+
}
814+
}
815+
}
816+
740817
customElements.define(
741818
"addon-installed-notification",
742819
class MozAddonInstalledNotification extends customElements.get(
743820
"popupnotification"
744821
) {
822+
#shouldIgnoreCheckboxStateChangeEvent = false;
823+
#browserActionWidgetObserver;
745824
connectedCallback() {
746825
this.descriptionEl = this.querySelector("#addon-install-description");
826+
this.pinExtensionEl = this.querySelector(
827+
"#addon-pin-toolbarbutton-checkbox"
828+
);
747829

748830
this.addEventListener("click", this);
831+
this.pinExtensionEl.addEventListener("CheckboxStateChange", this);
832+
this.#browserActionWidgetObserver?.startObserving();
749833
}
750834

751835
disconnectedCallback() {
752836
this.removeEventListener("click", this);
837+
this.pinExtensionEl.removeEventListener("CheckboxStateChange", this);
838+
this.#browserActionWidgetObserver?.stopObserving();
753839
}
754840

755841
get #settingsLinkId() {
@@ -763,13 +849,19 @@ customElements.define(
763849
case "click": {
764850
if (target.id === this.#settingsLinkId) {
765851
const { addonId } = this.notification.options.customElementOptions;
766-
767852
BrowserAddonUI.openAddonsMgr(
768853
"addons://detail/" + encodeURIComponent(addonId)
769854
);
770855
}
771856
break;
772857
}
858+
case "CheckboxStateChange":
859+
// CheckboxStateChange fires whenever the checked value changes.
860+
// Ignore the event if triggered by us instead of the user.
861+
if (!this.#shouldIgnoreCheckboxStateChangeEvent) {
862+
this.#handlePinnedCheckboxStateChange();
863+
}
864+
break;
773865
}
774866
}
775867

@@ -786,7 +878,16 @@ customElements.define(
786878
);
787879
}
788880

881+
this.#browserActionWidgetObserver?.stopObserving();
882+
this.#browserActionWidgetObserver = new BrowserActionWidgetObserver(
883+
this.notification.options.customElementOptions.addonId,
884+
() => this.#renderPinToolbarButtonCheckbox()
885+
);
886+
789887
this.render();
888+
if (this.isConnected) {
889+
this.#browserActionWidgetObserver.startObserving();
890+
}
790891
}
791892

792893
render() {
@@ -808,6 +909,7 @@ customElements.define(
808909
}
809910

810911
this.ownerDocument.l10n.setAttributes(this.descriptionEl, fluentId);
912+
this.#renderPinToolbarButtonCheckbox();
811913
}
812914

813915
get #dataCollectionPermissionsEnabled() {
@@ -816,6 +918,50 @@ customElements.define(
816918
false
817919
);
818920
}
921+
922+
#renderPinToolbarButtonCheckbox() {
923+
// If the extension has a browser action, show the checkbox to allow the
924+
// user to customize its location. Hide by default until we know for
925+
// certain that the conditions have been met.
926+
this.pinExtensionEl.hidden = true;
927+
928+
if (!this.#browserActionWidgetObserver.hasBrowserActionUI()) {
929+
return;
930+
}
931+
const widgetId = this.#browserActionWidgetObserver.widgetId;
932+
933+
// Extension buttons appear in AREA_ADDONS by default. There are several
934+
// ways for the default to differ for a specific add-on, including the
935+
// extension specifying default_area in its manifest.json file, an
936+
// enterprise policy having been configured, or the user having moved the
937+
// button someplace else. We only show the checkbox if it is either in
938+
// AREA_ADDONS or in the toolbar. This covers almost all common cases.
939+
const area = CustomizableUI.getPlacementOfWidget(widgetId)?.area;
940+
let shouldPinToToolbar = area !== CustomizableUI.AREA_ADDONS;
941+
if (shouldPinToToolbar && area !== CustomizableUI.AREA_NAVBAR) {
942+
// We only support AREA_ADDONS and AREA_NAVBAR for now.
943+
return;
944+
}
945+
this.#shouldIgnoreCheckboxStateChangeEvent = true;
946+
this.pinExtensionEl.checked = shouldPinToToolbar;
947+
this.#shouldIgnoreCheckboxStateChangeEvent = false;
948+
this.pinExtensionEl.hidden = false;
949+
}
950+
951+
#handlePinnedCheckboxStateChange() {
952+
if (!this.#browserActionWidgetObserver.hasBrowserActionUI()) {
953+
// Unexpected. #renderPinToolbarButtonCheckbox() should have hidden
954+
// the checkbox if there is no widget.
955+
const { addonId } = this.notification.options.customElementOptions;
956+
throw new Error(`No browser action widget found for ${addonId}!`);
957+
}
958+
const widgetId = this.#browserActionWidgetObserver.widgetId;
959+
const shouldPinToToolbar = this.pinExtensionEl.checked;
960+
if (shouldPinToToolbar) {
961+
gUnifiedExtensions._maybeMoveWidgetNodeBack(widgetId);
962+
}
963+
gUnifiedExtensions.pinToToolbar(widgetId, shouldPinToToolbar);
964+
}
819965
},
820966
{ extends: "popupnotification" }
821967
);

browser/components/customizableui/content/panelUI.inc.xhtml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@
225225
hidden="true">
226226
<popupnotificationcontent class="addon-installed-notification-content" orient="vertical">
227227
<description id="addon-install-description"></description>
228+
<checkbox id="addon-pin-toolbarbutton-checkbox"
229+
data-lazy-l10n-id="appmenu-addon-post-install-pin-toolbarbutton-checkbox"/>
228230
</popupnotificationcontent>
229231
</popupnotification>
230232

browser/components/extensions/test/browser/browser.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,8 @@ skip-if = [
762762

763763
["browser_unified_extensions_doorhangers.js"]
764764

765+
["browser_unified_extensions_doorhangers_postinstall.js"]
766+
765767
["browser_unified_extensions_item_messagebar.js"]
766768

767769
["browser_unified_extensions_messages.js"]

0 commit comments

Comments
 (0)