forked from mirrors/gecko-dev
This version addresses some popup sizing bugs, and also a few other issues I ran into when debugging Blake's problems: * The standalone popup needs a max width of 800px for Chrome compatibility, which is wider than our default max width. * I added a flex attribute to our browser so that it fills the entire space of the slide-in panel. This is only necessary for browsers with content that is shorter than the height of the panel when it gets its desired width, but becomes longer when it doesn't, so it didn't show up in my initial tests. * I also added an extra pixel to the width calculations, since I noticed that a lot of single lines of text were unexpectedly wrapping without it. I'll look into this more in a follow-up bug. I also added some comments, and renamed a couple of variables, where things seemed unclear. The test changes are mostly just updates to older browser action tests to use newer helpers, rather than ad-hoc events, to open/close/click the widgets. A few tests also needed updates to explicitly close the panel when they were done with it. --HG-- extra : commitid : BhHv1aZSrBL extra : rebase_source : 6fe0069ebd2f94b0ffd02ad757a799fbdff9226e extra : source : 628af985c7eba7e4665a1b62f9a60f6a6fd39822
316 lines
10 KiB
JavaScript
316 lines
10 KiB
JavaScript
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set sts=2 sw=2 et tw=80: */
|
|
"use strict";
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
|
|
"resource:///modules/CustomizableUI.jsm");
|
|
|
|
Cu.import("resource://devtools/shared/event-emitter.js");
|
|
|
|
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
|
var {
|
|
EventManager,
|
|
runSafe,
|
|
} = ExtensionUtils;
|
|
|
|
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
|
|
// WeakMap[Extension -> BrowserAction]
|
|
var browserActionMap = new WeakMap();
|
|
|
|
function browserActionOf(extension) {
|
|
return browserActionMap.get(extension);
|
|
}
|
|
|
|
// Responsible for the browser_action section of the manifest as well
|
|
// as the associated popup.
|
|
function BrowserAction(options, extension) {
|
|
this.extension = extension;
|
|
|
|
let widgetId = makeWidgetId(extension.id);
|
|
this.id = `${widgetId}-browser-action`;
|
|
this.viewId = `PanelUI-webext-${widgetId}-browser-action-view`;
|
|
this.widget = null;
|
|
|
|
this.tabManager = TabManager.for(extension);
|
|
|
|
let title = extension.localize(options.default_title || "");
|
|
let popup = extension.localize(options.default_popup || "");
|
|
if (popup) {
|
|
popup = extension.baseURI.resolve(popup);
|
|
}
|
|
|
|
this.defaults = {
|
|
enabled: true,
|
|
title: title || extension.name,
|
|
badgeText: "",
|
|
badgeBackgroundColor: null,
|
|
icon: IconDetails.normalize({ path: options.default_icon }, extension,
|
|
null, true),
|
|
popup: popup,
|
|
};
|
|
|
|
this.tabContext = new TabContext(tab => Object.create(this.defaults),
|
|
extension);
|
|
|
|
EventEmitter.decorate(this);
|
|
}
|
|
|
|
BrowserAction.prototype = {
|
|
build() {
|
|
let widget = CustomizableUI.createWidget({
|
|
id: this.id,
|
|
viewId: this.viewId,
|
|
type: "view",
|
|
removable: true,
|
|
label: this.defaults.title || this.extension.name,
|
|
tooltiptext: this.defaults.title || "",
|
|
defaultArea: CustomizableUI.AREA_NAVBAR,
|
|
|
|
onBeforeCreated: document => {
|
|
let view = document.createElementNS(XUL_NS, "panelview");
|
|
view.id = this.viewId;
|
|
view.setAttribute("flex", "1");
|
|
|
|
document.getElementById("PanelUI-multiView").appendChild(view);
|
|
},
|
|
|
|
onDestroyed: document => {
|
|
let view = document.getElementById(this.viewId);
|
|
if (view) {
|
|
view.remove();
|
|
}
|
|
},
|
|
|
|
onCreated: node => {
|
|
node.classList.add("badged-button");
|
|
node.setAttribute("constrain-size", "true");
|
|
|
|
this.updateButton(node, this.defaults);
|
|
},
|
|
|
|
onViewShowing: event => {
|
|
let document = event.target.ownerDocument;
|
|
let tabbrowser = document.defaultView.gBrowser;
|
|
|
|
let tab = tabbrowser.selectedTab;
|
|
let popupURL = this.getProperty(tab, "popup");
|
|
this.tabManager.addActiveTabPermission(tab);
|
|
|
|
// If the widget has a popup URL defined, we open a popup, but do not
|
|
// dispatch a click event to the extension.
|
|
// If it has no popup URL defined, we dispatch a click event, but do not
|
|
// open a popup.
|
|
if (popupURL) {
|
|
try {
|
|
new ViewPopup(this.extension, event.target, popupURL);
|
|
} catch (e) {
|
|
Cu.reportError(e);
|
|
event.preventDefault();
|
|
}
|
|
} else {
|
|
// This isn't not a hack, but it seems to provide the correct behavior
|
|
// with the fewest complications.
|
|
event.preventDefault();
|
|
this.emit("click");
|
|
}
|
|
},
|
|
});
|
|
|
|
this.tabContext.on("tab-select", // eslint-disable-line mozilla/balanced-listeners
|
|
(evt, tab) => { this.updateWindow(tab.ownerDocument.defaultView); });
|
|
|
|
this.widget = widget;
|
|
},
|
|
|
|
// Update the toolbar button |node| with the tab context data
|
|
// in |tabData|.
|
|
updateButton(node, tabData) {
|
|
let title = tabData.title || this.extension.name;
|
|
node.setAttribute("tooltiptext", title);
|
|
node.setAttribute("label", title);
|
|
|
|
if (tabData.badgeText) {
|
|
node.setAttribute("badge", tabData.badgeText);
|
|
} else {
|
|
node.removeAttribute("badge");
|
|
}
|
|
|
|
if (tabData.enabled) {
|
|
node.removeAttribute("disabled");
|
|
} else {
|
|
node.setAttribute("disabled", "true");
|
|
}
|
|
|
|
let badgeNode = node.ownerDocument.getAnonymousElementByAttribute(node,
|
|
"class", "toolbarbutton-badge");
|
|
if (badgeNode) {
|
|
let color = tabData.badgeBackgroundColor;
|
|
if (Array.isArray(color)) {
|
|
color = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
|
|
}
|
|
badgeNode.style.backgroundColor = color || "";
|
|
}
|
|
|
|
let iconURL = IconDetails.getURL(
|
|
tabData.icon, node.ownerDocument.defaultView, this.extension);
|
|
node.setAttribute("image", iconURL);
|
|
},
|
|
|
|
// Update the toolbar button for a given window.
|
|
updateWindow(window) {
|
|
let widget = this.widget.forWindow(window);
|
|
if (widget) {
|
|
let tab = window.gBrowser.selectedTab;
|
|
this.updateButton(widget.node, this.tabContext.get(tab));
|
|
}
|
|
},
|
|
|
|
// Update the toolbar button when the extension changes the icon,
|
|
// title, badge, etc. If it only changes a parameter for a single
|
|
// tab, |tab| will be that tab. Otherwise it will be null.
|
|
updateOnChange(tab) {
|
|
if (tab) {
|
|
if (tab.selected) {
|
|
this.updateWindow(tab.ownerDocument.defaultView);
|
|
}
|
|
} else {
|
|
for (let window of WindowListManager.browserWindows()) {
|
|
this.updateWindow(window);
|
|
}
|
|
}
|
|
},
|
|
|
|
// tab is allowed to be null.
|
|
// prop should be one of "icon", "title", "badgeText", "popup", or "badgeBackgroundColor".
|
|
setProperty(tab, prop, value) {
|
|
if (tab == null) {
|
|
this.defaults[prop] = value;
|
|
} else if (value != null) {
|
|
this.tabContext.get(tab)[prop] = value;
|
|
} else {
|
|
delete this.tabContext.get(tab)[prop];
|
|
}
|
|
|
|
this.updateOnChange(tab);
|
|
},
|
|
|
|
// tab is allowed to be null.
|
|
// prop should be one of "title", "badgeText", "popup", or "badgeBackgroundColor".
|
|
getProperty(tab, prop) {
|
|
if (tab == null) {
|
|
return this.defaults[prop];
|
|
} else {
|
|
return this.tabContext.get(tab)[prop];
|
|
}
|
|
},
|
|
|
|
shutdown() {
|
|
this.tabContext.shutdown();
|
|
CustomizableUI.destroyWidget(this.id);
|
|
},
|
|
};
|
|
|
|
/* eslint-disable mozilla/balanced-listeners */
|
|
extensions.on("manifest_browser_action", (type, directive, extension, manifest) => {
|
|
let browserAction = new BrowserAction(manifest.browser_action, extension);
|
|
browserAction.build();
|
|
browserActionMap.set(extension, browserAction);
|
|
});
|
|
|
|
extensions.on("shutdown", (type, extension) => {
|
|
if (browserActionMap.has(extension)) {
|
|
browserActionMap.get(extension).shutdown();
|
|
browserActionMap.delete(extension);
|
|
}
|
|
});
|
|
/* eslint-enable mozilla/balanced-listeners */
|
|
|
|
extensions.registerSchemaAPI("browserAction", null, (extension, context) => {
|
|
return {
|
|
browserAction: {
|
|
onClicked: new EventManager(context, "browserAction.onClicked", fire => {
|
|
let listener = () => {
|
|
let tab = TabManager.activeTab;
|
|
fire(TabManager.convert(extension, tab));
|
|
};
|
|
browserActionOf(extension).on("click", listener);
|
|
return () => {
|
|
browserActionOf(extension).off("click", listener);
|
|
};
|
|
}).api(),
|
|
|
|
enable: function(tabId) {
|
|
let tab = tabId !== null ? TabManager.getTab(tabId) : null;
|
|
browserActionOf(extension).setProperty(tab, "enabled", true);
|
|
},
|
|
|
|
disable: function(tabId) {
|
|
let tab = tabId !== null ? TabManager.getTab(tabId) : null;
|
|
browserActionOf(extension).setProperty(tab, "enabled", false);
|
|
},
|
|
|
|
setTitle: function(details) {
|
|
let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
|
|
|
|
let title = details.title;
|
|
// Clear the tab-specific title when given a null string.
|
|
if (tab && title == "") {
|
|
title = null;
|
|
}
|
|
browserActionOf(extension).setProperty(tab, "title", title);
|
|
},
|
|
|
|
getTitle: function(details, callback) {
|
|
let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
|
|
let title = browserActionOf(extension).getProperty(tab, "title");
|
|
runSafe(context, callback, title);
|
|
},
|
|
|
|
setIcon: function(details, callback) {
|
|
let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
|
|
let icon = IconDetails.normalize(details, extension, context);
|
|
browserActionOf(extension).setProperty(tab, "icon", icon);
|
|
},
|
|
|
|
setBadgeText: function(details) {
|
|
let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
|
|
browserActionOf(extension).setProperty(tab, "badgeText", details.text);
|
|
},
|
|
|
|
getBadgeText: function(details, callback) {
|
|
let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
|
|
let text = browserActionOf(extension).getProperty(tab, "badgeText");
|
|
runSafe(context, callback, text);
|
|
},
|
|
|
|
setPopup: function(details) {
|
|
let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
|
|
// Note: Chrome resolves arguments to setIcon relative to the calling
|
|
// context, but resolves arguments to setPopup relative to the extension
|
|
// root.
|
|
// For internal consistency, we currently resolve both relative to the
|
|
// calling context.
|
|
let url = details.popup && context.uri.resolve(details.popup);
|
|
browserActionOf(extension).setProperty(tab, "popup", url);
|
|
},
|
|
|
|
getPopup: function(details, callback) {
|
|
let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
|
|
let popup = browserActionOf(extension).getProperty(tab, "popup");
|
|
runSafe(context, callback, popup);
|
|
},
|
|
|
|
setBadgeBackgroundColor: function(details) {
|
|
let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
|
|
browserActionOf(extension).setProperty(tab, "badgeBackgroundColor", details.color);
|
|
},
|
|
|
|
getBadgeBackgroundColor: function(details, callback) {
|
|
let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
|
|
let color = browserActionOf(extension).getProperty(tab, "badgeBackgroundColor");
|
|
runSafe(context, callback, color);
|
|
},
|
|
},
|
|
};
|
|
});
|