Bug 1363182 - Add a "send to device" subview to the page action menu. r=eoger,mikedeboer

Add a Send to Device subview to the page action panel.  When the page isn't
sendable, disable the Send to Device menu item.  When the user doesn't have any
devices, show a menu item that opens the Firefox Account preferences pane.

Generalize gSync.populateSendTabToDevicesMenu() so that it can be used to
populate any kind of container, not only a menupopup with menuitems.

Add an SVG that shows a phone and an SVG that shows a desktop.

MozReview-Commit-ID: EZQKAEAr08q

--HG--
extra : rebase_source : bda87f105712a6c6ba83da1a78179eee93b5f4d0
This commit is contained in:
Drew Willcoxon 2017-05-24 15:49:43 -07:00
parent 918f0728dc
commit 547921aa65
17 changed files with 463 additions and 29 deletions

View File

@ -280,10 +280,20 @@ var gSync = {
Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title);
},
populateSendTabToDevicesMenu(devicesPopup, url, title) {
populateSendTabToDevicesMenu(devicesPopup, url, title, createDeviceNodeFn) {
if (!createDeviceNodeFn) {
createDeviceNodeFn = (clientId, name, clientType) => {
let eltName = name ? "menuitem" : "menuseparator";
return document.createElement(eltName);
};
}
// remove existing menu items
while (devicesPopup.hasChildNodes()) {
devicesPopup.firstChild.remove();
for (let i = devicesPopup.childNodes.length - 1; i >= 0; --i) {
let child = devicesPopup.childNodes[i];
if (child.classList.contains("sync-menuitem")) {
child.remove();
}
}
const fragment = document.createDocumentFragment();
@ -296,26 +306,28 @@ var gSync = {
clients.forEach(clientId => this.sendTabToDevice(url, clientId, title));
}
function addTargetDevice(clientId, name) {
const targetDevice = document.createElement("menuitem");
function addTargetDevice(clientId, name, clientType) {
const targetDevice = createDeviceNodeFn(clientId, name, clientType);
targetDevice.addEventListener("command", onTargetDeviceCommand, true);
targetDevice.setAttribute("class", "sendtab-target");
targetDevice.classList.add("sync-menuitem", "sendtab-target");
targetDevice.setAttribute("clientId", clientId);
targetDevice.setAttribute("clientType", clientType);
targetDevice.setAttribute("label", name);
fragment.appendChild(targetDevice);
}
const clients = this.remoteClients;
for (let client of clients) {
addTargetDevice(client.id, client.name);
addTargetDevice(client.id, client.name, client.type);
}
// "All devices" menu item
if (clients.length > 1) {
const separator = document.createElement("menuseparator");
const separator = createDeviceNodeFn();
separator.classList.add("sync-menuitem");
fragment.appendChild(separator);
const allDevicesLabel = this.fxaStrings.GetStringFromName("sendTabToAllDevices.menuitem");
addTargetDevice("", allDevicesLabel);
addTargetDevice("", allDevicesLabel, "");
}
devicesPopup.appendChild(fragment);

View File

@ -1333,4 +1333,11 @@ toolbarpaletteitem[place="palette"][hidden] {
-moz-window-shadow: none;
}
/* Page action menu */
#page-action-sendToDeviceView-body[signedin] > #page-action-sendToDevice-fxa-button,
#page-action-sendToDeviceView-body:not([signedin]) > #page-action-no-devices-button,
#page-action-sendToDeviceView-body[hasdevices] > #page-action-no-devices-button {
display: none;
}
%include theme-vars.inc.css

View File

@ -7737,6 +7737,11 @@ var gPageActionButton = {
return this.panel = document.getElementById("page-action-panel");
},
get sendToDeviceBody() {
delete this.sendToDeviceBody;
return this.sendToDeviceBody = document.getElementById("page-action-sendToDeviceView-body");
},
init() {
if (getBoolPref("browser.photon.structure.enabled")) {
this.button.hidden = false;
@ -7752,10 +7757,20 @@ var gPageActionButton = {
return; // Left click, space or enter only
}
this._preparePanelToBeShown();
this.panel.hidden = false;
this.panel.openPopup(this.button, "bottomcenter topright");
},
_preparePanelToBeShown() {
let browser = gBrowser.selectedBrowser;
let url = browser.currentURI.spec;
let sendToDeviceItem =
document.getElementById("page-action-send-to-device-button");
sendToDeviceItem.disabled = !gSync.isSendableURI(url);
},
copyURL() {
this.panel.hidePopup();
Cc["@mozilla.org/widget/clipboardhelper;1"]
@ -7767,6 +7782,44 @@ var gPageActionButton = {
this.panel.hidePopup();
MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser);
},
showSendToDeviceView(subviewButton) {
let browser = gBrowser.selectedBrowser;
let url = browser.currentURI.spec;
let title = browser.contentTitle;
let body = this.sendToDeviceBody;
gSync.populateSendTabToDevicesMenu(body, url, title, (clientId, name, clientType) => {
if (!name) {
return document.createElement("toolbarseparator");
}
let item = document.createElement("toolbarbutton");
item.classList.add("page-action-sendToDevice-device", "subviewbutton");
if (clientId) {
item.classList.add("subviewbutton-iconic");
}
return item;
});
if (gSync.remoteClients.length) {
body.setAttribute("hasdevices", "true");
} else {
body.removeAttribute("hasdevices");
}
if (UIState.get().status == UIState.STATUS_SIGNED_IN) {
body.setAttribute("signedin", "true");
} else {
body.removeAttribute("signedin");
}
PanelUI.showSubView("page-action-sendToDeviceView", subviewButton);
},
fxaButtonClicked() {
this.panel.hidePopup();
gSync.openPrefs();
},
};
/**

View File

@ -465,6 +465,26 @@
class="subviewbutton subviewbutton-iconic"
label="&emailPageCmd.label;"
command="PageAction:emailLink"/>
<toolbarbutton id="page-action-send-to-device-button"
class="subviewbutton subviewbutton-iconic subviewbutton-nav"
label="&sendToDevice.label;"
closemenu="none"
oncommand="gPageActionButton.showSendToDeviceView(this);"/>
</vbox>
</panelview>
<panelview id="page-action-sendToDeviceView"
class="PanelUI-subView"
title="&sendToDevice.viewTitle;">
<vbox id="page-action-sendToDeviceView-body" class="panel-subview-body">
<toolbarbutton id="page-action-sendToDevice-fxa-button"
class="subviewbutton subviewbutton-iconic"
label="&syncBrand.fxAccount.label;"
shortcut="&sendToDevice.fxaRequired.label;"
oncommand="gPageActionButton.fxaButtonClicked();"/>
<toolbarbutton id="page-action-no-devices-button"
class="subviewbutton"
label="&sendToDevice.noDevices.label;"
disabled="true"/>
</vbox>
</panelview>
</photonpanelmultiview>

View File

@ -48,8 +48,8 @@ function getVisibleMenuItems(aMenu, aData) {
var isPageMenuItem = item.hasAttribute("generateditemid");
if (item.nodeName == "menuitem") {
var isGenerated = item.className == "spell-suggestion"
|| item.className == "sendtab-target";
var isGenerated = item.classList.contains("spell-suggestion")
|| item.classList.contains("sendtab-target");
if (isGenerated) {
is(item.id, "", "child menuitem #" + i + " is generated");
} else if (isPageMenuItem) {

View File

@ -114,3 +114,5 @@ run-if = e10s
subsuite = clipboard
support-files =
test_wyciwyg_copying.html
[browser_page_action_menu.js]
run-if = nightly_build # Photon only

View File

@ -0,0 +1,317 @@
"use strict";
let gPanel = document.getElementById("page-action-panel");
add_task(async function copyURL() {
// Open the panel.
await promisePanelOpen();
// Click Copy URL.
let copyURLButton = document.getElementById("page-action-copy-url-button");
let hiddenPromise = promisePanelHidden();
EventUtils.synthesizeMouseAtCenter(copyURLButton, {});
await hiddenPromise;
// Check the clipboard.
let transferable = Cc["@mozilla.org/widget/transferable;1"]
.createInstance(Ci.nsITransferable);
transferable.init(null);
let flavor = "text/unicode";
transferable.addDataFlavor(flavor);
Services.clipboard.getData(transferable, Services.clipboard.kGlobalClipboard);
let strObj = {};
transferable.getTransferData(flavor, strObj, {});
Assert.ok(!!strObj.value);
strObj.value.QueryInterface(Ci.nsISupportsString);
Assert.equal(strObj.value.data, gBrowser.selectedBrowser.currentURI.spec);
});
add_task(async function emailLink() {
// Replace the email-link entry point to check whether it's called.
let originalFn = MailIntegration.sendLinkForBrowser;
let fnCalled = false;
MailIntegration.sendLinkForBrowser = () => {
fnCalled = true;
};
registerCleanupFunction(() => {
MailIntegration.sendLinkForBrowser = originalFn;
});
// Open the panel and click Email Link.
await promisePanelOpen();
let emailLinkButton =
document.getElementById("page-action-email-link-button");
let hiddenPromise = promisePanelHidden();
EventUtils.synthesizeMouseAtCenter(emailLinkButton, {});
await hiddenPromise;
Assert.ok(fnCalled);
});
add_task(async function sendToDevice_nonSendable() {
// Open a tab that's not sendable.
await BrowserTestUtils.withNewTab("about:blank", async () => {
// Open the panel. Send to Device should be disabled.
await promisePanelOpen();
let sendToDeviceButton =
document.getElementById("page-action-send-to-device-button");
Assert.ok(sendToDeviceButton.disabled);
let hiddenPromise = promisePanelHidden();
gPanel.hidePopup();
await hiddenPromise;
});
});
add_task(async function sendToDevice_notSignedIn() {
// Open a tab that's sendable.
await BrowserTestUtils.withNewTab("http://example.com/", async () => {
await promiseSyncReady();
// Open the panel.
await promisePanelOpen();
let sendToDeviceButton =
document.getElementById("page-action-send-to-device-button");
Assert.ok(!sendToDeviceButton.disabled);
// Click Send to Device.
let viewPromise = promiseViewShown();
EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
let view = await viewPromise;
Assert.equal(view.id, "page-action-sendToDeviceView");
// The Fxa button should be shown.
checkSendToDeviceItems([
{
id: "page-action-sendToDevice-fxa-button",
},
{
id: "page-action-no-devices-button",
display: "none",
disabled: true,
},
]);
// Click the Fxa button.
let body = view.firstChild;
let fxaButton = body.childNodes[0];
Assert.equal(fxaButton.id, "page-action-sendToDevice-fxa-button");
let prefsTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
let hiddenPromise = promisePanelHidden();
EventUtils.synthesizeMouseAtCenter(fxaButton, {});
let values = await Promise.all([prefsTabPromise, hiddenPromise]);
let tab = values[0];
// The Fxa prefs pane should open. The full URL is something like:
// about:preferences?entrypoint=syncbutton#sync
// Just make sure it's about:preferences#sync.
let urlObj = new URL(gBrowser.selectedBrowser.currentURI.spec);
let url = urlObj.protocol + urlObj.pathname + urlObj.hash;
Assert.equal(url, "about:preferences#sync");
await BrowserTestUtils.removeTab(tab);
});
});
add_task(async function sendToDevice_noDevices() {
// Open a tab that's sendable.
await BrowserTestUtils.withNewTab("http://example.com/", async () => {
await promiseSyncReady();
UIState._internal._state = { status: UIState.STATUS_SIGNED_IN };
// Open the panel.
await promisePanelOpen();
let sendToDeviceButton =
document.getElementById("page-action-send-to-device-button");
Assert.ok(!sendToDeviceButton.disabled);
// Click Send to Device.
let viewPromise = promiseViewShown();
EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
let view = await viewPromise;
Assert.equal(view.id, "page-action-sendToDeviceView");
// The no-devices item should be shown.
checkSendToDeviceItems([
{
id: "page-action-sendToDevice-fxa-button",
display: "none",
},
{
id: "page-action-no-devices-button",
disabled: true,
},
]);
// Done, hide the panel.
let hiddenPromise = promisePanelHidden();
gPanel.hidePopup();
await hiddenPromise;
await UIState.reset();
});
});
add_task(async function sendToDevice_devices() {
// Open a tab that's sendable.
await BrowserTestUtils.withNewTab("http://example.com/", async () => {
await promiseSyncReady();
UIState._internal._state = { status: UIState.STATUS_SIGNED_IN };
// Set up mock remote clients.
let mockRemoteClients = [
{ id: "0", name: "foo", type: "mobile" },
{ id: "1", name: "bar", type: "desktop" },
{ id: "2", name: "baz", type: "mobile" },
];
let originalGetter =
Object.getOwnPropertyDescriptor(gSync, "remoteClients").get;
Object.defineProperty(gSync, "remoteClients", {
get() { return mockRemoteClients; }
});
let cleanUp = () => {
Object.defineProperty(gSync, "remoteClients", {
get: originalGetter
});
};
registerCleanupFunction(cleanUp);
// Open the panel.
await promisePanelOpen();
let sendToDeviceButton =
document.getElementById("page-action-send-to-device-button");
Assert.ok(!sendToDeviceButton.disabled);
// Click Send to Device.
let viewPromise = promiseViewShown();
EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
let view = await viewPromise;
Assert.equal(view.id, "page-action-sendToDeviceView");
// The devices should be shown in the subview.
let expectedItems = [
{
id: "page-action-sendToDevice-fxa-button",
display: "none",
},
{
id: "page-action-no-devices-button",
display: "none",
disabled: true,
},
];
for (let client of mockRemoteClients) {
expectedItems.push({
attrs: {
clientId: client.id,
label: client.name,
clientType: client.type,
},
});
}
expectedItems.push(
null,
{
label: "All Devices",
}
);
checkSendToDeviceItems(expectedItems);
// Done, hide the panel.
let hiddenPromise = promisePanelHidden();
gPanel.hidePopup();
await hiddenPromise;
cleanUp();
await UIState.reset();
});
});
function promisePanelOpen() {
let button = document.getElementById("urlbar-page-action-button");
let shownPromise = promisePanelShown();
EventUtils.synthesizeMouseAtCenter(button, {});
return shownPromise;
}
function promisePanelShown() {
return promisePanelEvent("popupshown");
}
function promisePanelHidden() {
return promisePanelEvent("popuphidden");
}
function promisePanelEvent(name) {
return new Promise(resolve => {
gPanel.addEventListener(name, () => {
setTimeout(() => {
resolve();
});
}, { once: true });
});
}
function promiseViewShown() {
return Promise.all([
promiseViewShowing(),
promiseTransitionEnd(),
]).then(values => {
return new Promise(resolve => {
setTimeout(() => {
resolve(values[0]);
});
});
});
}
function promiseViewShowing() {
return new Promise(resolve => {
gPanel.addEventListener("ViewShowing", (event) => {
resolve(event.target);
}, { once: true });
});
}
function promiseTransitionEnd() {
return new Promise(resolve => {
gPanel.addEventListener("transitionend", (event) => {
resolve(event.target);
}, { once: true });
});
}
function promiseSyncReady() {
let service = Cc["@mozilla.org/weave/service;1"]
.getService(Components.interfaces.nsISupports)
.wrappedJSObject;
return service.whenLoaded().then(() => {
UIState.isReady();
return UIState.refresh();
});
}
function checkSendToDeviceItems(expectedItems) {
let body = document.getElementById("page-action-sendToDeviceView-body");
Assert.equal(body.childNodes.length, expectedItems.length);
for (let i = 0; i < expectedItems.length; i++) {
let expected = expectedItems[i];
let actual = body.childNodes[i];
if (!expected) {
Assert.equal(actual.localName, "toolbarseparator");
continue;
}
if ("id" in expected) {
Assert.equal(actual.id, expected.id);
}
let display = "display" in expected ? expected.display : "-moz-box";
Assert.equal(getComputedStyle(actual).display, display);
let disabled = "disabled" in expected ? expected.disabled : false;
Assert.equal(actual.disabled, disabled);
if ("attrs" in expected) {
for (let name in expected.attrs) {
Assert.ok(actual.hasAttribute(name));
Assert.equal(actual.getAttribute(name), expected.attrs[name]);
}
}
}
}

View File

@ -948,3 +948,8 @@ you can use these alternative items. Otherwise, their values should be empty. -
<!ENTITY updateRestart.panelUI.label2 "Restart to update &brandShorterName;">
<!ENTITY pageActionButton.tooltip "Page actions">
<!ENTITY sendToDevice.label "Send to Device…">
<!ENTITY sendToDevice.viewTitle "Send to Device">
<!ENTITY sendToDevice.fxaRequired.label "Required">
<!ENTITY sendToDevice.noDevices.label "No Devices Available">

View File

@ -78,9 +78,9 @@
-moz-appearance: none;
border-style: none;
list-style-image: url("chrome://browser/skin/page-action.svg");
-moz-context-properties: fill;
margin: 0;
padding: 0 6px;
-moz-context-properties: fill;
fill: currentColor;
}
@ -90,12 +90,24 @@
#page-action-copy-url-button {
list-style-image: url("chrome://browser/skin/copy-url.svg");
-moz-context-properties: fill;
fill: currentColor;
}
#page-action-email-link-button {
list-style-image: url("chrome://browser/skin/email-link.svg");
-moz-context-properties: fill;
fill: currentColor;
}
#page-action-send-to-device-button {
list-style-image: url("chrome://browser/skin/device-mobile.svg");
}
.page-action-sendToDevice-device[clientType=mobile] {
list-style-image: url("chrome://browser/skin/device-mobile.svg");
}
.page-action-sendToDevice-device[clientType=desktop] {
list-style-image: url("chrome://browser/skin/device-desktop.svg");
}
#page-action-sendToDevice-fxa-button {
list-style-image: url("chrome://browser/skin/sync.svg");
}

View File

@ -1297,10 +1297,14 @@ photonpanelmultiview .PanelUI-subView .panel-subview-footer {
font-size: 1.2rem;
}
photonpanelmultiview .subviewbutton {
-moz-context-properties: fill;
fill: currentColor;
}
photonpanelmultiview .subviewbutton[checked="true"] {
background: none;
list-style-image: url(chrome://browser/skin/menu-icons/check.svg);
-moz-context-properties: fill;
}
photonpanelmultiview .subviewbutton > .menu-iconic-left {

View File

@ -1,4 +1,3 @@
<?xml version="1.0"?>
<!-- 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/. -->

Before

Width:  |  Height:  |  Size: 622 B

After

Width:  |  Height:  |  Size: 600 B

View File

@ -0,0 +1,6 @@
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="context-fill" d="M0 12h16a1.959 1.959 0 0 1-2 2H2a1.959 1.959 0 0 1-2-2zM13.107 2H2.894A1.894 1.894 0 0 0 1 3.894V11h14V3.893A1.893 1.893 0 0 0 13.107 2zM14 10H2V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1z"/>
</svg>

After

Width:  |  Height:  |  Size: 516 B

View File

@ -0,0 +1,6 @@
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="context-fill" d="M10 1H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2zm1 11.5a.5.5 0 0 1-.5.5h-5a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 491 B

View File

@ -1,4 +1,3 @@
<?xml version="1.0"?>
<!-- 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/. -->

Before

Width:  |  Height:  |  Size: 657 B

After

Width:  |  Height:  |  Size: 635 B

View File

@ -1,4 +1,3 @@
<?xml version="1.0"?>
<!-- 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/. -->

Before

Width:  |  Height:  |  Size: 439 B

After

Width:  |  Height:  |  Size: 417 B

View File

@ -203,6 +203,8 @@
skin/classic/browser/page-action.svg (../shared/icons/page-action.svg)
skin/classic/browser/copy-url.svg (../shared/icons/copy-url.svg)
skin/classic/browser/email-link.svg (../shared/icons/email-link.svg)
skin/classic/browser/device-mobile.svg (../shared/icons/device-mobile.svg)
skin/classic/browser/device-desktop.svg (../shared/icons/device-desktop.svg)
skin/classic/browser/menu-icons/back.svg (../shared/menu-icons/back.svg)
skin/classic/browser/menu-icons/back-small.svg (../shared/menu-icons/back-small.svg)
skin/classic/browser/menu-icons/addons.svg (../shared/menu-icons/addons.svg)

View File

@ -184,47 +184,38 @@ toolbarpaletteitem[place="palette"] > #zoom-controls > #zoom-in-button {
#appMenu-new-window-button {
list-style-image: url(chrome://browser/skin/menu-icons/new-window.svg);
-moz-context-properties: fill;
}
#appMenu-private-window-button {
list-style-image: url(chrome://browser/skin/menu-icons/private-window.svg);
-moz-context-properties: fill;
}
#appMenu-print-button {
list-style-image: url(chrome://browser/skin/menu-icons/print.svg);
-moz-context-properties: fill;
}
#appMenu-library-button {
list-style-image: url(chrome://browser/skin/menu-icons/library.svg);
-moz-context-properties: fill;
}
#appMenu-addons-button {
list-style-image: url(chrome://browser/skin/menu-icons/addons.svg);
-moz-context-properties: fill;
}
#appMenu-preferences-button {
list-style-image: url(chrome://browser/skin/menu-icons/settings.svg);
-moz-context-properties: fill;
}
#appMenu-customize-button {
list-style-image: url(chrome://browser/skin/menu-icons/customize.svg);
-moz-context-properties: fill;
}
#appMenu-find-button {
list-style-image: url(chrome://browser/skin/menu-icons/find.svg);
-moz-context-properties: fill;
}
#appMenu-help-button {
list-style-image: url(chrome://browser/skin/menu-icons/help.svg);
-moz-context-properties: fill;
}
#appMenu-cut-button {