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