Bug 1254544 - Show clipboard commands in the synced tabs filter context menu. r=markh

MozReview-Commit-ID: 9XTYrU0xp9E

--HG--
extra : transplant_source : %08n%86%F3%C7%B7j%E2%C5%A7W%D5%91%A2Zs%A1%D9%DD%F1
This commit is contained in:
Kit Cambridge 2016-03-16 16:31:19 -07:00
Родитель 023c3d96a4
Коммит 85bc9bbd3f
3 изменённых файлов: 228 добавлений и 17 удалений

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

@ -472,6 +472,33 @@
accesskey="&syncSyncNowItem.accesskey;"
id="syncedTabsRefresh"/>
</menupopup>
<menupopup id="SyncedTabsSidebarTabsFilterContext"
class="textbox-contextmenu">
<menuitem label="&undoCmd.label;"
accesskey="&undoCmd.accesskey;"
cmd="cmd_undo"/>
<menuseparator/>
<menuitem label="&cutCmd.label;"
accesskey="&cutCmd.accesskey;"
cmd="cmd_cut"/>
<menuitem label="&copyCmd.label;"
accesskey="&copyCmd.accesskey;"
cmd="cmd_copy"/>
<menuitem label="&pasteCmd.label;"
accesskey="&pasteCmd.accesskey;"
cmd="cmd_paste"/>
<menuitem label="&deleteCmd.label;"
accesskey="&deleteCmd.accesskey;"
cmd="cmd_delete"/>
<menuseparator/>
<menuitem label="&selectAllCmd.label;"
accesskey="&selectAllCmd.accesskey;"
cmd="cmd_selectAll"/>
<menuseparator/>
<menuitem label="&syncSyncNowItem.label;"
accesskey="&syncSyncNowItem.accesskey;"
id="syncedTabsRefreshFilter"/>
</menupopup>
</popupset>
#ifdef CAN_DRAW_IN_TITLEBAR

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

