Bug 830064 - The new downloads view does not support drag and drop. r=mak, a=akeybl
authorAsaf Romano <mano@mozilla.com>
Wed, 16 Jan 2013 22:39:16 +0200
changeset 127205 6766940f3ac857f436f48c11ad0c0bfc2080c3a0
parent 127204 884c383e811383369e8670277e32bf8604b1306a
child 127206 331b0a4805860b3844ac3f6c7a6ade509931ab99
push id2151
push userlsblakk@mozilla.com
push dateTue, 19 Feb 2013 18:06:57 +0000
treeherdermozilla-beta@4952e88741ec [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak, akeybl
bugs830064
milestone20.0a2
Bug 830064 - The new downloads view does not support drag and drop. r=mak, a=akeybl
browser/components/downloads/content/allDownloadsViewOverlay.js
browser/components/downloads/content/allDownloadsViewOverlay.xul
--- a/browser/components/downloads/content/allDownloadsViewOverlay.js
+++ b/browser/components/downloads/content/allDownloadsViewOverlay.js
@@ -36,16 +36,32 @@ 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", "downloadsCmd_clearDownloads"];
 
 const NOT_AVAILABLE = Number.MAX_VALUE;
 
 /**
+ * Download a URL.
+ *
+ * @param aURL
+ *        the url to download (nsIURI object)
+ * @param [optional] aFileName
+ *        the destination file name
+ */
+function DownloadURL(aURL, aFileName) {
+  // For private browsing, try to get document out of the most recent browser
+  // window, or provide our own if there's no browser window.
+  let browserWin = RecentWindow.getMostRecentBrowserWindow();
+  let initiatingDoc = browserWin ? browserWin.document : document;
+  saveURL(aURL, aFileName, null, true, true, undefined, initiatingDoc);
+}
+
+/**
  * 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.
@@ -155,17 +171,17 @@ DownloadElementShell.prototype = {
 
   get _downloadURIObj() {
     if (!("__downloadURIObj" in this))
       this.__downloadURIObj = NetUtil.newURI(this.downloadURI);
     return this.__downloadURIObj;
   },
 
   _getIcon: function DES__getIcon() {
-    let metaData = this._getDownloadMetaData();
+    let metaData = this.getDownloadMetaData();
     if ("filePath" in metaData)
       return "moz-icon://" + metaData.filePath + "?size=32";
 
     if (this._placesNode) {
       // Try to extract an extension from the uri.
       let ext = this._downloadURIObj.QueryInterface(Ci.nsIURL).fileExtension;
       if (ext)
         return "moz-icon://." + ext + "?size=32";
@@ -208,17 +224,17 @@ DownloadElementShell.prototype = {
   },
 
   _fetchTargetFileInfo: function DES__fetchTargetFileInfo(aUpdateMetaDataAndStatusUI = false) {
     if (this._targetFileInfoFetched)
       throw new Error("_fetchTargetFileInfo should not be called if the information was already fetched");
     if (!this.active)
       throw new Error("Trying to _fetchTargetFileInfo on an inactive download shell");
 
-    let path = this._getDownloadMetaData().filePath;
+    let path = this.getDownloadMetaData().filePath;
 
     // In previous version, the target file annotations were not set,
     // so we cannot tell where is the file.
     if (path === undefined) {
       this._targetFileInfoFetched = true;
       this._targetFileExists = false;
       if (aUpdateMetaDataAndStatusUI) {
         this._metaData = null;
@@ -280,34 +296,34 @@ DownloadElementShell.prototype = {
    *
    * - state - any download state defined in nsIDownloadManager.  If this field
    *   is not set, the download state is unknown.
    * - endTime: the end time of the download.
    * - filePath: the downloaded file path on the file system, when it
    *   was downloaded.  The file may not exist.  This is set for session
    *   downloads that have a local file set, and for history downloads done
    *   after the landing of bug 591289.
+   * - fileName: the downloaded file name on the file system. Set if filePath
+   *   is set.
    * - displayName: the user-facing label for the download.  This is always
    *   set.  If available, it's set to the downloaded file name.  If not,
    *   the places title for the download uri is used it's set.  As a last
    *   resort, we fallback to the download uri.
    * - fileSize (only set for downloads which completed succesfully):
    *   the downloaded file size.  For downloads done after the landing of
    *   bug 826991, this value is "static" - that is, it does not necessarily
    *   mean that the file is in place and has this size.
    */
-  _getDownloadMetaData: function DES__getDownloadMetaData() {
-    if (!this.active)
-      throw new Error("_getDownloadMetaData called for an inactive item.");
-
+  getDownloadMetaData: function DES_getDownloadMetaData() {
     if (!this._metaData) {
       if (this._dataItem) {
         this._metaData = {
           state:       this._dataItem.state,
           endTime:     this._dataItem.endTime,
+          fileName:    this._dataItem.target,
           displayName: this._dataItem.target
         };
         if (this._dataItem.done)
           this._metaData.fileSize = this._dataItem.maxBytes;
         if (this._dataItem.localFile)
           this._metaData.filePath = this._dataItem.localFile.path;
       }
       else {
@@ -323,18 +339,19 @@ DownloadElementShell.prototype = {
           }
 
           // This is actually the start-time, but it's the best we can get.
           this._metaData.endTime = this._placesNode.time / 1000;
         }
 
         try {
           let targetFileURI = this._getAnnotation(DESTINATION_FILE_URI_ANNO);
-          [this._metaData.filePath, this._metaData.displayName] =
+          [this._metaData.filePath, this._metaData.fileName] =
             this._extractFilePathAndNameFromFileURI(targetFileURI);
+          this._metaData.displayName = this._metaData.fileName;
         }
         catch(ex) {
           this._metaData.displayName = this._placesNode.title || this.downloadURI;
         }
       }
     }
     return this._metaData;
   },
@@ -368,17 +385,17 @@ DownloadElementShell.prototype = {
         return s.stateScanning;
       }
 
       throw new Error("_getStatusText called with a bogus download state");
     }
 
     // This is a not-in-progress or history download.
     let stateLabel = "";
