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