@ -21,6 +21,10 @@ function getContextMenu(window) {
return getChromeWindow(window).document.getElementById("SyncedTabsSidebarContext");
}
function getTabsFilterContextMenu(window) {
return getChromeWindow(window).document.getElementById("SyncedTabsSidebarTabsFilterContext");
}
/*
* TabListView
*
@ -330,21 +334,71 @@ TabListView.prototype = {
// Set up the custom context menu
_setupContextMenu() {
this._handleContentContextMenu = event =>
this.handleContentContextMenu(event);
this._handleContentContextMenuCommand = event =>
this.handleContentContextMenuCommand(event);
Services.els.addSystemEventListener(this._window, "contextmenu", this._handleContentContextMenu, false);
let menu = getContextMenu(this._window);
menu.addEventListener("command", this._handleContentContextMenuCommand, true);
Services.els.addSystemEventListener(this._window, "contextmenu", this, false);
for (let getMenu of [getContextMenu, getTabsFilterContextMenu]) {
let menu = getMenu(this._window);
menu.addEventListener("popupshowing", this, true);
menu.addEventListener("command", this, true);
}
},
_teardownContextMenu() {
// Tear down context menu
Services.els.removeSystemEventListener(this._window, "contextmenu", this._handleContentContextMenu, false);
let menu = getContextMenu(this._window);
menu.removeEventListener("command", this._handleContentContextMenuCommand, true);
Services.els.removeSystemEventListener(this._window, "contextmenu", this, false);
for (let getMenu of [getContextMenu, getTabsFilterContextMenu]) {
let menu = getMenu(this._window);
menu.removeEventListener("popupshowing", this, true);
menu.removeEventListener("command", this, true);
}
},
handleEvent(event) {
switch (event.type) {
case "contextmenu":
this.handleContextMenu(event);
break;
case "popupshowing": {
if (event.target.getAttribute("id") == "SyncedTabsSidebarTabsFilterContext") {
this.handleTabsFilterContextMenuShown(event);
}
break;
}
case "command": {
let menu = event.target.closest("menupopup");
switch (menu.getAttribute("id")) {
case "SyncedTabsSidebarContext":
this.handleContentContextMenuCommand(event);
break;
case "SyncedTabsSidebarTabsFilterContext":
this.handleTabsFilterContextMenuCommand(event);
break;
}
break;
}
}
},
handleTabsFilterContextMenuShown(event) {
let document = event.target.ownerDocument;
let focusedElement = document.commandDispatcher.focusedElement;
if (focusedElement != this.tabsFilter) {
this.tabsFilter.focus();
}
for (let item of event.target.children) {
if (!item.hasAttribute("cmd")) {
continue;
}
let command = item.getAttribute("cmd");
let controller = document.commandDispatcher.getControllerForCommand(command);
if (controller.isCommandEnabled(command)) {
item.removeAttribute("disabled");
} else {
item.setAttribute("disabled", "true");
}
}
},
handleContentContextMenuCommand(event) {
@ -357,19 +411,33 @@ TabListView.prototype = {
this.onBookmarkTab();
break;
case "syncedTabsRefresh":
case "syncedTabsRefreshFilter":
this.props.onSyncRefresh();
break;
}
},
handleContentContextMenu(event) {
let itemNode = this._findParentItemNode(event.target);
if (itemNode) {
this._selectRow(itemNode);
handleTabsFilterContextMenuCommand(event) {
let command = event.target.getAttribute("cmd");
let dispatcher = getChromeWindow(this._window).document.commandDispatcher;
let controller = dispatcher.focusedElement.controllers.getControllerForCommand(command);
controller.doCommand(command);
},
handleContextMenu(event) {
let menu;
if (event.target == this.tabsFilter) {
menu = getTabsFilterContextMenu(this._window);
} else {
let itemNode = this._findParentItemNode(event.target);
if (itemNode) {
this._selectRow(itemNode);
}
menu = getContextMenu(this._window);
this.adjustContextMenu(menu);
}
let menu = getContextMenu(this._window);
this.adjustContextMenu(menu);
menu.openPopupAtScreen(event.screenX, event.screenY, true, event);
},

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

@ -255,6 +255,78 @@ add_task(function* testSyncedTabsSidebarStatus() {
add_task(testClean);
add_task(function* testSyncedTabsSidebarContextMenu() {
yield SidebarUI.show('viewTabsSidebar');
let syncedTabsDeckComponent = window.SidebarUI.browser.contentWindow.syncedTabsDeckComponent;
let SyncedTabs = window.SidebarUI.browser.contentWindow.SyncedTabs;
Assert.ok(syncedTabsDeckComponent, "component exists");
originalSyncedTabsInternal = SyncedTabs._internal;
SyncedTabs._internal = {
isConfiguredToSyncTabs: true,
hasSyncedThisSession: true,
getTabClients() { return Promise.resolve([])},
syncTabs() {return Promise.resolve();},
};
sinon.stub(syncedTabsDeckComponent, "_accountStatus", ()=> Promise.resolve(true));
sinon.stub(SyncedTabs._internal, "getTabClients", ()=> Promise.resolve(Cu.cloneInto(FIXTURE, {})));
yield syncedTabsDeckComponent.updatePanel();
// This is a hacky way of waiting for the view to render. The view renders
// after the following promise (a different instance of which is triggered
// in updatePanel) resolves, so we wait for it here as well
yield syncedTabsDeckComponent.tabListComponent._store.getData();
info("Right-clicking the search box should show text-related actions");
let filterMenuItems = [
"menuitem[cmd=cmd_undo]",
"menuseparator",
// We don't check whether the commands are enabled due to platform
// differences. On OS X and Windows, "cut" and "copy" are always enabled
// for HTML inputs; on Linux, they're only enabled if text is selected.
"menuitem[cmd=cmd_cut]",
"menuitem[cmd=cmd_copy]",
"menuitem[cmd=cmd_paste]",
"menuitem[cmd=cmd_delete]",
"menuseparator",
"menuitem[cmd=cmd_selectAll]",
"menuseparator",
"menuitem#syncedTabsRefreshFilter",
];
yield* testContextMenu(syncedTabsDeckComponent,
"#SyncedTabsSidebarTabsFilterContext",
".tabsFilter",
filterMenuItems);
info("Right-clicking a tab should show additional actions");
let tabMenuItems = [
["menuitem#syncedTabsOpenSelected", { hidden: false }],
["menuitem#syncedTabsBookmarkSelected", { hidden: false }],
["menuseparator", { hidden: false }],
["menuitem#syncedTabsRefresh", { hidden: false }],
];
yield* testContextMenu(syncedTabsDeckComponent,
"#SyncedTabsSidebarContext",
"#tab-7cqCr77ptzX3-0",
tabMenuItems);
info("Right-clicking a client shouldn't show any actions");
let sidebarMenuItems = [
["menuitem#syncedTabsOpenSelected", { hidden: true }],
["menuitem#syncedTabsBookmarkSelected", { hidden: true }],
["menuseparator", { hidden: true }],
["menuitem#syncedTabsRefresh", { hidden: false }],
];
yield* testContextMenu(syncedTabsDeckComponent,
"#SyncedTabsSidebarContext",
"#item-OL3EJCsdb2JD",
sidebarMenuItems);
});
add_task(testClean);
function checkItem(node, item) {
Assert.ok(node.classList.contains("item"),
"Node should have .item class");
@ -279,3 +351,47 @@ function checkItem(node, item) {
}
}
function* testContextMenu(syncedTabsDeckComponent, contextSelector, triggerSelector, menuSelectors) {
let contextMenu = document.querySelector(contextSelector);
let triggerElement = syncedTabsDeckComponent.container.querySelector(triggerSelector);
let promisePopupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
let chromeWindow = triggerElement.ownerDocument.defaultView.top;
let rect = triggerElement.getBoundingClientRect();
let contentRect = chromeWindow.SidebarUI.browser.getBoundingClientRect();
// The offsets in `rect` are relative to the content window, but
// `synthesizeMouseAtPoint` calls `nsIDOMWindowUtils.sendMouseEvent`,
// which interprets the offsets relative to the containing *chrome* window.
// This means we need to account for the width and height of any elements
// outside the `browser` element, like `sidebarheader`.
let offsetX = contentRect.x + rect.x + (rect.width / 2);
let offsetY = contentRect.y + rect.y + (rect.height / 4);
yield EventUtils.synthesizeMouseAtPoint(offsetX, offsetY, {
type: "contextmenu",
button: 2,
}, chromeWindow);
yield promisePopupShown;
checkChildren(contextMenu, menuSelectors);
let promisePopupHidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
contextMenu.hidePopup();
yield promisePopupHidden;
}
function checkChildren(node, selectors) {
is(node.children.length, selectors.length, "Menu item count doesn't match");
for (let index = 0; index < node.children.length; index++) {
let child = node.children[index];
let [selector, props] = [].concat(selectors[index]);
ok(selector, `Node at ${index} should have selector`);
ok(child.matches(selector), `Node ${
index} should match ${selector}`);
if (props) {
Object.keys(props).forEach(prop => {
is(child[prop], props[prop], `${prop} value at ${index} should match`);
});
}
}
}