-    let state = this._getDownloadMetaData().state;
+    let state = this.getDownloadMetaData().state;
     switch (state) {
       case nsIDM.DOWNLOAD_FAILED:
         stateLabel = s.stateFailed;
         break;
       case nsIDM.DOWNLOAD_CANCELED:
         stateLabel = s.stateCanceled;
         break;
       case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
@@ -387,17 +404,17 @@ DownloadElementShell.prototype = {
       case nsIDM.DOWNLOAD_BLOCKED_POLICY:
         stateLabel = s.stateBlockedPolicy;
         break;
       case nsIDM.DOWNLOAD_DIRTY:
         stateLabel = s.stateDirty;
         break;
       case nsIDM.DOWNLOAD_FINISHED:{
         // For completed downloads, show the file size (e.g. "1.5 MB")
-        let metaData = this._getDownloadMetaData();
+        let metaData = this.getDownloadMetaData();
         if ("fileSize" in metaData) {
           let [size, unit] = DownloadUtils.convertByteUnits(metaData.fileSize);
           stateLabel = s.sizeWithUnits(size, unit);
           break;
         }
         // Fallback to default unknown state.
       }
       default:
@@ -405,17 +422,17 @@ DownloadElementShell.prototype = {
         break;
     }
 
     // TODO (bug 829201): history downloads should get the referrer from Places.
     let referrer = this._dataItem && this._dataItem.referrer ||
                    this.downloadURI;
     let [displayHost, fullHost] = DownloadUtils.getURIHost(referrer);
 
-    let date = new Date(this._getDownloadMetaData().endTime);
+    let date = new Date(this.getDownloadMetaData().endTime);
     let [displayDate, fullDate] = DownloadUtils.getReadableDates(date);
 
     // We use the same XUL label to display the state, the host name, and the
     // end time.
     let firstPart = s.statusSeparator(stateLabel, displayHost);
     return s.statusSeparator(firstPart, displayDate);
   },
 
@@ -431,17 +448,17 @@ DownloadElementShell.prototype = {
 
   // 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() {
     if (!this.active)
       throw new Error("_updateDownloadStatusUI called for an inactive item.");
 
-    let state = this._getDownloadMetaData().state;
+    let state = this.getDownloadMetaData().state;
     if (state !== undefined)
       this._element.setAttribute("state", state);
 
     this._element.setAttribute("status", this._getStatusText());
 
     // For past-downloads, we're done. For session-downloads, we may also need
     // to update the progress-meter.
     if (!this._dataItem)
@@ -469,91 +486,92 @@ DownloadElementShell.prototype = {
     if (this._progressElement) {
       let event = document.createEvent("Events");
       event.initEvent("ValueChange", true, true);
       this._progressElement.dispatchEvent(event);
     }
   },
 
   _updateDisplayNameAndIcon: function DES__updateDisplayNameAndIcon() {
-    let metaData = this._getDownloadMetaData();
+    let metaData = this.getDownloadMetaData();
     this._element.setAttribute("displayName", metaData.displayName);
     this._element.setAttribute("image", this._getIcon());
   },
 
   _updateUI: function DES__updateUI() {
     if (!this.active)
       throw new Error("Trying to _updateUI on an inactive download shell");
 
     this._metaData = null;
     this._targetFileInfoFetched = false;
 
     this._updateDisplayNameAndIcon();
 
     // For history downloads done in past releases, the downloads/metaData
     // annotation is not set, and therefore we cannot tell the download
     // state without the target file information.
-    if (this._dataItem || this._getDownloadMetaData().state !== undefined)
+    if (this._dataItem || this.getDownloadMetaData().state !== undefined)
       this._updateDownloadStatusUI();
     else
       this._fetchTargetFileInfo(true);
   },
 
   placesNodeIconChanged: function DES_placesNodeIconChanged() {
     if (!this._dataItem)
       this._element.setAttribute("image", this._getIcon());
   },
 
   placesNodeTitleChanged: function DES_placesNodeTitleChanged() {
     // If there's a file path, we use the leaf name for the title.
-    if (!this._dataItem && this.active && !this._getDownloadMetaData().filePath) {
+    if (!this._dataItem && this.active && !this.getDownloadMetaData().filePath) {
       this._metaData = null;
       this._updateDisplayNameAndIcon();
     }
   },
 
   placesNodeAnnotationChanged: function DES_placesNodeAnnotationChanged(aAnnoName) {
     this._annotations.delete(aAnnoName);
     if (!this._dataItem && this.active) {
       if (aAnnoName == DOWNLOAD_META_DATA_ANNO) {
-        let metaData = this._getDownloadMetaData();
+        let metaData = this.getDownloadMetaData();
         let annotatedMetaData = this._getAnnotatedMetaData();
         metaData.endTme = annotatedMetaData.endTime;
         if ("fileSize" in annotatedMetaData)
           metaData.fileSize = annotatedMetaData.fileSize;
         else
           delete metaData.fileSize;
 
         if (metaData.state != annotatedMetaData.state) {
           metaData.state = annotatedMetaData.state;
           if (this._element.selected)
             goUpdateDownloadCommands();
         }
 
         this._updateDownloadStatusUI();
       }
       else if (aAnnoName == DESTINATION_FILE_URI_ANNO) {
-        let metaData = this._getDownloadMetaData();
+        let metaData = this.getDownloadMetaData();
         let targetFileURI = this._getAnnotation(DESTINATION_FILE_URI_ANNO);
-        [metaData.filePath, metaData.displayName] =
+        [metaData.filePath, metaData.fileName] =
             this._extractFilePathAndNameFromFileURI(targetFileURI);
+        metaData.displayName = metaData.fileName;
         this._updateDisplayNameAndIcon();
 
         if (this._targetFileInfoFetched) {
           // This will also update the download commands if necessary.
           this._targetFileInfoFetched = false;
           this._fetchTargetFileInfo();
         }
       }
     }
   },
 
   /* DownloadView */
   onStateChange: function DES_onStateChange(aOldState) {
-    let metaData = this._getDownloadMetaData();
+    let metaData = this.getDownloadMetaData();
     metaData.state = this.dataItem.state;
     if (aOldState != nsIDM.DOWNLOAD_FINISHED && aOldState != metaData.state) {
       // See comment in DVI_onStateChange in downloads.js (the panel-view)
       this._element.setAttribute("image", this._getIcon() + "&state=normal");
       metaData.fileSize = this._dataItem.maxBytes;
       if (this._targetFileInfoFetched) {
         this._targetFileInfoFetched = false;
         this._fetchTargetFileInfo();
@@ -585,30 +603,30 @@ DownloadElementShell.prototype = {
         if (this._dataItem && !this._dataItem.openable)
           return false;
 
         if (this._targetFileInfoFetched)
           return this._targetFileExists;
 
         // If the target file information is not yet fetched,
         // temporarily assume that the file is in place.
-        return this._getDownloadMetaData().state == nsIDM.DOWNLOAD_FINISHED;
+        return this.getDownloadMetaData().state == nsIDM.DOWNLOAD_FINISHED;
       }
       case "downloadsCmd_show": {
         // TODO: Bug 827010 - Handle part-file asynchronously.
         if (this._dataItem &&
             this._dataItem.partFile && this._dataItem.partFile.exists())
           return true;
 
         if (this._targetFileInfoFetched)
           return this._targetFileExists;
 
         // If the target file information is not yet fetched,
         // temporarily assume that the file is in place.
-        return this._getDownloadMetaData().state == nsIDM.DOWNLOAD_FINISHED;
+        return this.getDownloadMetaData().state == nsIDM.DOWNLOAD_FINISHED;
       }
       case "downloadsCmd_pauseResume":
         return this._dataItem && this._dataItem.inProgress && this._dataItem.resumable;
       case "downloadsCmd_retry":
         // An history download can always be retried.
         return !this._dataItem || this._dataItem.canRetry;
       case "downloadsCmd_openReferrer":
         return this._dataItem && !!this._dataItem.referrer;
@@ -622,42 +640,36 @@ DownloadElementShell.prototype = {
     }
     return false;
   },
 
   _retryAsHistoryDownload: function DES__retryAsHistoryDownload() {
     // In future we may try to download into the same original target uri, when
     // we have it.  Though that requires verifying the path is still valid and
     // may surprise the user if he wants to be requested every time.
-
-    // For private browsing, try to get document out of the most recent browser
-    // window, or provide our own if there's no browser window.
-    let browserWin = RecentWindow.getMostRecentBrowserWindow();
-    let initiatingDoc = browserWin ? browserWin.document : document;
-    saveURL(this.downloadURI, this._getDownloadMetaData().displayName, null, true, true, undefined,
-            initiatingDoc);
+    DownloadURL(this.downloadURI, this.getDownloadMetaData().fileName);
   },
 
   /* nsIController */
   doCommand: function DES_doCommand(aCommand) {
     switch (aCommand) {
       case "downloadsCmd_open": {
         let file = this._dataItem ?
           this.dataItem.localFile :
-          new FileUtils.File(this._getDownloadMetaData().filePath);
+          new FileUtils.File(this.getDownloadMetaData().filePath);
 
         DownloadsCommon.openDownloadedFile(file, null, window);
         break;
       }
       case "downloadsCmd_show": {
         if (this._dataItem) {
           this._dataItem.showLocalFile();
         }
         else {
-          let file = new FileUtils.File(this._getDownloadMetaData().filePath);
+          let file = new FileUtils.File(this.getDownloadMetaData().filePath);
           DownloadsCommon.showDownloadedFile(file);
         }
         break;
       }
       case "downloadsCmd_openReferrer": {
         openURL(this._dataItem.referrer);
         break;
       }
@@ -688,18 +700,18 @@ DownloadElementShell.prototype = {
 
   // 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) {
     if (!aTerm)
       return true;
     aTerm = aTerm.toLowerCase();
-    return this._getDownloadMetaData().displayName.toLowerCase().indexOf(aTerm) != -1 ||
-           this.downloadURI.toLowerCase().indexOf(aTerm) != -1;
+    return this.getDownloadMetaData().displayName.toLowerCase().contains(aTerm) ||
+           this.downloadURI.toLowerCase().contains(aTerm);
   },
 
   // 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:
@@ -717,17 +729,17 @@ DownloadElementShell.prototype = {
           return "downloadsCmd_show";
         case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
         case nsIDM.DOWNLOAD_DIRTY:
         case nsIDM.DOWNLOAD_BLOCKED_POLICY:
           return "downloadsCmd_openReferrer";
       }
       return "";
     }
-    let command = getDefaultCommandForState(this._getDownloadMetaData().state);
+    let command = getDefaultCommandForState(this.getDownloadMetaData().state);
     if (this.isCommandEnabled(command))
       this.doCommand(command);
   },
 
   /**
    * At the first time an item is selected, we don't yet have
    * the target file information.  Thus the call to goUpdateDownloadCommands
    * in DPV_onSelect would result in best-guess enabled/disabled result.
@@ -1303,17 +1315,16 @@ DownloadsPlacesView.prototype = {
       if (document.activeElement == this._richlistbox ||
           aCommand == "downloadsCmd_clearDownloads") {
         return true;
       }
     }
     return false;
   },
 
-
   isCommandEnabled: function DPV_isCommandEnabled(aCommand) {
     switch (aCommand) {
       case "cmd_copy":
         return this._richlistbox.selectedItems.length > 0;
       case "cmd_selectAll":
         return true;
       case "cmd_paste":
         return this._canDownloadClipboardURL();
@@ -1373,17 +1384,17 @@ DownloadsPlacesView.prototype = {
 
   _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);
+    DownloadURL(url, name);
   },
 
   doCommand: function DPV_doCommand(aCommand) {
     switch (aCommand) {
       case "cmd_copy":
         this._copySelectedDownloadsToClipboard();
         break;
       case "cmd_selectAll":
@@ -1421,17 +1432,17 @@ DownloadsPlacesView.prototype = {
   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");
-    let state = element._shell._getDownloadMetaData().state;
+    let state = element._shell.getDownloadMetaData().state;
     if (state !== undefined)
       contextMenu.setAttribute("state", state);
     else
       contextMenu.removeAttribute("state");
 
     return true;
   },
 
@@ -1476,16 +1487,61 @@ DownloadsPlacesView.prototype = {
   onSelect: function DPV_onSelect() {
     goUpdateDownloadCommands();
 
     let selectedElements = this._richlistbox.selectedItems;
     for (let elt of selectedElements) {
       if (elt._shell)
         elt._shell.onSelect();
     }
+  },
+
+  onDragStart: function DPV_onDragStart(aEvent) {
+    // TODO Bug 831358: Support d&d for multiple selection.
+    // For now, we just drag the first element.
+    let selectedItem = this._richlistbox.selectedItem;
+    if (!selectedItem)
+      return;
+
+    let metaData = selectedItem._shell.getDownloadMetaData();
+    if (!("filePath" in metaData))
+      return;
+    let file = new FileUtils.File(metaData.filePath);
+    if (!file.exists())
+      return;
+
+    let dt = aEvent.dataTransfer;
+    dt.mozSetDataAt("application/x-moz-file", file, 0);
+    let url = Services.io.newFileURI(file).spec;
+    dt.setData("text/uri-list", url);
+    dt.setData("text/plain", url);
+    dt.effectAllowed = "copyMove";
+    dt.addElement(selectedItem);
+  },
+
+  onDragOver: function DPV_onDragOver(aEvent) {
+    let types = aEvent.dataTransfer.types;
+    if (types.contains("text/uri-list") ||
+        types.contains("text/x-moz-url") ||
+        types.contains("text/plain")) {
+      aEvent.preventDefault();
+    }
+  },
+
+  onDrop: function DPV_onDrop(aEvent) {
+    let dt = aEvent.dataTransfer;
+    // If dragged item is from our source, do not try to
+    // redownload already downloaded file.
+    if (dt.mozGetDataAt("application/x-moz-file", 0))
+      return;
+
+    let name = { };
+    let url = Services.droppedLinkHandler.dropLink(aEvent, name);
+    if (url)
+      DownloadURL(url, name.value);
   }
 };
 
 for (let methodName of ["load", "applyFilter", "selectNode", "selectItems"]) {
   DownloadsPlacesView.prototype[methodName] = function() {
     throw new Error("|" + methodName + "| is not implemented by the downloads view.");
   }
 }
--- a/browser/components/downloads/content/allDownloadsViewOverlay.xul
+++ b/browser/components/downloads/content/allDownloadsViewOverlay.xul
@@ -41,16 +41,19 @@
 
   <richlistbox flex="1"
                seltype="multiple"
                id="downloadsRichListBox" context="downloadsContextMenu"
                onscroll="return this._placesView.onScroll();"
                onkeypress="return this._placesView.onKeyPress(event);"
                ondblclick="return this._placesView.onDoubleClick(event);"
                oncontextmenu="return this._placesView.onContextMenu(event);"
+               ondragstart="this._placesView.onDragStart(event);"
+               ondragover="this._placesView.onDragOver(event);"
+               ondrop="this._placesView.onDrop(event);"
                onfocus="goUpdateDownloadCommands();"
                onselect="this._placesView.onSelect();"
                onblur="goUpdateDownloadCommands();"/>
 
   <commandset id="downloadCommands"
               commandupdater="true"
               events="focus,select,contextmenu"
               oncommandupdate="goUpdateDownloadCommands();">