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.
6 * FileManager constructor.
8 * FileManager objects encapsulate the functionality of the file selector
9 * dialogs, as well as the full screen file manager application (though the
10 * latter is not yet implemented).
15 function FileManager() {
16 // --------------------------------------------------------------------------
17 // Services FileManager depends on.
21 * @type {VolumeManagerWrapper}
24 this.volumeManager_ = null;
28 * @type {MetadataCache}
31 this.metadataCache_ = null;
34 * File operation manager.
35 * @type {FileOperationManager}
38 this.fileOperationManager_ = null;
41 * File transfer controller.
42 * @type {FileTransferController}
45 this.fileTransferController_ = null;
52 this.fileFilter_ = null;
59 this.fileWatcher_ = null;
62 * Model of current directory.
63 * @type {DirectoryModel}
66 this.directoryModel_ = null;
69 * Model of folder shortcuts.
70 * @type {FolderShortcutsDataModel}
73 this.folderShortcutsModel_ = null;
76 * VolumeInfo of the current volume.
80 this.currentVolumeInfo_ = null;
83 * Handler for command events.
84 * @type {CommandHandler}
86 this.commandHandler = null;
89 * Handler for the change of file selection.
90 * @type {FileSelectionHandler}
93 this.selectionHandler_ = null;
96 * Dialog action controller.
97 * @type {DialogActionController}
100 this.dialogActionController_ = null;
102 // --------------------------------------------------------------------------
103 // Parameters determining the type of file manager.
106 * Dialog type of this window.
109 this.dialogType = DialogType.FULL_PAGE;
112 * List of acceptable file types for open dialog.
113 * @type {!Array.<Object>}
116 this.fileTypes_ = [];
119 * Startup parameters for this application.
120 * @type {?{includeAllFiles:boolean,
122 * shouldReturnLocalPath:boolean}}
128 * Startup preference about the view.
132 this.viewOptions_ = {};
135 * The user preference.
139 this.preferences_ = null;
141 // --------------------------------------------------------------------------
145 * UI management class of file manager.
146 * @type {FileManagerUI}
152 * Progress center panel.
153 * @type {ProgressCenterPanel}
156 this.progressCenterPanel_ = null;
160 * @type {DirectoryTree}
163 this.directoryTree_ = null;
167 * @type {NamingController}
170 this.namingController_ = null;
173 * Controller for search UI.
174 * @type {SearchController}
177 this.searchController_ = null;
180 * Controller for directory scan.
181 * @type {ScanController}
184 this.scanController_ = null;
187 * Controller for spinner.
188 * @type {SpinnerController}
191 this.spinnerController_ = null;
194 * Banners in the file list.
195 * @type {FileListBannerController}
198 this.bannersController_ = null;
200 // --------------------------------------------------------------------------
205 * @type {ErrorDialog}
211 * @type {cr.ui.dialogs.AlertDialog}
217 * @type {cr.ui.dialogs.ConfirmDialog}
223 * @type {cr.ui.dialogs.PromptDialog}
229 * @type {ShareDialog}
232 this.shareDialog_ = null;
235 * Default task picker.
236 * @type {cr.filebrowser.DefaultActionDialog}
238 this.defaultTaskPicker = null;
241 * Suggest apps dialog.
242 * @type {SuggestAppsDialog}
244 this.suggestAppsDialog = null;
246 // --------------------------------------------------------------------------
250 * Context menu for texts.
254 this.textContextMenu_ = null;
256 // --------------------------------------------------------------------------
261 * @type {BackgroundWindow}
264 this.backgroundPage_ = null;
267 * The root DOM element of this app.
268 * @type {HTMLBodyElement}
271 this.dialogDom_ = null;
274 * The document object of this app.
275 * @type {HTMLDocument}
278 this.document_ = null;
281 * The menu item to toggle "Do not use mobile data for sync".
282 * @type {HTMLMenuItemElement}
284 this.syncButton = null;
287 * The menu item to toggle "Show Google Docs files".
288 * @type {HTMLMenuItemElement}
290 this.hostedButton = null;
293 * The menu item for doing an action.
294 * @type {HTMLMenuItemElement}
297 this.actionMenuItem_ = null;
300 * The button to open gear menu.
301 * @type {cr.ui.MenuButton}
304 this.gearButton_ = null;
307 * The combo button to specify the task.
308 * @type {HTMLButtonElement}
311 this.taskItems_ = null;
314 * The container element of the dialog.
315 * @type {HTMLDivElement}
318 this.dialogContainer_ = null;
321 * Open-with command in the context menu.
322 * @type {cr.ui.Command}
325 this.openWithCommand_ = null;
327 // --------------------------------------------------------------------------
331 * Bound function for onCopyProgress_.
332 * @type {?function(this:FileManager, Event)}
335 this.onCopyProgressBound_ = null;
338 * Bound function for onEntriesChanged_.
339 * @type {?function(this:FileManager, Event)}
342 this.onEntriesChangedBound_ = null;
344 // --------------------------------------------------------------------------
345 // Miscellaneous FileManager's states.
348 * Queue for ordering FileManager's initialization process.
349 * @type {AsyncUtil.Group}
352 this.initializeQueue_ = new AsyncUtil.Group();
355 * True while a user is pressing <Tab>.
356 * This is used for identifying the trigger causing the filelist to
361 this.pressingTab_ = false;
364 * True while a user is pressing <Ctrl>.
366 * TODO(fukino): This key is used only for controlling gear menu, so it
367 * should be moved to GearMenu class. crbug.com/366032.
372 this.pressingCtrl_ = false;
375 * True if shown gear menu is in secret mode.
377 * TODO(fukino): The state of gear menu should be moved to GearMenu class.
383 this.isSecretGearMenuShown_ = false;
386 * The last clicked item in the file list.
387 * @type {HTMLLIElement}
390 this.lastClickedItem_ = null;
393 * Count of the SourceNotFound error.
397 this.sourceNotFoundErrorCount_ = 0;
400 * Whether the app should be closed on unmount.
404 this.closeOnUnmount_ = false;
407 * The key for storing startup preference.
411 this.startupPrefName_ = '';
414 * URL of directory which should be initial current directory.
418 this.initCurrentDirectoryURL_ = '';
421 * URL of entry which should be initially selected.
425 this.initSelectionURL_ = '';
428 * The name of target entry (not URL).
432 this.initTargetName_ = '';
435 // Object.seal() has big performance/memory overhead for now, so we use
436 // Object.preventExtensions() here. crbug.com/412239.
437 Object.preventExtensions(this);
440 FileManager.prototype = /** @struct */ {
441 __proto__: cr.EventTarget.prototype,
443 * @return {DirectoryModel}
445 get directoryModel() {
446 return this.directoryModel_;
449 * @return {DirectoryTree}
451 get directoryTree() {
452 return this.directoryTree_;
455 * @return {HTMLDocument}
458 return this.document_;
461 * @return {FileTransferController}
463 get fileTransferController() {
464 return this.fileTransferController_;
467 * @return {NamingController}
469 get namingController() {
470 return this.namingController_;
473 * @return {FileOperationManager}
475 get fileOperationManager() {
476 return this.fileOperationManager_;
479 * @return {BackgroundWindow}
481 get backgroundPage() {
482 return this.backgroundPage_;
485 * @return {VolumeManagerWrapper}
487 get volumeManager() {
488 return this.volumeManager_;
491 * @return {FileManagerUI}
499 * List of dialog types.
501 * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except
502 * FULL_PAGE which is specific to this code.
508 SELECT_FOLDER: 'folder',
509 SELECT_UPLOAD_FOLDER: 'upload-folder',
510 SELECT_SAVEAS_FILE: 'saveas-file',
511 SELECT_OPEN_FILE: 'open-file',
512 SELECT_OPEN_MULTI_FILE: 'open-multi-file',
513 FULL_PAGE: 'full-page'
517 * @param {DialogType} type Dialog type.
518 * @return {boolean} Whether the type is modal.
520 DialogType.isModal = function(type) {
521 return type == DialogType.SELECT_FOLDER ||
522 type == DialogType.SELECT_UPLOAD_FOLDER ||
523 type == DialogType.SELECT_SAVEAS_FILE ||
524 type == DialogType.SELECT_OPEN_FILE ||
525 type == DialogType.SELECT_OPEN_MULTI_FILE;
529 * @param {DialogType} type Dialog type.
530 * @return {boolean} Whether the type is open dialog.
532 DialogType.isOpenDialog = function(type) {
533 return type == DialogType.SELECT_OPEN_FILE ||
534 type == DialogType.SELECT_OPEN_MULTI_FILE ||
535 type == DialogType.SELECT_FOLDER ||
536 type == DialogType.SELECT_UPLOAD_FOLDER;
540 * @param {DialogType} type Dialog type.
541 * @return {boolean} Whether the type is open dialog for file(s).
543 DialogType.isOpenFileDialog = function(type) {
544 return type == DialogType.SELECT_OPEN_FILE ||
545 type == DialogType.SELECT_OPEN_MULTI_FILE;
549 * @param {DialogType} type Dialog type.
550 * @return {boolean} Whether the type is folder selection dialog.
552 DialogType.isFolderDialog = function(type) {
553 return type == DialogType.SELECT_FOLDER ||
554 type == DialogType.SELECT_UPLOAD_FOLDER;
557 Object.freeze(DialogType);
560 * Bottom margin of the list and tree for transparent preview panel.
563 var BOTTOM_MARGIN_FOR_PREVIEW_PANEL_PX = 52;
565 // Anonymous "namespace".
567 // Private variables and helper functions.
570 * Number of milliseconds in a day.
572 var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
575 * Some UI elements react on a single click and standard double click handling
576 * leads to confusing results. We ignore a second click if it comes soon
579 var DOUBLE_CLICK_TIMEOUT = 200;
582 * Updates the element to display the information about remaining space for
585 * @param {!Object<string, number>} sizeStatsResult Map containing remaining
587 * @param {!Element} spaceInnerBar Block element for a percentage bar
588 * representing the remaining space.
589 * @param {!Element} spaceInfoLabel Inline element to contain the message.
590 * @param {!Element} spaceOuterBar Block element around the percentage bar.
592 var updateSpaceInfo = function(
593 sizeStatsResult, spaceInnerBar, spaceInfoLabel, spaceOuterBar) {
594 spaceInnerBar.removeAttribute('pending');
595 if (sizeStatsResult) {
596 var sizeStr = util.bytesToString(sizeStatsResult.remainingSize);
597 spaceInfoLabel.textContent = strf('SPACE_AVAILABLE', sizeStr);
600 sizeStatsResult.totalSize - sizeStatsResult.remainingSize;
601 spaceInnerBar.style.width =
602 (100 * usedSpace / sizeStatsResult.totalSize) + '%';
604 spaceOuterBar.hidden = false;
606 spaceOuterBar.hidden = true;
607 spaceInfoLabel.textContent = str('FAILED_SPACE_INFO');
611 FileManager.prototype.initPreferences_ = function(callback) {
612 var group = new AsyncUtil.Group();
614 // DRIVE preferences should be initialized before creating DirectoryModel
615 // to rebuild the roots list.
616 group.add(this.getPreferences_.bind(this));
618 // Get startup preferences.
619 group.add(function(done) {
620 chrome.storage.local.get(this.startupPrefName_, function(values) {
621 var value = values[this.startupPrefName_];
626 // Load the global default options.
628 this.viewOptions_ = JSON.parse(value);
630 // Override with window-specific options.
631 if (window.appState && window.appState.viewOptions) {
632 for (var key in window.appState.viewOptions) {
633 if (window.appState.viewOptions.hasOwnProperty(key))
634 this.viewOptions_[key] = window.appState.viewOptions[key];
645 * One time initialization for the file system and related things.
647 * @param {function()} callback Completion callback.
650 FileManager.prototype.initFileSystemUI_ = function(callback) {
651 this.ui_.listContainer.startBatchUpdates();
653 this.initFileList_();
654 this.setupCurrentDirectory_();
656 // PyAuto tests monitor this state by polling this variable
657 this.__defineGetter__('workerInitialized_', function() {
658 return this.metadataCache_.isInitialized();
661 this.initDateTimeFormatters_();
665 // Get the 'allowRedeemOffers' preference before launching
666 // FileListBannerController.
667 this.getPreferences_(function(pref) {
668 /** @type {boolean} */
669 var showOffers = !!pref['allowRedeemOffers'];
670 self.bannersController_ = new FileListBannerController(
671 self.directoryModel_, self.volumeManager_, self.document_,
673 self.bannersController_.addEventListener('relayout',
674 self.onResize_.bind(self));
677 var dm = this.directoryModel_;
678 dm.addEventListener('directory-changed',
679 this.onDirectoryChanged_.bind(this));
681 var listBeingUpdated = null;
682 dm.addEventListener('begin-update-files', function() {
683 self.ui_.listContainer.currentList.startBatchUpdates();
684 // Remember the list which was used when updating files started, so
685 // endBatchUpdates() is called on the same list.
686 listBeingUpdated = self.ui_.listContainer.currentList;
688 dm.addEventListener('end-update-files', function() {
689 self.namingController_.restoreItemBeingRenamed();
690 listBeingUpdated.endBatchUpdates();
691 listBeingUpdated = null;
694 this.initContextMenus_();
695 this.initCommands_();
696 assert(this.directoryModel_);
697 assert(this.spinnerController_);
698 assert(this.commandHandler);
699 assert(this.selectionHandler_);
700 this.scanController_ = new ScanController(
701 this.directoryModel_,
702 this.ui_.listContainer,
703 this.spinnerController_,
705 this.selectionHandler_);
707 this.directoryTree_.addEventListener('change', function() {
708 this.ensureDirectoryTreeItemNotBehindPreviewPanel_();
711 var stateChangeHandler =
712 this.onPreferencesChanged_.bind(this);
713 chrome.fileManagerPrivate.onPreferencesChanged.addListener(
715 stateChangeHandler();
717 var driveConnectionChangedHandler =
718 this.onDriveConnectionChanged_.bind(this);
719 this.volumeManager_.addEventListener('drive-connection-changed',
720 driveConnectionChangedHandler);
721 driveConnectionChangedHandler();
723 // Set the initial focus.
725 // Set it as a fallback when there is no focus.
726 this.document_.addEventListener('focusout', function(e) {
727 setTimeout(function() {
728 // When there is no focus, the active element is the <body>.
729 if (this.document_.activeElement == this.document_.body)
734 this.initDataTransferOperations_();
736 this.updateFileTypeFilter_();
737 this.selectionHandler_.onFileSelectionChanged();
738 this.ui_.listContainer.endBatchUpdates();
744 * If |item| in the directory tree is behind the preview panel, scrolls up the
745 * parent view and make the item visible. This should be called when:
746 * - the selected item is changed in the directory tree.
747 * - the visibility of the the preview panel is changed.
751 FileManager.prototype.ensureDirectoryTreeItemNotBehindPreviewPanel_ =
753 var selectedSubTree = this.directoryTree_.selectedItem;
754 if (!selectedSubTree)
756 var item = selectedSubTree.rowElement;
757 var parentView = this.directoryTree_;
759 var itemRect = item.getBoundingClientRect();
763 var listRect = parentView.getBoundingClientRect();
767 var previewPanel = this.dialogDom_.querySelector('.preview-panel');
768 var previewPanelRect = previewPanel.getBoundingClientRect();
769 var panelHeight = previewPanelRect ? previewPanelRect.height : 0;
771 var itemBottom = itemRect.bottom;
772 var listBottom = listRect.bottom - panelHeight;
774 if (itemBottom > listBottom) {
775 var scrollOffset = itemBottom - listBottom;
776 parentView.scrollTop += scrollOffset;
783 FileManager.prototype.initDateTimeFormatters_ = function() {
784 var use12hourClock = !this.preferences_['use24hourClock'];
785 this.ui_.listContainer.table.setDateTimeFormat(use12hourClock);
791 FileManager.prototype.initDataTransferOperations_ = function() {
792 this.fileOperationManager_ =
793 this.backgroundPage_.background.fileOperationManager;
795 // CopyManager are required for 'Delete' operation in
796 // Open and Save dialogs. But drag-n-drop and copy-paste are not needed.
797 if (this.dialogType != DialogType.FULL_PAGE) return;
799 // TODO(hidehiko): Extract FileOperationManager related code from
800 // FileManager to simplify it.
801 this.onCopyProgressBound_ = this.onCopyProgress_.bind(this);
802 this.fileOperationManager_.addEventListener(
803 'copy-progress', this.onCopyProgressBound_);
805 this.onEntriesChangedBound_ = this.onEntriesChanged_.bind(this);
806 this.fileOperationManager_.addEventListener(
807 'entries-changed', this.onEntriesChangedBound_);
809 var controller = this.fileTransferController_ =
810 new FileTransferController(
812 this.fileOperationManager_,
814 this.directoryModel_,
816 this.ui_.multiProfileShareDialog,
817 this.backgroundPage_.background.progressCenter);
818 controller.attachDragSource(this.ui_.listContainer.table.list);
819 controller.attachFileListDropTarget(this.ui_.listContainer.table.list);
820 controller.attachDragSource(this.ui_.listContainer.grid);
821 controller.attachFileListDropTarget(this.ui_.listContainer.grid);
822 controller.attachTreeDropTarget(this.directoryTree_);
823 controller.attachCopyPasteHandlers();
824 controller.addEventListener('selection-copied',
825 this.blinkSelection.bind(this));
826 controller.addEventListener('selection-cut',
827 this.blinkSelection.bind(this));
828 controller.addEventListener('source-not-found',
829 this.onSourceNotFound_.bind(this));
833 * Handles an error that the source entry of file operation is not found.
836 FileManager.prototype.onSourceNotFound_ = function(event) {
837 var item = new ProgressCenterItem();
838 item.id = 'source-not-found-' + this.sourceNotFoundErrorCount_;
839 if (event.progressType === ProgressItemType.COPY)
840 item.message = strf('COPY_SOURCE_NOT_FOUND_ERROR', event.fileName);
841 else if (event.progressType === ProgressItemType.MOVE)
842 item.message = strf('MOVE_SOURCE_NOT_FOUND_ERROR', event.fileName);
843 item.state = ProgressItemState.ERROR;
844 this.backgroundPage_.background.progressCenter.updateItem(item);
845 this.sourceNotFoundErrorCount_++;
849 * One-time initialization of context menus.
852 FileManager.prototype.initContextMenus_ = function() {
853 assert(this.ui_.listContainer.grid);
854 assert(this.ui_.listContainer.table);
855 assert(this.document_);
856 assert(this.dialogDom_);
858 // Set up the context menu for the file list.
859 var fileContextMenu = queryRequiredElement(
860 this.dialogDom_, '#file-context-menu');
861 cr.ui.Menu.decorate(fileContextMenu);
862 fileContextMenu = /** @type {!cr.ui.Menu} */ (fileContextMenu);
864 cr.ui.contextMenuHandler.setContextMenu(
865 this.ui_.listContainer.grid, fileContextMenu);
866 cr.ui.contextMenuHandler.setContextMenu(
867 this.ui_.listContainer.table.list, fileContextMenu);
868 cr.ui.contextMenuHandler.setContextMenu(
869 queryRequiredElement(this.document_, '.drive-welcome.page'),
872 // Set up the context menu for the volume/shortcut items in directory tree.
873 var rootsContextMenu = queryRequiredElement(
874 this.dialogDom_, '#roots-context-menu');
875 cr.ui.Menu.decorate(rootsContextMenu);
876 rootsContextMenu = /** @type {!cr.ui.Menu} */ (rootsContextMenu);
878 this.directoryTree_.contextMenuForRootItems = rootsContextMenu;
880 // Set up the context menu for the folder items in directory tree.
881 var directoryTreeContextMenu = queryRequiredElement(
882 this.dialogDom_, '#directory-tree-context-menu');
883 cr.ui.Menu.decorate(directoryTreeContextMenu);
884 directoryTreeContextMenu =
885 /** @type {!cr.ui.Menu} */ (directoryTreeContextMenu);
887 this.directoryTree_.contextMenuForSubitems = directoryTreeContextMenu;
889 // Set up the context menu for the text editing.
890 var textContextMenu = queryRequiredElement(
891 this.dialogDom_, '#text-context-menu');
892 cr.ui.Menu.decorate(textContextMenu);
893 this.textContextMenu_ = /** @type {!cr.ui.Menu} */ (textContextMenu);
895 var gearButton = queryRequiredElement(this.dialogDom_, '#gear-button');
896 gearButton.addEventListener('menushow', this.onShowGearMenu_.bind(this));
897 this.dialogDom_.querySelector('#gear-menu').menuItemSelector =
899 cr.ui.decorate(gearButton, cr.ui.MenuButton);
900 this.gearButton_ = /** @type {!cr.ui.MenuButton} */ (gearButton);
902 this.syncButton.checkable = true;
903 this.hostedButton.checkable = true;
905 if (util.runningInBrowser()) {
906 // Suppresses the default context menu.
907 this.dialogDom_.addEventListener('contextmenu', function(e) {
914 FileManager.prototype.onShowGearMenu_ = function() {
915 this.refreshRemainingSpace_(false); /* Without loading caption. */
917 // If the menu is opened while CTRL key pressed, secret menu itemscan be
919 this.isSecretGearMenuShown_ = this.pressingCtrl_;
921 // Update view of drive-related settings.
922 this.commandHandler.updateAvailability();
923 this.document_.getElementById('drive-separator').hidden =
924 !this.shouldShowDriveSettings();
926 // Force to update the gear menu position.
927 // TODO(hirono): Remove the workaround for the crbug.com/374093 after fixing
929 var gearMenu = this.document_.querySelector('#gear-menu');
930 gearMenu.style.left = '';
931 gearMenu.style.right = '';
932 gearMenu.style.top = '';
933 gearMenu.style.bottom = '';
937 * One-time initialization of commands.
940 FileManager.prototype.initCommands_ = function() {
941 assert(this.textContextMenu_);
943 this.commandHandler = new CommandHandler(this);
945 // TODO(hirono): Move the following block to the UI part.
946 var commandButtons = this.dialogDom_.querySelectorAll('button[command]');
947 for (var j = 0; j < commandButtons.length; j++)
948 CommandButton.decorate(commandButtons[j]);
950 var inputs = this.dialogDom_.querySelectorAll(
951 'input[type=text], input[type=search], textarea');
952 for (var i = 0; i < inputs.length; i++) {
953 cr.ui.contextMenuHandler.setContextMenu(inputs[i], this.textContextMenu_);
954 this.registerInputCommands_(inputs[i]);
957 cr.ui.contextMenuHandler.setContextMenu(this.ui_.listContainer.renameInput,
958 this.textContextMenu_);
959 this.registerInputCommands_(this.ui_.listContainer.renameInput);
960 this.document_.addEventListener(
962 this.ui_.listContainer.clearHover.bind(this.ui_.listContainer));
966 * Registers cut, copy, paste and delete commands on input element.
968 * @param {Node} node Text input element to register on.
971 FileManager.prototype.registerInputCommands_ = function(node) {
972 CommandUtil.forceDefaultHandler(node, 'cut');
973 CommandUtil.forceDefaultHandler(node, 'copy');
974 CommandUtil.forceDefaultHandler(node, 'paste');
975 CommandUtil.forceDefaultHandler(node, 'delete');
976 node.addEventListener('keydown', function(e) {
977 var key = util.getKeyModifiers(e) + e.keyCode;
978 if (key === '190' /* '/' */ || key === '191' /* '.' */) {
979 // If this key event is propagated, this is handled search command,
980 // which calls 'preventDefault' method.
987 * Entry point of the initialization.
988 * This method is called from main.js.
990 FileManager.prototype.initializeCore = function() {
991 this.initializeQueue_.add(this.initGeneral_.bind(this), [], 'initGeneral');
992 this.initializeQueue_.add(this.initBackgroundPage_.bind(this),
993 [], 'initBackgroundPage');
994 this.initializeQueue_.add(this.initPreferences_.bind(this),
995 ['initGeneral'], 'initPreferences');
996 this.initializeQueue_.add(this.initVolumeManager_.bind(this),
997 ['initGeneral', 'initBackgroundPage'],
998 'initVolumeManager');
1000 this.initializeQueue_.run();
1001 window.addEventListener('pagehide', this.onUnload_.bind(this));
1004 FileManager.prototype.initializeUI = function(dialogDom, callback) {
1005 this.dialogDom_ = dialogDom;
1006 this.document_ = this.dialogDom_.ownerDocument;
1008 this.initializeQueue_.add(
1009 this.initEssentialUI_.bind(this),
1010 ['initGeneral', 'initBackgroundPage'],
1012 this.initializeQueue_.add(this.initAdditionalUI_.bind(this),
1013 ['initEssentialUI'], 'initAdditionalUI');
1014 this.initializeQueue_.add(
1015 this.initFileSystemUI_.bind(this),
1016 ['initAdditionalUI', 'initPreferences'], 'initFileSystemUI');
1018 // Run again just in case if all pending closures have completed and the
1019 // queue has stopped and monitor the completion.
1020 this.initializeQueue_.run(callback);
1024 * Initializes general purpose basic things, which are used by other
1025 * initializing methods.
1027 * @param {function()} callback Completion callback.
1030 FileManager.prototype.initGeneral_ = function(callback) {
1031 // Initialize the application state.
1032 // TODO(mtomasz): Unify window.appState with location.search format.
1033 if (window.appState) {
1034 this.params_ = window.appState.params || {};
1035 this.initCurrentDirectoryURL_ = window.appState.currentDirectoryURL;
1036 this.initSelectionURL_ = window.appState.selectionURL;
1037 this.initTargetName_ = window.appState.targetName;
1039 // Used by the select dialog only.
1040 this.params_ = location.search ?
1041 JSON.parse(decodeURIComponent(location.search.substr(1))) :
1043 this.initCurrentDirectoryURL_ = this.params_.currentDirectoryURL;
1044 this.initSelectionURL_ = this.params_.selectionURL;
1045 this.initTargetName_ = this.params_.targetName;
1048 // Initialize the member variables that depend this.params_.
1049 this.dialogType = this.params_.type || DialogType.FULL_PAGE;
1050 this.startupPrefName_ = 'file-manager-' + this.dialogType;
1051 this.fileTypes_ = this.params_.typeList || [];
1057 * Initialize the background page.
1058 * @param {function()} callback Completion callback.
1061 FileManager.prototype.initBackgroundPage_ = function(callback) {
1062 chrome.runtime.getBackgroundPage(function(backgroundPage) {
1063 this.backgroundPage_ = backgroundPage;
1064 this.backgroundPage_.background.ready(function() {
1065 loadTimeData.data = this.backgroundPage_.background.stringData;
1066 if (util.runningInBrowser())
1067 this.backgroundPage_.registerDialog(window);
1074 * Initializes the VolumeManager instance.
1075 * @param {function()} callback Completion callback.
1078 FileManager.prototype.initVolumeManager_ = function(callback) {
1079 // Auto resolving to local path does not work for folders (e.g., dialog for
1080 // loading unpacked extensions).
1081 var noLocalPathResolution = DialogType.isFolderDialog(this.params_.type);
1083 // If this condition is false, VolumeManagerWrapper hides all drive
1084 // related event and data, even if Drive is enabled on preference.
1085 // In other words, even if Drive is disabled on preference but Files.app
1086 // should show Drive when it is re-enabled, then the value should be set to
1088 // Note that the Drive enabling preference change is listened by
1089 // DriveIntegrationService, so here we don't need to take care about it.
1091 !noLocalPathResolution || !this.params_.shouldReturnLocalPath;
1092 this.volumeManager_ = new VolumeManagerWrapper(
1093 /** @type {VolumeManagerWrapper.DriveEnabledStatus} */ (driveEnabled),
1094 this.backgroundPage_);
1099 * One time initialization of the Files.app's essential UI elements. These
1100 * elements will be shown to the user. Only visible elements should be
1101 * initialized here. Any heavy operation should be avoided. Files.app's
1102 * window is shown at the end of this routine.
1104 * @param {function()} callback Completion callback.
1107 FileManager.prototype.initEssentialUI_ = function(callback) {
1108 // Record stats of dialog types. New values must NOT be inserted into the
1109 // array enumerating the types. It must be in sync with
1110 // FileDialogType enum in tools/metrics/histograms/histogram.xml.
1111 metrics.recordEnum('Create', this.dialogType,
1112 [DialogType.SELECT_FOLDER,
1113 DialogType.SELECT_UPLOAD_FOLDER,
1114 DialogType.SELECT_SAVEAS_FILE,
1115 DialogType.SELECT_OPEN_FILE,
1116 DialogType.SELECT_OPEN_MULTI_FILE,
1117 DialogType.FULL_PAGE]);
1119 // Create the metadata cache.
1120 this.metadataCache_ = MetadataCache.createFull(this.volumeManager_);
1122 // Create the root view of FileManager.
1123 assert(this.dialogDom_);
1124 this.ui_ = new FileManagerUI(this.dialogDom_, this.dialogType);
1126 // Show the window as soon as the UI pre-initialization is done.
1127 if (this.dialogType == DialogType.FULL_PAGE && !util.runningInBrowser()) {
1128 chrome.app.window.current().show();
1129 setTimeout(callback, 100); // Wait until the animation is finished.
1136 * One-time initialization of dialogs.
1139 FileManager.prototype.initDialogs_ = function() {
1140 // Initialize the dialog.
1141 this.ui_.initDialogs();
1142 FileManagerDialogBase.setFileManager(this);
1144 // Obtains the dialog instances from FileManagerUI.
1145 // TODO(hirono): Remove the properties from the FileManager class.
1146 this.error = this.ui_.errorDialog;
1147 this.alert = this.ui_.alertDialog;
1148 this.confirm = this.ui_.confirmDialog;
1149 this.prompt = this.ui_.promptDialog;
1150 this.shareDialog_ = this.ui_.shareDialog;
1151 this.defaultTaskPicker = this.ui_.defaultTaskPicker;
1152 this.suggestAppsDialog = this.ui_.suggestAppsDialog;
1156 * One-time initialization of various DOM nodes. Loads the additional DOM
1157 * elements visible to the user. Initialize here elements, which are expensive
1158 * or hidden in the beginning.
1160 * @param {function()} callback Completion callback.
1163 FileManager.prototype.initAdditionalUI_ = function(callback) {
1164 // Cache nodes we'll be manipulating.
1165 var dom = this.dialogDom_;
1168 this.initDialogs_();
1170 var table = queryRequiredElement(dom, '.detail-table');
1173 this.metadataCache_,
1174 this.volumeManager_,
1175 this.dialogType == DialogType.FULL_PAGE);
1176 var grid = queryRequiredElement(dom, '.thumbnail-grid');
1177 FileGrid.decorate(grid, this.metadataCache_, this.volumeManager_);
1179 this.ui_.initAdditionalUI(
1180 assertInstanceof(table, FileTable),
1181 assertInstanceof(grid, FileGrid),
1183 queryRequiredElement(dom, '.preview-panel'),
1184 DialogType.isOpenDialog(this.dialogType) ?
1185 PreviewPanel.VisibilityType.ALWAYS_VISIBLE :
1186 PreviewPanel.VisibilityType.AUTO,
1187 this.metadataCache_,
1188 this.volumeManager_));
1190 this.dialogDom_.addEventListener('click',
1191 this.onExternalLinkClick_.bind(this));
1194 var taskItems = queryRequiredElement(dom, '#tasks');
1195 this.taskItems_ = /** @type {HTMLButtonElement} */ (taskItems);
1197 this.ui_.locationLine = new LocationLine(
1198 queryRequiredElement(dom, '#location-breadcrumbs'),
1199 queryRequiredElement(dom, '#location-volume-icon'),
1200 this.metadataCache_,
1201 this.volumeManager_);
1202 this.ui_.locationLine.addEventListener(
1203 'pathclick', this.onBreadcrumbClick_.bind(this));
1205 // Initialize progress center panel.
1206 this.progressCenterPanel_ = new ProgressCenterPanel(
1207 queryRequiredElement(dom, '#progress-center'));
1208 this.backgroundPage_.background.progressCenter.addPanel(
1209 this.progressCenterPanel_);
1211 this.document_.addEventListener('keydown', this.onKeyDown_.bind(this));
1212 this.document_.addEventListener('keyup', this.onKeyUp_.bind(this));
1214 this.ui_.listContainer.element.addEventListener(
1215 'keydown', this.onListKeyDown_.bind(this));
1216 this.ui_.listContainer.element.addEventListener(
1217 ListContainer.EventType.TEXT_SEARCH, this.onTextSearch_.bind(this));
1219 // TODO(hirono): Rename the handler after creating the DialogFooter class.
1220 this.ui_.dialogFooter.filenameInput.addEventListener(
1221 'input', this.onFilenameInputInput_.bind(this));
1222 this.ui_.dialogFooter.filenameInput.addEventListener(
1223 'keydown', this.onFilenameInputKeyDown_.bind(this));
1224 this.ui_.dialogFooter.filenameInput.addEventListener(
1225 'focus', this.onFilenameInputFocus_.bind(this));
1227 this.decorateSplitter(
1228 this.dialogDom_.querySelector('#navigation-list-splitter'));
1230 this.dialogContainer_ = /** @type {!HTMLDivElement} */
1231 (this.dialogDom_.querySelector('.dialog-container'));
1233 this.syncButton = /** @type {!HTMLMenuItemElement} */
1234 (queryRequiredElement(this.dialogDom_,
1235 '#gear-menu-drive-sync-settings'));
1236 this.hostedButton = /** @type {!HTMLMenuItemElement} */
1237 (queryRequiredElement(this.dialogDom_,
1238 '#gear-menu-drive-hosted-settings'));
1240 this.ui_.toggleViewButton.addEventListener('click',
1241 this.onToggleViewButtonClick_.bind(this));
1243 cr.ui.ComboButton.decorate(this.taskItems_);
1244 this.taskItems_.showMenu = function(shouldSetFocus) {
1245 // Prevent the empty menu from opening.
1246 if (!this.menu.length)
1248 cr.ui.ComboButton.prototype.showMenu.call(this, shouldSetFocus);
1250 this.taskItems_.addEventListener('select',
1251 this.onTaskItemClicked_.bind(this));
1253 this.dialogDom_.ownerDocument.defaultView.addEventListener(
1254 'resize', this.onResize_.bind(this));
1256 this.actionMenuItem_ = /** @type {!HTMLMenuItemElement} */
1257 (queryRequiredElement(this.dialogDom_, '#default-action'));
1259 this.openWithCommand_ = /** @type {cr.ui.Command} */
1260 (this.dialogDom_.querySelector('#open-with'));
1262 this.actionMenuItem_.addEventListener('activate',
1263 this.onActionMenuItemActivated_.bind(this));
1265 this.ui_.dialogFooter.initFileTypeFilter(
1266 this.fileTypes_, this.params_.includeAllFiles);
1267 this.ui_.dialogFooter.fileTypeSelector.addEventListener(
1268 'change', this.updateFileTypeFilter_.bind(this));
1270 util.addIsFocusedMethod();
1272 // Populate the static localized strings.
1273 i18nTemplate.process(this.document_, loadTimeData);
1275 // Arrange the file list.
1276 this.ui_.listContainer.table.normalizeColumns();
1277 this.ui_.listContainer.table.redraw();
1283 * @param {Event} event Click event.
1286 FileManager.prototype.onBreadcrumbClick_ = function(event) {
1287 this.directoryModel_.changeDirectoryEntry(event.entry);
1291 * Constructs table and grid (heavy operation).
1294 FileManager.prototype.initFileList_ = function() {
1295 var singleSelection =
1296 this.dialogType == DialogType.SELECT_OPEN_FILE ||
1297 this.dialogType == DialogType.SELECT_FOLDER ||
1298 this.dialogType == DialogType.SELECT_UPLOAD_FOLDER ||
1299 this.dialogType == DialogType.SELECT_SAVEAS_FILE;
1301 assert(this.metadataCache_);
1302 this.fileFilter_ = new FileFilter(
1303 this.metadataCache_,
1304 false /* Don't show dot files and *.crdownload by default. */);
1306 this.fileWatcher_ = new FileWatcher(this.metadataCache_);
1307 this.fileWatcher_.addEventListener(
1308 'watcher-metadata-changed',
1309 this.onWatcherMetadataChanged_.bind(this));
1311 this.directoryModel_ = new DirectoryModel(
1315 this.metadataCache_,
1316 this.volumeManager_);
1318 this.folderShortcutsModel_ = new FolderShortcutsDataModel(
1319 this.volumeManager_);
1321 this.selectionHandler_ = new FileSelectionHandler(this);
1323 var dataModel = this.directoryModel_.getFileList();
1324 dataModel.addEventListener('permuted',
1325 this.updateStartupPrefs_.bind(this));
1327 this.directoryModel_.getFileListSelection().addEventListener('change',
1328 this.selectionHandler_.onFileSelectionChanged.bind(
1329 this.selectionHandler_));
1331 var onDetailClickBound = this.onDetailClick_.bind(this);
1332 this.ui_.listContainer.table.list.addEventListener(
1333 'click', onDetailClickBound);
1334 this.ui_.listContainer.grid.addEventListener(
1335 'click', onDetailClickBound);
1337 var fileListFocusBound = this.onFileListFocus_.bind(this);
1338 this.ui_.listContainer.table.list.addEventListener(
1339 'focus', fileListFocusBound);
1340 this.ui_.listContainer.grid.addEventListener('focus', fileListFocusBound);
1342 // TODO(mtomasz, yoshiki): Create navigation list earlier, and here just
1343 // attach the directory model.
1344 this.initDirectoryTree_();
1346 this.ui_.listContainer.table.addEventListener('column-resize-end',
1347 this.updateStartupPrefs_.bind(this));
1349 // Restore preferences.
1350 this.directoryModel_.getFileList().sort(
1351 this.viewOptions_.sortField || 'modificationTime',
1352 this.viewOptions_.sortDirection || 'desc');
1353 if (this.viewOptions_.columns) {
1354 var cm = this.ui_.listContainer.table.columnModel;
1355 for (var i = 0; i < cm.totalSize; i++) {
1356 if (this.viewOptions_.columns[i] > 0)
1357 cm.setWidth(i, this.viewOptions_.columns[i]);
1361 this.ui_.listContainer.dataModel = this.directoryModel_.getFileList();
1362 this.ui_.listContainer.selectionModel =
1363 this.directoryModel_.getFileListSelection();
1365 this.viewOptions_.listType || ListContainer.ListType.DETAIL);
1367 this.closeOnUnmount_ = (this.params_.action == 'auto-open');
1369 if (this.closeOnUnmount_) {
1370 this.volumeManager_.addEventListener('externally-unmounted',
1371 this.onExternallyUnmounted_.bind(this));
1374 // Create search controller.
1375 this.searchController_ = new SearchController(
1377 this.ui_.locationLine,
1378 this.directoryModel_,
1379 this.volumeManager_,
1381 // TODO (hirono): Make the real task controller and pass it here.
1382 doAction: this.doEntryAction_.bind(this)
1385 // Create naming controller.
1386 assert(this.ui_.alertDialog);
1387 assert(this.ui_.confirmDialog);
1388 this.namingController_ = new NamingController(
1389 this.ui_.listContainer,
1390 this.ui_.alertDialog,
1391 this.ui_.confirmDialog,
1392 this.directoryModel_,
1394 this.selectionHandler_);
1396 // Create spinner controller.
1397 this.spinnerController_ = new SpinnerController(
1398 this.ui_.listContainer.spinner, this.directoryModel_);
1399 this.spinnerController_.show();
1401 // Create dialog action controller.
1402 this.dialogActionController_ = new DialogActionController(
1404 this.ui_.dialogFooter,
1405 this.directoryModel_,
1406 this.metadataCache_,
1407 this.namingController_,
1408 this.params_.shouldReturnLocalPath);
1410 // Update metadata to change 'Today' and 'Yesterday' dates.
1411 var today = new Date();
1413 today.setMinutes(0);
1414 today.setSeconds(0);
1415 today.setMilliseconds(0);
1416 setTimeout(this.dailyUpdateModificationTime_.bind(this),
1417 today.getTime() + MILLISECONDS_IN_DAY - Date.now() + 1000);
1423 FileManager.prototype.initDirectoryTree_ = function() {
1424 var fakeEntriesVisible =
1425 this.dialogType !== DialogType.SELECT_SAVEAS_FILE;
1426 this.directoryTree_ = /** @type {DirectoryTree} */
1427 (this.dialogDom_.querySelector('#directory-tree'));
1428 DirectoryTree.decorate(this.directoryTree_,
1429 this.directoryModel_,
1430 this.volumeManager_,
1431 this.metadataCache_,
1432 fakeEntriesVisible);
1433 this.directoryTree_.dataModel = new NavigationListModel(
1434 this.volumeManager_, this.folderShortcutsModel_);
1436 // Visible height of the directory tree depends on the size of progress
1437 // center panel. When the size of progress center panel changes, directory
1438 // tree has to be notified to adjust its components (e.g. progress bar).
1439 var observer = new MutationObserver(
1440 this.directoryTree_.relayout.bind(this.directoryTree_));
1441 observer.observe(this.progressCenterPanel_.element,
1442 /** @type {MutationObserverInit} */
1443 ({subtree: true, attributes: true, childList: true}));
1449 FileManager.prototype.updateStartupPrefs_ = function() {
1450 var sortStatus = this.directoryModel_.getFileList().sortStatus;
1452 sortField: sortStatus.field,
1453 sortDirection: sortStatus.direction,
1455 listType: this.ui_.listContainer.currentListType
1457 var cm = this.ui_.listContainer.table.columnModel;
1458 for (var i = 0; i < cm.totalSize; i++) {
1459 prefs.columns.push(cm.getWidth(i));
1461 // Save the global default.
1463 items[this.startupPrefName_] = JSON.stringify(prefs);
1464 chrome.storage.local.set(items);
1466 // Save the window-specific preference.
1467 if (window.appState) {
1468 window.appState.viewOptions = prefs;
1469 util.saveAppState();
1473 FileManager.prototype.refocus = function() {
1475 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE)
1476 targetElement = this.ui_.dialogFooter.filenameInput;
1478 targetElement = this.ui.listContainer.currentList;
1480 // Hack: if the tabIndex is disabled, we can assume a modal dialog is
1481 // shown. Focus to a button on the dialog instead.
1482 if (!targetElement.hasAttribute('tabIndex') || targetElement.tabIndex == -1)
1483 targetElement = document.querySelector('button:not([tabIndex="-1"])');
1486 targetElement.focus();
1490 * File list focus handler. Used to select the top most element on the list
1491 * if nothing was selected.
1495 FileManager.prototype.onFileListFocus_ = function() {
1496 // If the file list is focused by <Tab>, select the first item if no item
1498 if (this.pressingTab_) {
1499 if (this.getSelection() && this.getSelection().totalCount == 0)
1500 this.directoryModel_.selectIndex(0);
1505 * Sets the current list type.
1506 * @param {ListContainer.ListType} type New list type.
1508 FileManager.prototype.setListType = function(type) {
1509 if ((type && type == this.ui_.listContainer.currentListType) ||
1510 !this.directoryModel_) {
1514 this.ui_.setCurrentListType(type);
1515 this.updateStartupPrefs_();
1522 FileManager.prototype.onCopyProgress_ = function(event) {
1523 if (event.reason == 'ERROR' &&
1524 event.error.code == util.FileOperationErrorType.FILESYSTEM_ERROR &&
1525 event.error.data.toDrive &&
1526 event.error.data.name == util.FileError.QUOTA_EXCEEDED_ERR) {
1527 this.alert.showHtml(
1528 strf('DRIVE_SERVER_OUT_OF_SPACE_HEADER'),
1529 strf('DRIVE_SERVER_OUT_OF_SPACE_MESSAGE',
1531 event.error.data.sourceFileUrl.split('/').pop()),
1532 str('GOOGLE_DRIVE_BUY_STORAGE_URL')));
1537 * Handler of file manager operations. Called when an entry has been
1539 * This updates directory model to reflect operation result immediately (not
1540 * waiting for directory update event). Also, preloads thumbnails for the
1541 * images of new entries.
1542 * See also FileOperationManager.EventRouter.
1544 * @param {Event} event An event for the entry change.
1547 FileManager.prototype.onEntriesChanged_ = function(event) {
1548 var kind = event.kind;
1549 var entries = event.entries;
1550 this.directoryModel_.onEntriesChanged(kind, entries);
1551 this.selectionHandler_.onFileSelectionChanged();
1553 if (kind !== util.EntryChangedKind.CREATED)
1556 var preloadThumbnail = function(entry) {
1557 var locationInfo = this.volumeManager_.getLocationInfo(entry);
1560 this.metadataCache_.getOne(entry, 'thumbnail|external',
1561 function(metadata) {
1562 var thumbnailLoader_ = new ThumbnailLoader(
1564 ThumbnailLoader.LoaderType.CANVAS,
1566 undefined, // Media type.
1567 locationInfo.isDriveBased ?
1568 ThumbnailLoader.UseEmbedded.USE_EMBEDDED :
1569 ThumbnailLoader.UseEmbedded.NO_EMBEDDED,
1570 10); // Very low priority.
1571 thumbnailLoader_.loadDetachedImage(function(success) {});
1575 for (var i = 0; i < entries.length; i++) {
1576 // Preload a thumbnail if the new copied entry an image.
1577 if (FileType.isImage(entries[i]))
1578 preloadThumbnail(entries[i]);
1583 * Filters file according to the selected file type.
1586 FileManager.prototype.updateFileTypeFilter_ = function() {
1587 this.fileFilter_.removeFilter('fileType');
1588 var selectedIndex = this.ui_.dialogFooter.selectedFilterIndex;
1589 if (selectedIndex > 0) { // Specific filter selected.
1590 var regexp = new RegExp('\\.(' +
1591 this.fileTypes_[selectedIndex - 1].extensions.join('|') + ')$', 'i');
1592 var filter = function(entry) {
1593 return entry.isDirectory || regexp.test(entry.name);
1595 this.fileFilter_.addFilter('fileType', filter);
1597 // In save dialog, update the destination name extension.
1598 if (this.dialogType === DialogType.SELECT_SAVEAS_FILE) {
1599 var current = this.ui_.dialogFooter.filenameInput.value;
1600 var newExt = this.fileTypes_[selectedIndex - 1].extensions[0];
1601 if (newExt && !regexp.test(current)) {
1602 var i = current.lastIndexOf('.');
1604 this.ui_.dialogFooter.filenameInput.value =
1605 current.substr(0, i) + '.' + newExt;
1606 this.selectTargetNameInFilenameInput_();
1614 * Resize details and thumb views to fit the new window size.
1617 FileManager.prototype.onResize_ = function() {
1618 // May not be available during initialization.
1619 if (this.directoryTree_)
1620 this.directoryTree_.relayout();
1622 this.ui_.relayout();
1626 * Handles local metadata changes in the currect directory.
1627 * @param {Event} event Change event.
1628 * @this {FileManager}
1631 FileManager.prototype.onWatcherMetadataChanged_ = function(event) {
1632 this.ui_.listContainer.currentView.updateListItemsMetadata(
1633 event.metadataType, event.entries);
1637 * Sets up the current directory during initialization.
1640 FileManager.prototype.setupCurrentDirectory_ = function() {
1641 var tracker = this.directoryModel_.createDirectoryChangeTracker();
1642 var queue = new AsyncUtil.Queue();
1644 // Wait until the volume manager is initialized.
1645 queue.run(function(callback) {
1647 this.volumeManager_.ensureInitialized(callback);
1650 var nextCurrentDirEntry;
1653 // Resolve the selectionURL to selectionEntry or to currentDirectoryEntry
1654 // in case of being a display root or a default directory to open files.
1655 queue.run(function(callback) {
1656 if (!this.initSelectionURL_) {
1660 webkitResolveLocalFileSystemURL(
1661 this.initSelectionURL_,
1663 var locationInfo = this.volumeManager_.getLocationInfo(inEntry);
1664 // If location information is not available, then the volume is
1665 // no longer (or never) available.
1666 if (!locationInfo) {
1670 // If the selection is root, then use it as a current directory
1671 // instead. This is because, selecting a root entry is done as
1673 if (locationInfo.isRootEntry)
1674 nextCurrentDirEntry = inEntry;
1676 // If this dialog attempts to open file(s) and the selection is a
1677 // directory, the selection should be the current directory.
1678 if (DialogType.isOpenFileDialog(this.dialogType) &&
1679 inEntry.isDirectory) {
1680 nextCurrentDirEntry = inEntry;
1683 // By default, the selection should be selected entry and the
1684 // parent directory of it should be the current directory.
1685 if (!nextCurrentDirEntry)
1686 selectionEntry = inEntry;
1689 }.bind(this), callback);
1691 // Resolve the currentDirectoryURL to currentDirectoryEntry (if not done
1692 // by the previous step).
1693 queue.run(function(callback) {
1694 if (nextCurrentDirEntry || !this.initCurrentDirectoryURL_) {
1698 webkitResolveLocalFileSystemURL(
1699 this.initCurrentDirectoryURL_,
1701 var locationInfo = this.volumeManager_.getLocationInfo(inEntry);
1702 if (!locationInfo) {
1706 nextCurrentDirEntry = inEntry;
1708 }.bind(this), callback);
1709 // TODO(mtomasz): Implement reopening on special search, when fake
1710 // entries are converted to directory providers.
1713 // If the directory to be changed to is not available, then first fallback
1714 // to the parent of the selection entry.
1715 queue.run(function(callback) {
1716 if (nextCurrentDirEntry || !selectionEntry) {
1720 selectionEntry.getParent(function(inEntry) {
1721 nextCurrentDirEntry = inEntry;
1726 // Check if the next current directory is not a virtual directory which is
1727 // not available in UI. This may happen to shared on Drive.
1728 queue.run(function(callback) {
1729 if (!nextCurrentDirEntry) {
1733 var locationInfo = this.volumeManager_.getLocationInfo(
1734 nextCurrentDirEntry);
1735 // If we can't check, assume that the directory is illegal.
1736 if (!locationInfo) {
1737 nextCurrentDirEntry = null;
1741 // Having root directory of DRIVE_OTHER here should be only for shared
1742 // with me files. Fallback to Drive root in such case.
1743 if (locationInfo.isRootEntry && locationInfo.rootType ===
1744 VolumeManagerCommon.RootType.DRIVE_OTHER) {
1745 var volumeInfo = this.volumeManager_.getVolumeInfo(nextCurrentDirEntry);
1747 nextCurrentDirEntry = null;
1751 volumeInfo.resolveDisplayRoot().then(
1753 nextCurrentDirEntry = entry;
1755 }).catch(function(error) {
1756 console.error(error.stack || error);
1757 nextCurrentDirEntry = null;
1765 // If the directory to be changed to is still not resolved, then fallback
1766 // to the default display root.
1767 queue.run(function(callback) {
1768 if (nextCurrentDirEntry) {
1772 this.volumeManager_.getDefaultDisplayRoot(function(displayRoot) {
1773 nextCurrentDirEntry = displayRoot;
1778 // If selection failed to be resolved (eg. didn't exist, in case of saving
1779 // a file, or in case of a fallback of the current directory, then try to
1780 // resolve again using the target name.
1781 queue.run(function(callback) {
1782 if (selectionEntry || !nextCurrentDirEntry || !this.initTargetName_) {
1786 // Try to resolve as a file first. If it fails, then as a directory.
1787 nextCurrentDirEntry.getFile(
1788 this.initTargetName_,
1790 function(targetEntry) {
1791 selectionEntry = targetEntry;
1794 // Failed to resolve as a file
1795 nextCurrentDirEntry.getDirectory(
1796 this.initTargetName_,
1798 function(targetEntry) {
1799 selectionEntry = targetEntry;
1802 // Failed to resolve as either file or directory.
1809 queue.run(function(callback) {
1810 // Check directory change.
1812 if (tracker.hasChanged) {
1816 // Finish setup current directory.
1817 this.finishSetupCurrentDirectory_(
1818 nextCurrentDirEntry,
1820 this.initTargetName_);
1826 * @param {DirectoryEntry} directoryEntry Directory to be opened.
1827 * @param {Entry=} opt_selectionEntry Entry to be selected.
1828 * @param {string=} opt_suggestedName Suggested name for a non-existing\
1832 FileManager.prototype.finishSetupCurrentDirectory_ = function(
1833 directoryEntry, opt_selectionEntry, opt_suggestedName) {
1834 // Open the directory, and select the selection (if passed).
1835 if (util.isFakeEntry(directoryEntry)) {
1836 this.directoryModel_.specialSearch(directoryEntry, '');
1838 this.directoryModel_.changeDirectoryEntry(directoryEntry, function() {
1839 if (opt_selectionEntry)
1840 this.directoryModel_.selectEntry(opt_selectionEntry);
1844 if (this.dialogType === DialogType.FULL_PAGE) {
1845 // In the FULL_PAGE mode if the restored URL points to a file we might
1846 // have to invoke a task after selecting it.
1847 if (this.params_.action === 'select')
1852 // TODO(mtomasz): Implement remounting archives after crash.
1853 // See: crbug.com/333139
1855 // If there is a task to be run, run it after the scan is completed.
1857 var listener = function() {
1858 if (!util.isSameEntry(this.directoryModel_.getCurrentDirEntry(),
1860 // Opened on a different URL. Probably fallbacked. Therefore,
1861 // do not invoke a task.
1864 this.directoryModel_.removeEventListener(
1865 'scan-completed', listener);
1868 this.directoryModel_.addEventListener('scan-completed', listener);
1870 } else if (this.dialogType === DialogType.SELECT_SAVEAS_FILE) {
1871 this.ui_.dialogFooter.filenameInput.value = opt_suggestedName || '';
1872 this.selectTargetNameInFilenameInput_();
1879 FileManager.prototype.refreshCurrentDirectoryMetadata_ = function() {
1880 var entries = this.directoryModel_.getFileList().slice();
1881 var directoryEntry = this.directoryModel_.getCurrentDirEntry();
1882 if (!directoryEntry)
1884 // We don't pass callback here. When new metadata arrives, we have an
1885 // observer registered to update the UI.
1887 // TODO(dgozman): refresh content metadata only when modificationTime
1889 var isFakeEntry = util.isFakeEntry(directoryEntry);
1890 var getEntries = (isFakeEntry ? [] : [directoryEntry]).concat(entries);
1892 this.metadataCache_.clearRecursively(directoryEntry, '*');
1893 this.metadataCache_.get(getEntries, 'filesystem|external', null);
1895 var visibleItems = this.ui.listContainer.currentList.items;
1896 var visibleEntries = [];
1897 for (var i = 0; i < visibleItems.length; i++) {
1898 var index = this.ui.listContainer.currentList.getIndexOfListItem(
1900 var entry = this.directoryModel_.getFileList().item(index);
1901 // The following check is a workaround for the bug in list: sometimes item
1902 // does not have listIndex, and therefore is not found in the list.
1903 if (entry) visibleEntries.push(entry);
1905 // Refreshes the metadata.
1906 this.metadataCache_.getLatest(visibleEntries, 'thumbnail', null);
1912 FileManager.prototype.dailyUpdateModificationTime_ = function() {
1913 var entries = this.directoryModel_.getFileList().slice();
1914 this.metadataCache_.get(
1918 this.ui_.listContainer.currentView.updateListItemsMetadata(
1919 'filesystem', entries);
1922 setTimeout(this.dailyUpdateModificationTime_.bind(this),
1923 MILLISECONDS_IN_DAY);
1927 * TODO(mtomasz): Move this to a utility function working on the root type.
1928 * @return {boolean} True if the current directory content is from Google
1931 FileManager.prototype.isOnDrive = function() {
1932 var rootType = this.directoryModel_.getCurrentRootType();
1933 return rootType != null &&
1934 VolumeManagerCommon.getVolumeTypeFromRootType(rootType) ==
1935 VolumeManagerCommon.VolumeType.DRIVE;
1939 * Check if the drive-related setting items should be shown on currently
1940 * displayed gear menu.
1941 * @return {boolean} True if those setting items should be shown.
1943 FileManager.prototype.shouldShowDriveSettings = function() {
1944 return this.isOnDrive();
1948 * Overrides default handling for clicks on hyperlinks.
1949 * In a packaged apps links with targer='_blank' open in a new tab by
1950 * default, other links do not open at all.
1952 * @param {Event} event Click event.
1955 FileManager.prototype.onExternalLinkClick_ = function(event) {
1956 if (event.target.tagName != 'A' || !event.target.href)
1959 if (this.dialogType != DialogType.FULL_PAGE)
1960 this.ui_.dialogFooter.cancelButton.click();
1964 * Task combobox handler.
1966 * @param {Object} event Event containing task which was clicked.
1969 FileManager.prototype.onTaskItemClicked_ = function(event) {
1970 var selection = this.getSelection();
1971 if (!selection.tasks) return;
1973 if (event.item.task) {
1974 // Task field doesn't exist on change-default dropdown item.
1975 selection.tasks.execute(event.item.task.taskId);
1977 var extensions = [];
1979 for (var i = 0; i < selection.entries.length; i++) {
1980 var match = /\.(\w+)$/g.exec(selection.entries[i].toURL());
1982 var ext = match[1].toUpperCase();
1983 if (extensions.indexOf(ext) == -1) {
1984 extensions.push(ext);
1991 if (extensions.length == 1) {
1992 format = extensions[0];
1995 // Change default was clicked. We should open "change default" dialog.
1996 selection.tasks.showTaskPicker(this.defaultTaskPicker,
1997 loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM'),
1998 strf('CHANGE_DEFAULT_CAPTION', format),
1999 this.onDefaultTaskDone_.bind(this),
2005 * Sets the given task as default, when this task is applicable.
2007 * @param {Object} task Task to set as default.
2010 FileManager.prototype.onDefaultTaskDone_ = function(task) {
2011 // TODO(dgozman): move this method closer to tasks.
2012 var selection = this.getSelection();
2013 // TODO(mtomasz): Move conversion from entry to url to custom bindings.
2014 // crbug.com/345527.
2015 chrome.fileManagerPrivate.setDefaultTask(
2017 util.entriesToURLs(selection.entries),
2018 selection.mimeTypes);
2019 selection.tasks = new FileTasks(this);
2020 selection.tasks.init(selection.entries, selection.mimeTypes);
2021 selection.tasks.display(this.taskItems_);
2022 this.refreshCurrentDirectoryMetadata_();
2023 this.selectionHandler_.onFileSelectionChanged();
2029 FileManager.prototype.onPreferencesChanged_ = function() {
2031 this.getPreferences_(function(prefs) {
2032 self.initDateTimeFormatters_();
2033 self.refreshCurrentDirectoryMetadata_();
2035 if (prefs.cellularDisabled)
2036 self.syncButton.setAttribute('checked', '');
2038 self.syncButton.removeAttribute('checked');
2040 if (self.hostedButton.hasAttribute('checked') ===
2041 prefs.hostedFilesDisabled && self.isOnDrive()) {
2042 self.directoryModel_.rescan(false);
2045 if (!prefs.hostedFilesDisabled)
2046 self.hostedButton.setAttribute('checked', '');
2048 self.hostedButton.removeAttribute('checked');
2050 true /* refresh */);
2053 FileManager.prototype.onDriveConnectionChanged_ = function() {
2054 var connection = this.volumeManager_.getDriveConnectionState();
2055 if (this.commandHandler)
2056 this.commandHandler.updateAvailability();
2057 if (this.dialogContainer_)
2058 this.dialogContainer_.setAttribute('connection', connection.type);
2059 this.shareDialog_.hideWithResult(ShareDialog.Result.NETWORK_ERROR);
2060 this.suggestAppsDialog.onDriveConnectionChanged(connection.type);
2064 * Tells whether the current directory is read only.
2065 * TODO(mtomasz): Remove and use EntryLocation directly.
2066 * @return {boolean} True if read only, false otherwise.
2068 FileManager.prototype.isOnReadonlyDirectory = function() {
2069 return this.directoryModel_.isReadOnly();
2073 * @param {Event} event Unmount event.
2076 FileManager.prototype.onExternallyUnmounted_ = function(event) {
2077 if (event.volumeInfo === this.currentVolumeInfo_) {
2078 if (this.closeOnUnmount_) {
2079 // If the file manager opened automatically when a usb drive inserted,
2080 // user have never changed current volume (that implies the current
2081 // directory is still on the device) then close this window.
2088 * @return {Array.<Entry>} List of all entries in the current directory.
2090 FileManager.prototype.getAllEntriesInCurrentDirectory = function() {
2091 return this.directoryModel_.getFileList().slice();
2095 * Return DirectoryEntry of the current directory or null.
2096 * @return {DirectoryEntry} DirectoryEntry of the current directory. Returns
2097 * null if the directory model is not ready or the current directory is
2100 FileManager.prototype.getCurrentDirectoryEntry = function() {
2101 return this.directoryModel_ && this.directoryModel_.getCurrentDirEntry();
2105 * Shows the share dialog for the selected file or directory.
2107 FileManager.prototype.shareSelection = function() {
2108 var entries = this.getSelection().entries;
2109 if (entries.length != 1) {
2110 console.warn('Unable to share multiple items at once.');
2113 // Add the overlapped class to prevent the applicaiton window from
2114 // captureing mouse events.
2115 this.shareDialog_.show(entries[0], function(result) {
2116 if (result == ShareDialog.Result.NETWORK_ERROR)
2117 this.error.show(str('SHARE_ERROR'));
2122 * Creates a folder shortcut.
2123 * @param {Entry} entry A shortcut which refers to |entry| to be created.
2125 FileManager.prototype.createFolderShortcut = function(entry) {
2127 if (this.folderShortcutExists(entry))
2130 this.folderShortcutsModel_.add(entry);
2134 * Checkes if the shortcut which refers to the given folder exists or not.
2135 * @param {Entry} entry Entry of the folder to be checked.
2137 FileManager.prototype.folderShortcutExists = function(entry) {
2138 return this.folderShortcutsModel_.exists(entry);
2142 * Removes the folder shortcut.
2143 * @param {Entry} entry The shortcut which refers to |entry| is to be removed.
2145 FileManager.prototype.removeFolderShortcut = function(entry) {
2146 this.folderShortcutsModel_.remove(entry);
2150 * Blinks the selection. Used to give feedback when copying or cutting the
2153 FileManager.prototype.blinkSelection = function() {
2154 var selection = this.getSelection();
2155 if (!selection || selection.totalCount == 0)
2158 for (var i = 0; i < selection.entries.length; i++) {
2159 var selectedIndex = selection.indexes[i];
2161 this.ui.listContainer.currentList.getListItemByIndex(selectedIndex);
2163 this.blinkListItem_(listItem);
2168 * @param {Element} listItem List item element.
2171 FileManager.prototype.blinkListItem_ = function(listItem) {
2172 listItem.classList.add('blink');
2173 setTimeout(function() {
2174 listItem.classList.remove('blink');
2181 FileManager.prototype.selectTargetNameInFilenameInput_ = function() {
2182 var input = this.ui_.dialogFooter.filenameInput;
2184 var selectionEnd = input.value.lastIndexOf('.');
2185 if (selectionEnd == -1) {
2188 input.selectionStart = 0;
2189 input.selectionEnd = selectionEnd;
2194 * Handles mouse click or tap.
2196 * @param {Event} event The click event.
2199 FileManager.prototype.onDetailClick_ = function(event) {
2200 if (this.namingController_.isRenamingInProgress()) {
2201 // Don't pay attention to clicks during a rename.
2205 var listItem = this.ui_.listContainer.findListItemForNode(
2206 event.touchedElement || event.srcElement);
2207 var selection = this.getSelection();
2208 if (!listItem || !listItem.selected || selection.totalCount != 1) {
2212 // React on double click, but only if both clicks hit the same item.
2213 // TODO(mtomasz): Simplify it, and use a double click handler if possible.
2214 var clickNumber = (this.lastClickedItem_ == listItem) ? 2 : undefined;
2215 this.lastClickedItem_ = listItem;
2217 if (event.detail != clickNumber)
2220 var entry = selection.entries[0];
2221 if (entry.isDirectory) {
2222 this.onDirectoryAction_(entry);
2224 this.dispatchSelectionAction_();
2231 FileManager.prototype.dispatchSelectionAction_ = function() {
2232 if (this.dialogType == DialogType.FULL_PAGE) {
2233 var selection = this.getSelection();
2234 var tasks = selection.tasks;
2235 var urls = selection.urls;
2236 var mimeTypes = selection.mimeTypes;
2238 tasks.executeDefault();
2241 if (!this.ui_.dialogFooter.okButton.disabled) {
2242 this.ui_.dialogFooter.okButton.click();
2249 * Handles activate event of action menu item.
2253 FileManager.prototype.onActionMenuItemActivated_ = function() {
2254 var tasks = this.getSelection().tasks;
2256 tasks.execute(this.actionMenuItem_.taskId);
2260 * Opens the suggest file dialog.
2262 * @param {Entry} entry Entry of the file.
2263 * @param {function()} onSuccess Success callback.
2264 * @param {function()} onCancelled User-cancelled callback.
2265 * @param {function()} onFailure Failure callback.
2268 FileManager.prototype.openSuggestAppsDialog =
2269 function(entry, onSuccess, onCancelled, onFailure) {
2275 this.metadataCache_.getOne(entry, 'external', function(prop) {
2276 if (!prop || !prop.contentMimeType) {
2281 var basename = entry.name;
2282 var splitted = util.splitExtension(basename);
2283 var filename = splitted[0];
2284 var extension = splitted[1];
2285 var mime = prop.contentMimeType;
2287 // Returns with failure if the file has neither extension nor mime.
2288 if (!extension || !mime) {
2293 var onDialogClosed = function(result) {
2295 case SuggestAppsDialog.Result.INSTALL_SUCCESSFUL:
2298 case SuggestAppsDialog.Result.FAILED:
2306 if (FileTasks.EXECUTABLE_EXTENSIONS.indexOf(extension) !== -1) {
2307 this.suggestAppsDialog.showByFilename(filename, onDialogClosed);
2309 this.suggestAppsDialog.showByExtensionAndMime(
2310 extension, mime, onDialogClosed);
2316 * Called when a dialog is shown or hidden.
2317 * @param {boolean} show True if a dialog is shown, false if hidden.
2319 FileManager.prototype.onDialogShownOrHidden = function(show) {
2321 // If a dialog is shown, activate the window.
2322 var appWindow = chrome.app.window.current();
2327 // Set/unset a flag to disable dragging on the title area.
2328 this.dialogContainer_.classList.toggle('disable-header-drag', show);
2332 * Executes directory action (i.e. changes directory).
2334 * @param {DirectoryEntry} entry Directory entry to which directory should be
2338 FileManager.prototype.onDirectoryAction_ = function(entry) {
2339 return this.directoryModel_.changeDirectoryEntry(entry);
2343 * Update the window title.
2346 FileManager.prototype.updateTitle_ = function() {
2347 if (this.dialogType != DialogType.FULL_PAGE)
2350 if (!this.currentVolumeInfo_)
2353 this.document_.title = this.currentVolumeInfo_.label;
2357 * Update the gear menu.
2360 FileManager.prototype.updateGearMenu_ = function() {
2361 this.refreshRemainingSpace_(true); // Show loading caption.
2365 * Refreshes space info of the current volume.
2366 * @param {boolean} showLoadingCaption Whether show loading caption or not.
2369 FileManager.prototype.refreshRemainingSpace_ = function(showLoadingCaption) {
2370 if (!this.currentVolumeInfo_)
2373 var volumeSpaceInfo = /** @type {!HTMLElement} */
2374 (this.dialogDom_.querySelector('#volume-space-info'));
2375 var volumeSpaceInfoSeparator = /** @type {!HTMLElement} */
2376 (this.dialogDom_.querySelector('#volume-space-info-separator'));
2377 var volumeSpaceInfoLabel = /** @type {!HTMLElement} */
2378 (this.dialogDom_.querySelector('#volume-space-info-label'));
2379 var volumeSpaceInnerBar = /** @type {!HTMLElement} */
2380 (this.dialogDom_.querySelector('#volume-space-info-bar'));
2381 var volumeSpaceOuterBar = /** @type {!HTMLElement} */
2382 (this.dialogDom_.querySelector('#volume-space-info-bar').parentNode);
2384 var currentVolumeInfo = this.currentVolumeInfo_;
2386 // TODO(mtomasz): Add support for remaining space indication for provided
2388 if (currentVolumeInfo.volumeType ==
2389 VolumeManagerCommon.VolumeType.PROVIDED) {
2390 volumeSpaceInfo.hidden = true;
2391 volumeSpaceInfoSeparator.hidden = true;
2395 volumeSpaceInfo.hidden = false;
2396 volumeSpaceInfoSeparator.hidden = false;
2397 volumeSpaceInnerBar.setAttribute('pending', '');
2399 if (showLoadingCaption) {
2400 volumeSpaceInfoLabel.innerText = str('WAITING_FOR_SPACE_INFO');
2401 volumeSpaceInnerBar.style.width = '100%';
2404 chrome.fileManagerPrivate.getSizeStats(
2405 currentVolumeInfo.volumeId, function(result) {
2406 var volumeInfo = this.volumeManager_.getVolumeInfo(
2407 this.directoryModel_.getCurrentDirEntry());
2408 if (currentVolumeInfo !== this.currentVolumeInfo_)
2410 updateSpaceInfo(result,
2411 volumeSpaceInnerBar,
2412 volumeSpaceInfoLabel,
2413 volumeSpaceOuterBar);
2418 * Update the UI when the current directory changes.
2420 * @param {Event} event The directory-changed event.
2423 FileManager.prototype.onDirectoryChanged_ = function(event) {
2424 var oldCurrentVolumeInfo = this.currentVolumeInfo_;
2426 // Remember the current volume info.
2427 this.currentVolumeInfo_ = this.volumeManager_.getVolumeInfo(
2430 // If volume has changed, then update the gear menu.
2431 if (oldCurrentVolumeInfo !== this.currentVolumeInfo_) {
2432 this.updateGearMenu_();
2433 // If the volume has changed, and it was previously set, then do not
2434 // close on unmount anymore.
2435 if (oldCurrentVolumeInfo)
2436 this.closeOnUnmount_ = false;
2439 this.selectionHandler_.onFileSelectionChanged();
2440 this.searchController_.clear();
2441 // TODO(mtomasz): Consider remembering the selection.
2442 util.updateAppState(
2443 this.getCurrentDirectoryEntry() ?
2444 this.getCurrentDirectoryEntry().toURL() : '',
2445 '' /* selectionURL */,
2446 '' /* opt_param */);
2448 if (this.commandHandler)
2449 this.commandHandler.updateAvailability();
2451 this.updateUnformattedVolumeStatus_();
2452 this.updateTitle_();
2454 var currentEntry = this.getCurrentDirectoryEntry();
2455 this.ui_.locationLine.show(currentEntry);
2456 this.ui_.previewPanel.currentEntry = util.isFakeEntry(currentEntry) ?
2457 null : currentEntry;
2460 FileManager.prototype.updateUnformattedVolumeStatus_ = function() {
2461 var volumeInfo = this.volumeManager_.getVolumeInfo(
2462 this.directoryModel_.getCurrentDirEntry());
2464 if (volumeInfo && volumeInfo.error) {
2465 this.dialogDom_.setAttribute('unformatted', '');
2467 var errorNode = this.dialogDom_.querySelector('#format-panel > .error');
2468 if (volumeInfo.error ===
2469 VolumeManagerCommon.VolumeError.UNSUPPORTED_FILESYSTEM) {
2470 errorNode.textContent = str('UNSUPPORTED_FILESYSTEM_WARNING');
2472 errorNode.textContent = str('UNKNOWN_FILESYSTEM_WARNING');
2475 // Update 'canExecute' for format command so the format button's disabled
2476 // property is properly set.
2477 if (this.commandHandler)
2478 this.commandHandler.updateAvailability();
2480 this.dialogDom_.removeAttribute('unformatted');
2485 * Unload handler for the page.
2488 FileManager.prototype.onUnload_ = function() {
2489 if (this.directoryModel_)
2490 this.directoryModel_.dispose();
2491 if (this.volumeManager_)
2492 this.volumeManager_.dispose();
2493 if (this.fileTransferController_) {
2495 i < this.fileTransferController_.pendingTaskIds.length;
2497 var taskId = this.fileTransferController_.pendingTaskIds[i];
2499 this.backgroundPage_.background.progressCenter.getItemById(taskId);
2501 item.state = ProgressItemState.CANCELED;
2502 this.backgroundPage_.background.progressCenter.updateItem(item);
2505 if (this.progressCenterPanel_) {
2506 this.backgroundPage_.background.progressCenter.removePanel(
2507 this.progressCenterPanel_);
2509 if (this.fileOperationManager_) {
2510 if (this.onCopyProgressBound_) {
2511 this.fileOperationManager_.removeEventListener(
2512 'copy-progress', this.onCopyProgressBound_);
2514 if (this.onEntriesChangedBound_) {
2515 this.fileOperationManager_.removeEventListener(
2516 'entries-changed', this.onEntriesChangedBound_);
2519 window.closing = true;
2520 if (this.backgroundPage_)
2521 this.backgroundPage_.background.tryClose();
2527 FileManager.prototype.onFilenameInputInput_ = function() {
2528 this.selectionHandler_.updateOkButton();
2532 * @param {Event} event Key event.
2535 FileManager.prototype.onFilenameInputKeyDown_ = function(event) {
2536 if ((util.getKeyModifiers(event) + event.keyCode) === '13' /* Enter */)
2537 this.ui_.dialogFooter.okButton.click();
2541 * @param {Event} event Focus event.
2544 FileManager.prototype.onFilenameInputFocus_ = function(event) {
2545 var input = this.ui_.dialogFooter.filenameInput;
2547 // On focus we want to select everything but the extension, but
2548 // Chrome will select-all after the focus event completes. We
2549 // schedule a timeout to alter the focus after that happens.
2550 setTimeout(function() {
2551 var selectionEnd = input.value.lastIndexOf('.');
2552 if (selectionEnd == -1) {
2555 input.selectionStart = 0;
2556 input.selectionEnd = selectionEnd;
2561 FileManager.prototype.createNewFolder = function() {
2562 var defaultName = str('DEFAULT_NEW_FOLDER_NAME');
2564 // Find a name that doesn't exist in the data model.
2565 var files = this.directoryModel_.getFileList();
2567 for (var i = 0; i < files.length; i++) {
2568 var name = files.item(i).name;
2569 // Filtering names prevents from conflicts with prototype's names
2571 if (name.substring(0, defaultName.length) == defaultName)
2575 var baseName = defaultName;
2580 var advance = function() {
2586 var current = function() {
2587 return baseName + separator + index + suffix;
2590 // Accessing hasOwnProperty is safe since hash properties filtered.
2591 while (hash.hasOwnProperty(current())) {
2596 var list = self.ui_.listContainer.currentList;
2598 var onSuccess = function(entry) {
2599 metrics.recordUserAction('CreateNewFolder');
2600 list.selectedItem = entry;
2602 self.ui_.listContainer.endBatchUpdates();
2604 self.namingController_.initiateRename();
2607 var onError = function(error) {
2608 self.ui_.listContainer.endBatchUpdates();
2610 self.alert.show(strf('ERROR_CREATING_FOLDER', current(),
2611 util.getFileErrorString(error.name)));
2614 var onAbort = function() {
2615 self.ui_.listContainer.endBatchUpdates();
2618 this.ui_.listContainer.startBatchUpdates();
2619 this.directoryModel_.createDirectory(current(),
2626 * Handles click event on the toggle-view button.
2627 * @param {Event} event Click event.
2630 FileManager.prototype.onToggleViewButtonClick_ = function(event) {
2631 if (this.ui_.listContainer.currentListType ===
2632 ListContainer.ListType.DETAIL) {
2633 this.setListType(ListContainer.ListType.THUMBNAIL);
2635 this.setListType(ListContainer.ListType.DETAIL);
2638 event.target.blur();
2642 * KeyDown event handler for the document.
2643 * @param {Event} event Key event.
2646 FileManager.prototype.onKeyDown_ = function(event) {
2647 if (event.keyCode === 9) // Tab
2648 this.pressingTab_ = true;
2649 if (event.keyCode === 17) // Ctrl
2650 this.pressingCtrl_ = true;
2652 if (event.srcElement === this.ui_.listContainer.renameInput) {
2653 // Ignore keydown handler in the rename input box.
2657 switch (util.getKeyModifiers(event) + event.keyIdentifier) {
2658 case 'Ctrl-U+00BE': // Ctrl-. => Toggle filter files.
2659 this.fileFilter_.setFilterHidden(
2660 !this.fileFilter_.isFilterHiddenOn());
2661 event.preventDefault();
2664 case 'U+001B': // Escape => Cancel dialog.
2665 if (this.dialogType != DialogType.FULL_PAGE) {
2666 // If there is nothing else for ESC to do, then cancel the dialog.
2667 event.preventDefault();
2668 this.ui_.dialogFooter.cancelButton.click();
2675 * KeyUp event handler for the document.
2676 * @param {Event} event Key event.
2679 FileManager.prototype.onKeyUp_ = function(event) {
2680 if (event.keyCode === 9) // Tab
2681 this.pressingTab_ = false;
2682 if (event.keyCode == 17) // Ctrl
2683 this.pressingCtrl_ = false;
2687 * KeyDown event handler for the div#list-container element.
2688 * @param {Event} event Key event.
2691 FileManager.prototype.onListKeyDown_ = function(event) {
2692 switch (util.getKeyModifiers(event) + event.keyIdentifier) {
2693 case 'U+0008': // Backspace => Up one directory.
2694 event.preventDefault();
2695 // TODO(mtomasz): Use Entry.getParent() instead.
2696 if (!this.getCurrentDirectoryEntry())
2698 var currentEntry = this.getCurrentDirectoryEntry();
2699 var locationInfo = this.volumeManager_.getLocationInfo(currentEntry);
2700 // TODO(mtomasz): There may be a tiny race in here.
2701 if (locationInfo && !locationInfo.isRootEntry &&
2702 !locationInfo.isSpecialSearchRoot) {
2703 currentEntry.getParent(function(parentEntry) {
2704 this.directoryModel_.changeDirectoryEntry(parentEntry);
2705 }.bind(this), function() { /* Ignore errors. */});
2709 case 'Enter': // Enter => Change directory or perform default action.
2710 // TODO(dgozman): move directory action to dispatchSelectionAction.
2711 var selection = this.getSelection();
2712 if (selection.totalCount === 1 &&
2713 selection.entries[0].isDirectory &&
2714 !DialogType.isFolderDialog(this.dialogType)) {
2715 var item = this.ui.listContainer.currentList.getListItemByIndex(
2716 selection.indexes[0]);
2717 // If the item is in renaming process, we don't allow to change
2719 if (!item.hasAttribute('renaming')) {
2720 event.preventDefault();
2721 this.onDirectoryAction_(selection.entries[0]);
2723 } else if (this.dispatchSelectionAction_()) {
2724 event.preventDefault();
2731 * Performs a 'text search' - selects a first list entry with name
2732 * starting with entered text (case-insensitive).
2735 FileManager.prototype.onTextSearch_ = function() {
2736 var text = this.ui_.listContainer.textSearchState.text;
2737 var dm = this.directoryModel_.getFileList();
2738 for (var index = 0; index < dm.length; ++index) {
2739 var name = dm.item(index).name;
2740 if (name.substring(0, text.length).toLowerCase() == text) {
2741 this.ui.listContainer.currentList.selectionModel.selectedIndexes =
2747 this.ui_.listContainer.textSearchState.text = '';
2751 * Verifies the user entered name for file or folder to be created or
2752 * renamed to. See also util.validateFileName.
2754 * @param {DirectoryEntry} parentEntry The URL of the parent directory entry.
2755 * @param {string} name New file or folder name.
2756 * @param {function(boolean)} onDone Function to invoke when user closes the
2757 * warning box or immediatelly if file name is correct. If the name was
2758 * valid it is passed true, and false otherwise.
2761 FileManager.prototype.validateFileName_ = function(
2762 parentEntry, name, onDone) {
2763 var fileNameErrorPromise = util.validateFileName(
2766 this.fileFilter_.isFilterHiddenOn());
2767 fileNameErrorPromise.then(onDone.bind(null, true), function(message) {
2768 this.alert.show(message, onDone.bind(null, false));
2769 }.bind(this)).catch(function(error) {
2770 console.error(error.stack || error);
2775 * Toggle whether mobile data is used for sync.
2777 FileManager.prototype.toggleDriveSyncSettings = function() {
2778 // If checked, the sync is disabled.
2779 var nowCellularDisabled = this.syncButton.hasAttribute('checked');
2780 var changeInfo = {cellularDisabled: !nowCellularDisabled};
2781 chrome.fileManagerPrivate.setPreferences(changeInfo);
2785 * Toggle whether Google Docs files are shown.
2787 FileManager.prototype.toggleDriveHostedSettings = function() {
2788 // If checked, showing drive hosted files is enabled.
2789 var nowHostedFilesEnabled = this.hostedButton.hasAttribute('checked');
2790 var nowHostedFilesDisabled = !nowHostedFilesEnabled;
2792 var changeInfo = {hostedFilesDisabled: !nowHostedFilesDisabled};
2794 var changeInfo = {};
2795 changeInfo['hostedFilesDisabled'] = !nowHostedFilesDisabled;
2796 chrome.fileManagerPrivate.setPreferences(changeInfo);
2799 FileManager.prototype.decorateSplitter = function(splitterElement) {
2802 var Splitter = cr.ui.Splitter;
2804 var customSplitter = cr.ui.define('div');
2806 customSplitter.prototype = {
2807 __proto__: Splitter.prototype,
2809 handleSplitterDragStart: function(e) {
2810 Splitter.prototype.handleSplitterDragStart.apply(this, arguments);
2811 this.ownerDocument.documentElement.classList.add('col-resize');
2814 handleSplitterDragMove: function(deltaX) {
2815 Splitter.prototype.handleSplitterDragMove.apply(this, arguments);
2819 handleSplitterDragEnd: function(e) {
2820 Splitter.prototype.handleSplitterDragEnd.apply(this, arguments);
2821 this.ownerDocument.documentElement.classList.remove('col-resize');
2825 customSplitter.decorate(splitterElement);
2829 * Updates action menu item to match passed task items.
2831 * @param {Array.<Object>=} opt_items List of items.
2833 FileManager.prototype.updateContextMenuActionItems = function(opt_items) {
2834 var items = opt_items || [];
2836 // When only one task is available, show it as default item.
2837 if (items.length === 1) {
2838 var actionItem = items[0];
2840 if (actionItem.iconType) {
2841 this.actionMenuItem_.style.backgroundImage = '';
2842 this.actionMenuItem_.setAttribute('file-type-icon',
2843 actionItem.iconType);
2844 } else if (actionItem.iconUrl) {
2845 this.actionMenuItem_.style.backgroundImage =
2846 'url(' + actionItem.iconUrl + ')';
2848 this.actionMenuItem_.style.backgroundImage = '';
2851 this.actionMenuItem_.label =
2852 actionItem.taskId === FileTasks.ZIP_UNPACKER_TASK_ID ?
2853 str('ACTION_OPEN') : actionItem.title;
2854 this.actionMenuItem_.disabled = !!actionItem.disabled;
2855 this.actionMenuItem_.taskId = actionItem.taskId;
2858 this.actionMenuItem_.hidden = items.length !== 1;
2860 // When multiple tasks are available, show them in open with.
2861 this.openWithCommand_.canExecuteChange();
2862 this.openWithCommand_.setHidden(items.length < 2);
2863 this.openWithCommand_.disabled = items.length < 2;
2865 // Hide default action separator when there does not exist available task.
2866 var defaultActionSeparator =
2867 this.dialogDom_.querySelector('#default-action-separator');
2868 defaultActionSeparator.hidden = items.length === 0;
2872 * @return {FileSelection} Selection object.
2874 FileManager.prototype.getSelection = function() {
2875 return this.selectionHandler_.selection;
2879 * @return {cr.ui.ArrayDataModel} File list.
2881 FileManager.prototype.getFileList = function() {
2882 return this.directoryModel_.getFileList();
2886 * @return {!cr.ui.List} Current list object.
2888 FileManager.prototype.getCurrentList = function() {
2889 return this.ui.listContainer.currentList;
2893 * Retrieve the preferences of the files.app. This method caches the result
2894 * and returns it unless opt_update is true.
2895 * @param {function(Object.<string, *>)} callback Callback to get the
2897 * @param {boolean=} opt_update If is's true, don't use the cache and
2898 * retrieve latest preference. Default is false.
2901 FileManager.prototype.getPreferences_ = function(callback, opt_update) {
2902 if (!opt_update && this.preferences_ !== null) {
2903 callback(this.preferences_);
2907 chrome.fileManagerPrivate.getPreferences(function(prefs) {
2908 this.preferences_ = prefs;
2914 * @param {FileEntry} entry
2917 FileManager.prototype.doEntryAction_ = function(entry) {
2918 if (this.dialogType == DialogType.FULL_PAGE) {
2919 this.metadataCache_.get([entry], 'external', function(props) {
2920 var tasks = new FileTasks(this);
2921 tasks.init([entry], [props[0].contentMimeType || '']);
2922 tasks.executeDefault_();
2925 var selection = this.getSelection();
2926 if (selection.entries.length === 1 &&
2927 util.isSameEntry(selection.entries[0], entry)) {
2928 this.ui_.dialogFooter.okButton.click();
2934 * Outputs the current state for debugging.
2936 FileManager.prototype.debugMe = function() {
2937 var out = 'Debug information.\n' +
2938 '1. fileManager.initializeQueue_.pendingTasks_\n';
2939 var keys = Object.keys(this.initializeQueue_.pendingTasks);
2940 out += 'Length: ' + keys.length + '\n';
2941 keys.forEach(function(key) {
2942 out += this.initializeQueue_.pendingTasks[key].toString() + '\n';
2945 out += '2. VolumeManagerWrapper\n' +
2946 this.volumeManager_.toString() + '\n';
2948 out += 'End of debug information.';