Bug 1616143 - Allow setting of properties per tab on action buttons. r=mkmelin

--HG--
extra : rebase_source : 188814385f1072d4761fc6846e3e8d3bec47633c
extra : histedit_source : 8992fd666c41be66bb175d4cb5811d299d0a7ebb
This commit is contained in:
Geoff Lankow 2020-02-17 23:23:30 +13:00
Родитель 84ff495b42
Коммит 71f9fa4ccf
9 изменённых файлов: 562 добавлений и 39 удалений

Просмотреть файл

@ -43,6 +43,14 @@ XPCOMUtils.defineLazyGlobalGetters(this, ["InspectorUtils"]);
var DEFAULT_ICON = "chrome://messenger/content/extension.svg";
this.ToolbarButtonAPI = class extends ExtensionAPI {
constructor(extension, global) {
super(extension);
this.global = global;
this.tabContext = new this.global.TabContext(target =>
this.getContextData(null)
);
}
/**
* Called when the extension is enabled.
*
@ -235,8 +243,9 @@ this.ToolbarButtonAPI = class extends ExtensionAPI {
async triggerAction(window) {
let { document } = window;
let button = document.getElementById(this.id);
let popupURL = this.getProperty(this.globals, "popup");
let enabled = this.getProperty(this.globals, "enabled");
let { popup: popupURL, enabled } = this.getContextData(
this.getTargetFromWindow(window)
);
if (button && popupURL && enabled) {
let popup =
@ -262,6 +271,9 @@ this.ToolbarButtonAPI = class extends ExtensionAPI {
this.triggerAction(window);
}
break;
case "TabSelect":
this.updateWindow(window);
break;
}
}
@ -428,7 +440,10 @@ this.ToolbarButtonAPI = class extends ExtensionAPI {
async updateWindow(window) {
let button = window.document.getElementById(this.id);
if (button) {
this.updateButton(button, this.globals);
this.updateButton(
button,
this.getContextData(this.getTargetFromWindow(window))
);
}
await new Promise(window.requestAnimationFrame);
}
@ -444,7 +459,7 @@ this.ToolbarButtonAPI = class extends ExtensionAPI {
*/
async updateOnChange(target) {
if (target) {
let window = target.ownerGlobal;
let window = Cu.getGlobalForObject(target);
if (target === window || target.selected) {
await this.updateWindow(window);
}
@ -460,38 +475,55 @@ this.ToolbarButtonAPI = class extends ExtensionAPI {
}
/**
* Gets the target object and its associated values corresponding to
* the `details` parameter of the various get* and set* API methods.
* Gets the active tab of the passed window if the window has tabs, or the
* window itself.
*
* @param {ChromeWindow} window
* @returns {XULElement|ChromeWindow}
*/
getTargetFromWindow(window) {
let tabmail = window.document.getElementById("tabmail");
if (tabmail) {
return tabmail.currentTabInfo;
}
return window;
}
/**
* Gets the target object corresponding to the `details` parameter of the various
* get* and set* API methods.
*
* @param {Object} details
* An object with optional `tabId` or `windowId` properties.
* @throws if both `tabId` and `windowId` are specified, or if they are invalid.
* @returns {Object}
* An object with two properties: `target` and `values`.
* - If a `tabId` was specified, `target` will be the corresponding
* XULElement tab. If a `windowId` was specified, `target` will be
* the corresponding ChromeWindow. Otherwise it will be `null`.
* - `values` will contain the icon, title, badge, etc. associated with
* the target.
* @throws if `windowId` is specified, this is not valid in Thunderbird.
* @returns {XULElement|ChromeWindow|null}
* If a `tabId` was specified, the corresponding XULElement tab.
* If a `windowId` was specified, the corresponding ChromeWindow.
* Otherwise, `null`.
*/
getContextData({ tabId, windowId }) {
if (tabId != null && windowId != null) {
throw new ExtensionError(
"Only one of tabId and windowId can be specified."
);
getTargetFromDetails({ tabId, windowId }) {
if (windowId != null) {
throw new ExtensionError("windowId is not allowed, use tabId instead.");
}
let target, values;
// if (tabId != null) {
// target = tabTracker.getTab(tabId);
// values = this.tabContext.get(target);
// } else if (windowId != null) {
// target = windowTracker.getWindow(windowId);
// values = this.tabContext.get(target);
// } else {
target = null;
values = this.globals;
// }
return { target, values };
if (tabId != null) {
return this.global.tabTracker.getTab(tabId);
}
return null;
}
/**
* Gets the data associated with a tab, window, or the global one.
*
* @param {XULElement|ChromeWindow|null} target
* A XULElement tab, a ChromeWindow, or null for the global data.
* @returns {Object}
* The icon, title, badge, etc. associated with the target.
*/
getContextData(target) {
if (target) {
return this.tabContext.get(target);
}
return this.globals;
}
/**
@ -506,7 +538,8 @@ this.ToolbarButtonAPI = class extends ExtensionAPI {
* Value for prop.
*/
async setProperty(details, prop, value) {
let { target, values } = this.getContextData(details);
let target = this.getTargetFromDetails(details);
let values = this.getContextData(target);
if (value === null) {
delete values[prop];
} else {
@ -528,7 +561,7 @@ this.ToolbarButtonAPI = class extends ExtensionAPI {
* Value of prop.
*/
getProperty(details, prop) {
return this.getContextData(details).values[prop];
return this.getContextData(this.getTargetFromDetails(details))[prop];
}
/**

Просмотреть файл

@ -23,6 +23,7 @@ this.browserAction = class extends ToolbarButtonAPI {
close() {
super.close();
browserActionMap.delete(this.extension);
windowTracker.removeListener("TabSelect", this);
}
static onUninstall(extensionId) {
@ -49,13 +50,14 @@ this.browserAction = class extends ToolbarButtonAPI {
}
constructor(extension) {
super(extension);
super(extension, global);
this.manifest_name = "browser_action";
this.manifestName = "browserAction";
this.windowURLs = ["chrome://messenger/content/messenger.xhtml"];
this.toolboxId = "mail-toolbox";
this.toolbarId = "mail-bar3";
this.global = global;
windowTracker.addListener("TabSelect", this);
}
};

Просмотреть файл

@ -10,7 +10,7 @@ ChromeUtils.defineModuleGetter(
this.composeAction = class extends ToolbarButtonAPI {
constructor(extension) {
super(extension);
super(extension, global);
this.manifest_name = "compose_action";
this.manifestName = "composeAction";
this.windowURLs = [
@ -25,8 +25,6 @@ this.composeAction = class extends ToolbarButtonAPI {
if (format) {
this.paint = this.paintFormatToolbar;
}
this.global = global;
}
paintFormatToolbar(window) {

Просмотреть файл

@ -164,6 +164,50 @@ function getTabBrowser(nativeTabInfo) {
}
global.getTabBrowser = getTabBrowser;
/**
* Manages tab-specific and window-specific context data, and dispatches
* tab select events across all windows.
*/
global.TabContext = class extends EventEmitter {
/**
* @param {Function} getDefaultPrototype
* Provides the prototype of the context value for a tab or window when there is none.
* Called with a XULElement or ChromeWindow argument.
* Should return an object or null.
*/
constructor(getDefaultPrototype) {
super();
this.getDefaultPrototype = getDefaultPrototype;
this.tabData = new WeakMap();
}
/**
* Returns the context data associated with `keyObject`.
*
* @param {XULElement|ChromeWindow} keyObject
* Browser tab or browser chrome window.
* @returns {Object}
*/
get(keyObject) {
if (!this.tabData.has(keyObject)) {
let data = Object.create(this.getDefaultPrototype(keyObject));
this.tabData.set(keyObject, data);
}
return this.tabData.get(keyObject);
}
/**
* Clears the context data associated with `keyObject`.
*
* @param {XULElement|ChromeWindow} keyObject
* Browser tab or browser chrome window.
*/
clear(keyObject) {
this.tabData.delete(keyObject);
}
};
/* global searchInitialized */
// This promise is used to wait for the search service to be initialized.
// None of the code in the WebExtension modules requests that initialization.

Просмотреть файл

@ -25,10 +25,11 @@ this.messageDisplayAction = class extends ToolbarButtonAPI {
close() {
super.close();
messageDisplayActionMap.delete(this.extension);
windowTracker.removeListener("TabSelect", this);
}
constructor(extension) {
super(extension);
super(extension, global);
this.manifest_name = "message_display_action";
this.manifestName = "messageDisplayAction";
this.windowURLs = [
@ -37,6 +38,8 @@ this.messageDisplayAction = class extends ToolbarButtonAPI {
];
this.toolboxId = "header-view-toolbox";
this.toolbarId = "header-view-toolbar";
windowTracker.addListener("TabSelect", this);
}
makeButton(window) {

Просмотреть файл

@ -16,6 +16,7 @@ tags = webextensions
[browser_ext_addressBooksUI.js]
[browser_ext_browserAction.js]
[browser_ext_browserAction_properties.js]
[browser_ext_commands_execute_browser_action.js]
[browser_ext_commands_getAll.js]
[browser_ext_commands_onCommand.js]
@ -24,6 +25,7 @@ tags = webextensions
[browser_ext_compose_details.js]
[browser_ext_compose_onBeforeSend.js]
[browser_ext_composeAction.js]
[browser_ext_composeAction_properties.js]
[browser_ext_mailTabs.js]
[browser_ext_menus.js]
support-files = data/content.html
@ -31,6 +33,7 @@ support-files = data/content.html
[browser_ext_menus_replace_menu_context.js]
[browser_ext_messageDisplay.js]
[browser_ext_messageDisplayAction.js]
[browser_ext_messageDisplayAction_properties.js]
[browser_ext_quickFilter.js]
[browser_ext_windows.js]
[browser_ext_windows_types.js]

Просмотреть файл

@ -0,0 +1,136 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/. */
add_task(async () => {
let account = createAccount();
addIdentity(account);
let rootFolder = account.incomingServer.rootFolder;
window.gFolderTreeView.selectFolder(rootFolder);
await new Promise(resolve => executeSoon(resolve));
let extension = ExtensionTestUtils.loadExtension({
background: async () => {
async function checkProperty(property, expectedDefault, ...expected) {
browser.test.log(
`${property}: ${expectedDefault}, ${expected.join(", ")}`
);
browser.test.assertEq(
expectedDefault,
await browser.browserAction[property]({})
);
for (let i = 0; i < 3; i++) {
browser.test.assertEq(
expected[i],
await browser.browserAction[property]({ tabId: tabIDs[i] })
);
}
await new Promise(resolve => {
browser.test.onMessage.addListener(function listener() {
browser.test.onMessage.removeListener(listener);
resolve();
});
browser.test.sendMessage("checkProperty", property, expected);
});
}
let tabs = await browser.mailTabs.query({});
let tabIDs = tabs.map(t => t.id);
await checkProperty("isEnabled", true, true, true, true);
await browser.browserAction.disable();
await checkProperty("isEnabled", false, false, false, false);
await browser.browserAction.enable(tabIDs[0]);
await checkProperty("isEnabled", false, true, false, false);
await browser.browserAction.enable();
await checkProperty("isEnabled", true, true, true, true);
await browser.browserAction.disable();
await checkProperty("isEnabled", false, true, false, false);
await browser.browserAction.disable(tabIDs[0]);
await checkProperty("isEnabled", false, false, false, false);
await browser.browserAction.enable();
await checkProperty("isEnabled", true, false, true, true);
await checkProperty(
"getTitle",
"default",
"default",
"default",
"default"
);
await browser.browserAction.setTitle({ tabId: tabIDs[2], title: "tab2" });
await checkProperty("getTitle", "default", "default", "default", "tab2");
await browser.browserAction.setTitle({ title: "new" });
await checkProperty("getTitle", "new", "new", "new", "tab2");
await browser.browserAction.setTitle({ tabId: tabIDs[1], title: "tab1" });
await checkProperty("getTitle", "new", "new", "tab1", "tab2");
await browser.browserAction.setTitle({ tabId: tabIDs[2], title: null });
await checkProperty("getTitle", "new", "new", "tab1", "new");
await browser.browserAction.setTitle({ title: null });
await checkProperty("getTitle", "default", "default", "tab1", "default");
await browser.browserAction.setTitle({ tabId: tabIDs[1], title: null });
await checkProperty(
"getTitle",
"default",
"default",
"default",
"default"
);
await browser.tabs.remove(tabIDs[0]);
await browser.tabs.remove(tabIDs[1]);
await browser.tabs.remove(tabIDs[2]);
browser.test.notifyPass("finished");
},
manifest: {
applications: {
gecko: {
id: "test1@mochi.test",
},
},
browser_action: {
default_title: "default",
},
},
});
await extension.startup();
let tabmail = document.getElementById("tabmail");
tabmail.openTab("folder", { folder: rootFolder, background: false });
tabmail.openTab("folder", { folder: rootFolder, background: false });
let mailTabs = tabmail.tabInfo;
is(mailTabs.length, 3);
let button = document.getElementById(
"test1_mochi_test-browserAction-toolbarbutton"
);
extension.onMessage("checkProperty", async (property, expected) => {
for (let i = 0; i < 3; i++) {
tabmail.switchToTab(mailTabs[i]);
await new Promise(resolve => requestAnimationFrame(resolve));
switch (property) {
case "isEnabled":
is(button.disabled, !expected[i], `button ${i} enabled state`);
break;
case "getTitle":
is(button.getAttribute("label"), expected[i], `button ${i} label`);
break;
}
}
extension.sendMessage();
});
await extension.awaitFinish("finished");
await extension.unload();
tabmail.closeTab(mailTabs[2]);
tabmail.closeTab(mailTabs[1]);
is(tabmail.tabInfo.length, 1);
});

Просмотреть файл

@ -0,0 +1,130 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/. */
add_task(async () => {
let account = createAccount();
addIdentity(account);
let rootFolder = account.incomingServer.rootFolder;
window.gFolderTreeView.selectFolder(rootFolder);
await new Promise(resolve => executeSoon(resolve));
let extension = ExtensionTestUtils.loadExtension({
background: async () => {
async function checkProperty(property, expectedDefault, ...expected) {
browser.test.log(
`${property}: ${expectedDefault}, ${expected.join(", ")}`
);
browser.test.assertEq(
expectedDefault,
await browser.composeAction[property]({})
);
for (let i = 0; i < 3; i++) {
browser.test.assertEq(
expected[i],
await browser.composeAction[property]({ tabId: tabIDs[i] })
);
}
await new Promise(resolve => {
browser.test.onMessage.addListener(function listener() {
browser.test.onMessage.removeListener(listener);
resolve();
});
browser.test.sendMessage("checkProperty", property, expected);
});
}
await browser.compose.beginNew();
await browser.compose.beginNew();
await browser.compose.beginNew();
let windows = await browser.windows.getAll({
populate: true,
windowTypes: ["messageCompose"],
});
let tabIDs = windows.map(w => w.tabs[0].id);
await checkProperty("isEnabled", true, true, true, true);
await browser.composeAction.disable();
await checkProperty("isEnabled", false, false, false, false);
await browser.composeAction.enable(tabIDs[0]);
await checkProperty("isEnabled", false, true, false, false);
await browser.composeAction.enable();
await checkProperty("isEnabled", true, true, true, true);
await browser.composeAction.disable();
await checkProperty("isEnabled", false, true, false, false);
await browser.composeAction.disable(tabIDs[0]);
await checkProperty("isEnabled", false, false, false, false);
await browser.composeAction.enable();
await checkProperty("isEnabled", true, false, true, true);
await checkProperty(
"getTitle",
"default",
"default",
"default",
"default"
);
await browser.composeAction.setTitle({ tabId: tabIDs[2], title: "tab2" });
await checkProperty("getTitle", "default", "default", "default", "tab2");
await browser.composeAction.setTitle({ title: "new" });
await checkProperty("getTitle", "new", "new", "new", "tab2");
await browser.composeAction.setTitle({ tabId: tabIDs[1], title: "tab1" });
await checkProperty("getTitle", "new", "new", "tab1", "tab2");
await browser.composeAction.setTitle({ tabId: tabIDs[2], title: null });
await checkProperty("getTitle", "new", "new", "tab1", "new");
await browser.composeAction.setTitle({ title: null });
await checkProperty("getTitle", "default", "default", "tab1", "default");
await browser.composeAction.setTitle({ tabId: tabIDs[1], title: null });
await checkProperty(
"getTitle",
"default",
"default",
"default",
"default"
);
await browser.tabs.remove(tabIDs[0]);
await browser.tabs.remove(tabIDs[1]);
await browser.tabs.remove(tabIDs[2]);
browser.test.notifyPass("finished");
},
manifest: {
applications: {
gecko: {
id: "test1@mochi.test",
},
},
compose_action: {
default_title: "default",
},
},
});
extension.onMessage("checkProperty", async (property, expected) => {
let composeWindows = [...Services.wm.getEnumerator("msgcompose")];
is(composeWindows.length, 3);
for (let i = 0; i < 3; i++) {
let button = composeWindows[i].document.getElementById(
"test1_mochi_test-composeAction-toolbarbutton"
);
switch (property) {
case "isEnabled":
is(button.disabled, !expected[i], `button ${i} enabled state`);
break;
case "getTitle":
is(button.getAttribute("label"), expected[i], `button ${i} label`);
break;
}
}
extension.sendMessage();
});
await extension.startup();
await extension.awaitFinish("finished");
await extension.unload();
});

Просмотреть файл

@ -0,0 +1,174 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/. */
add_task(async () => {
let account = createAccount();
addIdentity(account);
let rootFolder = account.incomingServer.rootFolder;
rootFolder.createSubfolder("test", null);
let folder = rootFolder.getChildNamed("test");
createMessages(folder, 1);
window.gFolderTreeView.selectFolder(folder);
window.gFolderDisplay.selectViewIndex(0);
window.MsgOpenSelectedMessages();
window.MsgOpenNewWindowForMessage();
await new Promise(resolve => executeSoon(resolve));
let extension = ExtensionTestUtils.loadExtension({
background: async () => {
async function checkProperty(property, expectedDefault, ...expected) {
browser.test.log(
`${property}: ${expectedDefault}, ${expected.join(", ")}`
);
browser.test.assertEq(
expectedDefault,
await browser.messageDisplayAction[property]({})
);
for (let i = 0; i < 3; i++) {
browser.test.assertEq(
expected[i],
await browser.messageDisplayAction[property]({ tabId: tabIDs[i] })
);
}
await new Promise(resolve => {
browser.test.onMessage.addListener(function listener() {
browser.test.onMessage.removeListener(listener);
resolve();
});
browser.test.sendMessage("checkProperty", property, expected);
});
}
let tabs = await browser.tabs.query({});
browser.test.assertEq(3, tabs.length);
let tabIDs = tabs.map(t => t.id);
await checkProperty("isEnabled", true, true, true, true);
await browser.messageDisplayAction.disable();
await checkProperty("isEnabled", false, false, false, false);
await browser.messageDisplayAction.enable(tabIDs[0]);
await checkProperty("isEnabled", false, true, false, false);
await browser.messageDisplayAction.enable();
await checkProperty("isEnabled", true, true, true, true);
await browser.messageDisplayAction.disable();
await checkProperty("isEnabled", false, true, false, false);
await browser.messageDisplayAction.disable(tabIDs[0]);
await checkProperty("isEnabled", false, false, false, false);
await browser.messageDisplayAction.enable();
await checkProperty("isEnabled", true, false, true, true);
await checkProperty(
"getTitle",
"default",
"default",
"default",
"default"
);
await browser.messageDisplayAction.setTitle({
tabId: tabIDs[2],
title: "tab2",
});
await checkProperty("getTitle", "default", "default", "default", "tab2");
await browser.messageDisplayAction.setTitle({ title: "new" });
await checkProperty("getTitle", "new", "new", "new", "tab2");
await browser.messageDisplayAction.setTitle({
tabId: tabIDs[1],
title: "tab1",
});
await checkProperty("getTitle", "new", "new", "tab1", "tab2");
await browser.messageDisplayAction.setTitle({
tabId: tabIDs[2],
title: null,
});
await checkProperty("getTitle", "new", "new", "tab1", "new");
await browser.messageDisplayAction.setTitle({ title: null });
await checkProperty("getTitle", "default", "default", "tab1", "default");
await browser.messageDisplayAction.setTitle({
tabId: tabIDs[1],
title: null,
});
await checkProperty(
"getTitle",
"default",
"default",
"default",
"default"
);
await browser.tabs.remove(tabIDs[0]);
await browser.tabs.remove(tabIDs[1]);
await browser.tabs.remove(tabIDs[2]);
browser.test.notifyPass("finished");
},
manifest: {
applications: {
gecko: {
id: "test1@mochi.test",
},
},
message_display_action: {
default_title: "default",
},
},
});
await extension.startup();
let tabmail = document.getElementById("tabmail");
let mainWindowTabs = tabmail.tabInfo;
is(mainWindowTabs.length, 2);
let mainWindowButton = document.getElementById(
"test1_mochi_test-messageDisplayAction-toolbarbutton"
);
let messageWindow = Services.wm.getMostRecentWindow("mail:messageWindow");
let messageWindowButton = messageWindow.document.getElementById(
"test1_mochi_test-messageDisplayAction-toolbarbutton"
);
extension.onMessage("checkProperty", async (property, expected) => {
function checkButton(button, expectedIndex) {
switch (property) {
case "isEnabled":
is(
button.disabled,
!expected[expectedIndex],
`button ${expectedIndex} enabled state`
);
break;
case "getTitle":
is(
button.getAttribute("label"),
expected[expectedIndex],
`button ${expectedIndex} label`
);
break;
}
}
for (let i = 0; i < 2; i++) {
tabmail.switchToTab(mainWindowTabs[i]);
await new Promise(resolve => requestAnimationFrame(resolve));
checkButton(mainWindowButton, i);
}
checkButton(messageWindowButton, 2);
extension.sendMessage();
});
await extension.awaitFinish("finished");
await extension.unload();
messageWindow.close();
tabmail.closeTab(mainWindowTabs[1]);
is(tabmail.tabInfo.length, 1);
document.getElementById("folderTree").focus();
await new Promise(resolve => executeSoon(resolve));
});