Home | History | Annotate | Download | only in js
      1 // Copyright (c) 2011 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 // WK Bug 55728 is fixed on the chrome 12 branch but not on the trunk.
      6 // TODO(rginda): Enable this everywhere once we have a trunk-worthy fix.
      7 const ENABLE_EXIF_READER = navigator.userAgent.match(/chrome\/12\.0/i);
      8 
      9 // Thumbnail view is painful without the exif reader.
     10 const ENABLE_THUMBNAIL_VIEW = ENABLE_EXIF_READER;
     11 
     12 var g_slideshow_data = null;
     13 
     14 /**
     15  * FileManager constructor.
     16  *
     17  * FileManager objects encapsulate the functionality of the file selector
     18  * dialogs, as well as the full screen file manager application (though the
     19  * latter is not yet implemented).
     20  *
     21  * @param {HTMLElement} dialogDom The DOM node containing the prototypical
     22  *     dialog UI.
     23  * @param {DOMFileSystem} filesystem The HTML5 filesystem object representing
     24  *     the root filesystem for the new FileManager.
     25  * @param {Object} params A map of parameter names to values controlling the
     26  *     appearance of the FileManager.  Names are:
     27  *     - type: A value from FileManager.DialogType defining what kind of
     28  *       dialog to present.  Defaults to FULL_PAGE.
     29  *     - title: The title for the dialog.  Defaults to a localized string based
     30  *       on the dialog type.
     31  *     - defaultPath: The default path for the dialog.  The default path should
     32  *       end with a trailing slash if it represents a directory.
     33  */
     34 function FileManager(dialogDom, rootEntries, params) {
     35   console.log('Init FileManager: ' + dialogDom);
     36 
     37   this.dialogDom_ = dialogDom;
     38   this.rootEntries_ = rootEntries;
     39   this.filesystem_ = rootEntries[0].filesystem;
     40   this.params_ = params || {};
     41 
     42   this.listType_ = null;
     43 
     44   this.exifCache_ = {};
     45 
     46   // True if we should filter out files that start with a dot.
     47   this.filterFiles_ = true;
     48 
     49   this.commands_ = {};
     50 
     51   this.document_ = dialogDom.ownerDocument;
     52   this.dialogType_ =
     53     this.params_.type || FileManager.DialogType.FULL_PAGE;
     54 
     55   this.defaultPath_ = this.params_.defaultPath || '/';
     56 
     57   // This is set to just the directory portion of defaultPath in initDialogType.
     58   this.defaultFolder_ = '/';
     59 
     60   this.showCheckboxes_ =
     61       (this.dialogType_ == FileManager.DialogType.FULL_PAGE ||
     62        this.dialogType_ == FileManager.DialogType.SELECT_OPEN_MULTI_FILE);
     63 
     64   // DirectoryEntry representing the current directory of the dialog.
     65   this.currentDirEntry_ = null;
     66 
     67   window.addEventListener('popstate', this.onPopState_.bind(this));
     68   this.addEventListener('directory-changed',
     69                         this.onDirectoryChanged_.bind(this));
     70   this.addEventListener('selection-summarized',
     71                         this.onSelectionSummarized_.bind(this));
     72 
     73   this.initCommands_();
     74   this.initDom_();
     75   this.initDialogType_();
     76 
     77   this.summarizeSelection_();
     78   this.updatePreview_();
     79   this.changeDirectory(this.defaultFolder_);
     80 
     81   chrome.fileBrowserPrivate.onDiskChanged.addListener(
     82       this.onDiskChanged_.bind(this));
     83 
     84   this.table_.list_.focus();
     85 
     86   if (ENABLE_EXIF_READER) {
     87     this.exifReader = new Worker('js/exif_reader.js');
     88     this.exifReader.onmessage = this.onExifReaderMessage_.bind(this);
     89     this.exifReader.postMessage({verb: 'init'});
     90   }
     91 }
     92 
     93 FileManager.prototype = {
     94   __proto__: cr.EventTarget.prototype
     95 };
     96 
     97 // Anonymous "namespace".
     98 (function() {
     99 
    100   // Private variables and helper functions.
    101 
    102   /**
    103    * Unicode codepoint for 'BLACK RIGHT-POINTING SMALL TRIANGLE'.
    104    */
    105   const RIGHT_TRIANGLE = '\u25b8';
    106 
    107   /**
    108    * The DirectoryEntry.fullPath value of the directory containing external
    109    * storage volumes.
    110    */
    111   const MEDIA_DIRECTORY = '/media';
    112 
    113   /**
    114    * Translated strings.
    115    */
    116   var localStrings;
    117 
    118   /**
    119    * Map of icon types to regular expressions.
    120    *
    121    * The first regexp to match the file name determines the icon type
    122    * assigned to dom elements for a file.  Order of evaluation is not
    123    * defined, so don't depend on it.
    124    */
    125   const iconTypes = {
    126     'audio': /\.(mp3|m4a|oga|ogg|wav)$/i,
    127     'html': /\.(html?)$/i,
    128     'image': /\.(bmp|gif|jpe?g|ico|png|webp)$/i,
    129     'pdf' : /\.(pdf)$/i,
    130     'text': /\.(pod|rst|txt|log)$/i,
    131     'video': /\.(mov|mp4|m4v|mpe?g4?|ogm|ogv|ogx|webm)$/i
    132   };
    133 
    134   const previewArt = {
    135     'audio': 'images/filetype_large_audio.png',
    136     'folder': 'images/filetype_large_folder.png',
    137     'unknown': 'images/filetype_large_generic.png',
    138     'video': 'images/filetype_large_video.png'
    139   };
    140 
    141   /**
    142    * Return a translated string.
    143    *
    144    * Wrapper function to make dealing with translated strings more concise.
    145    * Equivilant to localStrings.getString(id).
    146    *
    147    * @param {string} id The id of the string to return.
    148    * @return {string} The translated string.
    149    */
    150   function str(id) {
    151     return localStrings.getString(id);
    152   }
    153 
    154   /**
    155    * Return a translated string with arguments replaced.
    156    *
    157    * Wrapper function to make dealing with translated strings more concise.
    158    * Equivilant to localStrings.getStringF(id, ...).
    159    *
    160    * @param {string} id The id of the string to return.
    161    * @param {...string} The values to replace into the string.
    162    * @return {string} The translated string with replaced values.
    163    */
    164   function strf(id, var_args) {
    165     return localStrings.getStringF.apply(localStrings, arguments);
    166   }
    167 
    168   /**
    169    * Checks if |parent_path| is parent file path of |child_path|.
    170    *
    171    * @param {string} parent_path The parent path.
    172    * @param {string} child_path The child path.
    173    */
    174   function isParentPath(parent_path, child_path) {
    175     if (!parent_path || parent_path.length == 0 ||
    176         !child_path || child_path.length == 0)
    177       return false;
    178 
    179     if (parent_path[parent_path.length -1] != '/')
    180       parent_path += '/';
    181 
    182     if (child_path[child_path.length -1] != '/')
    183       child_path += '/';
    184 
    185     return child_path.indexOf(parent_path) == 0;
    186   }
    187 
    188   /**
    189    * Returns parent folder path of file path.
    190    *
    191    * @param {string} path The file path.
    192    */
    193   function getParentPath(path) {
    194     var parent = path.replace(/[\/]?[^\/]+[\/]?$/,'');
    195     if (parent.length == 0)
    196       parent = '/';
    197     return parent;
    198   }
    199 
    200   /**
    201    * Get the icon type for a given Entry.
    202    *
    203    * @param {Entry} entry An Entry subclass (FileEntry or DirectoryEntry).
    204    * @return {string} One of the keys from FileManager.iconTypes, or
    205    *     'unknown'.
    206    */
    207   function getIconType(entry) {
    208     if (entry.cachedIconType_)
    209       return entry.cachedIconType_;
    210 
    211     var rv = 'unknown';
    212 
    213     if (entry.isDirectory) {
    214       rv = 'folder';
    215     } else {
    216       for (var name in iconTypes) {
    217         var value = iconTypes[name];
    218 
    219         if (value instanceof RegExp) {
    220           if (value.test(entry.name))  {
    221             rv = name;
    222             break;
    223           }
    224         } else if (typeof value == 'function') {
    225           try {
    226             if (value(entry)) {
    227               rv = name;
    228               break;
    229             }
    230           } catch (ex) {
    231             console.error('Caught exception while evaluating iconType: ' +
    232                           name, ex);
    233           }
    234         } else {
    235           console.log('Unexpected value in iconTypes[' + name + ']: ' + value);
    236         }
    237       }
    238     }
    239 
    240     entry.cachedIconType_ = rv;
    241     return rv;
    242   }
    243 
    244   /**
    245    * Call an asynchronous method on dirEntry, batching multiple callers.
    246    *
    247    * This batches multiple callers into a single invocation, calling all
    248    * interested parties back when the async call completes.
    249    *
    250    * The Entry method to be invoked should take two callbacks as parameters
    251    * (one for success and one for failure), and it should invoke those
    252    * callbacks with a single parameter representing the result of the call.
    253    * Example methods are Entry.getMetadata() and FileEntry.file().
    254    *
    255    * Warning: Because this method caches the first result, subsequent changes
    256    * to the entry will not be visible to callers.
    257    *
    258    * Error results are never cached.
    259    *
    260    * @param {DirectoryEntry} dirEntry The DirectoryEntry to apply the method
    261    *     to.
    262    * @param {string} methodName The name of the method to dispatch.
    263    * @param {function(*)} successCallback The function to invoke if the method
    264    *     succeeds.  The result of the method will be the one parameter to this
    265    *     callback.
    266    * @param {function(*)} opt_errorCallback The function to invoke if the
    267    *     method fails.  The result of the method will be the one parameter to
    268    *     this callback.  If not provided, the default errorCallback will throw
    269    *     an exception.
    270    */
    271   function batchAsyncCall(entry, methodName, successCallback,
    272                           opt_errorCallback) {
    273     var resultCache = methodName + '_resultCache_';
    274 
    275     if (entry[resultCache]) {
    276       // The result cache for this method already exists.  Just invoke the
    277       // successCallback with the result of the previuos call.
    278       // Callback via a setTimeout so the sync/async semantics don't change
    279       // based on whether or not the value is cached.
    280       setTimeout(function() { successCallback(entry[resultCache]) }, 0);
    281       return;
    282     }
    283 
    284     if (!opt_errorCallback) {
    285       opt_errorCallback = util.ferr('Error calling ' + methodName + ' for: ' +
    286                                     entry.fullPath);
    287     }
    288 
    289     var observerList = methodName + '_observers_';
    290 
    291     if (entry[observerList]) {
    292       // The observer list already exists, indicating we have a pending call
    293       // out to this method.  Add this caller to the list of observers and
    294       // bail out.
    295       entry[observerList].push([successCallback, opt_errorCallback]);
    296       return;
    297     }
    298 
    299     entry[observerList] = [[successCallback, opt_errorCallback]];
    300 
    301     function onComplete(success, result) {
    302       if (success)
    303         entry[resultCache] = result;
    304 
    305       for (var i = 0; i < entry[observerList].length; i++) {
    306         entry[observerList][i][success ? 0 : 1](result);
    307       }
    308 
    309       delete entry[observerList];
    310     };
    311 
    312     entry[methodName](function(rv) { onComplete(true, rv) },
    313                       function(rv) { onComplete(false, rv) });
    314   }
    315 
    316   /**
    317    * Get the size of a file, caching the result.
    318    *
    319    * When this method completes, the fileEntry object will get a
    320    * 'cachedSize_' property (if it doesn't already have one) containing the
    321    * size of the file in bytes.
    322    *
    323    * @param {Entry} entry An HTML5 Entry object.
    324    * @param {function(Entry)} successCallback The function to invoke once the
    325    *     file size is known.
    326    */
    327   function cacheEntrySize(entry, successCallback) {
    328     if (entry.isDirectory) {
    329       // No size for a directory, -1 ensures it's sorted before 0 length files.
    330       entry.cachedSize_ = -1;
    331     }
    332 
    333     if ('cachedSize_' in entry) {
    334       if (successCallback) {
    335         // Callback via a setTimeout so the sync/async semantics don't change
    336         // based on whether or not the value is cached.
    337         setTimeout(function() { successCallback(entry) }, 0);
    338       }
    339       return;
    340     }
    341 
    342     batchAsyncCall(entry, 'file', function(file) {
    343       entry.cachedSize_ = file.size;
    344       if (successCallback)
    345         successCallback(entry);
    346     });
    347   }
    348 
    349   /**
    350    * Get the mtime of a file, caching the result.
    351    *
    352    * When this method completes, the fileEntry object will get a
    353    * 'cachedMtime_' property (if it doesn't already have one) containing the
    354    * last modified time of the file as a Date object.
    355    *
    356    * @param {Entry} entry An HTML5 Entry object.
    357    * @param {function(Entry)} successCallback The function to invoke once the
    358    *     mtime is known.
    359    */
    360   function cacheEntryDate(entry, successCallback) {
    361     if ('cachedMtime_' in entry) {
    362       if (successCallback) {
    363         // Callback via a setTimeout so the sync/async semantics don't change
    364         // based on whether or not the value is cached.
    365         setTimeout(function() { successCallback(entry) }, 0);
    366       }
    367       return;
    368     }
    369 
    370     if (entry.isFile) {
    371       batchAsyncCall(entry, 'file', function(file) {
    372         entry.cachedMtime_ = file.lastModifiedDate;
    373         if (successCallback)
    374           successCallback(entry);
    375       });
    376     } else {
    377       batchAsyncCall(entry, 'getMetadata', function(metadata) {
    378         entry.cachedMtime_ = metadata.modificationTime;
    379         if (successCallback)
    380           successCallback(entry);
    381       });
    382     }
    383   }
    384 
    385   /**
    386    * Get the icon type of a file, caching the result.
    387    *
    388    * When this method completes, the fileEntry object will get a
    389    * 'cachedIconType_' property (if it doesn't already have one) containing the
    390    * icon type of the file as a string.
    391    *
    392    * The successCallback is always invoked synchronously, since this does not
    393    * actually require an async call.  You should not depend on this, as it may
    394    * change if we were to start reading magic numbers (for example).
    395    *
    396    * @param {Entry} entry An HTML5 Entry object.
    397    * @param {function(Entry)} successCallback The function to invoke once the
    398    *     icon type is known.
    399    */
    400   function cacheEntryIconType(entry, successCallback) {
    401     getIconType(entry);
    402     if (successCallback)
    403       setTimeout(function() { successCallback(entry) }, 0);
    404   }
    405 
    406   // Public statics.
    407 
    408   /**
    409    * List of dialog types.
    410    *
    411    * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except
    412    * FULL_PAGE which is specific to this code.
    413    *
    414    * @enum {string}
    415    */
    416   FileManager.DialogType = {
    417     SELECT_FOLDER: 'folder',
    418     SELECT_SAVEAS_FILE: 'saveas-file',
    419     SELECT_OPEN_FILE: 'open-file',
    420     SELECT_OPEN_MULTI_FILE: 'open-multi-file',
    421     FULL_PAGE: 'full-page'
    422   };
    423 
    424   FileManager.ListType = {
    425     DETAIL: 'detail',
    426     THUMBNAIL: 'thumb'
    427   };
    428 
    429   /**
    430    * Load translated strings.
    431    */
    432   FileManager.initStrings = function(callback) {
    433     chrome.fileBrowserPrivate.getStrings(function(strings) {
    434       localStrings = new LocalStrings(strings);
    435       cr.initLocale(strings);
    436 
    437       if (callback)
    438         callback();
    439     });
    440   };
    441 
    442   // Instance methods.
    443 
    444   /**
    445    * One-time initialization of commands.
    446    */
    447   FileManager.prototype.initCommands_ = function() {
    448     var commands = this.dialogDom_.querySelectorAll('command');
    449     for (var i = 0; i < commands.length; i++) {
    450       var command = commands[i];
    451       cr.ui.Command.decorate(command);
    452       this.commands_[command.id] = command;
    453     }
    454 
    455     this.fileContextMenu_ = this.dialogDom_.querySelector('.file-context-menu');
    456     cr.ui.Menu.decorate(this.fileContextMenu_);
    457 
    458     this.document_.addEventListener(
    459         'canExecute', this.onRenameCanExecute_.bind(this));
    460     this.document_.addEventListener(
    461         'canExecute', this.onDeleteCanExecute_.bind(this));
    462 
    463     this.document_.addEventListener('command', this.onCommand_.bind(this));
    464   }
    465 
    466   /**
    467    * One-time initialization of various DOM nodes.
    468    */
    469   FileManager.prototype.initDom_ = function() {
    470     // Cache nodes we'll be manipulating.
    471     this.previewImage_ = this.dialogDom_.querySelector('.preview-img');
    472     this.previewFilename_ = this.dialogDom_.querySelector('.preview-filename');
    473     this.previewSummary_ = this.dialogDom_.querySelector('.preview-summary');
    474     this.filenameInput_ = this.dialogDom_.querySelector('.filename-input');
    475     this.taskButtons_ = this.dialogDom_.querySelector('.task-buttons');
    476     this.okButton_ = this.dialogDom_.querySelector('.ok');
    477     this.cancelButton_ = this.dialogDom_.querySelector('.cancel');
    478     this.newFolderButton_ = this.dialogDom_.querySelector('.new-folder');
    479 
    480     this.renameInput_ = this.document_.createElement('input');
    481     this.renameInput_.className = 'rename';
    482 
    483     this.renameInput_.addEventListener(
    484         'keydown', this.onRenameInputKeyDown_.bind(this));
    485     this.renameInput_.addEventListener(
    486         'blur', this.onRenameInputBlur_.bind(this));
    487 
    488     this.filenameInput_.addEventListener(
    489         'keyup', this.onFilenameInputKeyUp_.bind(this));
    490     this.filenameInput_.addEventListener(
    491         'focus', this.onFilenameInputFocus_.bind(this));
    492 
    493     this.dialogDom_.addEventListener('keydown', this.onKeyDown_.bind(this));
    494     this.okButton_.addEventListener('click', this.onOk_.bind(this));
    495     this.cancelButton_.addEventListener('click', this.onCancel_.bind(this));
    496 
    497     this.dialogDom_.querySelector('button.new-folder').addEventListener(
    498         'click', this.onNewFolderButtonClick_.bind(this));
    499 
    500     if (ENABLE_THUMBNAIL_VIEW) {
    501       this.dialogDom_.querySelector('button.detail-view').addEventListener(
    502           'click', this.onDetailViewButtonClick_.bind(this));
    503       this.dialogDom_.querySelector('button.thumbnail-view').addEventListener(
    504           'click', this.onThumbnailViewButtonClick_.bind(this));
    505     } else {
    506       this.dialogDom_.querySelector(
    507           'button.detail-view').style.display = 'none';
    508       this.dialogDom_.querySelector(
    509           'button.thumbnail-view').style.display = 'none';
    510     }
    511 
    512     this.dialogDom_.ownerDocument.defaultView.addEventListener(
    513         'resize', this.onResize_.bind(this));
    514 
    515     var ary = this.dialogDom_.querySelectorAll('[visibleif]');
    516     for (var i = 0; i < ary.length; i++) {
    517       var expr = ary[i].getAttribute('visibleif');
    518       if (!eval(expr))
    519         ary[i].style.display = 'none';
    520     }
    521 
    522     // Populate the static localized strings.
    523     i18nTemplate.process(this.document_, localStrings.templateData);
    524 
    525     // Always sharing the data model between the detail/thumb views confuses
    526     // them.  Instead we maintain this bogus data model, and hook it up to the
    527     // view that is not in use.
    528     this.emptyDataModel_ = new cr.ui.table.TableDataModel([]);
    529 
    530     this.dataModel_ = new cr.ui.table.TableDataModel([]);
    531     this.dataModel_.sort('name');
    532     this.dataModel_.addEventListener('sorted',
    533                                 this.onDataModelSorted_.bind(this));
    534     this.dataModel_.prepareSort = this.prepareSort_.bind(this);
    535 
    536     if (this.dialogType_ == FileManager.DialogType.SELECT_OPEN_FILE ||
    537         this.dialogType_ == FileManager.DialogType.SELECT_OPEN_FOLDER ||
    538         this.dialogType_ == FileManager.DialogType.SELECT_SAVEAS_FILE) {
    539       this.selectionModelClass_ = cr.ui.table.TableSingleSelectionModel;
    540     } else {
    541       this.selectionModelClass_ = cr.ui.table.TableSelectionModel;
    542     }
    543 
    544     this.initTable_();
    545     this.initGrid_();
    546 
    547     this.setListType(FileManager.ListType.DETAIL);
    548 
    549     this.onResize_();
    550     this.dialogDom_.style.opacity = '1';
    551   };
    552 
    553   /**
    554    * Force the canExecute events to be dispatched.
    555    */
    556   FileManager.prototype.updateCommands_ = function() {
    557     this.commands_['rename'].canExecuteChange();
    558     this.commands_['delete'].canExecuteChange();
    559   };
    560 
    561   /**
    562    * Invoked to decide whether the "rename" command can be executed.
    563    */
    564   FileManager.prototype.onRenameCanExecute_ = function(event) {
    565     event.canExecute =
    566         (// Full page mode.
    567          this.dialogType_ == FileManager.DialogType.FULL_PAGE &&
    568          // Rename not in progress.
    569          !this.renameInput_.currentEntry &&
    570          // Not in root directory.
    571          this.currentDirEntry_.fullPath != '/' &&
    572          // Not in media directory.
    573          this.currentDirEntry_.fullPath != MEDIA_DIRECTORY &&
    574          // Only one file selected.
    575          this.selection.totalCount == 1);
    576   };
    577 
    578   /**
    579    * Invoked to decide whether the "delete" command can be executed.
    580    */
    581   FileManager.prototype.onDeleteCanExecute_ = function(event) {
    582     event.canExecute =
    583         (// Full page mode.
    584          this.dialogType_ == FileManager.DialogType.FULL_PAGE &&
    585          // Rename not in progress.
    586          !this.renameInput_.currentEntry &&
    587          // Not in root directory.
    588          this.currentDirEntry_.fullPath != '/' &&
    589          // Not in media directory.
    590          this.currentDirEntry_.fullPath != MEDIA_DIRECTORY);
    591   };
    592 
    593   FileManager.prototype.setListType = function(type) {
    594     if (type && type == this.listType_)
    595       return;
    596 
    597     if (type == FileManager.ListType.DETAIL) {
    598       this.table_.dataModel = this.dataModel_;
    599       this.table_.style.display = '';
    600       this.grid_.style.display = 'none';
    601       this.grid_.dataModel = this.emptyDataModel_;
    602       this.currentList_ = this.table_;
    603       this.dialogDom_.querySelector('button.detail-view').disabled = true;
    604       this.dialogDom_.querySelector('button.thumbnail-view').disabled = false;
    605     } else if (type == FileManager.ListType.THUMBNAIL) {
    606       this.grid_.dataModel = this.dataModel_;
    607       this.grid_.style.display = '';
    608       this.table_.style.display = 'none';
    609       this.table_.dataModel = this.emptyDataModel_;
    610       this.currentList_ = this.grid_;
    611       this.dialogDom_.querySelector('button.thumbnail-view').disabled = true;
    612       this.dialogDom_.querySelector('button.detail-view').disabled = false;
    613     } else {
    614       throw new Error('Unknown list type: ' + type);
    615     }
    616 
    617     this.listType_ = type;
    618     this.onResize_();
    619   };
    620 
    621   /**
    622    * Initialize the file thumbnail grid.
    623    */
    624   FileManager.prototype.initGrid_ = function() {
    625     this.grid_ = this.dialogDom_.querySelector('.thumbnail-grid');
    626     cr.ui.Grid.decorate(this.grid_);
    627 
    628     var self = this;
    629     this.grid_.itemConstructor = function(entry) {
    630       return self.renderThumbnail_(entry);
    631     };
    632 
    633     this.grid_.selectionModel = new this.selectionModelClass_();
    634 
    635     this.grid_.addEventListener(
    636         'dblclick', this.onDetailDoubleClick_.bind(this));
    637     this.grid_.selectionModel.addEventListener(
    638         'change', this.onSelectionChanged_.bind(this));
    639 
    640     if (this.dialogType_ == FileManager.DialogType.FULL_PAGE) {
    641       cr.ui.contextMenuHandler.addContextMenuProperty(this.grid_);
    642       this.grid_.contextMenu = this.fileContextMenu_;
    643     }
    644 
    645     this.grid_.addEventListener('mousedown',
    646                                 this.onGridMouseDown_.bind(this));
    647   };
    648 
    649   /**
    650    * Initialize the file list table.
    651    */
    652   FileManager.prototype.initTable_ = function() {
    653     var checkWidth = this.showCheckboxes_ ? 5 : 0;
    654 
    655     var columns = [
    656         new cr.ui.table.TableColumn('cachedIconType_', '',
    657                                     5.4 + checkWidth),
    658         new cr.ui.table.TableColumn('name', str('NAME_COLUMN_LABEL'),
    659                                     64 - checkWidth),
    660         new cr.ui.table.TableColumn('cachedSize_',
    661                                     str('SIZE_COLUMN_LABEL'), 15.5),
    662         new cr.ui.table.TableColumn('cachedMtime_',
    663                                     str('DATE_COLUMN_LABEL'), 21)
    664     ];
    665 
    666     columns[0].renderFunction = this.renderIconType_.bind(this);
    667     columns[1].renderFunction = this.renderName_.bind(this);
    668     columns[2].renderFunction = this.renderSize_.bind(this);
    669     columns[3].renderFunction = this.renderDate_.bind(this);
    670 
    671     this.table_ = this.dialogDom_.querySelector('.detail-table');
    672     cr.ui.Table.decorate(this.table_);
    673 
    674     this.table_.selectionModel = new this.selectionModelClass_();
    675     this.table_.columnModel = new cr.ui.table.TableColumnModel(columns);
    676 
    677     this.table_.addEventListener(
    678         'dblclick', this.onDetailDoubleClick_.bind(this));
    679     this.table_.selectionModel.addEventListener(
    680         'change', this.onSelectionChanged_.bind(this));
    681 
    682     if (this.dialogType_ == FileManager.DialogType.FULL_PAGE) {
    683       cr.ui.contextMenuHandler.addContextMenuProperty(this.table_);
    684       this.table_.contextMenu = this.fileContextMenu_;
    685     }
    686 
    687     this.table_.addEventListener('mousedown',
    688                                  this.onTableMouseDown_.bind(this));
    689   };
    690 
    691   /**
    692    * Respond to a command being executed.
    693    */
    694   FileManager.prototype.onCommand_ = function(event) {
    695     switch (event.command.id) {
    696       case 'rename':
    697         var leadIndex = this.currentList_.selectionModel.leadIndex;
    698         var li = this.currentList_.getListItemByIndex(leadIndex);
    699         var label = li.querySelector('.filename-label');
    700         if (!label) {
    701           console.warn('Unable to find label for rename of index: ' +
    702                        leadIndex);
    703           return;
    704         }
    705 
    706         this.initiateRename_(label);
    707         break;
    708 
    709       case 'delete':
    710         this.deleteEntries(this.selection.entries);
    711         break;
    712     }
    713   };
    714 
    715   /**
    716    * Respond to the back button.
    717    */
    718   FileManager.prototype.onPopState_ = function(event) {
    719     this.changeDirectory(event.state, false);
    720   };
    721 
    722   /**
    723    * Resize details and thumb views to fit the new window size.
    724    */
    725   FileManager.prototype.onResize_ = function() {
    726     this.table_.style.height = this.grid_.style.height =
    727       this.grid_.parentNode.clientHeight + 'px';
    728     this.table_.style.width = this.grid_.style.width =
    729       this.grid_.parentNode.clientWidth + 'px';
    730 
    731     this.table_.list_.style.width = this.table_.parentNode.clientWidth + 'px';
    732     this.table_.list_.style.height = (this.table_.clientHeight - 1 -
    733                                       this.table_.header_.clientHeight) + 'px';
    734 
    735     if (this.listType_ == FileManager.ListType.THUMBNAIL) {
    736       var self = this;
    737       setTimeout(function () {
    738           self.grid_.columns = 0;
    739           self.grid_.redraw();
    740       }, 0);
    741     } else {
    742       this.currentList_.redraw();
    743     }
    744   };
    745 
    746   /**
    747    * Tweak the UI to become a particular kind of dialog, as determined by the
    748    * dialog type parameter passed to the constructor.
    749    */
    750   FileManager.prototype.initDialogType_ = function() {
    751     var defaultTitle;
    752     var okLabel = str('OPEN_LABEL');
    753 
    754     // Split the dirname from the basename.
    755     var ary = this.defaultPath_.match(/^(.*?)(?:\/([^\/]+))?$/);
    756     var defaultFolder;
    757     var defaultTarget;
    758 
    759     if (!ary) {
    760       console.warn('Unable to split defaultPath: ' + defaultPath);
    761       ary = [];
    762     }
    763 
    764     switch (this.dialogType_) {
    765       case FileManager.DialogType.SELECT_FOLDER:
    766         defaultTitle = str('SELECT_FOLDER_TITLE');
    767         defaultFolder = ary[1] || '/';
    768         defaultTarget = ary[2] || '';
    769         break;
    770 
    771       case FileManager.DialogType.SELECT_OPEN_FILE:
    772         defaultTitle = str('SELECT_OPEN_FILE_TITLE');
    773         defaultFolder = ary[1] || '/';
    774         defaultTarget = '';
    775 
    776         if (ary[2]) {
    777           console.warn('Open should NOT have provided a default ' +
    778                        'filename: ' + ary[2]);
    779         }
    780         break;
    781 
    782       case FileManager.DialogType.SELECT_OPEN_MULTI_FILE:
    783         defaultTitle = str('SELECT_OPEN_MULTI_FILE_TITLE');
    784         defaultFolder = ary[1] || '/';
    785         defaultTarget = '';
    786 
    787         if (ary[2]) {
    788           console.warn('Multi-open should NOT have provided a default ' +
    789                        'filename: ' + ary[2]);
    790         }
    791         break;
    792 
    793       case FileManager.DialogType.SELECT_SAVEAS_FILE:
    794         defaultTitle = str('SELECT_SAVEAS_FILE_TITLE');
    795         okLabel = str('SAVE_LABEL');
    796 
    797         defaultFolder = ary[1] || '/';
    798         defaultTarget = ary[2] || '';
    799         if (!defaultTarget)
    800           console.warn('Save-as should have provided a default filename.');
    801         break;
    802 
    803       case FileManager.DialogType.FULL_PAGE:
    804         defaultFolder = ary[1] || '/';
    805         defaultTarget = ary[2] || '';
    806         break;
    807 
    808       default:
    809         throw new Error('Unknown dialog type: ' + this.dialogType_);
    810     }
    811 
    812     this.okButton_.textContent = okLabel;
    813 
    814     dialogTitle = this.params_.title || defaultTitle;
    815     this.dialogDom_.querySelector('.dialog-title').textContent = dialogTitle;
    816 
    817     ary = defaultFolder.match(/^\/home\/[^\/]+\/user\/Downloads(\/.*)?$/);
    818     if (ary) {
    819         // Chrome will probably suggest the full path to Downloads, but
    820         // we're working with 'virtual paths', so we have to translate.
    821         // TODO(rginda): Maybe chrome should have suggested the correct place
    822         // to begin with, but that would mean it would have to treat the
    823         // file manager dialogs differently than the native ones.
    824         defaultFolder = '/Downloads' + (ary[1] || '');
    825       }
    826 
    827     this.defaultFolder_ = defaultFolder;
    828     this.filenameInput_.value = defaultTarget;
    829   };
    830 
    831   /**
    832    * Cache necessary data before a sort happens.
    833    *
    834    * This is called by the table code before a sort happens, so that we can
    835    * go fetch data for the sort field that we may not have yet.
    836    */
    837   FileManager.prototype.prepareSort_ = function(field, callback) {
    838     var cacheFunction;
    839 
    840     if (field == 'cachedMtime_') {
    841       cacheFunction = cacheEntryDate;
    842     } else if (field == 'cachedSize_') {
    843       cacheFunction = cacheEntrySize;
    844     } else if (field == 'cachedIconType_') {
    845       cacheFunction = cacheEntryIconType;
    846     } else {
    847       callback();
    848       return;
    849     }
    850 
    851     function checkCount() {
    852       if (uncachedCount == 0) {
    853         // Callback via a setTimeout so the sync/async semantics don't change
    854         // based on whether or not the value is cached.
    855         setTimeout(callback, 0);
    856       }
    857     }
    858 
    859     var dataModel = this.dataModel_;
    860     var uncachedCount = dataModel.length;
    861 
    862     for (var i = uncachedCount - 1; i >= 0 ; i--) {
    863       var entry = dataModel.item(i);
    864       if (field in entry) {
    865         uncachedCount--;
    866       } else {
    867         cacheFunction(entry, function() {
    868           uncachedCount--;
    869           checkCount();
    870         });
    871       }
    872     }
    873 
    874     checkCount();
    875   }
    876 
    877   /**
    878    * Render (and wire up) a checkbox to be used in either a detail or a
    879    * thumbnail list item.
    880    */
    881   FileManager.prototype.renderCheckbox_ = function(entry) {
    882     var input = this.document_.createElement('input');
    883     input.setAttribute('type', 'checkbox');
    884     input.className = 'file-checkbox';
    885     input.addEventListener('mousedown',
    886                            this.onCheckboxMouseDownUp_.bind(this));
    887     input.addEventListener('mouseup',
    888                            this.onCheckboxMouseDownUp_.bind(this));
    889     input.addEventListener('click',
    890                            this.onCheckboxClick_.bind(this));
    891 
    892     if (this.selection && this.selection.entries.indexOf(entry) != -1) {
    893       // Our DOM nodes get discarded as soon as we're scrolled out of view,
    894       // so we have to make sure the check state is correct when we're brought
    895       // back to life.
    896       input.checked = true;
    897     }
    898 
    899     return input;
    900   }
    901 
    902   FileManager.prototype.renderThumbnail_ = function(entry) {
    903     var li = this.document_.createElement('li');
    904     li.className = 'thumbnail-item';
    905 
    906     if (this.showCheckboxes_)
    907       li.appendChild(this.renderCheckbox_(entry));
    908 
    909     var div = this.document_.createElement('div');
    910     div.className = 'img-container';
    911     li.appendChild(div);
    912 
    913     var img = this.document_.createElement('img');
    914     this.getThumbnailURL(entry, function(type, url) { img.src = url });
    915     div.appendChild(img);
    916 
    917     div = this.document_.createElement('div');
    918     div.className = 'filename-label';
    919     var labelText = entry.name;
    920     if (this.currentDirEntry_.name == '')
    921       labelText = this.getLabelForRootPath_(labelText);
    922 
    923     div.textContent = labelText;
    924     div.entry = entry;
    925 
    926     li.appendChild(div);
    927 
    928     cr.defineProperty(li, 'lead', cr.PropertyKind.BOOL_ATTR);
    929     cr.defineProperty(li, 'selected', cr.PropertyKind.BOOL_ATTR);
    930     return li;
    931   }
    932 
    933   /**
    934    * Render the type column of the detail table.
    935    *
    936    * Invoked by cr.ui.Table when a file needs to be rendered.
    937    *
    938    * @param {Entry} entry The Entry object to render.
    939    * @param {string} columnId The id of the column to be rendered.
    940    * @param {cr.ui.Table} table The table doing the rendering.
    941    */
    942   FileManager.prototype.renderIconType_ = function(entry, columnId, table) {
    943     var div = this.document_.createElement('div');
    944     div.className = 'detail-icon-container';
    945 
    946     if (this.showCheckboxes_)
    947       div.appendChild(this.renderCheckbox_(entry));
    948 
    949     var icon = this.document_.createElement('div');
    950     icon.className = 'detail-icon';
    951     entry.cachedIconType_ = getIconType(entry);
    952     icon.setAttribute('iconType', entry.cachedIconType_);
    953     div.appendChild(icon);
    954 
    955     return div;
    956   };
    957 
    958   FileManager.prototype.getLabelForRootPath_ = function(path) {
    959     // This hack lets us localize the top level directories.
    960     if (path == 'Downloads')
    961       return str('DOWNLOADS_DIRECTORY_LABEL');
    962 
    963     if (path == 'media')
    964       return str('MEDIA_DIRECTORY_LABEL');
    965 
    966     return path || str('ROOT_DIRECTORY_LABEL');
    967   };
    968 
    969   /**
    970    * Render the Name column of the detail table.
    971    *
    972    * Invoked by cr.ui.Table when a file needs to be rendered.
    973    *
    974    * @param {Entry} entry The Entry object to render.
    975    * @param {string} columnId The id of the column to be rendered.
    976    * @param {cr.ui.Table} table The table doing the rendering.
    977    */
    978   FileManager.prototype.renderName_ = function(entry, columnId, table) {
    979     var label = this.document_.createElement('div');
    980     label.entry = entry;
    981     label.className = 'filename-label';
    982     if (this.currentDirEntry_.name == '') {
    983       label.textContent = this.getLabelForRootPath_(entry.name);
    984     } else {
    985       label.textContent = entry.name;
    986     }
    987 
    988     return label;
    989   };
    990 
    991   /**
    992    * Render the Size column of the detail table.
    993    *
    994    * @param {Entry} entry The Entry object to render.
    995    * @param {string} columnId The id of the column to be rendered.
    996    * @param {cr.ui.Table} table The table doing the rendering.
    997    */
    998   FileManager.prototype.renderSize_ = function(entry, columnId, table) {
    999     var div = this.document_.createElement('div');
   1000     div.className = 'detail-size';
   1001 
   1002     div.textContent = '...';
   1003     cacheEntrySize(entry, function(entry) {
   1004       if (entry.cachedSize_ == -1) {
   1005         div.textContent = '';
   1006       } else {
   1007         div.textContent = cr.locale.bytesToSi(entry.cachedSize_);
   1008       }
   1009     });
   1010 
   1011     return div;
   1012   };
   1013 
   1014   /**
   1015    * Render the Date column of the detail table.
   1016    *
   1017    * @param {Entry} entry The Entry object to render.
   1018    * @param {string} columnId The id of the column to be rendered.
   1019    * @param {cr.ui.Table} table The table doing the rendering.
   1020    */
   1021   FileManager.prototype.renderDate_ = function(entry, columnId, table) {
   1022     var div = this.document_.createElement('div');
   1023     div.className = 'detail-date';
   1024 
   1025     div.textContent = '...';
   1026 
   1027     var self = this;
   1028     cacheEntryDate(entry, function(entry) {
   1029       if (self.currentDirEntry_.fullPath == MEDIA_DIRECTORY &&
   1030           entry.cachedMtime_.getTime() == 0) {
   1031         // Mount points for FAT volumes have this time associated with them.
   1032         // We'd rather display nothing than this bogus date.
   1033         div.textContent = '---';
   1034       } else {
   1035         div.textContent = cr.locale.formatDate(entry.cachedMtime_,
   1036                                                str('LOCALE_FMT_DATE_SHORT'));
   1037       }
   1038     });
   1039 
   1040     return div;
   1041   };
   1042 
   1043   /**
   1044    * Compute summary information about the current selection.
   1045    *
   1046    * This method dispatches the 'selection-summarized' event when it completes.
   1047    * Depending on how many of the selected files already have known sizes, the
   1048    * dispatch may happen immediately, or after a number of async calls complete.
   1049    */
   1050   FileManager.prototype.summarizeSelection_ = function() {
   1051     var selection = this.selection = {
   1052       entries: [],
   1053       urls: [],
   1054       leadEntry: null,
   1055       totalCount: 0,
   1056       fileCount: 0,
   1057       directoryCount: 0,
   1058       bytes: 0,
   1059       iconType: null,
   1060       indexes: this.currentList_.selectionModel.selectedIndexes
   1061     };
   1062 
   1063     this.previewSummary_.textContent = str('COMPUTING_SELECTION');
   1064     this.taskButtons_.innerHTML = '';
   1065 
   1066     if (!selection.indexes.length) {
   1067       cr.dispatchSimpleEvent(this, 'selection-summarized');
   1068       return;
   1069     }
   1070 
   1071     var fileCount = 0;
   1072     var byteCount = 0;
   1073     var pendingFiles = [];
   1074 
   1075     for (var i = 0; i < selection.indexes.length; i++) {
   1076       var entry = this.dataModel_.item(selection.indexes[i]);
   1077 
   1078       selection.entries.push(entry);
   1079       selection.urls.push(entry.toURL());
   1080 
   1081       if (selection.iconType == null) {
   1082         selection.iconType = getIconType(entry);
   1083       } else if (selection.iconType != 'unknown') {
   1084         var iconType = getIconType(entry);
   1085         if (selection.iconType != iconType)
   1086           selection.iconType = 'unknown';
   1087       }
   1088 
   1089       selection.totalCount++;
   1090 
   1091       if (entry.isFile) {
   1092         if (!('cachedSize_' in entry)) {
   1093           // Any file that hasn't been rendered may be missing its cachedSize_
   1094           // property.  For example, visit a large file list, and press ctrl-a
   1095           // to select all.  In this case, we need to asynchronously get the
   1096           // sizes for these files before telling the world the selection has
   1097           // been summarized.  See the 'computeNextFile' logic below.
   1098           pendingFiles.push(entry);
   1099           continue;
   1100         } else {
   1101           selection.bytes += entry.cachedSize_;
   1102         }
   1103         selection.fileCount += 1;
   1104       } else {
   1105         selection.directoryCount += 1;
   1106       }
   1107     }
   1108 
   1109     var leadIndex = this.currentList_.selectionModel.leadIndex;
   1110     if (leadIndex > -1) {
   1111       selection.leadEntry = this.dataModel_.item(leadIndex);
   1112     } else {
   1113       selection.leadEntry = selection.entries[0];
   1114     }
   1115 
   1116     var self = this;
   1117 
   1118     function cacheNextFile(fileEntry) {
   1119       if (fileEntry) {
   1120         // We're careful to modify the 'selection', rather than 'self.selection'
   1121         // here, just in case the selection has changed since this summarization
   1122         // began.
   1123         selection.bytes += fileEntry.cachedSize_;
   1124       }
   1125 
   1126       if (pendingFiles.length) {
   1127         cacheEntrySize(pendingFiles.pop(), cacheNextFile);
   1128       } else {
   1129         self.dispatchEvent(new cr.Event('selection-summarized'));
   1130       }
   1131     };
   1132 
   1133     if (this.dialogType_ == FileManager.DialogType.FULL_PAGE) {
   1134       chrome.fileBrowserPrivate.getFileTasks(selection.urls,
   1135                                              this.onTasksFound_.bind(this));
   1136     }
   1137 
   1138     cacheNextFile();
   1139   };
   1140 
   1141   FileManager.prototype.onExifGiven_ = function(fileURL, metadata) {
   1142     var observers = this.exifCache_[fileURL];
   1143     if (!observers || !(observers instanceof Array)) {
   1144       console.error('Missing or invalid exif observers: ' + fileURL + ': ' +
   1145                     observers);
   1146       return;
   1147     }
   1148 
   1149     for (var i = 0; i < observers.length; i++) {
   1150       observers[i](metadata);
   1151     }
   1152 
   1153     this.exifCache_[fileURL] = metadata;
   1154   };
   1155 
   1156   FileManager.prototype.onExifError_ = function(fileURL, step, error) {
   1157     console.warn('Exif error: ' + fileURL + ': ' + step + ': ' + error);
   1158     this.onExifGiven_(fileURL, {});
   1159   };
   1160 
   1161   FileManager.prototype.onExifReaderMessage_ = function(event) {
   1162     var data = event.data;
   1163     var self = this;
   1164 
   1165     function fwd(methodName, args) { self[methodName].apply(self, args) };
   1166 
   1167     switch (data.verb) {
   1168       case 'log':
   1169         console.log.apply(console, ['exif:'].concat(data.arguments));
   1170         break;
   1171 
   1172       case 'give-exif':
   1173         fwd('onExifGiven_', data.arguments);
   1174         break;
   1175 
   1176       case 'give-exif-error':
   1177         fwd('onExifError_', data.arguments);
   1178         break;
   1179 
   1180       default:
   1181         console.log('Unknown message from exif reader: ' + data.verb, data);
   1182         break;
   1183     }
   1184   };
   1185 
   1186   FileManager.prototype.onTasksFound_ = function(tasksList) {
   1187     this.taskButtons_.innerHTML = '';
   1188     for (var i = 0; i < tasksList.length; i++) {
   1189       var task = tasksList[i];
   1190 
   1191       // Tweak images, titles of internal tasks.
   1192       var task_parts = task.taskId.split('|');
   1193       if (task_parts[0] == this.getExtensionId_()) {
   1194         if (task_parts[1] == 'preview') {
   1195           // TODO(serya): This hack needed until task.iconUrl get working
   1196           //              (see GetFileTasksFileBrowserFunction::RunImpl).
   1197           task.iconUrl =
   1198               chrome.extension.getURL('images/icon_preview_16x16.png');
   1199           task.title = str('PREVIEW_IMAGE');
   1200         } else if (task_parts[1] == 'play') {
   1201           task.iconUrl =
   1202               chrome.extension.getURL('images/icon_play_16x16.png');
   1203           task.title = str('PLAY_MEDIA').replace("&", "");
   1204         } else if (task_parts[1] == 'enqueue') {
   1205           task.iconUrl =
   1206               chrome.extension.getURL('images/icon_add_to_queue_16x16.png');
   1207           task.title = str('ENQUEUE');
   1208         }
   1209       }
   1210 
   1211       var button = this.document_.createElement('button');
   1212       button.addEventListener('click', this.onTaskButtonClicked_.bind(this));
   1213       button.className = 'task-button';
   1214       button.task = task;
   1215 
   1216       var img = this.document_.createElement('img');
   1217       img.src = task.iconUrl;
   1218 
   1219       button.appendChild(img);
   1220       button.appendChild(this.document_.createTextNode(task.title));
   1221 
   1222       this.taskButtons_.appendChild(button);
   1223     }
   1224   };
   1225 
   1226   FileManager.prototype.getExtensionId_ = function() {
   1227     return chrome.extension.getURL('').split('/')[2];
   1228   };
   1229 
   1230   FileManager.prototype.onTaskButtonClicked_ = function(event) {
   1231     // Check internal tasks first.
   1232     var task_parts = event.srcElement.task.taskId.split('|');
   1233     if (task_parts[0] == this.getExtensionId_()) {
   1234       if (task_parts[1] == 'preview') {
   1235         g_slideshow_data = this.selection.urls;
   1236         chrome.tabs.create({url: "slideshow.html"});
   1237       } else if (task_parts[1] == 'play') {
   1238         chrome.fileBrowserPrivate.viewFiles(this.selection.urls,
   1239             event.srcElement.task.taskId);
   1240       } else if (task_parts[1] == 'enqueue') {
   1241         chrome.fileBrowserPrivate.viewFiles(this.selection.urls,
   1242             event.srcElement.task.taskId);
   1243       }
   1244       return;
   1245     }
   1246 
   1247     chrome.fileBrowserPrivate.executeTask(event.srcElement.task.taskId,
   1248                                           this.selection.urls);
   1249   }
   1250 
   1251   /**
   1252    * Update the breadcrumb display to reflect the current directory.
   1253    */
   1254   FileManager.prototype.updateBreadcrumbs_ = function() {
   1255     var bc = this.dialogDom_.querySelector('.breadcrumbs');
   1256     bc.innerHTML = '';
   1257 
   1258     var fullPath = this.currentDirEntry_.fullPath.replace(/\/$/, '');
   1259     var pathNames = fullPath.split('/');
   1260     var path = '';
   1261 
   1262     for (var i = 0; i < pathNames.length; i++) {
   1263       var pathName = pathNames[i];
   1264       path += pathName + '/';
   1265 
   1266       var div = this.document_.createElement('div');
   1267       div.className = 'breadcrumb-path';
   1268       if (i <= 1) {
   1269         // i == 0: root directory itself, i == 1: the files it contains.
   1270         div.textContent = this.getLabelForRootPath_(pathName);
   1271       } else {
   1272         div.textContent = pathName;
   1273       }
   1274 
   1275       div.path = path;
   1276       div.addEventListener('click', this.onBreadcrumbClick_.bind(this));
   1277 
   1278       bc.appendChild(div);
   1279 
   1280       if (i == pathNames.length - 1) {
   1281         div.classList.add('breadcrumb-last');
   1282       } else {
   1283         var spacer = this.document_.createElement('div');
   1284         spacer.className = 'breadcrumb-spacer';
   1285         spacer.textContent = RIGHT_TRIANGLE;
   1286         bc.appendChild(spacer);
   1287       }
   1288     }
   1289   };
   1290 
   1291   /**
   1292    * Update the preview panel to display a given entry.
   1293    *
   1294    * The selection summary line is handled by the onSelectionSummarized handler
   1295    * rather than this function, because summarization may not complete quickly.
   1296    */
   1297   FileManager.prototype.updatePreview_ = function() {
   1298     // Clear the preview image first, in case the thumbnail takes long to load.
   1299     this.previewImage_.src = '';
   1300     // The transparent-background class is used to display the checkerboard
   1301     // background for image thumbnails.  We don't want to display it for
   1302     // non-thumbnail preview images.
   1303     this.previewImage_.classList.remove('transparent-background');
   1304     // The multiple-selected class indicates that more than one entry is
   1305     // selcted.
   1306     this.previewImage_.classList.remove('multiple-selected');
   1307 
   1308     if (!this.selection.totalCount) {
   1309       this.previewFilename_.textContent = '';
   1310       return;
   1311     }
   1312 
   1313     var previewName = this.selection.leadEntry.name;
   1314     if (this.currentDirEntry_.name == '')
   1315       previewName = this.getLabelForRootPath_(previewName);
   1316 
   1317     this.previewFilename_.textContent = previewName;
   1318 
   1319     var iconType = getIconType(this.selection.leadEntry);
   1320     if (iconType == 'image') {
   1321       if (fileManager.selection.totalCount > 1)
   1322         this.previewImage_.classList.add('multiple-selected');
   1323     }
   1324 
   1325     var self = this;
   1326     var leadEntry = this.selection.leadEntry;
   1327 
   1328     this.getThumbnailURL(this.selection.leadEntry, function(iconType, url) {
   1329       if (self.selection.leadEntry != leadEntry) {
   1330         // Selection has changed since we asked, nevermind.
   1331         return;
   1332       }
   1333 
   1334       if (url) {
   1335         self.previewImage_.src = url;
   1336         if (iconType == 'image')
   1337           self.previewImage_.classList.add('transparent-background');
   1338       } else {
   1339         self.previewImage_.src = previewArt['unknown'];
   1340       }
   1341     });
   1342   };
   1343 
   1344   FileManager.prototype.cacheExifMetadata_ = function(entry, callback) {
   1345     var url = entry.toURL();
   1346     var cacheValue = this.exifCache_[url];
   1347 
   1348     if (!cacheValue) {
   1349       // This is the first time anyone's asked, go get it.
   1350       this.exifCache_[url] = [callback];
   1351       this.exifReader.postMessage({verb: 'get-exif',
   1352                                    arguments: [entry.toURL()]});
   1353       return;
   1354     }
   1355 
   1356     if (cacheValue instanceof Array) {
   1357       // Something is already pending, add to the list of observers.
   1358       cacheValue.push(callback);
   1359       return;
   1360     }
   1361 
   1362     if (cacheValue instanceof Object) {
   1363       // We already know the answer, let the caller know in a fresh call stack.
   1364       setTimeout(function() { callback(cacheValue) });
   1365       return;
   1366     }
   1367 
   1368     console.error('Unexpected exif cache value:' + cacheValue);
   1369   };
   1370 
   1371   FileManager.prototype.getThumbnailURL = function(entry, callback) {
   1372     if (!entry)
   1373       return;
   1374 
   1375     var iconType = getIconType(entry);
   1376     if (iconType != 'image') {
   1377       // Not an image, display a canned clip-art graphic.
   1378       if (!(iconType in previewArt))
   1379         iconType = 'unknown';
   1380 
   1381       setTimeout(function() { callback(iconType, previewArt[iconType]) });
   1382       return;
   1383     }
   1384 
   1385     if (ENABLE_EXIF_READER) {
   1386       if (entry.name.match(/\.jpe?g$/i)) {
   1387         // File is a jpg image, fetch the exif thumbnail.
   1388         this.cacheExifMetadata_(entry, function(metadata) {
   1389           callback(iconType, metadata.thumbnailURL || entry.toURL());
   1390         });
   1391         return;
   1392       }
   1393     }
   1394 
   1395     // File is some other kind of image, just return the url to the whole
   1396     // thing.
   1397     setTimeout(function() { callback(iconType, entry.toURL()) });
   1398   };
   1399 
   1400   /**
   1401    * Change the current directory.
   1402    *
   1403    * Dispatches the 'directory-changed' event when the directory is successfully
   1404    * changed.
   1405    *
   1406    * @param {string} path The absolute path to the new directory.
   1407    * @param {bool} opt_saveHistory Save this in the history stack (defaults
   1408    *     to true).
   1409    */
   1410   FileManager.prototype.changeDirectory = function(path, opt_saveHistory) {
   1411     var self = this;
   1412 
   1413     if (arguments.length == 1) {
   1414       opt_saveHistory = true;
   1415     } else {
   1416       opt_saveHistory = !!opt_saveHistory;
   1417     }
   1418 
   1419     function onPathFound(dirEntry) {
   1420       if (self.currentDirEntry_ &&
   1421           self.currentDirEntry_.fullPath == dirEntry.fullPath) {
   1422         // Directory didn't actually change.
   1423         return;
   1424       }
   1425 
   1426       var e = new cr.Event('directory-changed');
   1427       e.previousDirEntry = self.currentDirEntry_;
   1428       e.newDirEntry = dirEntry;
   1429       e.saveHistory = opt_saveHistory;
   1430       self.currentDirEntry_ = dirEntry;
   1431       self.dispatchEvent(e);
   1432     };
   1433 
   1434     if (path == '/')
   1435       return onPathFound(this.filesystem_.root);
   1436 
   1437     this.filesystem_.root.getDirectory(
   1438         path, {create: false}, onPathFound,
   1439         function(err) {
   1440           console.error('Error changing directory to: ' + path + ', ' + err);
   1441           if (!self.currentDirEntry_) {
   1442             // If we've never successfully changed to a directory, force them
   1443             // to the root.
   1444             self.changeDirectory('/');
   1445           }
   1446         });
   1447   };
   1448 
   1449   FileManager.prototype.deleteEntries = function(entries) {
   1450     if (!window.confirm(str('CONFIRM_DELETE')))
   1451       return;
   1452 
   1453     var count = entries.length;
   1454 
   1455     var self = this;
   1456     function onDelete() {
   1457       if (--count == 0)
   1458          self.rescanDirectory_();
   1459     }
   1460 
   1461     for (var i = 0; i < entries.length; i++) {
   1462       var entry = entries[i];
   1463       if (entry.isFile) {
   1464         entry.remove(
   1465             onDelete,
   1466             util.flog('Error deleting file: ' + entry.fullPath, onDelete));
   1467       } else {
   1468         entry.removeRecursively(
   1469             onDelete,
   1470             util.flog('Error deleting folder: ' + entry.fullPath, onDelete));
   1471       }
   1472     }
   1473   };
   1474 
   1475   /**
   1476    * Invoked by the table dataModel after a sort completes.
   1477    *
   1478    * We use this hook to make sure selected files stay visible after a sort.
   1479    */
   1480   FileManager.prototype.onDataModelSorted_ = function() {
   1481     var i = this.currentList_.selectionModel.leadIndex;
   1482     this.currentList_.scrollIntoView(i);
   1483   }
   1484 
   1485   /**
   1486    * Update the selection summary UI when the selection summarization completes.
   1487    */
   1488   FileManager.prototype.onSelectionSummarized_ = function() {
   1489     if (this.selection.totalCount == 0) {
   1490       this.previewSummary_.textContent = str('NOTHING_SELECTED');
   1491 
   1492     } else if (this.selection.totalCount == 1) {
   1493       this.previewSummary_.textContent =
   1494         strf('ONE_FILE_SELECTED', cr.locale.bytesToSi(this.selection.bytes));
   1495 
   1496     } else {
   1497       this.previewSummary_.textContent =
   1498         strf('MANY_FILES_SELECTED', this.selection.totalCount,
   1499              cr.locale.bytesToSi(this.selection.bytes));
   1500     }
   1501   };
   1502 
   1503   /**
   1504    * Handle a click event on a breadcrumb element.
   1505    *
   1506    * @param {Event} event The click event.
   1507    */
   1508   FileManager.prototype.onBreadcrumbClick_ = function(event) {
   1509     this.changeDirectory(event.srcElement.path);
   1510   };
   1511 
   1512   FileManager.prototype.onCheckboxMouseDownUp_ = function(event) {
   1513     // If exactly one file is selected and its checkbox is *not* clicked,
   1514     // then this should be treated as a "normal" click (ie. the previous
   1515     // selection should be cleared).
   1516     if (this.selection.totalCount == 1 && this.selection.entries[0].isFile) {
   1517       var selectedIndex = this.selection.indexes[0];
   1518       var listItem = this.currentList_.getListItemByIndex(selectedIndex);
   1519       var checkbox = listItem.querySelector('input[type="checkbox"]');
   1520       if (!checkbox.checked)
   1521         return;
   1522     }
   1523 
   1524     // Otherwise, treat clicking on a checkbox the same as a ctrl-click.
   1525     // The default properties of event.ctrlKey make it read-only, but
   1526     // don't prevent deletion, so we delete first, then set it true.
   1527     delete event.ctrlKey;
   1528     event.ctrlKey = true;
   1529   };
   1530 
   1531   FileManager.prototype.onCheckboxClick_ = function(event) {
   1532     if (event.shiftKey) {
   1533       // Something about the timing of shift-clicks causes the checkbox
   1534       // to get selected and then very quickly unselected.  It appears that
   1535       // we forcibly select the checkbox as part of onSelectionChanged, and
   1536       // then the default action of this click event fires and toggles the
   1537       // checkbox back off.
   1538       //
   1539       // Since we're going to force checkboxes into the correct state for any
   1540       // multi-selection, we can prevent this shift click from toggling the
   1541       // checkbox and avoid the trouble.
   1542       event.preventDefault();
   1543     }
   1544   };
   1545 
   1546   /**
   1547    * Update the UI when the selection model changes.
   1548    *
   1549    * @param {cr.Event} event The change event.
   1550    */
   1551   FileManager.prototype.onSelectionChanged_ = function(event) {
   1552     var selectable;
   1553 
   1554     this.summarizeSelection_();
   1555     this.updateOkButton_();
   1556     this.updatePreview_();
   1557 
   1558     var self = this;
   1559     setTimeout(function() { self.onSelectionChangeComplete_(event) }, 0);
   1560   };
   1561 
   1562   FileManager.prototype.onSelectionChangeComplete_ = function(event) {
   1563     if (!this.showCheckboxes_)
   1564       return;
   1565 
   1566     for (var i = 0; i < event.changes.length; i++) {
   1567       // Turn off any checkboxes for items that are no longer selected.
   1568       var selectedIndex = event.changes[i].index;
   1569       var listItem = this.currentList_.getListItemByIndex(selectedIndex);
   1570       if (!listItem) {
   1571         // When changing directories, we get notified about list items
   1572         // that are no longer there.
   1573         continue;
   1574       }
   1575 
   1576       if (!event.changes[i].selected) {
   1577         var checkbox = listItem.querySelector('input[type="checkbox"]');
   1578         checkbox.checked = false;
   1579       }
   1580     }
   1581 
   1582     if (this.selection.fileCount > 1) {
   1583       // If more than one file is selected, make sure all checkboxes are lit
   1584       // up.
   1585       for (var i = 0; i < this.selection.entries.length; i++) {
   1586         if (!this.selection.entries[i].isFile)
   1587           continue;
   1588 
   1589         var selectedIndex = this.selection.indexes[i];
   1590         var listItem = this.currentList_.getListItemByIndex(selectedIndex);
   1591         if (listItem)
   1592           listItem.querySelector('input[type="checkbox"]').checked = true;
   1593       }
   1594     }
   1595   };
   1596 
   1597   FileManager.prototype.updateOkButton_ = function(event) {
   1598     if (this.dialogType_ == FileManager.DialogType.SELECT_FOLDER) {
   1599       selectable = this.selection.directoryCount == 1 &&
   1600           this.selection.fileCount == 0;
   1601     } else if (this.dialogType_ == FileManager.DialogType.SELECT_OPEN_FILE) {
   1602       selectable = (this.selection.directoryCount == 0 &&
   1603                     this.selection.fileCount == 1);
   1604     } else if (this.dialogType_ ==
   1605                FileManager.DialogType.SELECT_OPEN_MULTI_FILE) {
   1606       selectable = (this.selection.directoryCount == 0 &&
   1607                     this.selection.fileCount >= 1);
   1608     } else if (this.dialogType_ == FileManager.DialogType.SELECT_SAVEAS_FILE) {
   1609       if (this.selection.leadEntry && this.selection.leadEntry.isFile)
   1610         this.filenameInput_.value = this.selection.leadEntry.name;
   1611 
   1612       if (this.currentDirEntry_.fullPath == '/' ||
   1613           this.currentDirEntry_.fullPath == MEDIA_DIRECTORY) {
   1614         // Nothing can be saved in to the root or media/ directories.
   1615         selectable = false;
   1616       } else {
   1617         selectable = !!this.filenameInput_.value;
   1618       }
   1619     } else if (this.dialogType_ == FileManager.DialogType.FULL_PAGE) {
   1620       // No "select" buttons on the full page UI.
   1621       selectable = true;
   1622     } else {
   1623       throw new Error('Unknown dialog type');
   1624     }
   1625 
   1626     this.okButton_.disabled = !selectable;
   1627   };
   1628 
   1629   /**
   1630    * Handle a double-click event on an entry in the detail list.
   1631    *
   1632    * @param {Event} event The click event.
   1633    */
   1634   FileManager.prototype.onDetailDoubleClick_ = function(event) {
   1635     if (this.renameInput_.currentEntry) {
   1636       // Don't pay attention to double clicks during a rename.
   1637       return;
   1638     }
   1639 
   1640     var i = this.currentList_.selectionModel.leadIndex;
   1641     var entry = this.dataModel_.item(i);
   1642 
   1643     if (entry.isDirectory)
   1644       return this.changeDirectory(entry.fullPath);
   1645 
   1646     if (!this.okButton_.disabled)
   1647       this.onOk_();
   1648 
   1649   };
   1650 
   1651   /**
   1652    * Update the UI when the current directory changes.
   1653    *
   1654    * @param {cr.Event} event The directory-changed event.
   1655    */
   1656   FileManager.prototype.onDirectoryChanged_ = function(event) {
   1657     if (event.saveHistory) {
   1658       history.pushState(this.currentDirEntry_.fullPath,
   1659                         this.currentDirEntry_.fullPath,
   1660                         location.href);
   1661     }
   1662 
   1663     this.updateOkButton_();
   1664     // New folder should never be enabled in the root or media/ directories.
   1665     this.newFolderButton_.disabled =
   1666         (this.currentDirEntry_.fullPath == '/' ||
   1667          this.currentDirEntry_.fullPath == MEDIA_DIRECTORY);
   1668 
   1669     this.document_.title = this.currentDirEntry_.fullPath;
   1670     this.rescanDirectory_();
   1671   };
   1672 
   1673   /**
   1674    * Update the UI when a disk is mounted or unmounted.
   1675    *
   1676    * @param {string} path The path that has been mounted or unmounted.
   1677    */
   1678   FileManager.prototype.onDiskChanged_ = function(event) {
   1679     if (event.eventType == 'added') {
   1680       this.changeDirectory(event.volumeInfo.mountPath);
   1681     } else if (event.eventType == 'removed') {
   1682       if (this.currentDirEntry_ &&
   1683           isParentPath(event.volumeInfo.mountPath,
   1684                        this.currentDirEntry_.fullPath)) {
   1685         this.changeDirectory(getParentPath(event.volumeInfo.mountPath));
   1686       }
   1687     }
   1688   };
   1689 
   1690   /**
   1691    * Rescan the current directory, refreshing the list.
   1692    *
   1693    * @param {function()} opt_callback Optional function to invoke when the
   1694    *     rescan is complete.
   1695    */
   1696   FileManager.prototype.rescanDirectory_ = function(opt_callback) {
   1697     var self = this;
   1698     var reader;
   1699 
   1700     function onReadSome(entries) {
   1701       if (entries.length == 0) {
   1702         if (self.dataModel_.sortStatus.field != 'name')
   1703           self.dataModel_.updateIndex(0);
   1704 
   1705         if (opt_callback)
   1706           opt_callback();
   1707         return;
   1708       }
   1709 
   1710       // Splice takes the to-be-spliced-in array as individual parameters,
   1711       // rather than as an array, so we need to perform some acrobatics...
   1712       var spliceArgs = [].slice.call(entries);
   1713 
   1714       // Hide files that start with a dot ('.').
   1715       // TODO(rginda): User should be able to override this.  Support for other
   1716       // commonly hidden patterns might be nice too.
   1717       if (self.filterFiles_) {
   1718         spliceArgs = spliceArgs.filter(function(e) {
   1719             return e.name.substr(0, 1) != '.';
   1720           });
   1721       }
   1722 
   1723       spliceArgs.unshift(0, 0);  // index, deleteCount
   1724       self.dataModel_.splice.apply(self.dataModel_, spliceArgs);
   1725 
   1726       // Keep reading until entries.length is 0.
   1727       reader.readEntries(onReadSome);
   1728     };
   1729 
   1730     this.lastLabelClick_ = null;
   1731 
   1732     // Clear the table first.
   1733     this.dataModel_.splice(0, this.dataModel_.length);
   1734 
   1735     this.updateBreadcrumbs_();
   1736 
   1737     if (this.currentDirEntry_.fullPath != '/') {
   1738       // If not the root directory, just read the contents.
   1739       reader = this.currentDirEntry_.createReader();
   1740       reader.readEntries(onReadSome);
   1741       return;
   1742     }
   1743 
   1744     // Otherwise, use the provided list of root subdirectories, since the
   1745     // real local filesystem root directory (the one we use outside the
   1746     // harness) can't be enumerated yet.
   1747     var spliceArgs = [].slice.call(this.rootEntries_);
   1748     spliceArgs.unshift(0, 0);  // index, deleteCount
   1749     self.dataModel_.splice.apply(self.dataModel_, spliceArgs);
   1750     self.dataModel_.updateIndex(0);
   1751 
   1752     if (opt_callback)
   1753       opt_callback();
   1754   };
   1755 
   1756   FileManager.prototype.findListItem_ = function(event) {
   1757     var node = event.srcElement;
   1758     while (node) {
   1759       if (node.tagName == 'LI')
   1760         break;
   1761       node = node.parentNode;
   1762     }
   1763 
   1764     return node;
   1765   };
   1766 
   1767   FileManager.prototype.onGridMouseDown_ = function(event) {
   1768     this.updateCommands_();
   1769 
   1770     if (this.allowRenameClick_(event, event.srcElement.parentNode)) {
   1771       event.preventDefault();
   1772       this.initiateRename_(event.srcElement);
   1773     }
   1774 
   1775     if (event.button != 1)
   1776       return;
   1777 
   1778     var li = this.findListItem_(event);
   1779     if (!li)
   1780       return;
   1781   };
   1782 
   1783   FileManager.prototype.onTableMouseDown_ = function(event) {
   1784     this.updateCommands_();
   1785 
   1786     if (this.allowRenameClick_(event,
   1787                                event.srcElement.parentNode.parentNode)) {
   1788       event.preventDefault();
   1789       this.initiateRename_(event.srcElement);
   1790     }
   1791 
   1792     if (event.button != 1)
   1793       return;
   1794 
   1795     var li = this.findListItem_(event);
   1796     if (!li) {
   1797       console.log('li not found', event);
   1798       return;
   1799     }
   1800   };
   1801 
   1802   /**
   1803    * Determine whether or not a click should initiate a rename.
   1804    *
   1805    * Renames can happen on mouse click if the user clicks on a label twice,
   1806    * at least a half second apart.
   1807    */
   1808   FileManager.prototype.allowRenameClick_ = function(event, row) {
   1809     if (this.dialogType_ != FileManager.DialogType.FULL_PAGE ||
   1810         this.currentDirEntry_.name == '') {
   1811       // Renaming only enabled for full-page mode, outside of the root
   1812       // directory.
   1813       return false;
   1814     }
   1815 
   1816     // Rename already in progress.
   1817     if (this.renameInput_.currentEntry)
   1818       return false;
   1819 
   1820     // Didn't click on the label.
   1821     if (event.srcElement.className != 'filename-label')
   1822       return false;
   1823 
   1824     // Wrong button or using a keyboard modifier.
   1825     if (event.button != 0 || event.shiftKey || event.metaKey || event.altKey) {
   1826       this.lastLabelClick_ = null;
   1827       return false;
   1828     }
   1829 
   1830     var now = new Date();
   1831 
   1832     this.lastLabelClick_ = this.lastLabelClick_ || now;
   1833     var delay = now - this.lastLabelClick_;
   1834     if (!row.selected || delay < 500)
   1835       return false;
   1836 
   1837     this.lastLabelClick_ = now;
   1838     return true;
   1839   };
   1840 
   1841   FileManager.prototype.initiateRename_= function(label) {
   1842     var input = this.renameInput_;
   1843 
   1844     window.label = label;
   1845 
   1846     input.value = label.textContent;
   1847     input.style.top = label.offsetTop + 'px';
   1848     input.style.left = label.offsetLeft + 'px';
   1849     input.style.width = label.clientWidth + 'px';
   1850     label.parentNode.appendChild(input);
   1851     input.focus();
   1852     var selectionEnd = input.value.lastIndexOf('.');
   1853     if (selectionEnd == -1) {
   1854       input.select();
   1855     } else {
   1856       input.selectionStart = 0;
   1857       input.selectionEnd = selectionEnd;
   1858     }
   1859 
   1860     // This has to be set late in the process so we don't handle spurious
   1861     // blur events.
   1862     input.currentEntry = label.entry;
   1863   };
   1864 
   1865   FileManager.prototype.onRenameInputKeyDown_ = function(event) {
   1866     if (!this.renameInput_.currentEntry)
   1867       return;
   1868 
   1869     switch (event.keyCode) {
   1870       case 27:  // Escape
   1871         this.cancelRename_();
   1872         event.preventDefault();
   1873         break;
   1874 
   1875       case 13:  // Enter
   1876         this.commitRename_();
   1877         event.preventDefault();
   1878         break;
   1879     }
   1880   };
   1881 
   1882   FileManager.prototype.onRenameInputBlur_ = function(event) {
   1883     if (this.renameInput_.currentEntry)
   1884       this.cancelRename_();
   1885   };
   1886 
   1887   FileManager.prototype.commitRename_ = function() {
   1888     var entry = this.renameInput_.currentEntry;
   1889     var newName = this.renameInput_.value;
   1890 
   1891     this.renameInput_.currentEntry = null;
   1892     this.lastLabelClick_ = null;
   1893 
   1894     if (this.renameInput_.parentNode)
   1895       this.renameInput_.parentNode.removeChild(this.renameInput_);
   1896 
   1897     var self = this;
   1898     function onSuccess() {
   1899       self.rescanDirectory_(function () {
   1900         for (var i = 0; i < self.dataModel_.length; i++) {
   1901           if (self.dataModel_.item(i).name == newName) {
   1902             self.currentList_.selectionModel.selectedIndex = i;
   1903             self.currentList_.scrollIndexIntoView(i);
   1904             self.currentList_.focus();
   1905             return;
   1906           }
   1907         }
   1908       });
   1909     }
   1910 
   1911     function onError(err) {
   1912       window.alert(strf('ERROR_RENAMING', entry.name,
   1913                         util.getFileErrorMnemonic(err.code)));
   1914     }
   1915 
   1916     entry.moveTo(this.currentDirEntry_, newName, onSuccess, onError);
   1917   };
   1918 
   1919   FileManager.prototype.cancelRename_ = function(event) {
   1920     this.renameInput_.currentEntry = null;
   1921     this.lastLabelClick_ = null;
   1922 
   1923     if (this.renameInput_.parentNode)
   1924       this.renameInput_.parentNode.removeChild(this.renameInput_);
   1925   };
   1926 
   1927   FileManager.prototype.onFilenameInputKeyUp_ = function(event) {
   1928     this.okButton_.disabled = this.filenameInput_.value.length == 0;
   1929 
   1930     if (event.keyCode == 13 /* Enter */ && !this.okButton_.disabled)
   1931       this.onOk_();
   1932   };
   1933 
   1934   FileManager.prototype.onFilenameInputFocus_ = function(event) {
   1935     var input = this.filenameInput_;
   1936 
   1937     // On focus we want to select everything but the extension, but
   1938     // Chrome will select-all after the focus event completes.  We
   1939     // schedule a timeout to alter the focus after that happens.
   1940     setTimeout(function() {
   1941         var selectionEnd = input.value.lastIndexOf('.');
   1942         if (selectionEnd == -1) {
   1943           input.select();
   1944         } else {
   1945           input.selectionStart = 0;
   1946           input.selectionEnd = selectionEnd;
   1947         }
   1948     }, 0);
   1949   };
   1950 
   1951   FileManager.prototype.onNewFolderButtonClick_ = function(event) {
   1952     var name = '';
   1953 
   1954     while (1) {
   1955       name = window.prompt(str('NEW_FOLDER_PROMPT'), name);
   1956       if (!name)
   1957         return;
   1958 
   1959       if (name.indexOf('/') == -1)
   1960         break;
   1961 
   1962       alert(strf('ERROR_INVALID_FOLDER_CHARACTER', '/'));
   1963     }
   1964 
   1965     var self = this;
   1966 
   1967     function onSuccess(dirEntry) {
   1968       self.rescanDirectory_(function () {
   1969         for (var i = 0; i < self.dataModel_.length; i++) {
   1970           if (self.dataModel_.item(i).name == dirEntry.name) {
   1971             self.currentList_.selectionModel.selectedIndex = i;
   1972             self.currentList_.scrollIndexIntoView(i);
   1973             self.currentList_.focus();
   1974             return;
   1975           }
   1976         }
   1977       });
   1978     }
   1979 
   1980     function onError(err) {
   1981       window.alert(strf('ERROR_CREATING_FOLDER', name,
   1982                         util.getFileErrorMnemonic(err.code)));
   1983     }
   1984 
   1985     this.currentDirEntry_.getDirectory(name, {create: true, exclusive: true},
   1986                                        onSuccess, onError);
   1987   };
   1988 
   1989   FileManager.prototype.onDetailViewButtonClick_ = function(event) {
   1990     this.setListType(FileManager.ListType.DETAIL);
   1991   };
   1992 
   1993   FileManager.prototype.onThumbnailViewButtonClick_ = function(event) {
   1994     this.setListType(FileManager.ListType.THUMBNAIL);
   1995   };
   1996 
   1997   FileManager.prototype.onKeyDown_ = function(event) {
   1998     if (event.srcElement.tagName == 'INPUT')
   1999       return;
   2000 
   2001     switch (event.keyCode) {
   2002       case 8:  // Backspace => Up one directory.
   2003         event.preventDefault();
   2004         var path = this.currentDirEntry_.fullPath;
   2005         if (path && path != '/') {
   2006           var path = path.replace(/\/[^\/]+$/, '');
   2007           this.changeDirectory(path || '/');
   2008         }
   2009         break;
   2010 
   2011       case 13:  // Enter => Change directory or complete dialog.
   2012         if (this.selection.totalCount == 1 &&
   2013             this.selection.leadEntry.isDirectory &&
   2014             this.dialogType_ != FileManager.SELECT_FOLDER) {
   2015           this.changeDirectory(this.selection.leadEntry.fullPath);
   2016         } else if (!this.okButton_.disabled) {
   2017           this.onOk_();
   2018         }
   2019         break;
   2020 
   2021       case 32:  // Ctrl-Space => New Folder.
   2022         if (this.newFolderButton_.style.display != 'none' && event.ctrlKey) {
   2023           event.preventDefault();
   2024           this.onNewFolderButtonClick_();
   2025         }
   2026         break;
   2027 
   2028       case 190:  // Ctrl-. => Toggle filter files.
   2029         if (event.ctrlKey) {
   2030           this.filterFiles_ = !this.filterFiles_;
   2031           this.rescanDirectory_();
   2032         }
   2033         break;
   2034 
   2035       case 46:  // Delete.
   2036         if (this.dialogType_ == FileManager.DialogType.FULL_PAGE &&
   2037             this.selection.totalCount > 0) {
   2038           event.preventDefault();
   2039           this.deleteEntries(this.selection.entries);
   2040         }
   2041         break;
   2042     }
   2043   };
   2044 
   2045   /**
   2046    * Handle a click of the cancel button.  Closes the window.
   2047    *
   2048    * @param {Event} event The click event.
   2049    */
   2050   FileManager.prototype.onCancel_ = function(event) {
   2051     chrome.fileBrowserPrivate.cancelDialog();
   2052   };
   2053 
   2054   /**
   2055    * Handle a click of the ok button.
   2056    *
   2057    * The ok button has different UI labels depending on the type of dialog, but
   2058    * in code it's always referred to as 'ok'.
   2059    *
   2060    * @param {Event} event The click event.
   2061    */
   2062   FileManager.prototype.onOk_ = function(event) {
   2063     var currentDirUrl = this.currentDirEntry_.toURL();
   2064 
   2065     if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/')
   2066       currentDirUrl += '/';
   2067 
   2068     if (this.dialogType_ == FileManager.DialogType.SELECT_SAVEAS_FILE) {
   2069       // Save-as doesn't require a valid selection from the list, since
   2070       // we're going to take the filename from the text input.
   2071       var filename = this.filenameInput_.value;
   2072       if (!filename)
   2073         throw new Error('Missing filename!');
   2074 
   2075       chrome.fileBrowserPrivate.selectFile(currentDirUrl + encodeURI(filename),
   2076                                            0);
   2077       // Window closed by above call.
   2078       return;
   2079     }
   2080 
   2081     var ary = [];
   2082     var selectedIndexes = this.currentList_.selectionModel.selectedIndexes;
   2083 
   2084     // All other dialog types require at least one selected list item.
   2085     // The logic to control whether or not the ok button is enabled should
   2086     // prevent us from ever getting here, but we sanity check to be sure.
   2087     if (!selectedIndexes.length)
   2088       throw new Error('Nothing selected!');
   2089 
   2090     for (var i = 0; i < selectedIndexes.length; i++) {
   2091       var entry = this.dataModel_.item(selectedIndexes[i]);
   2092       if (!entry) {
   2093         console.log('Error locating selected file at index: ' + i);
   2094         continue;
   2095       }
   2096 
   2097       ary.push(currentDirUrl + encodeURI(entry.name));
   2098     }
   2099 
   2100     // Multi-file selection has no other restrictions.
   2101     if (this.dialogType_ == FileManager.DialogType.SELECT_OPEN_MULTI_FILE) {
   2102       chrome.fileBrowserPrivate.selectFiles(ary);
   2103       // Window closed by above call.
   2104       return;
   2105     }
   2106 
   2107     // In full screen mode, open all files for vieweing.
   2108     if (this.dialogType_ == FileManager.DialogType.FULL_PAGE) {
   2109       chrome.fileBrowserPrivate.viewFiles(ary, "default");
   2110       // Window stays open.
   2111       return;
   2112     }
   2113 
   2114     // Everything else must have exactly one.
   2115     if (ary.length > 1)
   2116       throw new Error('Too many files selected!');
   2117 
   2118     if (this.dialogType_ == FileManager.DialogType.SELECT_FOLDER) {
   2119       if (!this.selection.leadEntry.isDirectory)
   2120         throw new Error('Selected entry is not a folder!');
   2121     } else if (this.dialogType_ == FileManager.DialogType.SELECT_OPEN_FILE) {
   2122       if (!this.selection.leadEntry.isFile)
   2123         throw new Error('Selected entry is not a file!');
   2124     }
   2125 
   2126     chrome.fileBrowserPrivate.selectFile(ary[0], 0);
   2127     // Window closed by above call.
   2128   };
   2129 
   2130 })();
   2131