diff --git a/browser/components/downloads/content/downloads.js b/browser/components/downloads/content/downloads.js index fd6efefec163..f69dea0481a9 100644 --- a/browser/components/downloads/content/downloads.js +++ b/browser/components/downloads/content/downloads.js @@ -1187,6 +1187,12 @@ function DownloadsViewItemController(aElement) { } DownloadsViewItemController.prototype = { + ////////////////////////////////////////////////////////////////////////////// + //// Constants + + get kPrefBdmAlertOnExeOpen() "browser.download.manager.alertOnEXEOpen", + get kPrefBdmScanWhenDone() "browser.download.manager.scanWhenDone", + ////////////////////////////////////////////////////////////////////////////// //// Command dispatching @@ -1238,17 +1244,87 @@ DownloadsViewItemController.prototype = { commands: { cmd_delete: function DVIC_cmd_delete() { - this.dataItem.remove(); + this.dataItem.getDownload(function (aDownload) { + if (this.dataItem.inProgress) { + aDownload.cancel(); + this._ensureLocalFileRemoved(); + } + aDownload.remove(); + }.bind(this)); }, downloadsCmd_cancel: function DVIC_downloadsCmd_cancel() { - this.dataItem.cancel(); + if (this.dataItem.inProgress) { + this.dataItem.getDownload(function (aDownload) { + aDownload.cancel(); + this._ensureLocalFileRemoved(); + }.bind(this)); + } }, downloadsCmd_open: function DVIC_downloadsCmd_open() { - this.dataItem.openLocalFile(); + // Confirm opening executable files if required. + let localFile = this.dataItem.localFile; + if (localFile.isExecutable()) { + let showAlert = true; + try { + showAlert = Services.prefs.getBoolPref(this.kPrefBdmAlertOnExeOpen); + } catch (ex) { } + + // On Vista and above, we rely on native security prompting for + // downloaded content unless it's disabled. + if (DownloadsCommon.isWinVistaOrHigher) { + try { + if (Services.prefs.getBoolPref(this.kPrefBdmScanWhenDone)) { + showAlert = false; + } + } catch (ex) { } + } + + if (showAlert) { + let name = this.dataItem.target; + let message = + DownloadsCommon.strings.fileExecutableSecurityWarning(name, name); + let title = + DownloadsCommon.strings.fileExecutableSecurityWarningTitle; + let dontAsk = + DownloadsCommon.strings.fileExecutableSecurityWarningDontAsk; + + let checkbox = { value: false }; + let open = Services.prompt.confirmCheck(window, title, message, + dontAsk, checkbox); + if (!open) { + return; + } + + Services.prefs.setBoolPref(this.kPrefBdmAlertOnExeOpen, + !checkbox.value); + } + } + + // Actually open the file. + this.dataItem.getDownload(function (aDownload) { + try { + let launched = false; + try { + let mimeInfo = aDownload.MIMEInfo; + if (mimeInfo.preferredAction == mimeInfo.useHelperApp) { + mimeInfo.launchWithFile(localFile); + launched = true; + } + } catch (ex) { } + if (!launched) { + localFile.launch(); + } + } catch (ex) { + // If launch fails, try sending it through the system's external "file:" + // URL handler. + this._openExternal(localFile); + } + }.bind(this)); + // We explicitly close the panel here to give the user the feedback that // their click has been received, and we're handling the action. // Otherwise, we'd have to wait for the file-type handler to execute @@ -1259,7 +1335,26 @@ DownloadsViewItemController.prototype = { downloadsCmd_show: function DVIC_downloadsCmd_show() { - this.dataItem.showLocalFile(); + let localFile = this.dataItem.localFile; + + try { + // Show the directory containing the file and select the file. + localFile.reveal(); + } catch (ex) { + // If reveal fails for some reason (e.g., it's not implemented on unix + // or the file doesn't exist), try using the parent if we have it. + let parent = localFile.parent.QueryInterface(Ci.nsILocalFile); + if (parent) { + try { + // Open the parent directory to show where the file should be. + parent.launch(); + } catch (ex) { + // If launch also fails (probably because it's not implemented), let + // the OS handler try to open the parent. + this._openExternal(parent); + } + } + } // We explicitly close the panel here to give the user the feedback that // their click has been received, and we're handling the action. @@ -1271,12 +1366,20 @@ DownloadsViewItemController.prototype = { downloadsCmd_pauseResume: function DVIC_downloadsCmd_pauseResume() { - this.dataItem.togglePauseResume(); + this.dataItem.getDownload(function (aDownload) { + if (this.dataItem.paused) { + aDownload.resume(); + } else { + aDownload.pause(); + } + }.bind(this)); }, downloadsCmd_retry: function DVIC_downloadsCmd_retry() { - this.dataItem.retry(); + this.dataItem.getDownload(function (aDownload) { + aDownload.retry(); + }); }, downloadsCmd_openReferrer: function DVIC_downloadsCmd_openReferrer() @@ -1315,6 +1418,31 @@ DownloadsViewItemController.prototype = { // Invoke the command. this.doCommand(defaultCommand); } + }, + + /** + * Support function to open the specified nsIFile. + */ + _openExternal: function DVIC_openExternal(aFile) + { + let protocolSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService); + protocolSvc.loadUrl(makeFileURI(aFile)); + }, + + /** + * Support function that deletes the local file for a download. This is + * used in cases where the Download Manager service doesn't delete the file + * from disk when cancelling. See bug 732924. + */ + _ensureLocalFileRemoved: function DVIC_ensureLocalFileRemoved() + { + try { + let localFile = this.dataItem.localFile; + if (localFile.exists()) { + localFile.remove(false); + } + } catch (ex) { } } }; diff --git a/browser/components/downloads/src/DownloadsCommon.jsm b/browser/components/downloads/src/DownloadsCommon.jsm index 48c3a5653ec1..af7d82862c2a 100644 --- a/browser/components/downloads/src/DownloadsCommon.jsm +++ b/browser/components/downloads/src/DownloadsCommon.jsm @@ -63,9 +63,6 @@ const nsIDM = Ci.nsIDownloadManager; const kDownloadsStringBundleUrl = "chrome://browser/locale/downloads/downloads.properties"; -const kPrefBdmScanWhenDone = "browser.download.manager.scanWhenDone"; -const kPrefBdmAlertOnExeOpen = "browser.download.manager.alertOnEXEOpen"; - const kDownloadsStringsRequiringFormatting = { sizeWithUnits: true, shortTimeLeftSeconds: true, @@ -399,117 +396,6 @@ this.DownloadsCommon = { // never adding to the time left. Ensure that we never fall below one // second left until all downloads are actually finished. return aLastSeconds = Math.max(aSeconds, 1); - }, - - /** - * Opens a downloaded file. - * If you've a dataItem, you should call dataItem.openLocalFile. - * @param aFile - * the downloaded file to be opened. - * @param aMimeInfo - * the mime type info object. May be null. - * @param aOwnerWindow - * the window with which this action is associated. - */ - openDownloadedFile: function DC_openDownloadedFile(aFile, aMimeInfo, aOwnerWindow) { - if (!(aFile instanceof Ci.nsIFile)) - throw new Error("aFile must be a nsIFile object"); - if (aMimeInfo && !(aMimeInfo instanceof Ci.nsIMIMEInfo)) - throw new Error("Invalid value passed for aMimeInfo"); - if (!(aOwnerWindow instanceof Ci.nsIDOMWindow)) - throw new Error("aOwnerWindow must be a dom-window object"); - - // Confirm opening executable files if required. - if (aFile.isExecutable()) { - let showAlert = true; - try { - showAlert = Services.prefs.getBoolPref(kPrefBdmAlertOnExeOpen); - } catch (ex) { } - - // On Vista and above, we rely on native security prompting for - // downloaded content unless it's disabled. - if (DownloadsCommon.isWinVistaOrHigher) { - try { - if (Services.prefs.getBoolPref(kPrefBdmScanWhenDone)) { - showAlert = false; - } - } catch (ex) { } - } - - if (showAlert) { - let name = this.dataItem.target; - let message = - DownloadsCommon.strings.fileExecutableSecurityWarning(name, name); - let title = - DownloadsCommon.strings.fileExecutableSecurityWarningTitle; - let dontAsk = - DownloadsCommon.strings.fileExecutableSecurityWarningDontAsk; - - let checkbox = { value: false }; - let open = Services.prompt.confirmCheck(aOwnerWindow, title, message, - dontAsk, checkbox); - if (!open) { - return; - } - - Services.prefs.setBoolPref(kPrefBdmAlertOnExeOpen, - !checkbox.value); - } - } - - // Actually open the file. - try { - if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) { - aMimeInfo.launchWithFile(aFile); - return; - } - } - catch(ex) { } - - // If either we don't have the mime info, or the preferred action failed, - // attempt to launch the file directly. - try { - aFile.launch(); - } - catch(ex) { - // If launch fails, try sending it through the system's external "file:" - // URL handler. - Cc["@mozilla.org/uriloader/external-protocol-service;1"] - .getService(Ci.nsIExternalProtocolService) - .loadUrl(NetUtil.newURI(aFile)); - } - }, - - /** - * Show a donwloaded file in the system file manager. - * If you have a dataItem, use dataItem.showLocalFile. - * - * @param aFile - * a downloaded file. - */ - showDownloadedFile: function DC_showDownloadedFile(aFile) { - if (!(aFile instanceof Ci.nsIFile)) - throw new Error("aFile must be a nsIFile object"); - try { - // Show the directory containing the file and select the file. - aFile.reveal(); - } catch (ex) { - // If reveal fails for some reason (e.g., it's not implemented on unix - // or the file doesn't exist), try using the parent if we have it. - let parent = aFile.parent; - if (parent) { - try { - // Open the parent directory to show where the file should be. - parent.launch(); - } catch (ex) { - // If launch also fails (probably because it's not implemented), let - // the OS handler try to open the parent. - Cc["@mozilla.org/uriloader/external-protocol-service;1"] - .getService(Ci.nsIExternalProtocolService) - .loadUrl(NetUtil.newURI(parent)); - } - } - } } }; @@ -1326,101 +1212,6 @@ DownloadsDataItem.prototype = { // file, though this may throw an exception if the path is invalid. return new DownloadsLocalFileCtor(aFilename); } - }, - - /** - * Open the target file for this download. - * - * @param aOwnerWindow - * The window with which the required action is associated. - * @throws if the file cannot be opened. - */ - openLocalFile: function DDI_openLocalFile(aOwnerWindow) { - this.getDownload(function(aDownload) { - DownloadsCommon.openDownloadedFile(this.localFile, - aDownload.MIMEInfo, - aOwnerWindow); - }.bind(this)); - }, - - /** - * Show the downloaded file in the system file manager. - */ - showLocalFile: function DDI_showLocalFile() { - DownloadsCommon.showDownloadedFile(this.localFile); - }, - - /** - * Resumes the download if paused, pauses it if active. - * @throws if the download is not resumable or if has already done. - */ - togglePauseResume: function DDI_togglePauseResume() { - if (!this.inProgress || !this.resumable) - throw new Error("The given download cannot be paused or resumed"); - - this.getDownload(function(aDownload) { - if (this.inProgress) { - if (this.paused) - aDownload.resume(); - else - aDownload.pause(); - } - }.bind(this)); - }, - - /** - * Attempts to retry the download. - * @throws if we cannot. - */ - retry: function DDI_retry() { - if (!this.canRetry) - throw new Error("Cannot rerty this download"); - - this.getDownload(function(aDownload) { - aDownload.retry(); - }); - }, - - /** - * Support function that deletes the local file for a download. This is - * used in cases where the Download Manager service doesn't delete the file - * from disk when cancelling. See bug 732924. - */ - _ensureLocalFileRemoved: function DDI__ensureLocalFileRemoved() - { - try { - let localFile = this.localFile; - if (localFile.exists()) { - localFile.remove(false); - } - } catch (ex) { } - }, - - /** - * Cancels the download. - * @throws if the download is already done. - */ - cancel: function() { - if (!this.inProgress) - throw new Error("Cannot cancel this download"); - - this.getDownload(function (aDownload) { - aDownload.cancel(); - this._ensureLocalFileRemoved(); - }.bind(this)); - }, - - /** - * Remove the download. - */ - remove: function DDI_remove() { - this.getDownload(function (aDownload) { - if (this.inProgress) { - aDownload.cancel(); - this._ensureLocalFileRemoved(); - } - aDownload.remove(); - }.bind(this)); } }; diff --git a/browser/components/places/content/browserPlacesViews.js b/browser/components/places/content/browserPlacesViews.js index e64404e465c6..789fcd554831 100644 --- a/browser/components/places/content/browserPlacesViews.js +++ b/browser/components/places/content/browserPlacesViews.js @@ -20,8 +20,6 @@ PlacesViewBase.prototype = { _viewElt: null, get viewElt() this._viewElt, - get associatedElement() this._viewElt, - get controllers() this._viewElt.controllers, // The xul element that represents the root container. diff --git a/browser/components/places/content/download.css b/browser/components/places/content/download.css deleted file mode 100644 index bec39ca02ed3..000000000000 --- a/browser/components/places/content/download.css +++ /dev/null @@ -1,45 +0,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/. */ - -richlistitem.download button { - /* These buttons should never get focus, as that would "disable" - the downloads view controller (it's only used when the richlistbox - is focused). */ - -moz-user-focus: none; -} - -/*** Visibility of controls inside download items ***/ - -.download-state:-moz-any( [state="6"], /* Blocked (parental) */ - [state="8"], /* Blocked (dirty) */ - [state="9"]) /* Blocked (policy) */ - .downloadTypeIcon:not(.blockedIcon), - -.download-state:not(:-moz-any([state="6"], /* Blocked (parental) */ - [state="8"], /* Blocked (dirty) */ - [state="9"]) /* Blocked (policy) */) - .downloadTypeIcon.blockedIcon, - -.download-state:not(:-moz-any([state="-1"],/* Starting (initial) */ - [state="5"], /* Starting (queued) */ - [state="0"], /* Downloading */ - [state="4"], /* Paused */ - [state="7"]) /* Scanning */) - .downloadProgress, - -.download-state:not(:-moz-any([state="-1"],/* Starting (initial) */ - [state="5"], /* Starting (queued) */ - [state="0"], /* Downloading */ - [state="4"]) /* Paused */) - .downloadCancel, - -.download-state:not(:-moz-any([state="2"], /* Failed */ - [state="3"]) /* Canceled */) - .downloadRetry, - -.download-state:not( [state="1"] /* Finished */) - .downloadShow -{ - visibility: hidden; -} diff --git a/browser/components/places/content/download.xml b/browser/components/places/content/download.xml deleted file mode 100644 index 6033c226009d..000000000000 --- a/browser/components/places/content/download.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/browser/components/places/content/downloadsView.js b/browser/components/places/content/downloadsView.js deleted file mode 100644 index 89edd02b25b1..000000000000 --- a/browser/components/places/content/downloadsView.js +++ /dev/null @@ -1,1024 +0,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/. */ - -/** - * THE PLACES VIEW IMPLEMENTED IN THIS FILE HAS A VERY PARTICULAR USE CASE. - * IT IS HIGHLY RECOMMENDED NOT TO EXTEND IT FOR ANY OTHER USE CASES OR RELY - * ON IT AS AN API. - */ - -let Cu = Components.utils; -let Ci = Components.interfaces; -let Cc = Components.classes; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/NetUtil.jsm"); -Cu.import("resource://gre/modules/DownloadUtils.jsm"); -Cu.import("resource:///modules/DownloadsCommon.jsm"); - -const nsIDM = Ci.nsIDownloadManager; - -const DESTINATION_FILE_URI_ANNO = "downloads/destinationFileURI"; -const DESTINATION_FILE_NAME_ANNO = "downloads/destinationFileName"; -const DOWNLOAD_STATE_ANNO = "downloads/state"; - -const DOWNLOAD_VIEW_SUPPORTED_COMMANDS = - ["cmd_delete", "cmd_copy", "cmd_paste", "cmd_selectAll", - "downloadsCmd_pauseResume", "downloadsCmd_cancel", - "downloadsCmd_open", "downloadsCmd_show", "downloadsCmd_retry", - "downloadsCmd_openReferrer"]; - -function GetFileForFileURI(aFileURI) - Cc["@mozilla.org/network/protocol;1?name=file"] - .getService(Ci.nsIFileProtocolHandler) - .getFileFromURLSpec(aFileURI); - -/** - * A download element shell is responsible for handling the commands and the - * displayed data for a single download view element. The download element - * could represent either a past download (for which we get data from places) or - * a "session" download (using a data-item object. See DownloadsCommon.jsm), or both. - * - * Once initialized with either a data item or a places node, the created richlistitem - * can be accessed through the |element| getter, and can then be inserted/removed from - * a richlistbox. - * - * The shell doesn't take care of inserting the item, or removing it when it's no longer - * valid. That's the caller (a DownloadsPlacesView object) responsibility. - * - * The caller is also responsible for "passing over" notification from both the - * download-view and the places-result-observer, in the following manner: - * - The DownloadsPlacesView object implements getViewItem of the download-view - * pseudo interface. It returns this object (therefore we implement - * onStateChangea and onProgressChange here). - * - The DownloadsPlacesView object adds itself as a places result observer and - * calls this object's placesNodeIconChanged, placesNodeTitleChanged and - * placeNodeAnnotationChanged from its callbacks. - * - * @param [optional] aDataItem - * The data item of a the session download. Required if aPlacesNode is not set - * @param [optional] aPlacesNode - * The places node for a past download. Required if aDataItem is not set. - */ -function DownloadElementShell(aDataItem, aPlacesNode) { - this._element = document.createElement("richlistitem"); - this._element._shell = this; - - this._element.classList.add("download"); - this._element.classList.add("download-state"); - - if (aDataItem) - this.dataItem = aDataItem; - if (aPlacesNode) - this.placesNode = aPlacesNode; -} - -DownloadElementShell.prototype = { - // The richlistitem for the download - get element() this._element, - - // The data item for the download - get dataItem() this._dataItem, - - set dataItem(aValue) { - if (this._dataItem = aValue) { - this._wasDone = this._dataItem.done; - this._wasInProgress = this._dataItem.inProgress; - } - else if (this._placesNode) { - this._wasInProgress = false; - this._wasDone = this._state == nsIDM.DOWNLOAD_FINISHED; - } - - this._updateStatusUI(); - return aValue; - }, - - get placesNode() this._placesNode, - set placesNode(aNode) { - if (this._placesNode != aNode) { - this._annotations = new Map(); - this._placesNode = aNode; - if (!this._dataItem && this._placesNode) { - this._wasInProgress = false; - this._wasDone = this._state == nsIDM.DOWNLOAD_FINISHED; - this._updateStatusUI(); - } - } - return aNode; - }, - - // The download uri (as a string) - get downloadURI() { - if (this._dataItem) - return this._dataItem.uri; - if (this._placesNode) - return this._placesNode.uri; - throw new Error("Unexpected download element state"); - }, - - get _icon() { - if (this._targetFileURI) - return "moz-icon://" + this._targetFileURI + "?size=32"; - if (this._placesNode) - return this.placesNode.icon; - if (this._dataItem) - throw new Error("Session-download items should always have a target file uri"); - throw new Error("Unexpected download element state"); - }, - - // Helper for getting a places annotation set for the download. - _getAnnotation: function DES__getAnnotation(aAnnotation, aDefaultValue) { - if (this._annotations.has(aAnnotation)) - return this._annotations.get(aAnnotation); - - let value; - try { - value = PlacesUtils.annotations.getPageAnnotation( - NetUtil.newURI(this.downloadURI), aAnnotation); - } - catch(ex) { - if (aDefaultValue === undefined) { - throw new Error("Could not get required annotation '" + aAnnotation + - "' for download with url '" + this.downloadURI + "'"); - } - value = aDefaultValue; - } - this._annotations.set(aAnnotation, value); - return value; - }, - - // The uri (as a string) of the target file, if any. - get _targetFileURI() { - if (this._dataItem) - return this._dataItem.file; - - return this._getAnnotation(DESTINATION_FILE_URI_ANNO, ""); - }, - - // The label for the download - get _displayName() { - if (this._dataItem) - return this._dataItem.target; - - try { - return this._getAnnotation(DESTINATION_FILE_NAME_ANNO, ""); - } - catch(ex) { } - - // Fallback to the places title, or, at last, to the download uri. - return this._placesNode.title || this.downloadURI; - }, - - // If there's a target file for the download, this is its nsIFile object. - get _file() { - if (!("__file" in this)) { - if (this._dataItem) { - this.__file = this._dataItem.localFile; - } - else { - this.__file = this._targetFileURI ? - GetFileForFileURI(this._targetFileURI) : null; - } - } - return this.__file; - }, - - // The target's file size in bytes. If there's no target file, or If we - // cannot determine its size, 0 is returned. - get _fileSize() { - if (!this._file || !this._file.exists()) - return 0; - try { - return this._file.fileSize; - } - catch(ex) { - Cu.reportError(ex); - return 0; - } - }, - - // The download state (see nsIDownloadManager). - get _state() { - if (this._dataItem) - return this._dataItem.state; - - let state = -1; - try { - return this._getAnnotation(DOWNLOAD_STATE_ANNO); - } - catch (ex) { - // The state annotation didn't exist in past releases. - if (!this._file) { - state = nsIDM.DOWNLOAD_FAILED; - } - else if (this._file.exists()) { - state = this._fileSize > 0 ? - nsIDM.DOWNLOAD_FINISHED : nsIDM.DOWNLOAD_FAILED; - } - else { - // XXXmano I'm not sure if this right. We should probably show no - // status text at all in this case. - state = nsIDM.DOWNLOAD_CANCELED; - } - } - return state; - }, - - // The status text for the download - get _statusText() { - let s = DownloadsCommon.strings; - if (this._dataItem && this._dataItem.inProgress) { - if (this._dataItem.paused) { - let transfer = - DownloadUtils.getTransferTotal(this._dataItem.currBytes, - this._dataItem.maxBytes); - - // We use the same XUL label to display both the state and the amount - // transferred, for example "Paused - 1.1 MB". - return s.statusSeparatorBeforeNumber(s.statePaused, transfer); - } - if (this._dataItem.state == nsIDM.DOWNLOAD_DOWNLOADING) { - let [status, newEstimatedSecondsLeft] = - DownloadUtils.getDownloadStatus(this.dataItem.currBytes, - this.dataItem.maxBytes, - this.dataItem.speed, - this._lastEstimatedSecondsLeft); - this._lastEstimatedSecondsLeft = newEstimatedSecondsLeft; - return status; - } - if (this._dataItem.starting) { - return s.stateStarting; - } - if (this._dataItem.state == nsIDM.DOWNLOAD_SCANNING) { - return s.stateScanning; - } - - let [displayHost, fullHost] = - DownloadUtils.getURIHost(this._dataItem.referrer || - this._dataItem.uri); - - let end = new Date(this.dataItem.endTime); - let [displayDate, fullDate] = DownloadUtils.getReadableDates(end); - return s.statusSeparator(fullHost, fullDate); - } - - switch (this._state) { - case nsIDM.DOWNLOAD_FAILED: - return s.stateFailed; - case nsIDM.DOWNLOAD_CANCELED: - return s.stateCanceled; - case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: - return s.stateBlockedParentalControls; - case nsIDM.DOWNLOAD_BLOCKED_POLICY: - return s.stateBlockedPolicy; - case nsIDM.DOWNLOAD_DIRTY: - return s.stateDirty; - case nsIDM.DOWNLOAD_FINISHED:{ - // For completed downloads, show the file size (e.g. "1.5 MB") - if (this._fileSize > 0) { - let [size, unit] = DownloadUtils.convertByteUnits(this._fileSize); - return s.sizeWithUnits(size, unit); - } - break; - } - } - - return ""; - }, - - // The progressmeter element for the download - get _progressElement() { - let progressElement = document.getAnonymousElementByAttribute( - this._element, "anonid", "progressmeter"); - if (progressElement) { - delete this._progressElement; - return this._progressElement = progressElement; - } - return null; - }, - - // Updates the download state attribute (and by that hide/unhide the - // appropriate buttons and context menu items), the status text label, - // and the progress meter. - _updateDownloadStatusUI: function DES__updateDownloadStatusUI() { - this._element.setAttribute("state", this._state); - this._element.setAttribute("status", this._statusText); - - // For past-downloads, we're done. For session-downloads, we may also need - // to update the progress-meter. - if (!this._dataItem) - return; - - // Copied from updateProgress in downloads.js. - if (this._dataItem.starting) { - // Before the download starts, the progress meter has its initial value. - this._element.setAttribute("progressmode", "normal"); - this._element.setAttribute("progress", "0"); - } - else if (this._dataItem.state == nsIDM.DOWNLOAD_SCANNING || - this._dataItem.percentComplete == -1) { - // We might not know the progress of a running download, and we don't know - // the remaining time during the malware scanning phase. - this._element.setAttribute("progressmode", "undetermined"); - } - else { - // This is a running download of which we know the progress. - this._element.setAttribute("progressmode", "normal"); - this._element.setAttribute("progress", this._dataItem.percentComplete); - } - - // Dispatch the ValueChange event for accessibility, if possible. - if (this._progressElement) { - let event = document.createEvent("Events"); - event.initEvent("ValueChange", true, true); - this._progressElement.dispatchEvent(event); - } - - goUpdateDownloadCommands(); - }, - - _updateStatusUI: function DES__updateStatusUI() { - this._updateDownloadStatusUI(); - this._element.setAttribute("image", this._icon); - this._element.setAttribute("displayName", this._displayName); - }, - - placesNodeIconChanged: function DES_placesNodeIconChanged() { - if (!this._dataItem) - this._element.setAttribute("image", this._icon); - }, - - placesNodeTitleChanged: function DES_placesNodeTitleChanged() { - if (!this._dataItem) - this._element.setAttribute("displayName", this._displayName); - }, - - placesNodeAnnotationChanged: function DES_placesNodeAnnotationChanged(aAnnoName) { - this._annotations.delete(aAnnoName); - if (!this._dataItem) { - if (aAnnoName == DESTINATION_FILE_URI_ANNO) { - this._element.setAttribute("image", this._icon); - this._updateDownloadStatusUI(); - } - else if (aAnnoName == DESTINATION_FILE_NAME_ANNO) { - this._element.setAttribute("displayName", this._displayName); - } - else if (aAnnoName == DOWNLOAD_STATE_ANNO) { - this._updateDownloadStatusUI(); - } - } - }, - - /* DownloadView */ - onStateChange: function DES_onStateChange() { - // See comment in DVI_onStateChange in downloads.js (the panel-view) - if (!this._wasDone && this._dataItem.done) - this._element.setAttribute("image", this._icon + "&state=normal"); - - this._wasDone = this._dataItem.done; - - // Update the end time using the current time if required. - if (this._wasInProgress && !this._dataItem.inProgress) { - this._endTime = Date.now(); - } - - this._wasDone = this._dataItem.done; - this._wasInProgress = this._dataItem.inProgress; - - this._updateDownloadStatusUI(); - }, - - /* DownloadView */ - onProgressChange: function DES_onProgressChange() { - this._updateDownloadStatusUI(); - }, - - /* nsIController */ - isCommandEnabled: function DES_isCommandEnabled(aCommand) { - switch (aCommand) { - case "downloadsCmd_open": { - return this._file.exists() && - ((this._dataItem && this._dataItem.openable) || - (this._state == nsIDM.DOWNLOAD_FINISHED)); - } - case "downloadsCmd_show": { - return this._getTargetFileOrPartFileIfExists() != null; - } - case "downloadsCmd_pauseResume": - return this._dataItem && this._dataItem.inProgress && this._dataItem.resumable; - case "downloadsCmd_retry": - return ((this._dataItem && this._dataItem.canRetry) || - (!this._dataItem && this._file)) - case "downloadsCmd_openReferrer": - return this._dataItem && !!this._dataItem.referrer; - case "cmd_delete": - // The behavior in this case is somewhat unexpected, so we disallow that. - if (this._placesNode && this._dataItem && this._dataItem.inProgress) - return false; - return true; - case "downloadsCmd_cancel": - return this._dataItem != null; - } - }, - - _getTargetFileOrPartFileIfExists: function DES__getTargetFileOrPartFileIfExists() { - if (this._file && this._file.exists()) - return this._file; - if (this._dataItem && - this._dataItem.partFile && this._dataItem.partFile.exists()) - return this._dataItem.partFile; - return null; - }, - - _retryAsHistoryDownload: function DES__retryAsHistoryDownload() { - // TODO: save in the right location (the current saveURL api does not allow this) - saveURL(this.downloadURI, this._displayName, null, true, true, undefined, document); - }, - - /* nsIController */ - doCommand: function DES_doCommand(aCommand) { - switch (aCommand) { - case "downloadsCmd_open": { - if (this._dateItem) - this._dataItem.openLocalFile(window); - else - DownloadsCommon.openDownloadedFile(this._file, null, window); - break; - } - case "downloadsCmd_show": { - if (this._dataItem) - this._dataItem.showLocalFile(); - else - DownloadsCommon.showDownloadedFile(this._getTargetFileOrPartFileIfExists()); - break; - } - case "downloadsCmd_openReferrer": { - openURL(this._dataItem.referrer); - break; - } - case "downloadsCmd_cancel": { - this._dataItem.cancel(); - break; - } - case "cmd_delete": { - if (this._dataItem) - this._dataItem.remove(); - if (this._placesNode) - PlacesUtils.bhistory.removePage(NetUtil.newURI(this.downloadURI)); - break; - } - case "downloadsCmd_retry": { - if (this._dataItem) - this._dataItem.retry(); - else - this._retryAsHistoryDownload(); - break; - } - case "downloadsCmd_pauseResume": { - this._dataItem.togglePauseResume(); - break; - } - } - }, - - // Returns whether or not the download handled by this shell should - // show up in the search results for the given term. Both the display - // name for the download and the url are searched. - matchesSearchTerm: function DES_matchesSearchTerm(aTerm) { - // Stub implemention until we figure out something better - aTerm = aTerm.toLowerCase(); - return this._displayName.toLowerCase().indexOf(aTerm) != -1 || - this.downloadURI.toLowerCase().indexOf(aTerm) != -1; - }, - - // Handles return kepress on the element (the keypress listener is - // set in the DownloadsPlacesView object). - doDefaultCommand: function DES_doDefaultCommand() { - function getDefaultCommandForState(aState) { - switch (aState) { - case nsIDM.DOWNLOAD_FINISHED: - return "downloadsCmd_open"; - case nsIDM.DOWNLOAD_PAUSED: - return "downloadsCmd_pauseResume"; - case nsIDM.DOWNLOAD_NOTSTARTED: - case nsIDM.DOWNLOAD_QUEUED: - return "downloadsCmd_cancel"; - case nsIDM.DOWNLOAD_FAILED: - case nsIDM.DOWNLOAD_CANCELED: - return "downloadsCmd_retry"; - case nsIDM.DOWNLOAD_DOWNLOADING: - case nsIDM.DOWNLOAD_SCANNING: - return "downloadsCmd_show"; - case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: - case nsIDM.DOWNLOAD_DIRTY: - case nsIDM.DOWNLOAD_BLOCKED_POLICY: - return "downloadsCmd_openReferrer"; - } - } - let command = getDefaultCommandForState(this._state); - if (this.isCommandEnabled(command)) - this.doCommand(command); - } -}; - -/** - * A Downloads Places View is a places view designed to show a places query - * for history donwloads alongside the current "session"-downloads. - * - * As we don't use the places controller, some methods implemented by other - * places views are not implemented by this view. - * - * A richlistitem in this view can represent either a past download or a session - * download, or both. Session downloads are shown first in the view, and as long - * as they exist they "collapses" their history "counterpart" (So we don't show two - * items for every download). - */ -function DownloadsPlacesView(aRichListBox, aPlace) { - this._richlistbox = aRichListBox; - this._richlistbox._placesView = this; - this._downloadElementsShellsForURI = new Map(); - this._viewItemsForDataItems = new WeakMap(); - this._shells = new Set(); - this._lastSessionDownloadElement = null; - this.place = aPlace; - this._richlistbox.controllers.appendController(this); -} - -DownloadsPlacesView.prototype = { - _registerAsDownloadsView: function DPV__registerAsDownloadsView() { - let downloadsData = DownloadsCommon.getData(window.opener || window); - downloadsData.addView(this); - // Make sure to unregister the view if the window is closed. - window.addEventListener("unload", function() { - if (this._result) { - downloadsData.removeView(this); - } - }.bind(this), true); - }, - - get associatedElement() this._richlistbox, - - _forEachDownloadElementShellForURI: - function DPV__forEachDownloadElementShellForURI(aURI, aCallback) { - if (this._downloadElementsShellsForURI.has(aURI)) { - let downloadElementShells = this._downloadElementsShellsForURI.get(aURI); - for (let des of downloadElementShells) { - aCallback(des); - } - } - }, - - // Given a data item for a session download, or a places node for a past - // download, updates the view as necessary. - // 1. If the given data is a places node, we check whether there are any - // element for the same download url. If there are, then we just reset - // their places node. Otherwise we add a new download element. - // 2. If the given data is a data item, we first check if there's an history - // download in the list that is not associated with a data item. If we found - // one, we use it for the data item as well and reposition it alongside the - // other session downloads. If we don't, then we go ahead and create a new - // element for the download. - _addDownloadData: - function DPV_addDownload(aDataItem, aPlacesNode, aNewest) { - let downloadURI = aPlacesNode ? aPlacesNode.uri : aDataItem.uri; - let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI, null); - if (!shellsForURI) { - shellsForURI = new Set(); - this._downloadElementsShellsForURI.set(downloadURI, shellsForURI); - } - - let newOrUpdatedShell = null; - - // Trivial: if there are no shells for this download URI, we always - // need to create one. - let shouldCreateShell = shellsForURI.size == 0; - - // However, if we do have shells for this download uri, there are - // few options: - // 1) There's only one shell and it's for a history download (it has - // no data item). In this case, we update this shell and move it - // if necessary - // 2) There are multiple shells, indicicating multiple downloads for - // the same download uri are running. In this case we create - // anoter shell for the download (so we have one shell for each data - // item). - // - // Note: If a cancelled session download is already in the list, and the - // download is retired, onDataItemAdded is called again for the same - // data item. Thus, we also check that we make sure we don't have a view item - // already. - if (!shouldCreateShell && - aDataItem && this.getViewItem(aDataItem) == null) { - // If there's a past-download-only shell for this download-uri with no - // associated data item, use it for the new data item. Otherwise, go ahead - // and create another shell. - shouldCreateShell = true; - for (let shell of shellsForURI) { - if (!shell.dataItem) { - shouldCreateShell = false; - shell.dataItem = aDataItem; - newOrUpdatedShell = shell; - this._viewItemsForDataItems.set(aDataItem, shell); - break; - } - } - } - - if (shouldCreateShell) { - let shell = new DownloadElementShell(aDataItem, aPlacesNode); - newOrUpdatedShell = shell; - element = shell.element; - shellsForURI.add(shell); - if (aDataItem) - this._viewItemsForDataItems.set(aDataItem, shell); - } - else if (aPlacesNode) { - for (let shell of shellsForURI) { - if (shell.placesNode != aPlacesNode) - shell.placesNode = aPlacesNode; - } - } - - if (newOrUpdatedShell) { - if (aNewest) { - this._richlistbox.insertBefore(newOrUpdatedShell.element, - this._richlistbox.firstChild); - if (!this._lastSessionDownloadElement) { - this._lastSessionDownloadElement = newOrUpdatedShell.element; - } - } - else if (aDataItem) { - let before = this._lastSessionDownloadElement ? - this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild - this._richlistbox.insertBefore(newOrUpdatedShell.element, before) - this._lastSessionDownloadElement = newOrUpdatedShell.element; - } - else { - this._richlistbox.appendChild(newOrUpdatedShell.element); - } - } - }, - - _removeElement: function DPV__removeElement(aElement) { - // If the element was selected exclusively, select its next - // sibling first, if any. - if (aElement.nextSibling && - this._richlistbox.selectedItems && - this._richlistbox.selectedItems[0] == aElement) { - this._richlistbox.selectItem(aElement.nextSibling); - } - this._richlistbox.removeChild(aElement); - }, - - _historyDownloadRemoved: - function DPV__historyDownloadRemoved(aPlacesNode) { - let downloadURI = aPlacesNode.uri; - let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI, null); - if (shellsForURI) { - for (let shell of shellsForURI) { - if (shell.dataItem) { - shell.placesNode = null; - } - else { - this._removeElement(shell.element); - shellsForURI.delete(shell); - if (shellsForURI.size == 0) - this._downloadElementsShellsForURI.delete(downloadURI); - } - } - } - }, - - _sessionDownloadRemoved: - function DPV__sessionDownloadRemoved(aDataItem) { - let shells = this._downloadElementsShellsForURI.get(aDataItem.uri, null); - if (shells.size == 0) - throw new Error("Should have had at leaat one shell for this uri"); - - let shell = this.getViewItem(aDataItem); - if (!shells.has(shell)) - throw new Error("Missing download element shell in shells list for url"); - - // If there's more than one item for this download uri, we can let the - // view item for this this particular data item go away. - // If there's only one item for this download uri, we should only - // keep it if it is associated with a history download. - if (shells.size > 1 || !shell.placesNode) { - this._removeElement(shell.element); - shells.delete(shell); - if (shells.size == 0) - this._downloadElementsShellsForURI.delete(aDataItem.uri); - return; - } - else { - shell.dataItem = null; - // Move it below the session-download items; - if (this._lastSessionDownloadElement == shell.dataItem) { - this._lastSessionDownloadElement = shell.dataItem.previousSibling; - } - else { - let before = this._lastSessionDownloadElement ? - this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstchild; - this._richlistbox.insertBefore(shell.element, before); - } - } - }, - - get place() this._place, - - set place(val) { - // Cleanup - this.result = null; - - this._searchTerm = ""; - this._place = val; - - let history = PlacesUtils.history; - let queries = { }, options = { }; - history.queryStringToQueries(val, queries, { }, options); - if (!queries.value.length) - queries.value = [history.getNewQuery()]; - - let result = history.executeQueries(queries.value, queries.value.length, - options.value); - result.addObserver(this, false); - - this._registerAsDownloadsView(); - - return val; - }, - - _cleanUp: function() { - while (this._richlistbox.firstChild) { - this._richlistbox.removeChild(this._richlistbox.firstChild); - } - this._searchTerm = ""; - DownloadsCommon.getData(window.opener || window).removeView(); - this._downloadElementsShellsForURI = new Map(); - this._viewItemsForDataItems = new WeakMap(); - this._shells = new Set(); - this._resultNode = null; - this._result = null; - this._lastSessionDownloadElement = null; - }, - - _result: null, - get result() this._result, - set result(val) { - if (this._result == val) - return val; - - if (this._result) { - this._result.removeObserver(this); - this._resultNode.containerOpen = false; - } - - this._result = val; - if (val) { - this._resultNode = val.root; - this._resultNode.containerOpen = true; - } - else { - this._cleanUp(); - } - - return val; - }, - - get selectedNodes() { - let placesNodes = []; - let selectedElements = this._richlistbox.selectedItems; - for (let elt of selectedElements) { - if (elt.placesNode) - placesNodes.push(elt.placesNode); - } - return placesNodes; - }, - - get selectedNode() { - let selectedNodes = this.selectedNodes; - return selectedNodes.length == 1 ? selectedNodes[0] : null; - }, - - get hasSelection() this.selectedNodes.length > 0, - - containerStateChanged: - function DPV_containerStateChanged(aNode, aOldState, aNewState) { - this.invalidateContainer(aNode) - }, - - invalidateContainer: - function DPV_invalidateContainer(aContainer) { - if (aContainer != this._resultNode) - throw new Error("Unexpected container node"); - - if (aContainer.containerOpen) { - for (let i = 0; i < aContainer.childCount; i++) { - try { - this._addDownloadData(null, aContainer.getChild(i), false) - } - catch(ex) { - Cu.reportError(ex); - } - } - } - else { - throw new Error("Root container for the downloads query cannot be closed"); - } - }, - - nodeInserted: function DPV_nodeInserted(aParent, aPlacesNode) { - this._addDownloadData(null, aPlacesNode, false); - }, - - nodeRemoved: function DPV_nodeRemoved(aParent, aPlacesNode, aOldIndex) { - this._historyDownloadRemoved(aPlacesNode); - }, - - nodeIconChanged: function DPV_nodeIconChanged(aNode) { - this._forEachDownloadElementShellForURI(aNode.uri, function(aDownloadElementShell) { - aDownloadElementShell.placesNodeIconChanged(); - }); - }, - - nodeAnnotationChanged: function DPV_nodeAnnotationChanged(aNode, aAnnoName) { - this._forEachDownloadElementShellForURI(aNode.uri, function(aDownloadElementShell) { - aDownloadElementShell.placesNodeAnnotationChanged(aAnnoName); - }); - }, - - nodeTitleChanged: function DPV_nodeTitleChanged(aNode, aNewTitle) { - this._forEachDownloadElementShellForURI(aNode.uri, function(aDownloadElementShell) { - aDownloadElementShell.placesNodeTitleChanged(); - }); - }, - - nodeKeywordChanged: function() {}, - nodeDateAddedChanged: function() {}, - nodeLastModifiedChanged: function() {}, - nodeReplaced: function() {}, - nodeHistoryDetailsChanged: function() {}, - nodeTagsChanged: function() {}, - sortingChanged: function() {}, - nodeMoved: function() {}, - nodeURIChanged: function() {}, - - get controller() this._richlistbox.controller, - - get searchTerm() this._searchTerm, - set searchTerm(aValue) { - for (let element of this._richlistbox.childNodes) { - element.hidden = !element._shell.matchesSearchTerm(aValue); - } - return aValue; - }, - - applyFilter: function() { - throw new Error("applyFilter is not implemented by the DownloadsView") - }, - - load: function(aQueries, aOptions) { - throw new Error("|load| is not implemented by the Downloads View"); - }, - - onDataLoadStarting: function() { }, - onDataLoadCompleted: function() { }, - - onDataItemAdded: function DPV_onDataItemAdded(aDataItem, aNewest) { - this._addDownloadData(aDataItem, null, aNewest); - }, - - onDataItemRemoved: function DPV_onDataItemRemoved(aDataItem) { - this._sessionDownloadRemoved(aDataItem) - }, - - getViewItem: function(aDataItem) - this._viewItemsForDataItems.get(aDataItem, null), - - supportsCommand: function(aCommand) - DOWNLOAD_VIEW_SUPPORTED_COMMANDS.indexOf(aCommand) != -1, - - isCommandEnabled: function DPV_isCommandEnabled(aCommand) { - let selectedElements = this._richlistbox.selectedItems; - switch (aCommand) { - case "cmd_copy": - return selectedElements && selectedElements.length > 0; - case "cmd_selectAll": - return true; - case "cmd_paste": - return this._canDownloadClipboardURL(); - default: - return Array.every(selectedElements, function(element) { - return element._shell.isCommandEnabled(aCommand); - }); - } - }, - - _copySelectedDownloadsToClipboard: - function DPV__copySelectedDownloadsToClipboard() { - let selectedElements = this._richlistbox.selectedItems; - let urls = [e._shell.downloadURI for each (e in selectedElements)]; - - Cc["@mozilla.org/widget/clipboardhelper;1"]. - getService(Ci.nsIClipboardHelper).copyString(urls.join("\n"), document); - }, - - _getURLFromClipboardData: function DPV__getURLFromClipboardData() { - 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 [NetUtil.newURI(url, null, null).spec, name]; - } - catch(ex) { } - - return ["", ""]; - }, - - _canDownloadClipboardURL: function DPV__canDownloadClipboardURL() { - let [url, name] = this._getURLFromClipboardData(); - return url != ""; - }, - - _downloadURLFromClipboard: function DPV__downloadURLFromClipboard() { - let [url, name] = this._getURLFromClipboardData(); - saveURL(url, name || url, null, true, true, undefined, document); - }, - - doCommand: function DPV_doCommand(aCommand) { - switch (aCommand) { - case "cmd_copy": - this._copySelectedDownloadsToClipboard(); - break; - case "cmd_selectAll": - this._richlistbox.selectAll(); - break; - case "cmd_paste": - this._downloadURLFromClipboard(); - break; - default: { - let selectedElements = this._richlistbox.selectedItems; - for (let element of selectedElements) { - element._shell.doCommand(aCommand); - } - } - } - }, - - onEvent: function() { }, - - onContextMenu: function DPV_onContextMenu(aEvent) - { - let element = this._richlistbox.selectedItem; - if (!element || !element._shell) - return false; - - // Set the state attribute so that only the appropriate items are displayed. - let contextMenu = document.getElementById("downloadsContextMenu"); - contextMenu.setAttribute("state", element._shell._state); - }, - - onKeyPress: function DPV_onKeyPress(aEvent) { - let selectedElements = this._richlistbox.selectedItems; - if (!selectedElements) - return; - - if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) { - // In the content tree, opening bookmarks by pressing return is only - // supported when a single item is selected. To be consistent, do the - // same here. - if (selectedElements.length == 1) { - let element = selectedElements[0]; - if (element._shell) - element._shell.doDefaultCommand(); - } - } - else if (aEvent.charCode == " ".charCodeAt(0)) { - // Pausue/Resume every selected download - for (let element of selectedElements) { - if (element._shell.isCommandEnabled("downloadsCmd_pauseResume")) - element._shell.doCommand("downloadsCmd_pauseResume"); - } - } - } -}; - -function goUpdateDownloadCommands() { - for (let command of DOWNLOAD_VIEW_SUPPORTED_COMMANDS) { - goUpdateCommand(command); - } -} diff --git a/browser/components/places/content/places.css b/browser/components/places/content/places.css index 41c5afef3016..5151cca8254b 100644 --- a/browser/components/places/content/places.css +++ b/browser/components/places/content/places.css @@ -14,33 +14,3 @@ tree[type="places"] { menupopup[placespopup="true"] { -moz-binding: url("chrome://browser/content/places/menu.xml#places-popup-base"); } - -richlistitem.download { - -moz-binding: url('chrome://browser/content/places/download.xml#download'); -} - -.download-state:not( [state="0"] /* Downloading */) - .downloadPauseMenuItem, -.download-state:not( [state="4"] /* Paused */) - .downloadResumeMenuItem, -.download-state:not(:-moz-any([state="2"], /* Failed */ - [state="4"]) /* Paused */) - .downloadCancelMenuItem, -.download-state:not(:-moz-any([state="1"], /* Finished */ - [state="2"], /* Failed */ - [state="3"], /* Canceled */ - [state="6"], /* Blocked (parental) */ - [state="8"], /* Blocked (dirty) */ - [state="9"]) /* Blocked (policy) */) - .downloadRemoveFromListMenuItem, -.download-state:not(:-moz-any([state="-1"],/* Starting (initial) */ - [state="5"], /* Starting (queued) */ - [state="0"], /* Downloading */ - [state="4"]) /* Paused */) - .downloadShowMenuItem, - -.download-state[state="7"] .downloadCommandsSeparator - -{ - display: none; -} diff --git a/browser/components/places/content/places.js b/browser/components/places/content/places.js index e783ec407e29..bd9a0bacfd1d 100644 --- a/browser/components/places/content/places.js +++ b/browser/components/places/content/places.js @@ -5,13 +5,9 @@ Components.utils.import("resource:///modules/MigrationUtils.jsm"); -const DOWNLOADS_QUERY = "place:transition=" + - Components.interfaces.nsINavHistoryService.TRANSITION_DOWNLOAD + - "&sort=" + - Components.interfaces.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING; - var PlacesOrganizer = { _places: null, + _content: null, // IDs of fields from editBookmarkOverlay that should be hidden when infoBox // is minimal. IDs should be kept in sync with the IDs of the elements @@ -36,9 +32,8 @@ var PlacesOrganizer = { }, init: function PO_init() { - ContentArea.init(); - this._places = document.getElementById("placesList"); + this._content = document.getElementById("placeContent"); this._initFolderTree(); var leftPaneSelection = "AllBookmarks"; // default to all-bookmarks @@ -50,6 +45,12 @@ var PlacesOrganizer = { this._backHistory.splice(0, this._backHistory.length); document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true); + var view = this._content.treeBoxObject.view; + if (view.rowCount > 0) + view.selection.select(0); + + this._content.focus(); + // Set up the search UI. PlacesSearchBox.init(); @@ -83,13 +84,6 @@ var PlacesOrganizer = { #ifndef MOZ_PER_WINDOW_PRIVATE_BROWSING gPrivateBrowsingListener.init(); #endif - - // Select the first item in the content area view. - let view = ContentArea.currentView; - let root = view.result ? view.result.root : null; - if (root && root.containerOpen && root.childCount >= 0) - view.selectNode(root.getChild(0)); - ContentArea.focus(); }, QueryInterface: function PO_QueryInterface(aIID) { @@ -145,9 +139,9 @@ var PlacesOrganizer = { if (!this._places.hasSelection) { // If no node was found for the given place: uri, just load it directly - ContentArea.currentPlace = aLocation; + this._content.place = aLocation; } - this.updateDetailsPane(); + this.onContentTreeSelect(); // update navigation commands if (this._backHistory.length == 0) @@ -207,8 +201,8 @@ var PlacesOrganizer = { // If either the place of the content tree in the right pane has changed or // the user cleared the search box, update the place, hide the search UI, // and update the back/forward buttons by setting location. - if (ContentArea.currentPlace != placeURI || !resetSearchBox) { - ContentArea.currentPlace = placeURI; + if (this._content.place != placeURI || !resetSearchBox) { + this._content.place = placeURI; this.location = node.uri; } @@ -227,7 +221,8 @@ var PlacesOrganizer = { PlacesSearchBox.searchFilter.reset(); this._setSearchScopeForNode(node); - this.updateDetailsPane(); + if (this._places.treeBoxObject.focused) + this._fillDetailsPane([node]); }, /** @@ -252,39 +247,50 @@ var PlacesOrganizer = { }, /** - * Handle clicks on the places list. + * Handle clicks on the tree. * Single Left click, right click or modified click do not result in any * special action, since they're related to selection. * @param aEvent * The mouse event. */ - onPlacesListClick: function PO_onPlacesListClick(aEvent) { + onTreeClick: function PO_onTreeClick(aEvent) { // Only handle clicks on tree children. if (aEvent.target.localName != "treechildren") return; - let node = this._places.selectedNode; - if (node) { - let middleClick = aEvent.button == 1 && aEvent.detail == 1; - if (middleClick && PlacesUtils.nodeIsContainer(node)) { + var currentView = aEvent.currentTarget; + var selectedNode = currentView.selectedNode; + if (selectedNode) { + var doubleClickOnFlatList = (aEvent.button == 0 && aEvent.detail == 2 && + aEvent.target.parentNode.flatList); + var middleClick = (aEvent.button == 1 && aEvent.detail == 1); + + if (PlacesUtils.nodeIsURI(selectedNode) && + (doubleClickOnFlatList || middleClick)) { + // Open associated uri in the browser. + PlacesOrganizer.openSelectedNode(aEvent); + } + else if (middleClick && + PlacesUtils.nodeIsContainer(selectedNode)) { // The command execution function will take care of seeing if the // selection is a folder or a different container type, and will // load its contents in tabs. - PlacesUIUtils.openContainerNodeInTabs(selectedNode, aEvent, this._places); + PlacesUIUtils.openContainerNodeInTabs(selectedNode, aEvent, currentView); } } }, /** - * Handle focus changes on the places list and the current content view. + * Handle focus changes on the trees. + * When moving focus between panes we should update the details pane contents. + * @param aEvent + * The mouse event. */ - updateDetailsPane: function PO_updateDetailsPane() { - let view = PlacesUIUtils.getViewForNode(document.activeElement); - if (view) { - let selectedNodes = view.selectedNode ? - [view.selectedNode] : view.selectedNodes; - this._fillDetailsPane(selectedNodes); - } + onTreeFocus: function PO_onTreeFocus(aEvent) { + var currentView = aEvent.currentTarget; + var selectedNodes = currentView.selectedNode ? [currentView.selectedNode] : + this._content.selectedNodes; + this._fillDetailsPane(selectedNodes); }, openFlatContainer: function PO_openFlatContainerFlatContainer(aContainer) { @@ -294,12 +300,17 @@ var PlacesOrganizer = { this._places.selectPlaceURI(aContainer.uri); }, + openSelectedNode: function PO_openSelectedNode(aEvent) { + PlacesUIUtils.openNodeWithEvent(this._content.selectedNode, aEvent, + this._content); + }, + /** * Returns the options associated with the query currently loaded in the * main places pane. */ getCurrentOptions: function PO_getCurrentOptions() { - return PlacesUtils.asQuery(ContentArea.currentView.result.root).queryOptions; + return PlacesUtils.asQuery(this._content.result.root).queryOptions; }, /** @@ -307,7 +318,7 @@ var PlacesOrganizer = { * main places pane. */ getCurrentQueries: function PO_getCurrentQueries() { - return PlacesUtils.asQuery(ContentArea.currentView.result.root).getQueries(); + return PlacesUtils.asQuery(this._content.result.root).getQueries(); }, /** @@ -553,6 +564,11 @@ var PlacesOrganizer = { canvas.height = height; }, + onContentTreeSelect: function PO_onContentTreeSelect() { + if (this._content.treeBoxObject.focused) + this._fillDetailsPane(this._content.selectedNodes); + }, + _fillDetailsPane: function PO__fillDetailsPane(aNodeList) { var infoBox = document.getElementById("infoBox"); var detailsDeck = document.getElementById("detailsDeck"); @@ -655,15 +671,10 @@ var PlacesOrganizer = { else { detailsDeck.selectedIndex = 0; infoBox.hidden = true; - let selectItemDesc = document.getElementById("selectItemDescription"); - let itemsCountLabel = document.getElementById("itemsCountText"); - let itemsCount = 0; - if (ContentArea.currentView.result) { - let rootNode = ContentArea.currentView.result.root; - if (rootNode.containerOpen) - itemsCount = rootNode.childCount; - } - if (itemsCount == 0) { + var selectItemDesc = document.getElementById("selectItemDescription"); + var itemsCountLabel = document.getElementById("itemsCountText"); + var rowCount = this._content.treeBoxObject.view.rowCount; + if (rowCount == 0) { selectItemDesc.hidden = true; itemsCountLabel.value = PlacesUIUtils.getString("detailsPane.noItems"); } @@ -671,7 +682,7 @@ var PlacesOrganizer = { selectItemDesc.hidden = false; itemsCountLabel.value = PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", - itemsCount, [itemsCount]); + rowCount, [rowCount]); } } }, @@ -768,14 +779,14 @@ var PlacesSearchBox = { return; } - let currentView = ContentArea.currentView; - let currentOptions = PO.getCurrentOptions(); + var currentOptions = PO.getCurrentOptions(); + var content = PO._content; // Search according to the current scope, which was set by // PQB_setScope() switch (PlacesSearchBox.filterCollection) { case "bookmarks": - currentView.applyFilter(filterString, this.folders); + content.applyFilter(filterString, this.folders); break; case "history": if (currentOptions.queryType != Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) { @@ -786,17 +797,27 @@ var PlacesSearchBox = { options.resultType = currentOptions.RESULT_TYPE_URI; options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; options.includeHidden = true; - currentView.load([query], options); + content.load([query], options); } else { - currentView.applyFilter(filterString, null, true); + content.applyFilter(filterString, null, true); } break; - case "downloads": - currentView.searchTerm = filterString; + case "downloads": { + let query = PlacesUtils.history.getNewQuery(); + query.searchTerms = filterString; + query.setTransitions([Ci.nsINavHistoryService.TRANSITION_DOWNLOAD], 1); + let options = currentOptions.clone(); + // Make sure we're getting uri results. + options.resultType = currentOptions.RESULT_TYPE_URI; + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; + options.includeHidden = true; + content.load([query], options); break; - default: - throw "Invalid filterCollection on search"; + } + default: + throw "Invalid filterCollection on search"; + break; } // Update the details panel @@ -1230,94 +1251,3 @@ let gPrivateBrowsingListener = { } }; #endif - -let ContentArea = { - init: function CA_init() { - this._deck = document.getElementById("placesViewsDeck"); - this._specialViews = new Map(); - ContentTree.init(); - }, - - _shouldUseNewDownloadsView: function CA_shouldUseNewDownloadsView() { - try { - return Services.prefs.getBoolPref("browser.library.useNewDownloadsView"); - } - catch(ex) { } - return false; - }, - - getContentViewForQueryString: - function CA_getContentViewForQueryString(aQueryString) { - if (this._specialViews.has(aQueryString)) - return this._specialViews.get(aQueryString); - if (aQueryString == DOWNLOADS_QUERY && this._shouldUseNewDownloadsView()) { - let view = new DownloadsPlacesView(document.getElementById("downloadsRichListBox"), aQueryString); - this.setContentViewForQueryString(aQueryString, view); - return view; - } - return ContentTree.view; - }, - - setContentViewForQueryString: - function CA_setContentViewForQueryString(aQueryString, aView) { - this._specialViews.set(aQueryString, aView); - }, - - get currentView() PlacesUIUtils.getViewForNode(this._deck.selectedPanel), - set currentView(aView) { - if (this.currentView != aView) - this._deck.selectedPanel = aView.associatedElement; - return aView; - }, - - get currentPlace() this.currentView.place, - set currentPlace(aQueryString) { - this.currentView = this.getContentViewForQueryString(aQueryString); - this.currentView.place = aQueryString; - return aQueryString; - }, - - focus: function() { - this._deck.selectedPanel.focus(); - } -}; - -let ContentTree = { - init: function CT_init() { - this._view = document.getElementById("placeContent"); - }, - - get view() this._view, - - openSelectedNode: function CT_openSelectedNode(aEvent) { - let view = this.view; - PlacesUIUtils.openNodeWithEvent(view.selectedNode, aEvent, view); - }, - - onClick: function CT_onClick(aEvent) { - // Only handle clicks on tree children. - if (aEvent.target.localName != "treechildren") - return; - - let node = this.view.selectedNode; - if (node) { - let doubleClick = aEvent.button == 0 && aEvent.detail == 2; - let middleClick = aEvent.button == 1 && aEvent.detail == 1; - if (PlacesUtils.nodeIsURI(node) && (doubleClick || middleClick)) { - // Open associated uri in the browser. - this.openSelectedNode(aEvent); - } - else if (middleClick && PlacesUtils.nodeIsContainer(node)) { - // The command execution function will take care of seeing if the - // selection is a folder or a different container type, and will - // load its contents in tabs. - PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this.view); - } - } - }, - - onKeyPress: function CT_onKeyPress(aEvent) { - if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) - this.openSelectedNode(aEvent); - } -}; diff --git a/browser/components/places/content/places.xul b/browser/components/places/content/places.xul index e98088b74668..77e94c0a60a8 100644 --- a/browser/components/places/content/places.xul +++ b/browser/components/places/content/places.xul @@ -10,7 +10,6 @@ - @@ -29,8 +28,6 @@ %editMenuOverlayDTD; %browserDTD; - -%downloadsDTD; ]> -