Home | History | Annotate | Download | only in js
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 'use strict';
      6 
      7 /**
      8  * The current selection object.
      9  *
     10  * @param {FileManager} fileManager FileManager instance.
     11  * @param {Array.<number>} indexes Selected indexes.
     12  * @constructor
     13  */
     14 function FileSelection(fileManager, indexes) {
     15   this.fileManager_ = fileManager;
     16   this.computeBytesSequence_ = 0;
     17   this.indexes = indexes;
     18   this.entries = [];
     19   this.urls = [];
     20   this.totalCount = 0;
     21   this.fileCount = 0;
     22   this.directoryCount = 0;
     23   this.bytes = 0;
     24   this.showBytes = false;
     25   this.allDriveFilesPresent = false,
     26   this.iconType = null;
     27   this.bytesKnown = false;
     28   this.mustBeHidden_ = false;
     29 
     30   // Synchronously compute what we can.
     31   for (var i = 0; i < this.indexes.length; i++) {
     32     var entry = fileManager.getFileList().item(this.indexes[i]);
     33     if (!entry)
     34       continue;
     35 
     36     this.entries.push(entry);
     37     this.urls.push(entry.toURL());
     38 
     39     if (this.iconType == null) {
     40       this.iconType = FileType.getIcon(entry);
     41     } else if (this.iconType != 'unknown') {
     42       var iconType = FileType.getIcon(entry);
     43       if (this.iconType != iconType)
     44         this.iconType = 'unknown';
     45     }
     46 
     47     if (entry.isFile) {
     48       this.fileCount += 1;
     49     } else {
     50       this.directoryCount += 1;
     51     }
     52     this.totalCount++;
     53   }
     54 
     55   this.tasks = new FileTasks(this.fileManager_);
     56 }
     57 
     58 /**
     59  * Computes data required to get file tasks and requests the tasks.
     60  *
     61  * @param {function} callback The callback.
     62  */
     63 FileSelection.prototype.createTasks = function(callback) {
     64   if (!this.fileManager_.isOnDrive()) {
     65     this.tasks.init(this.urls);
     66     callback();
     67     return;
     68   }
     69 
     70   this.fileManager_.metadataCache_.get(this.urls, 'drive', function(props) {
     71     var present = props.filter(function(p) { return p && p.availableOffline });
     72     this.allDriveFilesPresent = present.length == props.length;
     73 
     74     // Collect all of the mime types and push that info into the selection.
     75     this.mimeTypes = props.map(function(value) {
     76       return (value && value.contentMimeType) || '';
     77     });
     78 
     79     this.tasks.init(this.urls, this.mimeTypes);
     80     callback();
     81   }.bind(this));
     82 };
     83 
     84 /**
     85  * Computes the total size of selected files.
     86  *
     87  * @param {function} callback Completion callback. Not called when cancelled,
     88  *     or a new call has been invoked in the meantime.
     89  */
     90 FileSelection.prototype.computeBytes = function(callback) {
     91   if (this.entries.length == 0) {
     92     this.bytesKnown = true;
     93     this.showBytes = false;
     94     this.bytes = 0;
     95     return;
     96   }
     97 
     98   var computeBytesSequence = ++this.computeBytesSequence_;
     99   var pendingMetadataCount = 0;
    100 
    101   var maybeDone = function() {
    102     if (pendingMetadataCount == 0) {
    103       this.bytesKnown = true;
    104       callback();
    105     }
    106   }.bind(this);
    107 
    108   var onProps = function(properties) {
    109     // Ignore if the call got cancelled, or there is another new one fired.
    110     if (computeBytesSequence != this.computeBytesSequence_)
    111       return;
    112 
    113     // It may happen that the metadata is not available because a file has been
    114     // deleted in the meantime.
    115     if (properties)
    116       this.bytes += properties.size;
    117     pendingMetadataCount--;
    118     maybeDone();
    119   }.bind(this);
    120 
    121   for (var index = 0; index < this.entries.length; index++) {
    122     var entry = this.entries[index];
    123     if (entry.isFile) {
    124       this.showBytes |= !FileType.isHosted(entry);
    125       pendingMetadataCount++;
    126       this.fileManager_.metadataCache_.get(entry, 'filesystem', onProps);
    127     } else if (entry.isDirectory) {
    128       // Don't compute the directory size as it's expensive.
    129       // crbug.com/179073.
    130       this.showBytes = false;
    131       break;
    132     }
    133   }
    134   maybeDone();
    135 };
    136 
    137 /**
    138  * Cancels any async computation by increasing the sequence number. Results
    139  * of any previous call to computeBytes() will be discarded.
    140  *
    141  * @private
    142  */
    143 FileSelection.prototype.cancelComputing_ = function() {
    144   this.computeBytesSequence_++;
    145 };
    146 
    147 /**
    148  * This object encapsulates everything related to current selection.
    149  *
    150  * @param {FileManager} fileManager File manager instance.
    151  * @extends {cr.EventTarget}
    152  * @constructor
    153  */
    154 function FileSelectionHandler(fileManager) {
    155   this.fileManager_ = fileManager;
    156   // TODO(dgozman): create a shared object with most of UI elements.
    157   this.okButton_ = fileManager.okButton_;
    158   this.filenameInput_ = fileManager.filenameInput_;
    159 
    160   this.previewPanel_ = fileManager.dialogDom_.querySelector('.preview-panel');
    161   this.previewThumbnails_ = this.previewPanel_.
    162       querySelector('.preview-thumbnails');
    163   this.previewSummary_ = this.previewPanel_.querySelector('.preview-summary');
    164   this.previewText_ = this.previewSummary_.querySelector('.preview-text');
    165   this.calculatingSize_ = this.previewSummary_.
    166       querySelector('.calculating-size');
    167   this.calculatingSize_.textContent = str('CALCULATING_SIZE');
    168 
    169   this.searchBreadcrumbs_ = fileManager.searchBreadcrumbs_;
    170   this.taskItems_ = fileManager.taskItems_;
    171 
    172   this.animationTimeout_ = null;
    173 }
    174 
    175 /**
    176  * FileSelectionHandler extends cr.EventTarget.
    177  */
    178 FileSelectionHandler.prototype.__proto__ = cr.EventTarget.prototype;
    179 
    180 /**
    181  * Maximum amount of thumbnails in the preview pane.
    182  *
    183  * @const
    184  * @type {number}
    185  */
    186 FileSelectionHandler.MAX_PREVIEW_THUMBNAIL_COUNT = 4;
    187 
    188 /**
    189  * Maximum width or height of an image what pops up when the mouse hovers
    190  * thumbnail in the bottom panel (in pixels).
    191  *
    192  * @const
    193  * @type {number}
    194  */
    195 FileSelectionHandler.IMAGE_HOVER_PREVIEW_SIZE = 200;
    196 
    197 /**
    198  * Update the UI when the selection model changes.
    199  *
    200  * @param {cr.Event} event The change event.
    201  */
    202 FileSelectionHandler.prototype.onFileSelectionChanged = function(event) {
    203   var indexes =
    204       this.fileManager_.getCurrentList().selectionModel.selectedIndexes;
    205   if (this.selection) this.selection.cancelComputing_();
    206   var selection = new FileSelection(this.fileManager_, indexes);
    207   this.selection = selection;
    208 
    209   if (this.fileManager_.dialogType == DialogType.SELECT_SAVEAS_FILE) {
    210     // If this is a save-as dialog, copy the selected file into the filename
    211     // input text box.
    212     if (this.selection.totalCount == 1 &&
    213         this.selection.entries[0].isFile &&
    214         this.filenameInput_.value != this.selection.entries[0].name) {
    215       this.filenameInput_.value = this.selection.entries[0].name;
    216     }
    217   }
    218 
    219   this.updateOkButton();
    220 
    221   if (this.selectionUpdateTimer_) {
    222     clearTimeout(this.selectionUpdateTimer_);
    223     this.selectionUpdateTimer_ = null;
    224   }
    225 
    226   this.hideCalculating_();
    227 
    228   // The rest of the selection properties are computed via (sometimes lengthy)
    229   // asynchronous calls. We initiate these calls after a timeout. If the
    230   // selection is changing quickly we only do this once when it slows down.
    231 
    232   var updateDelay = 200;
    233   var now = Date.now();
    234   if (now > (this.lastFileSelectionTime_ || 0) + updateDelay) {
    235     // The previous selection change happened a while ago. Update the UI soon.
    236     updateDelay = 0;
    237   }
    238   this.lastFileSelectionTime_ = now;
    239 
    240   this.selectionUpdateTimer_ = setTimeout(function() {
    241     this.selectionUpdateTimer_ = null;
    242     if (this.selection == selection)
    243       this.updateFileSelectionAsync(selection);
    244   }.bind(this), updateDelay);
    245 };
    246 
    247 /**
    248  * Clears the primary UI selection elements.
    249  */
    250 FileSelectionHandler.prototype.clearUI = function() {
    251   this.previewThumbnails_.textContent = '';
    252   this.previewText_.textContent = '';
    253   this.hideCalculating_();
    254   this.taskItems_.hidden = true;
    255   this.okButton_.disabled = true;
    256 };
    257 
    258 /**
    259  * Updates the Ok button enabled state.
    260  *
    261  * @return {boolean} Whether button is enabled.
    262  */
    263 FileSelectionHandler.prototype.updateOkButton = function() {
    264   var selectable;
    265   var dialogType = this.fileManager_.dialogType;
    266 
    267   if (dialogType == DialogType.SELECT_FOLDER ||
    268       dialogType == DialogType.SELECT_UPLOAD_FOLDER) {
    269     // In SELECT_FOLDER mode, we allow to select current directory
    270     // when nothing is selected.
    271     selectable = this.selection.directoryCount <= 1 &&
    272         this.selection.fileCount == 0;
    273   } else if (dialogType == DialogType.SELECT_OPEN_FILE) {
    274     selectable = (this.isFileSelectionAvailable() &&
    275                   this.selection.directoryCount == 0 &&
    276                   this.selection.fileCount == 1);
    277   } else if (dialogType == DialogType.SELECT_OPEN_MULTI_FILE) {
    278     selectable = (this.isFileSelectionAvailable() &&
    279                   this.selection.directoryCount == 0 &&
    280                   this.selection.fileCount >= 1);
    281   } else if (dialogType == DialogType.SELECT_SAVEAS_FILE) {
    282     if (this.fileManager_.isOnReadonlyDirectory()) {
    283       selectable = false;
    284     } else {
    285       selectable = !!this.filenameInput_.value;
    286     }
    287   } else if (dialogType == DialogType.FULL_PAGE) {
    288     // No "select" buttons on the full page UI.
    289     selectable = true;
    290   } else {
    291     throw new Error('Unknown dialog type');
    292   }
    293 
    294   this.okButton_.disabled = !selectable;
    295   return selectable;
    296 };
    297 
    298 /**
    299   * Check if all the files in the current selection are available. The only
    300   * case when files might be not available is when the selection contains
    301   * uncached Drive files and the browser is offline.
    302   *
    303   * @return {boolean} True if all files in the current selection are
    304   *                   available.
    305   */
    306 FileSelectionHandler.prototype.isFileSelectionAvailable = function() {
    307   return !this.fileManager_.isOnDrive() ||
    308       !this.fileManager_.isDriveOffline() ||
    309       this.selection.allDriveFilesPresent;
    310 };
    311 
    312 /**
    313  * Sets the flag to force the preview panel hidden.
    314  * @param {boolean} hidden True to force hidden.
    315  */
    316 FileSelectionHandler.prototype.setPreviewPanelMustBeHidden = function(hidden) {
    317   this.previewPanelMustBeHidden_ = hidden;
    318   this.updatePreviewPanelVisibility_();
    319 };
    320 
    321 /**
    322  * Animates preview panel show/hide transitions.
    323  *
    324  * @private
    325  */
    326 FileSelectionHandler.prototype.updatePreviewPanelVisibility_ = function() {
    327   var panel = this.previewPanel_;
    328   var state = panel.getAttribute('visibility');
    329   var mustBeVisible =
    330        // If one or more files are selected, show the file info.
    331       (this.selection.totalCount > 0 ||
    332        // If the directory is not root dir, show the directory info.
    333        !PathUtil.isRootPath(this.fileManager_.getCurrentDirectory()) ||
    334        // On Open File dialog, the preview panel is always shown.
    335        this.fileManager_.dialogType == DialogType.SELECT_OPEN_FILE ||
    336        this.fileManager_.dialogType == DialogType.SELECT_OPEN_MULTI_FILE);
    337 
    338   var stopHidingAndShow = function() {
    339     clearTimeout(this.hidingTimeout_);
    340     this.hidingTimeout_ = 0;
    341     setVisibility('visible');
    342   }.bind(this);
    343 
    344   var startHiding = function() {
    345     setVisibility('hiding');
    346     this.hidingTimeout_ = setTimeout(function() {
    347         this.hidingTimeout_ = 0;
    348         setVisibility('hidden');
    349         cr.dispatchSimpleEvent(this, 'hide-preview-panel');
    350       }.bind(this), 250);
    351   }.bind(this);
    352 
    353   var show = function() {
    354     setVisibility('visible');
    355     this.previewThumbnails_.textContent = '';
    356     cr.dispatchSimpleEvent(this, 'show-preview-panel');
    357   }.bind(this);
    358 
    359   var setVisibility = function(visibility) {
    360     panel.setAttribute('visibility', visibility);
    361   };
    362 
    363   switch (state) {
    364     case 'visible':
    365       if (!mustBeVisible || this.previewPanelMustBeHidden_)
    366         startHiding();
    367       break;
    368 
    369     case 'hiding':
    370       if (mustBeVisible && !this.previewPanelMustBeHidden_)
    371         stopHidingAndShow();
    372       break;
    373 
    374     case 'hidden':
    375       if (mustBeVisible && !this.previewPanelMustBeHidden_)
    376         show();
    377   }
    378 };
    379 
    380 /**
    381  * @return {boolean} True if space reserverd for the preview panel.
    382  * @private
    383  */
    384 FileSelectionHandler.prototype.isPreviewPanelVisibile_ = function() {
    385   return this.previewPanel_.getAttribute('visibility') == 'visible';
    386 };
    387 
    388 /**
    389  * Update the selection summary in preview panel.
    390  *
    391  * @private
    392  */
    393 FileSelectionHandler.prototype.updatePreviewPanelText_ = function() {
    394   var selection = this.selection;
    395   if (selection.totalCount <= 1) {
    396     // Hides the preview text if zero or one file is selected. We shows a
    397     // breadcrumb list instead on the preview panel.
    398     this.hideCalculating_();
    399     this.previewText_.textContent = '';
    400     return;
    401   }
    402 
    403   var text = '';
    404   if (selection.totalCount == 1) {
    405     text = selection.entries[0].name;
    406   } else if (selection.directoryCount == 0) {
    407     text = strf('MANY_FILES_SELECTED', selection.fileCount);
    408   } else if (selection.fileCount == 0) {
    409     text = strf('MANY_DIRECTORIES_SELECTED', selection.directoryCount);
    410   } else {
    411     text = strf('MANY_ENTRIES_SELECTED', selection.totalCount);
    412   }
    413 
    414   if (selection.bytesKnown) {
    415     this.hideCalculating_();
    416     if (selection.showBytes) {
    417       var bytes = util.bytesToString(selection.bytes);
    418       text += ', ' + bytes;
    419     }
    420   } else {
    421     this.showCalculating_();
    422   }
    423 
    424   this.previewText_.textContent = text;
    425 };
    426 
    427 /**
    428  * Displays the 'calculating size' label.
    429  *
    430  * @private
    431  */
    432 FileSelectionHandler.prototype.showCalculating_ = function() {
    433   if (this.animationTimeout_) {
    434     clearTimeout(this.animationTimeout_);
    435     this.animationTimeout_ = null;
    436   }
    437 
    438   var dotCount = 0;
    439 
    440   var advance = function() {
    441     this.animationTimeout_ = setTimeout(advance, 1000);
    442 
    443     var s = this.calculatingSize_.textContent;
    444     s = s.replace(/(\.)+$/, '');
    445     for (var i = 0; i < dotCount; i++) {
    446       s += '.';
    447     }
    448     this.calculatingSize_.textContent = s;
    449 
    450     dotCount = (dotCount + 1) % 3;
    451   }.bind(this);
    452 
    453   var start = function() {
    454     this.calculatingSize_.hidden = false;
    455     advance();
    456   }.bind(this);
    457 
    458   this.animationTimeout_ = setTimeout(start, 500);
    459 };
    460 
    461 /**
    462  * Hides the 'calculating size' label.
    463  *
    464  * @private
    465  */
    466 FileSelectionHandler.prototype.hideCalculating_ = function() {
    467   if (this.animationTimeout_) {
    468     clearTimeout(this.animationTimeout_);
    469     this.animationTimeout_ = null;
    470   }
    471   this.calculatingSize_.hidden = true;
    472 };
    473 
    474 /**
    475  * Calculates async selection stats and updates secondary UI elements.
    476  *
    477  * @param {FileSelection} selection The selection object.
    478  */
    479 FileSelectionHandler.prototype.updateFileSelectionAsync = function(selection) {
    480   if (this.selection != selection) return;
    481 
    482   // Update the file tasks.
    483   if (this.fileManager_.dialogType == DialogType.FULL_PAGE &&
    484       selection.directoryCount == 0 && selection.fileCount > 0) {
    485     selection.createTasks(function() {
    486       if (this.selection != selection)
    487         return;
    488       selection.tasks.display(this.taskItems_);
    489       selection.tasks.updateMenuItem();
    490     }.bind(this));
    491   } else {
    492     this.taskItems_.hidden = true;
    493   }
    494 
    495   // Update preview panels.
    496   var wasVisible = this.isPreviewPanelVisibile_();
    497   var thumbnailEntries;
    498   if (selection.totalCount == 0) {
    499     thumbnailEntries = [
    500       this.fileManager_.getCurrentDirectoryEntry()
    501     ];
    502   } else {
    503     thumbnailEntries = selection.entries;
    504     if (selection.totalCount != 1) {
    505       selection.computeBytes(function() {
    506         if (this.selection != selection)
    507           return;
    508         this.updatePreviewPanelText_();
    509       }.bind(this));
    510     }
    511   }
    512   this.updatePreviewPanelVisibility_();
    513   this.updatePreviewPanelText_();
    514   this.showPreviewThumbnails_(thumbnailEntries);
    515 
    516   // Update breadcrums.
    517   var updateTarget = null;
    518   var path = this.fileManager_.getCurrentDirectory();
    519   if (selection.totalCount == 1) {
    520     // Shows the breadcrumb list when a file is selected.
    521     updateTarget = selection.entries[0].fullPath;
    522   } else if (selection.totalCount == 0 &&
    523              this.isPreviewPanelVisibile_()) {
    524     // Shows the breadcrumb list when no file is selected and the preview
    525     // panel is visible.
    526     updateTarget = path;
    527   }
    528   this.updatePreviewPanelBreadcrumbs_(updateTarget);
    529 
    530   // Scroll to item
    531   if (!wasVisible && this.selection.totalCount == 1) {
    532     var list = this.fileManager_.getCurrentList();
    533     list.scrollIndexIntoView(list.selectionModel.selectedIndex);
    534   }
    535 
    536   // Sync the commands availability.
    537   if (selection.totalCount != 0)
    538     this.fileManager_.updateCommands();
    539 
    540   // Update context menu.
    541   this.fileManager_.updateContextMenuActionItems(null, false);
    542 
    543   // Inform tests it's OK to click buttons now.
    544   if (selection.totalCount > 0) {
    545     chrome.test.sendMessage('selection-change-complete');
    546   }
    547 };
    548 
    549 /**
    550  * Renders preview thumbnails in preview panel.
    551  *
    552  * @param {Array.<FileEntry>} entries The entries of selected object.
    553  * @private
    554  */
    555 FileSelectionHandler.prototype.showPreviewThumbnails_ = function(entries) {
    556   var selection = this.selection;
    557   var thumbnails = [];
    558   var thumbnailCount = 0;
    559   var thumbnailLoaded = -1;
    560   var forcedShowTimeout = null;
    561   var thumbnailsHaveZoom = false;
    562   var self = this;
    563 
    564   var showThumbnails = function() {
    565     // have-zoom class may be updated twice: then timeout exceeds and then
    566     // then all images loaded.
    567     if (self.selection == selection) {
    568       if (thumbnailsHaveZoom) {
    569         self.previewThumbnails_.classList.add('has-zoom');
    570       } else {
    571         self.previewThumbnails_.classList.remove('has-zoom');
    572       }
    573     }
    574 
    575     if (forcedShowTimeout === null)
    576       return;
    577     clearTimeout(forcedShowTimeout);
    578     forcedShowTimeout = null;
    579 
    580     // FileSelection could change while images are loading.
    581     if (self.selection == selection) {
    582       self.previewThumbnails_.textContent = '';
    583       for (var i = 0; i < thumbnails.length; i++)
    584         self.previewThumbnails_.appendChild(thumbnails[i]);
    585     }
    586   };
    587 
    588   var onThumbnailLoaded = function() {
    589     thumbnailLoaded++;
    590     if (thumbnailLoaded == thumbnailCount)
    591       showThumbnails();
    592   };
    593 
    594   var thumbnailClickHandler = function() {
    595     if (selection.tasks)
    596       selection.tasks.executeDefault();
    597   };
    598 
    599   var doc = this.fileManager_.document_;
    600   for (var i = 0; i < entries.length; i++) {
    601     var entry = entries[i];
    602 
    603     if (thumbnailCount < FileSelectionHandler.MAX_PREVIEW_THUMBNAIL_COUNT) {
    604       var box = doc.createElement('div');
    605       box.className = 'thumbnail';
    606       if (thumbnailCount == 0) {
    607         var zoomed = doc.createElement('div');
    608         zoomed.hidden = true;
    609         thumbnails.push(zoomed);
    610         var onFirstThumbnailLoaded = function(img, transform) {
    611           if (img && self.decorateThumbnailZoom_(zoomed, img, transform)) {
    612             zoomed.hidden = false;
    613             thumbnailsHaveZoom = true;
    614           }
    615           onThumbnailLoaded();
    616         };
    617         var thumbnail = this.renderThumbnail_(entry, onFirstThumbnailLoaded);
    618         zoomed.addEventListener('click', thumbnailClickHandler);
    619       } else {
    620         var thumbnail = this.renderThumbnail_(entry, onThumbnailLoaded);
    621       }
    622       thumbnailCount++;
    623       box.appendChild(thumbnail);
    624       box.style.zIndex =
    625           FileSelectionHandler.MAX_PREVIEW_THUMBNAIL_COUNT + 1 - i;
    626       box.addEventListener('click', thumbnailClickHandler);
    627 
    628       thumbnails.push(box);
    629     }
    630   }
    631 
    632   forcedShowTimeout = setTimeout(showThumbnails,
    633       FileManager.THUMBNAIL_SHOW_DELAY);
    634   onThumbnailLoaded();
    635 };
    636 
    637 /**
    638  * Renders a thumbnail for the buttom panel.
    639  *
    640  * @param {Entry} entry Entry to render for.
    641  * @param {function} callback Called when image loaded.
    642  * @return {HTMLDivElement} Created element.
    643  * @private
    644  */
    645 FileSelectionHandler.prototype.renderThumbnail_ = function(entry, callback) {
    646   var thumbnail = this.fileManager_.document_.createElement('div');
    647   FileGrid.decorateThumbnailBox(thumbnail,
    648                                 entry,
    649                                 this.fileManager_.metadataCache_,
    650                                 ThumbnailLoader.FillMode.FILL,
    651                                 FileGrid.ThumbnailQuality.LOW,
    652                                 callback);
    653   return thumbnail;
    654 };
    655 
    656 /**
    657  * Updates the breadcrumbs in the preview panel.
    658  *
    659  * @param {?string} path Path to be shown in the breadcrumbs list
    660  * @private
    661  */
    662 FileSelectionHandler.prototype.updatePreviewPanelBreadcrumbs_ = function(path) {
    663   if (!path)
    664     this.searchBreadcrumbs_.hide();
    665   else
    666     this.searchBreadcrumbs_.show(PathUtil.getRootPath(path), path);
    667 };
    668 
    669 /**
    670  * Updates the search breadcrumbs. This method should not be used in the new ui.
    671  *
    672  * @private
    673  */
    674 FileSelectionHandler.prototype.updateSearchBreadcrumbs_ = function() {
    675   var selectedIndexes =
    676       this.fileManager_.getCurrentList().selectionModel.selectedIndexes;
    677   if (selectedIndexes.length !== 1 ||
    678       !this.fileManager_.directoryModel_.isSearching()) {
    679     this.searchBreadcrumbs_.hide();
    680     return;
    681   }
    682 
    683   var entry = this.fileManager_.getFileList().item(
    684       selectedIndexes[0]);
    685   this.searchBreadcrumbs_.show(
    686       PathUtil.getRootPath(entry.fullPath),
    687       entry.fullPath);
    688 };
    689 
    690 /**
    691  * Creates enlarged image for a bottom pannel thumbnail.
    692  * Image's assumed to be just loaded and not inserted into the DOM.
    693  *
    694  * @param {HTMLElement} largeImageBox DIV element to decorate.
    695  * @param {HTMLElement} img Loaded image.
    696  * @param {Object} transform Image transformation description.
    697  * @return {boolean} True if zoomed image is present.
    698  * @private
    699  */
    700 FileSelectionHandler.prototype.decorateThumbnailZoom_ = function(
    701     largeImageBox, img, transform) {
    702   var width = img.width;
    703   var height = img.height;
    704   var THUMBNAIL_SIZE = 35;
    705   if (width < THUMBNAIL_SIZE * 2 && height < THUMBNAIL_SIZE * 2)
    706     return false;
    707 
    708   var scale = Math.min(1,
    709       FileSelectionHandler.IMAGE_HOVER_PREVIEW_SIZE / Math.max(width, height));
    710 
    711   var imageWidth = Math.round(width * scale);
    712   var imageHeight = Math.round(height * scale);
    713 
    714   var largeImage = this.fileManager_.document_.createElement('img');
    715   if (scale < 0.3) {
    716     // Scaling large images kills animation. Downscale it in advance.
    717 
    718     // Canvas scales images with liner interpolation. Make a larger
    719     // image (but small enough to not kill animation) and let IMG
    720     // scale it smoothly.
    721     var INTERMEDIATE_SCALE = 3;
    722     var canvas = this.fileManager_.document_.createElement('canvas');
    723     canvas.width = imageWidth * INTERMEDIATE_SCALE;
    724     canvas.height = imageHeight * INTERMEDIATE_SCALE;
    725     var ctx = canvas.getContext('2d');
    726     ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
    727     // Using bigger than default compression reduces image size by
    728     // several times. Quality degradation compensated by greater resolution.
    729     largeImage.src = canvas.toDataURL('image/jpeg', 0.6);
    730   } else {
    731     largeImage.src = img.src;
    732   }
    733   largeImageBox.className = 'popup';
    734 
    735   var boxWidth = Math.max(THUMBNAIL_SIZE, imageWidth);
    736   var boxHeight = Math.max(THUMBNAIL_SIZE, imageHeight);
    737 
    738   if (transform && transform.rotate90 % 2 == 1) {
    739     var t = boxWidth;
    740     boxWidth = boxHeight;
    741     boxHeight = t;
    742   }
    743 
    744   var style = largeImageBox.style;
    745   style.width = boxWidth + 'px';
    746   style.height = boxHeight + 'px';
    747   style.top = (-boxHeight + THUMBNAIL_SIZE) + 'px';
    748 
    749   var style = largeImage.style;
    750   style.width = imageWidth + 'px';
    751   style.height = imageHeight + 'px';
    752   style.left = (boxWidth - imageWidth) / 2 + 'px';
    753   style.top = (boxHeight - imageHeight) / 2 + 'px';
    754   style.position = 'relative';
    755 
    756   util.applyTransform(largeImage, transform);
    757 
    758   largeImageBox.appendChild(largeImage);
    759   largeImageBox.style.zIndex = 1000;
    760   return true;
    761 };
    762