mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 21:31:04 +00:00
Bug 677372 - Send Tab/Page/Link to device. r=markh
MozReview-Commit-ID: 6xnBjTPkiUQ --HG-- extra : transplant_source : 0P%F2%CD%88%0C%01%F6sO%D7%0E%D8%DDl%3B%B4%9E%F0%C0
This commit is contained in:
parent
9126be55bf
commit
a1af006e6d
@ -1106,6 +1106,12 @@ pref("services.sync.prefs.sync.xpinstall.whitelist.required", true);
|
||||
// user's tabs and bookmarks. Note this pref is also synced.
|
||||
pref("services.sync.syncedTabs.showRemoteIcons", true);
|
||||
|
||||
#ifdef NIGHTLY_BUILD
|
||||
pref("services.sync.sendTabToDevice.enabled", true);
|
||||
#else
|
||||
pref("services.sync.sendTabToDevice.enabled", false);
|
||||
#endif
|
||||
|
||||
// Developer edition preferences
|
||||
#ifdef MOZ_DEV_EDITION
|
||||
sticky_pref("lightweightThemes.selectedThemeID", "firefox-devedition@mozilla.org");
|
||||
|
@ -283,6 +283,14 @@
|
||||
label="&savePageCmd.label;"
|
||||
accesskey="&savePageCmd.accesskey2;"
|
||||
oncommand="gContextMenu.savePageAs();"/>
|
||||
<menuseparator id="context-sep-sendpagetodevice" hidden="true"/>
|
||||
<menu id="context-sendpagetodevice"
|
||||
label="&sendPageToDevice.label;"
|
||||
accesskey="&sendPageToDevice.accesskey;"
|
||||
hidden="true">
|
||||
<menupopup id="context-sendpagetodevice-popup"
|
||||
onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, gBrowser.currentURI.spec, gBrowser.contentTitle);"/>
|
||||
</menu>
|
||||
<menu id="context-markpageMenu" label="&social.markpageMenu.label;"
|
||||
accesskey="&social.markpageMenu.accesskey;">
|
||||
<menupopup/>
|
||||
@ -326,6 +334,14 @@
|
||||
oncommand="AddKeywordForSearchField();"/>
|
||||
<menuitem id="context-searchselect"
|
||||
oncommand="BrowserSearch.loadSearchFromContext(this.searchTerms);"/>
|
||||
<menuseparator id="context-sep-sendlinktodevice" hidden="true"/>
|
||||
<menu id="context-sendlinktodevice"
|
||||
label="&sendLinkToDevice.label;"
|
||||
accesskey="&sendLinkToDevice.accesskey;"
|
||||
hidden="true">
|
||||
<menupopup id="context-sendlinktodevice-popup"
|
||||
onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, gContextMenu.linkURL, gContextMenu.linkTextStr);"/>
|
||||
</menu>
|
||||
<menuitem id="context-shareselect"
|
||||
label="&shareSelect.label;"
|
||||
accesskey="&shareSelect.accesskey;"
|
||||
|
@ -78,6 +78,15 @@ var gFxAccounts = {
|
||||
return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
|
||||
},
|
||||
|
||||
get sendTabToDeviceEnabled() {
|
||||
return Services.prefs.getBoolPref("services.sync.sendTabToDevice.enabled");
|
||||
},
|
||||
|
||||
get remoteClients() {
|
||||
return Weave.Service.clientsEngine.remoteClients
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
},
|
||||
|
||||
init: function () {
|
||||
// Bail out if we're already initialized and for pop-up windows.
|
||||
if (this._initialized || !window.toolbar.visible) {
|
||||
@ -361,6 +370,81 @@ var gFxAccounts = {
|
||||
openSignInAgainPage: function (entryPoint) {
|
||||
this.openAccountsPage("reauth", { entrypoint: entryPoint });
|
||||
},
|
||||
|
||||
sendTabToDevice: function (url, clientId, title) {
|
||||
Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title);
|
||||
},
|
||||
|
||||
populateSendTabToDevicesMenu: function (devicesPopup, url, title) {
|
||||
// remove existing menu items
|
||||
while (devicesPopup.hasChildNodes()) {
|
||||
devicesPopup.removeChild(devicesPopup.firstChild);
|
||||
}
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
const onTargetDeviceCommand = (event) => {
|
||||
const clientId = event.target.getAttribute("clientId");
|
||||
const clients = clientId
|
||||
? [clientId]
|
||||
: this.remoteClients.map(client => client.id);
|
||||
|
||||
clients.forEach(clientId => this.sendTabToDevice(url, clientId, title));
|
||||
}
|
||||
|
||||
function addTargetDevice(clientId, name) {
|
||||
const targetDevice = document.createElement("menuitem");
|
||||
targetDevice.addEventListener("command", onTargetDeviceCommand, true);
|
||||
targetDevice.setAttribute("class", "sendtab-target");
|
||||
targetDevice.setAttribute("clientId", clientId);
|
||||
targetDevice.setAttribute("label", name);
|
||||
fragment.appendChild(targetDevice);
|
||||
}
|
||||
|
||||
const clients = this.remoteClients;
|
||||
for (let client of clients) {
|
||||
addTargetDevice(client.id, client.name);
|
||||
}
|
||||
|
||||
// "All devices" menu item
|
||||
const separator = document.createElement("menuseparator");
|
||||
fragment.appendChild(separator);
|
||||
const allDevicesLabel = this.strings.GetStringFromName("sendTabToAllDevices.menuitem");
|
||||
addTargetDevice("", allDevicesLabel);
|
||||
|
||||
devicesPopup.appendChild(fragment);
|
||||
},
|
||||
|
||||
updateTabContextMenu: function (aPopupMenu) {
|
||||
if (!this.sendTabToDeviceEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const remoteClientPresent = this.remoteClients.length > 0;
|
||||
["context_sendTabToDevice", "context_sendTabToDevice_separator"]
|
||||
.forEach(id => { document.getElementById(id).hidden = !remoteClientPresent });
|
||||
},
|
||||
|
||||
initPageContextMenu: function (contextMenu) {
|
||||
if (!this.sendTabToDeviceEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const remoteClientPresent = this.remoteClients.length > 0;
|
||||
// showSendLink and showSendPage are mutually exclusive
|
||||
const showSendLink = remoteClientPresent
|
||||
&& (contextMenu.onSaveableLink || contextMenu.onPlainTextLink);
|
||||
const showSendPage = !showSendLink && remoteClientPresent
|
||||
&& !(contextMenu.isContentSelected ||
|
||||
contextMenu.onImage || contextMenu.onCanvas ||
|
||||
contextMenu.onVideo || contextMenu.onAudio ||
|
||||
contextMenu.onLink || contextMenu.onTextInput);
|
||||
|
||||
["context-sendpagetodevice", "context-sep-sendpagetodevice"]
|
||||
.forEach(id => contextMenu.showItem(id, showSendPage));
|
||||
["context-sendlinktodevice", "context-sep-sendlinktodevice"]
|
||||
.forEach(id => contextMenu.showItem(id, showSendLink));
|
||||
}
|
||||
};
|
||||
|
||||
XPCOMUtils.defineLazyGetter(gFxAccounts, "FxAccountsCommon", function () {
|
||||
|
@ -7588,6 +7588,8 @@ var TabContextMenu = {
|
||||
|
||||
this.contextTab.addEventListener("TabAttrModified", this, false);
|
||||
aPopupMenu.addEventListener("popuphiding", this, false);
|
||||
|
||||
gFxAccounts.updateTabContextMenu(aPopupMenu);
|
||||
},
|
||||
handleEvent(aEvent) {
|
||||
switch (aEvent.type) {
|
||||
|
@ -104,6 +104,12 @@
|
||||
hidden="true"
|
||||
oncommand="gBrowser.openNonRemoteWindow(TabContextMenu.contextTab);"/>
|
||||
#endif
|
||||
<menuseparator id="context_sendTabToDevice_separator" hidden="true"/>
|
||||
<menu id="context_sendTabToDevice" label="&sendTabToDevice.label;"
|
||||
accesskey="&sendTabToDevice.accesskey;" hidden="true">
|
||||
<menupopup id="context_sendTabToDevicePopupMenu"
|
||||
onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, TabContextMenu.contextTab.linkedBrowser.currentURI.spec, TabContextMenu.contextTab.linkedBrowser.contentTitle);"/>
|
||||
</menu>
|
||||
<menuseparator/>
|
||||
<menuitem id="context_reloadAllTabs" label="&reloadAllTabs.label;" accesskey="&reloadAllTabs.accesskey;"
|
||||
tbattr="tabbrowser-multiple-visible"
|
||||
|
@ -117,6 +117,7 @@ nsContextMenu.prototype = {
|
||||
this.initLeaveDOMFullScreenItems();
|
||||
this.initClickToPlayItems();
|
||||
this.initPasswordManagerItems();
|
||||
this.initSyncItems();
|
||||
},
|
||||
|
||||
initPageMenuSeparator: function CM_initPageMenuSeparator() {
|
||||
@ -576,6 +577,10 @@ nsContextMenu.prototype = {
|
||||
popup.insertBefore(fragment, insertBeforeElement);
|
||||
},
|
||||
|
||||
initSyncItems: function() {
|
||||
gFxAccounts.initPageContextMenu(this);
|
||||
},
|
||||
|
||||
openPasswordManager: function() {
|
||||
LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
|
||||
},
|
||||
|
@ -852,6 +852,80 @@ add_task(function* test_input_spell_false() {
|
||||
);
|
||||
});
|
||||
|
||||
const remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
|
||||
|
||||
add_task(function* test_plaintext_sendpagetodevice() {
|
||||
if (!gFxAccounts.sendTabToDeviceEnabled) {
|
||||
return;
|
||||
}
|
||||
const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
|
||||
|
||||
let plainTextItems = ["context-navigation", null,
|
||||
["context-back", false,
|
||||
"context-forward", false,
|
||||
"context-reload", true,
|
||||
"context-bookmarkpage", true], null,
|
||||
"---", null,
|
||||
"context-savepage", true,
|
||||
...(hasPocket ? ["context-pocket", true] : []),
|
||||
"---", null,
|
||||
"context-sendpagetodevice", true,
|
||||
["*Foo", true,
|
||||
"*Bar", true,
|
||||
"---", null,
|
||||
"*All Devices", true], null,
|
||||
"---", null,
|
||||
"context-viewbgimage", false,
|
||||
"context-selectall", true,
|
||||
"---", null,
|
||||
"context-viewsource", true,
|
||||
"context-viewinfo", true
|
||||
];
|
||||
yield test_contextmenu("#test-text", plainTextItems, {
|
||||
onContextMenuShown() {
|
||||
yield openMenuItemSubmenu("context-sendpagetodevice");
|
||||
}
|
||||
});
|
||||
|
||||
restoreRemoteClients(oldGetter);
|
||||
});
|
||||
|
||||
add_task(function* test_link_sendlinktodevice() {
|
||||
if (!gFxAccounts.sendTabToDeviceEnabled) {
|
||||
return;
|
||||
}
|
||||
const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
|
||||
|
||||
yield test_contextmenu("#test-link",
|
||||
["context-openlinkintab", true,
|
||||
...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
|
||||
// We need a blank entry here because the containers submenu is
|
||||
// dynamically generated with no ids.
|
||||
...(hasContainers ? ["", null] : []),
|
||||
"context-openlink", true,
|
||||
"context-openlinkprivate", true,
|
||||
"---", null,
|
||||
"context-bookmarklink", true,
|
||||
"context-savelink", true,
|
||||
...(hasPocket ? ["context-savelinktopocket", true] : []),
|
||||
"context-copylink", true,
|
||||
"context-searchselect", true,
|
||||
"---", null,
|
||||
"context-sendlinktodevice", true,
|
||||
["*Foo", true,
|
||||
"*Bar", true,
|
||||
"---", null,
|
||||
"*All Devices", true], null,
|
||||
],
|
||||
{
|
||||
onContextMenuShown() {
|
||||
yield openMenuItemSubmenu("context-sendlinktodevice");
|
||||
}
|
||||
});
|
||||
|
||||
restoreRemoteClients(oldGetter);
|
||||
});
|
||||
|
||||
add_task(function* test_cleanup() {
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
|
@ -2,6 +2,8 @@
|
||||
* 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/. */
|
||||
|
||||
const remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
|
||||
|
||||
add_task(function* test() {
|
||||
// There should be one tab when we start the test
|
||||
let [origTab] = gBrowser.visibleTabs;
|
||||
@ -14,6 +16,21 @@ add_task(function* test() {
|
||||
is(document.getElementById("context_closeTab").disabled, false, "Close Tab is enabled");
|
||||
is(document.getElementById("context_reloadAllTabs").disabled, false, "Reload All Tabs is enabled");
|
||||
|
||||
|
||||
if (gFxAccounts.sendTabToDeviceEnabled) {
|
||||
// Check the send tab to device menu item
|
||||
const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
|
||||
yield updateTabContextMenu(origTab, function* () {
|
||||
yield openMenuItemSubmenu("context_sendTabToDevice");
|
||||
});
|
||||
is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown");
|
||||
let targets = document.getElementById("context_sendTabToDevicePopupMenu").childNodes;
|
||||
is(targets[0].getAttribute("label"), "Foo", "Foo target is present");
|
||||
is(targets[1].getAttribute("label"), "Bar", "Bar target is present");
|
||||
is(targets[3].getAttribute("label"), "All Devices", "All Devices target is present");
|
||||
restoreRemoteClients(oldGetter);
|
||||
}
|
||||
|
||||
// Hide the original tab.
|
||||
gBrowser.selectedTab = testTab;
|
||||
gBrowser.showOnlyTheseTabs([testTab]);
|
||||
|
@ -41,23 +41,24 @@ function getVisibleMenuItems(aMenu, aData) {
|
||||
if (key)
|
||||
key = key.toLowerCase();
|
||||
|
||||
var isGenerated = item.hasAttribute("generateditemid");
|
||||
var isPageMenuItem = item.hasAttribute("generateditemid");
|
||||
|
||||
if (item.nodeName == "menuitem") {
|
||||
var isSpellSuggestion = item.className == "spell-suggestion";
|
||||
if (isSpellSuggestion) {
|
||||
is(item.id, "", "child menuitem #" + i + " is a spelling suggestion");
|
||||
} else if (isGenerated) {
|
||||
is(item.id, "", "child menuitem #" + i + " is a generated item");
|
||||
var isGenerated = item.className == "spell-suggestion"
|
||||
|| item.className == "sendtab-target";
|
||||
if (isGenerated) {
|
||||
is(item.id, "", "child menuitem #" + i + " is generated");
|
||||
} else if (isPageMenuItem) {
|
||||
is(item.id, "", "child menuitem #" + i + " is a generated page menu item");
|
||||
} else {
|
||||
ok(item.id, "child menuitem #" + i + " has an ID");
|
||||
}
|
||||
var label = item.getAttribute("label");
|
||||
ok(label.length, "menuitem " + item.id + " has a label");
|
||||
if (isSpellSuggestion) {
|
||||
is(key, "", "Spell suggestions shouldn't have an access key");
|
||||
if (isGenerated) {
|
||||
is(key, "", "Generated items shouldn't have an access key");
|
||||
items.push("*" + label);
|
||||
} else if (isGenerated) {
|
||||
} else if (isPageMenuItem) {
|
||||
items.push("+" + label);
|
||||
} else if (item.id.indexOf("spell-check-dictionary-") != 0 &&
|
||||
item.id != "spell-no-suggestions" &&
|
||||
@ -71,10 +72,10 @@ function getVisibleMenuItems(aMenu, aData) {
|
||||
else
|
||||
accessKeys[key] = item.id;
|
||||
}
|
||||
if (!isSpellSuggestion && !isGenerated) {
|
||||
if (!isGenerated && !isPageMenuItem) {
|
||||
items.push(item.id);
|
||||
}
|
||||
if (isGenerated) {
|
||||
if (isPageMenuItem) {
|
||||
var p = {};
|
||||
p.type = item.getAttribute("type");
|
||||
p.icon = item.getAttribute("image");
|
||||
@ -89,11 +90,11 @@ function getVisibleMenuItems(aMenu, aData) {
|
||||
items.push("---");
|
||||
items.push(null);
|
||||
} else if (item.nodeName == "menu") {
|
||||
if (isGenerated) {
|
||||
if (isPageMenuItem) {
|
||||
item.id = "generated-submenu-" + aData.generatedSubmenuId++;
|
||||
}
|
||||
ok(item.id, "child menu #" + i + " has an ID");
|
||||
if (!isGenerated) {
|
||||
if (!isPageMenuItem) {
|
||||
ok(key, "menu has an access key");
|
||||
if (accessKeys[key])
|
||||
ok(false, "menu " + item.id + " has same accesskey as " + accessKeys[key]);
|
||||
|
@ -61,7 +61,7 @@ function whenDelayedStartupFinished(aWindow, aCallback) {
|
||||
}, "browser-delayed-startup-finished", false);
|
||||
}
|
||||
|
||||
function updateTabContextMenu(tab) {
|
||||
function updateTabContextMenu(tab, onOpened) {
|
||||
let menu = document.getElementById("tabContextMenu");
|
||||
if (!tab)
|
||||
tab = gBrowser.selectedTab;
|
||||
@ -69,7 +69,15 @@ function updateTabContextMenu(tab) {
|
||||
tab.dispatchEvent(evt);
|
||||
menu.openPopup(tab, "end_after", 0, 0, true, false, evt);
|
||||
is(TabContextMenu.contextTab, tab, "TabContextMenu context is the expected tab");
|
||||
menu.hidePopup();
|
||||
const onFinished = () => menu.hidePopup();
|
||||
if (onOpened) {
|
||||
return Task.spawn(function*() {
|
||||
yield onOpened();
|
||||
onFinished();
|
||||
});
|
||||
} else {
|
||||
onFinished();
|
||||
}
|
||||
}
|
||||
|
||||
function openToolbarCustomizationUI(aCallback, aBrowserWin) {
|
||||
@ -1159,3 +1167,25 @@ function getCertExceptionDialog(aLocation) {
|
||||
}
|
||||
}
|
||||
|
||||
function setupRemoteClientsFixture(fixture) {
|
||||
let oldRemoteClientsGetter =
|
||||
Object.getOwnPropertyDescriptor(gFxAccounts, "remoteClients").get;
|
||||
|
||||
Object.defineProperty(gFxAccounts, "remoteClients", {
|
||||
get: function() { return fixture; }
|
||||
});
|
||||
return oldRemoteClientsGetter;
|
||||
}
|
||||
|
||||
function restoreRemoteClients(getter) {
|
||||
Object.defineProperty(gFxAccounts, "remoteClients", {
|
||||
get: getter
|
||||
});
|
||||
}
|
||||
|
||||
function* openMenuItemSubmenu(id) {
|
||||
let menuPopup = document.getElementById(id).menupopup;
|
||||
let menuPopupPromise = BrowserTestUtils.waitForEvent(menuPopup, "popupshown");
|
||||
menuPopup.showPopup();
|
||||
yield menuPopupPromise;
|
||||
}
|
||||
|
@ -33,3 +33,7 @@ syncStartNotification.body = Firefox will begin syncing momentarily.
|
||||
# These strings are used in a notification shown after Sync was disconnected remotely.
|
||||
deviceDisconnectedNotification.title = Sync disconnected
|
||||
deviceDisconnectedNotification.body = This computer has been successfully disconnected from Firefox Sync.
|
||||
|
||||
# LOCALIZATION NOTE (sendTabToAllDevices.menuitem)
|
||||
# Displayed in the Send Tabs context menu when right clicking a tab, a page or a link.
|
||||
sendTabToAllDevices.menuitem = All Devices
|
||||
|
@ -40,6 +40,12 @@ can reach it easily. -->
|
||||
<!ENTITY pinTab.accesskey "P">
|
||||
<!ENTITY unpinTab.label "Unpin Tab">
|
||||
<!ENTITY unpinTab.accesskey "b">
|
||||
<!ENTITY sendTabToDevice.label "Send Tab to Device">
|
||||
<!ENTITY sendTabToDevice.accesskey "D">
|
||||
<!ENTITY sendPageToDevice.label "Send Page to Device">
|
||||
<!ENTITY sendPageToDevice.accesskey "D">
|
||||
<!ENTITY sendLinkToDevice.label "Send Link to Device">
|
||||
<!ENTITY sendLinkToDevice.accesskey "D">
|
||||
<!ENTITY moveToNewWindow.label "Move to New Window">
|
||||
<!ENTITY moveToNewWindow.accesskey "W">
|
||||
<!ENTITY bookmarkAllTabs.label "Bookmark All Tabs…">
|
||||
|
@ -75,6 +75,10 @@ ClientEngine.prototype = {
|
||||
Svc.Prefs.set(this.name + ".lastRecordUpload", Math.floor(value));
|
||||
},
|
||||
|
||||
get remoteClients() {
|
||||
return Object.values(this._store._remoteClients);
|
||||
},
|
||||
|
||||
// Aggregate some stats on the composition of clients on this account
|
||||
get stats() {
|
||||
let stats = {
|
||||
|
Loading…
Reference in New Issue
Block a user