Bug 1639069 - Add download context menu items to 'Use' and 'Always use' the system viewer to open the download. r=jaws

Differential Revision: https://phabricator.services.mozilla.com/D79396
This commit is contained in:
Sam Foster 2020-06-25 22:19:37 +00:00
Родитель 1c447df70b
Коммит 394b51b9e3
13 изменённых файлов: 389 добавлений и 8 удалений

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

@ -359,6 +359,13 @@ pref("browser.download.animateNotifications", true);
// This records whether or not the panel has been shown at least once.
pref("browser.download.panel.shown", false);
// This records whether or not to show the 'Open in system viewer' context menu item when appropriate
pref("browser.download.openInSystemViewerContextMenuItem", true);
// This records whether or not to show the 'Always open...' context menu item when appropriate
pref("browser.download.alwaysOpenInSystemViewerContextMenuItem", true);
// This controls whether the button is automatically shown/hidden depending
// on whether there are downloads to show.
pref("browser.download.autohideButton", true);

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

@ -229,6 +229,8 @@ var PrefObserver = {
PrefObserver.register({
// prefName: defaultValue
animateNotifications: true,
openInSystemViewerContextMenuItem: true,
alwaysOpenInSystemViewerContextMenuItem: true,
});
// DownloadsCommon
@ -296,6 +298,20 @@ var DownloadsCommon = {
return PrefObserver.animateNotifications;
},
/**
* Indicates whether or not to show the 'Open in system viewer' context menu item when appropriate
*/
get openInSystemViewerItemEnabled() {
return PrefObserver.openInSystemViewerContextMenuItem;
},
/**
* Indicates whether or not to show the 'Always open...' context menu item when appropriate
*/
get alwaysOpenInSystemViewerItemEnabled() {
return PrefObserver.alwaysOpenInSystemViewerContextMenuItem;
},
/**
* Get access to one of the DownloadsData, PrivateDownloadsData, or
* HistoryDownloadsData objects, depending on the privacy status of the
@ -610,6 +626,9 @@ var DownloadsCommon = {
* @param options.openWhere
* Optional string indicating how to handle opening a download target file URI.
* One of "window", "tab", "tabshifted".
* @param options.useSystemDefault
* Optional value indicating how to handle launching this download,
* this call only. Will override the associated mimeInfo.preferredAction
* @return {Promise}
* @resolves When the instruction to launch the file has been
* successfully given to the operating system or handled internally

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

@ -297,6 +297,11 @@ class DownloadsSubview extends DownloadsViewUI.BaseView {
while (!button._shell) {
button = button.parentNode;
}
let download = button._shell.download;
let { preferredAction, useSystemDefault } = DownloadsCommon.getMimeInfo(
download
);
menu.setAttribute("state", button.getAttribute("state"));
if (button.hasAttribute("exists")) {
menu.setAttribute("exists", button.getAttribute("exists"));
@ -307,6 +312,37 @@ class DownloadsSubview extends DownloadsViewUI.BaseView {
"temporary-block",
button.classList.contains("temporary-block")
);
// menu items are conditionally displayed via CSS based on an is-pdf attribute
DownloadsCommon.log(
"DownloadsSubview, updateContextMenu, download is pdf? ",
download.target.path,
button.hasAttribute("is-pdf")
);
if (button.hasAttribute("is-pdf")) {
menu.setAttribute("is-pdf", "true");
let alwaysUseSystemViewerItem = menu.querySelector(
".downloadAlwaysUseSystemDefaultMenuItem"
);
if (preferredAction === useSystemDefault) {
alwaysUseSystemViewerItem.setAttribute("checked", "true");
} else {
alwaysUseSystemViewerItem.removeAttribute("checked");
}
alwaysUseSystemViewerItem.toggleAttribute(
"enabled",
DownloadsCommon.alwaysOpenInSystemViewerItemEnabled
);
let useSystemViewerItem = menu.querySelector(
".downloadUseSystemDefaultMenuItem"
);
useSystemViewerItem.toggleAttribute(
"enabled",
DownloadsCommon.openInSystemViewerItemEnabled
);
} else {
menu.removeAttribute("is-pdf");
}
for (let menuitem of menu.getElementsByTagName("menuitem")) {
let command = menuitem.getAttribute("command");
if (!command) {

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

@ -24,6 +24,13 @@ XPCOMUtils.defineLazyModuleGetters(this, {
OS: "resource://gre/modules/osfile.jsm",
});
XPCOMUtils.defineLazyServiceGetter(
this,
"handlerSvc",
"@mozilla.org/uriloader/handler-service;1",
"nsIHandlerService"
);
const HTML_NS = "http://www.w3.org/1999/xhtml";
var gDownloadElementButtons = {
@ -457,9 +464,21 @@ DownloadsViewUI.DownloadElementShell.prototype = {
// on other properties. The order in which we check the properties of the
// Download object is the same used by stateOfDownload.
if (this.download.succeeded) {
DownloadsCommon.log(
"_updateStateInner, target exists? ",
this.download.target.path,
this.download.target.exists
);
if (this.download.target.exists) {
// This is a completed download, and the target file still exists.
this.element.setAttribute("exists", "true");
const isPDF = DownloadsCommon.isFileOfType(
this.download,
"application/pdf"
);
this.element.toggleAttribute("is-pdf", isPDF);
let sizeWithUnits = DownloadsViewUI.getSizeWithUnits(this.download);
if (this.isPanel) {
// In the Downloads Panel, we show the file size after the state
@ -728,6 +747,9 @@ DownloadsViewUI.DownloadElementShell.prototype = {
case "cmd_delete":
// We don't want in-progress downloads to be removed accidentally.
return this.download.stopped;
case "downloadsCmd_openInSystemViewer":
case "downloadsCmd_alwaysOpenInSystemViewer":
return DownloadsCommon.isFileOfType(this.download, "application/pdf");
}
return DownloadsViewUI.isCommandName(aCommand) && !!this[aCommand];
},
@ -807,4 +829,39 @@ DownloadsViewUI.DownloadElementShell.prototype = {
cmd_delete() {
DownloadsCommon.deleteDownload(this.download).catch(Cu.reportError);
},
downloadsCmd_openInSystemViewer() {
// For this interaction only, pass a flag to override the preferredAction for this
// mime-type and open using the system viewer
DownloadsCommon.openDownload(this.download, {
useSystemDefault: true,
}).catch(Cu.reportError);
},
downloadsCmd_alwaysOpenInSystemViewer() {
// this command toggles between setting preferredAction for this mime-type to open
// using the system viewer, or to open the file in browser.
const mimeInfo = DownloadsCommon.getMimeInfo(this.download);
if (mimeInfo.preferredAction !== mimeInfo.useSystemDefault) {
// User has selected to open this mime-type with the system viewer from now on
DownloadsCommon.log(
"downloadsCmd_alwaysOpenInSystemViewer command for download: ",
this.download,
"switching to use system default for " + mimeInfo.type
);
mimeInfo.preferredAction = mimeInfo.useSystemDefault;
mimeInfo.alwaysAskBeforeHandling = false;
} else {
DownloadsCommon.log(
"downloadsCmd_alwaysOpenInSystemViewer command for download: ",
this.download,
"currently uses system default, switching to handleInternally"
);
// User has selected to not open this mime-type with the system viewer
mimeInfo.preferredAction = mimeInfo.handleInternally;
mimeInfo.alwaysAskBeforeHandling = true;
}
handlerSvc.store(mimeInfo);
DownloadsCommon.openDownload(this.download).catch(Cu.reportError);
},
};

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

@ -705,6 +705,9 @@ DownloadsPlacesView.prototype = {
// Set the state attribute so that only the appropriate items are displayed.
let contextMenu = document.getElementById("downloadsContextMenu");
let download = element._shell.download;
let { preferredAction, useSystemDefault } = DownloadsCommon.getMimeInfo(
download
);
contextMenu.setAttribute(
"state",
DownloadsCommon.stateOfDownload(download)
@ -712,6 +715,31 @@ DownloadsPlacesView.prototype = {
contextMenu.setAttribute("exists", "true");
contextMenu.classList.toggle("temporary-block", !!download.hasBlockedData);
if (element.hasAttribute("is-pdf")) {
contextMenu.setAttribute("is-pdf", "true");
let alwaysUseSystemViewerItem = contextMenu.querySelector(
".downloadAlwaysUseSystemDefaultMenuItem"
);
if (preferredAction === useSystemDefault) {
alwaysUseSystemViewerItem.setAttribute("checked", "true");
} else {
alwaysUseSystemViewerItem.removeAttribute("checked");
}
alwaysUseSystemViewerItem.toggleAttribute(
"enabled",
DownloadsCommon.alwaysOpenInSystemViewerItemEnabled
);
let useSystemViewerItem = contextMenu.querySelector(
".downloadUseSystemDefaultMenuItem"
);
useSystemViewerItem.toggleAttribute(
"enabled",
DownloadsCommon.openInSystemViewerItemEnabled
);
} else {
contextMenu.removeAttribute("is-pdf");
}
if (!download.stopped) {
// The hasPartialData property of a download may change at any time after
// it has started, so ensure we update the related command now.

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

@ -95,8 +95,14 @@
.download-state[state="1"]:not([exists])
.downloadCommandsSeparator,
.download-state[state="8"]:not(.temporary-block)
.downloadCommandsSeparator
.downloadCommandsSeparator,
/* the system-viewer context menu items are only shown for certain mime-types
and can be individually enabled via prefs */
.download-state:not([is-pdf]) .downloadUseSystemDefaultMenuItem,
.download-state .downloadUseSystemDefaultMenuItem:not([enabled]),
.download-state .downloadAlwaysUseSystemDefaultMenuItem:not([enabled]),
.download-state:not([is-pdf]) .downloadAlwaysUseSystemDefaultMenuItem
{
display: none;
}

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

@ -937,6 +937,11 @@ var DownloadsView = {
DownloadsViewController.updateCommands();
let download = element._shell.download;
let { preferredAction, useSystemDefault } = DownloadsCommon.getMimeInfo(
download
);
// Set the state attribute so that only the appropriate items are displayed.
let contextMenu = document.getElementById("downloadsContextMenu");
contextMenu.setAttribute("state", element.getAttribute("state"));
@ -949,6 +954,30 @@ var DownloadsView = {
"temporary-block",
element.classList.contains("temporary-block")
);
if (element.hasAttribute("is-pdf")) {
contextMenu.setAttribute("is-pdf", "true");
let alwaysUseSystemViewerItem = contextMenu.querySelector(
".downloadAlwaysUseSystemDefaultMenuItem"
);
if (preferredAction === useSystemDefault) {
alwaysUseSystemViewerItem.setAttribute("checked", "true");
} else {
alwaysUseSystemViewerItem.removeAttribute("checked");
}
alwaysUseSystemViewerItem.toggleAttribute(
"enabled",
DownloadsCommon.alwaysOpenInSystemViewerItemEnabled
);
let useSystemViewerItem = contextMenu.querySelector(
".downloadUseSystemDefaultMenuItem"
);
useSystemViewerItem.toggleAttribute(
"enabled",
DownloadsCommon.openInSystemViewerItemEnabled
);
} else {
contextMenu.removeAttribute("is-pdf");
}
},
onDownloadDragStart(aEvent) {
@ -1093,6 +1122,22 @@ class DownloadsViewItem extends DownloadsViewUI.DownloadElementShell {
DownloadsPanel.hidePanel();
}
downloadsCmd_openInSystemViewer() {
super.downloadsCmd_openInSystemViewer();
// We explicitly close the panel here to give the user the feedback that
// their click has been received, and we're handling the action.
DownloadsPanel.hidePanel();
}
downloadsCmd_alwaysOpenInSystemViewer() {
super.downloadsCmd_alwaysOpenInSystemViewer();
// We explicitly close the panel here to give the user the feedback that
// their click has been received, and we're handling the action.
DownloadsPanel.hidePanel();
}
downloadsCmd_show() {
let file = new FileUtils.File(this.download.target.path);
DownloadsCommon.showDownloadedFile(file);

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

@ -22,4 +22,6 @@
<command id="downloadsCmd_retry"/>
<command id="downloadsCmd_openReferrer"/>
<command id="downloadsCmd_clearDownloads"/>
<command id="downloadsCmd_openInSystemViewer"/>
<command id="downloadsCmd_alwaysOpenInSystemViewer"/>
</commandset>

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

@ -17,6 +17,13 @@
<menuitem command="downloadsCmd_unblock"
class="downloadUnblockMenuItem"
data-l10n-id="downloads-cmd-unblock"/>
<menuitem command="downloadsCmd_openInSystemViewer"
class="downloadUseSystemDefaultMenuItem"
data-l10n-id="downloads-cmd-use-system-default"/>
<menuitem command="downloadsCmd_alwaysOpenInSystemViewer"
type="checkbox"
class="downloadAlwaysUseSystemDefaultMenuItem"
data-l10n-id="downloads-cmd-always-use-system-default"/>
<menuitem command="downloadsCmd_show"
class="downloadShowMenuItem"
#ifdef XP_MACOSX

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

@ -38,6 +38,10 @@
oncommand="goDoCommand('downloadsCmd_copyLocation')"/>
<command id="downloadsCmd_clearList"
oncommand="goDoCommand('downloadsCmd_clearList')"/>
<command id="downloadsCmd_openInSystemViewer"
oncommand="goDoCommand('downloadsCmd_openInSystemViewer')"/>
<command id="downloadsCmd_alwaysOpenInSystemViewer"
oncommand="goDoCommand('downloadsCmd_alwaysOpenInSystemViewer')"/>
</commandset>
<!-- The panel has level="top" to ensure that it is never hidden by the
@ -75,6 +79,13 @@
<menuitem command="downloadsCmd_unblock"
class="downloadUnblockMenuItem"
data-l10n-id="downloads-cmd-unblock"/>
<menuitem command="downloadsCmd_openInSystemViewer"
class="downloadUseSystemDefaultMenuItem"
data-l10n-id="downloads-cmd-use-system-default"/>
<menuitem command="downloadsCmd_alwaysOpenInSystemViewer"
type="checkbox"
class="downloadAlwaysUseSystemDefaultMenuItem"
data-l10n-id="downloads-cmd-always-use-system-default"/>
<menuitem command="downloadsCmd_show"
class="downloadShowMenuItem"
#ifdef XP_MACOSX

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

@ -29,6 +29,26 @@ const TestCases = [
tabSelected: true,
},
},
{
name: "Download panel, system viewer menu items prefd off",
whichUI: "downloadPanel",
itemSelector: "#downloadsListBox richlistitem .downloadMainArea",
async userEvents(itemTarget, win) {
EventUtils.synthesizeMouseAtCenter(itemTarget, {}, win);
},
prefs: [
["browser.download.openInSystemViewerContextMenuItem", false],
["browser.download.alwaysOpenInSystemViewerContextMenuItem", false],
],
expected: {
downloadCount: 1,
newWindow: false,
opensTab: true,
tabSelected: true,
useSystemMenuItemDisabled: true,
alwaysMenuItemDisabled: true,
},
},
{
name: "Download panel, open from keyboard",
whichUI: "downloadPanel",
@ -109,6 +129,26 @@ const TestCases = [
tabSelected: true,
},
},
{
name: "Library all downloads dialog, system viewer menu items prefd off",
whichUI: "allDownloads",
async userEvents(itemTarget, win) {
// double click
await triggerDblclickOn(itemTarget, {}, win);
},
prefs: [
["browser.download.openInSystemViewerContextMenuItem", false],
["browser.download.alwaysOpenInSystemViewerContextMenuItem", false],
],
expected: {
downloadCount: 1,
newWindow: false,
opensTab: true,
tabSelected: true,
useSystemMenuItemDisabled: true,
alwaysMenuItemDisabled: true,
},
},
{
name: "Library all downloads dialog, open from keyboard",
whichUI: "allDownloads",
@ -189,6 +229,28 @@ const TestCases = [
tabSelected: true,
},
},
{
name: "about:downloads, system viewer menu items prefd off",
whichUI: "aboutDownloads",
itemSelector: "#downloadsRichListBox richlistitem .downloadContainer",
async userEvents(itemSelector, win) {
let browser = win.gBrowser.selectedBrowser;
is(browser.currentURI.spec, "about:downloads");
await contentTriggerDblclickOn(itemSelector, {}, browser);
},
prefs: [
["browser.download.openInSystemViewerContextMenuItem", false],
["browser.download.alwaysOpenInSystemViewerContextMenuItem", false],
],
expected: {
downloadCount: 1,
newWindow: false,
opensTab: true,
tabSelected: true,
useSystemMenuItemDisabled: true,
alwaysMenuItemDisabled: true,
},
},
{
name: "about:downloads, open in new window",
whichUI: "aboutDownloads",
@ -306,6 +368,66 @@ function contentTriggerDblclickOn(selector, eventModifiers = {}, browser) {
);
}
async function openContextMenu(itemElement, win = window) {
let popupShownPromise = BrowserTestUtils.waitForEvent(
itemElement.ownerDocument,
"popupshown"
);
EventUtils.synthesizeMouseAtCenter(
itemElement,
{
type: "contextmenu",
button: 2,
},
win
);
let { target } = await popupShownPromise;
return target;
}
async function verifyContextMenu(contextMenu, expected = {}) {
info("verifyContextMenu with expected: " + JSON.stringify(expected, null, 2));
let alwaysMenuItem = contextMenu.querySelector(
".downloadAlwaysUseSystemDefaultMenuItem"
);
let useSystemMenuItem = contextMenu.querySelector(
".downloadUseSystemDefaultMenuItem"
);
await TestUtils.waitForCondition(
() => BrowserTestUtils.is_visible(contextMenu),
"The context menu is visible"
);
await TestUtils.waitForTick();
is(
BrowserTestUtils.is_hidden(useSystemMenuItem),
expected.useSystemMenuItemDisabled,
`The 'Use system viewer' menu item was ${
expected.useSystemMenuItemDisabled ? "hidden" : "visible"
}`
);
is(
BrowserTestUtils.is_hidden(alwaysMenuItem),
expected.alwaysMenuItemDisabled,
`The 'Use system viewer' menu item was ${
expected.alwaysMenuItemDisabled ? "hidden" : "visible"
}`
);
if (!expected.useSystemMenuItemDisabled && expected.alwaysChecked) {
is(
alwaysMenuItem.getAttribute("checked"),
"true",
"The 'Always...' menu item is checked"
);
} else if (!expected.useSystemMenuItemDisabled) {
ok(
!alwaysMenuItem.hasAttribute("checked"),
"The 'Always...' menu item not checked"
);
}
}
async function createDownloadedFile(pathname, contents) {
let encoder = new TextEncoder();
let file = new FileUtils.File(pathname);
@ -352,7 +474,7 @@ async function addPDFDownload(itemData) {
return download;
}
async function testSetup(testData = {}) {
async function testSetup() {
// remove download files, empty out collections
let downloadList = await Downloads.getList(Downloads.ALL);
let downloadCount = (await downloadList.getAll()).length;
@ -379,6 +501,7 @@ async function testOpenPDFPreview({
whichUI,
itemSelector,
expected,
prefs = [],
userEvents,
isPrivate,
}) {
@ -386,6 +509,11 @@ async function testOpenPDFPreview({
// Wait for focus first
await promiseFocus();
await testSetup();
if (prefs.length) {
await SpecialPowers.pushPrefEnv({
set: prefs,
});
}
// Populate downloads database with the data required by this test.
info("Adding download objects");
@ -454,12 +582,16 @@ async function testOpenPDFPreview({
}
let itemTarget;
let contextMenu;
switch (whichUI) {
case "downloadPanel":
info("Opening download panel");
await openDownloadPanel(expected.downloadCount);
info("/Opening download panel");
itemTarget = document.querySelector(itemSelector);
contextMenu = uiWindow.document.querySelector("#downloadsContextMenu");
break;
case "allDownloads":
// we'll be interacting with the library dialog
@ -467,11 +599,17 @@ async function testOpenPDFPreview({
let listbox = uiWindow.document.getElementById("downloadsRichListBox");
ok(listbox, "download list box present");
// wait for the expected number of items in the view
await TestUtils.waitForCondition(
() => listbox.itemChildren.length == expected.downloadCount
);
// wait for the expected number of items in the view,
// and for the first item to be visible && clickable
await TestUtils.waitForCondition(() => {
return (
listbox.itemChildren.length == expected.downloadCount &&
BrowserTestUtils.is_visible(listbox.itemChildren[0])
);
});
itemTarget = listbox.itemChildren[0];
contextMenu = uiWindow.document.querySelector("#downloadsContextMenu");
break;
case "aboutDownloads":
info("Preparing about:downloads browser window");
@ -537,6 +675,21 @@ async function testOpenPDFPreview({
break;
}
if (contextMenu) {
info("trigger the contextmenu");
await openContextMenu(itemTarget || itemSelector, uiWindow);
info("context menu should be open, verify its menu items");
let expectedValues = {
useSystemMenuItemDisabled: false,
alwaysMenuItemDisabled: false,
...expected,
};
await verifyContextMenu(contextMenu, expectedValues);
contextMenu.hidePopup();
} else {
todo(contextMenu, "No context menu checks for test: " + name);
}
info("Executing user events");
await userEvents(itemTarget || itemSelector, uiWindow);
@ -594,6 +747,9 @@ async function testOpenPDFPreview({
await lastPBContextExitedPromise;
});
await downloadList.removeFinished();
if (prefs.length) {
await SpecialPowers.popPrefEnv();
}
}
// register the tests

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

@ -726,6 +726,9 @@ Download.prototype = {
* @param options.openWhere Optional string indicating how to open when handling
* download by opening the target file URI.
* One of "window", "tab", "tabshifted"
* @param options.useSystemDefault
* Optional value indicating how to handle launching this download,
* this time only. Will override the associated mimeInfo.preferredAction
* @return {Promise}
* @resolves When the instruction to launch the file has been
* successfully given to the operating system. Note that

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

@ -712,6 +712,9 @@ var DownloadIntegration = {
* @param options.openWhere Optional string indicating how to open when handling
* download by opening the target file URI.
* One of "window", "tab", "tabshifted"
* @param options.useSystemDefault
* Optional value indicating how to handle launching this download,
* this time only. Will override the associated mimeInfo.preferredAction
*
* @return {Promise}
* @resolves When the instruction to launch the file has been
@ -721,7 +724,7 @@ var DownloadIntegration = {
* @rejects JavaScript exception if there was an error trying to launch
* the file.
*/
async launchDownload(aDownload, { openWhere }) {
async launchDownload(aDownload, { openWhere, useSystemDefault = null }) {
let file = new FileUtils.File(aDownload.target.path);
// In case of a double extension, like ".tar.gz", we only
@ -794,7 +797,8 @@ var DownloadIntegration = {
const PDF_CONTENT_TYPE = "application/pdf";
if (
aDownload.handleInternally ||
(mimeInfo &&
(!useSystemDefault && // No explicit instruction was passed to launch this download using the default system viewer.
mimeInfo &&
(mimeInfo.type == PDF_CONTENT_TYPE ||
fileExtension?.toLowerCase() == "pdf") &&
!mimeInfo.alwaysAskBeforeHandling &&