blob: db0993fff3bdc6f954f5ce3771414227d2b1c991 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* FileManager constructor.
*
* FileManager objects encapsulate the functionality of the file selector
* dialogs, as well as the full screen file manager application.
*
* @implements {CommandHandlerDeps}
* @constructor
* @struct
*/
function FileManager() {
// --------------------------------------------------------------------------
// Services FileManager depends on.
/**
* Volume manager.
* @type {FilteredVolumeManager}
* @private
*/
this.volumeManager_ = null;
/** @private {importer.HistoryLoader} */
this.historyLoader_ = null;
/**
* ImportHistory. Non-null only once history observer is added in
* {@code addHistoryObserver}.
*
* @type {importer.ImportHistory}
* @private
*/
this.importHistory_ = null;
/**
* Bound observer for use with {@code importer.ImportHistory.Observer}.
* The instance is bound once here as {@code ImportHistory.removeObserver}
* uses object equivilency to remove observers.
*
* @private {function(!importer.ImportHistory.ChangedEvent)}
*/
this.onHistoryChangedBound_ = this.onHistoryChanged_.bind(this);
/** @private {importer.MediaScanner} */
this.mediaScanner_ = null;
/** @private {importer.ImportController} */
this.importController_ = null;
/** @private {importer.ImportRunner} */
this.mediaImportHandler_ = null;
/**
* @private {MetadataModel}
*/
this.metadataModel_ = null;
/**
* @private {!FileMetadataFormatter}
*/
this.fileMetadataFormatter_ = new FileMetadataFormatter();
/**
* @private {ThumbnailModel}
*/
this.thumbnailModel_ = null;
/**
* File operation manager.
* @type {FileOperationManager}
* @private
*/
this.fileOperationManager_ = null;
/**
* File filter.
* @private {FileFilter}
*/
this.fileFilter_ = null;
/**
* Model of current directory.
* @type {DirectoryModel}
* @private
*/
this.directoryModel_ = null;
/**
* Model of folder shortcuts.
* @type {FolderShortcutsDataModel}
* @private
*/
this.folderShortcutsModel_ = null;
/**
* Model for providers (providing extensions).
* @type {ProvidersModel}
* @private
*/
this.providersModel_ = null;
/**
* Model for quick view.
* @type {QuickViewModel}
* @private
*/
this.quickViewModel_ = null;
/**
* Controller for actions for current selection.
* @private {ActionsController}
*/
this.actionsController_ = null;
/**
* Handler for command events.
* @private {CommandHandler}
*/
this.commandHandler_ = null;
/**
* Handler for the change of file selection.
* @type {FileSelectionHandler}
* @private
*/
this.selectionHandler_ = null;
/**
* UI management class of file manager.
* @type {FileManagerUI}
* @private
*/
this.ui_ = null;
/**
* @private {analytics.Tracker}
*/
this.tracker_ = null;
// --------------------------------------------------------------------------
// Parameters determining the type of file manager.
/**
* Dialog type of this window.
* @type {DialogType}
*/
this.dialogType = DialogType.FULL_PAGE;
/**
* Startup parameters for this application.
* @type {LaunchParam}
* @private
*/
this.launchParams_ = null;
/**
* Whether to allow touch-specific interaction.
* @type {boolean}
*/
this.enableTouchMode_ = false;
// --------------------------------------------------------------------------
// Controllers.
/**
* File transfer controller.
* @type {FileTransferController}
* @private
*/
this.fileTransferController_ = null;
/**
* Naming controller.
* @type {NamingController}
* @private
*/
this.namingController_ = null;
/**
* Directory tree naming controller.
* @private {DirectoryTreeNamingController}
*/
this.directoryTreeNamingController_ = null;
/**
* Controller for search UI.
* @type {SearchController}
* @private
*/
this.searchController_ = null;
/**
* Controller for directory scan.
* @type {ScanController}
* @private
*/
this.scanController_ = null;
/**
* Controller for spinner.
* @type {SpinnerController}
* @private
*/
this.spinnerController_ = null;
/**
* Sort menu controller.
* @type {SortMenuController}
* @private
*/
this.sortMenuController_ = null;
/**
* Gear menu controller.
* @type {GearMenuController}
* @private
*/
this.gearMenuController_ = null;
/**
* Controller for the context menu opened by the action bar button in the
* check-select mode.
* @type {SelectionMenuController}
* @private
*/
this.selectionMenuController_ = null;
/**
* Toolbar controller.
* @type {ToolbarController}
* @private
*/
this.toolbarController_ = null;
/**
* Empty folder controller.
* @private {EmptyFolderController}
*/
this.emptyFolderController_ = null;
/**
* App state controller.
* @type {AppStateController}
* @private
*/
this.appStateController_ = null;
/**
* Dialog action controller.
* @type {DialogActionController}
* @private
*/
this.dialogActionController_ = null;
/**
* List update controller.
* @type {MetadataUpdateController}
* @private
*/
this.metadataUpdateController_ = null;
/**
* Last modified controller.
* @private {LastModifiedController}
*/
this.lastModifiedController_ = null;
/**
* Component for main window and its misc UI parts.
* @type {MainWindowComponent}
* @private
*/
this.mainWindowComponent_ = null;
/**
* @type {TaskController}
* @private
*/
this.taskController_ = null;
/** @private {ColumnVisibilityController} */
this.columnVisibilityController_ = null;
/**
* @type {QuickViewUma}
* @private
*/
this.quickViewUma_ = null;
/**
* @type {QuickViewController}
* @private
*/
this.quickViewController_ = null;
/**
* Records histograms of directory-changed event.
* @type {NavigationUma}
* @private
*/
this.navigationUma_ = null;
// --------------------------------------------------------------------------
// DOM elements.
/**
* Background page.
* @type {BackgroundWindow}
* @private
*/
this.backgroundPage_ = null;
/**
* @type {FileBrowserBackgroundFull}
* @private
*/
this.fileBrowserBackground_ = null;
/**
* The root DOM element of this app.
* @type {HTMLBodyElement}
* @private
*/
this.dialogDom_ = null;
/**
* The document object of this app.
* @type {Document}
* @private
*/
this.document_ = null;
// --------------------------------------------------------------------------
// Miscellaneous FileManager's states.
/**
* Promise object which is fullfilled when initialization for app state
* controller is done.
* @type {Promise}
* @private
*/
this.initSettingsPromise_ = null;
/**
* Promise object which is fullfilled when initialization related to the
* background page is done.
* @type {Promise}
* @private
*/
this.initBackgroundPagePromise_ = null;
/**
* Flags async retrieved once at startup and can be used to switch behaviour
* on sync functions.
* @dict
* @private
*/
this.commandLineFlags_ = {};
}
FileManager.prototype = /** @struct */ {
__proto__: cr.EventTarget.prototype,
/**
* @return {DirectoryModel}
*/
get directoryModel() {
return this.directoryModel_;
},
/**
* @return {DirectoryTreeNamingController}
*/
get directoryTreeNamingController() {
return this.directoryTreeNamingController_;
},
/**
* @return {FileFilter}
*/
get fileFilter() {
return this.fileFilter_;
},
/**
* @return {FolderShortcutsDataModel}
*/
get folderShortcutsModel() {
return this.folderShortcutsModel_;
},
/**
* @return {ActionsController}
*/
get actionsController() {
return this.actionsController_;
},
/**
* @return {CommandHandler}
*/
get commandHandler() {
return this.commandHandler_;
},
/**
* @return {ProvidersModel}
*/
get providersModel() {
return this.providersModel_;
},
/**
* @return {MetadataModel}
*/
get metadataModel() {
return this.metadataModel_;
},
/**
* @return {FileSelectionHandler}
*/
get selectionHandler() {
return this.selectionHandler_;
},
/**
* @return {DirectoryTree}
*/
get directoryTree() {
return this.ui_.directoryTree;
},
/**
* @return {Document}
*/
get document() {
return this.document_;
},
/**
* @return {FileTransferController}
*/
get fileTransferController() {
return this.fileTransferController_;
},
/**
* @return {NamingController}
*/
get namingController() {
return this.namingController_;
},
/**
* @return {TaskController}
*/
get taskController() {
return this.taskController_;
},
/**
* @return {SpinnerController}
*/
get spinnerController() {
return this.spinnerController_;
},
/**
* @return {FileOperationManager}
*/
get fileOperationManager() {
return this.fileOperationManager_;
},
/**
* @return {BackgroundWindow}
*/
get backgroundPage() {
return this.backgroundPage_;
},
/**
* @return {FilteredVolumeManager}
*/
get volumeManager() {
return this.volumeManager_;
},
/**
* @return {importer.ImportController}
*/
get importController() {
return this.importController_;
},
/**
* @return {importer.HistoryLoader}
*/
get historyLoader() {
return this.historyLoader_;
},
/**
* @return {importer.ImportRunner}
*/
get mediaImportHandler() {
return this.mediaImportHandler_;
},
/**
* @return {FileManagerUI}
*/
get ui() {
return this.ui_;
},
/**
* @return {analytics.Tracker}
*/
get tracker() {
return this.tracker_;
}
};
// Anonymous "namespace".
(function() {
/**
* One time initialization for app state controller to load view option from
* local storage.
* @return {!Promise} A promise to be fillfilled when initialization is done.
* @private
*/
FileManager.prototype.startInitSettings_ = function() {
metrics.startInterval('Load.InitSettings');
this.appStateController_ = new AppStateController(this.dialogType);
return Promise
.all([
this.appStateController_.loadInitialViewOptions(),
util.isMyFilesNavigationDisabled(),
])
.then(values => {
this.commandLineFlags_['disable-my-files-navigation'] =
/** @type {boolean} */ (values[1]);
metrics.recordInterval('Load.InitSettings');
});
};
/**
* One time initialization for the file system and related things.
* @private
*/
FileManager.prototype.initFileSystemUI_ = function() {
this.ui_.listContainer.startBatchUpdates();
this.initFileList_();
this.setupCurrentDirectory_();
var self = this;
var listBeingUpdated = null;
this.directoryModel_.addEventListener('begin-update-files', function() {
self.ui_.listContainer.currentList.startBatchUpdates();
// Remember the list which was used when updating files started, so
// endBatchUpdates() is called on the same list.
listBeingUpdated = self.ui_.listContainer.currentList;
});
this.directoryModel_.addEventListener('end-update-files', function() {
self.namingController_.restoreItemBeingRenamed();
listBeingUpdated.endBatchUpdates();
listBeingUpdated = null;
});
this.volumeManager_.addEventListener(
VolumeManagerCommon.ARCHIVE_OPENED_EVENT_TYPE, function(event) {
assert(event.detail.mountPoint);
if (window.isFocused())
this.directoryModel_.changeDirectoryEntry(event.detail.mountPoint);
}.bind(this));
this.directoryModel_.addEventListener(
'directory-changed',
(/** @param {!Event} event */
function(event) {
this.navigationUma_.onDirectoryChanged(event.newDirEntry);
}).bind(this));
this.initCommands_();
assert(this.directoryModel_);
assert(this.spinnerController_);
assert(this.commandHandler_);
assert(this.selectionHandler_);
assert(this.launchParams_);
assert(this.volumeManager_);
assert(this.dialogDom_);
assert(this.fileFilter_);
this.scanController_ = new ScanController(
this.directoryModel_,
this.ui_.listContainer,
this.spinnerController_,
this.commandHandler_,
this.selectionHandler_);
this.sortMenuController_ = new SortMenuController(
this.ui_.sortButton,
this.ui_.sortButtonToggleRipple,
assert(this.directoryModel_.getFileList()));
this.gearMenuController_ = new GearMenuController(
this.ui_.gearButton,
this.ui_.gearButtonToggleRipple,
this.ui_.gearMenu,
this.directoryModel_,
this.commandHandler_,
assert(this.providersModel_));
this.selectionMenuController_ = new SelectionMenuController(
this.ui_.selectionMenuButton,
util.queryDecoratedElement('#file-context-menu', cr.ui.Menu));
this.toolbarController_ = new ToolbarController(
this.ui_.toolbar,
this.ui_.dialogNavigationList,
this.ui_.listContainer,
assert(this.ui_.locationLine),
this.selectionHandler_,
this.directoryModel_);
this.emptyFolderController_ = new EmptyFolderController(
this.ui_.emptyFolder, this.directoryModel_, this.ui_.alertDialog);
this.actionsController_ = new ActionsController(
this.volumeManager_, assert(this.metadataModel_), this.directoryModel_,
assert(this.folderShortcutsModel_),
this.fileBrowserBackground_.driveSyncHandler,
this.selectionHandler_, assert(this.ui_));
this.lastModifiedController_ = new LastModifiedController(
this.ui_.listContainer.table, this.directoryModel_);
this.quickViewModel_ = new QuickViewModel();
var fileListSelectionModel = /** @type {!cr.ui.ListSelectionModel} */ (
this.directoryModel_.getFileListSelection());
this.quickViewUma_ =
new QuickViewUma(assert(this.volumeManager_), assert(this.dialogType));
var metadataBoxController = new MetadataBoxController(
this.metadataModel_, this.quickViewModel_, this.fileMetadataFormatter_);
this.quickViewController_ = new QuickViewController(
assert(this.metadataModel_), assert(this.selectionHandler_),
assert(this.ui_.listContainer), assert(this.ui_.selectionMenuButton),
assert(this.quickViewModel_), assert(this.taskController_),
fileListSelectionModel, assert(this.quickViewUma_),
metadataBoxController, this.dialogType, assert(this.volumeManager_));
if (this.dialogType === DialogType.FULL_PAGE) {
importer.importEnabled().then(
function(enabled) {
if (enabled) {
this.importController_ = new importer.ImportController(
new importer.RuntimeControllerEnvironment(
this,
assert(this.selectionHandler_)),
assert(this.mediaScanner_),
assert(this.mediaImportHandler_),
new importer.RuntimeCommandWidget(),
assert(this.tracker_));
}
}.bind(this));
}
assert(this.fileFilter_);
assert(this.namingController_);
assert(this.appStateController_);
assert(this.taskController_);
this.mainWindowComponent_ = new MainWindowComponent(
this.dialogType,
this.ui_,
this.volumeManager_,
this.directoryModel_,
this.fileFilter_,
this.selectionHandler_,
this.namingController_,
this.appStateController_,
this.taskController_);
this.initDataTransferOperations_();
this.selectionHandler_.onFileSelectionChanged();
this.ui_.listContainer.endBatchUpdates();
this.ui_.initBanners(
new Banners(
this.directoryModel_,
this.volumeManager_,
this.document_,
// Whether to show any welcome banner.
this.dialogType === DialogType.FULL_PAGE));
this.ui_.attachFilesTooltip();
this.ui_.decorateFilesMenuItems();
util.isTouchModeEnabled().then(function(isEnabled) {
if (isEnabled) {
this.ui_.selectionMenuButton.hidden = false;
this.enableTouchMode_ = true;
}
}.bind(this));
};
/**
* @private
*/
FileManager.prototype.initDataTransferOperations_ = function() {
// CopyManager are required for 'Delete' operation in
// Open and Save dialogs. But drag-n-drop and copy-paste are not needed.
if (this.dialogType !== DialogType.FULL_PAGE)
return;
this.fileTransferController_ = new FileTransferController(
assert(this.document_), assert(this.ui_.listContainer),
assert(this.ui_.directoryTree), this.ui_.multiProfileShareDialog,
this.ui_.showConfirmationDialog.bind(this.ui_),
assert(this.fileBrowserBackground_.progressCenter),
assert(this.fileOperationManager_), assert(this.metadataModel_),
assert(this.thumbnailModel_), assert(this.directoryModel_),
assert(this.volumeManager_), assert(this.selectionHandler_),
CommandUtil.shouldShowMenuItemsForEntry.bind(
null, assert(this.volumeManager_)));
};
/**
* One-time initialization of commands.
* @private
*/
FileManager.prototype.initCommands_ = function() {
assert(this.ui_.textContextMenu);
this.commandHandler_ =
new CommandHandler(this, assert(this.selectionHandler_));
// TODO(hirono): Move the following block to the UI part.
var commandButtons = this.dialogDom_.querySelectorAll('button[command]');
for (var j = 0; j < commandButtons.length; j++)
CommandButton.decorate(commandButtons[j]);
var inputs = this.getDomInputs_();
for (let input of inputs)
this.setContextMenuForInput_(input);
this.setContextMenuForInput_(this.ui_.listContainer.renameInput);
this.setContextMenuForInput_(
this.directoryTreeNamingController_.getInputElement());
this.document_.addEventListener(
'command',
this.ui_.listContainer.clearHover.bind(this.ui_.listContainer));
};
/**
* Get input elements from root DOM element of this app.
* @private
*/
FileManager.prototype.getDomInputs_ = function() {
return this.dialogDom_.querySelectorAll(
'input[type=text], input[type=search], textarea, cr-input');
};
/**
* Set context menu and handlers for an input element.
* @private
*/
FileManager.prototype.setContextMenuForInput_ = function(input) {
var touchInduced = false;
// stop contextmenu propagation for touch-induced events.
input.addEventListener('touchstart', (e) => {
touchInduced = true;
});
input.addEventListener('contextmenu', (e) => {
if (touchInduced) {
e.stopImmediatePropagation();
}
touchInduced = false;
});
input.addEventListener('click', (e) => {
touchInduced = false;
});
cr.ui.contextMenuHandler.setContextMenu(input, this.ui_.textContextMenu);
this.registerInputCommands_(input);
};
/**
* Registers cut, copy, paste and delete commands on input element.
*
* @param {Node} node Text input element to register on.
* @private
*/
FileManager.prototype.registerInputCommands_ = function(node) {
CommandUtil.forceDefaultHandler(node, 'cut');
CommandUtil.forceDefaultHandler(node, 'copy');
CommandUtil.forceDefaultHandler(node, 'paste');
CommandUtil.forceDefaultHandler(node, 'delete');
node.addEventListener('keydown', function(e) {
var key = util.getKeyModifiers(e) + e.keyCode;
if (key === '190' /* '/' */ || key === '191' /* '.' */) {
// If this key event is propagated, this is handled search command,
// which calls 'preventDefault' method.
e.stopPropagation();
}
});
};
/**
* Entry point of the initialization.
* This method is called from main.js.
*/
FileManager.prototype.initializeCore = function() {
this.initGeneral_();
this.initSettingsPromise_ = this.startInitSettings_();
this.initBackgroundPagePromise_ = this.startInitBackgroundPage_();
this.initBackgroundPagePromise_.then(function() {
this.initVolumeManager_();
}.bind(this));
window.addEventListener('pagehide', this.onUnload_.bind(this));
};
/**
* @return {!Promise} A promise to be fillfilled when initialization is done.
*/
FileManager.prototype.initializeUI = function(dialogDom) {
this.dialogDom_ = dialogDom;
this.document_ = this.dialogDom_.ownerDocument;
metrics.startInterval('Load.InitDocuments');
return Promise
.all([this.initBackgroundPagePromise_, window.importElementsPromise])
.then(function() {
metrics.recordInterval('Load.InitDocuments');
metrics.startInterval('Load.InitUI');
this.initEssentialUI_();
this.initAdditionalUI_();
return this.initSettingsPromise_;
}.bind(this))
.then(function() {
this.initFileSystemUI_();
this.initUIFocus_();
metrics.recordInterval('Load.InitUI');
}.bind(this));
};
/**
* Initializes general purpose basic things, which are used by other
* initializing methods.
*
* @private
*/
FileManager.prototype.initGeneral_ = function() {
// Initialize the application state.
// TODO(mtomasz): Unify window.appState with location.search format.
if (window.appState) {
var params = {};
for (var name in window.appState) {
params[name] = window.appState[name];
}
for (var name in window.appState.params) {
params[name] = window.appState.params[name];
}
this.launchParams_ = new LaunchParam(params);
} else {
// Used by the select dialog only.
var json = location.search ?
JSON.parse(decodeURIComponent(location.search.substr(1))) : {};
this.launchParams_ = new LaunchParam(json instanceof Object ? json : {});
}
// Initialize the member variables that depend this.launchParams_.
this.dialogType = this.launchParams_.type;
// We used to share the tracker with background, but due to
// its use of instanceof checks for some functionality
// we really can't do this (as instanceof checks fail across
// different script contexts).
this.tracker_ = metrics.getTracker();
};
/**
* Initializes the background page.
* @return {!Promise} A promise to be fillfilled when initialization is done.
* @private
*/
FileManager.prototype.startInitBackgroundPage_ = function() {
return new Promise(function(resolve) {
metrics.startInterval('Load.InitBackgroundPage');
chrome.runtime.getBackgroundPage(/** @type {function(Window=)} */ (
function(opt_backgroundPage) {
assert(opt_backgroundPage);
this.backgroundPage_ =
/** @type {!BackgroundWindow} */ (opt_backgroundPage);
this.fileBrowserBackground_ =
/** @type {!FileBrowserBackgroundFull} */ (
this.backgroundPage_.background);
this.fileBrowserBackground_.ready(function() {
loadTimeData.data = this.fileBrowserBackground_.stringData;
if (util.runningInBrowser())
this.backgroundPage_.registerDialog(window);
this.fileOperationManager_ =
this.fileBrowserBackground_.fileOperationManager;
this.mediaImportHandler_ =
this.fileBrowserBackground_.mediaImportHandler;
this.mediaScanner_ =
this.fileBrowserBackground_.mediaScanner;
this.historyLoader_ =
this.fileBrowserBackground_.historyLoader;
metrics.recordInterval('Load.InitBackgroundPage');
resolve();
}.bind(this));
}.bind(this)));
}.bind(this));
};
/**
* Initializes the VolumeManager instance.
* @private
*/
FileManager.prototype.initVolumeManager_ = function() {
var allowedPaths = this.getAllowedPaths_();
var writableOnly =
this.launchParams_.type === DialogType.SELECT_SAVEAS_FILE;
// FilteredVolumeManager hides virtual file system related event and data
// even depends on the value of |supportVirtualPath|. If it is
// VirtualPathSupport.NO_VIRTUAL_PATH, it hides Drive even if Drive is
// enabled on preference.
// In other words, even if Drive is disabled on preference but the Files app
// should show Drive when it is re-enabled, then the value should be set to
// true.
// Note that the Drive enabling preference change is listened by
// DriveIntegrationService, so here we don't need to take care about it.
this.volumeManager_ = new FilteredVolumeManager(
allowedPaths, writableOnly, this.backgroundPage_);
};
/**
* One time initialization of the essential UI elements in the Files app.
* These elements will be shown to the user. Only visible elements should be
* initialized here. Any heavy operation should be avoided. The Files app's
* window is shown at the end of this routine.
* @private
*/
FileManager.prototype.initEssentialUI_ = function() {
// Record stats of dialog types. New values must NOT be inserted into the
// array enumerating the types. It must be in sync with
// FileDialogType enum in tools/metrics/histograms/histogram.xml.
metrics.recordEnum('Create', this.dialogType,
[DialogType.SELECT_FOLDER,
DialogType.SELECT_UPLOAD_FOLDER,
DialogType.SELECT_SAVEAS_FILE,
DialogType.SELECT_OPEN_FILE,
DialogType.SELECT_OPEN_MULTI_FILE,
DialogType.FULL_PAGE]);
// Create the metadata cache.
assert(this.volumeManager_);
this.metadataModel_ = MetadataModel.create(this.volumeManager_);
this.thumbnailModel_ = new ThumbnailModel(this.metadataModel_);
this.providersModel_ = new ProvidersModel(this.volumeManager_);
this.fileFilter_ = new FileFilter(this.metadataModel_);
// Create the root view of FileManager.
assert(this.dialogDom_);
assert(this.launchParams_);
this.ui_ = new FileManagerUI(
assert(this.providersModel_), this.dialogDom_, this.launchParams_);
};
/**
* One-time initialization of various DOM nodes. Loads the additional DOM
* elements visible to the user. Initialize here elements, which are expensive
* or hidden in the beginning.
* @private
*/
FileManager.prototype.initAdditionalUI_ = function() {
assert(this.metadataModel_);
assert(this.volumeManager_);
assert(this.historyLoader_);
assert(this.dialogDom_);
// Cache nodes we'll be manipulating.
var dom = this.dialogDom_;
assert(dom);
var table = queryRequiredElement('.detail-table', dom);
FileTable.decorate(
table,
this.metadataModel_,
this.volumeManager_,
this.historyLoader_,
this.dialogType == DialogType.FULL_PAGE);
var grid = queryRequiredElement('.thumbnail-grid', dom);
FileGrid.decorate(
grid,
this.metadataModel_,
this.volumeManager_,
this.historyLoader_);
this.addHistoryObserver_();
this.ui_.initAdditionalUI(
assertInstanceof(table, FileTable),
assertInstanceof(grid, FileGrid),
new LocationLine(
queryRequiredElement('#location-breadcrumbs', dom),
this.volumeManager_));
// Handle UI events.
this.fileBrowserBackground_.progressCenter.addPanel(
this.ui_.progressCenterPanel);
util.addIsFocusedMethod();
// Populate the static localized strings.
i18nTemplate.process(this.document_, loadTimeData);
// The cwd is not known at this point. Hide the import status column before
// redrawing, to avoid ugly flashing in the UI, caused when the first redraw
// has a visible status column, and then the cwd is later discovered to be
// not an import-eligible location.
this.ui_.listContainer.table.setImportStatusVisible(false);
// Arrange the file list.
this.ui_.listContainer.table.normalizeColumns();
this.ui_.listContainer.table.redraw();
};
/**
* One-time initialization of focus. This should run at the last of UI
* initialization.
* @private
*/
FileManager.prototype.initUIFocus_ = function() {
this.ui_.initUIFocus();
};
/**
* One-time initialization of import history observer. Provides
* the glue that updates the UI when history changes.
*
* @private
*/
FileManager.prototype.addHistoryObserver_ = function() {
// If, and only if history is ever fully loaded (it may not be),
// we want to update grid/list view when it changes.
this.historyLoader_.addHistoryLoadedListener(
(/**
* @param {!importer.ImportHistory} history
* @this {FileManager}
*/
function(history) {
this.importHistory_ = history;
history.addObserver(this.onHistoryChangedBound_);
}).bind(this));
};
/**
* Handles events when import history changed.
*
* @param {!importer.ImportHistory.ChangedEvent} event
* @private
*/
FileManager.prototype.onHistoryChanged_ = function(event) {
// Ignore any entry that isn't an immediate child of the
// current directory.
util.isChildEntry(event.entry, this.getCurrentDirectoryEntry())
.then(
(/**
* @param {boolean} isChild
* @this {FileManager}
*/
function(isChild) {
if (isChild) {
this.ui_.listContainer.grid.updateListItemsMetadata(
'import-history',
[event.entry]);
this.ui_.listContainer.table.updateListItemsMetadata(
'import-history',
[event.entry]);
}
}).bind(this));
};
/**
* Constructs table and grid (heavy operation).
* @private
*/
FileManager.prototype.initFileList_ = function() {
var singleSelection =
this.dialogType == DialogType.SELECT_OPEN_FILE ||
this.dialogType == DialogType.SELECT_FOLDER ||
this.dialogType == DialogType.SELECT_UPLOAD_FOLDER ||
this.dialogType == DialogType.SELECT_SAVEAS_FILE;
assert(this.volumeManager_);
assert(this.fileOperationManager_);
assert(this.metadataModel_);
this.directoryModel_ = new DirectoryModel(
singleSelection,
this.fileFilter_,
this.metadataModel_,
this.volumeManager_,
this.fileOperationManager_,
assert(this.tracker_));
this.folderShortcutsModel_ = new FolderShortcutsDataModel(
this.volumeManager_);
this.selectionHandler_ = new FileSelectionHandler(
assert(this.directoryModel_), assert(this.fileOperationManager_),
assert(this.ui_.listContainer), assert(this.metadataModel_),
assert(this.volumeManager_));
this.directoryModel_.getFileListSelection().addEventListener('change',
this.selectionHandler_.onFileSelectionChanged.bind(
this.selectionHandler_));
// TODO(mtomasz, yoshiki): Create navigation list earlier, and here just
// attach the directory model.
this.initDirectoryTree_();
this.ui_.listContainer.listThumbnailLoader = new ListThumbnailLoader(
this.directoryModel_,
assert(this.thumbnailModel_),
this.volumeManager_);
this.ui_.listContainer.dataModel = this.directoryModel_.getFileList();
this.ui_.listContainer.emptyDataModel =
this.directoryModel_.getEmptyFileList();
this.ui_.listContainer.selectionModel =
this.directoryModel_.getFileListSelection();
this.appStateController_.initialize(this.ui_, this.directoryModel_);
if (this.dialogType === DialogType.FULL_PAGE) {
this.columnVisibilityController_ = new ColumnVisibilityController(
this.ui_, this.directoryModel_, this.volumeManager_);
}
// Create metadata update controller.
this.metadataUpdateController_ = new MetadataUpdateController(
this.ui_.listContainer,
this.directoryModel_,
this.metadataModel_,
this.fileMetadataFormatter_);
// Create task controller.
this.taskController_ = new TaskController(
this.dialogType,
this.volumeManager_,
this.ui_,
this.metadataModel_,
this.directoryModel_,
this.selectionHandler_,
this.metadataUpdateController_);
// Create search controller.
this.searchController_ = new SearchController(
this.ui_.searchBox,
assert(this.ui_.locationLine),
this.directoryModel_,
this.volumeManager_,
assert(this.taskController_));
// Create naming controller.
assert(this.ui_.alertDialog);
assert(this.ui_.confirmDialog);
assert(this.fileFilter_);
this.namingController_ = new NamingController(
this.ui_.listContainer,
this.ui_.alertDialog,
this.ui_.confirmDialog,
this.directoryModel_,
this.fileFilter_,
this.selectionHandler_);
// Create directory tree naming controller.
this.directoryTreeNamingController_ = new DirectoryTreeNamingController(
this.directoryModel_,
assert(this.ui_.directoryTree),
this.ui_.alertDialog);
// Create spinner controller.
this.spinnerController_ = new SpinnerController(
this.ui_.listContainer.spinner);
this.spinnerController_.blink();
// Create dialog action controller.
assert(this.launchParams_);
this.dialogActionController_ = new DialogActionController(
this.dialogType,
this.ui_.dialogFooter,
this.directoryModel_,
this.metadataModel_,
this.volumeManager_,
this.fileFilter_,
this.namingController_,
this.selectionHandler_,
this.launchParams_);
};
/**
* @private
*/
FileManager.prototype.initDirectoryTree_ = function() {
var directoryTree = /** @type {DirectoryTree} */
(this.dialogDom_.querySelector('#directory-tree'));
var fakeEntriesVisible =
this.dialogType !== DialogType.SELECT_SAVEAS_FILE;
this.navigationUma_ = new NavigationUma(assert(this.volumeManager_));
DirectoryTree.decorate(directoryTree,
assert(this.directoryModel_),
assert(this.volumeManager_),
assert(this.metadataModel_),
assert(this.fileOperationManager_),
fakeEntriesVisible);
directoryTree.dataModel = new NavigationListModel(
assert(this.volumeManager_), assert(this.folderShortcutsModel_),
fakeEntriesVisible &&
!DialogType.isFolderDialog(this.launchParams_.type) ?
new NavigationModelFakeItem(
str('RECENT_ROOT_LABEL'), NavigationModelItemType.RECENT,
new FakeEntry(
str('RECENT_ROOT_LABEL'),
VolumeManagerCommon.RootType.RECENT, true,
this.getSourceRestriction_())) :
null,
null, // TODO(crbug.com/869252) remove this null.
this.commandLineFlags_['disable-my-files-navigation']);
this.setupCrostini_();
this.ui_.initDirectoryTree(directoryTree);
};
/**
* Check if crostini is enabled to create linuxFilesItem.
* @private
*/
FileManager.prototype.setupCrostini_ = function() {
chrome.fileManagerPrivate.isCrostiniEnabled((enabled) => {
// Check for 'crostini-files' cmd line flag.
chrome.commandLinePrivate.hasSwitch('crostini-files', (filesEnabled) => {
Crostini.IS_CROSTINI_FILES_ENABLED = filesEnabled;
});
// Setup Linux files fake root.
this.directoryTree.dataModel.linuxFilesItem = enabled ?
new NavigationModelFakeItem(
str('LINUX_FILES_ROOT_LABEL'), NavigationModelItemType.CROSTINI,
new FakeEntry(
str('LINUX_FILES_ROOT_LABEL'),
VolumeManagerCommon.RootType.CROSTINI, true)) :
null;
// Redraw the tree even if not enabled. This is required for testing.
this.directoryTree.redraw(false);
if (!enabled)
return;
// Load any existing shared paths.
chrome.fileManagerPrivate.getCrostiniSharedPaths((entries) => {
for (let i = 0; i < entries.length; i++) {
Crostini.registerSharedPath(entries[i], assert(this.volumeManager_));
}
});
});
};
/**
* Sets up the current directory during initialization.
* @private
*/
FileManager.prototype.setupCurrentDirectory_ = function() {
var tracker = this.directoryModel_.createDirectoryChangeTracker();
var queue = new AsyncUtil.Queue();
// Wait until the volume manager is initialized.
queue.run(function(callback) {
tracker.start();
this.volumeManager_.ensureInitialized(callback);
}.bind(this));
var nextCurrentDirEntry;
var selectionEntry;
// Resolve the selectionURL to selectionEntry or to currentDirectoryEntry
// in case of being a display root or a default directory to open files.
queue.run(function(callback) {
if (!this.launchParams_.selectionURL) {
callback();
return;
}
window.webkitResolveLocalFileSystemURL(
this.launchParams_.selectionURL,
function(inEntry) {
var locationInfo = this.volumeManager_.getLocationInfo(inEntry);
// If location information is not available, then the volume is
// no longer (or never) available.
if (!locationInfo) {
callback();
return;
}
// If the selection is root, then use it as a current directory
// instead. This is because, selecting a root entry is done as
// opening it.
if (locationInfo.isRootEntry)
nextCurrentDirEntry = inEntry;
// If this dialog attempts to open file(s) and the selection is a
// directory, the selection should be the current directory.
if (DialogType.isOpenFileDialog(this.dialogType) &&
inEntry.isDirectory) {
nextCurrentDirEntry = inEntry;
}
// By default, the selection should be selected entry and the
// parent directory of it should be the current directory.
if (!nextCurrentDirEntry)
selectionEntry = inEntry;
callback();
}.bind(this), callback);
}.bind(this));
// Resolve the currentDirectoryURL to currentDirectoryEntry (if not done
// by the previous step).
queue.run(function(callback) {
if (nextCurrentDirEntry || !this.launchParams_.currentDirectoryURL) {
callback();
return;
}
window.webkitResolveLocalFileSystemURL(
this.launchParams_.currentDirectoryURL,
function(inEntry) {
var locationInfo = this.volumeManager_.getLocationInfo(inEntry);
if (!locationInfo) {
callback();
return;
}
nextCurrentDirEntry = inEntry;
callback();
}.bind(this), callback);
// TODO(mtomasz): Implement reopening on special search, when fake
// entries are converted to directory providers. crbug.com/433161.
}.bind(this));
// If the directory to be changed to is not available, then first fallback
// to the parent of the selection entry.
queue.run(function(callback) {
if (nextCurrentDirEntry || !selectionEntry) {
callback();
return;
}
selectionEntry.getParent(function(inEntry) {
nextCurrentDirEntry = inEntry;
callback();
}.bind(this));
}.bind(this));
// Check if the next current directory is not a virtual directory which is
// not available in UI. This may happen to shared on Drive.
queue.run(function(callback) {
if (!nextCurrentDirEntry) {
callback();
return;
}
var locationInfo = this.volumeManager_.getLocationInfo(
nextCurrentDirEntry);
// If we can't check, assume that the directory is illegal.
if (!locationInfo) {
nextCurrentDirEntry = null;
callback();
return;
}
// Having root directory of DRIVE_OTHER here should be only for shared
// with me files. Fallback to Drive root in such case.
if (locationInfo.isRootEntry && locationInfo.rootType ===
VolumeManagerCommon.RootType.DRIVE_OTHER) {
var volumeInfo = this.volumeManager_.getVolumeInfo(nextCurrentDirEntry);
if (!volumeInfo) {
nextCurrentDirEntry = null;
callback();
return;
}
volumeInfo.resolveDisplayRoot().then(
function(entry) {
nextCurrentDirEntry = entry;
callback();
}).catch(function(error) {
console.error(error.stack || error);
nextCurrentDirEntry = null;
callback();
});
} else {
callback();
}
}.bind(this));
// If the directory to be changed to is still not resolved, then fallback
// to the default display root.
queue.run(function(callback) {
if (nextCurrentDirEntry) {
callback();
return;
}
this.volumeManager_.getDefaultDisplayRoot(function(displayRoot) {
nextCurrentDirEntry = displayRoot;
callback();
}.bind(this));
}.bind(this));
// If selection failed to be resolved (eg. didn't exist, in case of saving
// a file, or in case of a fallback of the current directory, then try to
// resolve again using the target name.
queue.run(function(callback) {
if (selectionEntry ||
!nextCurrentDirEntry ||
!this.launchParams_.targetName) {
callback();
return;
}
// Try to resolve as a file first. If it fails, then as a directory.
nextCurrentDirEntry.getFile(
this.launchParams_.targetName,
{},
function(targetEntry) {
selectionEntry = targetEntry;
callback();
}, function() {
// Failed to resolve as a file
nextCurrentDirEntry.getDirectory(
this.launchParams_.targetName,
{},
function(targetEntry) {
selectionEntry = targetEntry;
callback();
}, function() {
// Failed to resolve as either file or directory.
callback();
});
}.bind(this));
}.bind(this));
// Finalize.
queue.run(function(callback) {
// Check directory change.
tracker.stop();
if (tracker.hasChanged) {
callback();
return;
}
// Finish setup current directory.
this.finishSetupCurrentDirectory_(
nextCurrentDirEntry,
selectionEntry,
this.launchParams_.targetName);
callback();
}.bind(this));
};
/**
* @param {!DirectoryEntry} directoryEntry Directory to be opened.
* @param {Entry=} opt_selectionEntry Entry to be selected.
* @param {string=} opt_suggestedName Suggested name for a non-existing\
* selection.
* @private
*/
FileManager.prototype.finishSetupCurrentDirectory_ = function(
directoryEntry, opt_selectionEntry, opt_suggestedName) {
// Open the directory, and select the selection (if passed).
this.directoryModel_.changeDirectoryEntry(directoryEntry, function() {
if (opt_selectionEntry)
this.directoryModel_.selectEntry(opt_selectionEntry);
}.bind(this));
if (this.dialogType === DialogType.FULL_PAGE) {
// In the FULL_PAGE mode if the restored URL points to a file we might
// have to invoke a task after selecting it.
if (this.launchParams_.action === 'select')
return;
var task = null;
// TODO(mtomasz): Implement remounting archives after crash.
// See: crbug.com/333139
// If there is a task to be run, run it after the scan is completed.
if (task) {
var listener = function() {
if (!util.isSameEntry(this.directoryModel_.getCurrentDirEntry(),
directoryEntry)) {
// Opened on a different URL. Probably fallbacked. Therefore,
// do not invoke a task.
return;
}
this.directoryModel_.removeEventListener(
'scan-completed', listener);
task();
}.bind(this);
this.directoryModel_.addEventListener('scan-completed', listener);
}
} else if (this.dialogType === DialogType.SELECT_SAVEAS_FILE) {
this.ui_.dialogFooter.filenameInput.value = opt_suggestedName || '';
this.ui_.dialogFooter.selectTargetNameInFilenameInput();
}
};
/**
* Return DirectoryEntry of the current directory or null.
* @return {DirectoryEntry|FakeEntry|FilesAppDirEntry} DirectoryEntry of the
* current directory.
* Returns null if the directory model is not ready or the current
* directory is not set.
*/
FileManager.prototype.getCurrentDirectoryEntry = function() {
return this.directoryModel_ && this.directoryModel_.getCurrentDirEntry();
};
/**
* Unload handler for the page.
* @private
*/
FileManager.prototype.onUnload_ = function() {
if (this.importHistory_)
this.importHistory_.removeObserver(this.onHistoryChangedBound_);
if (this.directoryModel_)
this.directoryModel_.dispose();
if (this.volumeManager_)
this.volumeManager_.dispose();
if (this.fileTransferController_) {
for (var i = 0;
i < this.fileTransferController_.pendingTaskIds.length;
i++) {
var taskId = this.fileTransferController_.pendingTaskIds[i];
var item =
this.fileBrowserBackground_.progressCenter.getItemById(taskId);
item.message = '';
item.state = ProgressItemState.CANCELED;
this.fileBrowserBackground_.progressCenter.updateItem(item);
}
}
if (this.ui_ && this.ui_.progressCenterPanel) {
this.fileBrowserBackground_.progressCenter.removePanel(
this.ui_.progressCenterPanel);
}
};
/**
* Returns allowed path for the dialog by considering:
* 1) The launch parameter which specifies generic category of valid files
* paths.
* 2) Files app's unique capabilities and restrictions.
* @returns {AllowedPaths}
*/
FileManager.prototype.getAllowedPaths_ = function() {
var allowedPaths = this.launchParams_.allowedPaths;
// The native implementation of the Files app creates snapshot files for
// non-native files. But it does not work for folders (e.g., dialog for
// loading unpacked extensions).
if (allowedPaths === AllowedPaths.NATIVE_PATH &&
!DialogType.isFolderDialog(this.launchParams_.type)) {
if (this.launchParams_.type == DialogType.SELECT_SAVEAS_FILE) {
// Only drive can create snapshot files for saving.
allowedPaths = AllowedPaths.NATIVE_OR_DRIVE_PATH;
} else {
allowedPaths = AllowedPaths.ANY_PATH;
}
}
return allowedPaths;
};
/**
* Returns SourceRestriction which is used to communicate restrictions about
* sources to chrome.fileManagerPrivate.getRecentFiles API.
* @returns {chrome.fileManagerPrivate.SourceRestriction}
*/
FileManager.prototype.getSourceRestriction_ = function() {
var allowedPaths = this.getAllowedPaths_();
if (allowedPaths == AllowedPaths.NATIVE_PATH)
return chrome.fileManagerPrivate.SourceRestriction.NATIVE_SOURCE;
if (allowedPaths == AllowedPaths.NATIVE_OR_DRIVE_PATH)
return chrome.fileManagerPrivate.SourceRestriction.NATIVE_OR_DRIVE_SOURCE;
return chrome.fileManagerPrivate.SourceRestriction.ANY_SOURCE;
};
/**
* @return {FileSelection} Selection object.
*/
FileManager.prototype.getSelection = function() {
return this.selectionHandler_.selection;
};
/**
* @return {cr.ui.ArrayDataModel} File list.
*/
FileManager.prototype.getFileList = function() {
return this.directoryModel_.getFileList();
};
/**
* @return {!cr.ui.List} Current list object.
*/
FileManager.prototype.getCurrentList = function() {
return this.ui.listContainer.currentList;
};
})();