Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / file_manager / foreground / js / file_manager.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  * FileManager constructor.
9  *
10  * FileManager objects encapsulate the functionality of the file selector
11  * dialogs, as well as the full screen file manager application (though the
12  * latter is not yet implemented).
13  *
14  * @constructor
15  */
16 function FileManager() {
17   this.initializeQueue_ = new AsyncUtil.Group();
18
19   /**
20    * Current list type.
21    * @type {ListType}
22    * @private
23    */
24   this.listType_ = null;
25
26   /**
27    * Whether to suppress the focus moving or not.
28    * This is used to filter out focusing by mouse.
29    * @type {boolean}
30    * @private
31    */
32   this.suppressFocus_ = false;
33
34   /**
35    * SelectionHandler.
36    * @type {SelectionHandler}
37    * @private
38    */
39   this.selectionHandler_ = null;
40
41   /**
42    * VolumeInfo of the current volume.
43    * @type {VolumeInfo}
44    * @private
45    */
46   this.currentVolumeInfo_ = null;
47 }
48
49 FileManager.prototype = {
50   __proto__: cr.EventTarget.prototype,
51   get directoryModel() {
52     return this.directoryModel_;
53   },
54   get navigationList() {
55     return this.navigationList_;
56   },
57   get document() {
58     return this.document_;
59   },
60   get fileTransferController() {
61     return this.fileTransferController_;
62   },
63   get backgroundPage() {
64     return this.backgroundPage_;
65   },
66   get volumeManager() {
67     return this.volumeManager_;
68   },
69   get ui() {
70     return this.ui_;
71   }
72 };
73
74 /**
75  * Unload the file manager.
76  * Used by background.js (when running in the packaged mode).
77  */
78 function unload() {
79   fileManager.onBeforeUnload_();
80   fileManager.onUnload_();
81 }
82
83 /**
84  * List of dialog types.
85  *
86  * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except
87  * FULL_PAGE which is specific to this code.
88  *
89  * @enum {string}
90  */
91 var DialogType = {
92   SELECT_FOLDER: 'folder',
93   SELECT_UPLOAD_FOLDER: 'upload-folder',
94   SELECT_SAVEAS_FILE: 'saveas-file',
95   SELECT_OPEN_FILE: 'open-file',
96   SELECT_OPEN_MULTI_FILE: 'open-multi-file',
97   FULL_PAGE: 'full-page'
98 };
99
100 /**
101  * @param {string} type Dialog type.
102  * @return {boolean} Whether the type is modal.
103  */
104 DialogType.isModal = function(type) {
105   return type == DialogType.SELECT_FOLDER ||
106       type == DialogType.SELECT_UPLOAD_FOLDER ||
107       type == DialogType.SELECT_SAVEAS_FILE ||
108       type == DialogType.SELECT_OPEN_FILE ||
109       type == DialogType.SELECT_OPEN_MULTI_FILE;
110 };
111
112 /**
113  * @param {string} type Dialog type.
114  * @return {boolean} Whether the type is open dialog.
115  */
116 DialogType.isOpenDialog = function(type) {
117   return type == DialogType.SELECT_OPEN_FILE ||
118          type == DialogType.SELECT_OPEN_MULTI_FILE;
119 };
120
121 /**
122  * @param {string} type Dialog type.
123  * @return {boolean} Whether the type is folder selection dialog.
124  */
125 DialogType.isFolderDialog = function(type) {
126   return type == DialogType.SELECT_FOLDER ||
127          type == DialogType.SELECT_UPLOAD_FOLDER;
128 };
129
130 /**
131  * Bottom margin of the list and tree for transparent preview panel.
132  * @const
133  */
134 var BOTTOM_MARGIN_FOR_PREVIEW_PANEL_PX = 52;
135
136 // Anonymous "namespace".
137 (function() {
138
139   // Private variables and helper functions.
140
141   /**
142    * Number of milliseconds in a day.
143    */
144   var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
145
146   /**
147    * Some UI elements react on a single click and standard double click handling
148    * leads to confusing results. We ignore a second click if it comes soon
149    * after the first.
150    */
151   var DOUBLE_CLICK_TIMEOUT = 200;
152
153   /**
154    * Update the element to display the information about remaining space for
155    * the storage.
156    * @param {!Element} spaceInnerBar Block element for a percentage bar
157    *                                 representing the remaining space.
158    * @param {!Element} spaceInfoLabel Inline element to contain the message.
159    * @param {!Element} spaceOuterBar Block element around the percentage bar.
160    */
161    var updateSpaceInfo = function(
162       sizeStatsResult, spaceInnerBar, spaceInfoLabel, spaceOuterBar) {
163     spaceInnerBar.removeAttribute('pending');
164     if (sizeStatsResult) {
165       var sizeStr = util.bytesToString(sizeStatsResult.remainingSize);
166       spaceInfoLabel.textContent = strf('SPACE_AVAILABLE', sizeStr);
167
168       var usedSpace =
169           sizeStatsResult.totalSize - sizeStatsResult.remainingSize;
170       spaceInnerBar.style.width =
171           (100 * usedSpace / sizeStatsResult.totalSize) + '%';
172
173       spaceOuterBar.hidden = false;
174     } else {
175       spaceOuterBar.hidden = true;
176       spaceInfoLabel.textContent = str('FAILED_SPACE_INFO');
177     }
178   };
179
180   // Public statics.
181
182   FileManager.ListType = {
183     DETAIL: 'detail',
184     THUMBNAIL: 'thumb'
185   };
186
187   FileManager.prototype.initPreferences_ = function(callback) {
188     var group = new AsyncUtil.Group();
189
190     // DRIVE preferences should be initialized before creating DirectoryModel
191     // to rebuild the roots list.
192     group.add(this.getPreferences_.bind(this));
193
194     // Get startup preferences.
195     this.viewOptions_ = {};
196     group.add(function(done) {
197       util.platform.getPreference(this.startupPrefName_, function(value) {
198         // Load the global default options.
199         try {
200           this.viewOptions_ = JSON.parse(value);
201         } catch (ignore) {}
202         // Override with window-specific options.
203         if (window.appState && window.appState.viewOptions) {
204           for (var key in window.appState.viewOptions) {
205             if (window.appState.viewOptions.hasOwnProperty(key))
206               this.viewOptions_[key] = window.appState.viewOptions[key];
207           }
208         }
209         done();
210       }.bind(this));
211     }.bind(this));
212
213     // Get the command line option.
214     group.add(function(done) {
215       chrome.commandLinePrivate.hasSwitch(
216           'file-manager-show-checkboxes', function(flag) {
217         this.showCheckboxes_ = flag;
218         done();
219       }.bind(this));
220     }.bind(this));
221
222     group.run(callback);
223   };
224
225   /**
226    * One time initialization for the file system and related things.
227    *
228    * @param {function()} callback Completion callback.
229    * @private
230    */
231   FileManager.prototype.initFileSystemUI_ = function(callback) {
232     this.table_.startBatchUpdates();
233     this.grid_.startBatchUpdates();
234
235     this.initFileList_();
236     this.setupCurrentDirectory_();
237
238     // PyAuto tests monitor this state by polling this variable
239     this.__defineGetter__('workerInitialized_', function() {
240        return this.metadataCache_.isInitialized();
241     }.bind(this));
242
243     this.initDateTimeFormatters_();
244
245     var self = this;
246
247     // Get the 'allowRedeemOffers' preference before launching
248     // FileListBannerController.
249     this.getPreferences_(function(pref) {
250       /** @type {boolean} */
251       var showOffers = pref['allowRedeemOffers'];
252       self.bannersController_ = new FileListBannerController(
253           self.directoryModel_, self.volumeManager_, self.document_,
254           showOffers);
255       self.bannersController_.addEventListener('relayout',
256                                                self.onResize_.bind(self));
257     });
258
259     var dm = this.directoryModel_;
260     dm.addEventListener('directory-changed',
261                         this.onDirectoryChanged_.bind(this));
262     dm.addEventListener('begin-update-files', function() {
263       self.currentList_.startBatchUpdates();
264     });
265     dm.addEventListener('end-update-files', function() {
266       self.restoreItemBeingRenamed_();
267       self.currentList_.endBatchUpdates();
268     });
269     dm.addEventListener('scan-started', this.onScanStarted_.bind(this));
270     dm.addEventListener('scan-completed', this.onScanCompleted_.bind(this));
271     dm.addEventListener('scan-failed', this.onScanCancelled_.bind(this));
272     dm.addEventListener('scan-cancelled', this.onScanCancelled_.bind(this));
273     dm.addEventListener('scan-updated', this.onScanUpdated_.bind(this));
274     dm.addEventListener('rescan-completed',
275                         this.onRescanCompleted_.bind(this));
276
277     this.directoryTree_.addEventListener('change', function() {
278       this.ensureDirectoryTreeItemNotBehindPreviewPanel_();
279     }.bind(this));
280
281     var stateChangeHandler =
282         this.onPreferencesChanged_.bind(this);
283     chrome.fileBrowserPrivate.onPreferencesChanged.addListener(
284         stateChangeHandler);
285     stateChangeHandler();
286
287     var driveConnectionChangedHandler =
288         this.onDriveConnectionChanged_.bind(this);
289     this.volumeManager_.addEventListener('drive-connection-changed',
290         driveConnectionChangedHandler);
291     driveConnectionChangedHandler();
292
293     // Set the initial focus.
294     this.refocus();
295     // Set it as a fallback when there is no focus.
296     this.document_.addEventListener('focusout', function(e) {
297       setTimeout(function() {
298         // When there is no focus, the active element is the <body>.
299         if (this.document_.activeElement == this.document_.body)
300           this.refocus();
301       }.bind(this), 0);
302     }.bind(this));
303
304     this.initDataTransferOperations_();
305
306     this.initContextMenus_();
307     this.initCommands_();
308
309     this.updateFileTypeFilter_();
310
311     this.selectionHandler_.onFileSelectionChanged();
312
313     this.table_.endBatchUpdates();
314     this.grid_.endBatchUpdates();
315
316     callback();
317   };
318
319   /**
320    * If |item| in the directory tree is behind the preview panel, scrolls up the
321    * parent view and make the item visible. This should be called when:
322    *  - the selected item is changed in the directory tree.
323    *  - the visibility of the the preview panel is changed.
324    *
325    * @private
326    */
327   FileManager.prototype.ensureDirectoryTreeItemNotBehindPreviewPanel_ =
328       function() {
329     var selectedSubTree = this.directoryTree_.selectedItem;
330     if (!selectedSubTree)
331       return;
332     var item = selectedSubTree.rowElement;
333     var parentView = this.directoryTree_;
334
335     var itemRect = item.getBoundingClientRect();
336     if (!itemRect)
337       return;
338
339     var listRect = parentView.getBoundingClientRect();
340     if (!listRect)
341       return;
342
343     var previewPanel = this.dialogDom_.querySelector('.preview-panel');
344     var previewPanelRect = previewPanel.getBoundingClientRect();
345     var panelHeight = previewPanelRect ? previewPanelRect.height : 0;
346
347     var itemBottom = itemRect.bottom;
348     var listBottom = listRect.bottom - panelHeight;
349
350     if (itemBottom > listBottom) {
351       var scrollOffset = itemBottom - listBottom;
352       parentView.scrollTop += scrollOffset;
353     }
354   };
355
356   /**
357    * @private
358    */
359   FileManager.prototype.initDateTimeFormatters_ = function() {
360     var use12hourClock = !this.preferences_['use24hourClock'];
361     this.table_.setDateTimeFormat(use12hourClock);
362   };
363
364   /**
365    * @private
366    */
367   FileManager.prototype.initDataTransferOperations_ = function() {
368     this.fileOperationManager_ =
369         this.backgroundPage_.background.fileOperationManager;
370
371     // CopyManager are required for 'Delete' operation in
372     // Open and Save dialogs. But drag-n-drop and copy-paste are not needed.
373     if (this.dialogType != DialogType.FULL_PAGE) return;
374
375     // TODO(hidehiko): Extract FileOperationManager related code from
376     // FileManager to simplify it.
377     this.onCopyProgressBound_ = this.onCopyProgress_.bind(this);
378     this.fileOperationManager_.addEventListener(
379         'copy-progress', this.onCopyProgressBound_);
380
381     this.onEntryChangedBound_ = this.onEntryChanged_.bind(this);
382     this.fileOperationManager_.addEventListener(
383         'entry-changed', this.onEntryChangedBound_);
384
385     var controller = this.fileTransferController_ =
386         new FileTransferController(this.document_,
387                                    this.fileOperationManager_,
388                                    this.metadataCache_,
389                                    this.directoryModel_,
390                                    this.volumeManager_);
391     controller.attachDragSource(this.table_.list);
392     controller.attachFileListDropTarget(this.table_.list);
393     controller.attachDragSource(this.grid_);
394     controller.attachFileListDropTarget(this.grid_);
395     controller.attachTreeDropTarget(this.directoryTree_);
396     controller.attachNavigationListDropTarget(this.navigationList_, true);
397     controller.attachCopyPasteHandlers();
398     controller.addEventListener('selection-copied',
399         this.blinkSelection.bind(this));
400     controller.addEventListener('selection-cut',
401         this.blinkSelection.bind(this));
402     controller.addEventListener('source-not-found',
403         this.onSourceNotFound_.bind(this));
404   };
405
406   /**
407    * Handles an error that the source entry of file operation is not found.
408    * @private
409    */
410   FileManager.prototype.onSourceNotFound_ = function(event) {
411     // Ensure this.sourceNotFoundErrorCount_ is integer.
412     this.sourceNotFoundErrorCount_ = ~~this.sourceNotFoundErrorCount_;
413     var item = new ProgressCenterItem();
414     item.id = 'source-not-found-' + this.sourceNotFoundErrorCount_;
415     if (event.progressType === ProgressItemType.COPY)
416       item.message = strf('COPY_SOURCE_NOT_FOUND_ERROR', event.fileName);
417     else if (event.progressType === ProgressItemType.MOVE)
418       item.message = strf('MOVE_SOURCE_NOT_FOUND_ERROR', event.fileName);
419     item.state = ProgressItemState.ERROR;
420     this.backgroundPage_.background.progressCenter.updateItem(item);
421     this.sourceNotFoundErrorCount_++;
422   };
423
424   /**
425    * One-time initialization of context menus.
426    * @private
427    */
428   FileManager.prototype.initContextMenus_ = function() {
429     this.fileContextMenu_ = this.dialogDom_.querySelector('#file-context-menu');
430     cr.ui.Menu.decorate(this.fileContextMenu_);
431
432     cr.ui.contextMenuHandler.setContextMenu(this.grid_, this.fileContextMenu_);
433     cr.ui.contextMenuHandler.setContextMenu(this.table_.querySelector('.list'),
434         this.fileContextMenu_);
435     cr.ui.contextMenuHandler.setContextMenu(
436         this.document_.querySelector('.drive-welcome.page'),
437         this.fileContextMenu_);
438
439     this.rootsContextMenu_ =
440         this.dialogDom_.querySelector('#roots-context-menu');
441     cr.ui.Menu.decorate(this.rootsContextMenu_);
442     this.navigationList_.setContextMenu(this.rootsContextMenu_);
443
444     this.directoryTreeContextMenu_ =
445         this.dialogDom_.querySelector('#directory-tree-context-menu');
446     cr.ui.Menu.decorate(this.directoryTreeContextMenu_);
447     this.directoryTree_.contextMenuForSubitems = this.directoryTreeContextMenu_;
448
449     this.textContextMenu_ =
450         this.dialogDom_.querySelector('#text-context-menu');
451     cr.ui.Menu.decorate(this.textContextMenu_);
452
453     this.gearButton_ = this.dialogDom_.querySelector('#gear-button');
454     this.gearButton_.addEventListener('menushow',
455         this.refreshRemainingSpace_.bind(this,
456                                          false /* Without loading caption. */));
457     chrome.fileBrowserPrivate.onDesktopChanged.addListener(function() {
458       this.updateVisitDesktopMenus_();
459       this.ui_.updateProfileBadge();
460     }.bind(this));
461     chrome.fileBrowserPrivate.onProfileAdded.addListener(
462         this.updateVisitDesktopMenus_.bind(this));
463     this.updateVisitDesktopMenus_();
464
465     this.dialogDom_.querySelector('#gear-menu').menuItemSelector =
466         'menuitem, hr';
467     cr.ui.decorate(this.gearButton_, cr.ui.MenuButton);
468
469     if (this.dialogType == DialogType.FULL_PAGE) {
470       // This is to prevent the buttons from stealing focus on mouse down.
471       var preventFocus = function(event) {
472         event.preventDefault();
473       };
474
475       var maximizeButton = this.dialogDom_.querySelector('#maximize-button');
476       maximizeButton.addEventListener('click', this.onMaximize.bind(this));
477       maximizeButton.addEventListener('mousedown', preventFocus);
478
479       var closeButton = this.dialogDom_.querySelector('#close-button');
480       closeButton.addEventListener('click', this.onClose.bind(this));
481       closeButton.addEventListener('mousedown', preventFocus);
482     }
483
484     this.syncButton.checkable = true;
485     this.hostedButton.checkable = true;
486     this.detailViewButton_.checkable = true;
487     this.thumbnailViewButton_.checkable = true;
488
489     if (util.platform.runningInBrowser()) {
490       // Suppresses the default context menu.
491       this.dialogDom_.addEventListener('contextmenu', function(e) {
492         e.preventDefault();
493         e.stopPropagation();
494       });
495     }
496   };
497
498   FileManager.prototype.onMaximize = function() {
499     var appWindow = chrome.app.window.current();
500     if (appWindow.isMaximized())
501       appWindow.restore();
502     else
503       appWindow.maximize();
504   };
505
506   FileManager.prototype.onClose = function() {
507     window.close();
508   };
509
510   /**
511    * One-time initialization of commands.
512    * @private
513    */
514   FileManager.prototype.initCommands_ = function() {
515     this.commandHandler = new CommandHandler(this);
516
517     // TODO(hirono): Move the following block to the UI part.
518     var commandButtons = this.dialogDom_.querySelectorAll('button[command]');
519     for (var j = 0; j < commandButtons.length; j++)
520       CommandButton.decorate(commandButtons[j]);
521
522     var inputs = this.dialogDom_.querySelectorAll(
523         'input[type=text], input[type=search], textarea');
524     for (var i = 0; i < inputs.length; i++) {
525       cr.ui.contextMenuHandler.setContextMenu(inputs[i], this.textContextMenu_);
526       this.registerInputCommands_(inputs[i]);
527     }
528
529     cr.ui.contextMenuHandler.setContextMenu(this.renameInput_,
530                                             this.textContextMenu_);
531     this.registerInputCommands_(this.renameInput_);
532     this.document_.addEventListener('command',
533                                     this.setNoHover_.bind(this, true));
534   };
535
536   /**
537    * Registers cut, copy, paste and delete commands on input element.
538    *
539    * @param {Node} node Text input element to register on.
540    * @private
541    */
542   FileManager.prototype.registerInputCommands_ = function(node) {
543     CommandUtil.forceDefaultHandler(node, 'cut');
544     CommandUtil.forceDefaultHandler(node, 'copy');
545     CommandUtil.forceDefaultHandler(node, 'paste');
546     CommandUtil.forceDefaultHandler(node, 'delete');
547     node.addEventListener('keydown', function(e) {
548       var key = util.getKeyModifiers(e) + e.keyCode;
549       if (key === '190' /* '/' */ || key === '191' /* '.' */) {
550         // If this key event is propagated, this is handled search command,
551         // which calls 'preventDefault' method.
552         e.stopPropagation();
553       }
554     });
555   };
556
557   /**
558    * Entry point of the initialization.
559    * This method is called from main.js.
560    */
561   FileManager.prototype.initializeCore = function() {
562     this.initializeQueue_.add(this.initGeneral_.bind(this), [], 'initGeneral');
563     this.initializeQueue_.add(this.initBackgroundPage_.bind(this),
564                               [], 'initBackgroundPage');
565     this.initializeQueue_.add(this.initPreferences_.bind(this),
566                               ['initGeneral'], 'initPreferences');
567     this.initializeQueue_.add(this.initVolumeManager_.bind(this),
568                               ['initGeneral', 'initBackgroundPage'],
569                               'initVolumeManager');
570
571     this.initializeQueue_.run();
572   };
573
574   FileManager.prototype.initializeUI = function(dialogDom, callback) {
575     this.dialogDom_ = dialogDom;
576     this.document_ = this.dialogDom_.ownerDocument;
577
578     this.initializeQueue_.add(
579         this.initEssentialUI_.bind(this),
580         ['initGeneral', 'initBackgroundPage'],
581         'initEssentialUI');
582     this.initializeQueue_.add(this.initAdditionalUI_.bind(this),
583         ['initEssentialUI'], 'initAdditionalUI');
584     this.initializeQueue_.add(
585         this.initFileSystemUI_.bind(this),
586         ['initAdditionalUI', 'initPreferences'], 'initFileSystemUI');
587
588     // Run again just in case if all pending closures have completed and the
589     // queue has stopped and monitor the completion.
590     this.initializeQueue_.run(callback);
591   };
592
593   /**
594    * Initializes general purpose basic things, which are used by other
595    * initializing methods.
596    *
597    * @param {function()} callback Completion callback.
598    * @private
599    */
600   FileManager.prototype.initGeneral_ = function(callback) {
601     // Initialize the application state.
602     // TODO(mtomasz): Unify window.appState with location.search format.
603     if (window.appState) {
604       this.params_ = window.appState.params || {};
605       this.initCurrentDirectoryURL_ = window.appState.currentDirectoryURL;
606       this.initSelectionURL_ = window.appState.selectionURL;
607       this.initTargetName_ = window.appState.targetName;
608     } else {
609       // Used by the select dialog only.
610       this.params_ = location.search ?
611                      JSON.parse(decodeURIComponent(location.search.substr(1))) :
612                      {};
613       this.initCurrentDirectoryURL_ = this.params_.currentDirectoryURL;
614       this.initSelectionURL_ = this.params_.selectionURL;
615       this.initTargetName_ = this.params_.targetName;
616     }
617
618     // Initialize the member variables that depend this.params_.
619     this.dialogType = this.params_.type || DialogType.FULL_PAGE;
620     this.startupPrefName_ = 'file-manager-' + this.dialogType;
621     this.fileTypes_ = this.params_.typeList || [];
622
623     callback();
624   };
625
626   /**
627    * Initialize the background page.
628    * @param {function()} callback Completion callback.
629    * @private
630    */
631   FileManager.prototype.initBackgroundPage_ = function(callback) {
632     chrome.runtime.getBackgroundPage(function(backgroundPage) {
633       this.backgroundPage_ = backgroundPage;
634       this.backgroundPage_.background.ready(function() {
635         loadTimeData.data = this.backgroundPage_.background.stringData;
636         callback();
637       }.bind(this));
638     }.bind(this));
639   };
640
641   /**
642    * Initializes the VolumeManager instance.
643    * @param {function()} callback Completion callback.
644    * @private
645    */
646   FileManager.prototype.initVolumeManager_ = function(callback) {
647     // Auto resolving to local path does not work for folders (e.g., dialog for
648     // loading unpacked extensions).
649     var noLocalPathResolution = DialogType.isFolderDialog(this.params_.type);
650
651     // If this condition is false, VolumeManagerWrapper hides all drive
652     // related event and data, even if Drive is enabled on preference.
653     // In other words, even if Drive is disabled on preference but Files.app
654     // should show Drive when it is re-enabled, then the value should be set to
655     // true.
656     // Note that the Drive enabling preference change is listened by
657     // DriveIntegrationService, so here we don't need to take care about it.
658     var driveEnabled =
659         !noLocalPathResolution || !this.params_.shouldReturnLocalPath;
660     this.volumeManager_ = new VolumeManagerWrapper(
661         driveEnabled, this.backgroundPage_);
662     callback();
663   };
664
665   /**
666    * One time initialization of the Files.app's essential UI elements. These
667    * elements will be shown to the user. Only visible elements should be
668    * initialized here. Any heavy operation should be avoided. Files.app's
669    * window is shown at the end of this routine.
670    *
671    * @param {function()} callback Completion callback.
672    * @private
673    */
674   FileManager.prototype.initEssentialUI_ = function(callback) {
675     // Record stats of dialog types. New values must NOT be inserted into the
676     // array enumerating the types. It must be in sync with
677     // FileDialogType enum in tools/metrics/histograms/histogram.xml.
678     metrics.recordEnum('Create', this.dialogType,
679         [DialogType.SELECT_FOLDER,
680          DialogType.SELECT_UPLOAD_FOLDER,
681          DialogType.SELECT_SAVEAS_FILE,
682          DialogType.SELECT_OPEN_FILE,
683          DialogType.SELECT_OPEN_MULTI_FILE,
684          DialogType.FULL_PAGE]);
685
686     // Create the metadata cache.
687     this.metadataCache_ = MetadataCache.createFull(this.volumeManager_);
688
689     // Create the root view of FileManager.
690     this.ui_ = new FileManagerUI(this.dialogDom_, this.dialogType);
691     this.fileTypeSelector_ = this.ui_.fileTypeSelector;
692     this.okButton_ = this.ui_.okButton;
693     this.cancelButton_ = this.ui_.cancelButton;
694
695     // Show the window as soon as the UI pre-initialization is done.
696     if (this.dialogType == DialogType.FULL_PAGE &&
697         !util.platform.runningInBrowser()) {
698       chrome.app.window.current().show();
699       setTimeout(callback, 100);  // Wait until the animation is finished.
700     } else {
701       callback();
702     }
703   };
704
705   /**
706    * One-time initialization of dialogs.
707    * @private
708    */
709   FileManager.prototype.initDialogs_ = function() {
710     // Initialize the dialog.
711     this.ui_.initDialogs();
712     FileManagerDialogBase.setFileManager(this);
713
714     // Obtains the dialog instances from FileManagerUI.
715     // TODO(hirono): Remove the properties from the FileManager class.
716     this.error = this.ui_.errorDialog;
717     this.alert = this.ui_.alertDialog;
718     this.confirm = this.ui_.confirmDialog;
719     this.prompt = this.ui_.promptDialog;
720     this.shareDialog_ = this.ui_.shareDialog;
721     this.defaultTaskPicker = this.ui_.defaultTaskPicker;
722     this.suggestAppsDialog = this.ui_.suggestAppsDialog;
723   };
724
725   /**
726    * One-time initialization of various DOM nodes. Loads the additional DOM
727    * elements visible to the user. Initialize here elements, which are expensive
728    * or hidden in the beginning.
729    *
730    * @param {function()} callback Completion callback.
731    * @private
732    */
733   FileManager.prototype.initAdditionalUI_ = function(callback) {
734     this.initDialogs_();
735     this.ui_.initAdditionalUI();
736
737     this.dialogDom_.addEventListener('drop', function(e) {
738       // Prevent opening an URL by dropping it onto the page.
739       e.preventDefault();
740     });
741
742     this.dialogDom_.addEventListener('click',
743                                      this.onExternalLinkClick_.bind(this));
744     // Cache nodes we'll be manipulating.
745     var dom = this.dialogDom_;
746
747     this.filenameInput_ = dom.querySelector('#filename-input-box input');
748     this.taskItems_ = dom.querySelector('#tasks');
749
750     this.table_ = dom.querySelector('.detail-table');
751     this.grid_ = dom.querySelector('.thumbnail-grid');
752     this.spinner_ = dom.querySelector('#list-container > .spinner-layer');
753     this.showSpinner_(true);
754
755     // Check the option to hide the selecting checkboxes.
756     this.table_.showCheckboxes = this.showCheckboxes_;
757
758     var fullPage = this.dialogType == DialogType.FULL_PAGE;
759     FileTable.decorate(this.table_, this.metadataCache_, fullPage);
760     FileGrid.decorate(this.grid_, this.metadataCache_, this.volumeManager_);
761
762     this.previewPanel_ = new PreviewPanel(
763         dom.querySelector('.preview-panel'),
764         DialogType.isOpenDialog(this.dialogType) ?
765             PreviewPanel.VisibilityType.ALWAYS_VISIBLE :
766             PreviewPanel.VisibilityType.AUTO,
767         this.metadataCache_,
768         this.volumeManager_);
769     this.previewPanel_.addEventListener(
770         PreviewPanel.Event.VISIBILITY_CHANGE,
771         this.onPreviewPanelVisibilityChange_.bind(this));
772     this.previewPanel_.initialize();
773
774     this.previewPanel_.breadcrumbs.addEventListener(
775          'pathclick', this.onBreadcrumbClick_.bind(this));
776
777     // Initialize progress center panel.
778     this.progressCenterPanel_ = new ProgressCenterPanel(
779         dom.querySelector('#progress-center'));
780     this.backgroundPage_.background.progressCenter.addPanel(
781         this.progressCenterPanel_);
782
783     this.document_.addEventListener('keydown', this.onKeyDown_.bind(this));
784
785     // This capturing event is only used to distinguish focusing using
786     // keyboard from focusing using mouse.
787     this.document_.addEventListener('mousedown', function() {
788       this.suppressFocus_ = true;
789     }.bind(this), true);
790
791     this.renameInput_ = this.document_.createElement('input');
792     this.renameInput_.className = 'rename';
793
794     this.renameInput_.addEventListener(
795         'keydown', this.onRenameInputKeyDown_.bind(this));
796     this.renameInput_.addEventListener(
797         'blur', this.onRenameInputBlur_.bind(this));
798
799     // TODO(hirono): Rename the handler after creating the DialogFooter class.
800     this.filenameInput_.addEventListener(
801         'input', this.onFilenameInputInput_.bind(this));
802     this.filenameInput_.addEventListener(
803         'keydown', this.onFilenameInputKeyDown_.bind(this));
804     this.filenameInput_.addEventListener(
805         'focus', this.onFilenameInputFocus_.bind(this));
806
807     this.listContainer_ = this.dialogDom_.querySelector('#list-container');
808     this.listContainer_.addEventListener(
809         'keydown', this.onListKeyDown_.bind(this));
810     this.listContainer_.addEventListener(
811         'keypress', this.onListKeyPress_.bind(this));
812     this.listContainer_.addEventListener(
813         'mousemove', this.onListMouseMove_.bind(this));
814
815     this.okButton_.addEventListener('click', this.onOk_.bind(this));
816     this.onCancelBound_ = this.onCancel_.bind(this);
817     this.cancelButton_.addEventListener('click', this.onCancelBound_);
818
819     this.decorateSplitter(
820         this.dialogDom_.querySelector('#navigation-list-splitter'));
821     this.decorateSplitter(
822         this.dialogDom_.querySelector('#middlebar-splitter'));
823
824     this.dialogContainer_ = this.dialogDom_.querySelector('.dialog-container');
825
826     this.syncButton = this.dialogDom_.querySelector('#drive-sync-settings');
827     this.syncButton.addEventListener('click', this.onDrivePrefClick_.bind(
828         this, 'cellularDisabled', false /* not inverted */));
829
830     this.hostedButton = this.dialogDom_.querySelector('#drive-hosted-settings');
831     this.hostedButton.addEventListener('click', this.onDrivePrefClick_.bind(
832         this, 'hostedFilesDisabled', true /* inverted */));
833
834     this.detailViewButton_ =
835         this.dialogDom_.querySelector('#detail-view');
836     this.detailViewButton_.addEventListener('activate',
837         this.onDetailViewButtonClick_.bind(this));
838
839     this.thumbnailViewButton_ =
840         this.dialogDom_.querySelector('#thumbnail-view');
841     this.thumbnailViewButton_.addEventListener('activate',
842         this.onThumbnailViewButtonClick_.bind(this));
843
844     cr.ui.ComboButton.decorate(this.taskItems_);
845     this.taskItems_.showMenu = function(shouldSetFocus) {
846       // Prevent the empty menu from opening.
847       if (!this.menu.length)
848         return;
849       cr.ui.ComboButton.prototype.showMenu.call(this, shouldSetFocus);
850     };
851     this.taskItems_.addEventListener('select',
852         this.onTaskItemClicked_.bind(this));
853
854     this.dialogDom_.ownerDocument.defaultView.addEventListener(
855         'resize', this.onResize_.bind(this));
856
857     this.filePopup_ = null;
858
859     this.searchBoxWrapper_ = this.ui_.searchBox.element;
860     this.searchBox_ = this.ui_.searchBox.inputElement;
861     this.searchBox_.addEventListener(
862         'input', this.onSearchBoxUpdate_.bind(this));
863     this.ui_.searchBox.clearButton.addEventListener(
864         'click', this.onSearchClearButtonClick_.bind(this));
865
866     this.lastSearchQuery_ = '';
867
868     this.autocompleteList_ = this.ui_.searchBox.autocompleteList;
869     this.autocompleteList_.requestSuggestions =
870         this.requestAutocompleteSuggestions_.bind(this);
871
872     // Instead, open the suggested item when Enter key is pressed or
873     // mouse-clicked.
874     this.autocompleteList_.handleEnterKeydown = function(event) {
875       this.openAutocompleteSuggestion_();
876       this.lastAutocompleteQuery_ = '';
877       this.autocompleteList_.suggestions = [];
878     }.bind(this);
879     this.autocompleteList_.addEventListener('mousedown', function(event) {
880       this.openAutocompleteSuggestion_();
881       this.lastAutocompleteQuery_ = '';
882       this.autocompleteList_.suggestions = [];
883     }.bind(this));
884
885     this.defaultActionMenuItem_ =
886         this.dialogDom_.querySelector('#default-action');
887
888     this.openWithCommand_ =
889         this.dialogDom_.querySelector('#open-with');
890
891     this.driveBuyMoreStorageCommand_ =
892         this.dialogDom_.querySelector('#drive-buy-more-space');
893
894     this.defaultActionMenuItem_.addEventListener('click',
895         this.dispatchSelectionAction_.bind(this));
896
897     this.initFileTypeFilter_();
898
899     util.addIsFocusedMethod();
900
901     // Populate the static localized strings.
902     i18nTemplate.process(this.document_, loadTimeData);
903
904     // Arrange the file list.
905     this.table_.normalizeColumns();
906     this.table_.redraw();
907
908     callback();
909   };
910
911   /**
912    * @param {Event} event Click event.
913    * @private
914    */
915   FileManager.prototype.onBreadcrumbClick_ = function(event) {
916     this.directoryModel_.changeDirectoryEntry(event.entry);
917   };
918
919   /**
920    * Constructs table and grid (heavy operation).
921    * @private
922    **/
923   FileManager.prototype.initFileList_ = function() {
924     // Always sharing the data model between the detail/thumb views confuses
925     // them.  Instead we maintain this bogus data model, and hook it up to the
926     // view that is not in use.
927     this.emptyDataModel_ = new cr.ui.ArrayDataModel([]);
928     this.emptySelectionModel_ = new cr.ui.ListSelectionModel();
929
930     var singleSelection =
931         this.dialogType == DialogType.SELECT_OPEN_FILE ||
932         this.dialogType == DialogType.SELECT_FOLDER ||
933         this.dialogType == DialogType.SELECT_UPLOAD_FOLDER ||
934         this.dialogType == DialogType.SELECT_SAVEAS_FILE;
935
936     this.fileFilter_ = new FileFilter(
937         this.metadataCache_,
938         false  /* Don't show dot files by default. */);
939
940     this.fileWatcher_ = new FileWatcher(this.metadataCache_);
941     this.fileWatcher_.addEventListener(
942         'watcher-metadata-changed',
943         this.onWatcherMetadataChanged_.bind(this));
944
945     this.directoryModel_ = new DirectoryModel(
946         singleSelection,
947         this.fileFilter_,
948         this.fileWatcher_,
949         this.metadataCache_,
950         this.volumeManager_);
951
952     this.folderShortcutsModel_ = new FolderShortcutsDataModel(
953         this.volumeManager_);
954
955     this.selectionHandler_ = new FileSelectionHandler(this);
956
957     var dataModel = this.directoryModel_.getFileList();
958
959     this.table_.setupCompareFunctions(dataModel);
960
961     dataModel.addEventListener('permuted',
962                                this.updateStartupPrefs_.bind(this));
963
964     this.directoryModel_.getFileListSelection().addEventListener('change',
965         this.selectionHandler_.onFileSelectionChanged.bind(
966             this.selectionHandler_));
967
968     this.initList_(this.grid_);
969     this.initList_(this.table_.list);
970
971     var fileListFocusBound = this.onFileListFocus_.bind(this);
972     var fileListBlurBound = this.onFileListBlur_.bind(this);
973
974     this.table_.list.addEventListener('focus', fileListFocusBound);
975     this.grid_.addEventListener('focus', fileListFocusBound);
976
977     this.table_.list.addEventListener('blur', fileListBlurBound);
978     this.grid_.addEventListener('blur', fileListBlurBound);
979
980     var dragStartBound = this.onDragStart_.bind(this);
981     this.table_.list.addEventListener('dragstart', dragStartBound);
982     this.grid_.addEventListener('dragstart', dragStartBound);
983
984     var dragEndBound = this.onDragEnd_.bind(this);
985     this.table_.list.addEventListener('dragend', dragEndBound);
986     this.grid_.addEventListener('dragend', dragEndBound);
987     // This event is published by DragSelector because drag end event is not
988     // published at the end of drag selection.
989     this.table_.list.addEventListener('dragselectionend', dragEndBound);
990     this.grid_.addEventListener('dragselectionend', dragEndBound);
991
992     // TODO(mtomasz, yoshiki): Create navigation list earlier, and here just
993     // attach the directory model.
994     this.initNavigationList_();
995
996     this.table_.addEventListener('column-resize-end',
997                                  this.updateStartupPrefs_.bind(this));
998
999     // Restore preferences.
1000     this.directoryModel_.getFileList().sort(
1001         this.viewOptions_.sortField || 'modificationTime',
1002         this.viewOptions_.sortDirection || 'desc');
1003     if (this.viewOptions_.columns) {
1004       var cm = this.table_.columnModel;
1005       for (var i = 0; i < cm.totalSize; i++) {
1006         if (this.viewOptions_.columns[i] > 0)
1007           cm.setWidth(i, this.viewOptions_.columns[i]);
1008       }
1009     }
1010     this.setListType(this.viewOptions_.listType || FileManager.ListType.DETAIL);
1011
1012     this.textSearchState_ = {text: '', date: new Date()};
1013     this.closeOnUnmount_ = (this.params_.action == 'auto-open');
1014
1015     if (this.closeOnUnmount_) {
1016       this.volumeManager_.addEventListener('externally-unmounted',
1017          this.onExternallyUnmounted_.bind(this));
1018     }
1019
1020     // Update metadata to change 'Today' and 'Yesterday' dates.
1021     var today = new Date();
1022     today.setHours(0);
1023     today.setMinutes(0);
1024     today.setSeconds(0);
1025     today.setMilliseconds(0);
1026     setTimeout(this.dailyUpdateModificationTime_.bind(this),
1027                today.getTime() + MILLISECONDS_IN_DAY - Date.now() + 1000);
1028   };
1029
1030   /**
1031    * @private
1032    */
1033   FileManager.prototype.initNavigationList_ = function() {
1034     this.directoryTree_ = this.dialogDom_.querySelector('#directory-tree');
1035     DirectoryTree.decorate(this.directoryTree_,
1036                            this.directoryModel_,
1037                            this.volumeManager_);
1038
1039     this.navigationList_ = this.dialogDom_.querySelector('#navigation-list');
1040     NavigationList.decorate(this.navigationList_,
1041                             this.volumeManager_,
1042                             this.directoryModel_);
1043     this.navigationList_.fileManager = this;
1044     this.navigationList_.dataModel = new NavigationListModel(
1045         this.volumeManager_, this.folderShortcutsModel_);
1046   };
1047
1048   /**
1049    * @private
1050    */
1051   FileManager.prototype.updateMiddleBarVisibility_ = function() {
1052     var entry = this.directoryModel_.getCurrentDirEntry();
1053     if (!entry)
1054       return;
1055
1056     var driveVolume = this.volumeManager_.getVolumeInfo(entry);
1057     var visible = driveVolume && !driveVolume.error &&
1058         driveVolume.volumeType === util.VolumeType.DRIVE;
1059     this.dialogDom_.
1060         querySelector('.dialog-middlebar-contents').hidden = !visible;
1061     this.dialogDom_.querySelector('#middlebar-splitter').hidden = !visible;
1062     this.onResize_();
1063   };
1064
1065   /**
1066    * @private
1067    */
1068   FileManager.prototype.updateStartupPrefs_ = function() {
1069     var sortStatus = this.directoryModel_.getFileList().sortStatus;
1070     var prefs = {
1071       sortField: sortStatus.field,
1072       sortDirection: sortStatus.direction,
1073       columns: [],
1074       listType: this.listType_
1075     };
1076     var cm = this.table_.columnModel;
1077     for (var i = 0; i < cm.totalSize; i++) {
1078       prefs.columns.push(cm.getWidth(i));
1079     }
1080     // Save the global default.
1081     util.platform.setPreference(this.startupPrefName_, JSON.stringify(prefs));
1082
1083     // Save the window-specific preference.
1084     if (window.appState) {
1085       window.appState.viewOptions = prefs;
1086       util.saveAppState();
1087     }
1088   };
1089
1090   FileManager.prototype.refocus = function() {
1091     var targetElement;
1092     if (this.dialogType == DialogType.SELECT_SAVEAS_FILE)
1093       targetElement = this.filenameInput_;
1094     else
1095       targetElement = this.currentList_;
1096
1097     // Hack: if the tabIndex is disabled, we can assume a modal dialog is
1098     // shown. Focus to a button on the dialog instead.
1099     if (!targetElement.hasAttribute('tabIndex') || targetElement.tabIndex == -1)
1100       targetElement = document.querySelector('button:not([tabIndex="-1"])');
1101
1102     if (targetElement)
1103       targetElement.focus();
1104   };
1105
1106   /**
1107    * File list focus handler. Used to select the top most element on the list
1108    * if nothing was selected.
1109    *
1110    * @private
1111    */
1112   FileManager.prototype.onFileListFocus_ = function() {
1113     // Do not select default item if focused using mouse.
1114     if (this.suppressFocus_)
1115       return;
1116
1117     var selection = this.getSelection();
1118     if (!selection || selection.totalCount != 0)
1119       return;
1120
1121     this.directoryModel_.selectIndex(0);
1122   };
1123
1124   /**
1125    * File list blur handler.
1126    *
1127    * @private
1128    */
1129   FileManager.prototype.onFileListBlur_ = function() {
1130     this.suppressFocus_ = false;
1131   };
1132
1133   /**
1134    * Index of selected item in the typeList of the dialog params.
1135    *
1136    * @return {number} 1-based index of selected type or 0 if no type selected.
1137    * @private
1138    */
1139   FileManager.prototype.getSelectedFilterIndex_ = function() {
1140     var index = Number(this.fileTypeSelector_.selectedIndex);
1141     if (index < 0)  // Nothing selected.
1142       return 0;
1143     if (this.params_.includeAllFiles)  // Already 1-based.
1144       return index;
1145     return index + 1;  // Convert to 1-based;
1146   };
1147
1148   FileManager.prototype.setListType = function(type) {
1149     if (type && type == this.listType_)
1150       return;
1151
1152     this.table_.list.startBatchUpdates();
1153     this.grid_.startBatchUpdates();
1154
1155     // TODO(dzvorygin): style.display and dataModel setting order shouldn't
1156     // cause any UI bugs. Currently, the only right way is first to set display
1157     // style and only then set dataModel.
1158
1159     if (type == FileManager.ListType.DETAIL) {
1160       this.table_.dataModel = this.directoryModel_.getFileList();
1161       this.table_.selectionModel = this.directoryModel_.getFileListSelection();
1162       this.table_.hidden = false;
1163       this.grid_.hidden = true;
1164       this.grid_.selectionModel = this.emptySelectionModel_;
1165       this.grid_.dataModel = this.emptyDataModel_;
1166       this.table_.hidden = false;
1167       /** @type {cr.ui.List} */
1168       this.currentList_ = this.table_.list;
1169       this.detailViewButton_.setAttribute('checked', '');
1170       this.thumbnailViewButton_.removeAttribute('checked');
1171       this.detailViewButton_.setAttribute('disabled', '');
1172       this.thumbnailViewButton_.removeAttribute('disabled');
1173     } else if (type == FileManager.ListType.THUMBNAIL) {
1174       this.grid_.dataModel = this.directoryModel_.getFileList();
1175       this.grid_.selectionModel = this.directoryModel_.getFileListSelection();
1176       this.grid_.hidden = false;
1177       this.table_.hidden = true;
1178       this.table_.selectionModel = this.emptySelectionModel_;
1179       this.table_.dataModel = this.emptyDataModel_;
1180       this.grid_.hidden = false;
1181       /** @type {cr.ui.List} */
1182       this.currentList_ = this.grid_;
1183       this.thumbnailViewButton_.setAttribute('checked', '');
1184       this.detailViewButton_.removeAttribute('checked');
1185       this.thumbnailViewButton_.setAttribute('disabled', '');
1186       this.detailViewButton_.removeAttribute('disabled');
1187     } else {
1188       throw new Error('Unknown list type: ' + type);
1189     }
1190
1191     this.listType_ = type;
1192     this.updateStartupPrefs_();
1193     this.onResize_();
1194
1195     this.table_.list.endBatchUpdates();
1196     this.grid_.endBatchUpdates();
1197   };
1198
1199   /**
1200    * Initialize the file list table or grid.
1201    *
1202    * @param {cr.ui.List} list The list.
1203    * @private
1204    */
1205   FileManager.prototype.initList_ = function(list) {
1206     // Overriding the default role 'list' to 'listbox' for better accessibility
1207     // on ChromeOS.
1208     list.setAttribute('role', 'listbox');
1209     list.addEventListener('click', this.onDetailClick_.bind(this));
1210     list.id = 'file-list';
1211   };
1212
1213   /**
1214    * @private
1215    */
1216   FileManager.prototype.onCopyProgress_ = function(event) {
1217     if (event.reason == 'ERROR' &&
1218         event.error.code == util.FileOperationErrorType.FILESYSTEM_ERROR &&
1219         event.error.data.toDrive &&
1220         event.error.data.name == util.FileError.QUOTA_EXCEEDED_ERR) {
1221       this.alert.showHtml(
1222           strf('DRIVE_SERVER_OUT_OF_SPACE_HEADER'),
1223           strf('DRIVE_SERVER_OUT_OF_SPACE_MESSAGE',
1224               decodeURIComponent(
1225                   event.error.data.sourceFileUrl.split('/').pop()),
1226               str('GOOGLE_DRIVE_BUY_STORAGE_URL')));
1227     }
1228   };
1229
1230   /**
1231    * Handler of file manager operations. Called when an entry has been
1232    * changed.
1233    * This updates directory model to reflect operation result immediately (not
1234    * waiting for directory update event). Also, preloads thumbnails for the
1235    * images of new entries.
1236    * See also FileOperationManager.EventRouter.
1237    *
1238    * @param {Event} event An event for the entry change.
1239    * @private
1240    */
1241   FileManager.prototype.onEntryChanged_ = function(event) {
1242     var kind = event.kind;
1243     var entry = event.entry;
1244     this.directoryModel_.onEntryChanged(kind, entry);
1245     this.selectionHandler_.onFileSelectionChanged();
1246
1247     if (kind === util.EntryChangedKind.CREATED && FileType.isImage(entry)) {
1248       // Preload a thumbnail if the new copied entry an image.
1249       var locationInfo = this.volumeManager_.getLocationInfo(entry);
1250       if (!locationInfo)
1251         return;
1252       this.metadataCache_.get(entry, 'thumbnail|drive', function(metadata) {
1253         var thumbnailLoader_ = new ThumbnailLoader(
1254             entry,
1255             ThumbnailLoader.LoaderType.CANVAS,
1256             metadata,
1257             undefined,  // Media type.
1258             // TODO(mtomasz): Use Entry instead of paths.
1259             locationInfo.isDriveBased ?
1260                 ThumbnailLoader.UseEmbedded.USE_EMBEDDED :
1261                 ThumbnailLoader.UseEmbedded.NO_EMBEDDED,
1262             10);  // Very low priority.
1263         thumbnailLoader_.loadDetachedImage(function(success) {});
1264       });
1265     }
1266   };
1267
1268   /**
1269    * Fills the file type list or hides it.
1270    * @private
1271    */
1272   FileManager.prototype.initFileTypeFilter_ = function() {
1273     if (this.params_.includeAllFiles) {
1274       var option = this.document_.createElement('option');
1275       option.innerText = str('ALL_FILES_FILTER');
1276       this.fileTypeSelector_.appendChild(option);
1277       option.value = 0;
1278     }
1279
1280     for (var i = 0; i !== this.fileTypes_.length; i++) {
1281       var fileType = this.fileTypes_[i];
1282       var option = this.document_.createElement('option');
1283       var description = fileType.description;
1284       if (!description) {
1285         // See if all the extensions in the group have the same description.
1286         for (var j = 0; j !== fileType.extensions.length; j++) {
1287           var currentDescription = FileType.typeToString(
1288               FileType.getTypeForName('.' + fileType.extensions[j]));
1289           if (!description)  // Set the first time.
1290             description = currentDescription;
1291           else if (description != currentDescription) {
1292             // No single description, fall through to the extension list.
1293             description = null;
1294             break;
1295           }
1296         }
1297
1298         if (!description)
1299           // Convert ['jpg', 'png'] to '*.jpg, *.png'.
1300           description = fileType.extensions.map(function(s) {
1301            return '*.' + s;
1302           }).join(', ');
1303        }
1304        option.innerText = description;
1305
1306        option.value = i + 1;
1307
1308        if (fileType.selected)
1309          option.selected = true;
1310
1311        this.fileTypeSelector_.appendChild(option);
1312     }
1313
1314     var options = this.fileTypeSelector_.querySelectorAll('option');
1315     if (options.length >= 2) {
1316       // There is in fact no choice, show the selector.
1317       this.fileTypeSelector_.hidden = false;
1318
1319       this.fileTypeSelector_.addEventListener('change',
1320           this.updateFileTypeFilter_.bind(this));
1321     }
1322   };
1323
1324   /**
1325    * Filters file according to the selected file type.
1326    * @private
1327    */
1328   FileManager.prototype.updateFileTypeFilter_ = function() {
1329     this.fileFilter_.removeFilter('fileType');
1330     var selectedIndex = this.getSelectedFilterIndex_();
1331     if (selectedIndex > 0) { // Specific filter selected.
1332       var regexp = new RegExp('.*(' +
1333           this.fileTypes_[selectedIndex - 1].extensions.join('|') + ')$', 'i');
1334       var filter = function(entry) {
1335         return entry.isDirectory || regexp.test(entry.name);
1336       };
1337       this.fileFilter_.addFilter('fileType', filter);
1338     }
1339   };
1340
1341   /**
1342    * Resize details and thumb views to fit the new window size.
1343    * @private
1344    */
1345   FileManager.prototype.onResize_ = function() {
1346     if (this.listType_ == FileManager.ListType.THUMBNAIL)
1347       this.grid_.relayout();
1348     else
1349       this.table_.relayout();
1350
1351     // May not be available during initialization.
1352     if (this.directoryTree_)
1353       this.directoryTree_.relayout();
1354
1355     // TODO(mtomasz, yoshiki): Initialize navigation list earlier, before
1356     // file system is available.
1357     if (this.navigationList_)
1358       this.navigationList_.redraw();
1359
1360     this.ui_.searchBox.updateSizeRelatedStyle();
1361
1362     this.previewPanel_.breadcrumbs.truncate();
1363   };
1364
1365   /**
1366    * Handles local metadata changes in the currect directory.
1367    * @param {Event} event Change event.
1368    * @private
1369    */
1370   FileManager.prototype.onWatcherMetadataChanged_ = function(event) {
1371     this.updateMetadataInUI_(
1372         event.metadataType, event.entries, event.properties);
1373   };
1374
1375   /**
1376    * Resize details and thumb views to fit the new window size.
1377    * @private
1378    */
1379   FileManager.prototype.onPreviewPanelVisibilityChange_ = function() {
1380     // This method may be called on initialization. Some object may not be
1381     // initialized.
1382
1383     var panelHeight = this.previewPanel_.visible ?
1384         this.previewPanel_.height : 0;
1385     if (this.grid_)
1386       this.grid_.setBottomMarginForPanel(panelHeight);
1387     if (this.table_)
1388       this.table_.setBottomMarginForPanel(panelHeight);
1389
1390     if (this.directoryTree_) {
1391       this.directoryTree_.setBottomMarginForPanel(panelHeight);
1392       this.ensureDirectoryTreeItemNotBehindPreviewPanel_();
1393     }
1394   };
1395
1396   /**
1397    * Invoked when the drag is started on the list or the grid.
1398    * @private
1399    */
1400   FileManager.prototype.onDragStart_ = function() {
1401     // On open file dialog, the preview panel is always shown.
1402     if (DialogType.isOpenDialog(this.dialogType))
1403       return;
1404     this.previewPanel_.visibilityType =
1405         PreviewPanel.VisibilityType.ALWAYS_HIDDEN;
1406   };
1407
1408   /**
1409    * Invoked when the drag is ended on the list or the grid.
1410    * @private
1411    */
1412   FileManager.prototype.onDragEnd_ = function() {
1413     // On open file dialog, the preview panel is always shown.
1414     if (DialogType.isOpenDialog(this.dialogType))
1415       return;
1416     this.previewPanel_.visibilityType = PreviewPanel.VisibilityType.AUTO;
1417   };
1418
1419   /**
1420    * Sets up the current directory during initialization.
1421    * @private
1422    */
1423   FileManager.prototype.setupCurrentDirectory_ = function() {
1424     var tracker = this.directoryModel_.createDirectoryChangeTracker();
1425     var queue = new AsyncUtil.Queue();
1426
1427     // Wait until the volume manager is initialized.
1428     queue.run(function(callback) {
1429       tracker.start();
1430       this.volumeManager_.ensureInitialized(callback);
1431     }.bind(this));
1432
1433     var nextCurrentDirEntry;
1434     var selectionEntry;
1435
1436     // Resolve the selectionURL to selectionEntry or to currentDirectoryEntry
1437     // in case of being a display root.
1438     queue.run(function(callback) {
1439       if (!this.initSelectionURL_) {
1440         callback();
1441         return;
1442       }
1443       webkitResolveLocalFileSystemURL(
1444           this.initSelectionURL_,
1445           function(inEntry) {
1446             var locationInfo = this.volumeManager_.getLocationInfo(inEntry);
1447             // If location information is not available, then the volume is
1448             // no longer (or never) available.
1449             if (!locationInfo) {
1450               callback();
1451               return;
1452             }
1453             // If the selection is root, then use it as a current directory
1454             // instead. This is because, selecting a root entry is done as
1455             // opening it.
1456             if (locationInfo.isRootEntry)
1457               nextCurrentDirEntry = inEntry;
1458             else
1459               selectionEntry = inEntry;
1460             callback();
1461           }.bind(this), callback);
1462     }.bind(this));
1463     // Resolve the currentDirectoryURL to currentDirectoryEntry (if not done
1464     // by the previous step).
1465     queue.run(function(callback) {
1466       if (nextCurrentDirEntry || !this.initCurrentDirectoryURL_) {
1467         callback();
1468         return;
1469       }
1470       webkitResolveLocalFileSystemURL(
1471           this.initCurrentDirectoryURL_,
1472           function(inEntry) {
1473             var locationInfo = this.volumeManager_.getLocationInfo(inEntry);
1474             if (!locationInfo) {
1475               callback();
1476               return;
1477             }
1478             nextCurrentDirEntry = inEntry;
1479             callback();
1480           }.bind(this), callback);
1481       // TODO(mtomasz): Implement reopening on special search, when fake
1482       // entries are converted to directory providers.
1483     }.bind(this));
1484
1485     // If the directory to be changed to is not available, then first fallback
1486     // to the parent of the selection entry.
1487     queue.run(function(callback) {
1488       if (nextCurrentDirEntry || !selectionEntry) {
1489         callback();
1490         return;
1491       }
1492       selectionEntry.getParent(function(inEntry) {
1493         nextCurrentDirEntry = inEntry;
1494         callback();
1495       }.bind(this));
1496     }.bind(this));
1497
1498     // If the directory to be changed to is still not resolved, then fallback
1499     // to the default display root.
1500     queue.run(function(callback) {
1501       if (nextCurrentDirEntry) {
1502         callback();
1503         return;
1504       }
1505       this.volumeManager_.getDefaultDisplayRoot(function(displayRoot) {
1506         nextCurrentDirEntry = displayRoot;
1507         callback();
1508       }.bind(this));
1509     }.bind(this));
1510
1511     // If selection failed to be resolved (eg. didn't exist, in case of saving
1512     // a file, or in case of a fallback of the current directory, then try to
1513     // resolve again using the target name.
1514     queue.run(function(callback) {
1515       if (selectionEntry || !nextCurrentDirEntry || !this.initTargetName_) {
1516         callback();
1517         return;
1518       }
1519       // Try to resolve as a file first. If it fails, then as a directory.
1520       nextCurrentDirEntry.getFile(
1521           this.initTargetName_,
1522           {},
1523           function(targetEntry) {
1524             selectionEntry = targetEntry;
1525             callback();
1526           }, function() {
1527             // Failed to resolve as a file
1528             nextCurrentDirEntry.getDirectory(
1529               this.initTargetName_,
1530               {},
1531               function(targetEntry) {
1532                 selectionEntry = targetEntry;
1533                 callback();
1534               }, function() {
1535                 // Failed to resolve as either file or directory.
1536                 callback();
1537               });
1538           }.bind(this));
1539     }.bind(this));
1540
1541
1542     // Finalize.
1543     queue.run(function(callback) {
1544       // Check directory change.
1545       tracker.stop();
1546       if (tracker.hasChanged) {
1547         callback();
1548         return;
1549       }
1550       // Finish setup current directory.
1551       this.finishSetupCurrentDirectory_(
1552           nextCurrentDirEntry,
1553           selectionEntry,
1554           this.initTargetName_);
1555       callback();
1556     }.bind(this));
1557   };
1558
1559   /**
1560    * @param {DirectoryEntry} directoryEntry Directory to be opened.
1561    * @param {Entry=} opt_selectionEntry Entry to be selected.
1562    * @param {string=} opt_suggestedName Suggested name for a non-existing\
1563    *     selection.
1564    * @private
1565    */
1566   FileManager.prototype.finishSetupCurrentDirectory_ = function(
1567       directoryEntry, opt_selectionEntry, opt_suggestedName) {
1568     // Open the directory, and select the selection (if passed).
1569     if (util.isFakeEntry(directoryEntry)) {
1570       this.directoryModel_.specialSearch(directoryEntry, '');
1571     } else {
1572       this.directoryModel_.changeDirectoryEntry(directoryEntry, function() {
1573         if (opt_selectionEntry)
1574           this.directoryModel_.selectEntry(opt_selectionEntry);
1575       }.bind(this));
1576     }
1577
1578     if (this.dialogType === DialogType.FULL_PAGE) {
1579       // In the FULL_PAGE mode if the restored URL points to a file we might
1580       // have to invoke a task after selecting it.
1581       if (this.params_.action === 'select')
1582         return;
1583
1584       var task = null;
1585       // Handle restoring after crash, or the gallery action.
1586       // TODO(mtomasz): Use the gallery action instead of just the gallery
1587       //     field.
1588       if (this.params_.gallery || this.params_.action === 'gallery') {
1589         if (!opt_selectionEntry) {
1590           // Non-existent file or a directory.
1591           // Reloading while the Gallery is open with empty or multiple
1592           // selection. Open the Gallery when the directory is scanned.
1593           task = function() {
1594             new FileTasks(this, this.params_).openGallery([]);
1595           }.bind(this);
1596         } else {
1597           // The file or the directory exists.
1598           task = function() {
1599             new FileTasks(this, this.params_).openGallery([opt_selectionEntry]);
1600           }.bind(this);
1601         }
1602       } else {
1603         // TODO(mtomasz): Implement remounting archives after crash.
1604         //                See: crbug.com/333139
1605       }
1606
1607       // If there is a task to be run, run it after the scan is completed.
1608       if (task) {
1609         var listener = function() {
1610           if (!util.isSameEntry(this.directoryModel_.getCurrentDirEntry(),
1611                                directoryEntry)) {
1612             // Opened on a different URL. Probably fallbacked. Therefore,
1613             // do not invoke a task.
1614             return;
1615           }
1616           this.directoryModel_.removeEventListener(
1617               'scan-completed', listener);
1618           task();
1619         }.bind(this);
1620         this.directoryModel_.addEventListener('scan-completed', listener);
1621       }
1622     } else if (this.dialogType === DialogType.SELECT_SAVEAS_FILE) {
1623       this.filenameInput_.value = opt_suggestedName || '';
1624       this.selectTargetNameInFilenameInput_();
1625     }
1626   };
1627
1628   /**
1629    * @private
1630    */
1631   FileManager.prototype.refreshCurrentDirectoryMetadata_ = function() {
1632     var entries = this.directoryModel_.getFileList().slice();
1633     var directoryEntry = this.directoryModel_.getCurrentDirEntry();
1634     if (!directoryEntry)
1635       return;
1636     // We don't pass callback here. When new metadata arrives, we have an
1637     // observer registered to update the UI.
1638
1639     // TODO(dgozman): refresh content metadata only when modificationTime
1640     // changed.
1641     var isFakeEntry = util.isFakeEntry(directoryEntry);
1642     var getEntries = (isFakeEntry ? [] : [directoryEntry]).concat(entries);
1643     if (!isFakeEntry)
1644       this.metadataCache_.clearRecursively(directoryEntry, '*');
1645     this.metadataCache_.get(getEntries, 'filesystem', null);
1646
1647     if (this.isOnDrive())
1648       this.metadataCache_.get(getEntries, 'drive', null);
1649
1650     var visibleItems = this.currentList_.items;
1651     var visibleEntries = [];
1652     for (var i = 0; i < visibleItems.length; i++) {
1653       var index = this.currentList_.getIndexOfListItem(visibleItems[i]);
1654       var entry = this.directoryModel_.getFileList().item(index);
1655       // The following check is a workaround for the bug in list: sometimes item
1656       // does not have listIndex, and therefore is not found in the list.
1657       if (entry) visibleEntries.push(entry);
1658     }
1659     this.metadataCache_.get(visibleEntries, 'thumbnail', null);
1660   };
1661
1662   /**
1663    * @private
1664    */
1665   FileManager.prototype.dailyUpdateModificationTime_ = function() {
1666     var entries = this.directoryModel_.getFileList().slice();
1667     this.metadataCache_.get(
1668         entries,
1669         'filesystem',
1670         this.updateMetadataInUI_.bind(this, 'filesystem', entries));
1671
1672     setTimeout(this.dailyUpdateModificationTime_.bind(this),
1673                MILLISECONDS_IN_DAY);
1674   };
1675
1676   /**
1677    * @param {string} type Type of metadata changed.
1678    * @param {Array.<Entry>} entries Array of entries.
1679    * @param {Object.<string, Object>} props Map from entry URLs to metadata
1680    *     props.
1681    * @private
1682    */
1683   FileManager.prototype.updateMetadataInUI_ = function(
1684       type, entries, properties) {
1685     if (this.listType_ == FileManager.ListType.DETAIL)
1686       this.table_.updateListItemsMetadata(type, properties);
1687     else
1688       this.grid_.updateListItemsMetadata(type, properties);
1689     // TODO: update bottom panel thumbnails.
1690   };
1691
1692   /**
1693    * Restore the item which is being renamed while refreshing the file list. Do
1694    * nothing if no item is being renamed or such an item disappeared.
1695    *
1696    * While refreshing file list it gets repopulated with new file entries.
1697    * There is not a big difference whether DOM items stay the same or not.
1698    * Except for the item that the user is renaming.
1699    *
1700    * @private
1701    */
1702   FileManager.prototype.restoreItemBeingRenamed_ = function() {
1703     if (!this.isRenamingInProgress())
1704       return;
1705
1706     var dm = this.directoryModel_;
1707     var leadIndex = dm.getFileListSelection().leadIndex;
1708     if (leadIndex < 0)
1709       return;
1710
1711     var leadEntry = dm.getFileList().item(leadIndex);
1712     if (!util.isSameEntry(this.renameInput_.currentEntry, leadEntry))
1713       return;
1714
1715     var leadListItem = this.findListItemForNode_(this.renameInput_);
1716     if (this.currentList_ == this.table_.list) {
1717       this.table_.updateFileMetadata(leadListItem, leadEntry);
1718     }
1719     this.currentList_.restoreLeadItem(leadListItem);
1720   };
1721
1722   /**
1723    * TODO(mtomasz): Move this to a utility function working on the root type.
1724    * @return {boolean} True if the current directory content is from Google
1725    *     Drive.
1726    */
1727   FileManager.prototype.isOnDrive = function() {
1728     var rootType = this.directoryModel_.getCurrentRootType();
1729     return rootType === RootType.DRIVE ||
1730            rootType === RootType.DRIVE_SHARED_WITH_ME ||
1731            rootType === RootType.DRIVE_RECENT ||
1732            rootType === RootType.DRIVE_OFFLINE;
1733   };
1734
1735   /**
1736    * Overrides default handling for clicks on hyperlinks.
1737    * In a packaged apps links with targer='_blank' open in a new tab by
1738    * default, other links do not open at all.
1739    *
1740    * @param {Event} event Click event.
1741    * @private
1742    */
1743   FileManager.prototype.onExternalLinkClick_ = function(event) {
1744     if (event.target.tagName != 'A' || !event.target.href)
1745       return;
1746
1747     if (this.dialogType != DialogType.FULL_PAGE)
1748       this.onCancel_();
1749   };
1750
1751   /**
1752    * Task combobox handler.
1753    *
1754    * @param {Object} event Event containing task which was clicked.
1755    * @private
1756    */
1757   FileManager.prototype.onTaskItemClicked_ = function(event) {
1758     var selection = this.getSelection();
1759     if (!selection.tasks) return;
1760
1761     if (event.item.task) {
1762       // Task field doesn't exist on change-default dropdown item.
1763       selection.tasks.execute(event.item.task.taskId);
1764     } else {
1765       var extensions = [];
1766
1767       for (var i = 0; i < selection.entries.length; i++) {
1768         var match = /\.(\w+)$/g.exec(selection.entries[i].toURL());
1769         if (match) {
1770           var ext = match[1].toUpperCase();
1771           if (extensions.indexOf(ext) == -1) {
1772             extensions.push(ext);
1773           }
1774         }
1775       }
1776
1777       var format = '';
1778
1779       if (extensions.length == 1) {
1780         format = extensions[0];
1781       }
1782
1783       // Change default was clicked. We should open "change default" dialog.
1784       selection.tasks.showTaskPicker(this.defaultTaskPicker,
1785           loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM'),
1786           strf('CHANGE_DEFAULT_CAPTION', format),
1787           this.onDefaultTaskDone_.bind(this));
1788     }
1789   };
1790
1791   /**
1792    * Sets the given task as default, when this task is applicable.
1793    *
1794    * @param {Object} task Task to set as default.
1795    * @private
1796    */
1797   FileManager.prototype.onDefaultTaskDone_ = function(task) {
1798     // TODO(dgozman): move this method closer to tasks.
1799     var selection = this.getSelection();
1800     chrome.fileBrowserPrivate.setDefaultTask(
1801         task.taskId,
1802         util.entriesToURLs(selection.entries),
1803         selection.mimeTypes);
1804     selection.tasks = new FileTasks(this);
1805     selection.tasks.init(selection.entries, selection.mimeTypes);
1806     selection.tasks.display(this.taskItems_);
1807     this.refreshCurrentDirectoryMetadata_();
1808     this.selectionHandler_.onFileSelectionChanged();
1809   };
1810
1811   /**
1812    * @private
1813    */
1814   FileManager.prototype.onPreferencesChanged_ = function() {
1815     var self = this;
1816     this.getPreferences_(function(prefs) {
1817       self.initDateTimeFormatters_();
1818       self.refreshCurrentDirectoryMetadata_();
1819
1820       if (prefs.cellularDisabled)
1821         self.syncButton.setAttribute('checked', '');
1822       else
1823         self.syncButton.removeAttribute('checked');
1824
1825       if (self.hostedButton.hasAttribute('checked') !=
1826           prefs.hostedFilesDisabled && self.isOnDrive()) {
1827         self.directoryModel_.rescan();
1828       }
1829
1830       if (!prefs.hostedFilesDisabled)
1831         self.hostedButton.setAttribute('checked', '');
1832       else
1833         self.hostedButton.removeAttribute('checked');
1834     },
1835     true /* refresh */);
1836   };
1837
1838   FileManager.prototype.onDriveConnectionChanged_ = function() {
1839     var connection = this.volumeManager_.getDriveConnectionState();
1840     if (this.commandHandler)
1841       this.commandHandler.updateAvailability();
1842     if (this.dialogContainer_)
1843       this.dialogContainer_.setAttribute('connection', connection.type);
1844     this.shareDialog_.hideWithResult(ShareDialog.Result.NETWORK_ERROR);
1845     this.suggestAppsDialog.onDriveConnectionChanged(connection.type);
1846   };
1847
1848   /**
1849    * Tells whether the current directory is read only.
1850    * TODO(mtomasz): Remove and use EntryLocation directly.
1851    * @return {boolean} True if read only, false otherwise.
1852    */
1853   FileManager.prototype.isOnReadonlyDirectory = function() {
1854     return this.directoryModel_.isReadOnly();
1855   };
1856
1857   /**
1858    * @param {Event} Unmount event.
1859    * @private
1860    */
1861   FileManager.prototype.onExternallyUnmounted_ = function(event) {
1862     if (event.volumeInfo === this.currentVolumeInfo_) {
1863       if (this.closeOnUnmount_) {
1864         // If the file manager opened automatically when a usb drive inserted,
1865         // user have never changed current volume (that implies the current
1866         // directory is still on the device) then close this window.
1867         window.close();
1868       }
1869     }
1870   };
1871
1872   /**
1873    * Shows a modal-like file viewer/editor on top of the File Manager UI.
1874    *
1875    * @param {HTMLElement} popup Popup element.
1876    * @param {function()} closeCallback Function to call after the popup is
1877    *     closed.
1878    */
1879   FileManager.prototype.openFilePopup = function(popup, closeCallback) {
1880     this.closeFilePopup();
1881     this.filePopup_ = popup;
1882     this.filePopupCloseCallback_ = closeCallback;
1883     this.dialogDom_.insertBefore(
1884         this.filePopup_, this.dialogDom_.querySelector('#iframe-drag-area'));
1885     this.filePopup_.focus();
1886     this.document_.body.setAttribute('overlay-visible', '');
1887     this.document_.querySelector('#iframe-drag-area').hidden = false;
1888   };
1889
1890   /**
1891    * Closes the modal-like file viewer/editor popup.
1892    */
1893   FileManager.prototype.closeFilePopup = function() {
1894     if (this.filePopup_) {
1895       this.document_.body.removeAttribute('overlay-visible');
1896       this.document_.querySelector('#iframe-drag-area').hidden = true;
1897       // The window resize would not be processed properly while the relevant
1898       // divs had 'display:none', force resize after the layout fired.
1899       setTimeout(this.onResize_.bind(this), 0);
1900       if (this.filePopup_.contentWindow &&
1901           this.filePopup_.contentWindow.unload) {
1902         this.filePopup_.contentWindow.unload();
1903       }
1904
1905       if (this.filePopupCloseCallback_) {
1906         this.filePopupCloseCallback_();
1907         this.filePopupCloseCallback_ = null;
1908       }
1909
1910       // These operations have to be in the end, otherwise v8 crashes on an
1911       // assert. See: crbug.com/224174.
1912       this.dialogDom_.removeChild(this.filePopup_);
1913       this.filePopup_ = null;
1914     }
1915   };
1916
1917   /**
1918    * Updates visibility of the draggable app region in the modal-like file
1919    * viewer/editor.
1920    *
1921    * @param {boolean} visible True for visible, false otherwise.
1922    */
1923   FileManager.prototype.onFilePopupAppRegionChanged = function(visible) {
1924     if (!this.filePopup_)
1925       return;
1926
1927     this.document_.querySelector('#iframe-drag-area').hidden = !visible;
1928   };
1929
1930   /**
1931    * @return {Array.<Entry>} List of all entries in the current directory.
1932    */
1933   FileManager.prototype.getAllEntriesInCurrentDirectory = function() {
1934     return this.directoryModel_.getFileList().slice();
1935   };
1936
1937   FileManager.prototype.isRenamingInProgress = function() {
1938     return !!this.renameInput_.currentEntry;
1939   };
1940
1941   /**
1942    * @private
1943    */
1944   FileManager.prototype.focusCurrentList_ = function() {
1945     if (this.listType_ == FileManager.ListType.DETAIL)
1946       this.table_.focus();
1947     else  // this.listType_ == FileManager.ListType.THUMBNAIL)
1948       this.grid_.focus();
1949   };
1950
1951   /**
1952    * Return DirectoryEntry of the current directory or null.
1953    * @return {DirectoryEntry} DirectoryEntry of the current directory. Returns
1954    *     null if the directory model is not ready or the current directory is
1955    *     not set.
1956    */
1957   FileManager.prototype.getCurrentDirectoryEntry = function() {
1958     return this.directoryModel_ && this.directoryModel_.getCurrentDirEntry();
1959   };
1960
1961   /**
1962    * Deletes the selected file and directories recursively.
1963    */
1964   FileManager.prototype.deleteSelection = function() {
1965     // TODO(mtomasz): Remove this temporary dialog. crbug.com/167364
1966     var entries = this.getSelection().entries;
1967     var message = entries.length == 1 ?
1968         strf('GALLERY_CONFIRM_DELETE_ONE', entries[0].name) :
1969         strf('GALLERY_CONFIRM_DELETE_SOME', entries.length);
1970     this.confirm.show(message, function() {
1971       this.fileOperationManager_.deleteEntries(entries);
1972     }.bind(this));
1973   };
1974
1975   /**
1976    * Shows the share dialog for the selected file or directory.
1977    */
1978   FileManager.prototype.shareSelection = function() {
1979     var entries = this.getSelection().entries;
1980     if (entries.length != 1) {
1981       console.warn('Unable to share multiple items at once.');
1982       return;
1983     }
1984     // Add the overlapped class to prevent the applicaiton window from
1985     // captureing mouse events.
1986     this.shareDialog_.show(entries[0], function(result) {
1987       if (result == ShareDialog.Result.NETWORK_ERROR)
1988         this.error.show(str('SHARE_ERROR'));
1989     }.bind(this));
1990   };
1991
1992   /**
1993    * Creates a folder shortcut.
1994    * @param {Entry} entry A shortcut which refers to |entry| to be created.
1995    */
1996   FileManager.prototype.createFolderShortcut = function(entry) {
1997     // Duplicate entry.
1998     if (this.folderShortcutExists(entry))
1999       return;
2000
2001     this.folderShortcutsModel_.add(entry);
2002   };
2003
2004   /**
2005    * Checkes if the shortcut which refers to the given folder exists or not.
2006    * @param {Entry} entry Entry of the folder to be checked.
2007    */
2008   FileManager.prototype.folderShortcutExists = function(entry) {
2009     return this.folderShortcutsModel_.exists(entry);
2010   };
2011
2012   /**
2013    * Removes the folder shortcut.
2014    * @param {Entry} entry The shortcut which refers to |entry| is to be removed.
2015    */
2016   FileManager.prototype.removeFolderShortcut = function(entry) {
2017     this.folderShortcutsModel_.remove(entry);
2018   };
2019
2020   /**
2021    * Blinks the selection. Used to give feedback when copying or cutting the
2022    * selection.
2023    */
2024   FileManager.prototype.blinkSelection = function() {
2025     var selection = this.getSelection();
2026     if (!selection || selection.totalCount == 0)
2027       return;
2028
2029     for (var i = 0; i < selection.entries.length; i++) {
2030       var selectedIndex = selection.indexes[i];
2031       var listItem = this.currentList_.getListItemByIndex(selectedIndex);
2032       if (listItem)
2033         this.blinkListItem_(listItem);
2034     }
2035   };
2036
2037   /**
2038    * @param {Element} listItem List item element.
2039    * @private
2040    */
2041   FileManager.prototype.blinkListItem_ = function(listItem) {
2042     listItem.classList.add('blink');
2043     setTimeout(function() {
2044       listItem.classList.remove('blink');
2045     }, 100);
2046   };
2047
2048   /**
2049    * @private
2050    */
2051   FileManager.prototype.selectTargetNameInFilenameInput_ = function() {
2052     var input = this.filenameInput_;
2053     input.focus();
2054     var selectionEnd = input.value.lastIndexOf('.');
2055     if (selectionEnd == -1) {
2056       input.select();
2057     } else {
2058       input.selectionStart = 0;
2059       input.selectionEnd = selectionEnd;
2060     }
2061   };
2062
2063   /**
2064    * Handles mouse click or tap.
2065    *
2066    * @param {Event} event The click event.
2067    * @private
2068    */
2069   FileManager.prototype.onDetailClick_ = function(event) {
2070     if (this.isRenamingInProgress()) {
2071       // Don't pay attention to clicks during a rename.
2072       return;
2073     }
2074
2075     var listItem = this.findListItemForEvent_(event);
2076     var selection = this.getSelection();
2077     if (!listItem || !listItem.selected || selection.totalCount != 1) {
2078       return;
2079     }
2080
2081     // React on double click, but only if both clicks hit the same item.
2082     // TODO(mtomasz): Simplify it, and use a double click handler if possible.
2083     var clickNumber = (this.lastClickedItem_ == listItem) ? 2 : undefined;
2084     this.lastClickedItem_ = listItem;
2085
2086     if (event.detail != clickNumber)
2087       return;
2088
2089     var entry = selection.entries[0];
2090     if (entry.isDirectory) {
2091       this.onDirectoryAction_(entry);
2092     } else {
2093       this.dispatchSelectionAction_();
2094     }
2095   };
2096
2097   /**
2098    * @private
2099    */
2100   FileManager.prototype.dispatchSelectionAction_ = function() {
2101     if (this.dialogType == DialogType.FULL_PAGE) {
2102       var selection = this.getSelection();
2103       var tasks = selection.tasks;
2104       var urls = selection.urls;
2105       var mimeTypes = selection.mimeTypes;
2106       if (tasks)
2107         tasks.executeDefault();
2108       return true;
2109     }
2110     if (!this.okButton_.disabled) {
2111       this.onOk_();
2112       return true;
2113     }
2114     return false;
2115   };
2116
2117   /**
2118    * Opens the suggest file dialog.
2119    *
2120    * @param {Entry} entry Entry of the file.
2121    * @param {function()} onSuccess Success callback.
2122    * @param {function()} onCancelled User-cancelled callback.
2123    * @param {function()} onFailure Failure callback.
2124    * @private
2125    */
2126   FileManager.prototype.openSuggestAppsDialog =
2127       function(entry, onSuccess, onCancelled, onFailure) {
2128     if (!url) {
2129       onFailure();
2130       return;
2131     }
2132
2133     this.metadataCache_.get([entry], 'drive', function(props) {
2134       if (!props || !props[0] || !props[0].contentMimeType) {
2135         onFailure();
2136         return;
2137       }
2138
2139       var basename = entry.name;
2140       var splitted = PathUtil.splitExtension(basename);
2141       var filename = splitted[0];
2142       var extension = splitted[1];
2143       var mime = props[0].contentMimeType;
2144
2145       // Returns with failure if the file has neither extension nor mime.
2146       if (!extension || !mime) {
2147         onFailure();
2148         return;
2149       }
2150
2151       var onDialogClosed = function(result) {
2152         switch (result) {
2153           case SuggestAppsDialog.Result.INSTALL_SUCCESSFUL:
2154             onSuccess();
2155             break;
2156           case SuggestAppsDialog.Result.FAILED:
2157             onFailure();
2158             break;
2159           default:
2160             onCancelled();
2161         }
2162       };
2163
2164       if (FileTasks.EXECUTABLE_EXTENSIONS.indexOf(extension) !== -1) {
2165         this.suggestAppsDialog.showByFilename(filename, onDialogClosed);
2166       } else {
2167         this.suggestAppsDialog.showByExtensionAndMime(
2168             extension, mime, onDialogClosed);
2169       }
2170     }.bind(this));
2171   };
2172
2173   /**
2174    * Called when a dialog is shown or hidden.
2175    * @param {boolean} flag True if a dialog is shown, false if hidden.   */
2176   FileManager.prototype.onDialogShownOrHidden = function(show) {
2177     // Set/unset a flag to disable dragging on the title area.
2178     this.dialogContainer_.classList.toggle('disable-header-drag', show);
2179   };
2180
2181   /**
2182    * Executes directory action (i.e. changes directory).
2183    *
2184    * @param {DirectoryEntry} entry Directory entry to which directory should be
2185    *                               changed.
2186    * @private
2187    */
2188   FileManager.prototype.onDirectoryAction_ = function(entry) {
2189     return this.directoryModel_.changeDirectoryEntry(entry);
2190   };
2191
2192   /**
2193    * Update the window title.
2194    * @private
2195    */
2196   FileManager.prototype.updateTitle_ = function() {
2197     if (this.dialogType != DialogType.FULL_PAGE)
2198       return;
2199
2200     if (!this.currentVolumeInfo_)
2201       return;
2202
2203     this.document_.title = this.currentVolumeInfo_.getLabel();
2204   };
2205
2206   /**
2207    * Update the gear menu.
2208    * @private
2209    */
2210   FileManager.prototype.updateGearMenu_ = function() {
2211     var hideItemsForDrive = !this.isOnDrive();
2212     this.syncButton.hidden = hideItemsForDrive;
2213     this.hostedButton.hidden = hideItemsForDrive;
2214     this.document_.getElementById('drive-separator').hidden =
2215         hideItemsForDrive;
2216     this.refreshRemainingSpace_(true);  // Show loading caption.
2217   };
2218
2219   /**
2220    * Update menus that move the window to the other profile's desktop.
2221    * TODO(hirono): Add the GearMenu class and make it a member of the class.
2222    * TODO(hirono): Handle the case where a profile is added while the menu is
2223    *     opened.
2224    * @private
2225    */
2226   FileManager.prototype.updateVisitDesktopMenus_ = function() {
2227     var gearMenu = this.document_.querySelector('#gear-menu');
2228     var separator =
2229         this.document_.querySelector('#multi-profile-separator');
2230
2231     // Remove existing menu items.
2232     var oldItems =
2233         this.document_.querySelectorAll('#gear-menu .visit-desktop');
2234     for (var i = 0; i < oldItems.length; i++) {
2235       gearMenu.removeChild(oldItems[i]);
2236     }
2237     separator.hidden = true;
2238
2239     if (this.dialogType !== DialogType.FULL_PAGE)
2240       return;
2241
2242     // Obtain the profile information.
2243     chrome.fileBrowserPrivate.getProfiles(function(profiles,
2244                                                    currentId,
2245                                                    displayedId) {
2246       // Check if the menus are needed or not.
2247       var insertingPosition = separator.nextSibling;
2248       if (profiles.length === 1 && profiles[0].profileId === displayedId)
2249         return;
2250
2251       separator.hidden = false;
2252       for (var i = 0; i < profiles.length; i++) {
2253         var profile = profiles[i];
2254         if (profile.profileId === displayedId)
2255           continue;
2256         var item = this.document_.createElement('menuitem');
2257         cr.ui.MenuItem.decorate(item);
2258         gearMenu.insertBefore(item, insertingPosition);
2259         item.className = 'visit-desktop';
2260         item.label = strf('VISIT_DESKTOP_OF_USER',
2261                           profile.displayName,
2262                           profile.profileId);
2263         item.addEventListener('activate', function(inProfile, event) {
2264           // Stop propagate and hide the menu manually, in order to prevent the
2265           // focus from being back to the button. (cf. http://crbug.com/248479)
2266           event.stopPropagation();
2267           this.gearButton_.hideMenu();
2268           this.gearButton_.blur();
2269           chrome.fileBrowserPrivate.visitDesktop(inProfile.profileId);
2270         }.bind(this, profile));
2271       }
2272     }.bind(this));
2273   };
2274
2275   /**
2276    * Refreshes space info of the current volume.
2277    * @param {boolean} showLoadingCaption Whether show loading caption or not.
2278    * @private
2279    */
2280   FileManager.prototype.refreshRemainingSpace_ = function(showLoadingCaption) {
2281     if (!this.currentVolumeInfo_)
2282       return;
2283
2284     var volumeSpaceInfoLabel =
2285         this.dialogDom_.querySelector('#volume-space-info-label');
2286     var volumeSpaceInnerBar =
2287         this.dialogDom_.querySelector('#volume-space-info-bar');
2288     var volumeSpaceOuterBar =
2289         this.dialogDom_.querySelector('#volume-space-info-bar').parentNode;
2290
2291     volumeSpaceInnerBar.setAttribute('pending', '');
2292
2293     if (showLoadingCaption) {
2294       volumeSpaceInfoLabel.innerText = str('WAITING_FOR_SPACE_INFO');
2295       volumeSpaceInnerBar.style.width = '100%';
2296     }
2297
2298     var currentVolumeInfo = this.currentVolumeInfo_;
2299     chrome.fileBrowserPrivate.getSizeStats(
2300         currentVolumeInfo.volumeId, function(result) {
2301           var volumeInfo = this.volumeManager_.getVolumeInfo(
2302               this.directoryModel_.getCurrentDirEntry());
2303           if (currentVolumeInfo !== this.currentVolumeInfo_)
2304             return;
2305           updateSpaceInfo(result,
2306                           volumeSpaceInnerBar,
2307                           volumeSpaceInfoLabel,
2308                           volumeSpaceOuterBar);
2309         }.bind(this));
2310   };
2311
2312   /**
2313    * Update the UI when the current directory changes.
2314    *
2315    * @param {Event} event The directory-changed event.
2316    * @private
2317    */
2318   FileManager.prototype.onDirectoryChanged_ = function(event) {
2319     var newCurrentVolumeInfo = this.volumeManager_.getVolumeInfo(
2320         event.newDirEntry);
2321
2322     // If volume has changed, then update the gear menu.
2323     if (this.currentVolumeInfo_ !== newCurrentVolumeInfo) {
2324       this.updateGearMenu_();
2325       // If the volume has changed, and it was previously set, then do not
2326       // close on unmount anymore.
2327       if (this.currentVolumeInfo_)
2328         this.closeOnUnmount_ = false;
2329     }
2330
2331     // Remember the current volume info.
2332     this.currentVolumeInfo_ = newCurrentVolumeInfo;
2333
2334     this.selectionHandler_.onFileSelectionChanged();
2335     this.ui_.searchBox.clear();
2336     // TODO(mtomasz): Consider remembering the selection.
2337     util.updateAppState(
2338         this.getCurrentDirectoryEntry() ?
2339         this.getCurrentDirectoryEntry().toURL() : '',
2340         '' /* selectionURL */,
2341         '' /* opt_param */);
2342
2343     if (this.commandHandler)
2344       this.commandHandler.updateAvailability();
2345
2346     this.updateUnformattedVolumeStatus_();
2347     this.updateTitle_();
2348
2349     var currentEntry = this.getCurrentDirectoryEntry();
2350     this.previewPanel_.currentEntry = util.isFakeEntry(currentEntry) ?
2351         null : currentEntry;
2352   };
2353
2354   FileManager.prototype.updateUnformattedVolumeStatus_ = function() {
2355     var volumeInfo = this.volumeManager_.getVolumeInfo(
2356         this.directoryModel_.getCurrentDirEntry());
2357
2358     if (volumeInfo && volumeInfo.error) {
2359       this.dialogDom_.setAttribute('unformatted', '');
2360
2361       var errorNode = this.dialogDom_.querySelector('#format-panel > .error');
2362       if (volumeInfo.error == util.VolumeError.UNSUPPORTED_FILESYSTEM) {
2363         errorNode.textContent = str('UNSUPPORTED_FILESYSTEM_WARNING');
2364       } else {
2365         errorNode.textContent = str('UNKNOWN_FILESYSTEM_WARNING');
2366       }
2367
2368       // Update 'canExecute' for format command so the format button's disabled
2369       // property is properly set.
2370       if (this.commandHandler)
2371         this.commandHandler.updateAvailability();
2372     } else {
2373       this.dialogDom_.removeAttribute('unformatted');
2374     }
2375   };
2376
2377   FileManager.prototype.findListItemForEvent_ = function(event) {
2378     return this.findListItemForNode_(event.touchedElement || event.srcElement);
2379   };
2380
2381   FileManager.prototype.findListItemForNode_ = function(node) {
2382     var item = this.currentList_.getListItemAncestor(node);
2383     // TODO(serya): list should check that.
2384     return item && this.currentList_.isItem(item) ? item : null;
2385   };
2386
2387   /**
2388    * Unload handler for the page.  May be called manually for the file picker
2389    * dialog, because it closes by calling extension API functions that do not
2390    * return.
2391    *
2392    * TODO(hirono): This method is not called when Files.app is opend as a dialog
2393    *     and is closed by the close button in the dialog frame. crbug.com/309967
2394    * @private
2395    */
2396   FileManager.prototype.onUnload_ = function() {
2397     if (this.directoryModel_)
2398       this.directoryModel_.dispose();
2399     if (this.volumeManager_)
2400       this.volumeManager_.dispose();
2401     if (this.filePopup_ &&
2402         this.filePopup_.contentWindow &&
2403         this.filePopup_.contentWindow.unload)
2404       this.filePopup_.contentWindow.unload(true /* exiting */);
2405     if (this.progressCenterPanel_)
2406       this.backgroundPage_.background.progressCenter.removePanel(
2407           this.progressCenterPanel_);
2408     if (this.fileOperationManager_) {
2409       if (this.onCopyProgressBound_) {
2410         this.fileOperationManager_.removeEventListener(
2411             'copy-progress', this.onCopyProgressBound_);
2412       }
2413       if (this.onEntryChangedBound_) {
2414         this.fileOperationManager_.removeEventListener(
2415             'entry-changed', this.onEntryChangedBound_);
2416       }
2417     }
2418     window.closing = true;
2419     if (this.backgroundPage_ && util.platform.runningInBrowser())
2420       this.backgroundPage_.background.tryClose();
2421   };
2422
2423   FileManager.prototype.initiateRename = function() {
2424     var item = this.currentList_.ensureLeadItemExists();
2425     if (!item)
2426       return;
2427     var label = item.querySelector('.filename-label');
2428     var input = this.renameInput_;
2429
2430     input.value = label.textContent;
2431     label.parentNode.setAttribute('renaming', '');
2432     label.parentNode.appendChild(input);
2433     input.focus();
2434     var selectionEnd = input.value.lastIndexOf('.');
2435     if (selectionEnd == -1) {
2436       input.select();
2437     } else {
2438       input.selectionStart = 0;
2439       input.selectionEnd = selectionEnd;
2440     }
2441
2442     // This has to be set late in the process so we don't handle spurious
2443     // blur events.
2444     input.currentEntry = this.currentList_.dataModel.item(item.listIndex);
2445   };
2446
2447   /**
2448    * @type {Event} Key event.
2449    * @private
2450    */
2451   FileManager.prototype.onRenameInputKeyDown_ = function(event) {
2452     if (!this.isRenamingInProgress())
2453       return;
2454
2455     // Do not move selection or lead item in list during rename.
2456     if (event.keyIdentifier == 'Up' || event.keyIdentifier == 'Down') {
2457       event.stopPropagation();
2458     }
2459
2460     switch (util.getKeyModifiers(event) + event.keyCode) {
2461       case '27':  // Escape
2462         this.cancelRename_();
2463         event.preventDefault();
2464         break;
2465
2466       case '13':  // Enter
2467         this.commitRename_();
2468         event.preventDefault();
2469         break;
2470     }
2471   };
2472
2473   /**
2474    * @type {Event} Blur event.
2475    * @private
2476    */
2477   FileManager.prototype.onRenameInputBlur_ = function(event) {
2478     if (this.isRenamingInProgress() && !this.renameInput_.validation_)
2479       this.commitRename_();
2480   };
2481
2482   /**
2483    * @private
2484    */
2485   FileManager.prototype.commitRename_ = function() {
2486     var input = this.renameInput_;
2487     var entry = input.currentEntry;
2488     var newName = input.value;
2489
2490     if (newName == entry.name) {
2491       this.cancelRename_();
2492       return;
2493     }
2494
2495     var nameNode = this.findListItemForNode_(this.renameInput_).
2496                    querySelector('.filename-label');
2497
2498     input.validation_ = true;
2499     var validationDone = function(valid) {
2500       input.validation_ = false;
2501       // Alert dialog restores focus unless the item removed from DOM.
2502       if (this.document_.activeElement != input)
2503         this.cancelRename_();
2504       if (!valid)
2505         return;
2506
2507       // Validation succeeded. Do renaming.
2508
2509       this.cancelRename_();
2510       // Optimistically apply new name immediately to avoid flickering in
2511       // case of success.
2512       nameNode.textContent = newName;
2513
2514       util.rename(
2515           entry, newName,
2516           function(newEntry) {
2517             this.directoryModel_.onRenameEntry(entry, newEntry);
2518           }.bind(this),
2519           function(error) {
2520             // Write back to the old name.
2521             nameNode.textContent = entry.name;
2522
2523             // Show error dialog.
2524             var message;
2525             if (error.name == util.FileError.PATH_EXISTS_ERR ||
2526                 error.name == util.FileError.TYPE_MISMATCH_ERR) {
2527               // Check the existing entry is file or not.
2528               // 1) If the entry is a file:
2529               //   a) If we get PATH_EXISTS_ERR, a file exists.
2530               //   b) If we get TYPE_MISMATCH_ERR, a directory exists.
2531               // 2) If the entry is a directory:
2532               //   a) If we get PATH_EXISTS_ERR, a directory exists.
2533               //   b) If we get TYPE_MISMATCH_ERR, a file exists.
2534               message = strf(
2535                   (entry.isFile && error.name ==
2536                       util.FileError.PATH_EXISTS_ERR) ||
2537                   (!entry.isFile && error.name ==
2538                       util.FileError.TYPE_MISMATCH_ERR) ?
2539                       'FILE_ALREADY_EXISTS' :
2540                       'DIRECTORY_ALREADY_EXISTS',
2541                   newName);
2542             } else {
2543               message = strf('ERROR_RENAMING', entry.name,
2544                              util.getFileErrorString(error.name));
2545             }
2546
2547             this.alert.show(message);
2548           }.bind(this));
2549     };
2550
2551     // TODO(haruki): this.getCurrentDirectoryEntry() might not return the actual
2552     // parent if the directory content is a search result. Fix it to do proper
2553     // validation.
2554     this.validateFileName_(this.getCurrentDirectoryEntry(),
2555                            newName,
2556                            validationDone.bind(this));
2557   };
2558
2559   /**
2560    * @private
2561    */
2562   FileManager.prototype.cancelRename_ = function() {
2563     this.renameInput_.currentEntry = null;
2564
2565     var parent = this.renameInput_.parentNode;
2566     if (parent) {
2567       parent.removeAttribute('renaming');
2568       parent.removeChild(this.renameInput_);
2569     }
2570   };
2571
2572   /**
2573    * @param {Event} Key event.
2574    * @private
2575    */
2576   FileManager.prototype.onFilenameInputInput_ = function() {
2577     this.selectionHandler_.updateOkButton();
2578   };
2579
2580   /**
2581    * @param {Event} Key event.
2582    * @private
2583    */
2584   FileManager.prototype.onFilenameInputKeyDown_ = function(event) {
2585     if ((util.getKeyModifiers(event) + event.keyCode) === '13' /* Enter */)
2586       this.okButton_.click();
2587   };
2588
2589   /**
2590    * @param {Event} Focus event.
2591    * @private
2592    */
2593   FileManager.prototype.onFilenameInputFocus_ = function(event) {
2594     var input = this.filenameInput_;
2595
2596     // On focus we want to select everything but the extension, but
2597     // Chrome will select-all after the focus event completes.  We
2598     // schedule a timeout to alter the focus after that happens.
2599     setTimeout(function() {
2600         var selectionEnd = input.value.lastIndexOf('.');
2601         if (selectionEnd == -1) {
2602           input.select();
2603         } else {
2604           input.selectionStart = 0;
2605           input.selectionEnd = selectionEnd;
2606         }
2607     }, 0);
2608   };
2609
2610   /**
2611    * @private
2612    */
2613   FileManager.prototype.onScanStarted_ = function() {
2614     if (this.scanInProgress_) {
2615       this.table_.list.endBatchUpdates();
2616       this.grid_.endBatchUpdates();
2617     }
2618
2619     if (this.commandHandler)
2620       this.commandHandler.updateAvailability();
2621     this.table_.list.startBatchUpdates();
2622     this.grid_.startBatchUpdates();
2623     this.scanInProgress_ = true;
2624
2625     this.scanUpdatedAtLeastOnceOrCompleted_ = false;
2626     if (this.scanCompletedTimer_) {
2627       clearTimeout(this.scanCompletedTimer_);
2628       this.scanCompletedTimer_ = null;
2629     }
2630
2631     if (this.scanUpdatedTimer_) {
2632       clearTimeout(this.scanUpdatedTimer_);
2633       this.scanUpdatedTimer_ = null;
2634     }
2635
2636     if (this.spinner_.hidden) {
2637       this.cancelSpinnerTimeout_();
2638       this.showSpinnerTimeout_ =
2639           setTimeout(this.showSpinner_.bind(this, true), 500);
2640     }
2641   };
2642
2643   /**
2644    * @private
2645    */
2646   FileManager.prototype.onScanCompleted_ = function() {
2647     if (!this.scanInProgress_) {
2648       console.error('Scan-completed event recieved. But scan is not started.');
2649       return;
2650     }
2651
2652     if (this.commandHandler)
2653       this.commandHandler.updateAvailability();
2654     this.hideSpinnerLater_();
2655
2656     if (this.scanUpdatedTimer_) {
2657       clearTimeout(this.scanUpdatedTimer_);
2658       this.scanUpdatedTimer_ = null;
2659     }
2660
2661     // To avoid flickering postpone updating the ui by a small amount of time.
2662     // There is a high chance, that metadata will be received within 50 ms.
2663     this.scanCompletedTimer_ = setTimeout(function() {
2664       // Check if batch updates are already finished by onScanUpdated_().
2665       if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
2666         this.scanUpdatedAtLeastOnceOrCompleted_ = true;
2667         this.updateMiddleBarVisibility_();
2668       }
2669
2670       this.scanInProgress_ = false;
2671       this.table_.list.endBatchUpdates();
2672       this.grid_.endBatchUpdates();
2673       this.scanCompletedTimer_ = null;
2674     }.bind(this), 50);
2675   };
2676
2677   /**
2678    * @private
2679    */
2680   FileManager.prototype.onScanUpdated_ = function() {
2681     if (!this.scanInProgress_) {
2682       console.error('Scan-updated event recieved. But scan is not started.');
2683       return;
2684     }
2685
2686     if (this.scanUpdatedTimer_ || this.scanCompletedTimer_)
2687       return;
2688
2689     // Show contents incrementally by finishing batch updated, but only after
2690     // 200ms elapsed, to avoid flickering when it is not necessary.
2691     this.scanUpdatedTimer_ = setTimeout(function() {
2692       // We need to hide the spinner only once.
2693       if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
2694         this.scanUpdatedAtLeastOnceOrCompleted_ = true;
2695         this.hideSpinnerLater_();
2696         this.updateMiddleBarVisibility_();
2697       }
2698
2699       // Update the UI.
2700       if (this.scanInProgress_) {
2701         this.table_.list.endBatchUpdates();
2702         this.grid_.endBatchUpdates();
2703         this.table_.list.startBatchUpdates();
2704         this.grid_.startBatchUpdates();
2705       }
2706       this.scanUpdatedTimer_ = null;
2707     }.bind(this), 200);
2708   };
2709
2710   /**
2711    * @private
2712    */
2713   FileManager.prototype.onScanCancelled_ = function() {
2714     if (!this.scanInProgress_) {
2715       console.error('Scan-cancelled event recieved. But scan is not started.');
2716       return;
2717     }
2718
2719     if (this.commandHandler)
2720       this.commandHandler.updateAvailability();
2721     this.hideSpinnerLater_();
2722     if (this.scanCompletedTimer_) {
2723       clearTimeout(this.scanCompletedTimer_);
2724       this.scanCompletedTimer_ = null;
2725     }
2726     if (this.scanUpdatedTimer_) {
2727       clearTimeout(this.scanUpdatedTimer_);
2728       this.scanUpdatedTimer_ = null;
2729     }
2730     // Finish unfinished batch updates.
2731     if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
2732       this.scanUpdatedAtLeastOnceOrCompleted_ = true;
2733       this.updateMiddleBarVisibility_();
2734     }
2735
2736     this.scanInProgress_ = false;
2737     this.table_.list.endBatchUpdates();
2738     this.grid_.endBatchUpdates();
2739   };
2740
2741   /**
2742    * Handle the 'rescan-completed' from the DirectoryModel.
2743    * @private
2744    */
2745   FileManager.prototype.onRescanCompleted_ = function() {
2746     this.selectionHandler_.onFileSelectionChanged();
2747   };
2748
2749   /**
2750    * @private
2751    */
2752   FileManager.prototype.cancelSpinnerTimeout_ = function() {
2753     if (this.showSpinnerTimeout_) {
2754       clearTimeout(this.showSpinnerTimeout_);
2755       this.showSpinnerTimeout_ = null;
2756     }
2757   };
2758
2759   /**
2760    * @private
2761    */
2762   FileManager.prototype.hideSpinnerLater_ = function() {
2763     this.cancelSpinnerTimeout_();
2764     this.showSpinner_(false);
2765   };
2766
2767   /**
2768    * @param {boolean} on True to show, false to hide.
2769    * @private
2770    */
2771   FileManager.prototype.showSpinner_ = function(on) {
2772     if (on && this.directoryModel_ && this.directoryModel_.isScanning())
2773       this.spinner_.hidden = false;
2774
2775     if (!on && (!this.directoryModel_ ||
2776                 !this.directoryModel_.isScanning() ||
2777                 this.directoryModel_.getFileList().length != 0)) {
2778       this.spinner_.hidden = true;
2779     }
2780   };
2781
2782   FileManager.prototype.createNewFolder = function() {
2783     var defaultName = str('DEFAULT_NEW_FOLDER_NAME');
2784
2785     // Find a name that doesn't exist in the data model.
2786     var files = this.directoryModel_.getFileList();
2787     var hash = {};
2788     for (var i = 0; i < files.length; i++) {
2789       var name = files.item(i).name;
2790       // Filtering names prevents from conflicts with prototype's names
2791       // and '__proto__'.
2792       if (name.substring(0, defaultName.length) == defaultName)
2793         hash[name] = 1;
2794     }
2795
2796     var baseName = defaultName;
2797     var separator = '';
2798     var suffix = '';
2799     var index = '';
2800
2801     var advance = function() {
2802       separator = ' (';
2803       suffix = ')';
2804       index++;
2805     };
2806
2807     var current = function() {
2808       return baseName + separator + index + suffix;
2809     };
2810
2811     // Accessing hasOwnProperty is safe since hash properties filtered.
2812     while (hash.hasOwnProperty(current())) {
2813       advance();
2814     }
2815
2816     var self = this;
2817     var list = self.currentList_;
2818     var tryCreate = function() {
2819       self.directoryModel_.createDirectory(current(),
2820                                            onSuccess, onError);
2821     };
2822
2823     var onSuccess = function(entry) {
2824       metrics.recordUserAction('CreateNewFolder');
2825       list.selectedItem = entry;
2826       self.initiateRename();
2827     };
2828
2829     var onError = function(error) {
2830       self.alert.show(strf('ERROR_CREATING_FOLDER', current(),
2831                            util.getFileErrorString(error.name)));
2832     };
2833
2834     tryCreate();
2835   };
2836
2837   /**
2838    * @param {Event} event Click event.
2839    * @private
2840    */
2841   FileManager.prototype.onDetailViewButtonClick_ = function(event) {
2842     // Stop propagate and hide the menu manually, in order to prevent the focus
2843     // from being back to the button. (cf. http://crbug.com/248479)
2844     event.stopPropagation();
2845     this.gearButton_.hideMenu();
2846     this.gearButton_.blur();
2847     this.setListType(FileManager.ListType.DETAIL);
2848   };
2849
2850   /**
2851    * @param {Event} event Click event.
2852    * @private
2853    */
2854   FileManager.prototype.onThumbnailViewButtonClick_ = function(event) {
2855     // Stop propagate and hide the menu manually, in order to prevent the focus
2856     // from being back to the button. (cf. http://crbug.com/248479)
2857     event.stopPropagation();
2858     this.gearButton_.hideMenu();
2859     this.gearButton_.blur();
2860     this.setListType(FileManager.ListType.THUMBNAIL);
2861   };
2862
2863   /**
2864    * KeyDown event handler for the document.
2865    * @param {Event} event Key event.
2866    * @private
2867    */
2868   FileManager.prototype.onKeyDown_ = function(event) {
2869     if (event.srcElement === this.renameInput_) {
2870       // Ignore keydown handler in the rename input box.
2871       return;
2872     }
2873
2874     switch (util.getKeyModifiers(event) + event.keyCode) {
2875       case 'Ctrl-190':  // Ctrl-. => Toggle filter files.
2876         this.fileFilter_.setFilterHidden(
2877             !this.fileFilter_.isFilterHiddenOn());
2878         event.preventDefault();
2879         return;
2880
2881       case '27':  // Escape => Cancel dialog.
2882         if (this.dialogType != DialogType.FULL_PAGE) {
2883           // If there is nothing else for ESC to do, then cancel the dialog.
2884           event.preventDefault();
2885           this.cancelButton_.click();
2886         }
2887         break;
2888     }
2889   };
2890
2891   /**
2892    * KeyDown event handler for the div#list-container element.
2893    * @param {Event} event Key event.
2894    * @private
2895    */
2896   FileManager.prototype.onListKeyDown_ = function(event) {
2897     if (event.srcElement.tagName == 'INPUT') {
2898       // Ignore keydown handler in the rename input box.
2899       return;
2900     }
2901
2902     switch (util.getKeyModifiers(event) + event.keyCode) {
2903       case '8':  // Backspace => Up one directory.
2904         event.preventDefault();
2905         // TODO(mtomasz): Use Entry.getParent() instead.
2906         if (!this.getCurrentDirectoryEntry())
2907           break;
2908         var currentEntry = this.getCurrentDirectoryEntry();
2909         var locationInfo = this.volumeManager_.getLocationInfo(currentEntry);
2910         // TODO(mtomasz): There may be a tiny race in here.
2911         if (locationInfo && !locationInfo.isRootEntry &&
2912             !locationInfo.isSpecialSearchRoot) {
2913           currentEntry.getParent(function(parentEntry) {
2914             this.directoryModel_.changeDirectoryEntry(parentEntry);
2915           }.bind(this), function() { /* Ignore errors. */});
2916         }
2917         break;
2918
2919       case '13':  // Enter => Change directory or perform default action.
2920         // TODO(dgozman): move directory action to dispatchSelectionAction.
2921         var selection = this.getSelection();
2922         if (selection.totalCount == 1 &&
2923             selection.entries[0].isDirectory &&
2924             !DialogType.isFolderDialog(this.dialogType)) {
2925           event.preventDefault();
2926           this.onDirectoryAction_(selection.entries[0]);
2927         } else if (this.dispatchSelectionAction_()) {
2928           event.preventDefault();
2929         }
2930         break;
2931     }
2932
2933     switch (event.keyIdentifier) {
2934       case 'Home':
2935       case 'End':
2936       case 'Up':
2937       case 'Down':
2938       case 'Left':
2939       case 'Right':
2940         // When navigating with keyboard we hide the distracting mouse hover
2941         // highlighting until the user moves the mouse again.
2942         this.setNoHover_(true);
2943         break;
2944     }
2945   };
2946
2947   /**
2948    * Suppress/restore hover highlighting in the list container.
2949    * @param {boolean} on True to temporarity hide hover state.
2950    * @private
2951    */
2952   FileManager.prototype.setNoHover_ = function(on) {
2953     if (on) {
2954       this.listContainer_.classList.add('nohover');
2955     } else {
2956       this.listContainer_.classList.remove('nohover');
2957     }
2958   };
2959
2960   /**
2961    * KeyPress event handler for the div#list-container element.
2962    * @param {Event} event Key event.
2963    * @private
2964    */
2965   FileManager.prototype.onListKeyPress_ = function(event) {
2966     if (event.srcElement.tagName == 'INPUT') {
2967       // Ignore keypress handler in the rename input box.
2968       return;
2969     }
2970
2971     if (event.ctrlKey || event.metaKey || event.altKey)
2972       return;
2973
2974     var now = new Date();
2975     var char = String.fromCharCode(event.charCode).toLowerCase();
2976     var text = now - this.textSearchState_.date > 1000 ? '' :
2977         this.textSearchState_.text;
2978     this.textSearchState_ = {text: text + char, date: now};
2979
2980     this.doTextSearch_();
2981   };
2982
2983   /**
2984    * Mousemove event handler for the div#list-container element.
2985    * @param {Event} event Mouse event.
2986    * @private
2987    */
2988   FileManager.prototype.onListMouseMove_ = function(event) {
2989     // The user grabbed the mouse, restore the hover highlighting.
2990     this.setNoHover_(false);
2991   };
2992
2993   /**
2994    * Performs a 'text search' - selects a first list entry with name
2995    * starting with entered text (case-insensitive).
2996    * @private
2997    */
2998   FileManager.prototype.doTextSearch_ = function() {
2999     var text = this.textSearchState_.text;
3000     if (!text)
3001       return;
3002
3003     var dm = this.directoryModel_.getFileList();
3004     for (var index = 0; index < dm.length; ++index) {
3005       var name = dm.item(index).name;
3006       if (name.substring(0, text.length).toLowerCase() == text) {
3007         this.currentList_.selectionModel.selectedIndexes = [index];
3008         return;
3009       }
3010     }
3011
3012     this.textSearchState_.text = '';
3013   };
3014
3015   /**
3016    * Handle a click of the cancel button.  Closes the window.
3017    * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
3018    *
3019    * @param {Event} event The click event.
3020    * @private
3021    */
3022   FileManager.prototype.onCancel_ = function(event) {
3023     chrome.fileBrowserPrivate.cancelDialog();
3024     this.onUnload_();
3025     window.close();
3026   };
3027
3028   /**
3029    * Resolves selected file urls returned from an Open dialog.
3030    *
3031    * For drive files this involves some special treatment.
3032    * Starts getting drive files if needed.
3033    *
3034    * @param {Array.<string>} fileUrls Drive URLs.
3035    * @param {function(Array.<string>)} callback To be called with fixed URLs.
3036    * @private
3037    */
3038   FileManager.prototype.resolveSelectResults_ = function(fileUrls, callback) {
3039     if (this.isOnDrive()) {
3040       chrome.fileBrowserPrivate.getDriveFiles(
3041         fileUrls,
3042         function(localPaths) {
3043           callback(fileUrls);
3044         });
3045     } else {
3046       callback(fileUrls);
3047     }
3048   };
3049
3050   /**
3051    * Closes this modal dialog with some files selected.
3052    * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
3053    * @param {Object} selection Contains urls, filterIndex and multiple fields.
3054    * @private
3055    */
3056   FileManager.prototype.callSelectFilesApiAndClose_ = function(selection) {
3057     var self = this;
3058     function callback() {
3059       self.onUnload_();
3060       window.close();
3061     }
3062     if (selection.multiple) {
3063       chrome.fileBrowserPrivate.selectFiles(
3064           selection.urls, this.params_.shouldReturnLocalPath, callback);
3065     } else {
3066       var forOpening = (this.dialogType != DialogType.SELECT_SAVEAS_FILE);
3067       chrome.fileBrowserPrivate.selectFile(
3068           selection.urls[0], selection.filterIndex, forOpening,
3069           this.params_.shouldReturnLocalPath, callback);
3070     }
3071   };
3072
3073   /**
3074    * Tries to close this modal dialog with some files selected.
3075    * Performs preprocessing if needed (e.g. for Drive).
3076    * @param {Object} selection Contains urls, filterIndex and multiple fields.
3077    * @private
3078    */
3079   FileManager.prototype.selectFilesAndClose_ = function(selection) {
3080     if (!this.isOnDrive() ||
3081         this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
3082       setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0);
3083       return;
3084     }
3085
3086     var shade = this.document_.createElement('div');
3087     shade.className = 'shade';
3088     var footer = this.dialogDom_.querySelector('.button-panel');
3089     var progress = footer.querySelector('.progress-track');
3090     progress.style.width = '0%';
3091     var cancelled = false;
3092
3093     var progressMap = {};
3094     var filesStarted = 0;
3095     var filesTotal = selection.urls.length;
3096     for (var index = 0; index < selection.urls.length; index++) {
3097       progressMap[selection.urls[index]] = -1;
3098     }
3099     var lastPercent = 0;
3100     var bytesTotal = 0;
3101     var bytesDone = 0;
3102
3103     var onFileTransfersUpdated = function(statusList) {
3104       for (var index = 0; index < statusList.length; index++) {
3105         var status = statusList[index];
3106         var escaped = encodeURI(status.fileUrl);
3107         if (!(escaped in progressMap)) continue;
3108         if (status.total == -1) continue;
3109
3110         var old = progressMap[escaped];
3111         if (old == -1) {
3112           // -1 means we don't know file size yet.
3113           bytesTotal += status.total;
3114           filesStarted++;
3115           old = 0;
3116         }
3117         bytesDone += status.processed - old;
3118         progressMap[escaped] = status.processed;
3119       }
3120
3121       var percent = bytesTotal == 0 ? 0 : bytesDone / bytesTotal;
3122       // For files we don't have information about, assume the progress is zero.
3123       percent = percent * filesStarted / filesTotal * 100;
3124       // Do not decrease the progress. This may happen, if first downloaded
3125       // file is small, and the second one is large.
3126       lastPercent = Math.max(lastPercent, percent);
3127       progress.style.width = lastPercent + '%';
3128     }.bind(this);
3129
3130     var setup = function() {
3131       this.document_.querySelector('.dialog-container').appendChild(shade);
3132       setTimeout(function() { shade.setAttribute('fadein', 'fadein') }, 100);
3133       footer.setAttribute('progress', 'progress');
3134       this.cancelButton_.removeEventListener('click', this.onCancelBound_);
3135       this.cancelButton_.addEventListener('click', onCancel);
3136       chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener(
3137           onFileTransfersUpdated);
3138     }.bind(this);
3139
3140     var cleanup = function() {
3141       shade.parentNode.removeChild(shade);
3142       footer.removeAttribute('progress');
3143       this.cancelButton_.removeEventListener('click', onCancel);
3144       this.cancelButton_.addEventListener('click', this.onCancelBound_);
3145       chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener(
3146           onFileTransfersUpdated);
3147     }.bind(this);
3148
3149     var onCancel = function() {
3150       cancelled = true;
3151       // According to API cancel may fail, but there is no proper UI to reflect
3152       // this. So, we just silently assume that everything is cancelled.
3153       chrome.fileBrowserPrivate.cancelFileTransfers(
3154           selection.urls, function(response) {});
3155       cleanup();
3156     }.bind(this);
3157
3158     var onResolved = function(resolvedUrls) {
3159       if (cancelled) return;
3160       cleanup();
3161       selection.urls = resolvedUrls;
3162       // Call next method on a timeout, as it's unsafe to
3163       // close a window from a callback.
3164       setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0);
3165     }.bind(this);
3166
3167     var onProperties = function(properties) {
3168       for (var i = 0; i < properties.length; i++) {
3169         if (!properties[i] || properties[i].present) {
3170           // For files already in GCache, we don't get any transfer updates.
3171           filesTotal--;
3172         }
3173       }
3174       this.resolveSelectResults_(selection.urls, onResolved);
3175     }.bind(this);
3176
3177     setup();
3178
3179     // TODO(mtomasz): Use Entry instead of URLs, if possible.
3180     util.URLsToEntries(selection.urls, function(entries) {
3181       this.metadataCache_.get(entries, 'drive', onProperties);
3182     }.bind(this));
3183   };
3184
3185   /**
3186    * Handle a click of the ok button.
3187    *
3188    * The ok button has different UI labels depending on the type of dialog, but
3189    * in code it's always referred to as 'ok'.
3190    *
3191    * @param {Event} event The click event.
3192    * @private
3193    */
3194   FileManager.prototype.onOk_ = function(event) {
3195     if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
3196       // Save-as doesn't require a valid selection from the list, since
3197       // we're going to take the filename from the text input.
3198       var filename = this.filenameInput_.value;
3199       if (!filename)
3200         throw new Error('Missing filename!');
3201
3202       var directory = this.getCurrentDirectoryEntry();
3203       this.validateFileName_(directory, filename, function(isValid) {
3204         if (!isValid)
3205           return;
3206
3207         if (util.isFakeEntry(directory)) {
3208           // Can't save a file into a fake directory.
3209           return;
3210         }
3211
3212         var selectFileAndClose = function() {
3213           // TODO(mtomasz): Clean this up by avoiding constructing a URL
3214           //                via string concatenation.
3215           var currentDirUrl = directory.toURL();
3216           if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/')
3217           currentDirUrl += '/';
3218           this.selectFilesAndClose_({
3219             urls: [currentDirUrl + encodeURIComponent(filename)],
3220             multiple: false,
3221             filterIndex: this.getSelectedFilterIndex_(filename)
3222           });
3223         }.bind(this);
3224
3225         directory.getFile(
3226             filename, {create: false},
3227             function(entry) {
3228               // An existing file is found. Show confirmation dialog to
3229               // overwrite it. If the user select "OK" on the dialog, save it.
3230               this.confirm.show(strf('CONFIRM_OVERWRITE_FILE', filename),
3231                                 selectFileAndClose);
3232             }.bind(this),
3233             function(error) {
3234               if (error.name == util.FileError.NOT_FOUND_ERR) {
3235                 // The file does not exist, so it should be ok to create a
3236                 // new file.
3237                 selectFileAndClose();
3238                 return;
3239               }
3240               if (error.name == util.FileError.TYPE_MISMATCH_ERR) {
3241                 // An directory is found.
3242                 // Do not allow to overwrite directory.
3243                 this.alert.show(strf('DIRECTORY_ALREADY_EXISTS', filename));
3244                 return;
3245               }
3246
3247               // Unexpected error.
3248               console.error('File save failed: ' + error.code);
3249             }.bind(this));
3250       }.bind(this));
3251       return;
3252     }
3253
3254     var files = [];
3255     var selectedIndexes = this.currentList_.selectionModel.selectedIndexes;
3256
3257     if (DialogType.isFolderDialog(this.dialogType) &&
3258         selectedIndexes.length == 0) {
3259       var url = this.getCurrentDirectoryEntry().toURL();
3260       var singleSelection = {
3261         urls: [url],
3262         multiple: false,
3263         filterIndex: this.getSelectedFilterIndex_()
3264       };
3265       this.selectFilesAndClose_(singleSelection);
3266       return;
3267     }
3268
3269     // All other dialog types require at least one selected list item.
3270     // The logic to control whether or not the ok button is enabled should
3271     // prevent us from ever getting here, but we sanity check to be sure.
3272     if (!selectedIndexes.length)
3273       throw new Error('Nothing selected!');
3274
3275     var dm = this.directoryModel_.getFileList();
3276     for (var i = 0; i < selectedIndexes.length; i++) {
3277       var entry = dm.item(selectedIndexes[i]);
3278       if (!entry) {
3279         console.error('Error locating selected file at index: ' + i);
3280         continue;
3281       }
3282
3283       files.push(entry.toURL());
3284     }
3285
3286     // Multi-file selection has no other restrictions.
3287     if (this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE) {
3288       var multipleSelection = {
3289         urls: files,
3290         multiple: true
3291       };
3292       this.selectFilesAndClose_(multipleSelection);
3293       return;
3294     }
3295
3296     // Everything else must have exactly one.
3297     if (files.length > 1)
3298       throw new Error('Too many files selected!');
3299
3300     var selectedEntry = dm.item(selectedIndexes[0]);
3301
3302     if (DialogType.isFolderDialog(this.dialogType)) {
3303       if (!selectedEntry.isDirectory)
3304         throw new Error('Selected entry is not a folder!');
3305     } else if (this.dialogType == DialogType.SELECT_OPEN_FILE) {
3306       if (!selectedEntry.isFile)
3307         throw new Error('Selected entry is not a file!');
3308     }
3309
3310     var singleSelection = {
3311       urls: [files[0]],
3312       multiple: false,
3313       filterIndex: this.getSelectedFilterIndex_()
3314     };
3315     this.selectFilesAndClose_(singleSelection);
3316   };
3317
3318   /**
3319    * Verifies the user entered name for file or folder to be created or
3320    * renamed to. Name restrictions must correspond to File API restrictions
3321    * (see DOMFilePath::isValidPath). Curernt WebKit implementation is
3322    * out of date (spec is
3323    * http://dev.w3.org/2009/dap/file-system/file-dir-sys.html, 8.3) and going to
3324    * be fixed. Shows message box if the name is invalid.
3325    *
3326    * It also verifies if the name length is in the limit of the filesystem.
3327    *
3328    * @param {DirectoryEntry} parentEntry The URL of the parent directory entry.
3329    * @param {string} name New file or folder name.
3330    * @param {function} onDone Function to invoke when user closes the
3331    *    warning box or immediatelly if file name is correct. If the name was
3332    *    valid it is passed true, and false otherwise.
3333    * @private
3334    */
3335   FileManager.prototype.validateFileName_ = function(
3336       parentEntry, name, onDone) {
3337     var msg;
3338     var testResult = /[\/\\\<\>\:\?\*\"\|]/.exec(name);
3339     if (testResult) {
3340       msg = strf('ERROR_INVALID_CHARACTER', testResult[0]);
3341     } else if (/^\s*$/i.test(name)) {
3342       msg = str('ERROR_WHITESPACE_NAME');
3343     } else if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(name)) {
3344       msg = str('ERROR_RESERVED_NAME');
3345     } else if (this.fileFilter_.isFilterHiddenOn() && name[0] == '.') {
3346       msg = str('ERROR_HIDDEN_NAME');
3347     }
3348
3349     if (msg) {
3350       this.alert.show(msg, function() {
3351         onDone(false);
3352       });
3353       return;
3354     }
3355
3356     var self = this;
3357     chrome.fileBrowserPrivate.validatePathNameLength(
3358         parentEntry.toURL(), name, function(valid) {
3359           if (!valid) {
3360             self.alert.show(str('ERROR_LONG_NAME'),
3361                             function() { onDone(false); });
3362           } else {
3363             onDone(true);
3364           }
3365         });
3366   };
3367
3368   /**
3369    * Handler invoked on preference setting in drive context menu.
3370    *
3371    * @param {string} pref  The preference to alter.
3372    * @param {boolean} inverted Invert the value if true.
3373    * @param {Event}  event The click event.
3374    * @private
3375    */
3376   FileManager.prototype.onDrivePrefClick_ = function(pref, inverted, event) {
3377     var newValue = !event.target.hasAttribute('checked');
3378     if (newValue)
3379       event.target.setAttribute('checked', 'checked');
3380     else
3381       event.target.removeAttribute('checked');
3382
3383     var changeInfo = {};
3384     changeInfo[pref] = inverted ? !newValue : newValue;
3385     chrome.fileBrowserPrivate.setPreferences(changeInfo);
3386   };
3387
3388   /**
3389    * Invoked when the search box is changed.
3390    *
3391    * @param {Event} event The changed event.
3392    * @private
3393    */
3394   FileManager.prototype.onSearchBoxUpdate_ = function(event) {
3395     var searchString = this.searchBox_.value;
3396
3397     if (this.isOnDrive()) {
3398       // When the search text is changed, finishes the search and showes back
3399       // the last directory by passing an empty string to
3400       // {@code DirectoryModel.search()}.
3401       if (this.directoryModel_.isSearching() &&
3402           this.lastSearchQuery_ != searchString) {
3403         this.doSearch('');
3404       }
3405
3406       // On drive, incremental search is not invoked since we have an auto-
3407       // complete suggestion instead.
3408       return;
3409     }
3410
3411     this.search_(searchString);
3412   };
3413
3414   /**
3415    * Handle the search clear button click.
3416    * @private
3417    */
3418   FileManager.prototype.onSearchClearButtonClick_ = function() {
3419     this.ui_.searchBox.clear();
3420     this.onSearchBoxUpdate_();
3421   };
3422
3423   /**
3424    * Search files and update the list with the search result.
3425    *
3426    * @param {string} searchString String to be searched with.
3427    * @private
3428    */
3429   FileManager.prototype.search_ = function(searchString) {
3430     var noResultsDiv = this.document_.getElementById('no-search-results');
3431
3432     var reportEmptySearchResults = function() {
3433       if (this.directoryModel_.getFileList().length === 0) {
3434         // The string 'SEARCH_NO_MATCHING_FILES_HTML' may contain HTML tags,
3435         // hence we escapes |searchString| here.
3436         var html = strf('SEARCH_NO_MATCHING_FILES_HTML',
3437                         util.htmlEscape(searchString));
3438         noResultsDiv.innerHTML = html;
3439         noResultsDiv.setAttribute('show', 'true');
3440       } else {
3441         noResultsDiv.removeAttribute('show');
3442       }
3443     };
3444
3445     var hideNoResultsDiv = function() {
3446       noResultsDiv.removeAttribute('show');
3447     };
3448
3449     this.doSearch(searchString,
3450                   reportEmptySearchResults.bind(this),
3451                   hideNoResultsDiv.bind(this));
3452   };
3453
3454   /**
3455    * Performs search and displays results.
3456    *
3457    * @param {string} query Query that will be searched for.
3458    * @param {function()=} opt_onSearchRescan Function that will be called when
3459    *     the search directory is rescanned (i.e. search results are displayed).
3460    * @param {function()=} opt_onClearSearch Function to be called when search
3461    *     state gets cleared.
3462    */
3463   FileManager.prototype.doSearch = function(
3464       searchString, opt_onSearchRescan, opt_onClearSearch) {
3465     var onSearchRescan = opt_onSearchRescan || function() {};
3466     var onClearSearch = opt_onClearSearch || function() {};
3467
3468     this.lastSearchQuery_ = searchString;
3469     this.directoryModel_.search(searchString, onSearchRescan, onClearSearch);
3470   };
3471
3472   /**
3473    * Requests autocomplete suggestions for files on Drive.
3474    * Once the suggestions are returned, the autocomplete popup will show up.
3475    *
3476    * @param {string} query The text to autocomplete from.
3477    * @private
3478    */
3479   FileManager.prototype.requestAutocompleteSuggestions_ = function(query) {
3480     query = query.trimLeft();
3481
3482     // Only Drive supports auto-compelete
3483     if (!this.isOnDrive())
3484       return;
3485
3486     // Remember the most recent query. If there is an other request in progress,
3487     // then it's result will be discarded and it will call a new request for
3488     // this query.
3489     this.lastAutocompleteQuery_ = query;
3490     if (this.autocompleteSuggestionsBusy_)
3491       return;
3492
3493     // The autocomplete list should be resized and repositioned here as the
3494     // search box is resized when it's focused.
3495     this.autocompleteList_.syncWidthAndPositionToInput();
3496
3497     if (!query) {
3498       this.autocompleteList_.suggestions = [];
3499       return;
3500     }
3501
3502     var headerItem = {isHeaderItem: true, searchQuery: query};
3503     if (!this.autocompleteList_.dataModel ||
3504         this.autocompleteList_.dataModel.length == 0)
3505       this.autocompleteList_.suggestions = [headerItem];
3506     else
3507       // Updates only the head item to prevent a flickering on typing.
3508       this.autocompleteList_.dataModel.splice(0, 1, headerItem);
3509
3510     this.autocompleteSuggestionsBusy_ = true;
3511
3512     var searchParams = {
3513       'query': query,
3514       'types': 'ALL',
3515       'maxResults': 4
3516     };
3517     chrome.fileBrowserPrivate.searchDriveMetadata(
3518       searchParams,
3519       function(suggestions) {
3520         this.autocompleteSuggestionsBusy_ = false;
3521
3522         // Discard results for previous requests and fire a new search
3523         // for the most recent query.
3524         if (query != this.lastAutocompleteQuery_) {
3525           this.requestAutocompleteSuggestions_(this.lastAutocompleteQuery_);
3526           return;
3527         }
3528
3529         // Keeps the items in the suggestion list.
3530         this.autocompleteList_.suggestions = [headerItem].concat(suggestions);
3531       }.bind(this));
3532   };
3533
3534   /**
3535    * Opens the currently selected suggestion item.
3536    * @private
3537    */
3538   FileManager.prototype.openAutocompleteSuggestion_ = function() {
3539     var selectedItem = this.autocompleteList_.selectedItem;
3540
3541     // If the entry is the search item or no entry is selected, just change to
3542     // the search result.
3543     if (!selectedItem || selectedItem.isHeaderItem) {
3544       var query = selectedItem ?
3545           selectedItem.searchQuery : this.searchBox_.value;
3546       this.search_(query);
3547       return;
3548     }
3549
3550     var entry = selectedItem.entry;
3551     // If the entry is a directory, just change the directory.
3552     if (entry.isDirectory) {
3553       this.onDirectoryAction_(entry);
3554       return;
3555     }
3556
3557     var entries = [entry];
3558     var self = this;
3559
3560     // To open a file, first get the mime type.
3561     this.metadataCache_.get(entries, 'drive', function(props) {
3562       var mimeType = props[0].contentMimeType || '';
3563       var mimeTypes = [mimeType];
3564       var openIt = function() {
3565         if (self.dialogType == DialogType.FULL_PAGE) {
3566           var tasks = new FileTasks(self);
3567           tasks.init(entries, mimeTypes);
3568           tasks.executeDefault();
3569         } else {
3570           self.onOk_();
3571         }
3572       };
3573
3574       // Change the current directory to the directory that contains the
3575       // selected file. Note that this is necessary for an image or a video,
3576       // which should be opened in the gallery mode, as the gallery mode
3577       // requires the entry to be in the current directory model. For
3578       // consistency, the current directory is always changed regardless of
3579       // the file type.
3580       entry.getParent(function(parentEntry) {
3581         var onDirectoryChanged = function(event) {
3582           self.directoryModel_.removeEventListener('scan-completed',
3583                                                    onDirectoryChanged);
3584           self.directoryModel_.selectEntry(entry);
3585           openIt();
3586         };
3587         // changeDirectoryEntry() returns immediately. We should wait until the
3588         // directory scan is complete.
3589         self.directoryModel_.addEventListener('scan-completed',
3590                                               onDirectoryChanged);
3591         self.directoryModel_.changeDirectoryEntry(
3592           parentEntry,
3593           function() {
3594             // Remove the listner if the change directory failed.
3595             self.directoryModel_.removeEventListener('scan-completed',
3596                                                      onDirectoryChanged);
3597           });
3598       });
3599     });
3600   };
3601
3602   FileManager.prototype.decorateSplitter = function(splitterElement) {
3603     var self = this;
3604
3605     var Splitter = cr.ui.Splitter;
3606
3607     var customSplitter = cr.ui.define('div');
3608
3609     customSplitter.prototype = {
3610       __proto__: Splitter.prototype,
3611
3612       handleSplitterDragStart: function(e) {
3613         Splitter.prototype.handleSplitterDragStart.apply(this, arguments);
3614         this.ownerDocument.documentElement.classList.add('col-resize');
3615       },
3616
3617       handleSplitterDragMove: function(deltaX) {
3618         Splitter.prototype.handleSplitterDragMove.apply(this, arguments);
3619         self.onResize_();
3620       },
3621
3622       handleSplitterDragEnd: function(e) {
3623         Splitter.prototype.handleSplitterDragEnd.apply(this, arguments);
3624         this.ownerDocument.documentElement.classList.remove('col-resize');
3625       }
3626     };
3627
3628     customSplitter.decorate(splitterElement);
3629   };
3630
3631   /**
3632    * Updates default action menu item to match passed taskItem (icon,
3633    * label and action).
3634    *
3635    * @param {Object} defaultItem - taskItem to match.
3636    * @param {boolean} isMultiple - if multiple tasks available.
3637    */
3638   FileManager.prototype.updateContextMenuActionItems = function(defaultItem,
3639                                                                 isMultiple) {
3640     if (defaultItem) {
3641       if (defaultItem.iconType) {
3642         this.defaultActionMenuItem_.style.backgroundImage = '';
3643         this.defaultActionMenuItem_.setAttribute('file-type-icon',
3644                                                  defaultItem.iconType);
3645       } else if (defaultItem.iconUrl) {
3646         this.defaultActionMenuItem_.style.backgroundImage =
3647             'url(' + defaultItem.iconUrl + ')';
3648       } else {
3649         this.defaultActionMenuItem_.style.backgroundImage = '';
3650       }
3651
3652       this.defaultActionMenuItem_.label = defaultItem.title;
3653       this.defaultActionMenuItem_.disabled = !!defaultItem.disabled;
3654       this.defaultActionMenuItem_.taskId = defaultItem.taskId;
3655     }
3656
3657     var defaultActionSeparator =
3658         this.dialogDom_.querySelector('#default-action-separator');
3659
3660     this.openWithCommand_.canExecuteChange();
3661     this.openWithCommand_.setHidden(!(defaultItem && isMultiple));
3662     this.openWithCommand_.disabled = defaultItem && !!defaultItem.disabled;
3663
3664     this.defaultActionMenuItem_.hidden = !defaultItem;
3665     defaultActionSeparator.hidden = !defaultItem;
3666   };
3667
3668   /**
3669    * Window beforeunload handler.
3670    * @return {string} Message to show. Ignored when running as a packaged app.
3671    * @private
3672    */
3673   FileManager.prototype.onBeforeUnload_ = function() {
3674     if (this.filePopup_ &&
3675         this.filePopup_.contentWindow &&
3676         this.filePopup_.contentWindow.beforeunload) {
3677       // The gallery might want to prevent the unload if it is busy.
3678       return this.filePopup_.contentWindow.beforeunload();
3679     }
3680     return null;
3681   };
3682
3683   /**
3684    * @return {FileSelection} Selection object.
3685    */
3686   FileManager.prototype.getSelection = function() {
3687     return this.selectionHandler_.selection;
3688   };
3689
3690   /**
3691    * @return {ArrayDataModel} File list.
3692    */
3693   FileManager.prototype.getFileList = function() {
3694     return this.directoryModel_.getFileList();
3695   };
3696
3697   /**
3698    * @return {cr.ui.List} Current list object.
3699    */
3700   FileManager.prototype.getCurrentList = function() {
3701     return this.currentList_;
3702   };
3703
3704   /**
3705    * Retrieve the preferences of the files.app. This method caches the result
3706    * and returns it unless opt_update is true.
3707    * @param {function(Object.<string, *>)} callback Callback to get the
3708    *     preference.
3709    * @param {boolean=} opt_update If is's true, don't use the cache and
3710    *     retrieve latest preference. Default is false.
3711    * @private
3712    */
3713   FileManager.prototype.getPreferences_ = function(callback, opt_update) {
3714     if (!opt_update && this.preferences_ !== undefined) {
3715       callback(this.preferences_);
3716       return;
3717     }
3718
3719     chrome.fileBrowserPrivate.getPreferences(function(prefs) {
3720       this.preferences_ = prefs;
3721       callback(prefs);
3722     }.bind(this));
3723   };
3724 })();