diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/suite/components/downloads/content | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/suite/components/downloads/content')
8 files changed, 2169 insertions, 0 deletions
diff --git a/comm/suite/components/downloads/content/DownloadProgressListener.js b/comm/suite/components/downloads/content/DownloadProgressListener.js new file mode 100644 index 0000000000..b5bab95727 --- /dev/null +++ b/comm/suite/components/downloads/content/DownloadProgressListener.js @@ -0,0 +1,30 @@ +/* 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/. */ + +/** + * DownloadProgressListener "class" is used to help update download items shown + * in the Download Manager UI such as displaying amount transferred, transfer + * rate, and time left for each download. + */ +function DownloadProgressListener() {} + +DownloadProgressListener.prototype = { + onDownloadAdded: function(aDownload) { + gDownloadTreeView.addDownload(aDownload); + + // Update window title in-case we don't get all progress notifications + onUpdateProgress(); + }, + + onDownloadChanged: function(aDownload) { + gDownloadTreeView.updateDownload(aDownload); + + // Update window title + onUpdateProgress(); + }, + + onDownloadRemoved: function(aDownload) { + gDownloadTreeView.removeDownload(aDownload); + } +}; diff --git a/comm/suite/components/downloads/content/downloadmanager.js b/comm/suite/components/downloads/content/downloadmanager.js new file mode 100644 index 0000000000..d390e655dd --- /dev/null +++ b/comm/suite/components/downloads/content/downloadmanager.js @@ -0,0 +1,634 @@ +/* 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/. */ + +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetters(this, { + PluralForm: "resource://gre/modules/PluralForm.jsm", + Downloads: "resource://gre/modules/Downloads.jsm", + DownloadsCommon: "resource:///modules/DownloadsCommon.jsm", + PlacesUtils: "resource://gre/modules/PlacesUtils.jsm", + FileUtils: "resource://gre/modules/FileUtils.jsm", +}); + +var gDownloadTree; +var gDownloadTreeView; +var gDownloadList; +var gDownloadStatus; +var gDownloadListener; +var gSearchBox; + +function dmStartup() +{ + Downloads.getList(Downloads.PUBLIC).then(dmAsyncStartup); +} + +function dmAsyncStartup(aList) +{ + gDownloadList = aList; + + gDownloadTree = document.getElementById("downloadTree"); + gDownloadStatus = document.getElementById("statusbar-display"); + gSearchBox = document.getElementById("search-box"); + + // Insert as first controller on the whole window + window.controllers.insertControllerAt(0, dlTreeController); + + // We need to keep the view object around globally to access "local" + // non-nsITreeView methods + gDownloadTreeView = new DownloadTreeView(); + gDownloadTree.view = gDownloadTreeView; + + // The DownloadProgressListener (DownloadProgressListener.js) handles + // progress notifications. + gDownloadListener = new DownloadProgressListener(); + gDownloadList.addView(gDownloadListener); + + // correct keybinding command attributes which don't do our business yet + var key = document.getElementById("key_delete"); + if (key.hasAttribute("command")) + key.setAttribute("command", "cmd_stop"); + key = document.getElementById("key_delete2"); + if (key.hasAttribute("command")) + key.setAttribute("command", "cmd_stop"); + + gDownloadTree.focus(); + + if (gDownloadTree.view.rowCount > 0) + gDownloadTree.view.selection.select(0); +} + +function dmShutdown() +{ + gDownloadList.removeView(gDownloadListener); + window.controllers.removeController(dlTreeController); +} + +function searchDownloads(aInput) +{ + gDownloadTreeView.searchView(aInput); +} + +function sortDownloads(aEventTarget) +{ + var column = aEventTarget; + var colID = column.id; + var sortDirection = null; + + // If the target is a menuitem, handle it and forward to a column + if (/^menu_SortBy/.test(colID)) { + colID = colID.replace(/^menu_SortBy/, ""); + column = document.getElementById(colID); + var sortedColumn = gDownloadTree.columns.getSortedColumn(); + if (sortedColumn && sortedColumn.id == colID) + sortDirection = sortedColumn.element.getAttribute("sortDirection"); + else + sortDirection = "ascending"; + } + else if (colID == "menu_Unsorted") { + // calling .sortView() with an "unsorted" colID returns us to original order + colID = "unsorted"; + column = null; + sortDirection = "ascending"; + } + else if (colID == "menu_SortAscending" || colID == "menu_SortDescending") { + sortDirection = colID.replace(/^menu_Sort/, "").toLowerCase(); + var sortedColumn = gDownloadTree.columns.getSortedColumn(); + if (sortedColumn) { + colID = sortedColumn.id; + column = sortedColumn.element; + } + } + + // Abort if this is still no column + if (column && column.localName != "treecol") + return; + + // Abort on cyler columns, we don't sort them + if (column && column.getAttribute("cycler") == "true") + return; + + if (!sortDirection) { + // If not set above already, toggle the current direction + sortDirection = column.getAttribute("sortDirection") == "ascending" ? + "descending" : "ascending"; + } + + // Clear attributes on all columns, we're setting them again after sorting + for (let node = document.getElementById("Name"); node; node = node.nextSibling) { + node.removeAttribute("sortActive"); + node.removeAttribute("sortDirection"); + } + + // Actually sort the tree view + gDownloadTreeView.sortView(colID, sortDirection); + + if (column) { + // Set attributes to the sorting we did + column.setAttribute("sortActive", "true"); + column.setAttribute("sortDirection", sortDirection); + } +} + +async function removeDownload(aDownload) +{ + // Remove the associated history element first, if any, so that the views + // that combine history and session downloads won't resurrect the history + // download into the view just before it is deleted permanently. + try { + await PlacesUtils.history.remove(aDownload.source.url); + } catch (ex) { + Cu.reportError(ex); + } + let list = await Downloads.getList(Downloads.ALL); + await list.remove(aDownload); + await aDownload.finalize(true); +} + +function cancelDownload(aDownload) +{ + // This is the correct way to avoid race conditions when cancelling. + aDownload.cancel().catch(() => {}); + aDownload.removePartialData().catch(Cu.reportError); +} + +function openDownload(aDownload) +{ + let file = new FileUtils.File(aDownload.target.path); + DownloadsCommon.openDownloadedFile(file, null, window); +} + +function showDownload(aDownload) +{ + let file; + + if (aDownload.succeeded && + aDownload.target.exists) { + file = new FileUtils.File(aDownload.target.path); + } else { + file = new FileUtils.File(aDownload.target.partFilePath); + } + DownloadsCommon.showDownloadedFile(file); +} + +function showProperties(aDownload) +{ + openDialog("chrome://communicator/content/downloads/progressDialog.xul", + null, "chrome,titlebar,centerscreen,minimizable=yes,dialog=no", + { wrappedJSObject: aDownload }, true); +} + +function onTreeSelect(aEvent) +{ + var selectionCount = gDownloadTreeView.selection.count; + if (selectionCount == 1) { + var selItemData = gDownloadTreeView.getRowData(gDownloadTree.currentIndex); + gDownloadStatus.label = selItemData.target.path; + } else { + gDownloadStatus.label = ""; + } + + window.updateCommands("tree-select"); +} + +function onUpdateViewColumns(aMenuItem) +{ + while (aMenuItem) { + // Each menuitem should be checked if its column is not hidden. + var colID = aMenuItem.id.replace(/^menu_Toggle/, ""); + var column = document.getElementById(colID); + aMenuItem.setAttribute("checked", !column.hidden); + aMenuItem = aMenuItem.nextSibling; + } +} + +function toggleColumn(aMenuItem) +{ + var colID = aMenuItem.id.replace(/^menu_Toggle/, ""); + var column = document.getElementById(colID); + column.setAttribute("hidden", !column.hidden); +} + +function onUpdateViewSort(aMenuItem) +{ + var unsorted = true; + var ascending = true; + while (aMenuItem) { + switch (aMenuItem.id) { + case "": // separator + break; + case "menu_Unsorted": + if (unsorted) // this would work even if Unsorted was last + aMenuItem.setAttribute("checked", "true"); + break; + case "menu_SortAscending": + aMenuItem.setAttribute("disabled", unsorted); + if (!unsorted && ascending) + aMenuItem.setAttribute("checked", "true"); + break; + case "menu_SortDescending": + aMenuItem.setAttribute("disabled", unsorted); + if (!unsorted && !ascending) + aMenuItem.setAttribute("checked", "true"); + break; + default: + var colID = aMenuItem.id.replace(/^menu_SortBy/, ""); + var column = document.getElementById(colID); + var direction = column.getAttribute("sortDirection"); + if (column.getAttribute("sortActive") == "true" && direction) { + // We've found a sorted column. Remember its direction. + ascending = direction == "ascending"; + unsorted = false; + aMenuItem.setAttribute("checked", "true"); + } + } + aMenuItem = aMenuItem.nextSibling; + } +} + +// This is called by the progress listener. +var gLastComputedMean = -1; +var gLastActiveDownloads = 0; +function onUpdateProgress() +{ + var dls = gDownloadTreeView.getActiveDownloads(); + var numActiveDownloads = dls.length; + + // Use the default title and reset "last" values if there's no downloads + if (numActiveDownloads == 0) { + document.title = document.documentElement.getAttribute("statictitle"); + gLastComputedMean = -1; + gLastActiveDownloads = 0; + + return; + } + + // Establish the mean transfer speed and amount downloaded. + var mean = 0; + var base = 0; + for (var dl of dls) { + if (dl.totalBytes > 0) { + mean += dl.currentBytes; + base += dl.totalBytes; + } + } + + // Calculate the percent transferred, unless we don't have a total file size + var dlbundle = document.getElementById("dmBundle"); + if (base != 0) + mean = Math.floor((mean / base) * 100); + + // Update title of window + if (mean != gLastComputedMean || gLastActiveDownloads != numActiveDownloads) { + gLastComputedMean = mean; + gLastActiveDownloads = numActiveDownloads; + + var title; + if (base == 0) + title = dlbundle.getFormattedString("downloadsTitleFiles", + [numActiveDownloads]); + else + title = dlbundle.getFormattedString("downloadsTitlePercent", + [numActiveDownloads, mean]); + + // Get the correct plural form and insert number of downloads and percent + title = PluralForm.get(numActiveDownloads, title); + + document.title = title; + } +} + +function handlePaste() { + let trans = Cc["@mozilla.org/widget/transferable;1"] + .createInstance(Ci.nsITransferable); + trans.init(null); + + let flavors = ["text/x-moz-url", "text/unicode"]; + flavors.forEach(trans.addDataFlavor); + + Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard); + + // Getting the data or creating the nsIURI might fail + try { + let data = {}; + trans.getAnyTransferData({}, data, {}); + let [url, name] = data.value.QueryInterface(Ci + .nsISupportsString).data.split("\n"); + + if (!url) + return; + + DownloadURL(url, name || url, document); + } catch (ex) {} +} + +var dlTreeController = { + supportsCommand: function(aCommand) + { + switch (aCommand) { + case "cmd_play": + case "cmd_pause": + case "cmd_resume": + case "cmd_retry": + case "cmd_cancel": + case "cmd_remove": + case "cmd_stop": + case "cmd_open": + case "cmd_show": + case "cmd_openReferrer": + case "cmd_copyLocation": + case "cmd_properties": + case "cmd_paste": + case "cmd_selectAll": + case "cmd_clearList": + return true; + } + return false; + }, + + isCommandEnabled: function(aCommand) + { + var selectionCount = 0; + if (gDownloadTreeView && gDownloadTreeView.selection) + selectionCount = gDownloadTreeView.selection.count; + + var selItemData = []; + if (selectionCount) { + // walk all selected rows + let start = {}; + let end = {}; + let numRanges = gDownloadTreeView.selection.getRangeCount(); + for (let rg = 0; rg < numRanges; rg++) { + gDownloadTreeView.selection.getRangeAt(rg, start, end); + for (let row = start.value; row <= end.value; row++) + selItemData.push(gDownloadTreeView.getRowData(row)); + } + } + + switch (aCommand) { + case "cmd_play": + if (!selectionCount) + return false; + for (let dldata of selItemData) { + if (dldata.succeeded || (!dldata.stopped && !dldata.hasPartialData)) + return false; + } + return true; + case "cmd_pause": + if (!selectionCount) + return false; + for (let dldata of selItemData) { + if (dldata.stopped || !dldata.hasPartialData) + return false; + } + return true; + case "cmd_resume": + if (!selectionCount) + return false; + for (let dldata of selItemData) { + if (!dldata.stopped || !dldata.hasPartialData) + return false; + } + return true; + case "cmd_open": + return selectionCount == 1 && + selItemData[0].succeeded && + selItemData[0].target.exists; + case "cmd_show": + // target.exists is only set if the download finished and the target + // is still located there. + // For simplicity we just assume the target is there if the download + // has not succeeded e.g. is still in progress. This might be wrong + // but showDownload will deal with it. + return selectionCount == 1 && + ((selItemData[0].succeeded && + selItemData[0].target.exists) || + !selItemData[0].succeeded); + case "cmd_cancel": + if (!selectionCount) + return false; + for (let dldata of selItemData) { + if (dldata.stopped && !dldata.hasPartialData) + return false; + } + return true; + case "cmd_retry": + if (!selectionCount) + return false; + for (let dldata of selItemData) { + if (dldata.succeeded || !dldata.stopped || dldata.hasPartialData) + return false; + } + return true; + case "cmd_remove": + if (!selectionCount) + return false; + for (let dldata of selItemData) { + if (!dldata.stopped) + return false; + } + return true; + case "cmd_openReferrer": + return selectionCount == 1 && !!selItemData[0].source.referrer; + case "cmd_stop": + case "cmd_copyLocation": + return selectionCount > 0; + case "cmd_properties": + return selectionCount == 1; + case "cmd_selectAll": + return gDownloadTreeView.rowCount != selectionCount; + case "cmd_clearList": + // Since active downloads always sort before removable downloads, + // we only need to check that the last download has stopped. + return gDownloadTreeView.rowCount && + !gDownloadTreeView.getRowData(gDownloadTreeView.rowCount - 1).isActive; + case "cmd_paste": + return true; + default: + return false; + } + }, + + doCommand: function(aCommand) { + var selectionCount = 0; + if (gDownloadTreeView && gDownloadTreeView.selection) + selectionCount = gDownloadTreeView.selection.count; + + var selItemData = []; + if (selectionCount) { + // walk all selected rows + let start = {}; + let end = {}; + let numRanges = gDownloadTreeView.selection.getRangeCount(); + for (let rg = 0; rg < numRanges; rg++) { + gDownloadTreeView.selection.getRangeAt(rg, start, end); + for (let row = start.value; row <= end.value; row++) + selItemData.push(gDownloadTreeView.getRowData(row)); + } + } + + switch (aCommand) { + case "cmd_play": + for (let dldata of selItemData) { + if (!dldata.stopped) + dldata.cancel(); + else if (!dldata.succeeded) + dldata.start(); + } + break; + case "cmd_pause": + for (let dldata of selItemData) + dldata.cancel(); + break; + case "cmd_resume": + case "cmd_retry": + for (let dldata of selItemData) { + // Errors when retrying are already reported as download failures. + dldata.start(); + } + break; + case "cmd_cancel": + for (let dldata of selItemData) + cancelDownload(dldata); + break; + case "cmd_remove": + for (let dldata of selItemData) + removeDownload(dldata).catch(Cu.reportError); + break; + case "cmd_stop": + for (let dldata of selItemData) { + if (dldata.isActive) + cancelDownload(dldata); + else + gDownloadList.remove(dldata); + } + break; + case "cmd_open": + openDownload(selItemData[0]); + break; + case "cmd_show": + showDownload(selItemData[0]); + break; + case "cmd_openReferrer": + openUILink(selItemData[0].source.referrer); + break; + case "cmd_copyLocation": + var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper); + var uris = []; + for (let dldata of selItemData) + uris.push(dldata.source.url); + clipboard.copyString(uris.join("\n"), document); + break; + case "cmd_properties": + showProperties(selItemData[0]); + break; + case "cmd_selectAll": + gDownloadTreeView.selection.selectAll(); + break; + case "cmd_clearList": + // Remove each download starting from the end until we hit a download + // that is in progress + for (let idx = gDownloadTreeView.rowCount - 1; idx >= 0; idx--) { + let dldata = gDownloadTreeView.getRowData(idx); + if (!dldata.isActive) { + gDownloadList.remove(dldata); + } + } + + if (!gSearchBox.value) + break; + + // Clear the input as if the user did it and move focus to the list + gSearchBox.value = ""; + searchDownloads(""); + gDownloadTree.focus(); + break; + case "cmd_paste": + handlePaste(); + break; + } + }, + + onEvent: function(aEvent){ + switch (aEvent) { + case "tree-select": + this.onCommandUpdate(); + } + }, + + onCommandUpdate: function() { + var cmds = ["cmd_play", "cmd_pause", "cmd_resume", "cmd_retry", + "cmd_cancel", "cmd_remove", "cmd_stop", "cmd_open", "cmd_show", + "cmd_openReferrer", "cmd_copyLocation", "cmd_properties", + "cmd_selectAll", "cmd_clearList"]; + for (let command in cmds) + goUpdateCommand(cmds[command]); + } +}; + +var gDownloadDNDObserver = { + onDragStart: function (aEvent) + { + if (!gDownloadTreeView || + !gDownloadTreeView.selection || + !gDownloadTreeView.selection.count) + return; + + var selItemData = gDownloadTreeView.getRowData(gDownloadTree.currentIndex); + var file = new FileUtils.File(selItemData.target.path); + + if (!file.exists()) + return; + + var url = Services.io.newFileURI(file).spec; + var dt = aEvent.dataTransfer; + dt.mozSetDataAt("application/x-moz-file", file, 0); + dt.setData("text/uri-list", url + "\r\n"); + dt.setData("text/plain", url + "\n"); + dt.effectAllowed = "copyMove"; + if (gDownloadTreeView.selection.count == 1) + dt.setDragImage(gDownloadStatus, 16, 16); + }, + + onDragOver: function (aEvent) + { + if (disallowDrop(aEvent)) + return; + + var types = aEvent.dataTransfer.types; + if (types.includes("text/uri-list") || + types.includes("text/x-moz-url") || + types.includes("text/plain")) + aEvent.preventDefault(); + aEvent.stopPropagation(); + }, + + onDrop: function(aEvent) + { + if (disallowDrop(aEvent)) + return; + + var dt = aEvent.dataTransfer; + var url = dt.getData("URL"); + var name; + if (!url) { + url = dt.getData("text/x-moz-url") || dt.getData("text/plain"); + [url, name] = url.split("\n"); + } + if (url) { + let doc = dt.mozSourceNode ? dt.mozSourceNode.ownerDocument : document; + saveURL(url, name || url, null, true, true, null, doc); + } + } +}; + +function disallowDrop(aEvent) +{ + var dt = aEvent.dataTransfer; + var file = dt.mozGetDataAt("application/x-moz-file", 0); + // If this is a local file, Don't try to download it again. + return file && file instanceof Ci.nsIFile; +} diff --git a/comm/suite/components/downloads/content/downloadmanager.xul b/comm/suite/components/downloads/content/downloadmanager.xul new file mode 100644 index 0000000000..5633d284b6 --- /dev/null +++ b/comm/suite/components/downloads/content/downloadmanager.xul @@ -0,0 +1,452 @@ +<?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/. --> + +<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://communicator/skin/downloads/downloadmanager.css" type="text/css"?> + +<?xul-overlay href="chrome://communicator/content/tasksOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?> + +<!DOCTYPE window [ +<!ENTITY % downloadsDTD SYSTEM "chrome://communicator/locale/downloads/downloadmanager.dtd"> +%downloadsDTD; +<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> +%globalDTD; +]> + +<window id="downloadManager" + title="&downloadManager.title;" statictitle="&downloadManager.title;" + onload="dmStartup();" onunload="dmShutdown();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="500" height="400" screenX="10" screenY="10" + persist="width height screenX screenY sizemode" + toggletoolbar="true" + lightweightthemes="true" + lightweightthemesfooter="status-bar" + drawtitle="true" + windowtype="Download:Manager"> + + <script src="chrome://communicator/content/downloads/downloadmanager.js"/> + <script src="chrome://communicator/content/downloads/DownloadProgressListener.js"/> + <script src="chrome://communicator/content/downloads/treeView.js"/> + <script src="chrome://global/content/contentAreaUtils.js"/> + <script src="chrome://global/content/editMenuOverlay.js"/> + + <broadcaster id="Communicator:WorkMode"/> + + <stringbundleset id="stringbundleset"> + <stringbundle id="dmBundle" + src="chrome://communicator/locale/downloads/downloadmanager.properties"/> + </stringbundleset> + + <commandset id="dlWinCommands"> + <commandset id="tasksCommands"> + <!-- File Menu --> + <command id="cmd_close" oncommand="window.close()"/> + <!-- Search Box --> + <command id="cmd_search_focus" + oncommand="gSearchBox.focus();"/> + </commandset> + <commandset id="commandUpdate_Downloads" + commandupdater="true" + events="focus,tree-select" + oncommandupdate="dlTreeController.onCommandUpdate()"/> + + <commandset id="downloadCommands"> + <command id="cmd_play" + oncommand="goDoCommand('cmd_play');"/> + <command id="cmd_pause" + oncommand="goDoCommand('cmd_pause');"/> + <command id="cmd_resume" + oncommand="goDoCommand('cmd_resume');"/> + <command id="cmd_retry" + oncommand="goDoCommand('cmd_retry');"/> + <command id="cmd_cancel" + oncommand="goDoCommand('cmd_cancel');"/> + <command id="cmd_remove" + oncommand="goDoCommand('cmd_remove');"/> + <command id="cmd_stop" + oncommand="goDoCommand('cmd_stop');"/> + <command id="cmd_open" + oncommand="goDoCommand('cmd_open');"/> + <command id="cmd_show" + oncommand="goDoCommand('cmd_show');"/> + <command id="cmd_openReferrer" + oncommand="goDoCommand('cmd_openReferrer');"/> + <command id="cmd_copyLocation" + oncommand="goDoCommand('cmd_copyLocation');"/> + <command id="cmd_properties" + oncommand="goDoCommand('cmd_properties');"/> + <command id="cmd_clearList" + oncommand="goDoCommand('cmd_clearList');"/> + </commandset> + </commandset> + + <keyset id="tasksKeys"> + <!-- File Menu --> + <key id="key_open" + keycode="VK_RETURN" + command="cmd_open"/> + <key id="key_close"/> + <!-- Edit Menu --> + <key id="key_cut"/> + <key id="key_copy"/> + <key id="key_paste" + command="cmd_paste"/> + <key id="key_play" + key=" " + command="cmd_play"/> + <key id="key_delete"/> + <key id="key_delete2"/> + <key id="key_selectAll"/> + <!-- Search Box --> + <key id="key_search_focus" + command="cmd_search_focus" + key="&search.key;" + modifiers="accel"/> + </keyset> + + <popupset id="downloadPopupset"> + <menupopup id="downloadContext"> + <menuitem id="dlContext-pause" + label="&cmd.pause.label;" + accesskey="&cmd.pause.accesskey;" + command="cmd_pause"/> + <menuitem id="dlContext-resume" + label="&cmd.resume.label;" + accesskey="&cmd.resume.accesskey;" + command="cmd_resume"/> + <menuitem id="dlContext-retry" + label="&cmd.retry.label;" + accesskey="&cmd.retry.accesskey;" + command="cmd_retry"/> + <menuitem id="dlContext-cancel" + label="&cmd.cancel.label;" + accesskey="&cmd.cancel.accesskey;" + command="cmd_cancel"/> + <menuitem id="dlContext-remove" + label="&cmd.remove.label;" + accesskey="&cmd.remove.accesskey;" + command="cmd_remove"/> + <menuseparator/> + <menuitem id="dlContext-open" + label="&cmd.open.label;" + accesskey="&cmd.open.accesskey;" + command="cmd_open" + default="true"/> + <menuitem id="dlContext-show" + label="&cmd.show.label;" + accesskey="&cmd.show.accesskey;" + command="cmd_show"/> + <menuitem id="dlContext-openReferrer" + label="&cmd.goToDownloadPage.label;" + accesskey="&cmd.goToDownloadPage.accesskey;" + command="cmd_openReferrer"/> + <menuitem id="dlContext-copyLocation" + label="&cmd.copyDownloadLink.label;" + accesskey="&cmd.copyDownloadLink.accesskey;" + command="cmd_copyLocation"/> + <menuitem id="dlContext-properties" + label="&cmd.properties.label;" + accesskey="&cmd.properties.accesskey;" + command="cmd_properties"/> + <menuseparator/> + <menuitem id="context-selectall"/> + </menupopup> + </popupset> + + <vbox id="titlebar"/> + + <toolbox id="download-toolbox"> + <menubar id="download-menubar" + grippytooltiptext="&menuBar.tooltip;"> + <menu id="menu_File"> + <menupopup id="menu_FilePopup"> + <menuitem id="dlMenu_open" + label="&cmd.open.label;" + accesskey="&cmd.open.accesskey;" + key="key_open" + command="cmd_open"/> + <menuitem id="dlMenu_show" + label="&cmd.show.label;" + accesskey="&cmd.show.accesskey;" + command="cmd_show"/> + <menuitem id="dlMenu_openReferrer" + label="&cmd.goToDownloadPage.label;" + accesskey="&cmd.goToDownloadPage.accesskey;" + command="cmd_openReferrer"/> + <menuitem id="dlMenu_properties" + label="&cmd.properties.label;" + accesskey="&cmd.properties.accesskey;" + command="cmd_properties"/> + <menuseparator/> + <menuitem id="menu_close"/> + </menupopup> + </menu> + <menu id="menu_Edit"> + <menupopup id="menu_EditPopup"> + <menuitem id="dlMenu_pause" + label="&cmd.pause.label;" + accesskey="&cmd.pause.accesskey;" + command="cmd_pause"/> + <menuitem id="dlMenu_resume" + label="&cmd.resume.label;" + accesskey="&cmd.resume.accesskey;" + command="cmd_resume"/> + <menuitem id="dlMenu_retry" + label="&cmd.retry.label;" + accesskey="&cmd.retry.accesskey;" + command="cmd_retry"/> + <menuitem id="dlMenu_cancel" + label="&cmd.cancel.label;" + accesskey="&cmd.cancel.accesskey;" + command="cmd_cancel"/> + <menuseparator/> + <menuitem id="dlMenu_remove" + label="&cmd.remove.label;" + accesskey="&cmd.remove.accesskey;" + command="cmd_remove"/> + <menuitem id="dlMenu_copyLocation" + label="&cmd.copyDownloadLink.label;" + accesskey="&cmd.copyDownloadLink.accesskey;" + command="cmd_copyLocation"/> + <menuseparator/> + <menuitem id="dlMenu_clearList" + label="&cmd.clearList.label;" + accesskey="&cmd.clearList.accesskey;" + command="cmd_clearList"/> + <menuitem id="menu_selectAll"/> + </menupopup> + </menu> + <menu id="menu_View"> + <menupopup id="menu_ViewPopup"> + <menu id="menu_ViewColumns" + label="&view.columns.label;" + accesskey="&view.columns.accesskey;"> + <menupopup onpopupshowing="onUpdateViewColumns(this.firstChild);" + oncommand="toggleColumn(event.target);"> + <menuitem id="menu_ToggleName" type="checkbox" disabled="true" + label="&col.name.label;" + accesskey="&col.name.accesskey;"/> + <menuitem id="menu_ToggleStatus" type="checkbox" + label="&col.status.label;" + accesskey="&col.status.accesskey;"/> + <menuitem id="menu_ToggleActionPlay" type="checkbox" + label="&col.actionPlay.label;" + accesskey="&col.actionPlay.accesskey;"/> + <menuitem id="menu_ToggleActionStop" type="checkbox" + label="&col.actionStop.label;" + accesskey="&col.actionStop.accesskey;"/> + <menuitem id="menu_ToggleProgress" type="checkbox" + label="&col.progress.label;" + accesskey="&col.progress.accesskey;"/> + <menuitem id="menu_ToggleTimeRemaining" type="checkbox" + label="&col.timeremaining.label;" + accesskey="&col.timeremaining.accesskey;"/> + <menuitem id="menu_ToggleTransferred" type="checkbox" + label="&col.transferred.label;" + accesskey="&col.transferred.accesskey;"/> + <menuitem id="menu_ToggleTransferRate" type="checkbox" + label="&col.transferrate.label;" + accesskey="&col.transferrate.accesskey;"/> + <menuitem id="menu_ToggleTimeElapsed" type="checkbox" + label="&col.timeelapsed.label;" + accesskey="&col.timeelapsed.accesskey;"/> + <menuitem id="menu_ToggleStartTime" type="checkbox" + label="&col.starttime.label;" + accesskey="&col.starttime.accesskey;"/> + <menuitem id="menu_ToggleEndTime" type="checkbox" + label="&col.endtime.label;" + accesskey="&col.endtime.accesskey;"/> + <menuitem id="menu_ToggleProgressPercent" type="checkbox" + label="&col.progresstext.label;" + accesskey="&col.progresstext.accesskey;"/> + <menuitem id="menu_ToggleSource" type="checkbox" + label="&col.source.label;" + accesskey="&col.source.accesskey;"/> + </menupopup> + </menu> + <menu id="menu_ViewSortBy" label="&view.sortBy.label;" + accesskey="&view.sortBy.accesskey;"> + <menupopup onpopupshowing="onUpdateViewSort(this.firstChild);" + oncommand="sortDownloads(event.target);"> + <menuitem id="menu_Unsorted" type="radio" name="columns" + label="&view.unsorted.label;" + accesskey="&view.unsorted.accesskey;"/> + <menuseparator/> + <menuitem id="menu_SortByName" type="radio" name="columns" + label="&col.name.label;" + accesskey="&col.name.accesskey;"/> + <menuitem id="menu_SortByStatus" type="radio" name="columns" + label="&col.status.label;" + accesskey="&col.status.accesskey;"/> + <menuitem id="menu_SortByProgress" type="radio" name="columns" + label="&col.progress.label;" + accesskey="&col.progress.accesskey;"/> + <menuitem id="menu_SortByTimeRemaining" type="radio" name="columns" + label="&col.timeremaining.label;" + accesskey="&col.timeremaining.accesskey;"/> + <menuitem id="menu_SortByTransferred" type="radio" name="columns" + label="&col.transferred.label;" + accesskey="&col.transferred.accesskey;"/> + <menuitem id="menu_SortByTransferRate" type="radio" name="columns" + label="&col.transferrate.label;" + accesskey="&col.transferrate.accesskey;"/> + <menuitem id="menu_SortByTimeElapsed" type="radio" name="columns" + label="&col.timeelapsed.label;" + accesskey="&col.timeelapsed.accesskey;"/> + <menuitem id="menu_SortByStartTime" type="radio" name="columns" + label="&col.starttime.label;" + accesskey="&col.starttime.accesskey;"/> + <menuitem id="menu_SortByEndTime" type="radio" name="columns" + label="&col.endtime.label;" + accesskey="&col.endtime.accesskey;"/> + <menuitem id="menu_SortByProgressPercent" type="radio" name="columns" + label="&col.progresstext.label;" + accesskey="&col.progresstext.accesskey;"/> + <menuitem id="menu_SortBySource" type="radio" name="columns" + label="&col.source.label;" + accesskey="&col.source.accesskey;"/> + <menuseparator/> + <menuitem id="menu_SortAscending" type="radio" name="direction" + label="&view.sortAscending.label;" + accesskey="&view.sortAscending.accesskey;"/> + <menuitem id="menu_SortDescending" type="radio" name="direction" + label="&view.sortDescending.label;" + accesskey="&view.sortDescending.accesskey;"/> + </menupopup> + </menu> + </menupopup> + </menu> + <menu id="tasksMenu"> + <menupopup id="taskPopup"> + <menuitem id="dlMenu_find" + label="&search.label;" + accesskey="&search.accesskey;" + hidden="true" + command="cmd_search_focus" + key="key_search_focus"/> + <menuseparator hidden="true"/> + </menupopup> + </menu> + <menu id="windowMenu"/> + <menu id="menu_Help"/> + </menubar> + <toolbar class="chromeclass-toolbar" + id="downloadToolbar" + align="center" + grippytooltiptext="&searchBar.tooltip;"> + <textbox id="search-box" + clickSelectsAll="true" + type="search" + hidden="true" + aria-controls="downloadTree" + class="compact" + placeholder="&search.placeholder;" + oncommand="searchDownloads(this.value);"/> + <spacer flex="1"/> + <button id="clearListButton" command="cmd_clearList" + label="&cmd.clearList.label;" + accesskey="&cmd.clearList.accesskey;" + tooltiptext="&cmd.clearList.tooltip;"/> + </toolbar> + </toolbox> + + <tree id="downloadTree" + flex="1" type="downloads" + class="plain" + context="downloadContext" + enableColumnDrag="true" + onselect="onTreeSelect(event);"> + <treecols context="" onclick="sortDownloads(event.target)"> + <treecol id="Name" + label="&col.name.label;" + tooltiptext="&col.name.tooltip;" + flex="3" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol id="Status" hidden="true" + label="&col.status.label;" + tooltiptext="&col.status.tooltip;" + flex="1" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol id="ActionPlay" cycler="true" + label="&col.actionPlay.label;" + tooltiptext="&col.actionPlay.tooltip;" + class="treecol-image" fixed="true" + persist="hidden ordinal"/> + <splitter class="tree-splitter"/> + <treecol id="ActionStop" cycler="true" + label="&col.actionStop.label;" + tooltiptext="&col.actionStop.tooltip;" + class="treecol-image" fixed="true" + persist="hidden ordinal"/> + <splitter class="tree-splitter"/> + <treecol id="Progress" type="progressmeter" + label="&col.progress.label;" + tooltiptext="&col.progress.tooltip;" + flex="3" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol id="ProgressPercent" hidden="true" + label="&col.progresstext.label;" + tooltiptext="&col.progresstext.tooltip;" + flex="1" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol id="TimeRemaining" + label="&col.timeremaining.label;" + tooltiptext="&col.timeremaining.tooltip;" + flex="1" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol id="Transferred" + label="&col.transferred.label;" + tooltiptext="&col.transferred.tooltip;" + flex="1" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol id="TransferRate" + label="&col.transferrate.label;" + tooltiptext="&col.transferrate.tooltip;" + flex="1" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol id="TimeElapsed" hidden="true" + label="&col.timeelapsed.label;" + tooltiptext="&col.timeelapsed.tooltip;" + flex="1" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol id="StartTime" hidden="true" + label="&col.starttime.label;" + tooltiptext="&col.starttime.tooltip;" + flex="1" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol id="EndTime" hidden="true" + label="&col.endtime.label;" + tooltiptext="&col.endtime.tooltip;" + flex="1" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol id="Source" hidden="true" + label="&col.source.label;" + tooltiptext="&col.source.tooltip;" + flex="1" + persist="width hidden ordinal sortActive sortDirection"/> + </treecols> + <treechildren ondblclick="goDoCommand('cmd_open');" + ondragstart="gDownloadDNDObserver.onDragStart(event);" + ondragover="gDownloadDNDObserver.onDragOver(event);" + ondrop="gDownloadDNDObserver.onDrop(event);"/> + </tree> + <statusbar id="status-bar" class="chromeclass-status"> + <statusbarpanel id="statusbar-display" flex="1"/> + <statusbarpanel class="statusbarpanel-iconic" id="offline-status"/> + </statusbar> + +</window> diff --git a/comm/suite/components/downloads/content/progressDialog.js b/comm/suite/components/downloads/content/progressDialog.js new file mode 100644 index 0000000000..dae93f7fe9 --- /dev/null +++ b/comm/suite/components/downloads/content/progressDialog.js @@ -0,0 +1,240 @@ +/* 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/. */ + +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetters(this, { + DownloadUtils: "resource://gre/modules/DownloadUtils.jsm", + DownloadsCommon: "resource:///modules/DownloadsCommon.jsm", +}); + +var gDownload; +var gDownloadBundle; + +var gDlList; +var gDlStatus; +var gDlListener; +var gDlSize; +var gTimeLeft; +var gProgressMeter; +var gProgressText; +var gCloseWhenDone; + +function progressStartup() { + gDownload = window.arguments[0].wrappedJSObject; + Downloads.getList(gDownload.source.isPrivate ? Downloads.PRIVATE : Downloads.PUBLIC).then(progressAsyncStartup); +} + +function progressAsyncStartup(aList) { + gDlList = aList; + + // cache elements to save .getElementById() calls + gDownloadBundle = document.getElementById("dmBundle"); + gDlStatus = document.getElementById("dlStatus"); + gDlSize = document.getElementById("dlSize"); + gTimeLeft = document.getElementById("timeLeft"); + gProgressMeter = document.getElementById("progressMeter"); + gProgressText = document.getElementById("progressText"); + gCloseWhenDone = document.getElementById("closeWhenDone"); + + // Insert as first controller on the whole window + window.controllers.insertControllerAt(0, ProgressDlgController); + + if (gDownload.isPrivate) + gCloseWhenDone.hidden = true; + else + gCloseWhenDone.checked = Services.prefs.getBoolPref("browser.download.progress.closeWhenDone"); + + if (gDownload.succeeded) { + if (gCloseWhenDone.checked && !window.arguments[1]) + window.close(); + } + + var fName = document.getElementById("fileName"); + var fSource = document.getElementById("fileSource"); + fName.label = gDownload.displayName; + fName.tooltipText = gDownload.target.path; + var uri = Services.io.newURI(gDownload.source.url); + var fromString; + try { + fromString = uri.host; + } + catch (e) { } + if (!fromString) + fromString = uri.prePath; + fSource.label = gDownloadBundle.getFormattedString("fromSource", [fromString]); + fSource.tooltipText = gDownload.source.url; + + // The DlProgressListener handles progress notifications. + gDlListener = new DlProgressListener(); + gDlList.addView(gDlListener); + + updateDownload(); + updateButtons(); + window.updateCommands("dlstate-change"); +} + +function progressShutdown() { + gDlList.removeView(gDlListener); + window.controllers.removeController(ProgressDlgController); + if (!gCloseWhenDone.hidden) + Services.prefs.setBoolPref("browser.download.progress.closeWhenDone", + gCloseWhenDone.checked); +} + +function updateDownload() { + if (gDownload.hasProgress) { + gProgressText.value = gDownloadBundle.getFormattedString("percentFormat", + [gDownload.progress]); + gProgressText.hidden = false; + gProgressMeter.value = gDownload.progress; + gProgressMeter.mode = "determined"; + } else { + gProgressText.hidden = true; + gProgressMeter.mode = "undetermined"; + } + if (gDownload.stopped) { + gProgressMeter.style.opacity = 0.5; + } else { + gProgressMeter.style.opacity = 1; + } + // Update window title + let statusString = DownloadsCommon.stateOfDownloadText(gDownload); + + if (gDownload.hasProgress) { + document.title = gDownloadBundle.getFormattedString("progressTitlePercent", + [gDownload.progress, + gDownload.displayName, + statusString]); + } + else { + document.title = gDownloadBundle.getFormattedString("progressTitle", + [gDownload.displayName, + statusString]); + } + + // download size / transferred bytes + gDlSize.value = DownloadsCommon.getTransferredBytes(gDownload); + + // time remaining + gTimeLeft.value = DownloadsCommon.getTimeRemaining(gDownload); + + // download status + gDlStatus.value = statusString; + +} + +function updateButtons() { + document.getElementById("pauseButton").hidden = !ProgressDlgController.isCommandEnabled("cmd_pause"); + document.getElementById("resumeButton").hidden = !ProgressDlgController.isCommandEnabled("cmd_resume"); + document.getElementById("retryButton").hidden = !ProgressDlgController.isCommandEnabled("cmd_retry"); + document.getElementById("cancelButton").hidden = !ProgressDlgController.isCommandEnabled("cmd_cancel"); +} + +/** + * DlProgressListener "class" is used to help update download items shown + * in the progress dialog such as displaying amount transferred, transfer + * rate, and time left for the download. + * + * This class implements the downloadProgressListener interface. + */ +function DlProgressListener() {} + +DlProgressListener.prototype = { + onDownloadChanged: function(aDownload) { + if (aDownload == gDownload) { + if (gCloseWhenDone.checked && aDownload.succeeded) { + window.close(); + } + updateDownload(); + updateButtons(); + window.updateCommands("dlstate-change"); + } + }, + + onDownloadRemoved: function(aDownload) { + if (aDownload == gDownload) + window.close(); + } +}; + +var ProgressDlgController = { + supportsCommand: function(aCommand) { + switch (aCommand) { + case "cmd_pause": + case "cmd_resume": + case "cmd_retry": + case "cmd_cancel": + case "cmd_open": + case "cmd_show": + case "cmd_openReferrer": + case "cmd_copyLocation": + return true; + } + return false; + }, + + isCommandEnabled: function(aCommand) { + switch (aCommand) { + case "cmd_pause": + return !gDownload.stopped && gDownload.hasPartialData; + case "cmd_resume": + return gDownload.stopped && gDownload.hasPartialData; + case "cmd_open": + return gDownload.succeeded && gDownload.target.exists; + case "cmd_show": + return gDownload.target.exists; + case "cmd_cancel": + return !gDownload.stopped || gDownload.hasPartialData; + case "cmd_retry": + return !gDownload.succeeded && gDownload.stopped && !gDownload.hasPartialData; + case "cmd_openReferrer": + return !!gDownload.source.referrer; + case "cmd_copyLocation": + return true; + default: + return false; + } + }, + + doCommand: function(aCommand) { + switch (aCommand) { + case "cmd_pause": + gDownload.cancel(); + break; + case "cmd_resume": + case "cmd_retry": + gDownload.start(); + break; + case "cmd_cancel": + cancelDownload(gDownload); + break; + case "cmd_open": + openDownload(gDownload); + break; + case "cmd_show": + showDownload(gDownload); + break; + case "cmd_openReferrer": + openUILink(gDownload.source.referrer); + break; + case "cmd_copyLocation": + var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper); + clipboard.copyString(gDownload.source.url); + break; + } + }, + + onEvent: function(aEvent) { + }, + + onCommandUpdate: function() { + var cmds = ["cmd_pause", "cmd_resume", "cmd_retry", "cmd_cancel", + "cmd_open", "cmd_show", "cmd_openReferrer", "cmd_copyLocation"]; + for (let command in cmds) + goUpdateCommand(cmds[command]); + } +}; diff --git a/comm/suite/components/downloads/content/progressDialog.xul b/comm/suite/components/downloads/content/progressDialog.xul new file mode 100644 index 0000000000..cb8178f6fd --- /dev/null +++ b/comm/suite/components/downloads/content/progressDialog.xul @@ -0,0 +1,108 @@ +<?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/. --> + +<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://communicator/skin/downloads/downloadmanager.css" type="text/css"?> + +<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?> + +<!DOCTYPE window SYSTEM "chrome://communicator/locale/downloads/progressDialog.dtd"> + +<window id="dlProgressWindow" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="progressStartup();" onunload="progressShutdown();" + title="&progress.title;" + persist="screenX screenY" + style="width:40em;"> + + <script src="chrome://communicator/content/downloads/downloadmanager.js"/> + <script src="chrome://communicator/content/downloads/progressDialog.js"/> + + <stringbundleset id="stringbundleset"> + <stringbundle id="dmBundle" + src="chrome://communicator/locale/downloads/downloadmanager.properties"/> + </stringbundleset> + + <commandset id="dlProgressCommands"> + <commandset id="commandUpdate_DlProgress" + commandupdater="true" + events="focus,dlstate-change" + oncommandupdate="ProgressDlgController.onCommandUpdate();"/> + + <commandset id="downloadCommands"> + <command id="cmd_pause" + oncommand="goDoCommand('cmd_pause');"/> + <command id="cmd_resume" + oncommand="goDoCommand('cmd_resume');"/> + <command id="cmd_retry" + oncommand="goDoCommand('cmd_retry');"/> + <command id="cmd_cancel" + oncommand="goDoCommand('cmd_cancel');"/> + <command id="cmd_open" + oncommand="goDoCommand('cmd_open');"/> + <command id="cmd_show" + oncommand="goDoCommand('cmd_show');"/> + <command id="cmd_openReferrer" + oncommand="goDoCommand('cmd_openReferrer');"/> + <command id="cmd_copyLocation" + oncommand="goDoCommand('cmd_copyLocation');"/> + <command id="cmd_close" oncommand="window.close();"/> + </commandset> + </commandset> + + <keyset> + <key key="&closeWindow.key;" modifiers="accel" command="cmd_close"/> + <key keycode="VK_ESCAPE" command="cmd_close"/> + <key key="." modifiers="meta" command="cmd_close"/> + </keyset> + + <hbox align="end"> + <vbox flex="1" align="start"> + <button id="fileName" crop="center" label="" type="menu"> + <menupopup id="file-popup"> + <menuitem id="dlContext-open" + label="&cmd.open.label;" + accesskey="&cmd.open.accesskey;" + command="cmd_open"/> + <menuitem id="dlContext-show" + label="&cmd.show.label;" + accesskey="&cmd.show.accesskey;" + command="cmd_show"/> + </menupopup> + </button> + <button id="fileSource" crop="center" label="" type="menu"> + <menupopup id="source-popup"> + <menuitem id="dlContext-openReferrer" + label="&cmd.goToDownloadPage.label;" + accesskey="&cmd.goToDownloadPage.accesskey;" + command="cmd_openReferrer"/> + <menuitem id="dlContext-copyLocation" + label="&cmd.copyDownloadLink.label;" + accesskey="&cmd.copyDownloadLink.accesskey;" + command="cmd_copyLocation"/> + </menupopup> + </button> + <label id="dlSize" value=""/> + <label id="timeLeft" value=""/> + <label id="dlStatus" value=""/> + </vbox> + <button id="pauseButton" class="mini-button" + command="cmd_pause" tooltiptext="&cmd.pause.tooltip;"/> + <button id="resumeButton" class="mini-button" + command="cmd_resume" tooltiptext="&cmd.resume.tooltip;"/> + <button id="retryButton" class="mini-button" + command="cmd_retry" tooltiptext="&cmd.retry.tooltip;"/> + <button id="cancelButton" class="mini-button" + command="cmd_cancel" tooltiptext="&cmd.cancel.tooltip;"/> + </hbox> + <hbox id="progressBox"> + <progressmeter id="progressMeter" mode="determined" flex="1"/> + <label id="progressText" value=""/> + </hbox> + <checkbox id="closeWhenDone" + label="&closeWhenDone.label;" + accesskey="&closeWhenDone.accesskey;"/> +</window> diff --git a/comm/suite/components/downloads/content/treeView.js b/comm/suite/components/downloads/content/treeView.js new file mode 100644 index 0000000000..03e1c48a11 --- /dev/null +++ b/comm/suite/components/downloads/content/treeView.js @@ -0,0 +1,483 @@ +/* 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/. */ + +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetters(this, { + DownloadUtils: "resource://gre/modules/DownloadUtils.jsm", + DownloadsCommon: "resource:///modules/DownloadsCommon.jsm", + DownloadHistory: "resource://gre/modules/DownloadHistory.jsm", +}); + +function DownloadTreeView() { + this._dlList = []; + this._searchTerms = []; + this.dateTimeFormatter = + new Services.intl.DateTimeFormat(undefined, + {dateStyle: "short", + timeStyle: "long"}); +} + +DownloadTreeView.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView]), + + // ***** nsITreeView attributes and methods ***** + get rowCount() { + return this._dlList.length; + }, + + selection: null, + + getRowProperties: function(aRow) { + let dl = this._dlList[aRow]; + // (in)active + let properties = dl.isActive ? "active": "inactive"; + // resumable + if (dl.hasPartialData) + properties += " resumable"; + + // Download states + let state = DownloadsCommon.stateOfDownload(dl); + switch (state) { + case DownloadsCommon.DOWNLOAD_PAUSED: + properties += " paused"; + break; + case DownloadsCommon.DOWNLOAD_DOWNLOADING: + properties += " downloading"; + break; + case DownloadsCommon.DOWNLOAD_FINISHED: + properties += " finished"; + break; + case DownloadsCommon.DOWNLOAD_FAILED: + properties += " failed"; + break; + case DownloadsCommon.DOWNLOAD_CANCELED: + properties += " canceled"; + break; + case DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL: // Parental Controls + case DownloadsCommon.DOWNLOAD_BLOCKED_POLICY: // Security Zone Policy + case DownloadsCommon.DOWNLOAD_DIRTY: // possible virus/spyware + properties += " blocked"; + break; + } + + return properties; + }, + getCellProperties: function(aRow, aColumn) { + // Append all row properties to the cell + return this.getRowProperties(aRow); + }, + getColumnProperties: function(aColumn) { return ""; }, + isContainer: function(aRow) { return false; }, + isContainerOpen: function(aRow) { return false; }, + isContainerEmpty: function(aRow) { return false; }, + isSeparator: function(aRow) { return false; }, + isSorted: function() { return false; }, + canDrop: function(aIdx, aOrientation) { return false; }, + drop: function(aIdx, aOrientation) { }, + getParentIndex: function(aRow) { return -1; }, + hasNextSibling: function(aRow, aAfterIdx) { return false; }, + getLevel: function(aRow) { return 0; }, + + getImageSrc: function(aRow, aColumn) { + if (aColumn.id == "Name") + return "moz-icon://" + this._dlList[aRow].target.path + "?size=16"; + return ""; + }, + + getProgressMode: function(aRow, aColumn) { + if (aColumn.id == "Progress") + return this._dlList[aRow].progressMode; + return Ci.nsITreeView.PROGRESS_NONE; + }, + + getCellValue: function(aRow, aColumn) { + if (aColumn.id == "Progress") + return this._dlList[aRow].progress; + return ""; + }, + + getCellText: function(aRow, aColumn) { + let dl = this._dlList[aRow]; + switch (aColumn.id) { + case "Name": + return dl.displayName; + case "Status": + return DownloadsCommon.stateOfDownloadText(dl); + case "Progress": + if (dl.isActive) + return dl.progress; + return DownloadsCommon.stateOfDownloadText(dl); + case "ProgressPercent": + return dl.succeeded ? 100 : dl.progress; + case "TimeRemaining": + return DownloadsCommon.getTimeRemaining(dl); + case "Transferred": + return DownloadsCommon.getTransferredBytes(dl); + case "TransferRate": + let state = DownloadsCommon.stateOfDownload(dl); + switch (state) { + case DownloadsCommon.DOWNLOAD_DOWNLOADING: + let [rate, unit] = DownloadUtils.convertByteUnits(dl.speed); + return this._dlbundle.getFormattedString("speedFormat", [rate, unit]); + case DownloadsCommon.DOWNLOAD_PAUSED: + return this._dlbundle.getString("statePaused"); + case DownloadsCommon.DOWNLOAD_NOTSTARTED: + return this._dlbundle.getString("stateNotStarted"); + } + return ""; + case "TimeElapsed": + // With no end time persisted in the downloads backend this is + // utterly useless unless the download is progressing. + if (DownloadsCommon.stateOfDownload(dl) == + DownloadsCommon.DOWNLOAD_DOWNLOADING && dl.startTime) { + let seconds = (Date.now() - dl.startTime) / 1000; + let [time1, unit1, time2, unit2] = + DownloadUtils.convertTimeUnits(seconds); + if (seconds < 3600 || time2 == 0) { + return this._dlbundle.getFormattedString("timeSingle", [time1, unit1]); + } + return this._dlbundle.getFormattedString("timeDouble", [time1, unit1, time2, unit2]); + } + return ""; + case "StartTime": + if (dl.startTime) { + return this.dateTimeFormatter.format(dl.startTime); + } + return ""; + case "EndTime": + // This might end with an exception if it is an unsupported uri + // scheme. + let metaData = DownloadHistory.getPlacesMetaDataFor(dl.source.url); + + if (metaData.endTime) { + return this.dateTimeFormatter.format(metaData.endTime); + } + return ""; + case "Source": + return dl.source.url; + } + return ""; + }, + + setTree: function(aTree) { + this._tree = aTree; + this._dlbundle = document.getElementById("dmBundle"); + }, + + toggleOpenState: function(aRow) { }, + cycleHeader: function(aColumn) { }, + selectionChanged: function() { }, + cycleCell: function(aRow, aColumn) { + var dl = this._dlList[aRow]; + switch (aColumn.id) { + case "ActionPlay": + if (dl.stopped) { + if (!dl.succeeded) + dl.start(); + } else { + if (dl.hasPartialData) + dl.cancel(); + } + break; + case "ActionStop": + if (dl.isActive) + cancelDownload(dl); + else + removeDownload(dl); + break; + } + }, + isEditable: function(aRow, aColumn) { return false; }, + isSelectable: function(aRow, aColumn) { return false; }, + setCellValue: function(aRow, aColumn, aText) { }, + setCellText: function(aRow, aColumn, aText) { }, + + // ***** local public methods ***** + + addDownload: function(aDownload) { + aDownload.progressMode = Ci.nsITreeView.PROGRESS_NONE; + aDownload.lastSec = Infinity; + let state = DownloadsCommon.stateOfDownload(aDownload); + switch (state) { + case DownloadsCommon.DOWNLOAD_DOWNLOADING: + aDownload.endTime = Date.now(); + // At this point, we know if we are an indeterminate download or not. + aDownload.progressMode = aDownload.hasProgress ? + Ci.nsITreeView.PROGRESS_UNDETERMINED : + Ci.nsITreeView.PROGRESS_NORMAL; + case DownloadsCommon.DOWNLOAD_NOTSTARTED: + case DownloadsCommon.DOWNLOAD_PAUSED: + aDownload.isActive = 1; + break; + default: + aDownload.isActive = 0; + break; + } + + // prepend in natural sorting + aDownload.listIndex = this._lastListIndex--; + + // Prepend data to the download list + this._dlList.unshift(aDownload); + + // Tell the tree we added 1 row at index 0 + this._tree.rowCountChanged(0, 1); + + // Data has changed, so re-sorting might be needed + this.sortView("", "", aDownload, 0); + + window.updateCommands("tree-select"); + }, + + updateDownload: function(aDownload) { + var row = this._dlList.indexOf(aDownload); + if (row == -1) { + // No download row found to update, but as it's obviously going on, + // add it to the list now (can happen with very fast, e.g. local dls) + this.onDownloadAdded(aDownload); + return; + } + let state = DownloadsCommon.stateOfDownload(aDownload); + switch (state) { + case DownloadsCommon.DOWNLOAD_DOWNLOADING: + // At this point, we know if we are an indeterminate download or not. + aDownload.progressMode = aDownload.hasProgress ? + Ci.nsITreeView.PROGRESS_NORMAL : Ci.nsITreeView.PROGRESS_UNDETERMINED; + case DownloadsCommon.DOWNLOAD_NOTSTARTED: + case DownloadsCommon.DOWNLOAD_PAUSED: + aDownload.isActive = 1; + break; + default: + aDownload.isActive = 0; + aDownload.progressMode = Ci.nsITreeView.PROGRESS_NONE; + // This preference may not be set, so defaulting to two. + var flashCount = 2; + try { + flashCount = Services.prefs.getIntPref(PREF_FLASH_COUNT); + } catch (e) { } + getAttentionWithCycleCount(flashCount); + break; + } + + // Repaint the tree row + this._tree.invalidateRow(row); + + // Data has changed, so re-sorting might be needed + this.sortView("", "", aDownload, row); + + window.updateCommands("tree-select"); + }, + + removeDownload: function(aDownload) { + var row = this._dlList.indexOf(aDownload); + // Make sure we have an item to remove + if (row == -1) + return; + + var index = this.selection.currentIndex; + var wasSingleSelection = this.selection.count == 1; + + // Remove data from the download list + this._dlList.splice(row, 1); + + // Tell the tree we removed 1 row at the given row index + this._tree.rowCountChanged(row, -1); + + // Update selection if only removed download was selected + if (wasSingleSelection && this.selection.count == 0) { + index = Math.min(index, this.rowCount - 1); + if (index >= 0) + this.selection.select(index); + } + + window.updateCommands("tree-select"); + }, + + searchView: function(aInput) { + // Stringify the previous search + var prevSearch = this._searchTerms.join(" "); + + // Array of space-separated lower-case search terms + this._searchTerms = aInput.trim().toLowerCase().split(/\s+/); + + // Don't rebuild the download list if the search didn't change + if (this._searchTerms.join(" ") == prevSearch) + return; + + // Cache the current selection + this._cacheSelection(); + + // Rebuild the tree with set search terms + //this.initTree(); + + // Restore the selection + this._restoreSelection(); + }, + + sortView: function(aColumnID, aDirection, aDownload, aRow) { + var sortAscending = aDirection != "descending"; + + if (aColumnID == "" && aDirection == "") { + // Re-sort in already selected/cached order + var sortedColumn = this._tree.columns.getSortedColumn(); + if (sortedColumn) { + aColumnID = sortedColumn.id; + sortAscending = sortedColumn.element.getAttribute("sortDirection") != "descending"; + } + // no need for else, use default case of switch, sortAscending is true + } + + // Compare function for two _dlList items + var compfunc = function(a, b) { + // Active downloads are always at the beginning + // i.e. 0 for .isActive is larger (!) than 1 + if (a.isActive < b.isActive) + return 1; + if (a.isActive > b.isActive) + return -1; + // Same active/inactive state, sort normally + var comp_a = null; + var comp_b = null; + switch (aColumnID) { + case "Name": + comp_a = a.displayName.toLowerCase(); + comp_b = b.displayName.toLowerCase(); + break; + case "Status": + comp_a = DownloadsCommon.stateOfDownload(a); + comp_b = DownloadsCommon.stateOfDownload(b); + break; + case "Progress": + case "ProgressPercent": + // Use original sorting for inactive entries + // Use only one isActive to be sure we do the same + comp_a = a.isActive ? a.progress : a.listIndex; + comp_b = a.isActive ? b.progress : b.listIndex; + break; + case "TimeRemaining": + comp_a = a.isActive ? a.lastSec : a.listIndex; + comp_b = a.isActive ? b.lastSec : b.listIndex; + break; + case "Transferred": + comp_a = a.currentBytes; + comp_b = b.currentBytes; + break; + case "TransferRate": + comp_a = a.isActive ? a.speed : a.listIndex; + comp_b = a.isActive ? b.speed : b.listIndex; + break; + case "TimeElapsed": + comp_a = (a.endTime && a.startTime && (a.endTime > a.startTime)) + ? a.endTime - a.startTime + : 0; + comp_b = (b.endTime && b.startTime && (b.endTime > b.startTime)) + ? b.endTime - b.startTime + : 0; + break; + case "StartTime": + comp_a = a.startTime; + comp_b = b.startTime; + break; + case "EndTime": + comp_a = a.endTime; + comp_b = b.endTime; + break; + case "Source": + comp_a = a.source.url; + comp_b = b.source.url; + break; + case "unsorted": // Special case for reverting to original order + default: + comp_a = a.listIndex; + comp_b = b.listIndex; + } + if (comp_a > comp_b) + return sortAscending ? 1 : -1; + if (comp_a < comp_b) + return sortAscending ? -1 : 1; + return 0; + } + + // Cache the current selection + this._cacheSelection(); + + // Do the actual sorting of the array + this._dlList.sort(compfunc); + + var row = this._dlList.indexOf(aDownload); + if (row == -1) + // Repaint the tree + this._tree.invalidate(); + else if (row == aRow) + // No effect + this._selectionCache = null; + else if (row < aRow) + // Download moved up from aRow to row + this._tree.invalidateRange(row, aRow); + else + // Download moved down from aRow to row + this._tree.invalidateRange(aRow, row) + + // Restore the selection + this._restoreSelection(); + }, + + getRowData: function(aRow) { + return this._dlList[aRow]; + }, + + getActiveDownloads: function() { + return this._dlList.filter(dld => !dld.stopped); + }, + + // ***** local member vars ***** + + _tree: null, + _dlBundle: null, + _lastListIndex: 0, + _selectionCache: null, + + // ***** local helper functions ***** + + // Cache IDs of selected downloads for later restoration + _cacheSelection: function() { + // Abort if there's already something cached + if (this._selectionCache) + return; + + this._selectionCache = []; + if (this.selection.count < 1) + return; + + // Walk all selected rows and cache their download IDs + var start = {}; + var end = {}; + var numRanges = this.selection.getRangeCount(); + for (let rg = 0; rg < numRanges; rg++){ + this.selection.getRangeAt(rg, start, end); + for (let row = start.value; row <= end.value; row++){ + this._selectionCache.push(this._dlList[row]); + } + } + }, + + // Restore selection from cached IDs (as possible) + _restoreSelection: function() { + // Abort if the cache is empty + if (!this._selectionCache) + return; + + this.selection.clearSelection(); + for (let dl of this._selectionCache) { + // Find out what row this is now and if possible, add it to the selection + var row = this._dlList.indexOf(dl); + if (row != -1) + this.selection.rangedSelect(row, row, true); + } + // Work done, clear the cache + this._selectionCache = null; + }, +}; diff --git a/comm/suite/components/downloads/content/uploadProgress.js b/comm/suite/components/downloads/content/uploadProgress.js new file mode 100644 index 0000000000..0cd4d27817 --- /dev/null +++ b/comm/suite/components/downloads/content/uploadProgress.js @@ -0,0 +1,189 @@ +/* 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/. */ + +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); +const {DownloadUtils} = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm"); + +const kInterval = 750; // Default to .75 seconds. + +var gPersist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"] + .createInstance(Ci.nsIWebBrowserPersist); +var gSource = window.arguments[0].QueryInterface(Ci.nsIFileURL); +var gTarget = window.arguments[1].QueryInterface(Ci.nsIURL); +var gFileName = gSource.file.leafName; +var gFileSize = gSource.file.fileSize; +var gPercent = -1; +var gStartTime; +var gLastUpdate; +var gLastSeconds; +var gBundle; +var gStatus; +var gTime; +var gSize; +var gProgress; +var gMeter; + +function onLoad() +{ + gBundle = document.getElementById("dmBundle"); + gStatus = document.getElementById("status"); + gTime = document.getElementById("timeElapsed"); + gSize = document.getElementById("size"); + gProgress = document.getElementById("progressText"); + gMeter = document.getElementById("progress"); + var status = gBundle.getString("stateNotStarted"); + document.title = + gBundle.getFormattedString("progressTitle", [gFileName, status]); + gStatus.value = status; + gTime.value = gBundle.getFormattedString("timeSingle", + DownloadUtils.convertTimeUnits(0)); + gSize.value = DownloadUtils.getTransferTotal(0, gFileSize); + document.getElementById("target").value = + gBundle.getFormattedString("toTarget", [gTarget.resolve(".")]); + document.getElementById("source").value = + gBundle.getFormattedString("fromSource", [gSource.file.leafName]); + gPersist.progressListener = gProgressListener; + gPersist.saveURI(gSource, null, null, 0, null, null, gTarget, null); + document.documentElement.getButton("cancel").focus(); +} + +function onUnload() +{ + if (gPersist) + gPersist.cancel(Cr.NS_BINDING_ABORTED); + gPersist = null; +} + +function setPercent(aPercent, aStatus) +{ + gPercent = aPercent; + document.title = gBundle.getFormattedString("progressTitlePercent", + [aPercent, gFileName, aStatus]); + gProgress.value = gBundle.getFormattedString("percentFormat", [aPercent]); + gMeter.mode = "normal"; + gMeter.value = aPercent; +} + +var gProgressListener = { + // ----- nsIWebProgressListener methods ----- + + // Look for STATE_STOP and close dialog to indicate completion when it happens. + onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) { + if (aRequest instanceof Ci.nsIChannel && + aRequest.URI.equals(gTarget) && + aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + gPersist = null; + var status = gBundle.getString("stateCompleted"); + setPercent(100, status); + gStatus.value = status; + gSize.value = DownloadUtils.getTransferTotal(gFileSize, gFileSize); + setTimeout(window.close, kInterval); + } + }, + + // Handle progress notifications. + onProgressChange: function(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + return this.onProgressChange64(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress); + }, + + onProgressChange64: function(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + if (aRequest instanceof Ci.nsIChannel && + aRequest.URI.equals(gTarget)) { + // Get current time. + var now = Date.now(); + + // If interval hasn't elapsed, ignore it. + if (!gStartTime) + gStartTime = now; + else if (now - gLastUpdate < kInterval && aCurTotalProgress < gFileSize) + return; + + // Update this time. + gLastUpdate = now; + + // Update elapsed time. + var elapsed = (now - gStartTime) / 1000; + + // Calculate percentage. + var status = gBundle.getString("stateUploading"); + var percent = -1; + if (gFileSize > 0) + percent = Math.floor(aCurTotalProgress * 100 / gFileSize); + if (percent != gPercent) + setPercent(percent, status); + + // Update time remaining. + var rate = elapsed && aCurTotalProgress / elapsed; + if (rate && gFileSize) { + var timeLeft; + [timeLeft, gLastSeconds] = + DownloadUtils.getTimeLeft((gFileSize - aCurTotalProgress) / rate, + gLastSeconds); + status = gBundle.getFormattedString("statusActive", [status, timeLeft]); + } + gStatus.value = status; + + // Update dialog's display of elapsed time. + var timeUnits = DownloadUtils.convertTimeUnits(elapsed); + var timeString = timeUnits[2] ? "timeDouble" : "timeSingle"; + gTime.value = gBundle.getFormattedString(timeString, timeUnits); + + // Update size (nn KB of mm KB at xx.x KB/sec) + var size = DownloadUtils.getTransferTotal(aCurTotalProgress, gFileSize); + if (elapsed) + size = gBundle.getFormattedString("sizeSpeed", [size, + gBundle.getFormattedString("speedFormat", + DownloadUtils.convertByteUnits(rate))]); + gSize.value = size; + } + }, + + // Look for error notifications and display alert to user. + onStatusChange: function(aWebProgress, aRequest, aStatus, aMessage) { + // Check for error condition (only if dialog is still open). + if (!Cr.isSuccessCode(aStatus)) { + // Display error alert (using text supplied by back-end). + Services.prompt.alert(window, document.title, aMessage); + // Close the dialog. + window.close(); + } + }, + + // Ignore onLocationChange and onSecurityChange notifications. + onLocationChange: function( aWebProgress, aRequest, aLocation, aFlags ) { + }, + + onSecurityChange: function( aWebProgress, aRequest, aState ) { + }, + + // ---------- nsISupports methods ---------- + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIWebProgressListener2, + Ci.nsIWebProgressListener, + Ci.nsIInterfaceRequestor]), + + // ---------- nsIInterfaceRequestor methods ---------- + + getInterface: function(aIID) { + if (aIID.equals(Ci.nsIPrompt) || + aIID.equals(Ci.nsIAuthPrompt)) { + var prompt; + if (aIID.equals(Ci.nsIPrompt)) + prompt = Services.ww.getNewPrompter(window); + else + prompt = Services.ww.getNewAuthPrompter(window); + return prompt; + } + + throw Cr.NS_ERROR_NO_INTERFACE; + } +} diff --git a/comm/suite/components/downloads/content/uploadProgress.xul b/comm/suite/components/downloads/content/uploadProgress.xul new file mode 100644 index 0000000000..43e95d5432 --- /dev/null +++ b/comm/suite/components/downloads/content/uploadProgress.xul @@ -0,0 +1,33 @@ +<?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/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> + +<!DOCTYPE dialog> + +<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + buttons="cancel" + onload="onLoad();" + onunload="onUnload();" + style="width: 40em;"> + + <script src="chrome://communicator/content/downloads/uploadProgress.js"/> + + <stringbundleset id="stringbundleset"> + <stringbundle id="dmBundle" + src="chrome://communicator/locale/downloads/downloadmanager.properties"/> + </stringbundleset> + + <label id="source" value="" crop="center"/> + <label id="target" value="" crop="center"/> + <label id="size" value=""/> + <label id="timeElapsed" value=""/> + <label id="status" value=""/> + <hbox> + <progressmeter id="progress" mode="undetermined" value="0" flex="1"/> + <label id="progressText" value="" style="width: 4ch; text-align: right;"/> + </hbox> +</dialog> |