blob: 5819e35ff7faf81acb551ca82a019abcfe989861 [file] [log] [blame]
// Copyright 2014 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.
/**
* Component for the main window.
*
* The class receives UI events from UI components that does not have their own
* controller, and do corresponding action by using models/other controllers.
*
* The class also observes model/browser API's event to update the misc
* components.
*
* @param {DialogType} dialogType
* @param {!FileManagerUI} ui
* @param {!VolumeManager} volumeManager
* @param {!DirectoryModel} directoryModel
* @param {!FileFilter} fileFilter
* @param {!FileSelectionHandler} selectionHandler
* @param {!NamingController} namingController
* @param {!AppStateController} appStateController
* @param {!TaskController} taskController
* @constructor
* @struct
*/
function MainWindowComponent(
dialogType, ui, volumeManager, directoryModel, fileFilter, selectionHandler,
namingController, appStateController, taskController) {
/**
* @type {DialogType}
* @const
* @private
*/
this.dialogType_ = dialogType;
/**
* @type {!FileManagerUI}
* @const
* @private
*/
this.ui_ = ui;
/**
* @type {!VolumeManager}
* @const
* @private
*/
this.volumeManager_ = volumeManager;
/**
* @type {!DirectoryModel}
* @const
* @private
*/
this.directoryModel_ = directoryModel;
/**
* @type {!FileFilter}
* @const
* @private
*/
this.fileFilter_ = fileFilter;
/**
* @type {!FileSelectionHandler}
* @const
* @private
*/
this.selectionHandler_ = selectionHandler;
/**
* @type {!NamingController}
* @const
* @private
*/
this.namingController_ = namingController;
/**
* @type {!AppStateController}
* @const
* @private
*/
this.appStateController_ = appStateController;
/**
* @type {!TaskController}
* @const
* @private
*/
this.taskController_ = taskController;
/**
* True while a user is pressing <Tab>.
* This is used for identifying the trigger causing the filelist to
* be focused.
* @type {boolean}
* @private
*/
this.pressingTab_ = false;
// Register events.
ui.listContainer.element.addEventListener(
'keydown', this.onListKeyDown_.bind(this));
ui.directoryTree.addEventListener(
'keydown', this.onDirectoryTreeKeyDown_.bind(this));
ui.listContainer.element.addEventListener(
ListContainer.EventType.TEXT_SEARCH, this.onTextSearch_.bind(this));
ui.listContainer.table.list.addEventListener(
'dblclick', this.onDoubleClick_.bind(this));
ui.listContainer.grid.addEventListener(
'dblclick', this.onDoubleClick_.bind(this));
ui.listContainer.table.list.addEventListener(
'touchstart', this.handleTouchEvents_.bind(this));
ui.listContainer.grid.addEventListener(
'touchstart', this.handleTouchEvents_.bind(this));
ui.listContainer.table.list.addEventListener(
'touchend', this.handleTouchEvents_.bind(this));
ui.listContainer.grid.addEventListener(
'touchend', this.handleTouchEvents_.bind(this));
ui.listContainer.table.list.addEventListener(
'touchmove', this.handleTouchEvents_.bind(this));
ui.listContainer.grid.addEventListener(
'touchmove', this.handleTouchEvents_.bind(this));
ui.listContainer.table.list.addEventListener(
'focus', this.onFileListFocus_.bind(this));
ui.listContainer.grid.addEventListener(
'focus', this.onFileListFocus_.bind(this));
ui.locationLine.addEventListener(
'pathclick', this.onBreadcrumbClick_.bind(this));
ui.toggleViewButton.addEventListener(
'click', this.onToggleViewButtonClick_.bind(this));
directoryModel.addEventListener(
'directory-changed', this.onDirectoryChanged_.bind(this));
volumeManager.addEventListener(
'drive-connection-changed', this.onDriveConnectionChanged_.bind(this));
this.onDriveConnectionChanged_();
document.addEventListener('keydown', this.onKeyDown_.bind(this));
document.addEventListener('keyup', this.onKeyUp_.bind(this));
window.addEventListener('focus', this.onWindowFocus_.bind(this));
/**
* @type {!FileTapHandler}
* @private
* @const
*/
this.tapHandler_ = new FileTapHandler();
}
/**
* Handles touch events.
* @param {!Event} event
* @private
*/
MainWindowComponent.prototype.handleTouchEvents_ = function(event) {
// We only need to know that a tap is happend somewhere in the list.
// Also the 2nd parameter of handleTouchEvents is just passed back to the
// callback. Therefore we can pass a dummy value to it.
// TODO(yamaguchi): Revise TapHandler.handleTouchEvents to delete the param.
this.tapHandler_.handleTouchEvents(event, -1, (e, index, eventType) => {
if (eventType == FileTapHandler.TapEvent.TAP) {
if (e.target.classList.contains('detail-checkmark')) {
// Tap on the checkmark should only toggle select the item just like a
// mouse click on it.
return false;
}
// The selection model has the single selection at this point.
// When using touchscreen, the selection should be cleared because
// we don't want show the file selected when not in check-select
// mode.
return this.handleOpenDefault(
event, true /* clearSelectionAfterLaunch */);
}
return false;
});
};
/**
* @param {Event} event Click event.
* @private
*/
MainWindowComponent.prototype.onBreadcrumbClick_ = function(event) {
this.directoryModel_.changeDirectoryEntry(event.entry);
};
/**
* File list focus handler. Used to select the top most element on the list
* if nothing was selected.
*
* @private
*/
MainWindowComponent.prototype.onFileListFocus_ = function() {
// If the file list is focused by <Tab>, select the first item if no item
// is selected.
if (this.pressingTab_) {
const selection = this.selectionHandler_.selection;
if (selection && selection.totalCount === 0) {
const selectionModel = this.directoryModel_.getFileListSelection();
const targetIndex =
selectionModel.anchorIndex && selectionModel.anchorIndex !== -1 ?
selectionModel.anchorIndex :
0;
this.directoryModel_.selectIndex(targetIndex);
}
}
};
/**
* Handles a double click event.
*
* @param {Event} event The dblclick event.
* @private
*/
MainWindowComponent.prototype.onDoubleClick_ = function(event) {
this.handleOpenDefault(event, false);
};
/**
* Opens the selected item by the default command.
* If the item is a directory, change current directory to it.
* Otherwise, accepts the current selection.
*
* @param {Event} event The dblclick event.
* @param {boolean} clearSelectionAfterLaunch
* @return {boolean} true if successfully opened the item.
* @private
*/
MainWindowComponent.prototype.handleOpenDefault = function(
event, clearSelectionAfterLaunch) {
if (this.namingController_.isRenamingInProgress()) {
// Don't pay attention to clicks during a rename.
return false;
}
const listItem = this.ui_.listContainer.findListItemForNode(
event.touchedElement || event.srcElement);
// It is expected that the target item should have already been selected in
// LiseSelectionController.handlePointerDownUp on preceding mousedown event.
const selection = this.selectionHandler_.selection;
if (!listItem || !listItem.selected || selection.totalCount != 1) {
return false;
}
const entry = selection.entries[0];
if (entry.isDirectory) {
this.directoryModel_.changeDirectoryEntry(
/** @type {!DirectoryEntry} */ (entry));
} else {
return this.acceptSelection_(clearSelectionAfterLaunch);
}
return false;
};
/**
* Accepts the current selection depending on the mode.
* @param {boolean} clearSelectionAfterLaunch
* @return {boolean} true if successfully accepted the current selection.
* @private
*/
MainWindowComponent.prototype.acceptSelection_ = function(
clearSelectionAfterLaunch) {
const selection = this.selectionHandler_.selection;
if (this.dialogType_ == DialogType.FULL_PAGE) {
this.taskController_.getFileTasks()
.then(tasks => {
tasks.executeDefault();
if (clearSelectionAfterLaunch) {
this.directoryModel_.clearSelection();
}
})
.catch(error => {
if (error) {
console.error(error.stack || error);
}
});
return true;
}
if (!this.ui_.dialogFooter.okButton.disabled) {
this.ui_.dialogFooter.okButton.click();
return true;
}
return false;
};
/**
* Handles click event on the toggle-view button.
* @param {Event} event Click event.
* @private
*/
MainWindowComponent.prototype.onToggleViewButtonClick_ = function(event) {
const listType =
this.ui_.listContainer.currentListType === ListContainer.ListType.DETAIL ?
ListContainer.ListType.THUMBNAIL :
ListContainer.ListType.DETAIL;
this.ui_.setCurrentListType(listType);
const msgId = listType === ListContainer.ListType.DETAIL ?
'FILE_LIST_CHANGED_TO_LIST_VIEW' :
'FILE_LIST_CHANGED_TO_LIST_THUMBNAIL_VIEW';
this.ui_.speakA11yMessage(str(msgId));
this.appStateController_.saveViewOptions();
this.ui_.listContainer.focus();
metrics.recordEnum(
'ToggleFileListType', listType, ListContainer.ListTypesForUMA);
};
/**
* KeyDown event handler for the document.
* @param {Event} event Key event.
* @private
*/
MainWindowComponent.prototype.onKeyDown_ = function(event) {
if (event.keyCode === 9) { // Tab
this.pressingTab_ = true;
}
if (event.srcElement === this.ui_.listContainer.renameInput) {
// Ignore keydown handler in the rename input box.
return;
}
switch (util.getKeyModifiers(event) + event.key) {
case 'Escape': // Escape => Cancel dialog.
case 'Ctrl-w': // Ctrl+W => Cancel dialog.
if (this.dialogType_ != DialogType.FULL_PAGE) {
// If there is nothing else for ESC to do, then cancel the dialog.
event.preventDefault();
this.ui_.dialogFooter.cancelButton.click();
}
break;
}
};
/**
* KeyUp event handler for the document.
* @param {Event} event Key event.
* @private
*/
MainWindowComponent.prototype.onKeyUp_ = function(event) {
if (event.keyCode === 9) { // Tab
this.pressingTab_ = false;
}
};
/**
* KeyDown event handler for the directory tree element.
* @param {Event} event Key event.
* @private
*/
MainWindowComponent.prototype.onDirectoryTreeKeyDown_ = function(event) {
// Enter => Change directory or perform default action.
if (util.getKeyModifiers(event) + event.key === 'Enter') {
const selectedItem = this.ui_.directoryTree.selectedItem;
if (!selectedItem) {
return;
}
selectedItem.activate();
if (this.dialogType_ !== DialogType.FULL_PAGE &&
!selectedItem.hasAttribute('renaming') &&
util.isSameEntry(
this.directoryModel_.getCurrentDirEntry(), selectedItem.entry) &&
!this.ui_.dialogFooter.okButton.disabled) {
this.ui_.dialogFooter.okButton.click();
}
}
};
/**
* KeyDown event handler for the div#list-container element.
* @param {Event} event Key event.
* @private
*/
MainWindowComponent.prototype.onListKeyDown_ = function(event) {
switch (util.getKeyModifiers(event) + event.key) {
case 'Backspace': // Backspace => Up one directory.
event.preventDefault();
const components = this.ui_.locationLine.getCurrentPathComponents();
if (components.length < 2) {
break;
}
const parentPathComponent = components[components.length - 2];
parentPathComponent.resolveEntry().then((parentEntry) => {
this.directoryModel_.changeDirectoryEntry(
/** @type {!DirectoryEntry} */ (parentEntry));
});
break;
case 'Enter': // Enter => Change directory or perform default action.
const selection = this.selectionHandler_.selection;
if (selection.totalCount === 1 && selection.entries[0].isDirectory &&
!DialogType.isFolderDialog(this.dialogType_)) {
const item = this.ui_.listContainer.currentList.getListItemByIndex(
selection.indexes[0]);
// If the item is in renaming process, we don't allow to change
// directory.
if (item && !item.hasAttribute('renaming')) {
event.preventDefault();
this.directoryModel_.changeDirectoryEntry(
/** @type {!DirectoryEntry} */ (selection.entries[0]));
}
} else if (this.acceptSelection_(false /* clearSelectionAfterLaunch */)) {
event.preventDefault();
}
break;
}
};
/**
* Performs a 'text search' - selects a first list entry with name
* starting with entered text (case-insensitive).
* @private
*/
MainWindowComponent.prototype.onTextSearch_ = function() {
const text = this.ui_.listContainer.textSearchState.text;
const dm = this.directoryModel_.getFileList();
for (let index = 0; index < dm.length; ++index) {
const name = dm.item(index).name;
if (name.substring(0, text.length).toLowerCase() == text) {
this.ui_.listContainer.currentList.selectionModel.selectedIndexes =
[index];
return;
}
}
this.ui_.listContainer.textSearchState.text = '';
};
/**
* Update the UI when the current directory changes.
*
* @param {Event} event The directory-changed event.
* @private
*/
MainWindowComponent.prototype.onDirectoryChanged_ = function(event) {
event = /** @type {DirectoryChangeEvent} */ (event);
const newVolumeInfo = event.newDirEntry ?
this.volumeManager_.getVolumeInfo(event.newDirEntry) :
null;
// Update unformatted volume status.
if (newVolumeInfo && newVolumeInfo.error) {
this.ui_.element.setAttribute('unformatted', '');
if (newVolumeInfo.error ===
VolumeManagerCommon.VolumeError.UNSUPPORTED_FILESYSTEM) {
this.ui_.formatPanelError.textContent =
str('UNSUPPORTED_FILESYSTEM_WARNING');
} else {
this.ui_.formatPanelError.textContent = str('UNKNOWN_FILESYSTEM_WARNING');
}
} else {
this.ui_.element.removeAttribute('unformatted');
}
if (event.newDirEntry) {
this.ui_.locationLine.show(event.newDirEntry);
// Updates UI.
if (this.dialogType_ === DialogType.FULL_PAGE) {
const locationInfo =
this.volumeManager_.getLocationInfo(event.newDirEntry);
if (locationInfo) {
const label = util.getEntryLabel(locationInfo, event.newDirEntry);
document.title = `${str('FILEMANAGER_APP_NAME')} - ${label}`;
} else {
console.error(
'Could not find location info for entry: ' +
event.newDirEntry.fullPath);
}
}
} else {
this.ui_.locationLine.hide();
}
};
/**
* @private
*/
MainWindowComponent.prototype.onDriveConnectionChanged_ = function() {
const connection = this.volumeManager_.getDriveConnectionState();
this.ui_.dialogContainer.setAttribute('connection', connection.type);
this.ui_.suggestAppsDialog.onDriveConnectionChanged(connection.type);
};
/**
* @private
*/
MainWindowComponent.prototype.onWindowFocus_ = function() {
// When the window have got a focus while the current directory is Recent
// root, refresh the contents.
if (this.directoryModel_.getCurrentRootType() ===
VolumeManagerCommon.RootType.RECENT) {
this.directoryModel_.rescan(true /* refresh */);
// Do not start the spinner here to silently refresh the contents.
}
};