blob: 6d159d90f06009895cf59ff9bbe8bd2f312006cc [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.
'use strict';
/**
* The current selection object.
*
* @param {FileManager} fileManager FileManager instance.
* @param {Array.<number>} indexes Selected indexes.
* @constructor
*/
function FileSelection(fileManager, indexes) {
this.fileManager_ = fileManager;
this.computeBytesSequence_ = 0;
this.indexes = indexes;
this.entries = [];
this.urls = [];
this.totalCount = 0;
this.fileCount = 0;
this.directoryCount = 0;
this.bytes = 0;
this.showBytes = false;
this.allDriveFilesPresent = false,
this.iconType = null;
this.bytesKnown = false;
this.mustBeHidden_ = false;
// Synchronously compute what we can.
for (var i = 0; i < this.indexes.length; i++) {
var entry = fileManager.getFileList().item(this.indexes[i]);
if (!entry)
continue;
this.entries.push(entry);
this.urls.push(entry.toURL());
if (this.iconType == null) {
this.iconType = FileType.getIcon(entry);
} else if (this.iconType != 'unknown') {
var iconType = FileType.getIcon(entry);
if (this.iconType != iconType)
this.iconType = 'unknown';
}
if (entry.isFile) {
this.fileCount += 1;
} else {
this.directoryCount += 1;
}
this.totalCount++;
}
this.tasks = new FileTasks(this.fileManager_);
}
/**
* Computes data required to get file tasks and requests the tasks.
*
* @param {function} callback The callback.
*/
FileSelection.prototype.createTasks = function(callback) {
if (!this.fileManager_.isOnDrive()) {
this.tasks.init(this.urls);
callback();
return;
}
this.fileManager_.metadataCache_.get(this.urls, 'drive', function(props) {
var present = props.filter(function(p) { return p && p.availableOffline });
this.allDriveFilesPresent = present.length == props.length;
// Collect all of the mime types and push that info into the selection.
this.mimeTypes = props.map(function(value) {
return (value && value.contentMimeType) || '';
});
this.tasks.init(this.urls, this.mimeTypes);
callback();
}.bind(this));
};
/**
* Computes the total size of selected files.
*
* @param {function} callback Completion callback. Not called when cancelled,
* or a new call has been invoked in the meantime.
*/
FileSelection.prototype.computeBytes = function(callback) {
if (this.entries.length == 0) {
this.bytesKnown = true;
this.showBytes = false;
this.bytes = 0;
return;
}
var computeBytesSequence = ++this.computeBytesSequence_;
var pendingMetadataCount = 0;
var maybeDone = function() {
if (pendingMetadataCount == 0) {
this.bytesKnown = true;
callback();
}
}.bind(this);
var onProps = function(properties) {
// Ignore if the call got cancelled, or there is another new one fired.
if (computeBytesSequence != this.computeBytesSequence_)
return;
// It may happen that the metadata is not available because a file has been
// deleted in the meantime.
if (properties)
this.bytes += properties.size;
pendingMetadataCount--;
maybeDone();
}.bind(this);
for (var index = 0; index < this.entries.length; index++) {
var entry = this.entries[index];
if (entry.isFile) {
this.showBytes |= !FileType.isHosted(entry);
pendingMetadataCount++;
this.fileManager_.metadataCache_.get(entry, 'filesystem', onProps);
} else if (entry.isDirectory) {
// Don't compute the directory size as it's expensive.
// crbug.com/179073.
this.showBytes = false;
break;
}
}
maybeDone();
};
/**
* Cancels any async computation by increasing the sequence number. Results
* of any previous call to computeBytes() will be discarded.
*
* @private
*/
FileSelection.prototype.cancelComputing_ = function() {
this.computeBytesSequence_++;
};
/**
* This object encapsulates everything related to current selection.
*
* @param {FileManager} fileManager File manager instance.
* @extends {cr.EventTarget}
* @constructor
*/
function FileSelectionHandler(fileManager) {
this.fileManager_ = fileManager;
// TODO(dgozman): create a shared object with most of UI elements.
this.okButton_ = fileManager.okButton_;
this.filenameInput_ = fileManager.filenameInput_;
this.previewPanel_ = fileManager.dialogDom_.querySelector('.preview-panel');
this.previewThumbnails_ = this.previewPanel_.
querySelector('.preview-thumbnails');
this.previewSummary_ = this.previewPanel_.querySelector('.preview-summary');
this.previewText_ = this.previewSummary_.querySelector('.preview-text');
this.calculatingSize_ = this.previewSummary_.
querySelector('.calculating-size');
this.calculatingSize_.textContent = str('CALCULATING_SIZE');
this.searchBreadcrumbs_ = fileManager.searchBreadcrumbs_;
this.taskItems_ = fileManager.taskItems_;
this.animationTimeout_ = null;
}
/**
* FileSelectionHandler extends cr.EventTarget.
*/
FileSelectionHandler.prototype.__proto__ = cr.EventTarget.prototype;
/**
* Maximum amount of thumbnails in the preview pane.
*
* @const
* @type {number}
*/
FileSelectionHandler.MAX_PREVIEW_THUMBNAIL_COUNT = 4;
/**
* Maximum width or height of an image what pops up when the mouse hovers
* thumbnail in the bottom panel (in pixels).
*
* @const
* @type {number}
*/
FileSelectionHandler.IMAGE_HOVER_PREVIEW_SIZE = 200;
/**
* Update the UI when the selection model changes.
*
* @param {cr.Event} event The change event.
*/
FileSelectionHandler.prototype.onFileSelectionChanged = function(event) {
var indexes =
this.fileManager_.getCurrentList().selectionModel.selectedIndexes;
if (this.selection) this.selection.cancelComputing_();
var selection = new FileSelection(this.fileManager_, indexes);
this.selection = selection;
if (this.fileManager_.dialogType == DialogType.SELECT_SAVEAS_FILE) {
// If this is a save-as dialog, copy the selected file into the filename
// input text box.
if (this.selection.totalCount == 1 &&
this.selection.entries[0].isFile &&
this.filenameInput_.value != this.selection.entries[0].name) {
this.filenameInput_.value = this.selection.entries[0].name;
}
}
this.updateOkButton();
if (this.selectionUpdateTimer_) {
clearTimeout(this.selectionUpdateTimer_);
this.selectionUpdateTimer_ = null;
}
this.hideCalculating_();
// The rest of the selection properties are computed via (sometimes lengthy)
// asynchronous calls. We initiate these calls after a timeout. If the
// selection is changing quickly we only do this once when it slows down.
var updateDelay = 200;
var now = Date.now();
if (now > (this.lastFileSelectionTime_ || 0) + updateDelay) {
// The previous selection change happened a while ago. Update the UI soon.
updateDelay = 0;
}
this.lastFileSelectionTime_ = now;
this.selectionUpdateTimer_ = setTimeout(function() {
this.selectionUpdateTimer_ = null;
if (this.selection == selection)
this.updateFileSelectionAsync(selection);
}.bind(this), updateDelay);
};
/**
* Clears the primary UI selection elements.
*/
FileSelectionHandler.prototype.clearUI = function() {
this.previewThumbnails_.textContent = '';
this.previewText_.textContent = '';
this.hideCalculating_();
this.taskItems_.hidden = true;
this.okButton_.disabled = true;
};
/**
* Updates the Ok button enabled state.
*
* @return {boolean} Whether button is enabled.
*/
FileSelectionHandler.prototype.updateOkButton = function() {
var selectable;
var dialogType = this.fileManager_.dialogType;
if (dialogType == DialogType.SELECT_FOLDER ||
dialogType == DialogType.SELECT_UPLOAD_FOLDER) {
// In SELECT_FOLDER mode, we allow to select current directory
// when nothing is selected.
selectable = this.selection.directoryCount <= 1 &&
this.selection.fileCount == 0;
} else if (dialogType == DialogType.SELECT_OPEN_FILE) {
selectable = (this.isFileSelectionAvailable() &&
this.selection.directoryCount == 0 &&
this.selection.fileCount == 1);
} else if (dialogType == DialogType.SELECT_OPEN_MULTI_FILE) {
selectable = (this.isFileSelectionAvailable() &&
this.selection.directoryCount == 0 &&
this.selection.fileCount >= 1);
} else if (dialogType == DialogType.SELECT_SAVEAS_FILE) {
if (this.fileManager_.isOnReadonlyDirectory()) {
selectable = false;
} else {
selectable = !!this.filenameInput_.value;
}
} else if (dialogType == DialogType.FULL_PAGE) {
// No "select" buttons on the full page UI.
selectable = true;
} else {
throw new Error('Unknown dialog type');
}
this.okButton_.disabled = !selectable;
return selectable;
};
/**
* Check if all the files in the current selection are available. The only
* case when files might be not available is when the selection contains
* uncached Drive files and the browser is offline.
*
* @return {boolean} True if all files in the current selection are
* available.
*/
FileSelectionHandler.prototype.isFileSelectionAvailable = function() {
return !this.fileManager_.isOnDrive() ||
!this.fileManager_.isDriveOffline() ||
this.selection.allDriveFilesPresent;
};
/**
* Sets the flag to force the preview panel hidden.
* @param {boolean} hidden True to force hidden.
*/
FileSelectionHandler.prototype.setPreviewPanelMustBeHidden = function(hidden) {
this.previewPanelMustBeHidden_ = hidden;
this.updatePreviewPanelVisibility_();
};
/**
* Animates preview panel show/hide transitions.
*
* @private
*/
FileSelectionHandler.prototype.updatePreviewPanelVisibility_ = function() {
var panel = this.previewPanel_;
var state = panel.getAttribute('visibility');
var mustBeVisible =
// If one or more files are selected, show the file info.
(this.selection.totalCount > 0 ||
// If the directory is not root dir, show the directory info.
!PathUtil.isRootPath(this.fileManager_.getCurrentDirectory()) ||
// On Open File dialog, the preview panel is always shown.
this.fileManager_.dialogType == DialogType.SELECT_OPEN_FILE ||
this.fileManager_.dialogType == DialogType.SELECT_OPEN_MULTI_FILE);
var stopHidingAndShow = function() {
clearTimeout(this.hidingTimeout_);
this.hidingTimeout_ = 0;
setVisibility('visible');
}.bind(this);
var startHiding = function() {
setVisibility('hiding');
this.hidingTimeout_ = setTimeout(function() {
this.hidingTimeout_ = 0;
setVisibility('hidden');
cr.dispatchSimpleEvent(this, 'hide-preview-panel');
}.bind(this), 250);
}.bind(this);
var show = function() {
setVisibility('visible');
this.previewThumbnails_.textContent = '';
cr.dispatchSimpleEvent(this, 'show-preview-panel');
}.bind(this);
var setVisibility = function(visibility) {
panel.setAttribute('visibility', visibility);
};
switch (state) {
case 'visible':
if (!mustBeVisible || this.previewPanelMustBeHidden_)
startHiding();
break;
case 'hiding':
if (mustBeVisible && !this.previewPanelMustBeHidden_)
stopHidingAndShow();
break;
case 'hidden':
if (mustBeVisible && !this.previewPanelMustBeHidden_)
show();
}
};
/**
* @return {boolean} True if space reserverd for the preview panel.
* @private
*/
FileSelectionHandler.prototype.isPreviewPanelVisibile_ = function() {
return this.previewPanel_.getAttribute('visibility') == 'visible';
};
/**
* Update the selection summary in preview panel.
*
* @private
*/
FileSelectionHandler.prototype.updatePreviewPanelText_ = function() {
var selection = this.selection;
if (selection.totalCount <= 1) {
// Hides the preview text if zero or one file is selected. We shows a
// breadcrumb list instead on the preview panel.
this.hideCalculating_();
this.previewText_.textContent = '';
return;
}
var text = '';
if (selection.totalCount == 1) {
text = selection.entries[0].name;
} else if (selection.directoryCount == 0) {
text = strf('MANY_FILES_SELECTED', selection.fileCount);
} else if (selection.fileCount == 0) {
text = strf('MANY_DIRECTORIES_SELECTED', selection.directoryCount);
} else {
text = strf('MANY_ENTRIES_SELECTED', selection.totalCount);
}
if (selection.bytesKnown) {
this.hideCalculating_();
if (selection.showBytes) {
var bytes = util.bytesToString(selection.bytes);
text += ', ' + bytes;
}
} else {
this.showCalculating_();
}
this.previewText_.textContent = text;
};
/**
* Displays the 'calculating size' label.
*
* @private
*/
FileSelectionHandler.prototype.showCalculating_ = function() {
if (this.animationTimeout_) {
clearTimeout(this.animationTimeout_);
this.animationTimeout_ = null;
}
var dotCount = 0;
var advance = function() {
this.animationTimeout_ = setTimeout(advance, 1000);
var s = this.calculatingSize_.textContent;
s = s.replace(/(\.)+$/, '');
for (var i = 0; i < dotCount; i++) {
s += '.';
}
this.calculatingSize_.textContent = s;
dotCount = (dotCount + 1) % 3;
}.bind(this);
var start = function() {
this.calculatingSize_.hidden = false;
advance();
}.bind(this);
this.animationTimeout_ = setTimeout(start, 500);
};
/**
* Hides the 'calculating size' label.
*
* @private
*/
FileSelectionHandler.prototype.hideCalculating_ = function() {
if (this.animationTimeout_) {
clearTimeout(this.animationTimeout_);
this.animationTimeout_ = null;
}
this.calculatingSize_.hidden = true;
};
/**
* Calculates async selection stats and updates secondary UI elements.
*
* @param {FileSelection} selection The selection object.
*/
FileSelectionHandler.prototype.updateFileSelectionAsync = function(selection) {
if (this.selection != selection) return;
// Update the file tasks.
if (this.fileManager_.dialogType == DialogType.FULL_PAGE &&
selection.directoryCount == 0 && selection.fileCount > 0) {
selection.createTasks(function() {
if (this.selection != selection)
return;
selection.tasks.display(this.taskItems_);
selection.tasks.updateMenuItem();
}.bind(this));
} else {
this.taskItems_.hidden = true;
}
// Update preview panels.
var wasVisible = this.isPreviewPanelVisibile_();
var thumbnailEntries;
if (selection.totalCount == 0) {
thumbnailEntries = [
this.fileManager_.getCurrentDirectoryEntry()
];
} else {
thumbnailEntries = selection.entries;
if (selection.totalCount != 1) {
selection.computeBytes(function() {
if (this.selection != selection)
return;
this.updatePreviewPanelText_();
}.bind(this));
}
}
this.updatePreviewPanelVisibility_();
this.updatePreviewPanelText_();
this.showPreviewThumbnails_(thumbnailEntries);
// Update breadcrums.
var updateTarget = null;
var path = this.fileManager_.getCurrentDirectory();
if (selection.totalCount == 1) {
// Shows the breadcrumb list when a file is selected.
updateTarget = selection.entries[0].fullPath;
} else if (selection.totalCount == 0 &&
this.isPreviewPanelVisibile_()) {
// Shows the breadcrumb list when no file is selected and the preview
// panel is visible.
updateTarget = path;
}
this.updatePreviewPanelBreadcrumbs_(updateTarget);
// Scroll to item
if (!wasVisible && this.selection.totalCount == 1) {
var list = this.fileManager_.getCurrentList();
list.scrollIndexIntoView(list.selectionModel.selectedIndex);
}
// Sync the commands availability.
if (selection.totalCount != 0)
this.fileManager_.updateCommands();
// Update context menu.
this.fileManager_.updateContextMenuActionItems(null, false);
// Inform tests it's OK to click buttons now.
if (selection.totalCount > 0) {
chrome.test.sendMessage('selection-change-complete');
}
};
/**
* Renders preview thumbnails in preview panel.
*
* @param {Array.<FileEntry>} entries The entries of selected object.
* @private
*/
FileSelectionHandler.prototype.showPreviewThumbnails_ = function(entries) {
var selection = this.selection;
var thumbnails = [];
var thumbnailCount = 0;
var thumbnailLoaded = -1;
var forcedShowTimeout = null;
var thumbnailsHaveZoom = false;
var self = this;
var showThumbnails = function() {
// have-zoom class may be updated twice: then timeout exceeds and then
// then all images loaded.
if (self.selection == selection) {
if (thumbnailsHaveZoom) {
self.previewThumbnails_.classList.add('has-zoom');
} else {
self.previewThumbnails_.classList.remove('has-zoom');
}
}
if (forcedShowTimeout === null)
return;
clearTimeout(forcedShowTimeout);
forcedShowTimeout = null;
// FileSelection could change while images are loading.
if (self.selection == selection) {
self.previewThumbnails_.textContent = '';
for (var i = 0; i < thumbnails.length; i++)
self.previewThumbnails_.appendChild(thumbnails[i]);
}
};
var onThumbnailLoaded = function() {
thumbnailLoaded++;
if (thumbnailLoaded == thumbnailCount)
showThumbnails();
};
var thumbnailClickHandler = function() {
if (selection.tasks)
selection.tasks.executeDefault();
};
var doc = this.fileManager_.document_;
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
if (thumbnailCount < FileSelectionHandler.MAX_PREVIEW_THUMBNAIL_COUNT) {
var box = doc.createElement('div');
box.className = 'thumbnail';
if (thumbnailCount == 0) {
var zoomed = doc.createElement('div');
zoomed.hidden = true;
thumbnails.push(zoomed);
var onFirstThumbnailLoaded = function(img, transform) {
if (img && self.decorateThumbnailZoom_(zoomed, img, transform)) {
zoomed.hidden = false;
thumbnailsHaveZoom = true;
}
onThumbnailLoaded();
};
var thumbnail = this.renderThumbnail_(entry, onFirstThumbnailLoaded);
zoomed.addEventListener('click', thumbnailClickHandler);
} else {
var thumbnail = this.renderThumbnail_(entry, onThumbnailLoaded);
}
thumbnailCount++;
box.appendChild(thumbnail);
box.style.zIndex =
FileSelectionHandler.MAX_PREVIEW_THUMBNAIL_COUNT + 1 - i;
box.addEventListener('click', thumbnailClickHandler);
thumbnails.push(box);
}
}
forcedShowTimeout = setTimeout(showThumbnails,
FileManager.THUMBNAIL_SHOW_DELAY);
onThumbnailLoaded();
};
/**
* Renders a thumbnail for the buttom panel.
*
* @param {Entry} entry Entry to render for.
* @param {function} callback Called when image loaded.
* @return {HTMLDivElement} Created element.
* @private
*/
FileSelectionHandler.prototype.renderThumbnail_ = function(entry, callback) {
var thumbnail = this.fileManager_.document_.createElement('div');
FileGrid.decorateThumbnailBox(thumbnail,
entry,
this.fileManager_.metadataCache_,
ThumbnailLoader.FillMode.FILL,
FileGrid.ThumbnailQuality.LOW,
callback);
return thumbnail;
};
/**
* Updates the breadcrumbs in the preview panel.
*
* @param {?string} path Path to be shown in the breadcrumbs list
* @private
*/
FileSelectionHandler.prototype.updatePreviewPanelBreadcrumbs_ = function(path) {
if (!path)
this.searchBreadcrumbs_.hide();
else
this.searchBreadcrumbs_.show(PathUtil.getRootPath(path), path);
};
/**
* Updates the search breadcrumbs. This method should not be used in the new ui.
*
* @private
*/
FileSelectionHandler.prototype.updateSearchBreadcrumbs_ = function() {
var selectedIndexes =
this.fileManager_.getCurrentList().selectionModel.selectedIndexes;
if (selectedIndexes.length !== 1 ||
!this.fileManager_.directoryModel_.isSearching()) {
this.searchBreadcrumbs_.hide();
return;
}
var entry = this.fileManager_.getFileList().item(
selectedIndexes[0]);
this.searchBreadcrumbs_.show(
PathUtil.getRootPath(entry.fullPath),
entry.fullPath);
};
/**
* Creates enlarged image for a bottom pannel thumbnail.
* Image's assumed to be just loaded and not inserted into the DOM.
*
* @param {HTMLElement} largeImageBox DIV element to decorate.
* @param {HTMLElement} img Loaded image.
* @param {Object} transform Image transformation description.
* @return {boolean} True if zoomed image is present.
* @private
*/
FileSelectionHandler.prototype.decorateThumbnailZoom_ = function(
largeImageBox, img, transform) {
var width = img.width;
var height = img.height;
var THUMBNAIL_SIZE = 35;
if (width < THUMBNAIL_SIZE * 2 && height < THUMBNAIL_SIZE * 2)
return false;
var scale = Math.min(1,
FileSelectionHandler.IMAGE_HOVER_PREVIEW_SIZE / Math.max(width, height));
var imageWidth = Math.round(width * scale);
var imageHeight = Math.round(height * scale);
var largeImage = this.fileManager_.document_.createElement('img');
if (scale < 0.3) {
// Scaling large images kills animation. Downscale it in advance.
// Canvas scales images with liner interpolation. Make a larger
// image (but small enough to not kill animation) and let IMG
// scale it smoothly.
var INTERMEDIATE_SCALE = 3;
var canvas = this.fileManager_.document_.createElement('canvas');
canvas.width = imageWidth * INTERMEDIATE_SCALE;
canvas.height = imageHeight * INTERMEDIATE_SCALE;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// Using bigger than default compression reduces image size by
// several times. Quality degradation compensated by greater resolution.
largeImage.src = canvas.toDataURL('image/jpeg', 0.6);
} else {
largeImage.src = img.src;
}
largeImageBox.className = 'popup';
var boxWidth = Math.max(THUMBNAIL_SIZE, imageWidth);
var boxHeight = Math.max(THUMBNAIL_SIZE, imageHeight);
if (transform && transform.rotate90 % 2 == 1) {
var t = boxWidth;
boxWidth = boxHeight;
boxHeight = t;
}
var style = largeImageBox.style;
style.width = boxWidth + 'px';
style.height = boxHeight + 'px';
style.top = (-boxHeight + THUMBNAIL_SIZE) + 'px';
var style = largeImage.style;
style.width = imageWidth + 'px';
style.height = imageHeight + 'px';
style.left = (boxWidth - imageWidth) / 2 + 'px';
style.top = (boxHeight - imageHeight) / 2 + 'px';
style.position = 'relative';
util.applyTransform(largeImage, transform);
largeImageBox.appendChild(largeImage);
largeImageBox.style.zIndex = 1000;
return true;
};