diff options
author | Andras Becsi <andras.becsi@digia.com> | 2014-03-18 13:16:26 +0100 |
---|---|---|
committer | Frederik Gladhorn <frederik.gladhorn@digia.com> | 2014-03-20 15:55:39 +0100 |
commit | 3f0f86b0caed75241fa71c95a5d73bc0164348c5 (patch) | |
tree | 92b9fb00f2e9e90b0be2262093876d4f43b6cd13 /chromium/chrome/browser/resources/file_manager | |
parent | e90d7c4b152c56919d963987e2503f9909a666d2 (diff) | |
download | qtwebengine-chromium-3f0f86b0caed75241fa71c95a5d73bc0164348c5.tar.gz |
Update to new stable branch 1750
This also includes an updated ninja and chromium dependencies
needed on Windows.
Change-Id: Icd597d80ed3fa4425933c9f1334c3c2e31291c42
Reviewed-by: Zoltan Arvai <zarvai@inf.u-szeged.hu>
Reviewed-by: Zeno Albisser <zeno.albisser@digia.com>
Diffstat (limited to 'chromium/chrome/browser/resources/file_manager')
526 files changed, 49268 insertions, 0 deletions
diff --git a/chromium/chrome/browser/resources/file_manager/OWNERS b/chromium/chrome/browser/resources/file_manager/OWNERS new file mode 100644 index 00000000000..bca0cd957a2 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/OWNERS @@ -0,0 +1,3 @@ +hirono@chromium.org +mtomasz@chromium.org +yoshiki@chromium.org diff --git a/chromium/chrome/browser/resources/file_manager/background/js/background.js b/chromium/chrome/browser/resources/file_manager/background/js/background.js new file mode 100644 index 00000000000..e5fb6b85fc7 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/background/js/background.js @@ -0,0 +1,787 @@ +// 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'; + +/** + * Number of runtime errors catched in the background page. + * @type {number} + */ +var JSErrorCount = 0; + +/** + * Counts runtime JavaScript errors. + */ +window.onerror = function() { JSErrorCount++; }; + +/** + * Type of a Files.app's instance launch. + * @enum {number} + */ +var LaunchType = Object.freeze({ + ALWAYS_CREATE: 0, + FOCUS_ANY_OR_CREATE: 1, + FOCUS_SAME_OR_CREATE: 2 +}); + +/** + * Root class of the background page. + * @constructor + */ +function Background() { + /** + * Map of all currently open app windows. The key is an app id. + * @type {Object.<string, AppWindow>} + */ + this.appWindows = {}; + + /** + * Synchronous queue for asynchronous calls. + * @type {AsyncUtil.Queue} + */ + this.queue = new AsyncUtil.Queue(); + + /** + * Progress center of the background page. + * @type {ProgressCenter} + */ + this.progressCenter = new ProgressCenter(); + + /** + * File operation manager. + * @type {FileOperationManager} + */ + this.fileOperationManager = FileOperationManager.getInstance(); + + /** + * Event handler for progress center. + * @type {FileOperationHandler} + * @private + */ + this.fileOperationHandler_ = new FileOperationHandler(this); + + /** + * String assets. + * @type {Object.<string, string>} + */ + this.stringData = null; + + /** + * Callback list to be invoked after initialization. + * It turns to null after initialization. + * + * @type {Array.<function()>} + * @private + */ + this.initializeCallbacks_ = []; + + /** + * Last time when the background page can close. + * + * @type {number} + * @private + */ + this.lastTimeCanClose_ = null; + + // Seal self. + Object.seal(this); + + // Initialize handlers. + chrome.fileBrowserHandler.onExecute.addListener(this.onExecute_.bind(this)); + chrome.app.runtime.onLaunched.addListener(this.onLaunched_.bind(this)); + chrome.app.runtime.onRestarted.addListener(this.onRestarted_.bind(this)); + chrome.contextMenus.onClicked.addListener( + this.onContextMenuClicked_.bind(this)); + + // Fetch strings and initialize the context menu. + this.queue.run(function(callNextStep) { + chrome.fileBrowserPrivate.getStrings(function(strings) { + // Initialize string assets. + this.stringData = strings; + loadTimeData.data = strings; + this.initContextMenu_(); + + // Invoke initialize callbacks. + for (var i = 0; i < this.initializeCallbacks_.length; i++) { + this.initializeCallbacks_[i](); + } + this.initializeCallbacks_ = null; + + callNextStep(); + }.bind(this)); + }.bind(this)); +} + +/** + * A number of delay milliseconds from the first call of tryClose to the actual + * close action. + * @type {number} + * @const + * @private + */ +Background.CLOSE_DELAY_MS_ = 5000; + +/** + * Make a key of window geometry preferences for the given initial URL. + * @param {string} url Initialize URL that the window has. + * @return {string} Key of window geometry preferences. + */ +Background.makeGeometryKey = function(url) { + return 'windowGeometry' + ':' + url; +}; + +/** + * Register callback to be invoked after initialization. + * If the initialization is already done, the callback is invoked immediately. + * + * @param {function()} callback Initialize callback to be registered. + */ +Background.prototype.ready = function(callback) { + if (this.initializeCallbacks_ !== null) + this.initializeCallbacks_.push(callback); + else + callback(); +}; + +/** + * Checks the current condition of background page and closes it if possible. + */ +Background.prototype.tryClose = function() { + // If the file operation is going, the background page cannot close. + if (this.fileOperationManager.hasQueuedTasks()) { + this.lastTimeCanClose_ = null; + return; + } + + var views = chrome.extension.getViews(); + var closing = false; + for (var i = 0; i < views.length; i++) { + // If the window that is not the background page itself and it is not + // closing, the background page cannot close. + if (views[i] !== window && !views[i].closing) { + this.lastTimeCanClose_ = null; + return; + } + closing = closing || views[i].closing; + } + + // If some windows are closing, or the background page can close but could not + // 5 seconds ago, We need more time for sure. + if (closing || + this.lastTimeCanClose_ === null || + Date.now() - this.lastTimeCanClose_ < Background.CLOSE_DELAY_MS_) { + if (this.lastTimeCanClose_ === null) + this.lastTimeCanClose_ = Date.now(); + setTimeout(this.tryClose.bind(this), Background.CLOSE_DELAY_MS_); + return; + } + + // Otherwise we can close the background page. + close(); +}; + +/** + * Gets similar windows, it means with the same initial url. + * @param {string} url URL that the obtained windows have. + * @return {Array.<AppWindow>} List of similar windows. + */ +Background.prototype.getSimilarWindows = function(url) { + var result = []; + for (var appID in this.appWindows) { + if (this.appWindows[appID].contentWindow.appInitialURL === url) + result.push(this.appWindows[appID]); + } + return result; +}; + +/** + * Wrapper for an app window. + * + * Expects the following from the app scripts: + * 1. The page load handler should initialize the app using |window.appState| + * and call |util.platform.saveAppState|. + * 2. Every time the app state changes the app should update |window.appState| + * and call |util.platform.saveAppState| . + * 3. The app may have |unload| function to persist the app state that does not + * fit into |window.appState|. + * + * @param {string} url App window content url. + * @param {string} id App window id. + * @param {Object} options Options object to create it. + * @constructor + */ +function AppWindowWrapper(url, id, options) { + this.url_ = url; + this.id_ = id; + // Do deep copy for the template of options to assign customized params later. + this.options_ = JSON.parse(JSON.stringify(options)); + this.window_ = null; + this.appState_ = null; + this.openingOrOpened_ = false; + this.queue = new AsyncUtil.Queue(); + Object.seal(this); +} + +/** + * Shift distance to avoid overlapping windows. + * @type {number} + * @const + */ +AppWindowWrapper.SHIFT_DISTANCE = 40; + + +/** + * Opens the window. + * + * @param {Object} appState App state. + * @param {function()=} opt_callback Completion callback. + */ +AppWindowWrapper.prototype.launch = function(appState, opt_callback) { + // Check if the window is opened or not. + if (this.openingOrOpened_) { + console.error('The window is already opened.'); + if (opt_callback) + opt_callback(); + return; + } + this.openingOrOpened_ = true; + + // Save application state. + this.appState_ = appState; + + // Get similar windows, it means with the same initial url, eg. different + // main windows of Files.app. + var similarWindows = background.getSimilarWindows(this.url_); + + // Restore maximized windows, to avoid hiding them to tray, which can be + // confusing for users. + this.queue.run(function(callback) { + for (var index = 0; index < similarWindows.length; index++) { + if (similarWindows[index].isMaximized()) { + var createWindowAndRemoveListener = function() { + similarWindows[index].onRestored.removeListener( + createWindowAndRemoveListener); + callback(); + }; + similarWindows[index].onRestored.addListener( + createWindowAndRemoveListener); + similarWindows[index].restore(); + return; + } + } + // If no maximized windows, then create the window immediately. + callback(); + }); + + // Obtains the last geometry. + var lastBounds; + this.queue.run(function(callback) { + var key = Background.makeGeometryKey(this.url_); + chrome.storage.local.get(key, function(preferences) { + if (!chrome.runtime.lastError) + lastBounds = preferences[key]; + callback(); + }); + }.bind(this)); + + // Closure creating the window, once all preprocessing tasks are finished. + this.queue.run(function(callback) { + // Apply the last bounds. + if (lastBounds) + this.options_.bounds = lastBounds; + + // Create a window. + chrome.app.window.create(this.url_, this.options_, function(appWindow) { + this.window_ = appWindow; + callback(); + }.bind(this)); + }.bind(this)); + + // After creating. + this.queue.run(function(callback) { + // If there is another window in the same position, shift the window. + var makeBoundsKey = function(bounds) { + return bounds.left + '/' + bounds.top; + }; + var notAvailablePositions = {}; + for (var i = 0; i < similarWindows.length; i++) { + var key = makeBoundsKey(similarWindows[i].getBounds()); + notAvailablePositions[key] = true; + } + var candidateBounds = this.window_.getBounds(); + while (true) { + var key = makeBoundsKey(candidateBounds); + if (!notAvailablePositions[key]) + break; + // Make the position available to avoid an infinite loop. + notAvailablePositions[key] = false; + var nextLeft = candidateBounds.left + AppWindowWrapper.SHIFT_DISTANCE; + var nextRight = nextLeft + candidateBounds.width; + candidateBounds.left = nextRight >= screen.availWidth ? + nextRight % screen.availWidth : nextLeft; + var nextTop = candidateBounds.top + AppWindowWrapper.SHIFT_DISTANCE; + var nextBottom = nextTop + candidateBounds.height; + candidateBounds.top = nextBottom >= screen.availHeight ? + nextBottom % screen.availHeight : nextTop; + } + this.window_.moveTo(candidateBounds.left, candidateBounds.top); + + // Save the properties. + var appWindow = this.window_; + background.appWindows[this.id_] = appWindow; + var contentWindow = appWindow.contentWindow; + contentWindow.appID = this.id_; + contentWindow.appState = this.appState_; + contentWindow.appInitialURL = this.url_; + if (window.IN_TEST) + contentWindow.IN_TEST = true; + + // Register event listners. + appWindow.onBoundsChanged.addListener(this.onBoundsChanged_.bind(this)); + appWindow.onClosed.addListener(this.onClosed_.bind(this)); + + // Callback. + if (opt_callback) + opt_callback(); + callback(); + }.bind(this)); +}; + +/** + * Handles the onClosed extension API event. + * @private + */ +AppWindowWrapper.prototype.onClosed_ = function() { + // Unload the window. + var appWindow = this.window_; + var contentWindow = this.window_.contentWindow; + if (contentWindow.unload) + contentWindow.unload(); + this.window_ = null; + this.openingOrOpened_ = false; + + // Updates preferences. + if (contentWindow.saveOnExit) { + contentWindow.saveOnExit.forEach(function(entry) { + util.AppCache.update(entry.key, entry.value); + }); + } + chrome.storage.local.remove(this.id_); // Forget the persisted state. + + // Remove the window from the set. + delete background.appWindows[this.id_]; + + // If there is no application window, reset window ID. + if (!Object.keys(background.appWindows).length) + nextFileManagerWindowID = 0; + background.tryClose(); +}; + +/** + * Handles onBoundsChanged extension API event. + * @private + */ +AppWindowWrapper.prototype.onBoundsChanged_ = function() { + var preferences = {}; + preferences[Background.makeGeometryKey(this.url_)] = + this.window_.getBounds(); + chrome.storage.local.set(preferences); +}; + +/** + * Wrapper for a singleton app window. + * + * In addition to the AppWindowWrapper requirements the app scripts should + * have |reload| method that re-initializes the app based on a changed + * |window.appState|. + * + * @param {string} url App window content url. + * @param {Object|function()} options Options object or a function to return it. + * @constructor + */ +function SingletonAppWindowWrapper(url, options) { + AppWindowWrapper.call(this, url, url, options); +} + +/** + * Inherits from AppWindowWrapper. + */ +SingletonAppWindowWrapper.prototype = {__proto__: AppWindowWrapper.prototype}; + +/** + * Open the window. + * + * Activates an existing window or creates a new one. + * + * @param {Object} appState App state. + * @param {function()=} opt_callback Completion callback. + */ +SingletonAppWindowWrapper.prototype.launch = function(appState, opt_callback) { + // If the window is not opened yet, just call the parent method. + if (!this.openingOrOpened_) { + AppWindowWrapper.prototype.launch.call(this, appState, opt_callback); + return; + } + + // If the window is already opened, reload the window. + // The queue is used to wait until the window is opened. + this.queue.run(function(nextStep) { + this.window_.contentWindow.appState = appState; + this.window_.contentWindow.reload(); + this.window_.focus(); + if (opt_callback) + opt_callback(); + nextStep(); + }.bind(this)); +}; + +/** + * Reopen a window if its state is saved in the local storage. + */ +SingletonAppWindowWrapper.prototype.reopen = function() { + chrome.storage.local.get(this.id_, function(items) { + var value = items[this.id_]; + if (!value) + return; // No app state persisted. + + try { + var appState = JSON.parse(value); + } catch (e) { + console.error('Corrupt launch data for ' + this.id_, value); + return; + } + this.launch(appState); + }.bind(this)); +}; + +/** + * Prefix for the file manager window ID. + */ +var FILES_ID_PREFIX = 'files#'; + +/** + * Regexp matching a file manager window ID. + */ +var FILES_ID_PATTERN = new RegExp('^' + FILES_ID_PREFIX + '(\\d*)$'); + +/** + * Value of the next file manager window ID. + */ +var nextFileManagerWindowID = 0; + +/** + * File manager window create options. + * @type {Object} + * @const + */ +var FILE_MANAGER_WINDOW_CREATE_OPTIONS = Object.freeze({ + bounds: Object.freeze({ + left: Math.round(window.screen.availWidth * 0.1), + top: Math.round(window.screen.availHeight * 0.1), + width: Math.round(window.screen.availWidth * 0.8), + height: Math.round(window.screen.availHeight * 0.8) + }), + minWidth: 320, + minHeight: 240, + frame: 'none', + hidden: true, + transparentBackground: true +}); + +/** + * @param {Object=} opt_appState App state. + * @param {number=} opt_id Window id. + * @param {LaunchType=} opt_type Launch type. Default: ALWAYS_CREATE. + * @param {function(string)=} opt_callback Completion callback with the App ID. + */ +function launchFileManager(opt_appState, opt_id, opt_type, opt_callback) { + var type = opt_type || LaunchType.ALWAYS_CREATE; + + // Wait until all windows are created. + background.queue.run(function(onTaskCompleted) { + // Check if there is already a window with the same path. If so, then + // reuse it instead of opening a new one. + if (type == LaunchType.FOCUS_SAME_OR_CREATE || + type == LaunchType.FOCUS_ANY_OR_CREATE) { + if (opt_appState && opt_appState.defaultPath) { + for (var key in background.appWindows) { + if (!key.match(FILES_ID_PATTERN)) + continue; + + var contentWindow = background.appWindows[key].contentWindow; + if (contentWindow.appState && + opt_appState.defaultPath == contentWindow.appState.defaultPath) { + background.appWindows[key].focus(); + if (opt_callback) + opt_callback(key); + onTaskCompleted(); + return; + } + } + } + } + + // Focus any window if none is focused. Try restored first. + if (type == LaunchType.FOCUS_ANY_OR_CREATE) { + // If there is already a focused window, then finish. + for (var key in background.appWindows) { + if (!key.match(FILES_ID_PATTERN)) + continue; + + // The isFocused() method should always be available, but in case + // Files.app's failed on some error, wrap it with try catch. + try { + if (background.appWindows[key].contentWindow.isFocused()) { + if (opt_callback) + opt_callback(key); + onTaskCompleted(); + return; + } + } catch (e) { + console.error(e.message); + } + } + // Try to focus the first non-minimized window. + for (var key in background.appWindows) { + if (!key.match(FILES_ID_PATTERN)) + continue; + + if (!background.appWindows[key].isMinimized()) { + background.appWindows[key].focus(); + if (opt_callback) + opt_callback(key); + onTaskCompleted(); + return; + } + } + // Restore and focus any window. + for (var key in background.appWindows) { + if (!key.match(FILES_ID_PATTERN)) + continue; + + background.appWindows[key].focus(); + if (opt_callback) + opt_callback(key); + onTaskCompleted(); + return; + } + } + + // Create a new instance in case of ALWAYS_CREATE type, or as a fallback + // for other types. + + var id = opt_id || nextFileManagerWindowID; + nextFileManagerWindowID = Math.max(nextFileManagerWindowID, id + 1); + var appId = FILES_ID_PREFIX + id; + + var appWindow = new AppWindowWrapper( + 'main.html', + appId, + FILE_MANAGER_WINDOW_CREATE_OPTIONS); + appWindow.launch(opt_appState || {}, function() { + if (opt_callback) + opt_callback(appId); + onTaskCompleted(); + }); + }); +} + +/** + * Executes a file browser task. + * + * @param {string} action Task id. + * @param {Object} details Details object. + * @private + */ +Background.prototype.onExecute_ = function(action, details) { + var urls = details.entries.map(function(e) { return e.toURL(); }); + + switch (action) { + case 'play': + launchAudioPlayer({items: urls, position: 0}); + break; + + case 'watch': + launchVideoPlayer(urls[0]); + break; + + default: + var launchEnable = null; + var queue = new AsyncUtil.Queue(); + queue.run(function(nextStep) { + // If it is not auto-open (triggered by mounting external devices), we + // always launch Files.app. + if (action != 'auto-open') { + launchEnable = true; + nextStep(); + return; + } + // If the disable-default-apps flag is on, Files.app is not opened + // automatically on device mount because it obstculs the manual test. + chrome.commandLinePrivate.hasSwitch('disable-default-apps', + function(flag) { + launchEnable = !flag; + nextStep(); + }); + }); + queue.run(function(nextStep) { + if (!launchEnable) { + nextStep(); + return; + } + + // Every other action opens a Files app window. + var appState = { + params: { + action: action + }, + defaultPath: details.entries[0].fullPath + }; + // For mounted devices just focus any Files.app window. The mounted + // volume will appear on the navigation list. + var type = action == 'auto-open' ? LaunchType.FOCUS_ANY_OR_CREATE : + LaunchType.FOCUS_SAME_OR_CREATE; + launchFileManager(appState, /* App ID */ undefined, type, nextStep); + }); + break; + } +}; + +/** + * Audio player window create options. + * @type {Object} + * @const + */ +var AUDIO_PLAYER_CREATE_OPTIONS = Object.freeze({ + type: 'panel', + hidden: true, + minHeight: 35 + 58, + minWidth: 280, + height: 35 + 58, + width: 280 +}); + +var audioPlayer = new SingletonAppWindowWrapper('mediaplayer.html', + AUDIO_PLAYER_CREATE_OPTIONS); + +/** + * Launch the audio player. + * @param {Object} playlist Playlist. + */ +function launchAudioPlayer(playlist) { + audioPlayer.launch(playlist); +} + +var videoPlayer = new SingletonAppWindowWrapper('video_player.html', + {hidden: true}); + +/** + * Launch the video player. + * @param {string} url Video url. + */ +function launchVideoPlayer(url) { + videoPlayer.launch({url: url}); +} + +/** + * Launches the app. + * @private + */ +Background.prototype.onLaunched_ = function() { + if (nextFileManagerWindowID == 0) { + // The app just launched. Remove window state records that are not needed + // any more. + chrome.storage.local.get(function(items) { + for (var key in items) { + if (items.hasOwnProperty(key)) { + if (key.match(FILES_ID_PATTERN)) + chrome.storage.local.remove(key); + } + } + }); + } + launchFileManager(null, null, LaunchType.FOCUS_ANY_OR_CREATE); +}; + +/** + * Restarted the app, restore windows. + * @private + */ +Background.prototype.onRestarted_ = function() { + // Reopen file manager windows. + chrome.storage.local.get(function(items) { + for (var key in items) { + if (items.hasOwnProperty(key)) { + var match = key.match(FILES_ID_PATTERN); + if (match) { + var id = Number(match[1]); + try { + var appState = JSON.parse(items[key]); + launchFileManager(appState, id); + } catch (e) { + console.error('Corrupt launch data for ' + id); + } + } + } + } + }); + + // Reopen sub-applications. + audioPlayer.reopen(); + videoPlayer.reopen(); +}; + +/** + * Handles clicks on a custom item on the launcher context menu. + * @param {OnClickData} info Event details. + * @private + */ +Background.prototype.onContextMenuClicked_ = function(info) { + if (info.menuItemId == 'new-window') { + // Find the focused window (if any) and use it's current path for the + // new window. If not found, then launch with the default path. + for (var key in background.appWindows) { + try { + if (background.appWindows[key].contentWindow.isFocused()) { + var appState = { + defaultPath: background.appWindows[key].contentWindow. + appState.defaultPath + }; + launchFileManager(appState); + return; + } + } catch (ignore) { + // The isFocused method may not be defined during initialization. + // Therefore, wrapped with a try-catch block. + } + } + + // Launch with the default path. + launchFileManager(); + } +}; + +/** + * Initializes the context menu. Recreates if already exists. + * @private + */ +Background.prototype.initContextMenu_ = function() { + try { + chrome.contextMenus.remove('new-window'); + } catch (ignore) { + // There is no way to detect if the context menu is already added, therefore + // try to recreate it every time. + } + chrome.contextMenus.create({ + id: 'new-window', + contexts: ['launcher'], + title: str('NEW_WINDOW_BUTTON_LABEL') + }); +}; + +/** + * Singleton instance of Background. + * @type {Background} + */ +window.background = new Background(); diff --git a/chromium/chrome/browser/resources/file_manager/background/js/file_operation_handler.js b/chromium/chrome/browser/resources/file_manager/background/js/file_operation_handler.js new file mode 100644 index 00000000000..c51ed03515c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/background/js/file_operation_handler.js @@ -0,0 +1,315 @@ +// Copyright 2013 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'; + +/** + * An event handler of the background page for file operaitons. + * @param {Background} background Background page. + * @constructor + */ +var FileOperationHandler = function(background) { + /** + * Background page. + * @type {Background} + * @private + */ + this.background_ = background; + + /** + * File operation manager. + * @type {FileOperationManager} + * @private + */ + this.fileOperationManager_ = background.fileOperationManager; + + /** + * Progress center. + * @type {progressCenter} + * @private + */ + this.progressCenter_ = background.progressCenter; + + /** + * Pending items of delete operation. + * + * Delete operations are usually complete quickly. + * So we would not like to show the progress bar at first. + * If the operation takes more than FileOperationHandler.PENDING_TIME_MS_, + * we adds the item to the progress center. + * + * @type {Object.<string, ProgressCenterItem>}} + * @private + */ + this.pendingItems_ = {}; + + // Register event. + this.fileOperationManager_.addEventListener( + 'copy-progress', + this.onCopyProgress_.bind(this)); + this.fileOperationManager_.addEventListener( + 'delete', + this.onDeleteProgress_.bind(this)); + + // Seal the object. + Object.seal(this); +}; + +/** + * Pending time before a delete item is added to the progress center. + * + * @type {number} + * @const + * @private + */ +FileOperationHandler.PENDING_TIME_MS_ = 500; + +/** + * Generate a progress message from the event. + * @param {Event} event Progress event. + * @return {string} message. + * @private + */ +FileOperationHandler.getMessage_ = function(event) { + if (event.reason === 'ERROR') { + switch (event.error.code) { + case util.FileOperationErrorType.TARGET_EXISTS: + var name = event.error.data.name; + if (event.error.data.isDirectory) + name += '/'; + switch (event.status.operationType) { + case 'COPY': return strf('COPY_TARGET_EXISTS_ERROR', name); + case 'MOVE': return strf('MOVE_TARGET_EXISTS_ERROR', name); + case 'ZIP': return strf('ZIP_TARGET_EXISTS_ERROR', name); + default: return strf('TRANSFER_TARGET_EXISTS_ERROR', name); + } + + case util.FileOperationErrorType.FILESYSTEM_ERROR: + var detail = util.getFileErrorString(event.error.data.code); + switch (event.status.operationType) { + case 'COPY': return strf('COPY_FILESYSTEM_ERROR', detail); + case 'MOVE': return strf('MOVE_FILESYSTEM_ERROR', detail); + case 'ZIP': return strf('ZIP_FILESYSTEM_ERROR', detail); + default: return strf('TRANSFER_FILESYSTEM_ERROR', detail); + } + + default: + switch (event.status.operationType) { + case 'COPY': return strf('COPY_UNEXPECTED_ERROR', event.error.code); + case 'MOVE': return strf('MOVE_UNEXPECTED_ERROR', event.error.code); + case 'ZIP': return strf('ZIP_UNEXPECTED_ERROR', event.error.code); + default: return strf('TRANSFER_UNEXPECTED_ERROR', event.error.code); + } + } + } else if (event.status.numRemainingItems === 1) { + var name = event.status.processingEntry.name; + switch (event.status.operationType) { + case 'COPY': return strf('COPY_FILE_NAME', name); + case 'MOVE': return strf('MOVE_FILE_NAME', name); + case 'ZIP': return strf('ZIP_FILE_NAME', name); + default: return strf('TRANSFER_FILE_NAME', name); + } + } else { + var remainNumber = event.status.numRemainingItems; + switch (event.status.operationType) { + case 'COPY': return strf('COPY_ITEMS_REMAINING', remainNumber); + case 'MOVE': return strf('MOVE_ITEMS_REMAINING', remainNumber); + case 'ZIP': return strf('ZIP_ITEMS_REMAINING', remainNumber); + default: return strf('TRANSFER_ITEMS_REMAINING', remainNumber); + } + } +}; + +/** + * Generates a delete message from the event. + * @param {Event} event Progress event. + * @return {string} message. + * @private + */ +FileOperationHandler.getDeleteMessage_ = function(event) { + if (event.reason === 'ERROR') { + return str('DELETE_ERROR'); + } else if (event.entries.length == 1) { + var fileName = event.entries[0].name; + return strf('DELETE_FILE_NAME', fileName); + } else if (event.entries.length > 1) { + return strf('DELETE_ITEMS_REMAINING', event.entries.length); + } else { + return ''; + } +}; + +/** + * Obtains ProgressItemType from OperationType of FileTransferManager. + * @param {string} operationType OperationType of FileTransferManager. + * @return {ProgressItemType} ProgreeType corresponding to the specified + * operation type. + * @private + */ +FileOperationHandler.getType_ = function(operationType) { + switch (operationType) { + case 'COPY': return ProgressItemType.COPY; + case 'MOVE': return ProgressItemType.MOVE; + case 'ZIP': return ProgressItemType.ZIP; + default: + console.error('Unknown operation type.'); + return ProgressItemType.TRANSFER; + } +}; + +/** + * Handles the copy-progress event. + * @param {Event} event The copy-progress event. + * @private + */ +FileOperationHandler.prototype.onCopyProgress_ = function(event) { + // If the copy is finished, may be we can close the background page. + if (event.reason !== 'BEGIN' && event.reason !== 'PROGRESS') + this.background_.tryClose(); + + // Update progress center. + var progressCenter = this.progressCenter_; + var item; + switch (event.reason) { + case 'BEGIN': + item = new ProgressCenterItem(); + item.id = event.taskId; + item.type = FileOperationHandler.getType_(event.status.operationType); + item.message = FileOperationHandler.getMessage_(event); + item.progressMax = event.status.totalBytes; + item.progressValue = event.status.processedBytes; + item.cancelCallback = this.fileOperationManager_.requestTaskCancel.bind( + this.fileOperationManager_, + event.taskId); + progressCenter.updateItem(item); + break; + + case 'PROGRESS': + item = progressCenter.getItemById(event.taskId); + if (!item) { + console.error('Cannot find copying item.'); + return; + } + item.message = FileOperationHandler.getMessage_(event); + item.progressValue = event.status.processedBytes; + progressCenter.updateItem(item); + break; + + case 'SUCCESS': + case 'CANCELED': + case 'ERROR': + item = progressCenter.getItemById(event.taskId); + if (!item) { + // ERROR events can be dispatched before BEGIN events. + item = new ProgressCenterItem(); + item.type = FileOperationHandler.getType_(event.status.operationType); + item.id = event.taskId; + item.progressMax = 1; + } + if (event.reason === 'SUCCESS') { + item.message = ''; + item.state = ProgressItemState.COMPLETED; + item.progressValue = item.progressMax; + } else if (event.reason === 'CANCELED') { + item.message = ''; + item.state = ProgressItemState.CANCELED; + } else { + item.message = FileOperationHandler.getMessage_(event); + item.state = ProgressItemState.ERROR; + } + progressCenter.updateItem(item); + break; + } +}; + +/** + * Handles the delete event. + * @param {Event} event The delete event. + * @private + */ +FileOperationHandler.prototype.onDeleteProgress_ = function(event) { + // If the copy is finished, may be we can close the background page. + if (event.reason !== 'BEGIN' && event.reason !== 'PROGRESS') + this.background_.tryClose(); + + // Update progress center. + var progressCenter = this.progressCenter_; + var item; + var pending; + switch (event.reason) { + case 'BEGIN': + item = new ProgressCenterItem(); + item.id = event.taskId; + item.type = ProgressItemType.DELETE; + item.message = FileOperationHandler.getDeleteMessage_(event); + item.progressMax = event.totalBytes; + item.progressValue = event.processedBytes; + item.cancelCallback = this.fileOperationManager_.requestTaskCancel.bind( + this.fileOperationManager_, + event.taskId); + this.pendingItems_[item.id] = item; + setTimeout(this.showPendingItem_.bind(this, item), + FileOperationHandler.PENDING_TIME_MS_); + break; + + case 'PROGRESS': + pending = event.taskId in this.pendingItems_; + item = this.pendingItems_[event.taskId] || + progressCenter.getItemById(event.taskId); + if (!item) { + console.error('Cannot find deleting item.'); + return; + } + item.message = FileOperationHandler.getDeleteMessage_(event); + item.progressMax = event.totalBytes; + item.progressValue = event.processedBytes; + if (!pending) + progressCenter.updateItem(item); + break; + + case 'SUCCESS': + case 'CANCELED': + case 'ERROR': + // Obtain working variable. + pending = event.taskId in this.pendingItems_; + item = this.pendingItems_[event.taskId] || + progressCenter.getItemById(event.taskId); + if (!item) { + console.error('Cannot find deleting item.'); + return; + } + + // Update the item. + item.message = FileOperationHandler.getDeleteMessage_(event); + if (event.reason === 'SUCCESS') { + item.state = ProgressItemState.COMPLETED; + item.progressValue = item.progressMax; + } else if (event.reason === 'CANCELED') { + item.state = ProgressItemState.CANCELED; + } else { + item.state = ProgressItemState.ERROR; + } + + // Apply the change. + if (!pending || event.reason === 'ERROR') + progressCenter.updateItem(item); + if (pending) + delete this.pendingItems_[event.taskId]; + break; + } +}; + +/** + * Shows the pending item. + * + * @param {ProgressCenterItem} item Pending item. + * @private + */ +FileOperationHandler.prototype.showPendingItem_ = function(item) { + // The item is already gone. + if (!this.pendingItems_[item.id]) + return; + delete this.pendingItems_[item.id]; + this.progressCenter_.updateItem(item); +}; diff --git a/chromium/chrome/browser/resources/file_manager/background/js/file_operation_manager.js b/chromium/chrome/browser/resources/file_manager/background/js/file_operation_manager.js new file mode 100644 index 00000000000..79cbe1a1d59 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/background/js/file_operation_manager.js @@ -0,0 +1,1400 @@ +// Copyright 2013 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'; + +/** + * Utilities for FileOperationManager. + */ +var fileOperationUtil = {}; + +/** + * Simple wrapper for util.deduplicatePath. On error, this method translates + * the FileError to FileOperationManager.Error object. + * + * @param {DirectoryEntry} dirEntry The target directory entry. + * @param {string} relativePath The path to be deduplicated. + * @param {function(string)} successCallback Callback run with the deduplicated + * path on success. + * @param {function(FileOperationManager.Error)} errorCallback Callback run on + * error. + */ +fileOperationUtil.deduplicatePath = function( + dirEntry, relativePath, successCallback, errorCallback) { + util.deduplicatePath( + dirEntry, relativePath, successCallback, + function(err) { + var onFileSystemError = function(error) { + errorCallback(new FileOperationManager.Error( + util.FileOperationErrorType.FILESYSTEM_ERROR, error)); + }; + + if (err.code == FileError.PATH_EXISTS_ERR) { + // Failed to uniquify the file path. There should be an existing + // entry, so return the error with it. + util.resolvePath( + dirEntry, relativePath, + function(entry) { + errorCallback(new FileOperationManager.Error( + util.FileOperationErrorType.TARGET_EXISTS, entry)); + }, + onFileSystemError); + return; + } + onFileSystemError(err); + }); +}; + +/** + * Traverses files/subdirectories of the given entry, and returns them. + * In addition, this method annotate the size of each entry. The result will + * include the entry itself. + * + * @param {Entry} entry The root Entry for traversing. + * @param {function(Array.<Entry>)} successCallback Called when the traverse + * is successfully done with the array of the entries. + * @param {function(FileError)} errorCallback Called on error with the first + * occurred error (i.e. following errors will just be discarded). + */ +fileOperationUtil.resolveRecursively = function( + entry, successCallback, errorCallback) { + var result = []; + var error = null; + var numRunningTasks = 0; + + var maybeInvokeCallback = function() { + // If there still remain some running tasks, wait their finishing. + if (numRunningTasks > 0) + return; + + if (error) + errorCallback(error); + else + successCallback(result); + }; + + // The error handling can be shared. + var onError = function(fileError) { + // If this is the first error, remember it. + if (!error) + error = fileError; + --numRunningTasks; + maybeInvokeCallback(); + }; + + var process = function(entry) { + numRunningTasks++; + result.push(entry); + if (entry.isDirectory) { + // The size of a directory is 1 bytes here, so that the progress bar + // will work smoother. + // TODO(hidehiko): Remove this hack. + entry.size = 1; + + // Recursively traverse children. + var reader = entry.createReader(); + reader.readEntries( + function processSubEntries(subEntries) { + if (error || subEntries.length == 0) { + // If an error is found already, or this is the completion + // callback, then finish the process. + --numRunningTasks; + maybeInvokeCallback(); + return; + } + + for (var i = 0; i < subEntries.length; i++) + process(subEntries[i]); + + // Continue to read remaining children. + reader.readEntries(processSubEntries, onError); + }, + onError); + } else { + // For a file, annotate the file size. + entry.getMetadata(function(metadata) { + entry.size = metadata.size; + --numRunningTasks; + maybeInvokeCallback(); + }, onError); + } + }; + + process(entry); +}; + +/** + * Copies source to parent with the name newName recursively. + * This should work very similar to FileSystem API's copyTo. The difference is; + * - The progress callback is supported. + * - The cancellation is supported. + * + * @param {Entry} source The entry to be copied. + * @param {DirectoryEntry} parent The entry of the destination directory. + * @param {string} newName The name of copied file. + * @param {function(string, string)} entryChangedCallback + * Callback invoked when an entry is created with the source url and + * the destination url. + * @param {function(string, number)} progressCallback Callback invoked + * periodically during the copying. It takes the source url and the + * processed bytes of it. + * @param {function(string)} successCallback Callback invoked when the copy + * is successfully done with the url of the created entry. + * @param {function(FileError)} errorCallback Callback invoked when an error + * is found. + * @return {function()} Callback to cancel the current file copy operation. + * When the cancel is done, errorCallback will be called. The returned + * callback must not be called more than once. + */ +fileOperationUtil.copyTo = function( + source, parent, newName, entryChangedCallback, progressCallback, + successCallback, errorCallback) { + var copyId = null; + var pendingCallbacks = []; + + var onCopyProgress = function(progressCopyId, status) { + if (copyId == null) { + // If the copyId is not yet available, wait for it. + pendingCallbacks.push( + onCopyProgress.bind(null, progressCopyId, status)); + return; + } + + // This is not what we're interested in. + if (progressCopyId != copyId) + return; + + switch (status.type) { + case 'begin_copy_entry': + break; + + case 'end_copy_entry': + entryChangedCallback(status.sourceUrl, status.destinationUrl); + break; + + case 'progress': + progressCallback(status.sourceUrl, status.size); + break; + + case 'success': + chrome.fileBrowserPrivate.onCopyProgress.removeListener(onCopyProgress); + successCallback(status.destinationUrl); + break; + + case 'error': + chrome.fileBrowserPrivate.onCopyProgress.removeListener(onCopyProgress); + errorCallback(util.createFileError(status.error)); + break; + + default: + // Found unknown state. Cancel the task, and return an error. + console.error('Unknown progress type: ' + status.type); + chrome.fileBrowserPrivate.onCopyProgress.removeListener(onCopyProgress); + chrome.fileBrowserPrivate.cancelCopy(copyId); + errorCallback(util.createFileError(FileError.INVALID_STATE_ERR)); + } + }; + + // Register the listener before calling startCopy. Otherwise some events + // would be lost. + chrome.fileBrowserPrivate.onCopyProgress.addListener(onCopyProgress); + + // Then starts the copy. + chrome.fileBrowserPrivate.startCopy( + source.toURL(), parent.toURL(), newName, function(startCopyId) { + // last error contains the FileError code on error. + if (chrome.runtime.lastError) { + // Unsubscribe the progress listener. + chrome.fileBrowserPrivate.onCopyProgress.removeListener( + onCopyProgress); + errorCallback(util.createFileError( + Integer.parseInt(chrome.runtime.lastError, 10))); + return; + } + + copyId = startCopyId; + for (var i = 0; i < pendingCallbacks.length; i++) { + pendingCallbacks[i](); + } + }); + + return function() { + // If copyId is not yet available, wait for it. + if (copyId == null) { + pendingCallbacks.push(function() { + chrome.fileBrowserPrivate.cancelCopy(copyId); + }); + return; + } + + chrome.fileBrowserPrivate.cancelCopy(copyId); + }; +}; + +/** + * Thin wrapper of chrome.fileBrowserPrivate.zipSelection to adapt its + * interface similar to copyTo(). + * + * @param {Array.<Entry>} sources The array of entries to be archived. + * @param {DirectoryEntry} parent The entry of the destination directory. + * @param {string} newName The name of the archive to be created. + * @param {function(FileEntry)} successCallback Callback invoked when the + * operation is successfully done with the entry of the created archive. + * @param {function(FileError)} errorCallback Callback invoked when an error + * is found. + */ +fileOperationUtil.zipSelection = function( + sources, parent, newName, successCallback, errorCallback) { + // TODO(mtomasz): Pass Entries instead of URLs. Entries can be converted to + // URLs in custom bindings. + chrome.fileBrowserPrivate.zipSelection( + parent.toURL(), + util.entriesToURLs(sources), + newName, function(success) { + if (!success) { + // Failed to create a zip archive. + errorCallback( + util.createFileError(FileError.INVALID_MODIFICATION_ERR)); + return; + } + + // Returns the created entry via callback. + parent.getFile( + newName, {create: false}, successCallback, errorCallback); + }); +}; + +/** + * @constructor + */ +function FileOperationManager() { + this.copyTasks_ = []; + this.deleteTasks_ = []; + this.taskIdCounter_ = 0; + + this.eventRouter_ = new FileOperationManager.EventRouter(); + + Object.seal(this); +} + +/** + * Get FileOperationManager instance. In case is hasn't been initialized, a new + * instance is created. + * + * @return {FileOperationManager} A FileOperationManager instance. + */ +FileOperationManager.getInstance = function() { + if (!FileOperationManager.instance_) + FileOperationManager.instance_ = new FileOperationManager(); + + return FileOperationManager.instance_; +}; + +/** + * Manages Event dispatching. + * Currently this can send three types of events: "copy-progress", + * "copy-operation-completed" and "delete". + * + * TODO(hidehiko): Reorganize the event dispatching mechanism. + * @constructor + * @extends {cr.EventTarget} + */ +FileOperationManager.EventRouter = function() { +}; + +/** + * Extends cr.EventTarget. + */ +FileOperationManager.EventRouter.prototype.__proto__ = cr.EventTarget.prototype; + +/** + * Dispatches a simple "copy-progress" event with reason and current + * FileOperationManager status. If it is an ERROR event, error should be set. + * + * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS", + * "ERROR" or "CANCELLED". TODO(hidehiko): Use enum. + * @param {Object} status Current FileOperationManager's status. See also + * FileOperationManager.getStatus(). + * @param {string} taskId ID of task related with the event. + * @param {FileOperationManager.Error=} opt_error The info for the error. This + * should be set iff the reason is "ERROR". + */ +FileOperationManager.EventRouter.prototype.sendProgressEvent = function( + reason, status, taskId, opt_error) { + var event = new Event('copy-progress'); + event.reason = reason; + event.status = status; + event.taskId = taskId; + if (opt_error) + event.error = opt_error; + this.dispatchEvent(event); +}; + +/** + * Dispatches an event to notify that an entry is changed (created or deleted). + * @param {util.EntryChangedKind} kind The enum to represent if the entry is + * created or deleted. + * @param {Entry} entry The changed entry. + */ +FileOperationManager.EventRouter.prototype.sendEntryChangedEvent = function( + kind, entry) { + var event = new Event('entry-changed'); + event.kind = kind; + event.entry = entry; + this.dispatchEvent(event); +}; + +/** + * Dispatches an event to notify entries are changed for delete task. + * + * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS", + * or "ERROR". TODO(hidehiko): Use enum. + * @param {DeleteTask} task Delete task related with the event. + */ +FileOperationManager.EventRouter.prototype.sendDeleteEvent = function( + reason, task) { + var event = new Event('delete'); + event.reason = reason; + event.taskId = task.taskId; + event.entries = task.entries; + event.totalBytes = task.totalBytes; + event.processedBytes = task.processedBytes; + // TODO(hirono): Remove the urls property from the event. + event.urls = util.entriesToURLs(task.entries); + this.dispatchEvent(event); +}; + +/** + * A record of a queued copy operation. + * + * Multiple copy operations may be queued at any given time. Additional + * Tasks may be added while the queue is being serviced. Though a + * cancel operation cancels everything in the queue. + * + * @param {util.FileOperationType} operationType The type of this operation. + * @param {Array.<Entry>} sourceEntries Array of source entries. + * @param {DirectoryEntry} targetDirEntry Target directory. + * @constructor + */ +FileOperationManager.Task = function( + operationType, sourceEntries, targetDirEntry) { + this.operationType = operationType; + this.sourceEntries = sourceEntries; + this.targetDirEntry = targetDirEntry; + + /** + * An array of map from url to Entry being processed. + * @type {Array.<Object<string, Entry>>} + */ + this.processingEntries = null; + + /** + * Total number of bytes to be processed. Filled in initialize(). + * @type {number} + */ + this.totalBytes = 0; + + /** + * Total number of already processed bytes. Updated periodically. + * @type {number} + */ + this.processedBytes = 0; + + this.deleteAfterCopy = false; + + /** + * Set to true when cancel is requested. + * @private {boolean} + */ + this.cancelRequested_ = false; + + /** + * Callback to cancel the running process. + * @private {function()} + */ + this.cancelCallback_ = null; + + // TODO(hidehiko): After we support recursive copy, we don't need this. + // If directory already exists, we try to make a copy named 'dir (X)', + // where X is a number. When we do this, all subsequent copies from + // inside the subtree should be mapped to the new directory name. + // For example, if 'dir' was copied as 'dir (1)', then 'dir\file.txt' should + // become 'dir (1)\file.txt'. + this.renamedDirectories_ = []; +}; + +/** + * @param {function()} callback When entries resolved. + */ +FileOperationManager.Task.prototype.initialize = function(callback) { +}; + +/** + * Updates copy progress status for the entry. + * + * @param {number} size Number of bytes that has been copied since last update. + */ +FileOperationManager.Task.prototype.updateFileCopyProgress = function(size) { + this.completedBytes += size; +}; + +/** + * Requests cancellation of this task. + * When the cancellation is done, it is notified via callbacks of run(). + */ +FileOperationManager.Task.prototype.requestCancel = function() { + this.cancelRequested_ = true; + if (this.cancelCallback_) { + this.cancelCallback_(); + this.cancelCallback_ = null; + } +}; + +/** + * Runs the task. Sub classes must implement this method. + * + * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback + * Callback invoked when an entry is changed. + * @param {function()} progressCallback Callback invoked periodically during + * the operation. + * @param {function()} successCallback Callback run on success. + * @param {function(FileOperationManager.Error)} errorCallback Callback run on + * error. + */ +FileOperationManager.Task.prototype.run = function( + entryChangedCallback, progressCallback, successCallback, errorCallback) { +}; + +/** + * Get states of the task. + * TOOD(hirono): Removes this method and sets a task to progress events. + * @return {object} Status object. + */ +FileOperationManager.Task.prototype.getStatus = function() { + var numRemainingItems = this.countRemainingItems(); + return { + operationType: this.operationType, + numRemainingItems: numRemainingItems, + totalBytes: this.totalBytes, + processedBytes: this.processedBytes, + processingEntry: this.getSingleEntry() + }; +}; + +/** + * Counts the number of remaining items. + * @return {number} Number of remaining items. + */ +FileOperationManager.Task.prototype.countRemainingItems = function() { + var count = 0; + for (var i = 0; i < this.processingEntries.length; i++) { + for (var url in this.processingEntries[i]) { + count++; + } + } + return count; +}; + +/** + * Obtains the single processing entry. If there are multiple processing + * entries, it returns null. + * @return {Entry} First entry. + */ +FileOperationManager.Task.prototype.getSingleEntry = function() { + if (this.countRemainingItems() !== 1) + return null; + for (var i = 0; i < this.processingEntries.length; i++) { + var entryMap = this.processingEntries[i]; + for (var name in entryMap) + return entryMap[name]; + } + return null; +}; + +/** + * Task to copy entries. + * + * @param {Array.<Entry>} sourceEntries Array of source entries. + * @param {DirectoryEntry} targetDirEntry Target directory. + * @constructor + * @extends {FileOperationManager.Task} + */ +FileOperationManager.CopyTask = function(sourceEntries, targetDirEntry) { + FileOperationManager.Task.call( + this, util.FileOperationType.COPY, sourceEntries, targetDirEntry); +}; + +/** + * Extends FileOperationManager.Task. + */ +FileOperationManager.CopyTask.prototype.__proto__ = + FileOperationManager.Task.prototype; + +/** + * Initializes the CopyTask. + * @param {function()} callback Called when the initialize is completed. + */ +FileOperationManager.CopyTask.prototype.initialize = function(callback) { + var group = new AsyncUtil.Group(); + // Correct all entries to be copied for status update. + this.processingEntries = []; + for (var i = 0; i < this.sourceEntries.length; i++) { + group.add(function(index, callback) { + fileOperationUtil.resolveRecursively( + this.sourceEntries[index], + function(resolvedEntries) { + var resolvedEntryMap = {}; + for (var j = 0; j < resolvedEntries.length; ++j) { + var entry = resolvedEntries[j]; + entry.processedBytes = 0; + resolvedEntryMap[entry.toURL()] = entry; + } + this.processingEntries[index] = resolvedEntryMap; + callback(); + }.bind(this), + function(error) { + console.error( + 'Failed to resolve for copy: %s', + util.getFileErrorMnemonic(error.code)); + }); + }.bind(this, i)); + } + + group.run(function() { + // Fill totalBytes. + this.totalBytes = 0; + for (var i = 0; i < this.processingEntries.length; i++) { + for (var url in this.processingEntries[i]) + this.totalBytes += this.processingEntries[i][url].size; + } + + callback(); + }.bind(this)); +}; + +/** + * Copies all entries to the target directory. + * Note: this method contains also the operation of "Move" due to historical + * reason. + * + * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback + * Callback invoked when an entry is changed. + * @param {function()} progressCallback Callback invoked periodically during + * the copying. + * @param {function()} successCallback On success. + * @param {function(FileOperationManager.Error)} errorCallback On error. + * @override + */ +FileOperationManager.CopyTask.prototype.run = function( + entryChangedCallback, progressCallback, successCallback, errorCallback) { + // TODO(hidehiko): We should be able to share the code to iterate on entries + // with serviceMoveTask_(). + if (this.sourceEntries.length == 0) { + successCallback(); + return; + } + + // TODO(hidehiko): Delete after copy is the implementation of Move. + // Migrate the part into MoveTask.run(). + var deleteOriginals = function() { + var count = this.sourceEntries.length; + + var onEntryDeleted = function(entry) { + entryChangedCallback(util.EntryChangedKind.DELETED, entry); + count--; + if (!count) + successCallback(); + }; + + var onFilesystemError = function(err) { + errorCallback(new FileOperationManager.Error( + util.FileOperationErrorType.FILESYSTEM_ERROR, err)); + }; + + for (var i = 0; i < this.sourceEntries.length; i++) { + var entry = this.sourceEntries[i]; + util.removeFileOrDirectory( + entry, onEntryDeleted.bind(null, entry), onFilesystemError); + } + }.bind(this); + + AsyncUtil.forEach( + this.sourceEntries, + function(callback, entry, index) { + if (this.cancelRequested_) { + errorCallback(new FileOperationManager.Error( + util.FileOperationErrorType.FILESYSTEM_ERROR, + util.createFileError(FileError.ABORT_ERR))); + return; + } + progressCallback(); + this.processEntry_( + entry, this.targetDirEntry, + function(sourceUrl, destinationUrl) { + // Finalize the entry's progress state. + var entry = this.processingEntries[index][sourceUrl]; + if (entry) { + this.processedBytes += entry.size - entry.processedBytes; + progressCallback(); + delete this.processingEntries[index][sourceUrl]; + } + + webkitResolveLocalFileSystemURL( + destinationUrl, function(destinationEntry) { + entryChangedCallback( + util.EntryChangedKind.CREATED, destinationEntry); + }); + }.bind(this), + function(source_url, size) { + var entry = this.processingEntries[index][source_url]; + if (entry) { + this.processedBytes += size - entry.processedBytes; + entry.processedBytes = size; + progressCallback(); + } + }.bind(this), + callback, + errorCallback); + }, + function() { + if (this.deleteAfterCopy) { + deleteOriginals(); + } else { + successCallback(); + } + }.bind(this), + this); +}; + +/** + * Copies the source entry to the target directory. + * + * @param {Entry} sourceEntry An entry to be copied. + * @param {DirectoryEntry} destinationEntry The entry which will contain the + * copied entry. + * @param {function(string, string)} entryChangedCallback + * Callback invoked when an entry is created with the source url and + * the destination url. + * @param {function(string, number)} progressCallback Callback invoked + * periodically during the copying. + * @param {function()} successCallback On success. + * @param {function(FileOperationManager.Error)} errorCallback On error. + * @private + */ +FileOperationManager.CopyTask.prototype.processEntry_ = function( + sourceEntry, destinationEntry, entryChangedCallback, progressCallback, + successCallback, errorCallback) { + fileOperationUtil.deduplicatePath( + destinationEntry, sourceEntry.name, + function(destinationName) { + if (this.cancelRequested_) { + errorCallback(new FileOperationManager.Error( + util.FileOperationErrorType.FILESYSTEM_ERROR, + util.createFileError(FileError.ABORT_ERR))); + return; + } + this.cancelCallback_ = fileOperationUtil.copyTo( + sourceEntry, destinationEntry, destinationName, + entryChangedCallback, progressCallback, + function(entry) { + this.cancelCallback_ = null; + successCallback(); + }.bind(this), + function(error) { + this.cancelCallback_ = null; + errorCallback(new FileOperationManager.Error( + util.FileOperationErrorType.FILESYSTEM_ERROR, error)); + }.bind(this)); + }.bind(this), + errorCallback); +}; + +/** + * Task to move entries. + * + * @param {Array.<Entry>} sourceEntries Array of source entries. + * @param {DirectoryEntry} targetDirEntry Target directory. + * @constructor + * @extends {FileOperationManager.Task} + */ +FileOperationManager.MoveTask = function(sourceEntries, targetDirEntry) { + FileOperationManager.Task.call( + this, util.FileOperationType.MOVE, sourceEntries, targetDirEntry); +}; + +/** + * Extends FileOperationManager.Task. + */ +FileOperationManager.MoveTask.prototype.__proto__ = + FileOperationManager.Task.prototype; + +/** + * Initializes the MoveTask. + * @param {function()} callback Called when the initialize is completed. + */ +FileOperationManager.MoveTask.prototype.initialize = function(callback) { + // This may be moving from search results, where it fails if we + // move parent entries earlier than child entries. We should + // process the deepest entry first. Since move of each entry is + // done by a single moveTo() call, we don't need to care about the + // recursive traversal order. + this.sourceEntries.sort(function(entry1, entry2) { + return entry2.fullPath.length - entry1.fullPath.length; + }); + + this.processingEntries = []; + for (var i = 0; i < this.sourceEntries.length; i++) { + var processingEntryMap = {}; + var entry = this.sourceEntries[i]; + + // The move should be done with updating the metadata. So here we assume + // all the file size is 1 byte. (Avoiding 0, so that progress bar can + // move smoothly). + // TODO(hidehiko): Remove this hack. + entry.size = 1; + processingEntryMap[entry.toURL()] = entry; + this.processingEntries[i] = processingEntryMap; + } + + callback(); +}; + +/** + * Moves all entries in the task. + * + * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback + * Callback invoked when an entry is changed. + * @param {function()} progressCallback Callback invoked periodically during + * the moving. + * @param {function()} successCallback On success. + * @param {function(FileOperationManager.Error)} errorCallback On error. + * @override + */ +FileOperationManager.MoveTask.prototype.run = function( + entryChangedCallback, progressCallback, successCallback, errorCallback) { + if (this.sourceEntries.length == 0) { + successCallback(); + return; + } + + AsyncUtil.forEach( + this.sourceEntries, + function(callback, entry, index) { + if (this.cancelRequested_) { + errorCallback(new FileOperationManager.Error( + util.FileOperationErrorType.FILESYSTEM_ERROR, + util.createFileError(FileError.ABORT_ERR))); + return; + } + progressCallback(); + FileOperationManager.MoveTask.processEntry_( + entry, this.targetDirEntry, entryChangedCallback, + function() { + // Erase the processing entry. + this.processingEntries[index] = {}; + this.processedBytes++; + callback(); + }.bind(this), + errorCallback); + }, + function() { + successCallback(); + }.bind(this), + this); +}; + +/** + * Moves the sourceEntry to the targetDirEntry in this task. + * + * @param {Entry} sourceEntry An entry to be moved. + * @param {DirectoryEntry} destinationEntry The entry of the destination + * directory. + * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback + * Callback invoked when an entry is changed. + * @param {function()} successCallback On success. + * @param {function(FileOperationManager.Error)} errorCallback On error. + * @private + */ +FileOperationManager.MoveTask.processEntry_ = function( + sourceEntry, destinationEntry, entryChangedCallback, successCallback, + errorCallback) { + fileOperationUtil.deduplicatePath( + destinationEntry, + sourceEntry.name, + function(destinationName) { + sourceEntry.moveTo( + destinationEntry, destinationName, + function(movedEntry) { + entryChangedCallback(util.EntryChangedKind.CREATED, movedEntry); + entryChangedCallback(util.EntryChangedKind.DELETED, sourceEntry); + successCallback(); + }, + function(error) { + errorCallback(new FileOperationManager.Error( + util.FileOperationErrorType.FILESYSTEM_ERROR, error)); + }); + }, + errorCallback); +}; + +/** + * Task to create a zip archive. + * + * @param {Array.<Entry>} sourceEntries Array of source entries. + * @param {DirectoryEntry} targetDirEntry Target directory. + * @param {DirectoryEntry} zipBaseDirEntry Base directory dealt as a root + * in ZIP archive. + * @constructor + * @extends {FileOperationManager.Task} + */ +FileOperationManager.ZipTask = function( + sourceEntries, targetDirEntry, zipBaseDirEntry) { + FileOperationManager.Task.call( + this, util.FileOperationType.ZIP, sourceEntries, targetDirEntry); + this.zipBaseDirEntry = zipBaseDirEntry; +}; + +/** + * Extends FileOperationManager.Task. + */ +FileOperationManager.ZipTask.prototype.__proto__ = + FileOperationManager.Task.prototype; + + +/** + * Initializes the ZipTask. + * @param {function()} callback Called when the initialize is completed. + */ +FileOperationManager.ZipTask.prototype.initialize = function(callback) { + var resolvedEntryMap = {}; + var group = new AsyncUtil.Group(); + for (var i = 0; i < this.sourceEntries.length; i++) { + group.add(function(index, callback) { + fileOperationUtil.resolveRecursively( + this.sourceEntries[index], + function(entries) { + for (var j = 0; j < entries.length; j++) + resolvedEntryMap[entries[j].toURL()] = entries[j]; + callback(); + }, + function(error) {}); + }.bind(this, i)); + } + + group.run(function() { + // For zip archiving, all the entries are processed at once. + this.processingEntries = [resolvedEntryMap]; + + this.totalBytes = 0; + for (var url in resolvedEntryMap) + this.totalBytes += resolvedEntryMap[url].size; + + callback(); + }.bind(this)); +}; + +/** + * Runs a zip file creation task. + * + * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback + * Callback invoked when an entry is changed. + * @param {function()} progressCallback Callback invoked periodically during + * the moving. + * @param {function()} successCallback On complete. + * @param {function(FileOperationManager.Error)} errorCallback On error. + * @override + */ +FileOperationManager.ZipTask.prototype.run = function( + entryChangedCallback, progressCallback, successCallback, errorCallback) { + // TODO(hidehiko): we should localize the name. + var destName = 'Archive'; + if (this.sourceEntries.length == 1) { + var entryPath = this.sourceEntries[0].fullPath; + var i = entryPath.lastIndexOf('/'); + var basename = (i < 0) ? entryPath : entryPath.substr(i + 1); + i = basename.lastIndexOf('.'); + destName = ((i < 0) ? basename : basename.substr(0, i)); + } + + fileOperationUtil.deduplicatePath( + this.targetDirEntry, destName + '.zip', + function(destPath) { + // TODO: per-entry zip progress update with accurate byte count. + // For now just set completedBytes to same value as totalBytes so + // that the progress bar is full. + this.processedBytes = this.totalBytes; + progressCallback(); + + // The number of elements in processingEntries is 1. See also + // initialize(). + var entries = []; + for (var url in this.processingEntries[0]) + entries.push(this.processingEntries[0][url]); + + fileOperationUtil.zipSelection( + entries, + this.zipBaseDirEntry, + destPath, + function(entry) { + entryChangedCallback(util.EntryChangedKind.CREATE, entry); + successCallback(); + }, + function(error) { + errorCallback(new FileOperationManager.Error( + util.FileOperationErrorType.FILESYSTEM_ERROR, error)); + }); + }.bind(this), + errorCallback); +}; + +/** + * Error class used to report problems with a copy operation. + * If the code is UNEXPECTED_SOURCE_FILE, data should be a path of the file. + * If the code is TARGET_EXISTS, data should be the existing Entry. + * If the code is FILESYSTEM_ERROR, data should be the FileError. + * + * @param {util.FileOperationErrorType} code Error type. + * @param {string|Entry|FileError} data Additional data. + * @constructor + */ +FileOperationManager.Error = function(code, data) { + this.code = code; + this.data = data; +}; + +// FileOperationManager methods. + +/** + * @return {Object} Status object. + */ +FileOperationManager.prototype.getStatus = function() { + // TODO(hidehiko): Reorganize the structure when delete queue is merged + // into copy task queue. + var result = { + // Set to util.FileOperationType if all the running/pending tasks is + // the same kind of task. + operationType: null, + + // The number of entries to be processed. + numRemainingItems: 0, + + // The total number of bytes to be processed. + totalBytes: 0, + + // The number of bytes. + processedBytes: 0, + + // Available if numRemainingItems == 1. Pointing to an Entry which is + // begin processed. + processingEntry: task.getSingleEntry() + }; + + var operationType = + this.copyTasks_.length > 0 ? this.copyTasks_[0].operationType : null; + var task = null; + for (var i = 0; i < this.copyTasks_.length; i++) { + task = this.copyTasks_[i]; + if (task.operationType != operationType) + operationType = null; + + // Assuming the number of entries is small enough, count every time. + result.numRemainingItems += task.countRemainingItems(); + result.totalBytes += task.totalBytes; + result.processedBytes += task.processedBytes; + } + + result.operationType = operationType; + return result; +}; + +/** + * Adds an event listener for the tasks. + * @param {string} type The name of the event. + * @param {function(Event)} handler The handler for the event. + * This is called when the event is dispatched. + */ +FileOperationManager.prototype.addEventListener = function(type, handler) { + this.eventRouter_.addEventListener(type, handler); +}; + +/** + * Removes an event listener for the tasks. + * @param {string} type The name of the event. + * @param {function(Event)} handler The handler to be removed. + */ +FileOperationManager.prototype.removeEventListener = function(type, handler) { + this.eventRouter_.removeEventListener(type, handler); +}; + +/** + * Says if there are any tasks in the queue. + * @return {boolean} True, if there are any tasks. + */ +FileOperationManager.prototype.hasQueuedTasks = function() { + return this.copyTasks_.length > 0 || this.deleteTasks_.length > 0; +}; + +/** + * Completely clear out the copy queue, either because we encountered an error + * or completed successfully. + * + * @private + */ +FileOperationManager.prototype.resetQueue_ = function() { + this.copyTasks_ = []; +}; + +/** + * Requests the specified task to be canceled. + * @param {string} taskId ID of task to be canceled. + */ +FileOperationManager.prototype.requestTaskCancel = function(taskId) { + var task = null; + for (var i = 0; i < this.copyTasks_.length; i++) { + task = this.copyTasks_[i]; + if (task.taskId !== taskId) + continue; + task.requestCancel(); + // If the task is not on progress, remove it immediately. + if (i !== 0) { + this.eventRouter_.sendProgressEvent('CANCELED', + task.getStatus(), + task.taskId); + this.copyTasks_.splice(i, 1); + } + } + for (var i = 0; i < this.deleteTasks_.length; i++) { + task = this.deleteTasks_[i]; + if (task.taskId !== taskId) + continue; + task.cancelRequested = true; + // If the task is not on progress, remove it immediately. + if (i !== 0) { + this.eventRouter_.sendDeleteEvent('CANCELED', task); + this.deleteTasks_.splice(i, 1); + } + } +}; + +/** + * Kick off pasting. + * + * @param {Array.<string>} sourcePaths Path of the source files. + * @param {string} targetPath The destination path of the target directory. + * @param {boolean} isMove True if the operation is "move", otherwise (i.e. + * if the operation is "copy") false. + */ +FileOperationManager.prototype.paste = function( + sourcePaths, targetPath, isMove) { + // Do nothing if sourcePaths is empty. + if (sourcePaths.length == 0) + return; + + var errorCallback = function(error) { + this.eventRouter_.sendProgressEvent( + 'ERROR', + this.getStatus(), + this.generateTaskId_(null), + new FileOperationManager.Error( + util.FileOperationErrorType.FILESYSTEM_ERROR, error)); + }.bind(this); + + var targetEntry = null; + var entries = []; + + // Resolve paths to entries. + var resolveGroup = new AsyncUtil.Group(); + resolveGroup.add(function(callback) { + webkitResolveLocalFileSystemURL( + util.makeFilesystemUrl(targetPath), + function(entry) { + if (!entry.isDirectory) { + // Found a non directory entry. + errorCallback(util.createFileError(FileError.TYPE_MISMATCH_ERR)); + return; + } + + targetEntry = entry; + callback(); + }, + errorCallback); + }); + + for (var i = 0; i < sourcePaths.length; i++) { + resolveGroup.add(function(sourcePath, callback) { + webkitResolveLocalFileSystemURL( + util.makeFilesystemUrl(sourcePath), + function(entry) { + entries.push(entry); + callback(); + }, + errorCallback); + }.bind(this, sourcePaths[i])); + } + + resolveGroup.run(function() { + if (isMove) { + // Moving to the same directory is a redundant operation. + entries = entries.filter(function(entry) { + return targetEntry.fullPath + '/' + entry.name != entry.fullPath; + }); + + // Do nothing, if we have no entries to be moved. + if (entries.length == 0) + return; + } + + this.queueCopy_(targetEntry, entries, isMove); + }.bind(this)); +}; + +/** + * Checks if the move operation is available between the given two locations. + * + * @param {DirectoryEntry} sourceEntry An entry from the source. + * @param {DirectoryEntry} targetDirEntry Directory entry for the target. + * @return {boolean} Whether we can move from the source to the target. + */ +FileOperationManager.prototype.isMovable = function(sourceEntry, + targetDirEntry) { + return (PathUtil.isDriveBasedPath(sourceEntry.fullPath) && + PathUtil.isDriveBasedPath(targetDirEntry.fullPath)) || + (PathUtil.getRootPath(sourceEntry.fullPath) == + PathUtil.getRootPath(targetDirEntry.fullPath)); +}; + +/** + * Initiate a file copy. + * + * @param {DirectoryEntry} targetDirEntry Target directory. + * @param {Array.<Entry>} entries Entries to copy. + * @param {boolean} isMove In case of move. + * @return {FileOperationManager.Task} Copy task. + * @private + */ +FileOperationManager.prototype.queueCopy_ = function( + targetDirEntry, entries, isMove) { + // When copying files, null can be specified as source directory. + var task; + if (isMove) { + if (this.isMovable(entries[0], targetDirEntry)) { + task = new FileOperationManager.MoveTask(entries, targetDirEntry); + } else { + task = new FileOperationManager.CopyTask(entries, targetDirEntry); + task.deleteAfterCopy = true; + } + } else { + task = new FileOperationManager.CopyTask(entries, targetDirEntry); + } + + task.taskId = this.generateTaskId_(); + task.initialize(function() { + this.copyTasks_.push(task); + this.eventRouter_.sendProgressEvent('BEGIN', task.getStatus(), task.taskId); + if (this.copyTasks_.length == 1) + this.serviceAllTasks_(); + }.bind(this)); + + return task; +}; + +/** + * Service all pending tasks, as well as any that might appear during the + * copy. + * + * @private + */ +FileOperationManager.prototype.serviceAllTasks_ = function() { + if (!this.copyTasks_.length) { + // All tasks have been serviced, clean up and exit. + chrome.power.releaseKeepAwake(); + this.resetQueue_(); + return; + } + + // Prevent the system from sleeping while copy is in progress. + chrome.power.requestKeepAwake('system'); + + var onTaskProgress = function() { + this.eventRouter_.sendProgressEvent('PROGRESS', + this.copyTasks_[0].getStatus(), + this.copyTasks_[0].taskId); + }.bind(this); + + var onEntryChanged = function(kind, entry) { + this.eventRouter_.sendEntryChangedEvent(kind, entry); + }.bind(this); + + var onTaskError = function(err) { + var task = this.copyTasks_.shift(); + var reason = err.data.code === FileError.ABORT_ERR ? 'CANCELED' : 'ERROR'; + this.eventRouter_.sendProgressEvent(reason, + task.getStatus(), + task.taskId, + err); + this.serviceAllTasks_(); + }.bind(this); + + var onTaskSuccess = function() { + // The task at the front of the queue is completed. Pop it from the queue. + var task = this.copyTasks_.shift(); + this.eventRouter_.sendProgressEvent('SUCCESS', + task.getStatus(), + task.taskId); + this.serviceAllTasks_(); + }.bind(this); + + var nextTask = this.copyTasks_[0]; + this.eventRouter_.sendProgressEvent('PROGRESS', + nextTask.getStatus(), + nextTask.taskId); + nextTask.run(onEntryChanged, onTaskProgress, onTaskSuccess, onTaskError); +}; + +/** + * Timeout before files are really deleted (to allow undo). + */ +FileOperationManager.DELETE_TIMEOUT = 30 * 1000; + +/** + * Schedules the files deletion. + * + * @param {Array.<Entry>} entries The entries. + */ +FileOperationManager.prototype.deleteEntries = function(entries) { + // TODO(hirono): Make FileOperationManager.DeleteTask. + var task = Object.seal({ + entries: entries, + taskId: this.generateTaskId_(), + entrySize: {}, + totalBytes: 0, + processedBytes: 0, + cancelRequested: false + }); + + // Obtains entry size and sum them up. + var group = new AsyncUtil.Group(); + for (var i = 0; i < task.entries.length; i++) { + group.add(function(entry, callback) { + entry.getMetadata(function(metadata) { + var index = task.entries.indexOf(entries); + task.entrySize[entry.toURL()] = metadata.size; + task.totalBytes += metadata.size; + callback(); + }, function() { + // Fail to obtain the metadata. Use fake value 1. + task.entrySize[entry.toURL()] = 1; + task.totalBytes += 1; + callback(); + }); + }.bind(this, task.entries[i])); + } + + // Add a delete task. + group.run(function() { + this.deleteTasks_.push(task); + this.eventRouter_.sendDeleteEvent('BEGIN', task); + if (this.deleteTasks_.length === 1) + this.serviceAllDeleteTasks_(); + }.bind(this)); +}; + +/** + * Service all pending delete tasks, as well as any that might appear during the + * deletion. + * + * Must not be called if there is an in-flight delete task. + * + * @private + */ +FileOperationManager.prototype.serviceAllDeleteTasks_ = function() { + this.serviceDeleteTask_( + this.deleteTasks_[0], + function() { + this.deleteTasks_.shift(); + if (this.deleteTasks_.length) + this.serviceAllDeleteTasks_(); + }.bind(this)); +}; + +/** + * Performs the deletion. + * + * @param {Object} task The delete task (see deleteEntries function). + * @param {function()} callback Callback run on task end. + * @private + */ +FileOperationManager.prototype.serviceDeleteTask_ = function(task, callback) { + var queue = new AsyncUtil.Queue(); + + // Delete each entry. + var error = null; + var deleteOneEntry = function(inCallback) { + if (!task.entries.length || task.cancelRequested || error) { + inCallback(); + return; + } + this.eventRouter_.sendDeleteEvent('PROGRESS', task); + util.removeFileOrDirectory( + task.entries[0], + function() { + this.eventRouter_.sendEntryChangedEvent( + util.EntryChangedKind.DELETED, task.entries[0]); + task.processedBytes += task.entrySize[task.entries[0].toURL()]; + task.entries.shift(); + deleteOneEntry(inCallback); + }.bind(this), + function(inError) { + error = inError; + inCallback(); + }.bind(this)); + }.bind(this); + queue.run(deleteOneEntry); + + // Send an event and finish the async steps. + queue.run(function(inCallback) { + var reason; + if (error) + reason = 'ERROR'; + else if (task.cancelRequested) + reason = 'CANCELED'; + else + reason = 'SUCCESS'; + this.eventRouter_.sendDeleteEvent(reason, task); + inCallback(); + callback(); + }.bind(this)); +}; + +/** + * Creates a zip file for the selection of files. + * + * @param {Entry} dirEntry The directory containing the selection. + * @param {Array.<Entry>} selectionEntries The selected entries. + */ +FileOperationManager.prototype.zipSelection = function( + dirEntry, selectionEntries) { + var zipTask = new FileOperationManager.ZipTask( + selectionEntries, dirEntry, dirEntry); + zipTask.taskId = this.generateTaskId_(this.copyTasks_); + zipTask.zip = true; + zipTask.initialize(function() { + this.copyTasks_.push(zipTask); + this.eventRouter_.sendProgressEvent('BEGIN', + zipTask.getStatus(), + zipTask.taskId); + if (this.copyTasks_.length == 1) + this.serviceAllTasks_(); + }.bind(this)); +}; + +/** + * Generates new task ID. + * + * @return {string} New task ID. + * @private + */ +FileOperationManager.prototype.generateTaskId_ = function() { + return 'file-operation-' + this.taskIdCounter_++; +}; diff --git a/chromium/chrome/browser/resources/file_manager/background/js/progress_center.js b/chromium/chrome/browser/resources/file_manager/background/js/progress_center.js new file mode 100644 index 00000000000..b7d20cec316 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/background/js/progress_center.js @@ -0,0 +1,452 @@ +// Copyright 2013 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'; + +/** + * Progress center at the background page. + * @constructor + */ +var ProgressCenter = function() { + /** + * Current items managed by the progress center. + * @type {Array.<ProgressItem>} + * @private + */ + this.items_ = []; + + /** + * Map of progress ID and notification ID. + * @type {Object.<string, string>} + * @private + */ + this.notifications_ = new ProgressCenter.Notifications_( + this.requestCancel.bind(this)); + + /** + * List of panel UI managed by the progress center. + * @type {Array.<ProgressCenterPanel>} + * @private + */ + this.panels_ = []; + + /** + * Timeout callback to remove items. + * @type {ProgressCenter.TimeoutManager_} + * @private + */ + this.resetTimeout_ = new ProgressCenter.TimeoutManager_( + this.reset_.bind(this)); + + Object.seal(this); +}; + +/** + * The default amount of milliseconds time, before a progress item will reset + * after the last complete. + * @type {number} + * @private + * @const + */ +ProgressCenter.RESET_DELAY_TIME_MS_ = 5000; + +/** + * Notifications created by progress center. + * @param {function(string)} cancelCallback Callback to notify the progress + * center of cancel operation. + * @constructor + * @private + */ +ProgressCenter.Notifications_ = function(cancelCallback) { + /** + * ID set of notifications that is progressing now. + * @type {Object.<string, ProgressCenter.Notifications_.NotificationState_>} + * @private + */ + this.ids_ = {}; + + /** + * Async queue. + * @type {AsyncUtil.Queue} + * @private + */ + this.queue_ = new AsyncUtil.Queue(); + + /** + * Callback to notify the progress center of cancel operation. + * @type {function(string)} + * @private + */ + this.cancelCallback_ = cancelCallback; + + chrome.notifications.onButtonClicked.addListener( + this.onButtonClicked_.bind(this)); + chrome.notifications.onClosed.addListener(this.onClosed_.bind(this)); + + Object.seal(this); +}; + +/** + * State of notification. + * @enum {string} + * @const + * @private + */ +ProgressCenter.Notifications_.NotificationState_ = Object.freeze({ + VISIBLE: 'visible', + DISMISSED: 'dismissed' +}); + +/** + * Updates the notification according to the item. + * @param {ProgressCenterItem} item Item to contain new information. + * @param {boolean} newItemAcceptable Whether to accept new item or not. + */ +ProgressCenter.Notifications_.prototype.updateItem = function( + item, newItemAcceptable) { + var NotificationState = ProgressCenter.Notifications_.NotificationState_; + var newlyAdded = !(item.id in this.ids_); + + // If new item is not acceptable, just return. + if (newlyAdded && !newItemAcceptable) + return; + + // Update the ID map and return if we does not show a notification for the + // item. + if (item.state === ProgressItemState.PROGRESSING) { + if (newlyAdded) + this.ids_[item.id] = NotificationState.VISIBLE; + else if (this.ids_[item.id] === NotificationState.DISMISSED) + return; + } else { + // This notification is no longer tracked. + var previousState = this.ids_[item.id]; + delete this.ids_[item.id]; + // Clear notifications for complete or canceled items. + if (item.state === ProgressItemState.CANCELED || + item.state === ProgressItemState.COMPLETED) { + if (previousState === NotificationState.VISIBLE) { + this.queue_.run(function(proceed) { + chrome.notifications.clear(item.id, proceed); + }); + } + return; + } + } + + // Create/update the notification with the item. + this.queue_.run(function(proceed) { + var params = { + title: chrome.runtime.getManifest().name, + iconUrl: chrome.runtime.getURL('/common/images/icon96.png'), + type: item.state === ProgressItemState.PROGRESSING ? 'progress' : 'basic', + message: item.message, + buttons: item.cancelable ? [{title: str('CANCEL_LABEL')}] : undefined, + progress: item.state === ProgressItemState.PROGRESSING ? + item.progressRateByPercent : undefined + }; + if (newlyAdded) + chrome.notifications.create(item.id, params, proceed); + else + chrome.notifications.update(item.id, params, proceed); + }.bind(this)); +}; + +/** + * Handles cancel button click. + * @param {string} id Item ID. + * @private + */ +ProgressCenter.Notifications_.prototype.onButtonClicked_ = function(id) { + if (id in this.ids_) + this.cancelCallback_(id); +}; + +/** + * Handles notification close. + * @param {string} id Item ID. + * @private + */ +ProgressCenter.Notifications_.prototype.onClosed_ = function(id) { + if (id in this.ids_) + this.ids_[id] = ProgressCenter.Notifications_.NotificationState_.DISMISSED; +}; + +/** + * Utility for timeout callback. + * + * @param {function(*):*} callback Callback function. + * @constructor + * @private + */ +ProgressCenter.TimeoutManager_ = function(callback) { + this.callback_ = callback; + this.id_ = null; + Object.seal(this); +}; + +/** + * Requests timeout. Previous request is canceled. + * @param {number} milliseconds Time to invoke the callback function. + */ +ProgressCenter.TimeoutManager_.prototype.request = function(milliseconds) { + if (this.id_) + clearTimeout(this.id_); + this.id_ = setTimeout(function() { + this.id_ = null; + this.callback_(); + }.bind(this), milliseconds); +}; + +/** + * Cancels the timeout and invoke the callback function synchronously. + */ +ProgressCenter.TimeoutManager_.prototype.callImmediately = function() { + if (this.id_) + clearTimeout(this.id_); + this.id_ = null; + this.callback_(); +}; + +/** + * Updates the item in the progress center. + * If the item has a new ID, the item is added to the item list. + * + * @param {ProgressCenterItem} item Updated item. + */ +ProgressCenter.prototype.updateItem = function(item) { + // Update item. + var index = this.getItemIndex_(item.id); + if (index === -1) + this.items_.push(item); + else + this.items_[index] = item; + + // Update panels. + var summarizedItem = this.getSummarizedItem_(); + for (var i = 0; i < this.panels_.length; i++) { + this.panels_[i].updateItem(item); + this.panels_[i].updateCloseView(summarizedItem); + } + + // Update notifications. + this.notifications_.updateItem(item, !this.panels_.length); + + // Reset if there is no item. + for (var i = 0; i < this.items_.length; i++) { + switch (this.items_[i].state) { + case ProgressItemState.PROGRESSING: + return; + case ProgressItemState.ERROR: + this.resetTimeout_.request(ProgressCenter.RESET_DELAY_TIME_MS_); + return; + } + } + + // Cancel timeout and call reset function immediately. + this.resetTimeout_.callImmediately(); +}; + +/** + * Requests to cancel the progress item. + * @param {string} id Progress ID to be requested to cancel. + */ +ProgressCenter.prototype.requestCancel = function(id) { + var item = this.getItemById(id); + if (item && item.cancelCallback) + item.cancelCallback(); +}; + +/** + * Adds a panel UI to the notification center. + * @param {ProgressCenterPanel} panel Panel UI. + */ +ProgressCenter.prototype.addPanel = function(panel) { + if (this.panels_.indexOf(panel) !== -1) + return; + + // Update the panel list. + this.panels_.push(panel); + + // Set the current items. + for (var i = 0; i < this.items_.length; i++) + panel.updateItem(this.items_[i]); + var summarizedItem = this.getSummarizedItem_(); + if (summarizedItem) + panel.updateCloseView(summarizedItem); + + // Register the cancel callback. + panel.cancelCallback = this.requestCancel.bind(this); +}; + +/** + * Removes a panel UI from the notification center. + * @param {ProgressCenterPanel} panel Panel UI. + */ +ProgressCenter.prototype.removePanel = function(panel) { + var index = this.panels_.indexOf(panel); + if (index === -1) + return; + + this.panels_.splice(index, 1); + panel.cancelCallback = null; + + // If there is no panel, show the notifications. + if (this.panels_.length) + return; + for (var i = 0; i < this.items_.length; i++) + this.notifications_.updateItem(this.items_[i], true); +}; + +/** + * Obtains the summarized item to be displayed in the closed progress center + * panel. + * @return {ProgressCenterItem} Summarized item. Returns null if there is no + * item. + * @private + */ +ProgressCenter.prototype.getSummarizedItem_ = function() { + var summarizedItem = new ProgressCenterItem(); + var progressingItems = []; + var completedItems = []; + var canceledItems = []; + var errorItems = []; + + for (var i = 0; i < this.items_.length; i++) { + // Count states. + switch (this.items_[i].state) { + case ProgressItemState.PROGRESSING: + progressingItems.push(this.items_[i]); + break; + case ProgressItemState.COMPLETED: + completedItems.push(this.items_[i]); + break; + case ProgressItemState.CANCELED: + canceledItems.push(this.items_[i]); + break; + case ProgressItemState.ERROR: + errorItems.push(this.items_[i]); + break; + } + + // If all of the progressing items have the same type, then use + // it. Otherwise use TRANSFER, since it is the most generic. + if (this.items_[i].state === ProgressItemState.PROGRESSING) { + if (summarizedItem.type === null) + summarizedItem.type = this.items_[i].type; + else if (summarizedItem.type !== this.items_[i].type) + summarizedItem.type = ProgressItemType.TRANSFER; + } + + // Sum up the progress values. + if (this.items_[i].state === ProgressItemState.PROGRESSING || + this.items_[i].state === ProgressItemState.COMPLETED) { + summarizedItem.progressMax += this.items_[i].progressMax; + summarizedItem.progressValue += this.items_[i].progressValue; + } + } + + // If there are multiple visible (progressing and error) items, show the + // summarized message. + if (progressingItems.length + errorItems.length > 1) { + // Set item message. + var messages = []; + if (progressingItems.length > 0) { + switch (summarizedItem.type) { + case ProgressItemType.COPY: + messages.push(str('COPY_PROGRESS_SUMMARY')); + break; + case ProgressItemType.MOVE: + messages.push(str('MOVE_PROGRESS_SUMMARY')); + break; + case ProgressItemType.DELETE: + messages.push(str('DELETE_PROGRESS_SUMMARY')); + break; + case ProgressItemType.ZIP: + messages.push(str('ZIP_PROGRESS_SUMMARY')); + break; + case ProgressItemType.TRANSFER: + messages.push(str('TRANSFER_PROGRESS_SUMMARY')); + break; + } + } + if (errorItems.length === 1) + messages.push(str('ERROR_PROGRESS_SUMMARY')); + else if (errorItems.length > 1) + messages.push(strf('ERROR_PROGRESS_SUMMARY_PLURAL', errorItems.length)); + + summarizedItem.summarized = true; + summarizedItem.message = messages.join(' '); + summarizedItem.state = progressingItems.length > 0 ? + ProgressItemState.PROGRESSING : ProgressItemState.ERROR; + return summarizedItem; + } + + // If there is 1 visible item, show the item message. + if (progressingItems.length + errorItems.length === 1) { + var visibleItem = progressingItems[0] || errorItems[0]; + summarizedItem.id = visibleItem.id; + summarizedItem.cancelCallback = visibleItem.cancelCallback; + summarizedItem.type = visibleItem.type; + summarizedItem.message = visibleItem.message; + summarizedItem.state = visibleItem.state; + return summarizedItem; + } + + // If there is no visible item, the message can be empty. + if (completedItems.length > 0) { + summarizedItem.state = ProgressItemState.COMPLETED; + return summarizedItem; + } + if (canceledItems.length > 0) { + summarizedItem.state = ProgressItemState.CANCELED; + return summarizedItem; + } + + // If there is no item, return null. + return null; +}; + +/** + * Obtains item by ID. + * @param {string} id ID of progress item. + * @return {ProgressCenterItem} Progress center item having the specified + * ID. Null if the item is not found. + */ +ProgressCenter.prototype.getItemById = function(id) { + return this.items_[this.getItemIndex_(id)]; +}; + +/** + * Obtains item index that have the specifying ID. + * @param {string} id Item ID. + * @return {number} Item index. Returns -1 If the item is not found. + * @private + */ +ProgressCenter.prototype.getItemIndex_ = function(id) { + for (var i = 0; i < this.items_.length; i++) { + if (this.items_[i].id === id) + return i; + } + return -1; +}; + +/** + * Hides the progress center if there is no progressing items. + * @private + */ +ProgressCenter.prototype.reset_ = function() { + // If we have a progressing item, stop reset. + for (var i = 0; i < this.items_.length; i++) { + if (this.items_[i].state == ProgressItemState.PROGRESSING) + return; + } + + // Reset items. + this.items_.splice(0, this.items_.length); + + // Dispatch a event. + for (var i = 0; i < this.panels_.length; i++) + this.panels_[i].reset(); +}; diff --git a/chromium/chrome/browser/resources/file_manager/background/js/test_util.js b/chromium/chrome/browser/resources/file_manager/background/js/test_util.js new file mode 100644 index 00000000000..3a3bd78857e --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/background/js/test_util.js @@ -0,0 +1,868 @@ +// Copyright (c) 2013 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. + +/** + * Namespace for test related things. + */ +var test = test || {}; + +/** + * Namespace for test utility functions. + * + * Public functions in the test.util.sync and the test.util.async namespaces are + * published to test cases and can be called by using callRemoteTestUtil. The + * arguments are serialized as JSON internally. If application ID is passed to + * callRemoteTestUtil, the content window of the application is added as the + * first argument. The functions in the test.util.async namespace are passed the + * callback function as the last argument. + */ +test.util = {}; + +/** + * Namespace for synchronous utility functions. + */ +test.util.sync = {}; + +/** + * Namespace for asynchronous utility functions. + */ +test.util.async = {}; + +/** + * Extension ID of the testing extension. + * @type {string} + * @const + */ +test.util.TESTING_EXTENSION_ID = 'oobinhbdbiehknkpbpejbbpdbkdjmoco'; + +/** + * Interval of checking a condition in milliseconds. + * @type {number} + * @const + * @private + */ +test.util.WAITTING_INTERVAL_ = 50; + +/** + * Repeats the function until it returns true. + * @param {function()} closure Function expected to return true. + * @private + */ +test.util.repeatUntilTrue_ = function(closure) { + var step = function() { + if (closure()) + return; + setTimeout(step, test.util.WAITTING_INTERVAL_); + }; + step(); +}; + +/** + * Opens the main Files.app's window and waits until it is ready. + * + * @param {Object} appState App state. + * @param {function(string)} callback Completion callback with the new window's + * App ID. + */ +test.util.async.openMainWindow = function(appState, callback) { + var steps = [ + function() { + launchFileManager(appState, + undefined, // opt_type + undefined, // opt_id + steps.shift()); + }, + function(appId) { + test.util.repeatUntilTrue_(function() { + if (!background.appWindows[appId]) + return false; + var contentWindow = background.appWindows[appId].contentWindow; + var table = contentWindow.document.querySelector('#detail-table'); + if (!table) + return false; + callback(appId); + return true; + }); + } + ]; + steps.shift()(); +}; + +/** + * Waits for a window with the specified App ID prefix. Eg. `files` will match + * windows such as files#0, files#1, etc. + * + * @param {string} appIdPrefix ID prefix of the requested window. + * @param {function(string)} callback Completion callback with the new window's + * App ID. + */ +test.util.async.waitForWindow = function(appIdPrefix, callback) { + test.util.repeatUntilTrue_(function() { + for (var appId in background.appWindows) { + if (appId.indexOf(appIdPrefix) == 0 && + background.appWindows[appId].contentWindow) { + callback(appId); + return true; + } + } + return false; + }); +}; + +/** + * Gets a document in the Files.app's window, including iframes. + * + * @param {Window} contentWindow Window to be used. + * @param {string=} opt_iframeQuery Query for the iframe. + * @return {Document=} Returns the found document or undefined if not found. + * @private + */ +test.util.sync.getDocument_ = function(contentWindow, opt_iframeQuery) { + if (opt_iframeQuery) { + var iframe = contentWindow.document.querySelector(opt_iframeQuery); + return iframe && iframe.contentWindow && iframe.contentWindow.document; + } + + return contentWindow.document; +}; + +/** + * Gets total Javascript error count from each app window. + * @return {number} Error count. + */ +test.util.sync.getErrorCount = function() { + var totalCount = JSErrorCount; + for (var appId in background.appWindows) { + var contentWindow = background.appWindows[appId].contentWindow; + if (contentWindow.JSErrorCount) + totalCount += contentWindow.JSErrorCount; + } + return totalCount; +}; + +/** + * Resizes the window to the specified dimensions. + * + * @param {Window} contentWindow Window to be tested. + * @param {number} width Window width. + * @param {number} height Window height. + * @return {boolean} True for success. + */ +test.util.sync.resizeWindow = function(contentWindow, width, height) { + background.appWindows[contentWindow.appID].resizeTo(width, height); + return true; +}; + +/** + * Returns an array with the files currently selected in the file manager. + * + * @param {Window} contentWindow Window to be tested. + * @return {Array.<string>} Array of selected files. + */ +test.util.sync.getSelectedFiles = function(contentWindow) { + var table = contentWindow.document.querySelector('#detail-table'); + var rows = table.querySelectorAll('li'); + var selected = []; + for (var i = 0; i < rows.length; ++i) { + if (rows[i].hasAttribute('selected')) { + selected.push( + rows[i].querySelector('.filename-label').textContent); + } + } + return selected; +}; + +/** + * Returns an array with the files on the file manager's file list. + * + * @param {Window} contentWindow Window to be tested. + * @return {Array.<Array.<string>>} Array of rows. + */ +test.util.sync.getFileList = function(contentWindow) { + var table = contentWindow.document.querySelector('#detail-table'); + var rows = table.querySelectorAll('li'); + var fileList = []; + for (var j = 0; j < rows.length; ++j) { + var row = rows[j]; + fileList.push([ + row.querySelector('.filename-label').textContent, + row.querySelector('.size').textContent, + row.querySelector('.type').textContent, + row.querySelector('.date').textContent + ]); + } + return fileList; +}; + +/** + * Checkes if the given label and path of the volume are selected. + * @param {Window} contentWindow Window to be tested. + * @param {string} label Correct label the selected volume should have. + * @param {string} path Correct path the selected volume should have. + * @return {boolean} True for success. + */ +test.util.sync.checkSelectedVolume = function(contentWindow, label, path) { + var list = contentWindow.document.querySelector('#navigation-list'); + var rows = list.querySelectorAll('li'); + var selected = []; + for (var i = 0; i < rows.length; ++i) { + if (rows[i].hasAttribute('selected')) + selected.push(rows[i]); + } + // Selected item must be one. + if (selected.length !== 1) + return false; + + if (selected[0].modelItem.path !== path || + selected[0].querySelector('.root-label').textContent !== label) { + return false; + } + + return true; +}; + +/** + * Waits until the window is set to the specified dimensions. + * + * @param {Window} contentWindow Window to be tested. + * @param {number} width Requested width. + * @param {number} height Requested height. + * @param {function(Object)} callback Success callback with the dimensions. + */ +test.util.async.waitForWindowGeometry = function( + contentWindow, width, height, callback) { + test.util.repeatUntilTrue_(function() { + if (contentWindow.innerWidth == width && + contentWindow.innerHeight == height) { + callback({width: width, height: height}); + return true; + } + return false; + }); +}; + +/** + * Waits for an element and returns it as an array of it's attributes. + * + * @param {Window} contentWindow Window to be tested. + * @param {string} targetQuery Query to specify the element. + * @param {?string} iframeQuery Iframe selector or null if no iframe. + * @param {boolean=} opt_inverse True if the function should return if the + * element disappears, instead of appearing. + * @param {function(Object)} callback Callback with a hash array of attributes + * and contents as text. + */ +test.util.async.waitForElement = function( + contentWindow, targetQuery, iframeQuery, opt_inverse, callback) { + test.util.repeatUntilTrue_(function() { + var doc = test.util.sync.getDocument_(contentWindow, iframeQuery); + if (!doc) + return false; + var element = doc.querySelector(targetQuery); + if (!element) + return !!opt_inverse; + var attributes = {}; + for (var i = 0; i < element.attributes.length; i++) { + attributes[element.attributes[i].nodeName] = + element.attributes[i].nodeValue; + } + var text = element.textContent; + callback({attributes: attributes, text: text}); + return !opt_inverse; + }); +}; + +/** + * Calls getFileList until the number of displayed files is different from + * lengthBefore. + * + * @param {Window} contentWindow Window to be tested. + * @param {number} lengthBefore Number of items visible before. + * @param {function(Array.<Array.<string>>)} callback Change callback. + */ +test.util.async.waitForFileListChange = function( + contentWindow, lengthBefore, callback) { + test.util.repeatUntilTrue_(function() { + var files = test.util.sync.getFileList(contentWindow); + files.sort(); + var notReadyRows = files.filter(function(row) { + return row.filter(function(cell) { return cell == '...'; }).length; + }); + if (notReadyRows.length === 0 && + files.length !== lengthBefore && + files.length !== 0) { + callback(files); + return true; + } else { + return false; + } + }); +}; + +/** + * Returns an array of items on the file manager's autocomplete list. + * + * @param {Window} contentWindow Window to be tested. + * @return {Array.<string>} Array of items. + */ +test.util.sync.getAutocompleteList = function(contentWindow) { + var list = contentWindow.document.querySelector('#autocomplete-list'); + var lines = list.querySelectorAll('li'); + var items = []; + for (var j = 0; j < lines.length; ++j) { + var line = lines[j]; + items.push(line.innerText); + } + return items; +}; + +/** + * Performs autocomplete with the given query and waits until at least + * |numExpectedItems| items are shown, including the first item which + * always looks like "'<query>' - search Drive". + * + * @param {Window} contentWindow Window to be tested. + * @param {string} query Query used for autocomplete. + * @param {number} numExpectedItems number of items to be shown. + * @param {function(Array.<string>)} callback Change callback. + */ +test.util.async.performAutocompleteAndWait = function( + contentWindow, query, numExpectedItems, callback) { + // Dispatch a 'focus' event to the search box so that the autocomplete list + // is attached to the search box. Note that calling searchBox.focus() won't + // dispatch a 'focus' event. + var searchBox = contentWindow.document.querySelector('#search-box input'); + var focusEvent = contentWindow.document.createEvent('Event'); + focusEvent.initEvent('focus', true /* bubbles */, true /* cancelable */); + searchBox.dispatchEvent(focusEvent); + + // Change the value of the search box and dispatch an 'input' event so that + // the autocomplete query is processed. + searchBox.value = query; + var inputEvent = contentWindow.document.createEvent('Event'); + inputEvent.initEvent('input', true /* bubbles */, true /* cancelable */); + searchBox.dispatchEvent(inputEvent); + + test.util.repeatUntilTrue_(function() { + var items = test.util.sync.getAutocompleteList(contentWindow); + if (items.length >= numExpectedItems) { + callback(items); + return true; + } else { + return false; + } + }); +}; + +/** + * Waits until a dialog with an OK button is shown and accepts it. + * + * @param {Window} contentWindow Window to be tested. + * @param {function()} callback Success callback. + */ +test.util.async.waitAndAcceptDialog = function(contentWindow, callback) { + test.util.repeatUntilTrue_(function() { + var button = contentWindow.document.querySelector('.cr-dialog-ok'); + if (!button) + return false; + button.click(); + // Wait until the dialog is removed from the DOM. + test.util.repeatUntilTrue_(function() { + if (contentWindow.document.querySelector('.cr-dialog-container')) + return false; + callback(); + return true; + }); + return true; + }); +}; + +/** + * Fakes pressing the down arrow until the given |filename| is selected. + * + * @param {Window} contentWindow Window to be tested. + * @param {string} filename Name of the file to be selected. + * @return {boolean} True if file got selected, false otherwise. + */ +test.util.sync.selectFile = function(contentWindow, filename) { + var table = contentWindow.document.querySelector('#detail-table'); + var rows = table.querySelectorAll('li'); + for (var index = 0; index < rows.length; ++index) { + test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'Down', false); + var selection = test.util.sync.getSelectedFiles(contentWindow); + if (selection.length === 1 && selection[0] === filename) + return true; + } + console.error('Failed to select file "' + filename + '"'); + return false; +}; + +/** + * Open the file by selectFile and fakeMouseDoubleClick. + * + * @param {Window} contentWindow Window to be tested. + * @param {string} filename Name of the file to be opened. + * @return {boolean} True if file got selected and a double click message is + * sent, false otherwise. + */ +test.util.sync.openFile = function(contentWindow, filename) { + var query = '#file-list li.table-row[selected] .filename-label span'; + return test.util.sync.selectFile(contentWindow, filename) && + test.util.sync.fakeMouseDoubleClick(contentWindow, query); +}; + +/** + * Selects a volume specified by its icon name + * + * @param {Window} contentWindow Window to be tested. + * @param {string} iconName Name of the volume icon. + * @param {function(boolean)} callback Callback function to notify the caller + * whether the target is found and mousedown and click events are sent. + */ +test.util.async.selectVolume = function(contentWindow, iconName, callback) { + var query = '[volume-type-icon=' + iconName + ']'; + var driveQuery = '[volume-type-icon=drive]'; + var isDriveSubVolume = iconName == 'drive_recent' || + iconName == 'drive_shared_with_me' || + iconName == 'drive_offline'; + var preSelection = false; + var steps = { + checkQuery: function() { + if (contentWindow.document.querySelector(query)) { + steps.sendEvents(); + return; + } + // If the target volume is sub-volume of drive, we must click 'drive' + // before clicking the sub-item. + if (!preSelection) { + if (!isDriveSubVolume) { + callback(false); + return; + } + if (!(test.util.sync.fakeMouseDown(contentWindow, driveQuery) && + test.util.sync.fakeMouseClick(contentWindow, driveQuery))) { + callback(false); + return; + } + preSelection = true; + } + setTimeout(steps.checkQuery, 50); + }, + sendEvents: function() { + // To change the selected volume, we have to send both events 'mousedown' + // and 'click' to the navigation list. + callback(test.util.sync.fakeMouseDown(contentWindow, query) && + test.util.sync.fakeMouseClick(contentWindow, query)); + } + }; + steps.checkQuery(); +}; + +/** + * Waits the contents of file list becomes to equal to expected contents. + * + * @param {Window} contentWindow Window to be tested. + * @param {Array.<Array.<string>>} expected Expected contents of file list. + * @param {{orderCheck:boolean=, ignoreLastModifiedTime:boolean=}=} opt_options + * Options of the comparison. If orderCheck is true, it also compares the + * order of files. If ignoreLastModifiedTime is true, it compares the file + * without its last modified time. + * @param {function()} callback Callback function to notify the caller that + * expected files turned up. + */ +test.util.async.waitForFiles = function( + contentWindow, expected, opt_options, callback) { + var options = opt_options || {}; + test.util.repeatUntilTrue_(function() { + var files = test.util.sync.getFileList(contentWindow); + if (!options.orderCheck) { + files.sort(); + expected.sort(); + } + if (options.ignoreLastModifiedTime) { + for (var i = 0; i < Math.min(files.length, expected.length); i++) { + files[i][3] = ''; + expected[i][3] = ''; + } + } + if (chrome.test.checkDeepEq(expected, files)) { + callback(true); + return true; + } + return false; + }); +}; + +/** + * Executes Javascript code on a webview and returns the result. + * + * @param {Window} contentWindow Window to be tested. + * @param {string} webViewQuery Selector for the web view. + * @param {string} code Javascript code to be executed within the web view. + * @param {function(*)} callback Callback function with results returned by the + * script. + */ +test.util.async.executeScriptInWebView = function( + contentWindow, webViewQuery, code, callback) { + var webView = contentWindow.document.querySelector(webViewQuery); + webView.executeScript({code: code}, callback); +}; + +/** + * Sends an event to the element specified by |targetQuery|. + * + * @param {Window} contentWindow Window to be tested. + * @param {string} targetQuery Query to specify the element. + * @param {Event} event Event to be sent. + * @param {string=} opt_iframeQuery Optional iframe selector. + * @return {boolean} True if the event is sent to the target, false otherwise. + */ +test.util.sync.sendEvent = function( + contentWindow, targetQuery, event, opt_iframeQuery) { + var doc = test.util.sync.getDocument_(contentWindow, opt_iframeQuery); + if (doc) { + var target = doc.querySelector(targetQuery); + if (target) { + target.dispatchEvent(event); + return true; + } + } + console.error('Target element for ' + targetQuery + ' not found.'); + return false; +}; + +/** + * Sends an fake event having the specified type to the target query. + * + * @param {Window} contentWindow Window to be tested. + * @param {string} targetQuery Query to specify the element. + * @param {string} event Type of event. + * @return {boolean} True if the event is sent to the target, false otherwise. + */ +test.util.sync.fakeEvent = function(contentWindow, targetQuery, event) { + return test.util.sync.sendEvent( + contentWindow, targetQuery, new Event(event)); +}; + +/** + * Sends a fake key event to the element specified by |targetQuery| with the + * given |keyIdentifier| and optional |ctrl| modifier to the file manager. + * + * @param {Window} contentWindow Window to be tested. + * @param {string} targetQuery Query to specify the element. + * @param {string} keyIdentifier Identifier of the emulated key. + * @param {boolean} ctrl Whether CTRL should be pressed, or not. + * @param {string=} opt_iframeQuery Optional iframe selector. + * @return {boolean} True if the event is sent to the target, false otherwise. + */ +test.util.sync.fakeKeyDown = function( + contentWindow, targetQuery, keyIdentifier, ctrl, opt_iframeQuery) { + var event = new KeyboardEvent( + 'keydown', + { bubbles: true, keyIdentifier: keyIdentifier, ctrlKey: ctrl }); + return test.util.sync.sendEvent( + contentWindow, targetQuery, event, opt_iframeQuery); +}; + +/** + * Simulates a fake mouse click (left button, single click) on the element + * specified by |targetQuery|. This sends 'mouseover', 'mousedown', 'mouseup' + * and 'click' events in turns. + * + * @param {Window} contentWindow Window to be tested. + * @param {string} targetQuery Query to specify the element. + * @param {string=} opt_iframeQuery Optional iframe selector. + * @return {boolean} True if the all events are sent to the target, false + * otherwise. + */ +test.util.sync.fakeMouseClick = function( + contentWindow, targetQuery, opt_iframeQuery) { + var mouseOverEvent = new MouseEvent('mouseover', {bubbles: true, detail: 1}); + var resultMouseOver = test.util.sync.sendEvent( + contentWindow, targetQuery, mouseOverEvent, opt_iframeQuery); + var mouseDownEvent = new MouseEvent('mousedown', {bubbles: true, detail: 1}); + var resultMouseDown = test.util.sync.sendEvent( + contentWindow, targetQuery, mouseDownEvent, opt_iframeQuery); + var mouseUpEvent = new MouseEvent('mouseup', {bubbles: true, detail: 1}); + var resultMouseUp = test.util.sync.sendEvent( + contentWindow, targetQuery, mouseUpEvent, opt_iframeQuery); + var clickEvent = new MouseEvent('click', {bubbles: true, detail: 1}); + var resultClick = test.util.sync.sendEvent( + contentWindow, targetQuery, clickEvent, opt_iframeQuery); + return resultMouseOver && resultMouseDown && resultMouseUp && resultClick; +}; + +/** + * Simulates a fake double click event (left button) to the element specified by + * |targetQuery|. + * + * @param {Window} contentWindow Window to be tested. + * @param {string} targetQuery Query to specify the element. + * @param {string=} opt_iframeQuery Optional iframe selector. + * @return {boolean} True if the event is sent to the target, false otherwise. + */ +test.util.sync.fakeMouseDoubleClick = function( + contentWindow, targetQuery, opt_iframeQuery) { + // Double click is always preceded with a single click. + if (!test.util.sync.fakeMouseClick( + contentWindow, targetQuery, opt_iframeQuery)) { + return false; + } + + // Send the second click event, but with detail equal to 2 (number of clicks) + // in a row. + var event = new MouseEvent('click', { bubbles: true, detail: 2 }); + if (!test.util.sync.sendEvent( + contentWindow, targetQuery, event, opt_iframeQuery)) { + return false; + } + + // Send the double click event. + var event = new MouseEvent('dblclick', { bubbles: true }); + if (!test.util.sync.sendEvent( + contentWindow, targetQuery, event, opt_iframeQuery)) { + return false; + } + + return true; +}; + +/** + * Sends a fake mouse down event to the element specified by |targetQuery|. + * + * @param {Window} contentWindow Window to be tested. + * @param {string} targetQuery Query to specify the element. + * @param {string=} opt_iframeQuery Optional iframe selector. + * @return {boolean} True if the event is sent to the target, false otherwise. + */ +test.util.sync.fakeMouseDown = function( + contentWindow, targetQuery, opt_iframeQuery) { + var event = new MouseEvent('mousedown', { bubbles: true }); + return test.util.sync.sendEvent( + contentWindow, targetQuery, event, opt_iframeQuery); +}; + +/** + * Sends a fake mouse up event to the element specified by |targetQuery|. + * + * @param {Window} contentWindow Window to be tested. + * @param {string} targetQuery Query to specify the element. + * @param {string=} opt_iframeQuery Optional iframe selector. + * @return {boolean} True if the event is sent to the target, false otherwise. + */ +test.util.sync.fakeMouseUp = function( + contentWindow, targetQuery, opt_iframeQuery) { + var event = new MouseEvent('mouseup', { bubbles: true }); + return test.util.sync.sendEvent( + contentWindow, targetQuery, event, opt_iframeQuery); +}; + +/** + * Selects |filename| and fakes pressing Ctrl+C, Ctrl+V (copy, paste). + * + * @param {Window} contentWindow Window to be tested. + * @param {string} filename Name of the file to be copied. + * @return {boolean} True if copying got simulated successfully. It does not + * say if the file got copied, or not. + */ +test.util.sync.copyFile = function(contentWindow, filename) { + if (!test.util.sync.selectFile(contentWindow, filename)) + return false; + // Ctrl+C and Ctrl+V + test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+0043', true); + test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+0056', true); + return true; +}; + +/** + * Selects |filename| and fakes pressing the Delete key. + * + * @param {Window} contentWindow Window to be tested. + * @param {string} filename Name of the file to be deleted. + * @return {boolean} True if deleting got simulated successfully. It does not + * say if the file got deleted, or not. + */ +test.util.sync.deleteFile = function(contentWindow, filename) { + if (!test.util.sync.selectFile(contentWindow, filename)) + return false; + // Delete + test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+007F', false); + return true; +}; + +/** + * Wait for the elements' style to be changed as the expected values. The + * queries argument is a list of object that have the query property and the + * styles property. The query property is a string query to specify the + * element. The styles property is a string map of the style name and its + * expected value. + * + * @param {Window} contentWindow Window to be tested. + * @param {Array.<object>} queries Queries that specifies the elements and + * expected styles. + * @param {function()} callback Callback function to be notified the change of + * the styles. + */ +test.util.async.waitForStyles = function(contentWindow, queries, callback) { + test.util.repeatUntilTrue_(function() { + for (var i = 0; i < queries.length; i++) { + var element = contentWindow.document.querySelector(queries[i].query); + var styles = queries[i].styles; + for (var name in styles) { + if (contentWindow.getComputedStyle(element)[name] != styles[name]) + return false; + } + } + callback(); + return true; + }); +}; + +/** + * Execute a command on the document in the specified window. + * + * @param {Window} contentWindow Window to be tested. + * @param {string} command Command name. + * @return {boolean} True if the command is executed successfully. + */ +test.util.sync.execCommand = function(contentWindow, command) { + return contentWindow.document.execCommand(command); +}; + +/** + * Override the installWebstoreItem method in private api for test. + * + * @param {Window} contentWindow Window to be tested. + * @param {string} expectedItemId Item ID to be called this method with. + * @param {?string} intendedError Error message to be returned when the item id + * matches. 'null' represents no error. + * @return {boolean} Always return true. + */ +test.util.sync.overrideInstallWebstoreItemApi = + function(contentWindow, expectedItemId, intendedError) { + var setLastError = function(message) { + contentWindow.chrome.runtime.lastError = + message ? {message: message} : null; + }; + + var installWebstoreItem = function(itemId, callback) { + setTimeout(function() { + if (itemId !== expectedItemId) { + setLastError('Invalid Chrome Web Store item ID'); + callback(); + return; + } + + setLastError(intendedError); + callback(); + }); + }; + + test.util.executedTasks_ = []; + contentWindow.chrome.fileBrowserPrivate.installWebstoreItem = + installWebstoreItem; + return true; +}; + +/** + * Override the task-related methods in private api for test. + * + * @param {Window} contentWindow Window to be tested. + * @param {Array.<Object>} taskList List of tasks to be returned in + * fileBrowserPrivate.getFileTasks(). + * @return {boolean} Always return true. + */ +test.util.sync.overrideTasks = function(contentWindow, taskList) { + var getFileTasks = function(urls, mime, onTasks) { + // Call onTask asynchronously (same with original getFileTasks). + setTimeout(function() { + onTasks(taskList); + }); + }; + + var executeTask = function(taskId, url) { + test.util.executedTasks_.push(taskId); + }; + + test.util.executedTasks_ = []; + contentWindow.chrome.fileBrowserPrivate.getFileTasks = getFileTasks; + contentWindow.chrome.fileBrowserPrivate.executeTask = executeTask; + return true; +}; + +/** + * Check if Files.app has ordered to execute the given task or not yet. This + * method must be used with test.util.sync.overrideTasks(). + * + * @param {Window} contentWindow Window to be tested. + * @param {string} taskId Taskid of the task which should be executed. + * @param {function()} callback Callback function to be notified the order of + * the execution. + */ +test.util.async.waitUntilTaskExecutes = + function(contentWindow, taskId, callback) { + if (!test.util.executedTasks_) { + console.error('Please call overrideTasks() first.'); + return; + } + + test.util.repeatUntilTrue_(function() { + if (test.util.executedTasks_.indexOf(taskId) === -1) + return false; + callback(); + return true; + }); +}; + +/** + * Registers message listener, which runs test utility functions. + */ +test.util.registerRemoteTestUtils = function() { + // Register the message listener. + var onMessage = chrome.runtime ? chrome.runtime.onMessageExternal : + chrome.extension.onMessageExternal; + // Return true for asynchronous functions and false for synchronous. + onMessage.addListener(function(request, sender, sendResponse) { + // Check the sender. + if (sender.id != test.util.TESTING_EXTENSION_ID) { + console.error('The testing extension must be white-listed.'); + return false; + } + // Set a global flag that we are in tests, so other components are aware + // of it. + window.IN_TEST = true; + // Check the function name. + if (!request.func || request.func[request.func.length - 1] == '_') { + request.func = ''; + } + // Prepare arguments. + var args = request.args.slice(); // shallow copy + if (request.appId) { + if (!background.appWindows[request.appId]) { + console.error('Specified window not found.'); + return false; + } + args.unshift(background.appWindows[request.appId].contentWindow); + } + // Call the test utility function and respond the result. + if (test.util.async[request.func]) { + args[test.util.async[request.func].length - 1] = function() { + console.debug('Received the result of ' + request.func); + sendResponse.apply(null, arguments); + }; + console.debug('Waiting for the result of ' + request.func); + test.util.async[request.func].apply(null, args); + return true; + } else if (test.util.sync[request.func]) { + sendResponse(test.util.sync[request.func].apply(null, args)); + return false; + } else { + console.error('Invalid function name.'); + return false; + } + }); +}; + +// Register the test utils. +test.util.registerRemoteTestUtils(); diff --git a/chromium/chrome/browser/resources/file_manager/background/js/volume_manager.js b/chromium/chrome/browser/resources/file_manager/background/js/volume_manager.js new file mode 100644 index 00000000000..35e94f2cd7e --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/background/js/volume_manager.js @@ -0,0 +1,727 @@ +// 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'; + +/** + * Represents each volume, such as "drive", "download directory", each "USB + * flush storage", or "mounted zip archive" etc. + * + * @param {util.VolumeType} volumeType The type of the volume. + * @param {string} mountPath Where the volume is mounted. + * @param {string} volumeId ID of the volume. + * @param {DirectoryEntry} root The root directory entry of this volume. + * @param {string} error The error if an error is found. + * @param {string} deviceType The type of device ('usb'|'sd'|'optical'|'mobile' + * |'unknown') (as defined in chromeos/disks/disk_mount_manager.cc). + * Can be null. + * @param {boolean} isReadOnly True if the volume is read only. + * @param {!{displayName:string, isCurrentProfile:boolean}} profile Profile + * information. + * @constructor + */ +function VolumeInfo( + volumeType, + mountPath, + volumeId, + root, + error, + deviceType, + isReadOnly, + profile) { + this.volumeType = volumeType; + // TODO(hidehiko): This should include FileSystem instance. + this.mountPath = mountPath; + this.volumeId = volumeId; + this.root = root; + + // Note: This represents if the mounting of the volume is successfully done + // or not. (If error is empty string, the mount is successfully done). + // TODO(hidehiko): Rename to make this more understandable. + this.error = error; + this.deviceType = deviceType; + this.isReadOnly = isReadOnly; + this.profile = Object.freeze(profile); + + // VolumeInfo is immutable. + Object.freeze(this); +} + +/** + * Obtains a URL of the display root directory that users can see as a root. + * @return {string} URL of root entry. + */ +VolumeInfo.prototype.getDisplayRootDirectoryURL = function() { + return this.root.toURL() + + (this.volumeType === util.VolumeType.DRIVE ? '/root' : ''); +}; + +/** + * Obtains volume label. + * @return {string} Label for the volume. + */ +VolumeInfo.prototype.getLabel = function() { + if (this.volumeType === util.VolumeType.DRIVE) + return str('DRIVE_DIRECTORY_LABEL'); + else + return PathUtil.getFolderLabel(this.mountPath); +}; + +/** + * Utilities for volume manager implementation. + */ +var volumeManagerUtil = {}; + +/** + * Throws an Error when the given error is not in util.VolumeError. + * @param {util.VolumeError} error Status string usually received from APIs. + */ +volumeManagerUtil.validateError = function(error) { + for (var key in util.VolumeError) { + if (error === util.VolumeError[key]) + return; + } + + throw new Error('Invalid mount error: ' + error); +}; + +/** + * Returns the root entry of a volume mounted at mountPath. + * + * @param {string} mountPath The mounted path of the volume. + * @param {function(DirectoryEntry)} successCallback Called when the root entry + * is found. + * @param {function(FileError)} errorCallback Called when an error is found. + * @private + */ +volumeManagerUtil.getRootEntry_ = function( + mountPath, successCallback, errorCallback) { + // We always request FileSystem here, because requestFileSystem() grants + // permissions if necessary, especially for Drive File System at first mount + // time. + // Note that we actually need to request FileSystem after multi file system + // support, so this will be more natural code then. + chrome.fileBrowserPrivate.requestFileSystem( + 'compatible', + function(fileSystem) { + // TODO(hidehiko): chrome.runtime.lastError should have error reason. + if (!fileSystem) { + errorCallback(util.createFileError(FileError.NOT_FOUND_ERR)); + return; + } + + fileSystem.root.getDirectory( + mountPath.substring(1), // Strip leading '/'. + {create: false}, successCallback, errorCallback); + }); +}; + +/** + * Builds the VolumeInfo data from VolumeMetadata. + * @param {VolumeMetadata} volumeMetadata Metadata instance for the volume. + * @param {function(VolumeInfo)} callback Called on completion. + */ +volumeManagerUtil.createVolumeInfo = function(volumeMetadata, callback) { + volumeManagerUtil.getRootEntry_( + volumeMetadata.mountPath, + function(entry) { + if (volumeMetadata.volumeType === util.VolumeType.DRIVE) { + // After file system is mounted, we "read" drive grand root + // entry at first. This triggers full feed fetch on background. + // Note: we don't need to handle errors here, because even if + // it fails, accessing to some path later will just become + // a fast-fetch and it re-triggers full-feed fetch. + entry.createReader().readEntries( + function() { /* do nothing */ }, + function(error) { + console.error( + 'Triggering full feed fetch is failed: ' + + util.getFileErrorMnemonic(error.code)); + }); + } + callback(new VolumeInfo( + volumeMetadata.volumeType, + volumeMetadata.mountPath, + volumeMetadata.volumeId, + entry, + volumeMetadata.mountCondition, + volumeMetadata.deviceType, + volumeMetadata.isReadOnly, + volumeMetadata.profile)); + }, + function(fileError) { + console.error('Root entry is not found: ' + + volumeMetadata.mountPath + ', ' + + util.getFileErrorMnemonic(fileError.code)); + callback(new VolumeInfo( + volumeMetadata.volumeType, + volumeMetadata.mountPath, + volumeMetadata.volumeId, + null, // Root entry is not found. + volumeMetadata.mountCondition, + volumeMetadata.deviceType, + volumeMetadata.isReadOnly, + volumeMetadata.profile)); + }); +}; + +/** + * The order of the volume list based on root type. + * @type {Array.<string>} + * @const + * @private + */ +volumeManagerUtil.volumeListOrder_ = [ + RootType.DRIVE, RootType.DOWNLOADS, RootType.ARCHIVE, RootType.REMOVABLE +]; + +/** + * Compares mount paths to sort the volume list order. + * @param {string} mountPath1 The mount path for the first volume. + * @param {string} mountPath2 The mount path for the second volume. + * @return {number} 0 if mountPath1 and mountPath2 are same, -1 if VolumeInfo + * mounted at mountPath1 should be listed before the one mounted at + * mountPath2, otherwise 1. + */ +volumeManagerUtil.compareMountPath = function(mountPath1, mountPath2) { + var order1 = volumeManagerUtil.volumeListOrder_.indexOf( + PathUtil.getRootType(mountPath1)); + var order2 = volumeManagerUtil.volumeListOrder_.indexOf( + PathUtil.getRootType(mountPath2)); + if (order1 !== order2) + return order1 < order2 ? -1 : 1; + + if (mountPath1 !== mountPath2) + return mountPath1 < mountPath2 ? -1 : 1; + + // The path is same. + return 0; +}; + +/** + * The container of the VolumeInfo for each mounted volume. + * @constructor + */ +function VolumeInfoList() { + /** + * Holds VolumeInfo instances. + * @type {cr.ui.ArrayDataModel} + * @private + */ + this.model_ = new cr.ui.ArrayDataModel([]); + + Object.freeze(this); +} + +VolumeInfoList.prototype = { + get length() { return this.model_.length; } +}; + +/** + * Adds the event listener to listen the change of volume info. + * @param {string} type The name of the event. + * @param {function(Event)} handler The handler for the event. + */ +VolumeInfoList.prototype.addEventListener = function(type, handler) { + this.model_.addEventListener(type, handler); +}; + +/** + * Removes the event listener. + * @param {string} type The name of the event. + * @param {function(Event)} handler The handler to be removed. + */ +VolumeInfoList.prototype.removeEventListener = function(type, handler) { + this.model_.removeEventListener(type, handler); +}; + +/** + * Adds the volumeInfo to the appropriate position. If there already exists, + * just replaces it. + * @param {VolumeInfo} volumeInfo The information of the new volume. + */ +VolumeInfoList.prototype.add = function(volumeInfo) { + var index = this.findLowerBoundIndex_(volumeInfo.mountPath); + if (index < this.length && + this.item(index).mountPath === volumeInfo.mountPath) { + // Replace the VolumeInfo. + this.model_.splice(index, 1, volumeInfo); + } else { + // Insert the VolumeInfo. + this.model_.splice(index, 0, volumeInfo); + } +}; + +/** + * Removes the VolumeInfo of the volume mounted at mountPath. + * @param {string} mountPath The path to the location where the volume is + * mounted. + */ +VolumeInfoList.prototype.remove = function(mountPath) { + var index = this.findLowerBoundIndex_(mountPath); + if (index < this.length && this.item(index).mountPath === mountPath) + this.model_.splice(index, 1); +}; + +/** + * Searches the information of the volume mounted at mountPath. + * @param {string} mountPath The path to the location where the volume is + * mounted. + * @return {VolumeInfo} The volume's information, or null if not found. + */ +VolumeInfoList.prototype.find = function(mountPath) { + var index = this.findLowerBoundIndex_(mountPath); + if (index < this.length && this.item(index).mountPath === mountPath) + return this.item(index); + + // Not found. + return null; +}; + +/** + * Searches the information of the volume that contains an item pointed by the + * path. + * @param {string} path Path pointing an entry on a volume. + * @return {VolumeInfo} The volume's information, or null if not found. + */ +VolumeInfoList.prototype.findByPath = function(path) { + for (var i = 0; i < this.length; i++) { + var mountPath = this.item(i).mountPath; + if (path === mountPath || path.indexOf(mountPath + '/') === 0) + return this.item(i); + } + return null; +}; + +/** + * @param {string} mountPath The mount path of searched volume. + * @return {number} The index of the volume if found, or the inserting + * position of the volume. + * @private + */ +VolumeInfoList.prototype.findLowerBoundIndex_ = function(mountPath) { + // Assuming the number of elements in the array data model is very small + // in most cases, use simple linear search, here. + for (var i = 0; i < this.length; i++) { + if (volumeManagerUtil.compareMountPath( + this.item(i).mountPath, mountPath) >= 0) + return i; + } + return this.length; +}; + +/** + * @param {number} index The index of the volume in the list. + * @return {VolumeInfo} The VolumeInfo instance. + */ +VolumeInfoList.prototype.item = function(index) { + return this.model_.item(index); +}; + +/** + * VolumeManager is responsible for tracking list of mounted volumes. + * + * @constructor + * @extends {cr.EventTarget} + */ +function VolumeManager() { + /** + * The list of archives requested to mount. We will show contents once + * archive is mounted, but only for mounts from within this filebrowser tab. + * @type {Object.<string, Object>} + * @private + */ + this.requests_ = {}; + + /** + * The list of VolumeInfo instances for each mounted volume. + * @type {VolumeInfoList} + */ + this.volumeInfoList = new VolumeInfoList(); + + // The status should be merged into VolumeManager. + // TODO(hidehiko): Remove them after the migration. + this.driveConnectionState_ = { + type: util.DriveConnectionType.OFFLINE, + reason: util.DriveConnectionReason.NO_SERVICE + }; + + chrome.fileBrowserPrivate.onDriveConnectionStatusChanged.addListener( + this.onDriveConnectionStatusChanged_.bind(this)); + this.onDriveConnectionStatusChanged_(); +} + +/** + * Invoked when the drive connection status is changed. + * @private_ + */ +VolumeManager.prototype.onDriveConnectionStatusChanged_ = function() { + chrome.fileBrowserPrivate.getDriveConnectionState(function(state) { + this.driveConnectionState_ = state; + cr.dispatchSimpleEvent(this, 'drive-connection-changed'); + }.bind(this)); +}; + +/** + * Returns the drive connection state. + * @return {util.DriveConnectionType} Connection type. + */ +VolumeManager.prototype.getDriveConnectionState = function() { + return this.driveConnectionState_; +}; + +/** + * VolumeManager extends cr.EventTarget. + */ +VolumeManager.prototype.__proto__ = cr.EventTarget.prototype; + +/** + * Time in milliseconds that we wait a response for. If no response on + * mount/unmount received the request supposed failed. + */ +VolumeManager.TIMEOUT = 15 * 60 * 1000; + +/** + * Queue to run getInstance sequentially. + * @type {AsyncUtil.Queue} + * @private + */ +VolumeManager.getInstanceQueue_ = new AsyncUtil.Queue(); + +/** + * The singleton instance of VolumeManager. Initialized by the first invocation + * of getInstance(). + * @type {VolumeManager} + * @private + */ +VolumeManager.instance_ = null; + +/** + * Returns the VolumeManager instance asynchronously. If it is not created or + * under initialization, it will waits for the finish of the initialization. + * @param {function(VolumeManager)} callback Called with the VolumeManager + * instance. + */ +VolumeManager.getInstance = function(callback) { + VolumeManager.getInstanceQueue_.run(function(continueCallback) { + if (VolumeManager.instance_) { + callback(VolumeManager.instance_); + continueCallback(); + return; + } + + VolumeManager.instance_ = new VolumeManager(); + VolumeManager.instance_.initialize_(function() { + callback(VolumeManager.instance_); + continueCallback(); + }); + }); +}; + +/** + * Initializes mount points. + * @param {function()} callback Called upon the completion of the + * initialization. + * @private + */ +VolumeManager.prototype.initialize_ = function(callback) { + chrome.fileBrowserPrivate.getVolumeMetadataList(function(volumeMetadataList) { + // Create VolumeInfo for each volume. + var group = new AsyncUtil.Group(); + for (var i = 0; i < volumeMetadataList.length; i++) { + group.add(function(volumeMetadata, continueCallback) { + volumeManagerUtil.createVolumeInfo( + volumeMetadata, + function(volumeInfo) { + this.volumeInfoList.add(volumeInfo); + if (volumeMetadata.volumeType === util.VolumeType.DRIVE) + this.onDriveConnectionStatusChanged_(); + continueCallback(); + }.bind(this)); + }.bind(this, volumeMetadataList[i])); + } + + // Then, finalize the initialization. + group.run(function() { + // Subscribe to the mount completed event when mount points initialized. + chrome.fileBrowserPrivate.onMountCompleted.addListener( + this.onMountCompleted_.bind(this)); + callback(); + }.bind(this)); + }.bind(this)); +}; + +/** + * Event handler called when some volume was mounted or unmounted. + * @param {MountCompletedEvent} event Received event. + * @private + */ +VolumeManager.prototype.onMountCompleted_ = function(event) { + if (event.eventType === 'mount') { + if (event.volumeMetadata.mountPath) { + var requestKey = this.makeRequestKey_( + 'mount', + event.volumeMetadata.sourcePath); + + var error = event.status === 'success' ? '' : event.status; + + volumeManagerUtil.createVolumeInfo( + event.volumeMetadata, + function(volumeInfo) { + this.volumeInfoList.add(volumeInfo); + this.finishRequest_(requestKey, event.status, volumeInfo.mountPath); + + if (volumeInfo.volumeType === util.VolumeType.DRIVE) { + // Update the network connection status, because until the + // drive is initialized, the status is set to not ready. + // TODO(hidehiko): The connection status should be migrated into + // VolumeMetadata. + this.onDriveConnectionStatusChanged_(); + } + }.bind(this)); + } else { + console.warn('No mount path.'); + this.finishRequest_(requestKey, event.status); + } + } else if (event.eventType === 'unmount') { + var mountPath = event.volumeMetadata.mountPath; + var status = event.status; + if (status === util.VolumeError.PATH_UNMOUNTED) { + console.warn('Volume already unmounted: ', mountPath); + status = 'success'; + } + var requestKey = this.makeRequestKey_('unmount', mountPath); + var requested = requestKey in this.requests_; + var volumeInfo = this.volumeInfoList.find(mountPath); + if (event.status === 'success' && !requested && volumeInfo) { + console.warn('Mounted volume without a request: ', mountPath); + var e = new Event('externally-unmounted'); + // TODO(mtomasz): The mountPath field is deprecated. Remove it. + e.mountPath = mountPath; + e.volumeInfo = volumeInfo; + this.dispatchEvent(e); + } + this.finishRequest_(requestKey, status); + + if (event.status === 'success') + this.volumeInfoList.remove(mountPath); + } +}; + +/** + * Creates string to match mount events with requests. + * @param {string} requestType 'mount' | 'unmount'. TODO(hidehiko): Replace by + * enum. + * @param {string} path Source path provided by API for mount request, or + * mount path for unmount request. + * @return {string} Key for |this.requests_|. + * @private + */ +VolumeManager.prototype.makeRequestKey_ = function(requestType, path) { + return requestType + ':' + path; +}; + +/** + * @param {string} fileUrl File url to the archive file. + * @param {function(string)} successCallback Success callback. + * @param {function(util.VolumeError)} errorCallback Error callback. + */ +VolumeManager.prototype.mountArchive = function( + fileUrl, successCallback, errorCallback) { + chrome.fileBrowserPrivate.addMount(fileUrl, function(sourcePath) { + console.info( + 'Mount request: url=' + fileUrl + '; sourceUrl=' + sourcePath); + var requestKey = this.makeRequestKey_('mount', sourcePath); + this.startRequest_(requestKey, successCallback, errorCallback); + }.bind(this)); +}; + +/** + * Unmounts volume. + * @param {string} mountPath Volume mounted path. + * @param {function(string)} successCallback Success callback. + * @param {function(util.VolumeError)} errorCallback Error callback. + */ +VolumeManager.prototype.unmount = function(mountPath, + successCallback, + errorCallback) { + var volumeInfo = this.volumeInfoList.find(mountPath); + if (!volumeInfo) { + errorCallback(util.VolumeError.NOT_MOUNTED); + return; + } + + chrome.fileBrowserPrivate.removeMount(util.makeFilesystemUrl(mountPath)); + var requestKey = this.makeRequestKey_('unmount', volumeInfo.mountPath); + this.startRequest_(requestKey, successCallback, errorCallback); +}; + +/** + * Resolves the absolute path to its entry. Shouldn't be used outside of the + * Files app's initialization. + * @param {string} path The path to be resolved. + * @param {function(Entry)} successCallback Called with the resolved entry on + * success. + * @param {function(FileError)} errorCallback Called on error. + */ +VolumeManager.prototype.resolveAbsolutePath = function( + path, successCallback, errorCallback) { + // Make sure the path is in the mounted volume. + var volumeInfo = this.getVolumeInfo(path); + if (!volumeInfo || !volumeInfo.root) { + errorCallback(util.createFileError(FileError.NOT_FOUND_ERR)); + return; + } + + webkitResolveLocalFileSystemURL( + util.makeFilesystemUrl(path), successCallback, errorCallback); +}; + +/** + * Obtains the information of the volume that containing an entry pointed by the + * specified path. + * TODO(hirono): Stop to use path to get a volume info. + * + * @param {string|Entry} target Path or Entry pointing anywhere on a volume. + * @return {VolumeInfo} The data about the volume. + */ +VolumeManager.prototype.getVolumeInfo = function(target) { + if (typeof target === 'string') + return this.volumeInfoList.findByPath(target); + else if (util.isFakeEntry(target)) + return this.getCurrentProfileVolumeInfo(util.VolumeType.DRIVE); + else + return this.volumeInfoList.findByPath(target.fullPath); +}; + +/** + * Obtains a volume information from a file entry URL. + * TODO(hirono): Check a file system to find a volume. + * + * @param {string} url URL of entry. + * @return {VolumeInfo} Volume info. + */ +VolumeManager.prototype.getVolumeInfoByURL = function(url) { + return this.getVolumeInfo(util.extractFilePath(url)); +}; + +/** + * Obtains a volume infomration of the current profile. + * + * @param {util.VolumeType} volumeType Volume type. + * @return {VolumeInfo} Volume info. + */ +VolumeManager.prototype.getCurrentProfileVolumeInfo = function(volumeType) { + for (var i = 0; i < this.volumeInfoList.length; i++) { + var volumeInfo = this.volumeInfoList.item(i); + if (volumeInfo.profile.isCurrentProfile && + volumeInfo.volumeType === volumeType) + return volumeInfo; + } + return null; +}; + +/** + * Obtains location information from an entry. + * + * @param {Entry|Object} entry File or directory entry. It can be a fake entry. + * @return {EntryLocation} Location information. + */ +VolumeManager.prototype.getLocationInfo = function(entry) { + if (util.isFakeEntry(entry)) { + return new EntryLocation( + // TODO(hirono): Specify currect volume. + this.getCurrentProfileVolumeInfo(RootType.DRIVE), + entry.rootType, + true /* the entry points a root directory. */); + } else { + return this.getLocationInfoByPath(entry.fullPath); + } +}; + +/** + * Obtains location information from a path. + * TODO(hirono): Remove the method before introducing separate file system. + * + * @param {string} path Path. + * @return {EntryLocation} Location information. + */ +VolumeManager.prototype.getLocationInfoByPath = function(path) { + var volumeInfo = this.volumeInfoList.findByPath(path); + return volumeInfo && PathUtil.getLocationInfo(volumeInfo, path); +}; + +/** + * @param {string} key Key produced by |makeRequestKey_|. + * @param {function(string)} successCallback To be called when request finishes + * successfully. + * @param {function(util.VolumeError)} errorCallback To be called when + * request fails. + * @private + */ +VolumeManager.prototype.startRequest_ = function(key, + successCallback, errorCallback) { + if (key in this.requests_) { + var request = this.requests_[key]; + request.successCallbacks.push(successCallback); + request.errorCallbacks.push(errorCallback); + } else { + this.requests_[key] = { + successCallbacks: [successCallback], + errorCallbacks: [errorCallback], + + timeout: setTimeout(this.onTimeout_.bind(this, key), + VolumeManager.TIMEOUT) + }; + } +}; + +/** + * Called if no response received in |TIMEOUT|. + * @param {string} key Key produced by |makeRequestKey_|. + * @private + */ +VolumeManager.prototype.onTimeout_ = function(key) { + this.invokeRequestCallbacks_(this.requests_[key], + util.VolumeError.TIMEOUT); + delete this.requests_[key]; +}; + +/** + * @param {string} key Key produced by |makeRequestKey_|. + * @param {util.VolumeError|'success'} status Status received from the API. + * @param {string=} opt_mountPath Mount path. + * @private + */ +VolumeManager.prototype.finishRequest_ = function(key, status, opt_mountPath) { + var request = this.requests_[key]; + if (!request) + return; + + clearTimeout(request.timeout); + this.invokeRequestCallbacks_(request, status, opt_mountPath); + delete this.requests_[key]; +}; + +/** + * @param {Object} request Structure created in |startRequest_|. + * @param {util.VolumeError|string} status If status === 'success' + * success callbacks are called. + * @param {string=} opt_mountPath Mount path. Required if success. + * @private + */ +VolumeManager.prototype.invokeRequestCallbacks_ = function(request, status, + opt_mountPath) { + var callEach = function(callbacks, self, args) { + for (var i = 0; i < callbacks.length; i++) { + callbacks[i].apply(self, args); + } + }; + if (status === 'success') { + callEach(request.successCallbacks, this, [opt_mountPath]); + } else { + volumeManagerUtil.validateError(status); + callEach(request.errorCallbacks, this, [status]); + } +}; diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/archive.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/archive.png Binary files differnew file mode 100644 index 00000000000..2a054b38515 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/archive.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/archive_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/archive_white.png Binary files differnew file mode 100644 index 00000000000..94ffaf0f310 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/archive_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/audio.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/audio.png Binary files differnew file mode 100644 index 00000000000..a5b4ac9e774 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/audio.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/audio_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/audio_white.png Binary files differnew file mode 100644 index 00000000000..819e4065021 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/audio_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/chart.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/chart.png Binary files differnew file mode 100644 index 00000000000..4f7bfa145eb --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/chart.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/chart_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/chart_white.png Binary files differnew file mode 100644 index 00000000000..7500f3f83f6 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/chart_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/excel.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/excel.png Binary files differnew file mode 100644 index 00000000000..cb27f5c242c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/excel.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/excel_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/excel_white.png Binary files differnew file mode 100644 index 00000000000..0900b09a741 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/excel_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/folder.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/folder.png Binary files differnew file mode 100644 index 00000000000..3f6e17f0892 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/folder.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/folder_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/folder_white.png Binary files differnew file mode 100644 index 00000000000..9a56eb9b8d8 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/folder_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/form.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/form.png Binary files differnew file mode 100644 index 00000000000..c3d2087db8a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/form.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/form_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/form_white.png Binary files differnew file mode 100644 index 00000000000..21c18adf387 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/form_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdoc.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdoc.png Binary files differnew file mode 100644 index 00000000000..102f1c0f432 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdoc.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdoc_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdoc_white.png Binary files differnew file mode 100644 index 00000000000..a66a338421f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdoc_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdraw.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdraw.png Binary files differnew file mode 100644 index 00000000000..b1120418420 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdraw.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdraw_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdraw_white.png Binary files differnew file mode 100644 index 00000000000..f92f18b5f1e --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdraw_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/generic.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/generic.png Binary files differnew file mode 100644 index 00000000000..bff3cdacba7 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/generic.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/generic_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/generic_white.png Binary files differnew file mode 100644 index 00000000000..a369f66eabc --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/generic_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/glink.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/glink.png Binary files differnew file mode 100644 index 00000000000..bff3cdacba7 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/glink.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/glink_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/glink_white.png Binary files differnew file mode 100644 index 00000000000..a369f66eabc --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/glink_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gsheet.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gsheet.png Binary files differnew file mode 100644 index 00000000000..6fbd46b3052 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gsheet.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gsheet_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gsheet_white.png Binary files differnew file mode 100644 index 00000000000..39cf43df5ff --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gsheet_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gslides.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gslides.png Binary files differnew file mode 100644 index 00000000000..76523cc9746 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gslides.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gslides_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gslides_white.png Binary files differnew file mode 100644 index 00000000000..ea14b91430c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gslides_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gtable.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gtable.png Binary files differnew file mode 100644 index 00000000000..a78b26e54f7 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gtable.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gtable_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gtable_white.png Binary files differnew file mode 100644 index 00000000000..9198c9e99ee --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gtable_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/image.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/image.png Binary files differnew file mode 100644 index 00000000000..9ec2dbd5cb7 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/image.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/image_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/image_white.png Binary files differnew file mode 100644 index 00000000000..739614ffc0a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/image_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/pdf.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/pdf.png Binary files differnew file mode 100644 index 00000000000..ec6d679f5c9 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/pdf.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/pdf_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/pdf_white.png Binary files differnew file mode 100644 index 00000000000..076820461c0 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/pdf_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/ppt.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/ppt.png Binary files differnew file mode 100644 index 00000000000..0a834451830 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/ppt.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/ppt_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/ppt_white.png Binary files differnew file mode 100644 index 00000000000..f18491debb3 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/ppt_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/script.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/script.png Binary files differnew file mode 100644 index 00000000000..054f59a78fc --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/script.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/script_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/script_white.png Binary files differnew file mode 100644 index 00000000000..2760eb805b9 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/script_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/sites.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/sites.png Binary files differnew file mode 100644 index 00000000000..e4e0ffe720f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/sites.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/sites_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/sites_white.png Binary files differnew file mode 100644 index 00000000000..11646f82866 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/sites_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/video.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/video.png Binary files differnew file mode 100644 index 00000000000..73b70f63b23 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/video.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/video_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/video_white.png Binary files differnew file mode 100644 index 00000000000..ac1c0806c9f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/video_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/word.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/word.png Binary files differnew file mode 100644 index 00000000000..a2bf315f901 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/word.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/word_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/word_white.png Binary files differnew file mode 100644 index 00000000000..9bc0f21f7d7 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/word_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/archive.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/archive.png Binary files differnew file mode 100644 index 00000000000..f51418150f7 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/archive.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/archive_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/archive_white.png Binary files differnew file mode 100644 index 00000000000..6b522cf0c7b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/archive_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/audio.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/audio.png Binary files differnew file mode 100644 index 00000000000..ad2f540c2c3 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/audio.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/audio_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/audio_white.png Binary files differnew file mode 100644 index 00000000000..997d6106b00 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/audio_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/chart.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/chart.png Binary files differnew file mode 100644 index 00000000000..5b03c369a1a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/chart.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/chart_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/chart_white.png Binary files differnew file mode 100644 index 00000000000..366fc85f3ac --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/chart_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/excel.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/excel.png Binary files differnew file mode 100644 index 00000000000..22a4eb5d6fd --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/excel.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/excel_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/excel_white.png Binary files differnew file mode 100644 index 00000000000..bf0a61c07b5 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/excel_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/folder.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/folder.png Binary files differnew file mode 100644 index 00000000000..782227f9bde --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/folder.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/folder_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/folder_white.png Binary files differnew file mode 100644 index 00000000000..9124621632c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/folder_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/form.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/form.png Binary files differnew file mode 100644 index 00000000000..7908b559402 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/form.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/form_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/form_white.png Binary files differnew file mode 100644 index 00000000000..0252d4c94fb --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/form_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdoc.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdoc.png Binary files differnew file mode 100644 index 00000000000..80ae026b49d --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdoc.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdoc_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdoc_white.png Binary files differnew file mode 100644 index 00000000000..366180cbea4 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdoc_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdraw.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdraw.png Binary files differnew file mode 100644 index 00000000000..c36fca643fe --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdraw.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdraw_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdraw_white.png Binary files differnew file mode 100644 index 00000000000..98401d94722 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdraw_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/generic.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/generic.png Binary files differnew file mode 100644 index 00000000000..8df85a372f2 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/generic.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/generic_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/generic_white.png Binary files differnew file mode 100644 index 00000000000..9b9b295bbac --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/generic_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/glink.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/glink.png Binary files differnew file mode 100644 index 00000000000..d60d809d64f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/glink.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/glink_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/glink_white.png Binary files differnew file mode 100644 index 00000000000..d9a2070240e --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/glink_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gsheet.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gsheet.png Binary files differnew file mode 100644 index 00000000000..0545e69dea8 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gsheet.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gsheet_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gsheet_white.png Binary files differnew file mode 100644 index 00000000000..801de01d657 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gsheet_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gslides.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gslides.png Binary files differnew file mode 100644 index 00000000000..3f1cd75575c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gslides.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gslides_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gslides_white.png Binary files differnew file mode 100644 index 00000000000..b6c24ab20f8 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gslides_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gtable.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gtable.png Binary files differnew file mode 100644 index 00000000000..fec2061ef44 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gtable.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gtable_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gtable_white.png Binary files differnew file mode 100644 index 00000000000..8c695c58aaa --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gtable_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/image.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/image.png Binary files differnew file mode 100644 index 00000000000..df61101582d --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/image.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/image_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/image_white.png Binary files differnew file mode 100644 index 00000000000..46017e6895c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/image_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/pdf.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/pdf.png Binary files differnew file mode 100644 index 00000000000..0f62d9e620d --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/pdf.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/pdf_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/pdf_white.png Binary files differnew file mode 100644 index 00000000000..e53bda7a989 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/pdf_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/ppt.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/ppt.png Binary files differnew file mode 100644 index 00000000000..5f06e4bffd3 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/ppt.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/ppt_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/ppt_white.png Binary files differnew file mode 100644 index 00000000000..9824222e668 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/ppt_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/script.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/script.png Binary files differnew file mode 100644 index 00000000000..01a2a1ad4b6 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/script.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/script_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/script_white.png Binary files differnew file mode 100644 index 00000000000..b04a21002f1 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/script_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/sites.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/sites.png Binary files differnew file mode 100644 index 00000000000..541c328f7fe --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/sites.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/sites_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/sites_white.png Binary files differnew file mode 100644 index 00000000000..613e5178f1c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/sites_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/video.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/video.png Binary files differnew file mode 100644 index 00000000000..a8346a3f200 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/video.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/video_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/video_white.png Binary files differnew file mode 100644 index 00000000000..3cbcdba263d --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/video_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/word.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/word.png Binary files differnew file mode 100644 index 00000000000..23b78f6782d --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/word.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/word_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/word_white.png Binary files differnew file mode 100644 index 00000000000..424ea80a12f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/word_white.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/icon128.png b/chromium/chrome/browser/resources/file_manager/common/images/icon128.png Binary files differnew file mode 100644 index 00000000000..d4c6c555040 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/icon128.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/icon16.png b/chromium/chrome/browser/resources/file_manager/common/images/icon16.png Binary files differnew file mode 100644 index 00000000000..dc3b8d8b399 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/icon16.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/icon256.png b/chromium/chrome/browser/resources/file_manager/common/images/icon256.png Binary files differnew file mode 100644 index 00000000000..31864fbe0bc --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/icon256.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/icon32.png b/chromium/chrome/browser/resources/file_manager/common/images/icon32.png Binary files differnew file mode 100644 index 00000000000..47b8a55acf1 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/icon32.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/icon48.png b/chromium/chrome/browser/resources/file_manager/common/images/icon48.png Binary files differnew file mode 100644 index 00000000000..b3c7dbfe691 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/icon48.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/icon64.png b/chromium/chrome/browser/resources/file_manager/common/images/icon64.png Binary files differnew file mode 100644 index 00000000000..3130c1c4059 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/icon64.png diff --git a/chromium/chrome/browser/resources/file_manager/common/images/icon96.png b/chromium/chrome/browser/resources/file_manager/common/images/icon96.png Binary files differnew file mode 100644 index 00000000000..fdc1ab1fec2 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/images/icon96.png diff --git a/chromium/chrome/browser/resources/file_manager/common/js/async_util.js b/chromium/chrome/browser/resources/file_manager/common/js/async_util.js new file mode 100644 index 00000000000..f74a9d49381 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/js/async_util.js @@ -0,0 +1,262 @@ +// Copyright (c) 2013 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'; + +/** + * Namespace for async utility functions. + */ +var AsyncUtil = {}; + +/** + * Asynchronous version of Array.forEach. + * This executes a provided function callback once per array element, then + * run completionCallback to notify the completion. + * The callback can be an asynchronous function, but the execution is + * sequentially done. + * + * @param {Array.<T>} array The array to be iterated. + * @param {function(function(), T, number, Array.<T>} callback The iteration + * callback. The first argument is a callback to notify the completion of + * the iteration. + * @param {function()} completionCallback Called when all iterations are + * completed. + * @param {Object=} opt_thisObject Bound to callback if given. + * @template T + */ +AsyncUtil.forEach = function( + array, callback, completionCallback, opt_thisObject) { + if (opt_thisObject) + callback = callback.bind(opt_thisObject); + + var queue = new AsyncUtil.Queue(); + for (var i = 0; i < array.length; i++) { + queue.run(function(element, index, iterationCompletionCallback) { + callback(iterationCompletionCallback, element, index, array); + }.bind(null, array[i], i)); + } + queue.run(function(iterationCompletionCallback) { + completionCallback(); // Don't pass iteration completion callback. + }); +}; + +/** + * Creates a class for executing several asynchronous closures in a fifo queue. + * Added tasks will be executed sequentially in order they were added. + * + * @constructor + */ +AsyncUtil.Queue = function() { + this.running_ = false; + this.closures_ = []; +}; + +/** + * @return {boolean} True when a task is running, otherwise false. + */ +AsyncUtil.Queue.prototype.isRunning = function() { + return this.running_; +}; + +/** + * Enqueues a closure to be executed. + * @param {function(function())} closure Closure with a completion callback to + * be executed. + */ +AsyncUtil.Queue.prototype.run = function(closure) { + this.closures_.push(closure); + if (!this.running_) + this.continue_(); +}; + +/** + * Serves the next closure from the queue. + * @private + */ +AsyncUtil.Queue.prototype.continue_ = function() { + if (!this.closures_.length) { + this.running_ = false; + return; + } + + // Run the next closure. + this.running_ = true; + var closure = this.closures_.shift(); + closure(this.continue_.bind(this)); +}; + +/** + * Cancels all pending tasks. Note that this does NOT cancel the task running + * currently. + */ +AsyncUtil.Queue.prototype.cancel = function() { + this.closures_ = []; +}; + +/** + * Creates a class for executing several asynchronous closures in a group in + * a dependency order. + * + * @constructor + */ +AsyncUtil.Group = function() { + this.addedTasks_ = {}; + this.pendingTasks_ = {}; + this.finishedTasks_ = {}; + this.completionCallbacks_ = []; +}; + +/** + * Enqueues a closure to be executed after dependencies are completed. + * + * @param {function(function())} closure Closure with a completion callback to + * be executed. + * @param {Array.<string>=} opt_dependencies Array of dependencies. If no + * dependencies, then the the closure will be executed immediately. + * @param {string=} opt_name Task identifier. Specify to use in dependencies. + */ +AsyncUtil.Group.prototype.add = function(closure, opt_dependencies, opt_name) { + var length = Object.keys(this.addedTasks_).length; + var name = opt_name || ('(unnamed#' + (length + 1) + ')'); + + var task = { + closure: closure, + dependencies: opt_dependencies || [], + name: name + }; + + this.addedTasks_[name] = task; + this.pendingTasks_[name] = task; +}; + +/** + * Runs the enqueued closured in order of dependencies. + * + * @param {function()=} opt_onCompletion Completion callback. + */ +AsyncUtil.Group.prototype.run = function(opt_onCompletion) { + if (opt_onCompletion) + this.completionCallbacks_.push(opt_onCompletion); + this.continue_(); +}; + +/** + * Runs enqueued pending tasks whose dependencies are completed. + * @private + */ +AsyncUtil.Group.prototype.continue_ = function() { + // If all of the added tasks have finished, then call completion callbacks. + if (Object.keys(this.addedTasks_).length == + Object.keys(this.finishedTasks_).length) { + for (var index = 0; index < this.completionCallbacks_.length; index++) { + var callback = this.completionCallbacks_[index]; + callback(); + } + this.completionCallbacks_ = []; + return; + } + + for (var name in this.pendingTasks_) { + var task = this.pendingTasks_[name]; + var dependencyMissing = false; + for (var index = 0; index < task.dependencies.length; index++) { + var dependency = task.dependencies[index]; + // Check if the dependency has finished. + if (!this.finishedTasks_[dependency]) + dependencyMissing = true; + } + // All dependences finished, therefore start the task. + if (!dependencyMissing) { + delete this.pendingTasks_[task.name]; + task.closure(this.finish_.bind(this, task)); + } + } +}; + +/** + * Finishes the passed task and continues executing enqueued closures. + * + * @param {Object} task Task object. + * @private + */ +AsyncUtil.Group.prototype.finish_ = function(task) { + this.finishedTasks_[task.name] = task; + this.continue_(); +}; + +/** + * Aggregates consecutive calls and executes the closure only once instead of + * several times. The first call is always called immediately, and the next + * consecutive ones are aggregated and the closure is called only once once + * |delay| amount of time passes after the last call to run(). + * + * @param {function()} closure Closure to be aggregated. + * @param {number=} opt_delay Minimum aggregation time in milliseconds. Default + * is 50 milliseconds. + * @constructor + */ +AsyncUtil.Aggregation = function(closure, opt_delay) { + /** + * @type {number} + * @private + */ + this.delay_ = opt_delay || 50; + + /** + * @type {function()} + * @private + */ + this.closure_ = closure; + + /** + * @type {number?} + * @private + */ + this.scheduledRunsTimer_ = null; + + /** + * @type {number} + * @private + */ + this.lastRunTime_ = 0; +}; + +/** + * Runs a closure. Skips consecutive calls. The first call is called + * immediately. + */ +AsyncUtil.Aggregation.prototype.run = function() { + // If recently called, then schedule the consecutive call with a delay. + if (Date.now() - this.lastRunTime_ < this.delay_) { + this.cancelScheduledRuns_(); + this.scheduledRunsTimer_ = setTimeout(this.runImmediately_.bind(this), + this.delay_ + 1); + this.lastRunTime_ = Date.now(); + return; + } + + // Otherwise, run immediately. + this.runImmediately_(); +}; + +/** + * Calls the schedule immediately and cancels any scheduled calls. + * @private + */ +AsyncUtil.Aggregation.prototype.runImmediately_ = function() { + this.cancelScheduledRuns_(); + this.closure_(); + this.lastRunTime_ = Date.now(); +}; + +/** + * Cancels all scheduled runs (if any). + * @private + */ +AsyncUtil.Aggregation.prototype.cancelScheduledRuns_ = function() { + if (this.scheduledRunsTimer_) { + clearTimeout(this.scheduledRunsTimer_); + this.scheduledRunsTimer_ = null; + } +}; diff --git a/chromium/chrome/browser/resources/file_manager/common/js/path_util.js b/chromium/chrome/browser/resources/file_manager/common/js/path_util.js new file mode 100644 index 00000000000..3b0d4663cab --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/js/path_util.js @@ -0,0 +1,471 @@ +// 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'; + +/** + * Type of a root directory. + * @enum {string} + * @const + */ +var RootType = Object.freeze({ + // Root of local directory. + DOWNLOADS: 'downloads', + + // Root of mounted archive file. + ARCHIVE: 'archive', + + // Root of removal volume. + REMOVABLE: 'removable', + + // Root of drive directory. + DRIVE: 'drive', + + // Root for entries that is not located under RootType.DRIVE. e.g. shared + // files. + DRIVE_OTHER: 'drive_other', + + // Fake root for offline available files on the drive. + DRIVE_OFFLINE: 'drive_offline', + + // Fake root for shared files on the drive. + DRIVE_SHARED_WITH_ME: 'drive_shared_with_me', + + // Fake root for recent files on the drive. + DRIVE_RECENT: 'drive_recent' +}); + +/** + * Top directory for each root type. + * TODO(mtomasz): Deprecated. Remove this. + * @enum {string} + * @const + */ +var RootDirectory = Object.freeze({ + DOWNLOADS: '/Downloads', + ARCHIVE: '/archive', + REMOVABLE: '/removable', + DRIVE: '/drive', + DRIVE_OFFLINE: '/drive_offline', // A fake root. Not the actual filesystem. + DRIVE_SHARED_WITH_ME: '/drive_shared_with_me', // A fake root. + DRIVE_RECENT: '/drive_recent' // A fake root. +}); + +/** + * Sub root directory for Drive. "root" and "other". This is not used now. + * TODO(haruki): Add namespaces support. http://crbug.com/174233. + * @enum {string} + * @const + */ +var DriveSubRootDirectory = Object.freeze({ + ROOT: 'root', + OTHER: 'other', +}); + +var PathUtil = {}; + +/** + * The default mount point. + * TODO(mtomasz): Deprecated. Use the volume manager instead. + * @type {string} + * @const + */ +PathUtil.DEFAULT_MOUNT_POINT = '/Downloads'; + +/** + * Checks if the given path represents a special search. Fake entries in + * RootDirectory correspond to special searches. + * @param {string} path Path to check. + * @return {boolean} True if the given path represents a special search. + */ +PathUtil.isSpecialSearchRoot = function(path) { + var type = PathUtil.getRootType(path); + return type == RootType.DRIVE_OFFLINE || + type == RootType.DRIVE_SHARED_WITH_ME || + type == RootType.DRIVE_RECENT; +}; + +/** + * Checks |path| and return true if it is under Google Drive or a special + * search root which represents a special search from Google Drive. + * @param {string} path Path to check. + * @return {boolean} True if the given path represents a Drive based path. + */ +PathUtil.isDriveBasedPath = function(path) { + var rootType = PathUtil.getRootType(path); + return rootType === RootType.DRIVE || + rootType === RootType.DRIVE_SHARED_WITH_ME || + rootType === RootType.DRIVE_RECENT || + rootType === RootType.DRIVE_OFFLINE; +}; + +/** + * @param {string} path Path starting with '/'. + * @return {string} Top directory (starting with '/'). + */ +PathUtil.getTopDirectory = function(path) { + var i = path.indexOf('/', 1); + return i === -1 ? path : path.substring(0, i); +}; + +/** + * Obtains the parent path of the specified path. + * @param {string} path Path string. + * @return {string} Parent path. + */ +PathUtil.getParentDirectory = function(path) { + if (path[path.length - 1] == '/') + return PathUtil.getParentDirectory(path.substring(0, path.length - 1)); + var index = path.lastIndexOf('/'); + if (index == 0) + return '/'; + else if (index == -1) + return '.'; + return path.substring(0, index); +}; + +/** + * @param {string} path Any unix-style path (may start or not start from root). + * @return {Array.<string>} Path components. + */ +PathUtil.split = function(path) { + var fromRoot = false; + if (path[0] === '/') { + fromRoot = true; + path = path.substring(1); + } + + var components = path.split('/'); + if (fromRoot) + components[0] = '/' + components[0]; + return components; +}; + +/** + * Returns a directory part of the given |path|. In other words, the path + * without its base name. + * + * Examples: + * PathUtil.dirname('abc') -> '' + * PathUtil.dirname('a/b') -> 'a' + * PathUtil.dirname('a/b/') -> 'a/b' + * PathUtil.dirname('a/b/c') -> 'a/b' + * PathUtil.dirname('/') -> '/' + * PathUtil.dirname('/abc') -> '/' + * PathUtil.dirname('/abc/def') -> '/abc' + * PathUtil.dirname('') -> '' + * + * @param {string} path The path to be parsed. + * @return {string} The directory path. + */ +PathUtil.dirname = function(path) { + var index = path.lastIndexOf('/'); + if (index < 0) + return ''; + if (index == 0) + return '/'; + return path.substring(0, index); +}; + +/** + * Returns the base name (the last component) of the given |path|. If the + * |path| ends with '/', returns an empty component. + * + * Examples: + * PathUtil.basename('abc') -> 'abc' + * PathUtil.basename('a/b') -> 'b' + * PathUtil.basename('a/b/') -> '' + * PathUtil.basename('a/b/c') -> 'c' + * PathUtil.basename('/') -> '' + * PathUtil.basename('/abc') -> 'abc' + * PathUtil.basename('/abc/def') -> 'def' + * PathUtil.basename('') -> '' + * + * @param {string} path The path to be parsed. + * @return {string} The base name. + */ +PathUtil.basename = function(path) { + var index = path.lastIndexOf('/'); + return index >= 0 ? path.substring(index + 1) : path; +}; + +/** + * Join path components into a single path. Can be called either with a list of + * components as arguments, or with an array of components as the only argument. + * + * Examples: + * Path.join('abc', 'def') -> 'abc/def' + * Path.join('/', 'abc', 'def/ghi') -> '/abc/def/ghi' + * Path.join(['/abc/def', 'ghi']) -> '/abc/def/ghi' + * + * @return {string} Resulting path. + */ +PathUtil.join = function() { + var components; + + if (arguments.length === 1 && typeof(arguments[0]) === 'object') { + components = arguments[0]; + } else { + components = arguments; + } + + var path = ''; + for (var i = 0; i < components.length; i++) { + if (components[i][0] === '/') { + path = components[i]; + continue; + } + if (path.length === 0 || path[path.length - 1] !== '/') + path += '/'; + path += components[i]; + } + return path; +}; + +/** + * @param {string} path Path starting with '/'. + * @return {RootType} RootType.DOWNLOADS, RootType.DRIVE etc. + */ +PathUtil.getRootType = function(path) { + var rootDir = PathUtil.getTopDirectory(path); + for (var type in RootDirectory) { + if (rootDir === RootDirectory[type]) + return RootType[type]; + } +}; + +/** + * @param {string} path Any path. + * @return {string} The root path. + */ +PathUtil.getRootPath = function(path) { + var type = PathUtil.getRootType(path); + + if (type == RootType.DOWNLOADS || type == RootType.DRIVE_OFFLINE || + type == RootType.DRIVE_SHARED_WITH_ME || type == RootType.DRIVE_RECENT) + return PathUtil.getTopDirectory(path); + + if (type == RootType.DRIVE || type == RootType.ARCHIVE || + type == RootType.REMOVABLE) { + var components = PathUtil.split(path); + if (components.length > 1) { + return PathUtil.join(components[0], components[1]); + } else { + return components[0]; + } + } + + return '/'; +}; + +/** + * @param {string} path A path. + * @return {boolean} True if it is a path to the root. + */ +PathUtil.isRootPath = function(path) { + return PathUtil.getRootPath(path) === path; +}; + +/** + * @param {string} path A root path. + * @return {boolean} True if the given path is root and user can unmount it. + */ +PathUtil.isUnmountableByUser = function(path) { + if (!PathUtil.isRootPath(path)) + return false; + + var type = PathUtil.getRootType(path); + return (type == RootType.ARCHIVE || type == RootType.REMOVABLE); +}; + +/** + * @param {string} parent_path The parent path. + * @param {string} child_path The child path. + * @return {boolean} True if |parent_path| is parent file path of |child_path|. + */ +PathUtil.isParentPath = function(parent_path, child_path) { + if (!parent_path || parent_path.length == 0 || + !child_path || child_path.length == 0) + return false; + + if (parent_path[parent_path.length - 1] != '/') + parent_path += '/'; + + if (child_path[child_path.length - 1] != '/') + child_path += '/'; + + return child_path.indexOf(parent_path) == 0; +}; + +/** + * Return the localized name for the root. + * TODO(hirono): Support all RootTypes and stop to use paths. + * + * @param {string|RootType} path The full path of the root (starting with slash) + * or root type. + * @return {string} The localized name. + */ +PathUtil.getRootLabel = function(path) { + var str = function(id) { + return loadTimeData.getString(id); + }; + + if (path === RootDirectory.DOWNLOADS) + return str('DOWNLOADS_DIRECTORY_LABEL'); + + if (path === RootDirectory.ARCHIVE) + return str('ARCHIVE_DIRECTORY_LABEL'); + if (PathUtil.isParentPath(RootDirectory.ARCHIVE, path)) + return path.substring(RootDirectory.ARCHIVE.length + 1); + + if (path === RootDirectory.REMOVABLE) + return str('REMOVABLE_DIRECTORY_LABEL'); + if (PathUtil.isParentPath(RootDirectory.REMOVABLE, path)) + return path.substring(RootDirectory.REMOVABLE.length + 1); + + // TODO(haruki): Add support for "drive/root" and "drive/other". + if (path === RootDirectory.DRIVE + '/' + DriveSubRootDirectory.ROOT) + return str('DRIVE_MY_DRIVE_LABEL'); + + if (path === RootDirectory.DRIVE_OFFLINE) + return str('DRIVE_OFFLINE_COLLECTION_LABEL'); + + if (path === RootDirectory.DRIVE_SHARED_WITH_ME || + path === RootType.DRIVE_SHARED_WITH_ME) + return str('DRIVE_SHARED_WITH_ME_COLLECTION_LABEL'); + + if (path === RootDirectory.DRIVE_RECENT) + return str('DRIVE_RECENT_COLLECTION_LABEL'); + + return path; +}; + +/** + * Return the label of the folder to be shown. Eg. + * - '/foo/bar/baz' -> 'baz' + * - '/hoge/fuga/ -> 'fuga' + * If the directory is root, returns the root label, which is same as + * PathUtil.getRootLabel(). + * + * @param {string} directoryPath The full path of the folder. + * @return {string} The label to be shown. + */ +PathUtil.getFolderLabel = function(directoryPath) { + var label = ''; + if (PathUtil.isRootPath(directoryPath)) + label = PathUtil.getRootLabel(directoryPath); + + if (label && label != directoryPath) + return label; + + var matches = directoryPath.match(/([^\/]*)[\/]?$/); + if (matches[1]) + return matches[1]; + + return directoryPath; +}; + +/** + * Returns if the given path can be a target path of folder shortcut. + * + * @param {string} directoryPath Directory path to be checked. + * @return {boolean} True if the path can be a target path of the shortcut. + */ +PathUtil.isEligibleForFolderShortcut = function(directoryPath) { + return !PathUtil.isSpecialSearchRoot(directoryPath) && + !PathUtil.isRootPath(directoryPath) && + PathUtil.isDriveBasedPath(directoryPath); +}; + +/** + * Extracts the extension of the path. + * + * Examples: + * PathUtil.splitExtension('abc.ext') -> ['abc', '.ext'] + * PathUtil.splitExtension('a/b/abc.ext') -> ['a/b/abc', '.ext'] + * PathUtil.splitExtension('a/b') -> ['a/b', ''] + * PathUtil.splitExtension('.cshrc') -> ['', '.cshrc'] + * PathUtil.splitExtension('a/b.backup/hoge') -> ['a/b.backup/hoge', ''] + * + * @param {string} path Path to be extracted. + * @return {Array.<string>} Filename and extension of the given path. + */ +PathUtil.splitExtension = function(path) { + var dotPosition = path.lastIndexOf('.'); + if (dotPosition <= path.lastIndexOf('/')) + dotPosition = -1; + + var filename = dotPosition != -1 ? path.substr(0, dotPosition) : path; + var extension = dotPosition != -1 ? path.substr(dotPosition) : ''; + return [filename, extension]; +}; + +/** + * Obtains location information from a path. + * + * @param {!VolumeInfo} volumeInfo Volume containing an entry pointed by path. + * @param {string} fullPath Full path. + * @return {EntryLocation} Location information. + */ +PathUtil.getLocationInfo = function(volumeInfo, fullPath) { + var rootPath; + var rootType; + if (volumeInfo.volumeType === util.VolumeType.DRIVE) { + // If the volume is drive, root path can be either mountPath + '/root' or + // mountPath + '/other'. + if ((fullPath + '/').indexOf(volumeInfo.mountPath + '/root/') === 0) { + rootPath = volumeInfo.mountPath + '/root'; + rootType = RootType.DRIVE; + } else if ((fullPath + '/').indexOf( + volumeInfo.mountPath + '/other/') === 0) { + rootPath = volumeInfo.mountPath + '/other'; + rootType = RootType.DRIVE_OTHER; + } else { + throw new Error(fullPath + ' is an invalid drive path.'); + } + } else { + // Otherwise, root path is same with a mount path of the volume. + rootPath = volumeInfo.mountPath; + switch (volumeInfo.volumeType) { + case util.VolumeType.DOWNLOADS: rootType = RootType.DOWNLOADS; break; + case util.VolumeType.REMOVABLE: rootType = RootType.REMOVABLE; break; + case util.VolumeType.ARCHIVE: rootType = RootType.ARCHIVE; break; + default: throw new Error( + 'Invalid volume type: ' + volumeInfo.volumeType); + } + } + var isRootEntry = (fullPath.substr(0, rootPath.length) || '/') === fullPath; + return new EntryLocation(volumeInfo, rootType, isRootEntry); +}; + +/** + * Location information which shows where the path points in FileManager's + * file system. + * + * @param {!VolumeInfo} volumeInfo Volume information. + * @param {RootType} rootType Root type. + * @param {boolean} isRootEntry Whether the entry is root entry or not. + * @constructor + */ +function EntryLocation(volumeInfo, rootType, isRootEntry) { + /** + * Volume information. + * @type {!VolumeInfo} + */ + this.volumeInfo = volumeInfo; + + /** + * Root type. + * @type {RootType} + */ + this.rootType = rootType; + + /** + * Whether the entry is root entry or not. + * @type {boolean} + */ + this.isRootEntry = isRootEntry; + + Object.freeze(this); +} diff --git a/chromium/chrome/browser/resources/file_manager/common/js/progress_center_common.js b/chromium/chrome/browser/resources/file_manager/common/js/progress_center_common.js new file mode 100644 index 00000000000..f19d6cbb90b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/js/progress_center_common.js @@ -0,0 +1,149 @@ +// Copyright 2013 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'; + +/** + * Event of the ProgressCenter class. + * @enum {string} + * @const + */ +var ProgressCenterEvent = Object.freeze({ + /** + * Background page notifies item update to application windows. + */ + ITEM_UPDATED: 'itemUpdated', + + /** + * Background page notifies all the items are cleared. + */ + RESET: 'reset' +}); + +/** + * State of progress items. + * @enum {string} + * @const + */ +var ProgressItemState = Object.freeze({ + PROGRESSING: 'progressing', + COMPLETED: 'completed', + ERROR: 'error', + CANCELED: 'canceled' +}); + +/** + * Type of progress items. + * @enum {string} + * @const + */ +var ProgressItemType = Object.freeze({ + // The item is file copy operation. + COPY: 'copy', + // The item is file move operation. + MOVE: 'move', + // The item is file delete opeartion. + DELETE: 'delete', + // The item is file zip operation. + ZIP: 'zip', + // The item is general file transfer operation. + // This is used for the mixed operation of summarized item. + TRANSFER: 'transfer' +}); + +/** + * Item of the progress center. + * @constructor + */ +var ProgressCenterItem = function() { + /** + * Item ID. + * @type {string} + * @private + */ + this.id_ = null; + + /** + * State of the progress item. + * @type {ProgressItemState} + */ + this.state = ProgressItemState.PROGRESSING; + + /** + * Message of the progress item. + * @type {string} + */ + this.message = ''; + + /** + * Max value of the progress. + * @type {number} + */ + this.progressMax = 0; + + /** + * Current value of the progress. + * @type {number} + */ + this.progressValue = 0; + + /** + * Type of progress item. + * @type {ProgressItemType} + */ + this.type = null; + + /** + * Whether the item is summarized item or not. + * @type {boolean} + */ + this.summarized = false; + + /** + * Callback function to cancel the item. + * @type {function()} + */ + this.cancelCallback = null; + + Object.seal(this); +}; + +ProgressCenterItem.prototype = { + /** + * Setter of Item ID. + * @param {string} value New value of ID. + */ + set id(value) { + if (!this.id_) + this.id_ = value; + else + console.error('The ID is already set. (current ID: ' + this.id_ + ')'); + }, + + /** + * Getter of Item ID. + * @return {string} Item ID. + */ + get id() { + return this.id_; + }, + + /** + * Gets progress rate in percent. + * @return {number} Progress rate in percent. + */ + get progressRateInPercent() { + return ~~(100 * this.progressValue / this.progressMax); + }, + + /** + * Whether the item can be canceled or not. + * @return {boolean} True if the item can be canceled. + */ + get cancelable() { + return !!(this.state == ProgressItemState.PROGRESSING && + this.cancelCallback && + !this.summarized); + } +}; diff --git a/chromium/chrome/browser/resources/file_manager/common/js/util.js b/chromium/chrome/browser/resources/file_manager/common/js/util.js new file mode 100644 index 00000000000..07df6b81d88 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/common/js/util.js @@ -0,0 +1,1259 @@ +// 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'; + +/** + * Namespace for utility functions. + */ +var util = {}; + +/** + * Returns a function that console.log's its arguments, prefixed by |msg|. + * + * @param {string} msg The message prefix to use in the log. + * @param {function(...string)=} opt_callback A function to invoke after + * logging. + * @return {function(...string)} Function that logs. + */ +util.flog = function(msg, opt_callback) { + return function() { + var ary = Array.apply(null, arguments); + console.log(msg + ': ' + ary.join(', ')); + if (opt_callback) + opt_callback.apply(null, arguments); + }; +}; + +/** + * Returns a function that throws an exception that includes its arguments + * prefixed by |msg|. + * + * @param {string} msg The message prefix to use in the exception. + * @return {function(...string)} Function that throws. + */ +util.ferr = function(msg) { + return function() { + var ary = Array.apply(null, arguments); + throw new Error(msg + ': ' + ary.join(', ')); + }; +}; + +/** + * Install a sensible toString() on the FileError object. + * + * FileError.prototype.code is a numeric code describing the cause of the + * error. The FileError constructor has a named property for each possible + * error code, but provides no way to map the code to the named property. + * This toString() implementation fixes that. + */ +util.installFileErrorToString = function() { + FileError.prototype.toString = function() { + return '[object FileError: ' + util.getFileErrorMnemonic(this.code) + ']'; + }; +}; + +/** + * @param {number} code The file error code. + * @return {string} The file error mnemonic. + */ +util.getFileErrorMnemonic = function(code) { + for (var key in FileError) { + if (key.search(/_ERR$/) != -1 && FileError[key] == code) + return key; + } + + return code; +}; + +/** + * @param {number} code File error code (from FileError object). + * @return {string} Translated file error string. + */ +util.getFileErrorString = function(code) { + for (var key in FileError) { + var match = /(.*)_ERR$/.exec(key); + if (match && FileError[key] == code) { + // This would convert 1 to 'NOT_FOUND'. + code = match[1]; + break; + } + } + console.warn('File error: ' + code); + return loadTimeData.getString('FILE_ERROR_' + code) || + loadTimeData.getString('FILE_ERROR_GENERIC'); +}; + +/** + * @param {string} str String to escape. + * @return {string} Escaped string. + */ +util.htmlEscape = function(str) { + return str.replace(/[<>&]/g, function(entity) { + switch (entity) { + case '<': return '<'; + case '>': return '>'; + case '&': return '&'; + } + }); +}; + +/** + * @param {string} str String to unescape. + * @return {string} Unescaped string. + */ +util.htmlUnescape = function(str) { + return str.replace(/&(lt|gt|amp);/g, function(entity) { + switch (entity) { + case '<': return '<'; + case '>': return '>'; + case '&': return '&'; + } + }); +}; + +/** + * Iterates the entries contained by dirEntry, and invokes callback once for + * each entry. On completion, successCallback will be invoked. + * + * @param {DirectoryEntry} dirEntry The entry of the directory. + * @param {function(Entry, function())} callback Invoked for each entry. + * @param {function()} successCallback Invoked on completion. + * @param {function(FileError)} errorCallback Invoked if an error is found on + * directory entry reading. + */ +util.forEachDirEntry = function( + dirEntry, callback, successCallback, errorCallback) { + var reader = dirEntry.createReader(); + var iterate = function() { + reader.readEntries(function(entries) { + if (entries.length == 0) { + successCallback(); + return; + } + + AsyncUtil.forEach( + entries, + function(forEachCallback, entry) { + // Do not pass index nor entries. + callback(entry, forEachCallback); + }, + iterate); + }, errorCallback); + }; + iterate(); +}; + +/** + * Reads contents of directory. + * @param {DirectoryEntry} root Root entry. + * @param {string} path Directory path. + * @param {function(Array.<Entry>)} callback List of entries passed to callback. + */ +util.readDirectory = function(root, path, callback) { + var onError = function(e) { + callback([], e); + }; + root.getDirectory(path, {create: false}, function(entry) { + var reader = entry.createReader(); + var r = []; + var readNext = function() { + reader.readEntries(function(results) { + if (results.length == 0) { + callback(r, null); + return; + } + r.push.apply(r, results); + readNext(); + }, onError); + }; + readNext(); + }, onError); +}; + +/** + * Utility function to resolve multiple directories with a single call. + * + * The successCallback will be invoked once for each directory object + * found. The errorCallback will be invoked once for each + * path that could not be resolved. + * + * The successCallback is invoked with a null entry when all paths have + * been processed. + * + * @param {DirEntry} dirEntry The base directory. + * @param {Object} params The parameters to pass to the underlying + * getDirectory calls. + * @param {Array.<string>} paths The list of directories to resolve. + * @param {function(!DirEntry)} successCallback The function to invoke for + * each DirEntry found. Also invoked once with null at the end of the + * process. + * @param {function(FileError)} errorCallback The function to invoke + * for each path that cannot be resolved. + */ +util.getDirectories = function(dirEntry, params, paths, successCallback, + errorCallback) { + + // Copy the params array, since we're going to destroy it. + params = [].slice.call(params); + + var onComplete = function() { + successCallback(null); + }; + + var getNextDirectory = function() { + var path = paths.shift(); + if (!path) + return onComplete(); + + dirEntry.getDirectory( + path, params, + function(entry) { + successCallback(entry); + getNextDirectory(); + }, + function(err) { + errorCallback(err); + getNextDirectory(); + }); + }; + + getNextDirectory(); +}; + +/** + * Utility function to resolve multiple files with a single call. + * + * The successCallback will be invoked once for each directory object + * found. The errorCallback will be invoked once for each + * path that could not be resolved. + * + * The successCallback is invoked with a null entry when all paths have + * been processed. + * + * @param {DirEntry} dirEntry The base directory. + * @param {Object} params The parameters to pass to the underlying + * getFile calls. + * @param {Array.<string>} paths The list of files to resolve. + * @param {function(!FileEntry)} successCallback The function to invoke for + * each FileEntry found. Also invoked once with null at the end of the + * process. + * @param {function(FileError)} errorCallback The function to invoke + * for each path that cannot be resolved. + */ +util.getFiles = function(dirEntry, params, paths, successCallback, + errorCallback) { + // Copy the params array, since we're going to destroy it. + params = [].slice.call(params); + + var onComplete = function() { + successCallback(null); + }; + + var getNextFile = function() { + var path = paths.shift(); + if (!path) + return onComplete(); + + dirEntry.getFile( + path, params, + function(entry) { + successCallback(entry); + getNextFile(); + }, + function(err) { + errorCallback(err); + getNextFile(); + }); + }; + + getNextFile(); +}; + +/** + * Resolve a path to either a DirectoryEntry or a FileEntry, regardless of + * whether the path is a directory or file. + * + * @param {DirectoryEntry} root The root of the filesystem to search. + * @param {string} path The path to be resolved. + * @param {function(Entry)} resultCallback Called back when a path is + * successfully resolved. Entry will be either a DirectoryEntry or + * a FileEntry. + * @param {function(FileError)} errorCallback Called back if an unexpected + * error occurs while resolving the path. + */ +util.resolvePath = function(root, path, resultCallback, errorCallback) { + if (path == '' || path == '/') { + resultCallback(root); + return; + } + + root.getFile( + path, {create: false}, + resultCallback, + function(err) { + if (err.code == FileError.TYPE_MISMATCH_ERR) { + // Bah. It's a directory, ask again. + root.getDirectory( + path, {create: false}, + resultCallback, + errorCallback); + } else { + errorCallback(err); + } + }); +}; + +/** + * Locate the file referred to by path, creating directories or the file + * itself if necessary. + * @param {DirEntry} root The root entry. + * @param {string} path The file path. + * @param {function(FileEntry)} successCallback The callback. + * @param {function(FileError)} errorCallback The callback. + */ +util.getOrCreateFile = function(root, path, successCallback, errorCallback) { + var dirname = null; + var basename = null; + + var onDirFound = function(dirEntry) { + dirEntry.getFile(basename, { create: true }, + successCallback, errorCallback); + }; + + var i = path.lastIndexOf('/'); + if (i > -1) { + dirname = path.substr(0, i); + basename = path.substr(i + 1); + } else { + basename = path; + } + + if (!dirname) { + onDirFound(root); + return; + } + + util.getOrCreateDirectory(root, dirname, onDirFound, errorCallback); +}; + +/** + * Locate the directory referred to by path, creating directories along the + * way. + * @param {DirEntry} root The root entry. + * @param {string} path The directory path. + * @param {function(FileEntry)} successCallback The callback. + * @param {function(FileError)} errorCallback The callback. + */ +util.getOrCreateDirectory = function(root, path, successCallback, + errorCallback) { + var names = path.split('/'); + + var getOrCreateNextName = function(dir) { + if (!names.length) + return successCallback(dir); + + var name; + do { + name = names.shift(); + } while (!name || name == '.'); + + dir.getDirectory(name, { create: true }, getOrCreateNextName, + errorCallback); + }; + + getOrCreateNextName(root); +}; + +/** + * Renames the entry to newName. + * @param {Entry} entry The entry to be renamed. + * @param {string} newName The new name. + * @param {function(Entry)} successCallback Callback invoked when the rename + * is successfully done. + * @param {function(FileError)} errorCallback Callback invoked when an error + * is found. + */ +util.rename = function(entry, newName, successCallback, errorCallback) { + entry.getParent(function(parent) { + // Before moving, we need to check if there is an existing entry at + // parent/newName, since moveTo will overwrite it. + // Note that this way has some timing issue. After existing check, + // a new entry may be create on background. However, there is no way not to + // overwrite the existing file, unfortunately. The risk should be low, + // assuming the unsafe period is very short. + (entry.isFile ? parent.getFile : parent.getDirectory).call( + parent, newName, {create: false}, + function(entry) { + // The entry with the name already exists. + errorCallback(util.createFileError(FileError.PATH_EXISTS_ERR)); + }, + function(error) { + if (error.code != FileError.NOT_FOUND_ERR) { + // Unexpected error is found. + errorCallback(error); + return; + } + + // No existing entry is found. + entry.moveTo(parent, newName, successCallback, errorCallback); + }); + }, errorCallback); +}; + +/** + * Remove a file or a directory. + * @param {Entry} entry The entry to remove. + * @param {function()} onSuccess The success callback. + * @param {function(FileError)} onError The error callback. + */ +util.removeFileOrDirectory = function(entry, onSuccess, onError) { + if (entry.isDirectory) + entry.removeRecursively(onSuccess, onError); + else + entry.remove(onSuccess, onError); +}; + +/** + * Checks if an entry exists at |relativePath| in |dirEntry|. + * If exists, tries to deduplicate the path by inserting parenthesized number, + * such as " (1)", before the extension. If it still exists, tries the + * deduplication again by increasing the number up to 10 times. + * For example, suppose "file.txt" is given, "file.txt", "file (1).txt", + * "file (2).txt", ..., "file (9).txt" will be tried. + * + * @param {DirectoryEntry} dirEntry The target directory entry. + * @param {string} relativePath The path to be deduplicated. + * @param {function(string)} onSuccess Called with the deduplicated path on + * success. + * @param {function(FileError)} onError Called on error. + */ +util.deduplicatePath = function(dirEntry, relativePath, onSuccess, onError) { + // The trial is up to 10. + var MAX_RETRY = 10; + + // Crack the path into three part. The parenthesized number (if exists) will + // be replaced by incremented number for retry. For example, suppose + // |relativePath| is "file (10).txt", the second check path will be + // "file (11).txt". + var match = /^(.*?)(?: \((\d+)\))?(\.[^.]*?)?$/.exec(relativePath); + var prefix = match[1]; + var copyNumber = match[2] ? parseInt(match[2], 10) : 0; + var ext = match[3] ? match[3] : ''; + + // The path currently checking the existence. + var trialPath = relativePath; + + var onNotResolved = function(err) { + // We expect to be unable to resolve the target file, since we're going + // to create it during the copy. However, if the resolve fails with + // anything other than NOT_FOUND, that's trouble. + if (err.code != FileError.NOT_FOUND_ERR) { + onError(err); + return; + } + + // Found a path that doesn't exist. + onSuccess(trialPath); + }; + + var numRetry = MAX_RETRY; + var onResolved = function(entry) { + if (--numRetry == 0) { + // Hit the limit of the number of retrial. + // Note that we cannot create FileError object directly, so here we use + // Object.create instead. + onError(util.createFileError(FileError.PATH_EXISTS_ERR)); + return; + } + + ++copyNumber; + trialPath = prefix + ' (' + copyNumber + ')' + ext; + util.resolvePath(dirEntry, trialPath, onResolved, onNotResolved); + }; + + // Check to see if the target exists. + util.resolvePath(dirEntry, trialPath, onResolved, onNotResolved); +}; + +/** + * Convert a number of bytes into a human friendly format, using the correct + * number separators. + * + * @param {number} bytes The number of bytes. + * @return {string} Localized string. + */ +util.bytesToString = function(bytes) { + // Translation identifiers for size units. + var UNITS = ['SIZE_BYTES', + 'SIZE_KB', + 'SIZE_MB', + 'SIZE_GB', + 'SIZE_TB', + 'SIZE_PB']; + + // Minimum values for the units above. + var STEPS = [0, + Math.pow(2, 10), + Math.pow(2, 20), + Math.pow(2, 30), + Math.pow(2, 40), + Math.pow(2, 50)]; + + var str = function(n, u) { + // TODO(rginda): Switch to v8Locale's number formatter when it's + // available. + return strf(u, n.toLocaleString()); + }; + + var fmt = function(s, u) { + var rounded = Math.round(bytes / s * 10) / 10; + return str(rounded, u); + }; + + // Less than 1KB is displayed like '80 bytes'. + if (bytes < STEPS[1]) { + return str(bytes, UNITS[0]); + } + + // Up to 1MB is displayed as rounded up number of KBs. + if (bytes < STEPS[2]) { + var rounded = Math.ceil(bytes / STEPS[1]); + return str(rounded, UNITS[1]); + } + + // This loop index is used outside the loop if it turns out |bytes| + // requires the largest unit. + var i; + + for (i = 2 /* MB */; i < UNITS.length - 1; i++) { + if (bytes < STEPS[i + 1]) + return fmt(STEPS[i], UNITS[i]); + } + + return fmt(STEPS[i], UNITS[i]); +}; + +/** + * Utility function to read specified range of bytes from file + * @param {File} file The file to read. + * @param {number} begin Starting byte(included). + * @param {number} end Last byte(excluded). + * @param {function(File, Uint8Array)} callback Callback to invoke. + * @param {function(FileError)} onError Error handler. + */ +util.readFileBytes = function(file, begin, end, callback, onError) { + var fileReader = new FileReader(); + fileReader.onerror = onError; + fileReader.onloadend = function() { + callback(file, new ByteReader(fileReader.result)); + }; + fileReader.readAsArrayBuffer(file.slice(begin, end)); +}; + +/** + * Write a blob to a file. + * Truncates the file first, so the previous content is fully overwritten. + * @param {FileEntry} entry File entry. + * @param {Blob} blob The blob to write. + * @param {function(Event)} onSuccess Completion callback. The first argument is + * a 'writeend' event. + * @param {function(FileError)} onError Error handler. + */ +util.writeBlobToFile = function(entry, blob, onSuccess, onError) { + var truncate = function(writer) { + writer.onerror = onError; + writer.onwriteend = write.bind(null, writer); + writer.truncate(0); + }; + + var write = function(writer) { + writer.onwriteend = onSuccess; + writer.write(blob); + }; + + entry.createWriter(truncate, onError); +}; + +/** + * Returns a string '[Ctrl-][Alt-][Shift-][Meta-]' depending on the event + * modifiers. Convenient for writing out conditions in keyboard handlers. + * + * @param {Event} event The keyboard event. + * @return {string} Modifiers. + */ +util.getKeyModifiers = function(event) { + return (event.ctrlKey ? 'Ctrl-' : '') + + (event.altKey ? 'Alt-' : '') + + (event.shiftKey ? 'Shift-' : '') + + (event.metaKey ? 'Meta-' : ''); +}; + +/** + * @param {HTMLElement} element Element to transform. + * @param {Object} transform Transform object, + * contains scaleX, scaleY and rotate90 properties. + */ +util.applyTransform = function(element, transform) { + element.style.webkitTransform = + transform ? 'scaleX(' + transform.scaleX + ') ' + + 'scaleY(' + transform.scaleY + ') ' + + 'rotate(' + transform.rotate90 * 90 + 'deg)' : + ''; +}; + +/** + * Makes filesystem: URL from the path. + * @param {string} path File or directory path. + * @return {string} URL. + */ +util.makeFilesystemUrl = function(path) { + path = path.split('/').map(encodeURIComponent).join('/'); + var prefix = 'external'; + return 'filesystem:' + chrome.runtime.getURL(prefix + path); +}; + +/** + * Extracts path from filesystem: URL. + * @param {string} url Filesystem URL. + * @return {string} The path. + */ +util.extractFilePath = function(url) { + var match = + /^filesystem:[\w-]*:\/\/[\w]*\/(external|persistent|temporary)(\/.*)$/. + exec(url); + var path = match && match[2]; + if (!path) return null; + return decodeURIComponent(path); +}; + +/** + * Traverses a directory tree whose root is the given entry, and invokes + * callback for each entry. Upon completion, successCallback will be called. + * On error, errorCallback will be called. + * + * @param {Entry} entry The root entry. + * @param {function(Entry):boolean} callback Callback invoked for each entry. + * If this returns false, entries under it won't be traversed. Note that + * its siblings (and their children) will be still traversed. + * @param {function()} successCallback Called upon successful completion. + * @param {function(error)} errorCallback Called upon error. + */ +util.traverseTree = function(entry, callback, successCallback, errorCallback) { + if (!callback(entry)) { + successCallback(); + return; + } + + util.forEachDirEntry( + entry, + function(child, iterationCallback) { + util.traverseTree(child, callback, iterationCallback, errorCallback); + }, + successCallback, + errorCallback); +}; + +/** + * A shortcut function to create a child element with given tag and class. + * + * @param {HTMLElement} parent Parent element. + * @param {string=} opt_className Class name. + * @param {string=} opt_tag Element tag, DIV is omitted. + * @return {Element} Newly created element. + */ +util.createChild = function(parent, opt_className, opt_tag) { + var child = parent.ownerDocument.createElement(opt_tag || 'div'); + if (opt_className) + child.className = opt_className; + parent.appendChild(child); + return child; +}; + +/** + * Update the app state. + * + * @param {string} path Path to be put in the address bar after the hash. + * If null the hash is left unchanged. + * @param {string|Object=} opt_param Search parameter. Used directly if string, + * stringified if object. If omitted the search query is left unchanged. + */ +util.updateAppState = function(path, opt_param) { + window.appState = window.appState || {}; + if (typeof opt_param == 'string') + window.appState.params = {}; + else if (typeof opt_param == 'object') + window.appState.params = opt_param; + if (path) + window.appState.defaultPath = path; + util.saveAppState(); + return; +}; + +/** + * Return a translated string. + * + * Wrapper function to make dealing with translated strings more concise. + * Equivalent to loadTimeData.getString(id). + * + * @param {string} id The id of the string to return. + * @return {string} The translated string. + */ +function str(id) { + return loadTimeData.getString(id); +} + +/** + * Return a translated string with arguments replaced. + * + * Wrapper function to make dealing with translated strings more concise. + * Equivalent to loadTimeData.getStringF(id, ...). + * + * @param {string} id The id of the string to return. + * @param {...string} var_args The values to replace into the string. + * @return {string} The translated string with replaced values. + */ +function strf(id, var_args) { + return loadTimeData.getStringF.apply(loadTimeData, arguments); +} + +/** + * Adapter object that abstracts away the the difference between Chrome app APIs + * v1 and v2. Is only necessary while the migration to v2 APIs is in progress. + * TODO(mtomasz): Clean up this. crbug.com/240606. + */ +util.platform = { + /** + * @return {boolean} True if Files.app is running as an open files or a select + * folder dialog. False otherwise. + */ + runningInBrowser: function() { + return !window.appID; + }, + + /** + * @param {function(Object)} callback Function accepting a preference map. + */ + getPreferences: function(callback) { + chrome.storage.local.get(callback); + }, + + /** + * @param {string} key Preference name. + * @param {function(string)} callback Function accepting the preference value. + */ + getPreference: function(key, callback) { + chrome.storage.local.get(key, function(items) { + callback(items[key]); + }); + }, + + /** + * @param {string} key Preference name. + * @param {string|Object} value Preference value. + * @param {function()=} opt_callback Completion callback. + */ + setPreference: function(key, value, opt_callback) { + if (typeof value != 'string') + value = JSON.stringify(value); + + var items = {}; + items[key] = value; + chrome.storage.local.set(items, opt_callback); + } +}; + +/** + * Attach page load handler. + * @param {function()} handler Application-specific load handler. + */ +util.addPageLoadHandler = function(handler) { + document.addEventListener('DOMContentLoaded', function() { + handler(); + }); +}; + +/** + * Save app launch data to the local storage. + */ +util.saveAppState = function() { + if (window.appState) + util.platform.setPreference(window.appID, window.appState); +}; + +/** + * AppCache is a persistent timestamped key-value storage backed by + * HTML5 local storage. + * + * It is not designed for frequent access. In order to avoid costly + * localStorage iteration all data is kept in a single localStorage item. + * There is no in-memory caching, so concurrent access is _almost_ safe. + * + * TODO(kaznacheev) Reimplement this based on Indexed DB. + */ +util.AppCache = function() {}; + +/** + * Local storage key. + */ +util.AppCache.KEY = 'AppCache'; + +/** + * Max number of items. + */ +util.AppCache.CAPACITY = 100; + +/** + * Default lifetime. + */ +util.AppCache.LIFETIME = 30 * 24 * 60 * 60 * 1000; // 30 days. + +/** + * @param {string} key Key. + * @param {function(number)} callback Callback accepting a value. + */ +util.AppCache.getValue = function(key, callback) { + util.AppCache.read_(function(map) { + var entry = map[key]; + callback(entry && entry.value); + }); +}; + +/** + * Update the cache. + * + * @param {string} key Key. + * @param {string} value Value. Remove the key if value is null. + * @param {number=} opt_lifetime Maximum time to keep an item (in milliseconds). + */ +util.AppCache.update = function(key, value, opt_lifetime) { + util.AppCache.read_(function(map) { + if (value != null) { + map[key] = { + value: value, + expire: Date.now() + (opt_lifetime || util.AppCache.LIFETIME) + }; + } else if (key in map) { + delete map[key]; + } else { + return; // Nothing to do. + } + util.AppCache.cleanup_(map); + util.AppCache.write_(map); + }); +}; + +/** + * @param {function(Object)} callback Callback accepting a map of timestamped + * key-value pairs. + * @private + */ +util.AppCache.read_ = function(callback) { + util.platform.getPreference(util.AppCache.KEY, function(json) { + if (json) { + try { + callback(JSON.parse(json)); + } catch (e) { + // The local storage item somehow got messed up, start fresh. + } + } + callback({}); + }); +}; + +/** + * @param {Object} map A map of timestamped key-value pairs. + * @private + */ +util.AppCache.write_ = function(map) { + util.platform.setPreference(util.AppCache.KEY, JSON.stringify(map)); +}; + +/** + * Remove over-capacity and obsolete items. + * + * @param {Object} map A map of timestamped key-value pairs. + * @private + */ +util.AppCache.cleanup_ = function(map) { + // Sort keys by ascending timestamps. + var keys = []; + for (var key in map) { + if (map.hasOwnProperty(key)) + keys.push(key); + } + keys.sort(function(a, b) { return map[a].expire > map[b].expire }); + + var cutoff = Date.now(); + + var obsolete = 0; + while (obsolete < keys.length && + map[keys[obsolete]].expire < cutoff) { + obsolete++; + } + + var overCapacity = Math.max(0, keys.length - util.AppCache.CAPACITY); + + var itemsToDelete = Math.max(obsolete, overCapacity); + for (var i = 0; i != itemsToDelete; i++) { + delete map[keys[i]]; + } +}; + +/** + * Load an image. + * + * @param {Image} image Image element. + * @param {string} url Source url. + * @param {Object=} opt_options Hash array of options, eg. width, height, + * maxWidth, maxHeight, scale, cache. + * @param {function()=} opt_isValid Function returning false iff the task + * is not valid and should be aborted. + * @return {?number} Task identifier or null if fetched immediately from + * cache. + */ +util.loadImage = function(image, url, opt_options, opt_isValid) { + return ImageLoaderClient.loadToImage(url, + image, + opt_options || {}, + function() {}, + function() { image.onerror(); }, + opt_isValid); +}; + +/** + * Cancels loading an image. + * @param {number} taskId Task identifier returned by util.loadImage(). + */ +util.cancelLoadImage = function(taskId) { + ImageLoaderClient.getInstance().cancel(taskId); +}; + +/** + * Finds proerty descriptor in the object prototype chain. + * @param {Object} object The object. + * @param {string} propertyName The property name. + * @return {Object} Property descriptor. + */ +util.findPropertyDescriptor = function(object, propertyName) { + for (var p = object; p; p = Object.getPrototypeOf(p)) { + var d = Object.getOwnPropertyDescriptor(p, propertyName); + if (d) + return d; + } + return null; +}; + +/** + * Calls inherited property setter (useful when property is + * overriden). + * @param {Object} object The object. + * @param {string} propertyName The property name. + * @param {*} value Value to set. + */ +util.callInheritedSetter = function(object, propertyName, value) { + var d = util.findPropertyDescriptor(Object.getPrototypeOf(object), + propertyName); + d.set.call(object, value); +}; + +/** + * Returns true if the board of the device matches the given prefix. + * @param {string} boardPrefix The board prefix to match against. + * (ex. "x86-mario". Prefix is used as the actual board name comes with + * suffix like "x86-mario-something". + * @return {boolean} True if the board of the device matches the given prefix. + */ +util.boardIs = function(boardPrefix) { + // The board name should be lower-cased, but making it case-insensitive for + // backward compatibility just in case. + var board = str('CHROMEOS_RELEASE_BOARD'); + var pattern = new RegExp('^' + boardPrefix, 'i'); + return board.match(pattern) != null; +}; + +/** + * Adds an isFocused method to the current window object. + */ +util.addIsFocusedMethod = function() { + var focused = true; + + window.addEventListener('focus', function() { + focused = true; + }); + + window.addEventListener('blur', function() { + focused = false; + }); + + /** + * @return {boolean} True if focused. + */ + window.isFocused = function() { + return focused; + }; +}; + +/** + * Makes a redirect to the specified Files.app's window from another window. + * @param {number} id Window id. + * @param {string} url Target url. + * @return {boolean} True if the window has been found. False otherwise. + */ +util.redirectMainWindow = function(id, url) { + // TODO(mtomasz): Implement this for Apps V2, once the photo importer is + // restored. + return false; +}; + +/** + * Checks, if the Files.app's window is in a full screen mode. + * + * @param {AppWindow} appWindow App window to be maximized. + * @return {boolean} True if the full screen mode is enabled. + */ +util.isFullScreen = function(appWindow) { + if (appWindow) { + return appWindow.isFullscreen(); + } else { + console.error('App window not passed. Unable to check status of ' + + 'the full screen mode.'); + return false; + } +}; + +/** + * Toggles the full screen mode. + * + * @param {AppWindow} appWindow App window to be maximized. + * @param {boolean} enabled True for enabling, false for disabling. + */ +util.toggleFullScreen = function(appWindow, enabled) { + if (appWindow) { + if (enabled) + appWindow.fullscreen(); + else + appWindow.restore(); + return; + } + + console.error( + 'App window not passed. Unable to toggle the full screen mode.'); +}; + +/** + * The type of a file operation. + * @enum {string} + */ +util.FileOperationType = { + COPY: 'COPY', + MOVE: 'MOVE', + ZIP: 'ZIP', +}; + +/** + * The type of a file operation error. + * @enum {number} + */ +util.FileOperationErrorType = { + UNEXPECTED_SOURCE_FILE: 0, + TARGET_EXISTS: 1, + FILESYSTEM_ERROR: 2, +}; + +/** + * The kind of an entry changed event. + * @enum {number} + */ +util.EntryChangedKind = { + CREATED: 0, + DELETED: 1, +}; + +/** + * Obtains whether an entry is fake or not. + * @param {Entry|Object} entry Entry of fake entry. + * @return {boolean} True if the given entry is fake. + */ +util.isFakeEntry = function(entry) { + return !('getParent' in entry); +}; + +/** + * Creates a FileError instance with given code. + * Note that we cannot create FileError instance by "new FileError(code)", + * unfortunately, so here we use Object.create. + * @param {number} code Error code for the FileError. + * @return {FileError} FileError instance + */ +util.createFileError = function(code) { + return Object.create(FileError.prototype, { + code: { get: function() { return code; } } + }); +}; + +/** + * Compares two entries. + * @param {Entry|Object} entry1 The entry to be compared. Can be a fake. + * @param {Entry|Object} entry2 The entry to be compared. Can be a fake. + * @return {boolean} True if the both entry represents a same file or + * directory. Returns true if both entries are null. + */ +util.isSameEntry = function(entry1, entry2) { + // Currently, we can assume there is only one root. + // When we support multi-file system, we need to look at filesystem, too. + return (entry1 && entry2 && entry1.fullPath === entry2.fullPath) || + (!entry1 && !entry2); +}; + +/** + * @param {Entry|Object} parent The parent entry. Can be a fake. + * @param {Entry|Object} child The child entry. Can be a fake. + * @return {boolean} True if parent entry is actualy the parent of the child + * entry. + */ +util.isParentEntry = function(parent, child) { + // Currently, we can assume there is only one root. + // When we support multi-file system, we need to look at filesystem, too. + return PathUtil.isParentPath(parent.fullPath, child.fullPath); +}; + +/** + * Views files in the browser. + * + * @param {Array.<string>} urls URLs of files to view. + * @param {function(bool)} callback Callback notifying success or not. + */ +util.viewFilesInBrowser = function(urls, callback) { + var taskId = chrome.runtime.id + '|file|view-in-browser'; + chrome.fileBrowserPrivate.executeTask(taskId, urls, callback); +}; + +/** + * Visit the URL. + * + * If the browser is opening, the url is opened in a new tag, otherwise the url + * is opened in a new window. + * + * @param {string} url URL to visit. + */ +util.visitURL = function(url) { + var params = {url: url}; + chrome.tabs.create(params, function() { + if (chrome.runtime.lastError) + chrome.windows.create(params); + }); +}; + +/** + * Returns normalized current locale, or default locale - 'en'. + * @return {string} Current locale + */ +util.getCurrentLocaleOrDefault = function() { + // chrome.i18n.getMessage('@@ui_locale') can't be used in packed app. + // Instead, we pass it from C++-side with strings. + return str('UI_LOCALE') || 'en'; +}; + +/** + * Converts array of entries to an array of corresponding URLs. + * @param {Array.<Entry>} entries Input array of entries. + * @return {Array.<string>} Output array of URLs. + */ +util.entriesToURLs = function(entries) { + // TODO(mtomasz): Make all callers use entries instead of URLs, and then + // remove this utility function. + console.warn('Converting entries to URLs is deprecated.'); + return entries.map(function(entry) { + return entry.toURL(); + }); +}; + +/** + * Converts array of URLs to an array of corresponding Entries. + * + * @param {Array.<string>} urls Input array of URLs. + * @param {function(Array.<Entry>)} callback Completion callback with array of + * Entries. + */ +util.URLsToEntries = function(urls, callback) { + var result = []; + AsyncUtil.forEach( + urls, + function(forEachCallback, url) { + webkitResolveLocalFileSystemURL(url, function(entry) { + result.push(entry); + forEachCallback(); + }, function() { + // Not an error. Possibly, the file is not accessible anymore. + console.warn('Failed to resolve the file with url: ' + url + '.'); + forEachCallback(); + }); + }, + function() { + callback(result); + }); +}; + +/** + * Error type of VolumeManager. + * @enum {string} + * @const + */ +util.VolumeError = Object.freeze({ + /* Internal errors */ + NOT_MOUNTED: 'not_mounted', + TIMEOUT: 'timeout', + + /* System events */ + UNKNOWN: 'error_unknown', + INTERNAL: 'error_internal', + UNKNOWN_FILESYSTEM: 'error_unknown_filesystem', + UNSUPPORTED_FILESYSTEM: 'error_unsupported_filesystem', + INVALID_ARCHIVE: 'error_invalid_archive', + AUTHENTICATION: 'error_authentication', + PATH_UNMOUNTED: 'error_path_unmounted' +}); + +/** + * List of connection types of drive. + * + * Keep this in sync with the kDriveConnectionType* constants in + * private_api_dirve.cc. + * + * @enum {string} + * @const + */ +util.DriveConnectionType = Object.freeze({ + OFFLINE: 'offline', // Connection is offline or drive is unavailable. + METERED: 'metered', // Connection is metered. Should limit traffic. + ONLINE: 'online' // Connection is online. +}); + +/** + * List of reasons of DriveConnectionType. + * + * Keep this in sync with the kDriveConnectionReason constants in + * private_api_drive.cc. + * + * @enum {string} + * @const + */ +util.DriveConnectionReason = Object.freeze({ + NOT_READY: 'not_ready', // Drive is not ready or authentication is failed. + NO_NETWORK: 'no_network', // Network connection is unavailable. + NO_SERVICE: 'no_service' // Drive service is unavailable. +}); + +/** + * The type of each volume. + * @enum {string} + * @const + */ +util.VolumeType = Object.freeze({ + DRIVE: 'drive', + DOWNLOADS: 'downloads', + REMOVABLE: 'removable', + ARCHIVE: 'archive' +}); diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/action_choice.css b/chromium/chrome/browser/resources/file_manager/foreground/css/action_choice.css new file mode 100644 index 00000000000..508809c520d --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/css/action_choice.css @@ -0,0 +1,172 @@ +/* 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. */ + +input:focus, +button:focus { + outline-color: rgb(77, 144, 254); +} + +.action-choice { + -webkit-box-align: center; + -webkit-box-pack: center; + -webkit-user-select: none; + background: white; + bottom: 0; + color: black; + display: -webkit-box; + font-family: Open Sans, Droid Sans Fallback, sans-serif; + font-size: 84%; + left: 0; + margin: 0; + overflow: hidden; + padding: 10px; + position: absolute; + right: 0; + top: 0; +} + +.action-choice[loading] .content, +.action-choice:not([loading]) .loading { + display: none; +} + +.action-choice h1 { + font-size: 14px; + font-weight: normal; + line-height: 1.5; + margin: 5px 3px; +} + +/* The loading preview */ + +.loading { + -webkit-box-align: center; + -webkit-box-orient: vertical; + color: #333; + display: -webkit-box; + font-size: 12px; +} + +.spinner { + background-image: url('../images/common/spinner.svg'); + background-size: 100%; + height: 21px; + left: 44px; + margin-left: -10px; + margin-top: -10px; + opacity: 0.5; + position: absolute; + top: 29px; + width: 21px; +} + +.device-type { + height: 64px; + position: relative; + width: 116px; +} + +.device-type[device-type=usb] { + background-image: -webkit-image-set( + url('../images/volumes/device_usb_large.png') 1x, + url('../images/volumes/2x/device_usb_large.png') 2x); +} + +.device-type[device-type=sd] { + -webkit-transform: rotate(-90deg); + background-image: -webkit-image-set( + url('../images/volumes/device_sd_large.png') 1x, + url('../images/volumes/2x/device_sd_large.png') 2x); + bottom: 10px; /* Adjust the rotated image to not overlap with element below */ +} + +/* The action choice content */ + +.content { + -webkit-box-align: start; + -webkit-box-orient: vertical; + display: -webkit-box; + height: 100%; + overflow: hidden; + position: relative; + width: 100%; +} + +.previews { + -webkit-box-orient: horizontal; + -webkit-mask-image: linear-gradient(to left, rgba(0, 0, 0, 0) 0, + rgba(0, 0, 0, 1) 80px); + display: -webkit-box; + position: relative; + width: 100%; +} + +.img-container { + height: 120px; + margin: 0 2px; + overflow: hidden; + position: relative; + width: 120px; +} + +.img-container > img { + -webkit-user-drag: none; + position: absolute; +} + +.counter { + color: #808080; + margin: 5px 3px; + width: 100%; +} + +.choices { + width: 100%; +} + +/* Padding counterweights negative margins of items, thus eliminating scroll + bar when it's not needed. Max height is set to fit 8 items before showing + scroll bar. */ +#actions-list { + display: block; + max-height: 328px; + outline: none; + overflow: auto; + padding: 1px 0; + position: relative; +} + +#actions-list > li { + cursor: default; + list-style-type: none; +} + +#actions-list > li > div { + background-position: 5px center; + background-repeat: no-repeat; + line-height: 39px; + padding-left: 43px; +} + +#actions-list > [selected] { + background-color: #dedede; +} + +#actions-list:focus > [selected] { + background-color: rgb(203, 219, 241); +} + +#actions-list > [selected]:hover { + background-color: rgb(193, 211, 236); + border-color: hsl(214, 91%, 85%); +} + +#actions-list > :hover { + background-color: #f1f1f1; + border-color: hsl(214, 91%, 85%); +} + +#actions-list > li > div.disabled { + opacity: 0.5; +} diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/audio_player.css b/chromium/chrome/browser/resources/file_manager/foreground/css/audio_player.css new file mode 100644 index 00000000000..1f6b7bbfb91 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/css/audio_player.css @@ -0,0 +1,404 @@ +/* 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. */ + +body { + -webkit-user-select: none; + overflow: hidden; + padding: 0; +} + +.audio-player { + background-color: #1d1d1d; + bottom: 0; + color: white; + cursor: default; + font-family: Open Sans, Droid Sans Fallback, sans-serif; + font-size: 10pt; + left: 0; + position: absolute; + right: 0; + top: 0; +} + +/* Title buttons. + * In the collapsed/single-track mode they overlap the first track. */ + +.title-button { + background-position: center center; + background-repeat: no-repeat; + cursor: pointer; + height: 29px; + opacity: 0.4; + position: absolute; + top: 0; + width: 29px; + z-index: 2; +} + +.title-button:hover { + background-color: rgb(60, 126, 255) !important; + opacity: 1; +} + +.audio-player:not(.collapsed):not(.single-track) > .title-button { + background-color: #1f1f1f; +} + +.title-button.close { + background-image: -webkit-image-set( + url('../images/media/media_close.png') 1x, + url('../images/media/2x/media_close.png') 2x); + right: 0; +} + +.title-button.collapse { + background-image: -webkit-image-set( + url('../images/media/media_collapse.png') 1x, + url('../images/media/2x/media_collapse.png') 2x); + right: 0; +} + +.audio-player:not(.frameless) .title-button.close { + display: none; +} + +.audio-player.frameless .title-button.collapse { + right: 29px; +} + +.collapsed .title-button.collapse { + background-image: -webkit-image-set( + url('../images/media/media_expand.png') 1x, + url('../images/media/2x/media_expand.png') 2x); +} + +.single-track .title-button.collapse { + display: none; +} + +/* Common properties for track containers. */ +.audio-player > .track-list, +.audio-player > .track-stack { + bottom: 35px; /* Room for the controls bar. */ + left: 0; + position: absolute; + right: 0; +} + +/* Scrollable list of tracks. + * Displayed in expanded mode if there is more than one track. */ +.audio-player > .track-list { + -webkit-box-align: center; + -webkit-box-orient: vertical; + -webkit-box-pack: start; + display: -webkit-box; + overflow-x: hidden; + overflow-y: auto; + top: 0; +} + +/* A single track container. + * Displayed in the compact mode or when there is only one track. */ +.audio-player > .track-stack { + height: 58px; +} + +.audio-player.collapsed > .track-list, +.audio-player.single_track > .track-list, +.audio-player:not(.collapsed):not(.single-track) > .track-stack { + opacity: 0; + pointer-events: none; +} + +/* Track item. */ +.track { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: start; + display: -webkit-box; + height: 58px; + width: 100%; +} + +/* In the expanded mode the selected track is highlighted. */ +.track-list .track.selected { + background-color: #2d2d2d; +} + +.track-list .track:hover { + background-color: #272727 !important; +} + +.track-list .track:not(.selected) .data { + opacity: 0.7; +} + +/* In the compact mode all tracks are in the same position, only the selected + is visible.*/ +.track-stack > .track { + position: absolute; + top: 0; +} + +.track-stack > .track.selected { + z-index: 1; +} + +/* Opacity transition is controlled differently for the text and the artwork. + * Text transition looks better if fade-in and fade-out go in parallel. + * For the artwork we start fading out the old icon only after the new one + * is completely opaque (otherwise when transitioning between identical icons + * we see some fading because the background transpires). */ +.track-stack > .track:not(.selected) .data, +.track-stack > .track:not(.visible) .art { + opacity: 0; + transition: opacity 220ms ease-out; +} + +/* Track data. */ + +.track .art { + box-sizing: border-box; + height: 48px; + margin: 4px 0 6px 4px; + position: relative; + width: 48px; +} + +.track .art.blank { + background-color: #111; + border: 1px solid #333; +} + +.track .art img { + height: 100%; + width: 100%; +} + +.track .art.blank img { + display: none; +} + +.track .art.error { + background-image: -webkit-image-set( + url('../images/media/error.png') 1x, + url('../images/media/2x/error.png') 2x); + background-position: center center; + background-repeat: no-repeat; +} + +.noart .track .art { + display: none; +} + +.track .data { + -webkit-box-flex: 1; + -webkit-box-orient: vertical; + -webkit-box-pack: center; + display: -webkit-box; + margin-left: 8px; + margin-right: 4px; +} + +.track .data .data-title, +.track .data .data-artist { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.track .data .data-title { + font-weight: bold; +} + +/* TODO(kaznacheev): Set to 20px when the audio player is title-less. */ +.single-track .data-title { + padding-right: 0; +} + +/* TODO(kaznacheev): Set to 50px when the audio player is title-less. */ +.collapsed:not(.single-track) .data-title { + padding-right: 20px; +} + +/* Controls bar. */ + +.audio-controls { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + background-color: #2D2D2D; + border-top: 1px solid rgba(255, 255, 255, 0.1); + bottom: 0; + display: -webkit-box; + height: 30px; + left: 0; + padding: 0 0 4px 13px; + position: absolute; + right: 0; +} + +.audio-controls .media-button { + height: 29px; + margin-top: 1px; + width: 29px; +} + +.audio-controls .media-button.play { + margin-left: -10px; + margin-right: -8px; +} + +.audio-controls .media-button.play > .default.normal, +.audio-controls .media-button.play > .ended.normal { + background-image: -webkit-image-set( + url('../images/media/media_play_audio.png') 1x, + url('../images/media/2x/media_play_audio.png') 2x); +} + +.audio-controls .media-button.play > .default.hover, +.audio-controls .media-button.play > .ended.hover { + background-image: -webkit-image-set( + url('../images/media/media_play_audio_hover.png') 1x, + url('../images/media/2x/media_play_audio_hover.png') 2x); +} + +.audio-controls .media-button.play > .default.active, +.audio-controls .media-button.play > .ended.active { + background-image: -webkit-image-set( + url('../images/media/media_play_audio_down.png') 1x, + url('../images/media/2x/media_play_audio_down.png') 2x); +} + +.audio-controls .media-button.play > .playing.normal { + background-image: -webkit-image-set( + url('../images/media/media_pause_audio.png') 1x, + url('../images/media/2x/media_pause_audio.png') 2x); +} + +.audio-controls .media-button.play > .playing.hover { + background-image: -webkit-image-set( + url('../images/media/media_pause_audio_hover.png') 1x, + url('../images/media/2x/media_pause_audio_hover.png') 2x); +} + +.audio-controls .media-button.play > .playing.active { + background-image: -webkit-image-set( + url('../images/media/media_pause_audio_down.png') 1x, + url('../images/media/2x/media_pause_audio_down.png') 2x); +} + +.audio-controls .time-controls { + margin-left: 10px; + margin-right: 9px; +} + +.audio-controls .time-controls .time { + margin-left: 11px; +} + +.media-button.previous { + margin-left: -2px; +} + +.media-button.previous > .normal { + background-image: -webkit-image-set( + url('../images/media/media_previous.png') 1x, + url('../images/media/2x/media_previous.png') 2x); +} + +.media-button.previous > .hover { + background-image: -webkit-image-set( + url('../images/media/media_previous_hover.png') 1x, + url('../images/media/2x/media_previous_hover.png') 2x); +} + +.media-button.previous > .active { + background-image: -webkit-image-set( + url('../images/media/media_previous_down.png') 1x, + url('../images/media/2x/media_previous_down.png') 2x); +} + +.media-button.next { + margin-right: -2px; +} + +.media-button.next > .normal { + background-image: -webkit-image-set( + url('../images/media/media_next.png') 1x, + url('../images/media/2x/media_next.png') 2x); +} + +.media-button.next > .hover { + background-image: -webkit-image-set( + url('../images/media/media_next_hover.png') 1x, + url('../images/media/2x/media_next_hover.png') 2x); +} + +.media-button.next > .active { + background-image: -webkit-image-set( + url('../images/media/media_next_down.png') 1x, + url('../images/media/2x/media_next_down.png') 2x); +} + +.single-track .media-button.next, +.single-track .media-button.previous { + display: none; +} + +/* Customized scrollbar for the playlist. */ + +::-webkit-scrollbar { + height: 16px; + width: 16px; +} + +::-webkit-scrollbar-button { + height: 0; + width: 0; +} + +::-webkit-scrollbar-thumb { + background-clip: padding-box; + background-color: rgba(255, 255, 255, 0.15); + box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.10), + inset 0 -1px 0 rgba(0, 0, 0, 0.07); + min-height: 28px; + padding-top: 100px; +} + +::-webkit-scrollbar-thumb:hover { + background-color: rgba(255,255,255,0.20); + box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.25); +} + +::-webkit-scrollbar-thumb:active { + background-color: rgba(255, 255, 255, 0.25); + box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.35); +} + +::-webkit-scrollbar-thumb:vertical { + border-bottom: 0 solid transparent; + border-left: 5px solid transparent; + border-right: 0 solid transparent; + border-top: 0 solid transparent; +} + +::-webkit-scrollbar-track:hover { + background-color: rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.10); +} + +::-webkit-scrollbar-track:active { + background-color: rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.14), + inset -1px -1px 0 rgba(0, 0, 0, 0.07); +} + +::-webkit-scrollbar-track:vertical { + background-clip: padding-box; + background-color: transparent; + border-left: 5px solid transparent; + border-right: 0 solid transparent; +} diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/combobutton.css b/chromium/chrome/browser/resources/file_manager/foreground/css/combobutton.css new file mode 100644 index 00000000000..2651aad6bbb --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/css/combobutton.css @@ -0,0 +1,34 @@ +/* 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. */ + +.buttonbar button.combobutton { + -webkit-box-align: stretch; + display: -webkit-box; +} + +.buttonbar .combobutton > .action { + background-position: left center; + background-repeat: no-repeat; + background-size: 16px 16px; + padding-left: 21px; +} + +.buttonbar .combobutton > .trigger { + border-left: solid 1px #dcdcdc; + margin-left: 8px; + margin-right: -8px; + width: 22px; +} + +.buttonbar .combobutton:not([multiple]) > .trigger { + display: none; +} + +.buttonbar .combobutton > div > span.disclosureindicator { + -webkit-transform: rotate(90deg); +} + +.buttonbar .combobutton[hidden] { + display: none; +} diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/common.css b/chromium/chrome/browser/resources/file_manager/foreground/css/common.css new file mode 100644 index 00000000000..e9051864258 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/css/common.css @@ -0,0 +1,501 @@ +/* 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. */ + +/* Special attribute to hide elements. */ +[hidden] { + display: none !important; +} + +/* This file contains "borrowed" copy of standard styles. To simplify merging, + * when altering, please preserve original property value by adding comments. */ +input.common[type='checkbox'], +input.common[type='radio'] { + -webkit-appearance: none; + border: 1px solid #555; + border-radius: 1px; + box-sizing: border-box; + cursor: default; + height: 13px; + margin: 0; + opacity: 0.4; + width: 13px; +} + +input.common[type='checkbox']:hover, +input.common[type='checkbox']:checked, +input.common[type='radio']:hover, +input.common[type='radio']:checked { + opacity: 1; +} + +input.common[type='checkbox'] { + position: relative; +} + +input.common[type='checkbox']:checked::after { + background-image: -webkit-image-set( + url('../images/common/check_no_box.png') 1x, + url('../images/common/2x/check_no_box.png') 2x); + background-position: -3px -4px; + background-repeat: no-repeat; +} + +input.common[type='checkbox'].white { + border: none; +} + +input.common[type='checkbox'].white:not(:checked)::after { + background-image: -webkit-image-set( + url('../images/common/checkbox_white_unchecked.png') 1x, + url('../images/common/2x/checkbox_white_unchecked.png') 2x); + background-position: -1px 0; +} + +input.common[type='checkbox'].white:checked::after { + background-image: -webkit-image-set( + url('../images/common/checkbox_white_checked.png') 1x, + url('../images/common/2x/checkbox_white_checked.png') 2x); + background-position: -1px 0; +} + +input.common[type='checkbox']::after { + content: ''; + display: -webkit-box; + height: 15px; + left: -2px; + position: absolute; + top: -2px; + width: 17px; +} + +.bubble { + background: #FFF; + border-radius: 2px; + cursor: default; + outline: 1px solid rgba(0, 0, 0, 0.2); + padding: 16px; +} + +.bubble .pointer { + background: -webkit-image-set( + url('../images/common/bubble_point_white.png') 1x, + url('../images/common/2x/bubble_point_white.png') 2x); + display: block; + height: 11px; + left: 24px; + margin: 0 0 0 -5px; + outline: none; + position: absolute; + width: 17px; +} + +.bubble .pointer:not(.bottom) { + top: -11px; +} + +.bubble .pointer.bottom { + -webkit-transform: rotate(180deg); + bottom: -11px; +} + +.bubble .close-x { + background: -webkit-image-set( + url('../images/common/close_x_gray.png') 1x, + url('../images/common/2x/close_x_gray.png') 2x); + height: 21px; + opacity: 0.3; + position: absolute; + right: 3px; + top: 3px; + width: 21px; +} + +.bubble .close-x:hover { + opacity: 0.7; +} + +.buttonbar { + display: -webkit-box; + height: 31px; +} + +.buttonbar button:active img { + opacity: 1.0; +} + +.buttonbar button:hover img { + opacity: 0.72; +} + +.buttonbar button[disabled] img { + opacity: 0.9; +} + +.buttonbar button img { + display: inline-block; + margin: -3px 0 0; + opacity: 0.55; + vertical-align: middle; +} + +.buttonbar button.menubutton span.disclosureindicator { + -webkit-transform: rotate(90deg); + float: right; + margin-left: 7px; + margin-top: 10px; + opacity: .8; + transition: none; +} + +span.disclosureindicator { + background-image: -webkit-image-set( + url('../images/common/disclosure_arrow_dk_grey.png') 1x, + url('../images/common/2x/disclosure_arrow_dk_grey.png') 2x); + background-position: center; + background-repeat: no-repeat; + display: inline-block; + height: 7px; + width: 5px; +} + +/* "chrome-menu" class overrides some standard menu.css styles, to make custom + menus in FileBrowser look like native ChromeOS menus. */ + +menu.chrome-menu { + background-color: rgb(250, 250, 250); + border-radius: 3px; + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, .5); + color: rgb(34, 34, 34); + outline: none; + overflow: hidden; + padding: 5px 0; + transition: opacity 200ms ease-in; + z-index: 600; /* Must be below the overlay pane (1000). */ +} + +menu.chrome-menu[hidden] { + display: block !important; /* Overrides default [hidden] for animation. */ + opacity: 0; + pointer-events: none; + visibility: hidden; +} + +menu.chrome-menu.hide-delayed[hidden] { + transition-delay: 120ms; + transition-property: opacity, visibility; +} + +menu.chrome-menu > :not(hr) { + background-position: right 10px center; + background-repeat: no-repeat; + line-height: 30px; + padding-left: 20px; + padding-right: 20px; +} + +menu.chrome-menu > .menuitem-button { + background-position: center; + background-repeat: no-repeat; + border: 1px solid rgb(235, 235, 235); + height: 42px; + margin: -36px -1px -1px 0; + min-width: 60px; + padding: 0; + position: absolute; + width: 60px; +} + +menu.chrome-menu > .menuitem-button[checked] { + background-color: rgb(235, 235, 235); +} + +menu.chrome-menu > .menuitem-button.left { + right: 59px; +} + +menu.chrome-menu > .menuitem-button.right { + right: 0; +} + +menu.chrome-menu > menuitem[disabled] { + color: rgb(153, 153, 153); +} + +menu.chrome-menu > menuitem:not([disabled])[selected], +menu.chrome-menu > menuitem:not([disabled])[selected]:active { + background-color: rgb(66, 129, 244); + color: white; +} + +menu.chrome-menu > hr { + background: rgb(235, 235, 235); + height: 1px; + margin: 5px 0; +} + +menu.chrome-menu > menuitem[checked] { + background-image: -webkit-image-set( + url('../images/common/check_no_box.png') 1x, + url('../images/common/2x/check_no_box.png') 2x); +} + +menu.chrome-menu > [checked]::before { + display: none; +} + +menu[showShortcuts] > menuitem[shortcutText][selected]:not([disabled])::after { + color: white; +} + +/** + * Ok/Cancel style buttons + * Height: 31px (content:21px + border:5px * 2) + **/ +button, +input[type='button'], +input[type='submit'], +select { + background-color: rgb(250, 250, 250); + background-image: none; + background-position: center; + background-repeat: no-repeat; + border: 5px solid transparent; + border-image: -webkit-image-set( + url('chrome://resources/images/apps/button.png') 1x, + url('chrome://resources/images/2x/apps/button.png') + 2x) 5 / 5px / 2px repeat; + box-sizing: content-box; + color: rgb(34, 34, 34); + cursor: default; + height: 21px; + line-height: 21px; + margin: 0; + min-height: 21px; + min-width: 55px; + padding: 0 10px; + position: relative; + text-align: center; + z-index: 1; +} + +.buttonbar button { + -webkit-margin-start: 10px; +} + +button:hover, +input[type='button']:hover, +input[type='submit']:hover, +select:hover { + border-image: -webkit-image-set( + url('chrome://resources/images/apps/button_hover.png') 1x, + url('chrome://resources/images/2x/apps/button_hover.png') + 2x) 5 fill / 5px / 2px repeat; + color: #222; +} + +button:active, +input[type='button']:active, +input[type='submit']:active { + border-image: -webkit-image-set( + url('chrome://resources/images/apps/button_pressed.png') 1x, + url('chrome://resources/images/2x/apps/button_pressed.png') + 2x) 5 fill / 5px / 2px repeat; + color: #333; +} + +button[disabled], +input[type='button'][disabled], +input[type='submit'][disabled], +button[disabled]:hover, +input[type='button'][disabled]:hover, +input[type='submit'][disabled]:hover { + background-color: rgb(250, 250, 250); + background-image: none; + border-image: -webkit-image-set( + url('chrome://resources/images/apps/button.png') 1x, + url('chrome://resources/images/2x/apps/button.png') + 2x) 5 fill / 5px / 2px repeat; + color: rgb(150, 150, 150); +} + +/* Gray progress bar. */ +.progress-bar { + background-color: #e6e6e6; + border-radius: 3px; + height: 6px; +} + +.progress-track { + background-color: #888; + border-radius: 3px; + height: 6px; + min-width: 6px; +} + +.progress-track.smoothed { + transition: width 1s linear; +} + +/* Icons for the action choice dialog and choosing the default app. */ +div.import-photos-to-drive-icon { + background-image: -webkit-image-set( + url('../images/media/drive.png') 1x, + url('../images/media/2x/drive.png') 2x); +} + +div.view-files-icon { + background-image: -webkit-image-set( + url('../../common/images/icon32.png') 1x, + url('../../common/images/icon64.png') 2x); +} + +div.watch-single-video-icon { + background-image: -webkit-image-set( + url('../images/media/watch.png') 1x, + url('../images/media/2x/watch.png') 2x); +} + +/* Pop-up dialogs. */ + +.cr-dialog-container { + -webkit-box-align: center; + -webkit-box-pack: center; + -webkit-user-select: none; + display: -webkit-box; + height: 100%; + left: 0; + overflow: hidden; + position: absolute; + top: 0; + transition: opacity 250ms linear; + width: 100%; + z-index: 9999; +} + +.cr-dialog-frame { + -webkit-box-orient: vertical; + background-color: rgb(250, 250, 250); + border: 1px solid rgb(255, 255, 255); + border-radius: 2px; + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, .5); + color: rgb(34, 34, 34); + cursor: default; + display: -webkit-box; + padding: 20px; + position: relative; + width: 460px; +} + +.cr-dialog-frame:focus { + outline: none; +} + +@-webkit-keyframes pulse { + 0% { + -webkit-transform: scale(1); + } + 40% { + -webkit-transform: scale(1.02); + } + 60% { + -webkit-transform: scale(1.02); + } + 100% { + -webkit-transform: scale(1); + } +} + +.cr-dialog-frame.pulse { + -webkit-animation-duration: 180ms; + -webkit-animation-iteration-count: 1; + -webkit-animation-name: pulse; + -webkit-animation-timing-function: ease-in-out; +} + +.shown > .cr-dialog-frame { + -webkit-transform: perspective(500px) scale(1) + translateY(0) rotateX(0); + opacity: 1; +} + +.cr-dialog-frame { + -webkit-transform: perspective(500px) scale(0.99) + translateY(-20px) rotateX(5deg); + opacity: 0; + transition: all 180ms; + transition-duration: 250ms; +} + +.cr-dialog-shield { + background-color: white; + bottom: 0; + display: block; + left: 0; + opacity: 0; + pointer-events: none; + position: absolute; + right: 0; + top: 0; + transition: opacity 500ms; +} + +.shown > .cr-dialog-shield { + opacity: 0.5; + transition: opacity 500ms; +} + +.cr-dialog-title { + -webkit-margin-after: 10px; + -webkit-margin-end: 20px; + display: block; + font-size: 125%; + white-space: nowrap; + word-wrap: normal; +} + +.cr-dialog-text { + margin: 13px 0; +} + +.cr-dialog-text, +.cr-dialog-title { + overflow: hidden; + text-overflow: ellipsis; +} + +.cr-dialog-frame input { + box-sizing: border-box; + width: 100%; +} + +.cr-dialog-buttons { + -webkit-box-orient: horizontal; + -webkit-box-pack: end; + display: -webkit-box; + padding-top: 10px; +} + +.cr-dialog-buttons button { + -webkit-margin-start: 8px; + line-height: 1.8; +} + +.cr-dialog-close { + background: url('chrome://theme/IDR_CLOSE_DIALOG') center no-repeat; + display: inline-block; + height: 44px; + opacity: 0.7; + position: absolute; + right: 0; + top: 0; + width: 44px; +} + +.cr-dialog-close:hover { + background-image: url('chrome://theme/IDR_CLOSE_DIALOG_H'); +} + +.cr-dialog-close:active { + background-image: url('chrome://theme/IDR_CLOSE_DIALOG_P'); +} diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/drive_welcome.css b/chromium/chrome/browser/resources/file_manager/foreground/css/drive_welcome.css new file mode 100644 index 00000000000..a3c5ddb8999 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/css/drive_welcome.css @@ -0,0 +1,188 @@ +/* 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. */ + +/* Google Drive welcome banners.*/ +.drive-welcome-wrapper { + /* This image looks good in high DPI as is. */ + background-image: url(chrome://resources/images/clouds.png); + background-repeat: repeat-x; + color: #333; +} + +.drive-welcome-icon { + background-image: -webkit-image-set( + url('../images/files/ui/drive_logo.png') 1x, + url('../images/files/ui/2x/drive_logo.png') 2x); + background-repeat: no-repeat; +} + +.drive-welcome-links { + -webkit-box-orient: horizontal; + display: -webkit-box; +} + +.drive-welcome-button { + -webkit-user-select: none; + background-image: linear-gradient(to bottom, #f5f5f5, #f1f1f1); + border: 1px solid rgba(0,0,0,0.1); + border-radius: 2px; + color: #444; + cursor: default; + display: inline-block; + font-size: 13px; + font-weight: bold; + height: 27px; + line-height: 27px; + padding: 0 8px; + text-align: center; + transition: all 218ms; +} + +.drive-welcome-button:hover { + background-image: linear-gradient(to bottom, #f8f8f8, #f1f1f1); + border-color: #C6C6C6; + box-shadow: 0 1px 1px rgba(0,0,0,0.1); + color: #222; + transition: all 0; +} + +.drive-welcome-button:active { + background-image: linear-gradient(to bottom, #f6f6f6, #f1f1f1); + box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); +} + + +.drive-welcome-button.drive-welcome-start { + background-image: + linear-gradient(to bottom, rgb(77, 144, 254), rgb(71, 135, 237)); + border-color: rgb(48, 121, 237); + color: white; + text-decoration: none; +} + +.drive-welcome-button.drive-welcome-start:hover { + background-image: + linear-gradient(to bottom, rgb(77, 144, 254), rgb(53, 122, 232)); + border-color: rgb(47, 91, 183); + box-shadow: 0 1px 1px rgba(0,0,0,0.1); +} + +/* Header welcome banner. */ +.drive-welcome.header { + -webkit-box-flex: 0; + height: 100px; + overflow: hidden; + position: relative; + transition: height 180ms ease, visibility 0 linear 180ms; +} + +.dialog-container:not([drive-welcome='header']) .drive-welcome.header { + height: 0; + visibility: hidden; +} + +.drive-welcome.header .drive-welcome-wrapper { + -webkit-box-orient: horizontal; + background-size: 308px 100px; + bottom: 0; + display: -webkit-box; + left: 0; + position: absolute; + right: 0; + top: 0; +} + +.drive-welcome.header .drive-welcome-icon { + background-position: center 18px; + background-size: 51px 44px; + width: 120px; +} + +.drive-welcome.header .drive-welcome-message { + -webkit-box-flex: 1; + -webkit-box-orient: vertical; + display: -webkit-box; +} + +.drive-welcome.header .drive-welcome-title { + font-size: 140%; + margin-bottom: 4px; + margin-top: 14px; +} + +.drive-welcome.header .drive-welcome-text { + margin-bottom: 6px; +} + +.drive-welcome.header .drive-welcome-dismiss { + display: none; +} + +/* Full page welcome banner. */ +.drive-welcome.page { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; +} + +.dialog-container:not([drive-welcome='page']) .drive-welcome.page { + display: none; +} + +.drive-welcome.page .cr-dialog-close { + display: none; +} + +.drive-welcome.page .drive-welcome-wrapper { + -webkit-box-align: center; + -webkit-box-orient: vertical; + background-size: 520px 173px; + bottom: 0; + display: -webkit-box; + font-size: 120%; + left: 0; + overflow: hidden; + position: absolute; + right: 0; + top: 0; +} + +.drive-welcome.page .drive-welcome-icon { + background-position: center center; + height: 240px; + left: 0; + right: 0; + top: 0; + width: 100%; +} + +.drive-welcome.page .drive-welcome-message { + margin-left: 10px; + margin-right: 10px; + max-width: 525px; +} + +.drive-welcome.page .drive-welcome-title { + font-size: 133%; + margin-bottom: 30px; + text-align: center; +} + +.drive-welcome.page .drive-welcome-text { + margin-bottom: 24px; +} + +.drive-welcome.page .drive-welcome-dismiss { + margin-left: 20px; +} + +body:not([type='full-page']) .drive-welcome.page .drive-welcome-wrapper { + background-position: 0 0; +} + +body:not([type='full-page']) .drive-welcome.page .drive-welcome-icon { + height: 200px; +} diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/file_manager.css b/chromium/chrome/browser/resources/file_manager/foreground/css/file_manager.css new file mode 100644 index 00000000000..560cef7e9a5 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/css/file_manager.css @@ -0,0 +1,2075 @@ +/* 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. */ + +/* The order of z-index: + * - 2: drag-selection-bodrder + * - 3: preview-panel + * - 500: scrollbar + * - 500: splitter + * - 525: spinner + * - 550: autocomplete-suggestions + * - 600: menus + * - 600: tooltip + * - 1000: preview thumbnail popup + * - 1000: overlay panel (ie. image editor) + */ + +/* Special attribute used in HTML to hide elements. */ +body[type='folder'] [invisibleif~='folder'], +body[type='upload-folder'] [invisibleif~='upload-folder'], +body[type='saveas-file'] [invisibleif~='saveas-file'], +body[type='open-file'] [invisibleif~='open-file'], +body[type='open-multi-file'] [invisibleif~='open-multi-file'], +body[type='full-page'] [invisibleif~='full-page'], + +body[type='folder'] [visibleif]:not([visibleif~='folder']), +body[type='upload-folder'] [visibleif]:not([visibleif~='upload-folder']), +body[type='saveas-file'] [visibleif]:not([visibleif~='saveas-file']), +body[type='open-file'] [visibleif]:not([visibleif~='open-file']), +body[type='open-multi-file'] [visibleif]:not([visibleif~='open-multi-file']), +body[type='full-page'] [visibleif]:not([visibleif~='full-page']) { + display: none !important; +} + +html { + height: 100%; +} + +html.col-resize * { + cursor: col-resize !important; +} + +/* Outer frame of the dialog. */ +body { + -webkit-box-flex: 1; + -webkit-box-orient: vertical; + -webkit-tap-highlight-color: transparent; + -webkit-user-select: none; + cursor: default; + display: -webkit-box; + height: 100%; + margin: 0; + padding: 0; + width: 100%; +} + +/* Drop opacity of selected rows to give a visual feedback on copy/cut + * operation. */ +.blink { + opacity: 0.8; +} + +::-webkit-scrollbar { + height: 0; + width: 0; +} + +/* TODO(mtomasz): Flip scrollbars to the opposite side for RTL languages. */ +.scrollbar-vertical { + bottom: 0; + position: absolute; + right: 0; + top: 0; + width: 10px; + z-index: 500; /* Must be below the contextmenu (600). */ +} + +.scrollbar-button { + background-color: black; + border: 1px solid #ccc; + border-radius: 3px; + box-sizing: border-box; + height: 50%; + margin-right: 2px; + opacity: 0; + position: absolute; + transition: opacity 100ms; + width: 8px; +} + +:hover > .scrollbar-vertical > .scrollbar-button { + opacity: 0.3; +} + +.scrollbar-vertical > .scrollbar-button:hover { + opacity: 0.4; +} + +.scrollbar-vertical > .scrollbar-button.pressed { + opacity: 0.5; +} + +/* Main part of the dialog between header and footer. */ +.dialog-container { + -webkit-box-align: stretch; + -webkit-box-flex: 1; + -webkit-box-orient: horizontal; + background-color: white; /* Makes #drag-container invisible. */ + border-radius: 2px; + display: -webkit-box; + overflow: hidden; + position: relative; +} + +/* The style applied when a modal dialog box overlap the dialog container. */ +.dialog-container.disable-header-drag .dialog-navigation-list-header, +.dialog-container.disable-header-drag .dialog-header { + -webkit-app-region: no-drag; +} + +/* List/grid and preview are inside this container. */ +.dialog-main { + -webkit-box-align: stretch; + -webkit-box-flex: 1; + -webkit-box-orient: vertical; + display: -webkit-box; +} + +/* Directory tree at the left. */ +.dialog-navigation-list { + -webkit-border-end: 1px solid rgba(20, 20, 22, 0.1); + -webkit-box-flex: 0; + -webkit-box-orient: vertical; + background-color: #f1f1f1; + display: flex; + flex-direction: column; + max-width: 50%; + min-width: 100px; + overflow: hidden; + position: relative; + width: 150px; +} + +.dialog-navigation-list-header { + -webkit-app-region: drag; + flex: none; + height: 48px; /* Keep in sync with #dialog-header. */ + line-height: 45px; +} + +.dialog-navigation-list-header #app-name { + -webkit-margin-start: 15px; + color: #303030; + font-size: 130%; +} + +.dialog-navigation-list-contents { + display: -webkit-box; + flex: 1 1 auto; + position: relative; +} + +.dialog-navigation-list-footer { + display: -webkit-flex; + flex: none; + flex-direction: column; +} + +/* A vertical splitter between the roots list and the file list. It is actually + a transparent area centered on the roots list right border.*/ +div.splitter { + -webkit-box-flex: 0; + cursor: col-resize; + margin-left: -3px; + margin-right: -3px; + position: relative; + width: 6px; + z-index: 500; /* Must be below the contextmenu (600). */ +} + +#navigation-list { + -webkit-box-flex: 1; + -webkit-box-orient: vertical; + display: -webkit-box; +} + +#navigation-list > * { + height: 40px; + padding: 0 5px; +} + +#navigation-list > .accepts, +#navigation-list > [lead][selected], +#navigation-list > [lead], +#navigation-list > [selected], +#navigation-list > [anchor] { + background-color: rgb(225, 225, 225); +} + +#navigation-list:focus > .accepts, +#navigation-list:focus > [lead][selected], +#navigation-list:focus > [lead], +#navigation-list:focus > [selected], +#navigation-list:focus > [anchor] { + background-color: rgb(66, 129, 244); + color: white; +} + +#navigation-list li.root-item { + -webkit-box-align: center; + display: -webkit-box; + line-height: 22px; /* To accomodate for icons. */ + padding-left: 11px; +} + +#navigation-list li.root-item > .root-label { + -webkit-box-flex: 1; + margin: 0 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#navigation-list .volume-icon { + background-position: center 2px; + background-repeat: no-repeat; + height: 24px; + width: 24px; +} + +#middlebar-header { + -webkit-border-end: 1px solid rgba(20, 20, 22, 0.1); + -webkit-box-sizing: border-box; + -webkit-padding-start: 20px; + color: rgb(100, 100, 100); + height: 47px; + line-height: 40px; + overflow-x: hidden; + position: absolute; + text-overflow: ellipsis; + width: 100%; +} + +#directory-tree { + -webkit-border-end: 1px solid rgba(20, 20, 22, 0.1); + bottom: 0; + left: 0; + overflow-x: hidden; + overflow-y: auto; + padding-bottom: 0; /* For the preview panel. Will be overridden by JS. */ + position: absolute; + right: 0; + top: 47px; +} + +#directory-tree .tree-row { + cursor: pointer; + display: -webkit-box; + line-height: 29px; + padding: 0 3px; +} + +/* For rows of subitems (non-top items) */ +#directory-tree .tree-children .tree-row { + line-height: 29px; +} + +#directory-tree .tree-row > .expand-icon { + height: 37px; + left: 3px; + margin: -13px; + right: 3px; + top: 0; + vertical-align: middle; + width: 37px; +} + +#directory-tree:focus .tree-row[selected] > .expand-icon { + background-image: -webkit-canvas(tree-triangle-inverted); +} + +#directory-tree .tree-row > .volume-icon { + background-position: center 2px; + background-repeat: no-repeat; + height: 24px; + vertical-align: middle; + width: 24px; +} + +#directory-tree .tree-row > .label { + -webkit-box-flex: 1; + display: block; + margin: 0 3px; + overflow-x: hidden; + text-overflow: ellipsis; +} + +#directory-tree .tree-item.accepts > .tree-row, +#directory-tree .tree-row[lead][selected], +#directory-tree .tree-row[lead], +#directory-tree .tree-row[selected], +#directory-tree .tree-row[anchor] { + background-color: rgb(204, 204, 204); +} + +#directory-tree .tree-item.accepts > .tree-row, +#directory-tree .tree-row[lead][selected], +#directory-tree .tree-row[lead], +#directory-tree .tree-row[selected], +#directory-tree .tree-row[anchor] { + background-color: rgb(225, 225, 225); +} + +#directory-tree:focus .tree-item.accepts > .tree-row, +#directory-tree:focus .tree-row[lead][selected], +#directory-tree:focus .tree-row[lead], +#directory-tree:focus .tree-row[selected], +#directory-tree:focus .tree-row[anchor] { + background-color: rgb(193, 209, 232); +} + +#directory-tree:focus .tree-item.accepts > .tree-row, +#directory-tree:focus .tree-row[lead][selected], +#directory-tree:focus .tree-row[lead], +#directory-tree:focus .tree-row[selected], +#directory-tree:focus .tree-row[anchor] { + background-color: rgb(66, 129, 244); + color: white; +} + +#navigation-list .root-item > div.root-eject { + background-image: -webkit-image-set( + url('../images/files/ui/eject.png') 1x, + url('../images/files/ui/2x/eject.png') 2x); + background-position: center center; + background-repeat: no-repeat; + cursor: pointer; + height: 20px; + margin-right: 6px; + opacity: 0.7; + transition: opacity 70ms linear; + vertical-align: middle; + width: 20px; +} + +#navigation-list:focus .root-item[selected] > div.root-eject { + -webkit-filter: brightness(0) invert(); + opacity: 1; +} + +#directory-tree .root-item[disabled] { + opacity: 0.5; + pointer-events: none; +} + +/* Breadcrumbs and things under the title but above the list view. */ +.dialog-header { + -webkit-app-region: drag; + -webkit-box-align: center; + -webkit-box-orient: horizontal; + display: flex; + height: 48px; + margin: 0; + transition: all 180ms ease; +} + +/* Search box */ + +#search-box { + display: flex; + flex: auto; +} + +#search-box.too-short { + visibility: hidden; +} + +#search-box .icon { + -webkit-app-region: no-drag; + -webkit-padding-end: 0; + -webkit-padding-start: 10px; + background: transparent -webkit-image-set( + url(../images/files/ui/search_icon_inactive.png) 1x, + url(../images/files/ui/2x/search_icon_inactive.png) 2x) + no-repeat center; + flex: none; + height: 32px; + padding-bottom: 8px; + padding-top: 8px; + width: 32px; +} + +#search-box .icon:hover, +#search-box.has-cursor .icon, +#search-box.has-text .icon { + background-image: -webkit-image-set( + url(../images/files/ui/search_icon_active.png) 1x, + url(../images/files/ui/2x/search_icon_active.png) 2x); +} + +#search-box .full-size { + flex: 1 0 0; +} + +#search-box input { + -webkit-app-region: no-drag; + background-color: #fff; + border-style: none; + color: #333; + cursor: default; + display: block; + height: 48px; + line-height: 1em; + margin: 0; + max-width: 100%; + outline: none; + padding: 0; +} + +#search-box input::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +#search-box.has-cursor input, +#search-box.has-text input { + cursor: text; +} + +#search-box .clear { + -webkit-app-region: no-drag; + -webkit-margin-end: 30px; + align-self: center; + background: -webkit-image-set( + url(../images/files/ui/search_clear.png) 1x, + url(../images/files/ui/2x/search_clear.png) 2x) + no-repeat center; + border: none; + display: none; + flex: none; + height: 12px; + min-height: 0; + min-width: 0; + outline: none; + padding: 0; + width: 12px; +} + +#search-box.has-text .clear { + display: block; +} + +#search-box .clear:hover { + background-image: -webkit-image-set( + url(../images/files/ui/search_clear_hover.png) 1x, + url(../images/files/ui/2x/search_clear_hover.png) 2x); +} + +#search-box .clear:active { + background-image: -webkit-image-set( + url(../images/files/ui/search_clear_pressed.png) 1x, + url(../images/files/ui/2x/search_clear_pressed.png) 2x); +} + +.topbutton-bar { + flex: none; +} + +/* Container for the detail and thumbnail list views. */ +.dialog-body { + -webkit-box-flex: 1; + -webkit-transition: all 180ms ease; + border-top: 1px solid rgba(20, 20, 22, 0.1); + position: relative; +} + +.main-panel { + bottom: 0; + display: -webkit-box; + left: 0; + position: absolute; + right: 0; + top: 0; +} + +.dialog-middlebar-contents { + display: -webkit-box; + max-width: 50%; + min-width: 45px; + position: relative; + width: 180px; +} + +/* Container for the ok/cancel buttons. */ +.dialog-footer { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + border-top: 1px solid rgb(225, 225, 225); + display: -webkit-box; + outline: none; + padding: 10px; +} + +.progressable:not([progress]) .progress-bar, +.progressable:not([progress]) .preparing-label { + display: none; +} + +.progressable[progress] .ok, +.progressable[progress] #filename-input-box, +.progressable[progress] #preview-lines, +.progressable[progress] .file-type { + display: none; +} + +.progressable .progress-bar { + -webkit-box-flex: 1; + -webkit-margin-end: 20px; + -webkit-margin-start: 20px; +} + +select.file-type:hover { + /* Original value is '5 fill', which hides the dropdown triangle. */ + border-image-slice: 5; +} + +/* The container for breadcrumb elements. */ +.breadcrumbs { + -webkit-box-align: center; + -webkit-box-flex: 1; + -webkit-box-orient: horizontal; + display: -webkit-box; + line-height: 20px; + overflow: hidden; + padding-top: 1px; +} + +#dir-breadcrumbs { + -webkit-margin-end: 5px; + -webkit-margin-start: 10px; +} + +/* The icon for offline mode */ +.offline-icon { + -webkit-margin-end: 0; + -webkit-margin-start: 10px; + background-image: -webkit-image-set( + url('../images/files/ui/offline.png') 1x, + url('../images/files/ui/2x/offline.png') 2x); + height: 16px; + opacity: 0; + transition-duration: 200ms; + transition-property: opacity; + transition-timing-function: ease-out; + width: 16px; +} + +/* Transition for '-webkit-margin-start' (or -end) property is not working. + * So I added .offline-icon-space to animate 'width' property. */ +.offline-icon-space { + -webkit-margin-end: 0; + -webkit-margin-start: -26px; /* Clear width of .offline-icon */ + transition-duration: 200ms; + transition-property: width; + transition-timing-function: ease-out; + width: 0; +} + +body[drive] .dialog-container[connection='offline'] .offline-icon, +body[drive] .dialog-container[connection='metered'] .offline-icon { + opacity: 1; +} + +body[drive] .dialog-container[connection='offline'] .offline-icon-space, +body[drive] .dialog-container[connection='metered'] .offline-icon-space { + width: 26px; +} + +.breadcrumbs > [collapsed]::before { + content: '...'; +} + +.breadcrumbs > [collapsed] { + width: 1em; +} + +/* A single directory name in the list of path breadcrumbs. */ +.breadcrumb-path { + color: #969696; + cursor: pointer; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* The final breadcrumb, representing the current directory. */ +#search-breadcrumbs .breadcrumb-path.breadcrumb-last { + color: #141414; + cursor: default; +} + +/* The > arrow between breadcrumbs. */ + +.breadcrumbs .separator { + background-image: -webkit-image-set( + url('../images/files/ui/breadcrumb-separator.png') 1x, + url('../images/files/ui/2x/breadcrumb-separator.png') 2x); + background-position: center center; + background-repeat: no-repeat; + height: 10px; + overflow: hidden; + width: 25px; +} + +#filename-input-box input { + border: 1px solid #c8c8c8; + border-radius: 1px; + box-sizing: border-box; + height: 31px; /* border-box */ + margin-right: 30px; +} + +.filelist-panel { + -webkit-box-flex: 1; + -webkit-box-orient: vertical; + display: -webkit-box; +} + +#list-container { + -webkit-box-flex: 1; + -webkit-box-orient: vertical; + display: -webkit-box; + position: relative; +} + +#detail-table { + -webkit-box-flex: 1; + -webkit-box-orient: vertical; + display: -webkit-box; +} + +#detail-table > list, +.thumbnail-grid { + -webkit-box-flex: 1; + padding-bottom: 0; /* For the preview panel. Will be overridden by JS. */ +} + +#file-list .drag-selection-border { + -webkit-box-sizing: border-box; + background-color: rgba(255, 255, 255, 0.3); + border: 2px solid rgba(255, 255, 255, 0.6); + outline: 1px solid rgba(0, 0, 0, 0.1); + position: absolute; + z-index: 2; +} + +.spinner { + background: 100% url(../images/common/spinner.svg); + height: 16px; + left: 50%; + margin-left: -8px; + margin-top: -8px; + opacity: 0.5; + position: absolute; + top: 50%; + width: 16px; +} + +.spinner-layer { + background: url(../images/common/spinner.svg) center / 16px no-repeat; + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + z-index: 525; +} + +.downloads-warning { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + background-color: #f0f0f0; + background-image: -webkit-image-set( + url('../images/files/ui/warning_icon_square.png') 1x, + url('../images/files/ui/2x/warning_icon_square.png') 2x); + background-position: 15px center; + background-repeat: no-repeat; + color: #666; + display: -webkit-box; + font-size: 13px; + height: 57px; + overflow: hidden; + padding-left: 57px; /* Make space for the icon. */ + transition: height 70ms linear; +} + +.downloads-warning[hidden] { + display: -webkit-box !important; /* Overrides [hidden] for animation. */ + height: 0; +} + +@-webkit-keyframes heightAnimation { + 0% { + height: 0; + display: -webkit-box; + } +} + +/* Drive space warning banner. */ +.volume-warning { + -webkit-animation: heightAnimation 70ms linear; + -webkit-box-align: center; + -webkit-box-orient: horizontal; + background-image: url(chrome://resources/images/clouds.png); + background-repeat: repeat-x; + background-size: 150px 44px; + color: #333; + display: -webkit-box; + font-size: 13px; + height: 44px; + overflow: hidden; + position: relative; +} + +.volume-warning[hidden] { + border-top-width: 0; + height: 0; +} + +.volume-warning .drive-icon { + background-image: -webkit-image-set( + url('../images/files/ui/drive_logo.png') 1x, + url('../images/files/ui/2x/drive_logo.png') 2x); + background-position: center; + background-repeat: no-repeat; + background-size: 25px 22px; + height: 44px; + width: 50px; +} + +.volume-warning .drive-text { + margin-right: 11px; +} + +/* The cr.ui.Grid representing the detailed file list. */ +.thumbnail-grid { + overflow-y: auto; + padding-bottom: 0; /* For the preview panel. Will be overridden by JS. */ + width: 100%; +} + +body[type='full-page'] .thumbnail-frame > .img-container { + position: relative; +} + +body[type='full-page'] .thumbnail-frame > .img-container, +body[type='full-page'] .detail-name .detail-icon { + cursor: pointer; +} + +.img-container > img { + -webkit-user-drag: none; + position: absolute; +} + +.img-container > img:not(.cached):not(.drag-thumbnail) { + -webkit-animation: fadeIn 250ms linear; +} + +.thumbnail-bottom { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + bottom: 0; + cursor: auto; + display: -webkit-box; + left: 0; + padding: 0 10px; + position: absolute; + right: 0; +} + +.thumbnail-bottom .filename-label { + -webkit-box-flex: 1; +} + +/* Styles specific for the grid view. */ + +.thumbnail-grid .thumbnail-item { + -webkit-margin-start: 21px; + border: 3px solid transparent; /* Selection will make the border visible. */ + margin-top: 20px; + position: relative; +} + +.thumbnail-grid .thumbnail-frame { + background-color: rgb(245, 245, 245); + height: 120px; + overflow: hidden; + position: relative; + width: 160px; +} + +.thumbnail-grid .thumbnail-item[selected] .thumbnail-frame, +.thumbnail-grid .thumbnail-item.accepts .thumbnail-frame { + border-color: white; +} + +.thumbnail-grid .img-container { + height: 100%; + width: 100%; +} + +.thumbnail-grid .thumbnail-bottom { + background: rgba(0, 0, 0, 0.55); + color: #fff; + height: 30px; +} + +/* Padding counterweights negative margins of items, thus eliminating scroll + bar when it's not needed. Max height is set to fit 8 items before showing + scroll bar. */ +#default-actions-list { + max-height: 328px; + padding: 1px 0; +} + +#default-actions-list > li > * { + background-position: 5px center; + background-repeat: no-repeat; + background-size: 16px 16px; + padding-left: 26px; +} + +#list-container list > li[selected], +#list-container grid > li[selected], +#default-actions-list > li[selected] { + background-color: rgb(225, 225, 225); +} + +#list-container list:focus > li[selected], +#list-container grid:focus > li[selected], +#default-actions-list:focus > li[selected] { + background-color: rgb(66, 129, 244); + color: white; +} + +#list-container list > li.accepts[selected], +#list-container grid > li.accepts[selected] { + background-color: rgb(215, 215, 215); +} + +#list-container list:focus > li.accepts[selected], +#list-container grid:focus > li.accepts[selected] { + background-color: rgb(48, 125, 254); +} + +#list-container list > li.accepts, +#list-container grid > li.accepts { + background-color: #f1f1f1; +} + +#list-container.nohover grid > .accepts { + background-color: transparent; +} + +#directory-tree .tree-item.accepts > .tree-row, +#navigation-list > .accepts, +#list-container list > li.accepts, +#list-container grid > li.accepts { + -webkit-animation: acceptsBlink 200ms linear 1s 3; +} + +@-webkit-keyframes acceptsBlink { + 0% { + background-color: transparent; + } +} + +.table-row-cell .selection-label { + -webkit-margin-end: 10px; + height: 15px; +} + +.table-row-cell .filename-label, +.thumbnail-item .filename-label, +/* Show ellipsis in cells. The name column has different structure and overrides + this rule. */ +.table-row-cell > div { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Text box used for renaming in the detail list. */ +.table-row-cell input.rename { + border-width: 0; + padding: 2px 0; +} + +input.rename:focus { + outline-color: rgb(77, 144, 254); +} + +input.rename { + font: inherit; + line-height: 1; + text-align: inherit; +} + +.table-row-cell .filename-label, +.table-row-cell input.rename { + -webkit-box-flex: 1; +} + +[renaming] > .filename-label { + display: none; +} + +/* Text box used for renaming in the thumbnail list. */ +.thumbnail-grid input.rename { + -webkit-margin-start: -1px; + box-sizing: border-box; + height: 20px; + width: 114px; +} + +/* The cr.ui.Table representing the detailed file list. */ +.detail-table { + width: 100%; +} + +/* Bottom pane describing current selection. */ +.preview-panel { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-transition: background-color 150ms ease; + background: linear-gradient( + to bottom, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 1)); + border-top: 1px solid rgba(20, 20, 22, 0.1); + bottom: 0; + display: -webkit-box; + height: 51px; + left: 0; + opacity: 1; + padding: 0 10px 0 7px; + position: absolute; + right: 0; + z-index: 3; +} + +.preview-panel[visibility=hiding] { + -webkit-transform: translate(0, 5px); + opacity: 0; + /* Using all seems to cause preview panel and checkbox flicking issue. */ + transition: opacity 220ms ease, + -webkit-transform 220ms ease; +} + +.preview-panel[visibility=hidden] { + display: none; + opacity: 0; +} + +.preview-panel > .left, +.dialog-footer > .left { + -webkit-box-align: center; + -webkit-box-flex: 1; + -webkit-box-orient: horizontal; + display: -webkit-box; +} + +.preview-panel > .right, +.dialog-footer > .right { + -webkit-box-pack: end; +} + +.preview-panel .preparing-label { + -webkit-margin-start: 30px; +} + +.preview-panel .progress-bar { + -webkit-box-flex: 1; +} + +.preview-thumbnails { + -webkit-box-orient: horizontal; + display: -webkit-box; + padding-left: 25px; +} + +.preview-thumbnails > .img-container { + background-color: white; + background-size: 35px 35px; /* For file icons. */ + border: 2px solid white; + box-shadow: 0 1px 1px rgba(80, 80, 80, 0.5); + box-sizing: border-box; + cursor: pointer; + height: 35px; + margin: 0 0 0 -25px; /* Overlapped images. */ + overflow: hidden; + position: relative; + width: 35px; +} + +.preview-thumbnails > .popup { + -webkit-transform: translate(0, 3px) scale(0.95); + background-color: #f2f2f2; + border: 2px solid #fff; + bottom: 8px; + box-shadow: 0 0 0 1px #F0F0F0, + 0 0 0 2px #D0D0D0, + 2px 2px 6px rgba(0, 0, 0, 0.2); + display: -webkit-flex; + left: -8px; + opacity: 0; + pointer-events: none; + position: absolute; + transition: opacity 180ms ease-in 300ms, + -webkit-transform 180ms ease-in 300ms; + z-index: 1000; +} + +.preview-thumbnails.has-zoom:hover > .popup { + -webkit-transform: translate(0, 0) scale(1.0); + opacity: 1; + pointer-events: auto; +} + +@-webkit-keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.preview-thumbnails img { + -webkit-animation: fadeIn 180ms ease-in-out; +} + +.preview-thumbnails > .popup > img { + -webkit-flex: 1 1 0; + -webkit-user-drag: none; +} + +/* Table splitter element */ +.table-header-splitter { + background-image: -webkit-image-set( + url('../images/files/ui/vertical_separator.png') 1x, + url('../images/files/ui/2x/vertical_separator.png') 2x); + background-position: center; + background-repeat: repeat-y; + height: 20px; + top: 10px; + width: 5px; +} + +.table-header-splitter:last-child { + display: none; +} + +/* Container for a table header. */ +.table-header { + box-sizing: border-box; + height: 47px; +} + +.table-header-sort-image-desc::after { + -webkit-padding-start: 13px; + background-image: -webkit-image-set( + url('../images/files/ui/sort_desc.png') 1x, + url('../images/files/ui/2x/sort_desc.png') 2x); + background-position: center center; + background-repeat: no-repeat; + color: #888; + content: '\00a0'; + position: relative; + top: 1px; +} + +.table-header-sort-image-asc::after { + -webkit-padding-start: 13px; + background-image: -webkit-image-set( + url('../images/files/ui/sort_asc.png') 1x, + url('../images/files/ui/2x/sort_asc.png') 2x); + background-position: center center; + background-repeat: no-repeat; + color: #888; + content: '\00a0'; + position: relative; + top: -1px; +} + +.preview-container .table-header { + border-radius: 0 4px 0 0; +} + +/* Text label in a table header. */ +.table-header-label { + color: rgb(100, 100, 100); + line-height: 40px; + margin: 0 7px; +} + +.table-row-cell > * { + -webkit-box-align: center; + -webkit-box-flex: 1; + -webkit-box-orient: horizontal; + padding: 0 10px; +} + +.table-row-cell { + color: rgb(100, 100, 100); +} + +.table-row-cell > .detail-name { + display: -webkit-box; +} + +.table-row-cell > .detail-name { + color: rgb(0, 0, 0); +} + + +#list-container list:focus > [selected] .table-row-cell, +#list-container list:focus > [selected] .detail-name { + color: white; +} + +.table-row-cell { + -webkit-box-align: center; +} + +.file-checkbox { + -webkit-margin-end: 0; + -webkit-margin-start: 0; + position: relative; + z-index: 2; +} + +#select-all-checkbox { + -webkit-margin-end: 13px; + -webkit-margin-start: 3px; + margin-bottom: 0; + margin-top: 0; + vertical-align: middle; +} + +#list-container .table-header #select-all-checkbox, +#list-container li.table-row .file-checkbox { + -webkit-appearance: none; + background-image: -webkit-image-set( + url('../images/files/ui/select_checkbox.png') 1x, + url('../images/files/ui/2x/select_checkbox.png') 2x); + background-position: center; + background-repeat: no-repeat; + border-style: none; + height: 15px; + width: 15px; +} + +#list-container li.table-row .file-checkbox { + vertical-align: top; +} + +#list-container .table-header #select-all-checkbox::after, +#list-container li.table-row .file-checkbox::after { + content: none; +} + +#list-container .table-header #select-all-checkbox:checked, +#list-container li.table-row .file-checkbox:checked { + background-image: -webkit-image-set( + url('../images/files/ui/select_checkbox_checked.png') 1x, + url('../images/files/ui/2x/select_checkbox_checked.png') 2x); +} + +#list-container .table-header #select-all-checkbox:checked, +#list-container list li.table-row[selected] .file-checkbox { + -webkit-filter: brightness(0) opacity(40%); +} + +#list-container list:focus li.table-row[selected] .file-checkbox { + -webkit-filter: brightness(0) invert(); +} + +#list-container li.table-row, +#default-actions-list li { + height: 29px; + line-height: 29px; +} + +/* The icon in the name column. See file_types.css for specific icons. */ +.detail-icon { + height: 24px; + width: 24px; +} + +#detail-table .detail-icon { + /* To shift the icon position. */ + margin-bottom: 2px; +} + +.metadata-item { + -webkit-box-flex: 1; + -webkit-box-orient: horizontal; + -webkit-padding-start: 8px; + display: -webkit-box; +} + +.metadata-label { + -webkit-margin-end: 6px; +} + +.preview-panel .spacer { + -webkit-box-flex: 1; +} + +#delete-button { + min-width: 21px; /* overrride */ + padding: 0; /* overrride */ + width: 21px; +} + +#delete-button::before { + /* Background image should be specified in the before pseudo element because + * border image fill is specified to delete-button. */ + background: -webkit-image-set( + url(../images/files/ui/onbutton_trash.png) 1x, + url(../images/files/ui/2x/onbutton_trash.png) 2x) no-repeat center; + content: ''; + display: block; + height: 100%; + width: 100%; +} + +#delete-button[disabled] { + display: none; +} + +#tasks-menu menuitem:not(.change-default) { + background-position: left 10px center; + padding-left: 32px; +} + +#share-button { + display: block; + min-width: 0; /* overrride */ +} + +#preview-lines { + -webkit-box-flex: 1; + -webkit-margin-end: 10px; + -webkit-margin-start: 10px; + vertical-align: middle; +} + +/* The selection summary text at the bottom of the preview pane. */ +.preview-summary { + color: #666; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; + white-space: nowrap; +} + +.preview-summary .calculating-size { + margin-left: 5px; + opacity: 0.5; +} + +.detail-name > * { + -webkit-box-align: center; + display: -webkit-box; +} + +/* Overriding input.common[type='checkbox'] rule in common.css. */ +.detail-name > input.common[type='checkbox'] { + -webkit-margin-end: 4px; + -webkit-margin-start: -1px; + border-color: #444; +} + +list .detail-name > .file-checkbox::before, +.pin::before { + /* Invisible area that reacts on mouse events. */ + content: ''; + display: -webkit-box; + height: 38px; + left: -8px; + position: absolute; + right: -9px; + top: -14px; +} + +#filename-input-box { + -webkit-box-align: center; + -webkit-box-flex: 1; + display: -webkit-box; +} + +#filename-input-box input { + -webkit-box-flex: 1; + display: -webkit-box; + padding: 1px 2px; +} + +#filename-input-box .filename-label { + -webkit-box-orient: horizontal; + background-color: white; + color: #333; + display: -webkit-box; + padding-right: 4px; +} + +body:not([type='saveas-file']) #filename-input-box { + display: none; +} + +/* A vertical spring. */ +.vertical-spacer { + -webkit-box-flex: 1; + -webkit-box-orient: vertical; + display: -webkit-box; +} + +/* Dimmed items */ + +body[type='folder'] .file, +body[type='upload-folder'] .file, +body[drive] .dialog-container[connection='offline'] .dim-offline { + opacity: 0.4; +} + +/* Overlay pane covering the entire file manager window (e.g. image editor)*/ +.overlay-pane { + -webkit-app-region: no-drag; + border: none; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + z-index: 1000; /* Must be above all elements in file manager container. */ +} + +/* When the overlay pane is visible hide everything else so that the tab order + is not confused. */ +body[overlay-visible] > :not(.overlay-pane) { + display: none !important; +} + +/* Invisible container for elements representing files while dragging. */ +#drag-container { + left: 0; + /* Hack for extra margins caused by setDragImage(). */ + padding: 1000px 0 0 1000px; + position: fixed; + top: 0; + z-index: -1; /* below .dialog-container */ +} + +#drag-container .drag-contents { + -webkit-box-orient: horizontal; + background-color: #fafafa; + border: 1px solid #bbb; + border-radius: 3px; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .2); + display: -webkit-box; + margin-bottom: 5px; + padding: 6px; + transition: opacity 200ms ease-in; +} + +#drag-container .drag-contents.for-image { + padding: 2px; +} + +#drag-container .thumbnail-item { + -webkit-box-orient: horizontal; + display: -webkit-box; +} + +/* When changing these properties please preserve these conditions: + 1. width == height (so that the rotated thumbnail does not look off-center) + 2. width % 8 == 0 (to minimize rounding errors in the centering code) */ +#drag-container .img-container { + -webkit-box-flex: 0; + display: -webkit-box; + height: 64px; + overflow: hidden; + position: relative; + width: 64px; +} + +#drag-container .label { + -webkit-box-flex: 1; + font-weight: bold; + line-height: 24px; + max-width: 320px; + overflow: hidden; + padding: 0 5px; + text-overflow: ellipsis; + white-space: nowrap; +} + +menu.file-context-menu { + z-index: 600; /* Must be below the overlay pane (1000). */ +} + +menu.chrome-menu hr { + color: transparent; + font-size: 0; +} + +div.offline { + -webkit-box-pack: center; + display: -webkit-box; +} + +div.offline > * { + -webkit-box-align: center; + display: -webkit-box; +} + +div.shade { + /* transition: opacity 1000ms linear; */ + background-color: rgba(255, 255, 255, 0.8); + bottom: 0; + left: 0; + opacity: 0; + position: absolute; + right: 0; + top: 0; +} + +div.shade[fadein] { + opacity: 1; +} + +/* Message panel for unmounted Drive */ +#unmounted-panel, +#format-panel { + bottom: 0; + color: #333; + display: none; + left: 0; + padding-left: 50px; + padding-top: 20px; + position: absolute; + right: 0; + top: 0; +} + +body[drive='mounting'] .dialog-container #unmounted-panel, +body[drive='error'] .dialog-container #unmounted-panel, +body[unformatted] .dialog-container #format-panel { + display: block; +} + +body[drive='unmounted'] .dialog-container .filelist-panel, +body[drive='mounting'] .dialog-container .filelist-panel, +body[drive='error'] .dialog-container .filelist-panel, +body[unformatted] .dialog-container .filelist-panel { + /* Hide file list when Drive is not mounted. + Use opacity to avoid manual resizing.*/ + opacity: 0; +} + +#unmounted-panel > *, +#format-panel > * { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: start; + display: none; + height: 22px; + margin-bottom: 10px; +} + +#unmounted-panel > .loading { + position: relative; +} + +#unmounted-panel > .loading > .spinner-box { + bottom: 0; + position: absolute; + right: 100%; + top: 0; + width: 40px; +} + +body[unformatted] #format-panel > .error, +body[drive='mounting'] #unmounted-panel > .loading, +body[drive='error'] #unmounted-panel > .error, +#format-panel > #format-button, +#unmounted-panel.retry-enabled > .learn-more { + display: -webkit-box; +} + +.plain-link { + color: rgb(17, 85, 204); + cursor: pointer; + text-decoration: none; +} + +.buttonbar > * { + position: relative; +} + +.buttonbar .tooltip, +.topbutton-bar .tooltip { + right: -12px; + top: 35px; +} + +/* Tooltips */ +.tooltip { + background: #2d2d2d; + border-radius: 0; + box-shadow: 1px 2px 4px #ccc; + box-sizing: border-box; + color: white; + display: block; + font-size: 11px; + font-weight: bold; + height: 29px; + line-height: 29px; + margin-left: -20px; + min-width: 50px; + opacity: 0; + outline: 1px solid rgba(255, 255, 255, 0.5); + padding: 0 10px; + pointer-events: none; + position: absolute; + text-align: center; + top: 5px; + white-space: nowrap; + z-index: 600; /* Must be below the overlay pane (1000). */ +} + +.tooltip::after, +.tooltip::before { + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: transparent; + content: ''; + display: block; + margin-left: -5px; + position: absolute; + right: 24px; + top: -5px; +} + +.tooltip::after { + border-bottom: 5px solid #2d2d2d; +} + +.tooltip::before { + border-bottom: 5px solid rgba(255, 255, 255, 0.5); +} + +/* Show with delay, disappear instantly */ +@-webkit-keyframes tooltip-show { + 0% { opacity: 0; } + 90% { opacity: 0; } + 100% { opacity: 1; } +} + +:hover > .tooltip { + -webkit-animation-duration: 800ms; + -webkit-animation-iteration-count: 1; + -webkit-animation-name: tooltip-show; + -webkit-animation-timing-function: linear; + opacity: 1; +} + +#no-search-results { + bottom: 0; + display: none; + left: 0; + padding: 10px; + position: absolute; + right: 0; + top: 28px; /* Leave room for the file list header. */ +} + +.dialog-container:not([drive-welcome='page']) #no-search-results[show] { + display: block; +} + +#volume-space-info-contents { + -webkit-box-align: center; + display: -webkit-box; +} + +#volume-space-info-contents > div { + -webkit-box-flex: 1; + -webkit-margin-start: 15px; + display: -webkit-box; +} + +#list-container .table-header-inner { + height: 100%; +} + +#list-container .table-header-cell:hover { + background-color: inherit; +} + +#list-container .table-header-cell:first-child { + -webkit-box-sizing: border-box; + -webkit-padding-start: 8px; +} + +button:focus { + outline-color: rgb(77, 144, 254); +} + +#new-folder-button { + margin-right: 30px; +} + +#default-action-dialog { + min-width: 300px; + width: auto; +} + +.drive-welcome-wrapper { + /* drive_welcome.css will override it once loaded. */ + display: none; +} + +list.autocomplete-suggestions { + -webkit-margin-before: -7px; + -webkit-margin-start: -38px; + background-color: rgb(250, 250, 250); + border-radius: 3px; + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, .5); + box-sizing: border-box; /* To match the width with the search box's. */ + color: rgb(34, 34, 34); + overflow: hidden; + padding: 5px 0; + position: fixed; + width: 300px !important; /* This overrides the value specified by script. */ + z-index: 550; +} + +list.autocomplete-suggestions > li { + -webkit-box-align: center; + display: -webkit-box; + padding: 3px 0; +} + +list.autocomplete-suggestions > li > div.detail-icon { + -webkit-margin-end: 6px; + -webkit-margin-start: 6px; +} + +list.autocomplete-suggestions > li > div.detail-text { + -webkit-box-flex: 1; + overflow-x: hidden; + text-overflow: ellipsis; +} + +list.autocomplete-suggestions > li > div.detail-text em { + color: rgb(150, 150, 150); + font-style: normal; +} + +list.autocomplete-suggestions > li > div[search-icon] { + background: -webkit-image-set( + url('../images/files/ui/search_icon_active.png') 1x, + url('../images/files/ui/2x/search_icon_active.png') 2x); + background-position: center; + background-repeat: no-repeat; +} + +list.autocomplete-suggestions > li[selected] > div[search-icon], +list.autocomplete-suggestions > li[lead] > div[search-icon] { + -webkit-filter: brightness(0) invert(); +} + +list.autocomplete-suggestions > [selected], +list.autocomplete-suggestions > [lead] { + background-color: rgb(66, 129, 244); + color: white; +} + +list.autocomplete-suggestions > [selected] > div.detail-text em, +list.autocomplete-suggestions > [lead] > div.detail-text em { + color: white; +} + +#gear-menu { + margin-top: 8px; +} + +#gear-menu > menuitem:not(.menuitem-button) { + margin-right: 50px; +} + +/* View buttons in the gear menu. */ + +menuitem#detail-view { + background-image: -webkit-image-set( + url('../images/files/ui/button_list_view.png') 1x, + url('../images/files/ui/2x/button_list_view.png') 2x); +} + +menuitem#detail-view[selected]:not([disabled]), +menuitem#detail-view[lead]:not([disabled]) { + background-image: -webkit-image-set( + url('../images/files/ui/button_list_view_white.png') 1x, + url('../images/files/ui/2x/button_list_view_white.png') 2x); +} + +menuitem#thumbnail-view { + background-image: -webkit-image-set( + url('../images/files/ui/button_mosaic_view.png') 1x, + url('../images/files/ui/2x/button_mosaic_view.png') 2x); +} + +menuitem#thumbnail-view[selected]:not([disabled]), +menuitem#thumbnail-view[lead]:not([disabled]) { + background-image: -webkit-image-set( + url('../images/files/ui/button_mosaic_view_white.png') 1x, + url('../images/files/ui/2x/button_mosaic_view_white.png') 2x); +} + +#iframe-drag-area { + -webkit-app-region: drag; + height: 48px; + left: 64px; + position: absolute; + right: 92px; + top: 0; + width: auto; + z-index: 101; +} + +#suggest-app-dialog { + background-color: #fff; + border: 0; + padding: 0; + width: auto; +} + +#suggest-app-dialog .cr-dialog-title { + /* Entire height: 44px (content-box 22px + padding 11px * 2) */ + font-size: 16px; + height: 22px; + margin: 0; + padding: 11px 18px; +} + +#suggest-app-dialog #webview-container { + border-bottom: solid 1px #bbb; + border-top: solid 1px #bbb; + position: relative; +} + +#suggest-app-dialog.show-spinner #webview-container webview { + pointer-events: none; +} + +#suggest-app-dialog:not(.show-spinner) .spinner-layer { + display: none; +} + +#suggest-app-dialog .spinner-layer { + background-color: rgba(255, 255, 255, 0.7); +} + +#suggest-app-dialog .cr-dialog-buttons, +#suggest-app-dialog .cr-dialog-ok, +#suggest-app-dialog .cr-dialog-cancel { + display: none; +} + +#suggest-app-dialog .cr-dialog-text { + -webkit-padding-after: 10px; + -webkit-padding-before: 0; + -webkit-padding-end: 20px; + -webkit-padding-start: 20px; + margin: 0; +} + +#suggest-app-dialog #buttons { + background: #eee; + width: 100%; +} + +#suggest-app-dialog #buttons > #webstore-button { + -webkit-padding-after: 10px; + -webkit-padding-before: 10px; + -webkit-padding-end: 10px; + -webkit-padding-start: 36px; + background-image: -webkit-image-set( + url('chrome://theme/IDR_WEBSTORE_ICON_16') 1x, + url('chrome://theme/IDR_WEBSTORE_ICON_16@2x') 2x); + background-position: 12px center; + background-repeat: no-repeat; + color: #00f; + cursor: pointer; + display: inline-block; + height: 16px; +} + +.cr-dialog-frame.error-dialog-frame { + width: 300px; +} + +.error-dialog-frame .error-dialog-img { + background-image: -webkit-image-set( + url('chrome://theme/IDR_ERROR_NETWORK_GENERIC') 1x, + url('chrome://theme/IDR_ERROR_NETWORK_GENERIC@2x') 2x); + background-position: center; + background-repeat: no-repeat; + height: 40px; +} + +.error-dialog-frame .cr-dialog-cancel { + display: none; +} + +.error-dialog-frame .cr-dialog-close, +.error-dialog-frame .cr-dialog-title { + display: none; +} + +.error-dialog-frame .cr-dialog-text { + text-align: center; +} + +.cr-dialog-frame.share-dialog-frame { + background-color: white; + width: auto; +} + +.share-dialog-webview-wrapper { + height: 100px; + margin-top: 10px; + min-width: 300px; + overflow: hidden; + transition: height 200ms ease; +} + +.share-dialog-webview { + height: 100%; + width: 100%; +} + +.share-dialog-webview-wrapper:not(.loaded) .share-dialog-webview { + visibility: hidden; +} + +.share-dialog-frame .cr-dialog-text, +.share-dialog-frame .cr-dialog-buttons { + display: none; +} + +#conflict-confirm-dialog .cr-dialog-buttons { + align-items: baseline; + display: flex; +} + +#conflict-confirm-dialog input[type=checkbox] { + -webkit-margin-start: -2px; + width: auto; +} + +#conflict-confirm-dialog label { + flex: 1 0 auto; +} + +/* Progress center */ + +@-webkit-keyframes progress-center-toggle { + /* Height values of each frame are set by script. */ + from { + } + to { + } +} + +#progress-center { + background-color: transparent; + border-top: 1px solid transparent; + overflow: hidden; + position: relative; + transition: background-color 300ms linear, + border 300ms linear; +} + +#progress-center.opened { + background-color: #ebebeb; + border-top: 1px solid #d8d8d8; +} + +#progress-center.animated { + -webkit-animation: progress-center-toggle 300ms ease-out; +} + +#progress-center-open-view { + opacity: 1; + padding-top: 42px; + transition: opacity 300ms linear; +} + +#progress-center:not(.opened) #progress-center-open-view { + opacity: 0; + pointer-events: none; + position: absolute; +} + +#progress-center-close-view { + opacity: 1; + transition: opacity 300ms linear; +} + +#progress-center.opened #progress-center-close-view { + opacity: 0; + pointer-events: none; + position: absolute; +} + +#progress-center.animated #progress-center-open-view, +#progress-center.animated #progress-center-close-view { + left: 0; + pointer-events: none; + position: absolute; + right: 0; + top: 0; + z-index: 1; +} + +#progress-center li { + display: block; + min-height: 29px; /* label 17px + frame 12px */ + padding-bottom: 20px; +} + +#progress-center label { + color: #777; + display: block; + font: 12px/17px normal; + min-height: 17px; + overflow: hidden; + padding: 0 20px; + text-overflow: ellipsis; + white-space: nowrap; +} + +#progress-center li.error label { + white-space: normal; +} + +#progress-center .progress-frame { + -webkit-padding-end: 10px; + -webkit-padding-start: 20px; + align-items: center; + display: flex; + margin-top: 4px; +} + +#progress-center li.error .progress-frame { + height: 0; +} + +#progress-center .progress-bar { + -webkit-margin-end: 24px; + background: #d8d8d8; + border-radius: 3px; + flex: 1 0 0; + height: 6px; + opacity: 1; + overflow: hidden; +} + +#progress-center li.error .progress-bar, +#progress-center li.canceled .progress-bar { + visibility: hidden; +} + +#progress-center .progress-track { + background: #787878; + height: 100%; +} + +#progress-center .progress-track.animated { + transition: width 300ms linear; +} + +#progress-center button { + background: orange; + border: none; + cursor: pointer; + flex: 0 0 auto; + height: 12px; + min-height: 0; + min-width: 0; + outline: none; + padding: 0; + width: 12px; + z-index: 0; +} + +#progress-center button.toggle { + background: -webkit-image-set( + url(../images/files/ui/process_drawer_button_opened.png) 1x, + url(../images/files/ui/2x/process_drawer_button_opened.png) 2x) + no-repeat; + position: absolute; + right: 10px; + top: 17px; /* label height */ + transition: top 300ms ease-out; + z-index: 1; +} + +#progress-center button.toggle:hover { + background: -webkit-image-set( + url(../images/files/ui/process_drawer_button_opened_hover.png) 1x, + url(../images/files/ui/2x/process_drawer_button_opened_hover.png) 2x) + no-repeat; +} + +#progress-center button.toggle:active { + background: -webkit-image-set( + url(../images/files/ui/process_drawer_button_opened_pressed.png) 1x, + url(../images/files/ui/2x/process_drawer_button_opened_pressed.png) 2x) + no-repeat; +} + +/* + * If the closed progress center has only one item, + * toggle button turned into cancel button the item. + */ +#progress-center:not(.opened) #progress-center-close-view.single ~ + button.toggle, +#progress-center button.cancel { + background: -webkit-image-set( + url(../images/files/ui/close_bar.png) 1x, + url(../images/files/ui/2x/close_bar.png) 2x) + no-repeat; +} + +#progress-center:not(.opened) + #progress-center-close-view.single:not(.cancelable) button.toggle, +#progress-center li:not(.cancelable) button.cancel { + visibility: hidden; +} + +#progress-center.opened button.toggle { + background: -webkit-image-set( + url(../images/files/ui/process_drawer_button_closed.png) 1x, + url(../images/files/ui/2x/process_drawer_button_closed.png) 2x) + no-repeat; + top: 10px; +} + +#progress-center.opened button.toggle:hover { + background: -webkit-image-set( + url(../images/files/ui/process_drawer_button_closed_hover.png) 1x, + url(../images/files/ui/2x/process_drawer_button_closed_hover.png) 2x) + no-repeat; +} + +#progress-center.opened button.toggle:active { + background: -webkit-image-set( + url(../images/files/ui/process_drawer_button_closed_pressed.png) 1x, + url(../images/files/ui/2x/process_drawer_button_closed_pressed.png) 2x) + no-repeat; +} diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/file_types.css b/chromium/chrome/browser/resources/file_manager/foreground/css/file_types.css new file mode 100644 index 00000000000..75603497f07 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/css/file_types.css @@ -0,0 +1,477 @@ +/* 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. */ + +/* Small icons for file types, used in lists and menus. */ +[file-type-icon] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/generic.png') 1x, + url('../../common/images/file_types/200/generic.png') 2x); + background-position: center; + background-repeat: no-repeat; +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon], +list:focus [selected] [file-type-icon], +list.autocomplete-suggestions [selected] [file-type-icon] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/generic_white.png') 1x, + url('../../common/images/file_types/200/generic_white.png') 2x); +} + +[file-type-icon='archive'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/archive.png') 1x, + url('../../common/images/file_types/200/archive.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='archive'], +list:focus [selected] [file-type-icon='archive'], +list.autocomplete-suggestions [selected] [file-type-icon='archive'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/archive_white.png') 1x, + url('../../common/images/file_types/200/archive_white.png') 2x); +} + +[file-type-icon='audio'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/audio.png') 1x, + url('../../common/images/file_types/200/audio.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='audio'], +list:focus [selected] [file-type-icon='audio'], +list.autocomplete-suggestions [selected] [file-type-icon='audio'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/audio_white.png') 1x, + url('../../common/images/file_types/200/audio_white.png') 2x); +} + +[file-type-icon='excel'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/excel.png') 1x, + url('../../common/images/file_types/200/excel.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='excel'], +list:focus [selected] [file-type-icon='excel'], +list.autocomplete-suggestions [selected] [file-type-icon='excel'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/excel_white.png') 1x, + url('../../common/images/file_types/200/excel_white.png') 2x); +} + +[file-type-icon='folder'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/folder.png') 1x, + url('../../common/images/file_types/200/folder.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='folder'], +list:focus [selected] [file-type-icon='folder'], +list.autocomplete-suggestions [selected] [file-type-icon='folder'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/folder_white.png') 1x, + url('../../common/images/file_types/200/folder_white.png') 2x); +} + +[file-type-icon='form'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/form.png') 1x, + url('../../common/images/file_types/200/form.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='form'], +list:focus [selected] [file-type-icon='form'], +list.autocomplete-suggestions [selected] [file-type-icon='form'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/form_white.png') 1x, + url('../../common/images/file_types/200/form_white.png') 2x); +} + +[file-type-icon='gdoc'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/gdoc.png') 1x, + url('../../common/images/file_types/200/gdoc.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='gdoc'], +list:focus [selected] [file-type-icon='gdoc'], +list.autocomplete-suggestions [selected] [file-type-icon='gdoc'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/gdoc_white.png') 1x, + url('../../common/images/file_types/200/gdoc_white.png') 2x); +} + +[file-type-icon='gdraw'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/gdraw.png') 1x, + url('../../common/images/file_types/200/gdraw.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='gdraw'], +list:focus [selected] [file-type-icon='gdraw'], +list.autocomplete-suggestions [selected] [file-type-icon='gdraw'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/gdraw_white.png') 1x, + url('../../common/images/file_types/200/gdraw_white.png') 2x); +} + +[file-type-icon='glink'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/glink.png') 1x, + url('../../common/images/file_types/200/glink.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='glink'], +list:focus [selected] [file-type-icon='glink'], +list.autocomplete-suggestions [selected] [file-type-icon='glink'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/glink_white.png') 1x, + url('../../common/images/file_types/200/glink_white.png') 2x); +} + +[file-type-icon='gsheet'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/gsheet.png') 1x, + url('../../common/images/file_types/200/gsheet.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='gsheet'], +list:focus [selected] [file-type-icon='gsheet'], +list.autocomplete-suggestions [selected] [file-type-icon='gsheet'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/gsheet_white.png') 1x, + url('../../common/images/file_types/200/gsheet_white.png') 2x); +} + +[file-type-icon='gslides'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/gslides.png') 1x, + url('../../common/images/file_types/200/gslides.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='gslides'], +list:focus [selected] [file-type-icon='gslides'], +list.autocomplete-suggestions [selected] [file-type-icon='gslides'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/gslides_white.png') 1x, + url('../../common/images/file_types/200/gslides_white.png') 2x); +} + +[file-type-icon='gtable'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/gtable.png') 1x, + url('../../common/images/file_types/200/gtable.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='gtable'], +list:focus [selected] [file-type-icon='gtable'], +list.autocomplete-suggestions [selected] [file-type-icon='gtable'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/gtable_white.png') 1x, + url('../../common/images/file_types/200/gtable_white.png') 2x); +} + +[file-type-icon='gform'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/form.png') 1x, + url('../../common/images/file_types/200/form.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='gform'], +list:focus [selected] [file-type-icon='gform'], +list.autocomplete-suggestions [selected] [file-type-icon='gform'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/form_white.png') 1x, + url('../../common/images/file_types/200/form_white.png') 2x); +} + +[file-type-icon='image'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/image.png') 1x, + url('../../common/images/file_types/200/image.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='image'], +list:focus [selected] [file-type-icon='image'], +list.autocomplete-suggestions [selected] [file-type-icon='image'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/image_white.png') 1x, + url('../../common/images/file_types/200/image_white.png') 2x); +} + +[file-type-icon='pdf'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/pdf.png') 1x, + url('../../common/images/file_types/200/pdf.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='pdf'], +list:focus [selected] [file-type-icon='pdf'], +list.autocomplete-suggestions [selected] [file-type-icon='pdf'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/pdf_white.png') 1x, + url('../../common/images/file_types/200/pdf_white.png') 2x); +} + +[file-type-icon='ppt'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/ppt.png') 1x, + url('../../common/images/file_types/200/ppt.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='ppt'], +list:focus [selected] [file-type-icon='ppt'], +list.autocomplete-suggestions [selected] [file-type-icon='ppt'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/ppt_white.png') 1x, + url('../../common/images/file_types/200/ppt_white.png') 2x); +} + +[file-type-icon='script'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/script.png') 1x, + url('../../common/images/file_types/200/script.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='script'], +list:focus [selected] [file-type-icon='script'], +list.autocomplete-suggestions [selected] [file-type-icon='script'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/script_white.png') 1x, + url('../../common/images/file_types/200/script_white.png') 2x); +} + +[file-type-icon='sites'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/sites.png') 1x, + url('../../common/images/file_types/200/sites.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='sites'], +list:focus [selected] [file-type-icon='sites'], +list.autocomplete-suggestions [selected] [file-type-icon='sites'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/sites_white.png') 1x, + url('../../common/images/file_types/200/sites_white.png') 2x); +} + +[file-type-icon='video'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/video.png') 1x, + url('../../common/images/file_types/200/video.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='video'], +list:focus [selected] [file-type-icon='video'], +list.autocomplete-suggestions [selected] [file-type-icon='video'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/video_white.png') 1x, + url('../../common/images/file_types/200/video_white.png') 2x); +} + +[file-type-icon='word'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/word.png') 1x, + url('../../common/images/file_types/200/word.png') 2x); +} + +tree:focus .tree-item[selected] > .tree-row > [file-type-icon='word'], +list:focus [selected] [file-type-icon='word'], +list.autocomplete-suggestions [selected] [file-type-icon='word'] { + background-image: -webkit-image-set( + url('../../common/images/file_types/100/word_white.png') 1x, + url('../../common/images/file_types/200/word_white.png') 2x); +} + +[file-type-icon='drive'] { + background-image: -webkit-image-set( + url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_DRIVE') 1x, + url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_DRIVE@2x') 2x); +} + +/* Large generic thumbnails, used when a file does not have a thumbnail. */ +[generic-thumbnail] { + background-image: -webkit-image-set( + url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_GENERIC') 1x, + url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_GENERIC@2x') 2x); + background-position: center center; + background-repeat: no-repeat; +} + +[generic-thumbnail='audio'] { + background-image: -webkit-image-set( + url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_AUDIO') 1x, + url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_AUDIO@2x') 2x); +} + +[generic-thumbnail='folder'] { + background-image: -webkit-image-set( + url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_FOLDER') 1x, + url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_FOLDER@2x') 2x); +} + +[generic-thumbnail='image'] { + background-image: -webkit-image-set( + url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_IMAGE') 1x, + url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_IMAGE@2x') 2x); +} + +[generic-thumbnail='video'] { + background-image: -webkit-image-set( + url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_VIDEO') 1x, + url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_VIDEO@2x') 2x); +} + +/* Icons for volume types. */ + +[volume-type-icon='archive'] { + background-image: -webkit-image-set( + url('../images/volumes/black_archive.png') 1x, + url('../images/volumes/2x/black_archive.png') 2x); +} + +list:focus li[selected] [volume-type-icon='archive'], +tree:focus .tree-item[selected] > .tree-row > [volume-type-icon='archive'] { + background-image: -webkit-image-set( + url('../images/volumes/white_archive.png') 1x, + url('../images/volumes/2x/white_archive.png') 2x); +} + +[volume-type-icon='downloads'] { + background-image: -webkit-image-set( + url('../images/volumes/black_downloads.png') 1x, + url('../images/volumes/2x/black_downloads.png') 2x); +} + +list:focus li[selected] [volume-type-icon='downloads'], +tree:focus .tree-item[selected] > .tree-row > [volume-type-icon='downloads'] { + background-image: -webkit-image-set( + url('../images/volumes/white_downloads.png') 1x, + url('../images/volumes/2x/white_downloads.png') 2x); +} + +[volume-type-icon='drive'] { + background-image: -webkit-image-set( + url('../images/volumes/black_drive.png') 1x, + url('../images/volumes/2x/black_drive.png') 2x); +} + +list:focus li[selected] [volume-type-icon='drive'], +tree:focus .tree-item[selected] > .tree-row > [volume-type-icon='drive'] { + background-image: -webkit-image-set( + url('../images/volumes/white_drive.png') 1x, + url('../images/volumes/2x/white_drive.png') 2x); +} + +[volume-type-icon='drive_offline'] { + background-image: -webkit-image-set( + url('../images/volumes/black_offline.png') 1x, + url('../images/volumes/2x/black_offline.png') 2x); +} + +list:focus li[selected] [volume-type-icon='drive_offline'], +tree:focus .tree-item[selected] > .tree-row > + [volume-type-icon='drive_offline'] { + background-image: -webkit-image-set( + url('../images/volumes/white_offline.png') 1x, + url('../images/volumes/2x/white_offline.png') 2x); +} + +[volume-type-icon='drive_shared_with_me'] { + background-image: -webkit-image-set( + url('../images/volumes/black_shared.png') 1x, + url('../images/volumes/2x/black_shared.png') 2x); +} + +list:focus li[selected] [volume-type-icon='drive_shared_with_me'], +tree:focus .tree-item[selected] > .tree-row > + [volume-type-icon='drive_shared_with_me'] { + background-image: -webkit-image-set( + url('../images/volumes/white_shared.png') 1x, + url('../images/volumes/2x/white_shared.png') 2x); +} + +[volume-type-icon='drive_recent'] { + background-image: -webkit-image-set( + url('../images/volumes/black_recent.png') 1x, + url('../images/volumes/2x/black_recent.png') 2x); +} + +list:focus li[selected] [volume-type-icon='drive_recent'], +tree:focus .tree-item[selected] > .tree-row > + [volume-type-icon='drive_recent'] { + background-image: -webkit-image-set( + url('../images/volumes/white_recent.png') 1x, + url('../images/volumes/2x/white_recent.png') 2x); +} + +[volume-type-icon='removable'] { + background-image: -webkit-image-set( + url('../images/volumes/black_usb.png') 1x, + url('../images/volumes/2x/black_usb.png') 2x); +} + +list:focus li[selected] [volume-type-icon='removable'], +tree:focus .tree-item[selected] > .tree-row > + [volume-type-icon='removable'] { + background-image: -webkit-image-set( + url('../images/volumes/white_usb.png') 1x, + url('../images/volumes/2x/white_usb.png') 2x); +} + +[volume-type-icon='removable'][volume-subtype='sd'] { + background-image: -webkit-image-set( + url('../images/volumes/black_sd.png') 1x, + url('../images/volumes/2x/black_sd.png') 2x); +} + +list:focus li[selected] [volume-type-icon='removable'][volume-subtype='sd'], +tree:focus .tree-item[selected] > .tree-row > + [volume-type-icon='removable'][volume-subtype='sd'] { + background-image: -webkit-image-set( + url('../images/volumes/white_sd.png') 1x, + url('../images/volumes/2x/white_sd.png') 2x); +} + +[volume-type-icon='removable'][volume-subtype='optical'] { + background-image: -webkit-image-set( + url('../images/volumes/black_optical.png') 1x, + url('../images/volumes/2x/black_optical.png') 2x); +} + +list:focus div[selected] + [volume-type-icon='removable'][volume-subtype='optical'], +tree:focus .tree-item[selected] > .tree-row > + [volume-type-icon='removable'][volume-subtype='optical'] { + background-image: -webkit-image-set( + url('../images/volumes/white_optical.png') 1x, + url('../images/volumes/2x/white_optical.png') 2x); +} + +list:focus li[selected] + [volume-type-icon='removable'][volume-subtype='optical'], +tree:focus .tree-item[selected] > .tree-row > + [volume-type-icon='removable'][volume-subtype='optical'] { + background-image: -webkit-image-set( + url('../images/volumes/white_optical.png') 1x, + url('../images/volumes/2x/white_optical.png') 2x); +} + +/* TODO(kaznacheev): consider a better icon for volume-subtype=unknown. + Also find out if we need an icon for volume-subtype=mobile */ +[volume-type-icon='removable'][volume-subtype='unknown'] { + background-image: -webkit-image-set( + url('../images/volumes/black_hdd.png') 1x, + url('../images/volumes/2x/black_hdd.png') 2x); +} + +list:focus li[selected] + [volume-type-icon='removable'][volume-subtype='unknown'], +tree:focus .tree-item[selected] > .tree-row > + [volume-type-icon='removable'][volume-subtype='unknown'] { + background-image: -webkit-image-set( + url('../images/volumes/white_hdd.png') 1x, + url('../images/volumes/2x/white_hdd.png') 2x); +} diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/gallery.css b/chromium/chrome/browser/resources/file_manager/foreground/css/gallery.css new file mode 100644 index 00000000000..7680f4d3100 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/css/gallery.css @@ -0,0 +1,1374 @@ +/* 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. */ + +body { + -webkit-user-select: none; + font-family: Open Sans, Droid Sans Fallback, sans-serif; + font-size: 84%; + margin: 0; +} + +.gallery, +.gallery .content { + bottom: 0; + left: 0; + overflow: hidden; + position: absolute; + right: 0; + top: 0; +} + +/* Common background for both mosaic and slide mode. */ +.gallery .content { + background-color: black; +} + +/* Close button */ + +/* We actually want (left,top) to be (0,0) but for some weird reason + this triggers :hover style on page reload which is ugly. */ +.gallery > .back-button { + cursor: pointer; + left: 1px; + position: absolute; + top: 1px; + z-index: 200; +} + +/* The close icon is in a nested div so that its opacity can be manipulated + independently from its parent (which can be dimmed when the crop frame + overlaps it) */ +.gallery > .back-button div { + background-image: -webkit-image-set( + url('../images/gallery/back_to_files.png') 1x, + url('../images/gallery/2x/back_to_files.png') 2x); + background-position: center center; + background-repeat: no-repeat; + height: 40px; + opacity: 0; + width: 64px; +} + +.gallery[tools] > .back-button div { + opacity: 0.5; +} + +.gallery[tools] > .back-button div:hover { + opacity: 1; +} + +/* Image container and canvas elements */ + +.gallery .image-container { + cursor: none; /* Only visible when the toolbar is active */ + height: 100%; + position: absolute; + width: 100%; +} + +.gallery[tools] .image-container[cursor='default'] { + cursor: default; +} + +.gallery[tools] .image-container[cursor='move'] { + cursor: -webkit-image-set( + url('../images/gallery/cursor_move.png') 1x, + url('../images/gallery/2x/cursor_move.png') 2x) 15 15, auto; +} + +.gallery[tools] .image-container[cursor='crop'] { + cursor: -webkit-image-set( + url('../images/gallery/cursor_crop.png') 1x, + url('../images/gallery/2x/cursor_crop.png') 2x) 15 15, auto; +} + +.gallery[tools] .image-container[cursor='n-resize'], +.gallery[tools] .image-container[cursor='s-resize'] { + cursor: -webkit-image-set( + url('../images/gallery/cursor_updown.png') 1x, + url('../images/gallery/2x/cursor_updown.png') 2x) 15 15, auto; +} + +.gallery[tools] .image-container[cursor='e-resize'], +.gallery[tools] .image-container[cursor='w-resize'] { + cursor: -webkit-image-set( + url('../images/gallery/cursor_leftright.png') 1x, + url('../images/gallery/2x/cursor_leftright.png') 2x) 15 15, auto; +} + +.gallery[tools] .image-container[cursor='nw-resize'], +.gallery[tools] .image-container[cursor='se-resize'] { + cursor: -webkit-image-set( + url('../images/gallery/cursor_nwse.png') 1x, + url('../images/gallery/2x/cursor_nwse.png') 2x) 15 15, auto; +} + +.gallery[tools] .image-container[cursor='ne-resize'], +.gallery[tools] .image-container[cursor='sw-resize'] { + cursor: -webkit-image-set( + url('../images/gallery/cursor_swne.png') 1x, + url('../images/gallery/2x/cursor_swne.png') 2x) 15 15, auto; +} + +.gallery .image-container > .image { + pointer-events: none; + position: absolute; + /* Duration and timing function are set in Javascript. */ + transition-property: -webkit-transform, opacity; +} + +.gallery .image-container > .image[fade] { + opacity: 0; +} + +/* Full resolution image is invisible unless printing. */ +.gallery .image-container > canvas.fullres { + display: none; +} + +@media print { + /* Do not print anything but the image content. */ + .gallery > :not(.content) { + display: none !important; + } + + /* Center the printed image. */ + .gallery .image-container { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + display: -webkit-box; + } + + /* Do not print the screen resolution image. */ + .gallery .image-container > canvas.image { + display: none !important; + } + + /* Print the full resolution image instead. */ + .gallery .image-container > canvas.fullres { + display: block !important; + max-height: 100%; + max-width: 100%; + } + + /* Print video at the center of the page */ + .gallery .image-container > video.image { + position: auto !important; + } +} + +/* Toolbar */ + +.gallery > .header, +.gallery > .toolbar { + -webkit-box-align: stretch; + -webkit-box-orient: horizontal; + -webkit-box-pack: start; + background-color: rgba(30, 30, 30, 0.8); + display: -webkit-box; + left: 0; + opacity: 0; + padding: 0 10px; + pointer-events: none; + position: absolute; + right: 0; + transition: opacity 300ms ease; +} + +.gallery > .header { + -webkit-box-align: center; + -webkit-box-pack: end; + border-bottom: 1px solid rgba(50, 50, 50, 0.8); + display: -webkit-box; + height: 45px; + top: 0; +} + +.gallery > .toolbar { + border-top: 1px solid rgba(50, 50, 50, 0.8); + bottom: 0; + height: 55px; + min-width: 800px; +} + +.gallery[tools]:not([slideshow]) > .header, +.gallery[tools]:not([slideshow]) > .toolbar { + opacity: 1; + pointer-events: auto; +} + +/* Hide immediately when entering the slideshow. */ +.gallery[tools][slideshow] > .toolbar { + transition-duration: 0; +} + +.gallery[tools][locked] > .toolbar { + pointer-events: none; +} + +.gallery .arrow-box { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + display: -webkit-box; + height: 100%; + pointer-events: none; + position: absolute; + width: 100%; + z-index: 100; +} + +.gallery .arrow-box .arrow { + opacity: 0; + pointer-events: none; +} + +.gallery .arrow-box .arrow-spacer { + -webkit-box-flex: 1; + pointer-events: none; +} + +.gallery[tools] .arrow-box[active] .arrow { + cursor: pointer; + opacity: 1; + pointer-events: auto; +} + +/* The arrow icons are in nested divs so that their opacity can be manipulated + * independently from their parent (which can be dimmed when the crop frame + * overlaps it) */ +.gallery .arrow div { + background-position: center center; + background-repeat: no-repeat; + height: 193px; + opacity: 0; + width: 105px; +} + +.gallery[tools] .arrow-box[active] .arrow div { + opacity: 0.25; +} + +.gallery[tools] .arrow-box[active] .arrow div:hover { + opacity: 1; +} + +.gallery .arrow.left div { + background-image: -webkit-image-set( + url('../images/gallery/arrow_left.png') 1x, + url('../images/gallery/2x/arrow_left.png') 2x); +} + +.gallery .arrow.right div { + background-image: -webkit-image-set( + url('../images/gallery/arrow_right.png') 1x, + url('../images/gallery/2x/arrow_right.png') 2x); +} + +/* Special behavior on mouse drag. + Redundant .gallery attributes included to make the rules more specific */ + +/* Everything but the image container should become mouse-transparent */ +.gallery[tools][editing][mousedrag] * { + pointer-events: none; +} + +.gallery[tools][editing][mousedrag] .image-container { + pointer-events: auto; +} + +/* The editor marks elements with 'dimmed' attribute to get them out of the way + of the crop frame */ +.gallery[tools][editing] [dimmed], +.gallery[tools][editing] [dimmed] * { + pointer-events: none; +} + +.gallery[tools][editing] [dimmed] { + opacity: 0.2; +} + +/* Filename */ + +.gallery .filename-spacer { + position: relative; + width: 270px; +} + +.gallery .filename-spacer > * { + background-color: transparent; + overflow: hidden; + position: absolute; + transition: visibility 0 linear 180ms, all 180ms linear; + width: 260px; +} + +.gallery .filename-spacer * { + color: white; +} + +.gallery .filename-spacer .namebox { + height: 22px; + top: 15px; +} + +.gallery[editing] .filename-spacer .namebox { + height: 21px; + top: 5px; +} + + +.gallery .filename-spacer .namebox { + background-color: transparent; + border: none; + box-sizing: border-box; + cursor: pointer; + display: block; + font-size: 120%; + outline: none; + overflow: hidden; + padding: 0 3px; + position: absolute; + text-overflow: ellipsis; + white-space: nowrap; +} + +.gallery .filename-spacer .namebox[disabled] { + -webkit-user-select: none; + cursor: default; +} + +.gallery .filename-spacer .namebox:not([disabled]):not(:focus):hover { + background-color: rgba(48, 48, 48, 1.0); +} + +.gallery .filename-spacer .namebox:focus { + background-color: white; + color: black; + cursor: text; +} + +.gallery .filename-spacer .options { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: start; + display: -webkit-box; + opacity: 0; + top: 50px; + visibility: hidden; +} + +.gallery[editing] .filename-spacer .options { + opacity: 1; + top: 28px; + visibility: visible; +} + +.gallery .filename-spacer .saved, +.gallery .filename-spacer .overwrite-original { + cursor: inherit; + font-size: 90%; + margin-left: 3px; + margin-right: 18px; + opacity: 0; + pointer-events: none; + transition: all linear 120ms; +} + +.gallery[editing] .filename-spacer .saved { + color: white; + opacity: 0.5; +} + +.gallery[editing] .filename-spacer .overwrite-original, +.gallery[editing] .filename-spacer .overwrite-original > * { + cursor: pointer; + opacity: 1; + pointer-events: auto; +} + +.gallery[editing] .options[saved] .overwrite-original { + opacity: 0.5; +} + +.gallery[editing] .options[saved] .overwrite-original, +.gallery[editing] .options[saved] .overwrite-original > * { + cursor: default; + pointer-events: none; +} + +.gallery .filename-spacer .overwrite-original input { + margin-bottom: -2px; + margin-right: 6px; +} + +.gallery .filename-spacer .saved[highlighted] { + -webkit-transform: scaleX(1.1) scaleY(1.1) rotate(0); + opacity: 1; +} + +/* Bubble */ +.gallery .toolbar .bubble { + bottom: 65px; + font-size: 85%; + left: 50px; + position: absolute; + width: 220px; +} + +.gallery:not([editing]) .toolbar .bubble { + display: none; +} + +/* Toolbar buttons */ + +.gallery .button-spacer { + -webkit-box-flex: 1; + display: -webkit-box; +} + +/* Thumbnails */ + +.gallery .ribbon-spacer { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + display: -webkit-box; + height: 100%; + left: 280px; + position: absolute; + right: 280px; +} + +.gallery .toolbar .ribbon { + -webkit-box-flex: 0; + -webkit-box-orient: horizontal; + -webkit-box-pack: start; + display: -webkit-box; + height: 100%; + overflow: hidden; + transition: opacity 180ms linear, visibility 0 linear; + z-index: 0; +} + +.gallery[editing] .toolbar .ribbon { + opacity: 0; + transition-delay: 0, 180ms; + visibility: hidden; +} + +.gallery .ribbon-image { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + border: 2px solid rgba(255, 255, 255, 0); /* transparent white */ + cursor: pointer; + display: -webkit-box; + height: 47px; + margin: 2px; + overflow: hidden; + transition: all 180ms linear; + width: 47px; +} + +.ribbon-image[vanishing='smooth'] { + border-left-width: 0; + border-right-width: 0; + margin-left: 0; + margin-right: 0; + width: 0; +} + +.gallery .ribbon-image[selected] { + border: 2px solid rgba(255, 233, 168, 1); +} + +.gallery .toolbar .ribbon.fade-left { + -webkit-mask-image: linear-gradient(to right, rgba(0, 0, 0, 0) 0, + rgba(0, 0, 0, 1) 40px); +} + +.gallery .toolbar .ribbon.fade-right { + -webkit-mask-image: linear-gradient(to left, rgba(0, 0, 0, 0) 0, + rgba(0, 0, 0, 1) 40px); +} + +.gallery .toolbar .ribbon.fade-left.fade-right { + -webkit-mask-image: linear-gradient(to right, rgba(0, 0, 0, 0) 0, + rgba(0, 0, 0, 1) 40px, + rgba(0, 0, 0, 1) 230px, + rgba(0, 0, 0, 0) 100%); +} + +.gallery .image-wrapper { + background-size: 45px 45px; + border: 1px solid rgba(0, 0, 0, 0); /* transparent black */ + height: 45px; + overflow: hidden; + position: relative; + width: 45px; +} + +.gallery .image-wrapper > img { + position: absolute; +} + +.gallery .image-wrapper > img:not(.cached) { + -webkit-animation: fadeIn 500ms ease-in; +} + +/* Editor buttons */ + +.gallery .edit-bar-spacer { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + display: -webkit-box; + height: 100%; + left: 280px; + opacity: 0; + position: absolute; + right: 280px; + transition: opacity 180ms linear, visibility 0 linear 180ms; + visibility: hidden; +} + +.gallery .toolbar .edit-main { + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + color: white; + display: -webkit-box; + height: 55px; + overflow: visible; +} + +.gallery[editing] .edit-bar-spacer { + opacity: 1.0; + pointer-events: auto; + transition-delay: 100ms, 100ms; + visibility: visible; +} + +.gallery .header button, +.gallery .toolbar button, +.gallery .header button[disabled], +.gallery .toolbar button[disabled] { + -webkit-box-align: center; + -webkit-box-flex: 0; + -webkit-box-orient: horizontal; + -webkit-box-pack: end; + background-color: rgba(0, 0, 0, 0); + background-position: center; + background-repeat: no-repeat; + border: none; + box-shadow: none; + color: white; + cursor: pointer; + display: -webkit-box; + opacity: 0.99; /* Workaround for http://crosbug.com/21065 */ + padding: 1px; /* Instead of a border. */ + position: relative; + z-index: 10; +} + +.gallery .header button, +.gallery .toolbar button { + height: 40px; + margin: 6px 0; + min-width: 40px; /* Reset. */ + width: 40px; +} + +/* By default, labels are hidden. */ +.gallery > .toolbar button span { + display: none; +} + +/* Show labels if there is enough space. */ +@media (min-width: 1180px) { + + .gallery .edit-main button, + .gallery .edit-main button[disabled] { + background-position: 5px center; + max-width: 60px; + min-width: 0; /* Reset. */ + padding: 0 10px 0 35px; + width: auto; + } + + .gallery > .toolbar button span { + display: inline; + } + +} + +.gallery .header button:hover, +.gallery .toolbar button:hover { + background-color: rgba(31, 31, 31, 1); + color: white; +} + +.gallery .header button:active, +.gallery .toolbar button:active, +.gallery .header button[pressed], +.gallery .toolbar button[pressed], +.gallery .header button[pressed]:hover, +.gallery .toolbar button[pressed]:hover { + background-color: rgba(240, 240, 240, 1); + color: black; +} + +.gallery > .toolbar button.autofix { + background-image: -webkit-image-set( + url('../images/gallery/icon_autofix.png') 1x, + url('../images/gallery/2x/icon_autofix.png') 2x); +} + +.gallery > .toolbar button.autofix:active, +.gallery > .toolbar button.autofix[pressed] { + background-image: -webkit-image-set( + url('../images/gallery/icon_autofix_selected.png') 1x, + url('../images/gallery/2x/icon_autofix_selected.png') 2x); +} + +.gallery > .toolbar button.crop { + background-image: -webkit-image-set( + url('../images/gallery/icon_crop.png') 1x, + url('../images/gallery/2x/icon_crop.png') 2x); +} + +.gallery > .toolbar button.crop:active, +.gallery > .toolbar button.crop[pressed] { + background-image: -webkit-image-set( + url('../images/gallery/icon_crop_selected.png') 1x, + url('../images/gallery/2x/icon_crop_selected.png') 2x); +} + +.gallery > .toolbar button.exposure { + background-image: -webkit-image-set( + url('../images/gallery/icon_brightness.png') 1x, + url('../images/gallery/2x/icon_brightness.png') 2x); +} + +.gallery > .toolbar button.exposure:active, +.gallery > .toolbar button.exposure[pressed] { + background-image: -webkit-image-set( + url('../images/gallery/icon_brightness_selected.png') 1x, + url('../images/gallery/2x/icon_brightness_selected.png') 2x); +} + +.gallery > .toolbar button.rotate_right { + background-image: -webkit-image-set( + url('../images/gallery/icon_rotate.png') 1x, + url('../images/gallery/2x/icon_rotate.png') 2x); +} + +.gallery > .toolbar button.rotate_right:active, +.gallery > .toolbar button.rotate_right[pressed] { + background-image: -webkit-image-set( + url('../images/gallery/icon_rotate_selected.png') 1x, + url('../images/gallery/2x/icon_rotate_selected.png') 2x); +} + +.gallery > .toolbar button.rotate_left { + background-image: -webkit-image-set( + url('../images/gallery/icon_rotate_left.png') 1x, + url('../images/gallery/2x/icon_rotate_left.png') 2x); +} + +.gallery > .toolbar button.rotate_left:active, +.gallery > .toolbar button.rotate_left[pressed] { + background-image: -webkit-image-set( + url('../images/gallery/icon_rotate_left_selected.png') 1x, + url('../images/gallery/2x/icon_rotate_left_selected.png') 2x); +} + +.gallery > .toolbar button.undo { + background-image: -webkit-image-set( + url('../images/gallery/icon_undo.png') 1x, + url('../images/gallery/2x/icon_undo.png') 2x); +} + +.gallery > .toolbar button.undo:active, +.gallery > .toolbar button.undo[pressed] { + background-image: -webkit-image-set( + url('../images/gallery/icon_undo_selected.png') 1x, + url('../images/gallery/2x/icon_undo_selected.png') 2x); +} + +.gallery > .toolbar button.redo { + background-image: -webkit-image-set( + url('../images/gallery/icon_redo.png') 1x, + url('../images/gallery/2x/icon_redo.png') 2x); + position: absolute; /* Exclude from center-packing*/ +} + +.gallery > .toolbar button.redo:active, +.gallery > .toolbar button.redo[pressed] { + background-image: -webkit-image-set( + url('../images/gallery/icon_redo_selected.png') 1x, + url('../images/gallery/2x/icon_redo_selected.png') 2x); +} + +.gallery > .toolbar button[disabled], +.gallery[tools][locked] > .toolbar button { + opacity: 0.5; + pointer-events: none; +} + +.gallery > .toolbar button[hidden] { + display: none; +} + +.gallery[mode='slide'] > .toolbar > button.mode { + background-image: -webkit-image-set( + url('../images/gallery/icon_mosaic.png') 1x, + url('../images/gallery/2x/icon_mosaic.png') 2x); +} + +.gallery[mode='slide'] > .toolbar > button.mode:active { + background-image: -webkit-image-set( + url('../images/gallery/icon_mosaic_selected.png') 1x, + url('../images/gallery/2x/icon_mosaic_selected.png') 2x); +} + +.gallery[mode='mosaic'] > .toolbar > button.mode { + background-image: -webkit-image-set( + url('../images/gallery/icon_1up.png') 1x, + url('../images/gallery/2x/icon_1up.png') 2x); +} + +.gallery[mode='mosaic'] > .toolbar > button.mode:active { + background-image: -webkit-image-set( + url('../images/gallery/icon_1up_selected.png') 1x, + url('../images/gallery/2x/icon_1up_selected.png') 2x); +} + +.gallery > .toolbar > button.slideshow { + background-image: -webkit-image-set( + url('../images/gallery/icon_slideshow.png') 1x, + url('../images/gallery/2x/icon_slideshow.png') 2x); +} + +.gallery > .toolbar > button.slideshow:active, +.gallery > .toolbar > button.slideshow[pressed] { + background-image: -webkit-image-set( + url('../images/gallery/icon_slideshow_selected.png') 1x, + url('../images/gallery/2x/icon_slideshow_selected.png') 2x); +} + +.gallery > .toolbar > button.delete { + background-image: -webkit-image-set( + url('../images/gallery/icon_delete.png') 1x, + url('../images/gallery/2x/icon_delete.png') 2x); +} + +.gallery > .toolbar > button.delete:active { + background-image: -webkit-image-set( + url('../images/gallery/icon_delete_selected.png') 1x, + url('../images/gallery/2x/icon_delete_selected.png') 2x); +} + +.gallery > .toolbar > button.edit { + background-image: -webkit-image-set( + url('../images/gallery/icon_edit.png') 1x, + url('../images/gallery/2x/icon_edit.png') 2x); +} + +.gallery > .toolbar > button.edit:active, +.gallery > .toolbar > button.edit[pressed] { + background-image: -webkit-image-set( + url('../images/gallery/icon_edit_selected.png') 1x, + url('../images/gallery/2x/icon_edit_selected.png') 2x); +} + +.gallery > .toolbar > button.print { + background-image: -webkit-image-set( + url('../images/gallery/icon_print.png') 1x, + url('../images/gallery/2x/icon_print.png') 2x); +} + +.gallery > .toolbar > button.print:active, +.gallery > .toolbar > button.print[pressed] { + background-image: -webkit-image-set( + url('../images/gallery/icon_print_selected.png') 1x, + url('../images/gallery/2x/icon_print_selected.png') 2x); +} + +.gallery > .toolbar > button.share { + background-image: -webkit-image-set( + url('../images/gallery/icon_share.png') 1x, + url('../images/gallery/2x/icon_share.png') 2x); +} + +.gallery > .toolbar > button.share:active, +.gallery > .toolbar > button.share[pressed] { + background-image: -webkit-image-set( + url('../images/gallery/icon_share_selected.png') 1x, + url('../images/gallery/2x/icon_share_selected.png') 2x); +} + +.gallery > .toolbar > button.share[disabled] { + display: none; +} + +/* Secondary toolbar (mode-specific tools) */ + +.gallery .edit-modal { + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + bottom: 80px; + display: -webkit-box; + height: 40px; + pointer-events: none; + position: absolute; + width: 100%; +} + +.gallery .edit-modal-wrapper[hidden] { + display: none; +} + +.gallery .edit-modal-wrapper { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + background-color: rgba(0, 0, 0, 0.75); + color: white; + display: -webkit-box; + padding-right: 5px; + pointer-events: auto; +} + +.gallery .edit-modal .label { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + background-position: 20px center; + background-repeat: no-repeat; + display: -webkit-box; + height: 20px; + padding-left: 50px; + padding-right: 10px; +} + +.gallery .edit-modal .label.brightness { + background-image: -webkit-image-set( + url('../images/gallery/icon_brightness.png') 1x, + url('../images/gallery/2x/icon_brightness.png') 2x); +} + +.gallery .edit-modal .label.contrast { + background-image: -webkit-image-set( + url('../images/gallery/icon_contrast.png') 1x, + url('../images/gallery/2x/icon_contrast.png') 2x); + height: 24px; + margin-left: 15px; +} + +.gallery .edit-modal .range { + -webkit-appearance: none !important; + height: 3px; + margin-right: 10px; + margin-top: 1px; +} + +.gallery .edit-modal .range::-webkit-slider-thumb { + -webkit-appearance: none; + background-image: -webkit-image-set( + url('../images/gallery/slider_thumb.png') 1x, + url('../images/gallery/2x/slider_thumb.png') 2x); + height: 29px; + width: 16px; +} + +/* Crop frame */ + +.gallery .crop-overlay { + -webkit-box-orient: vertical; + display: -webkit-box; + pointer-events: none; + position: absolute; +} + +.gallery .crop-overlay .shadow { + background-color: rgba(0, 0, 0, 0.65); +} + +.gallery .crop-overlay .middle-box { + -webkit-box-flex: 1; + -webkit-box-orient: horizontal; + display: -webkit-box; +} + +.gallery .crop-frame { + -webkit-box-flex: 1; + display: -webkit-box; + position: relative; +} + +.gallery .crop-frame div { + background-color: rgba(255, 255, 255, 1); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.75); + position: absolute; +} + +.gallery .crop-frame .horizontal { + height: 1px; + left: 7px; + right: 7px; +} + +.gallery .crop-frame .horizontal.top { + top: 0; +} + +.gallery .crop-frame .horizontal.bottom { + bottom: 0; +} + +.gallery .crop-frame .vertical { + bottom: 7px; + top: 7px; + width: 1px; +} + +.gallery .crop-frame .vertical.left { + left: 0; +} + +.gallery .crop-frame .vertical.right { + right: 0; +} + +.gallery .crop-frame .corner { + border-radius: 6px; + height: 13px; + width: 13px; +} + +.gallery .crop-frame .corner.left { + left: -6px; +} + +.gallery .crop-frame .corner.right { + right: -6px; +} + +.gallery .crop-frame .corner.top { + top: -6px; +} + +.gallery .crop-frame .corner.bottom { + bottom: -6px; +} + +/* Prompt/notification panel */ + +.gallery .prompt-wrapper { + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + display: -webkit-box; + height: 100%; + pointer-events: none; + position: absolute; + width: 100%; +} + +.gallery .prompt-wrapper[pos=top] { + -webkit-box-align: start; +} + +.gallery .prompt-wrapper[pos=center] { + -webkit-box-align: center; +} + +.gallery .prompt-wrapper[pos=center] .back-button { + display: none; +} + +.gallery .prompt-wrapper > div.dimmable { + opacity: 1; + transition: opacity 220ms ease; +} + +.gallery .prompt { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + background-color: rgba(0, 0, 0, 0.8); + color: white; + display: -webkit-box; + font-size: 120%; + height: 40px; + opacity: 0; + padding: 0 20px; + position: relative; + top: 5px; + transition: all 180ms ease; +} + +.gallery .prompt[state='fadein'] { + opacity: 1; + top: 0; +} + +.gallery .prompt[state='fadeout'] { + opacity: 0; + top: 0; +} + +.gallery .prompt-wrapper[pos=top] .prompt { + padding-right: 10px; +} + +.gallery .prompt .back-button { + background-image: -webkit-image-set( + url('../images/gallery/butterbar_close_button.png') 1x, + url('../images/gallery/2x/butterbar_close_button.png') 2x); + background-position: center center; + background-repeat: no-repeat; + height: 16px; + margin-left: 16px; + opacity: 0.65; + pointer-events: auto; + width: 16px; +} + +.gallery .prompt .back-button:hover { + background-color: rgba(81, 81, 81, 1); + opacity: 1.0; +} + +.gallery .share-menu { + -webkit-box-align: stretch; + -webkit-box-orient: vertical; + -webkit-box-pack: start; + background-color: white; + border: 1px solid #7f7f7f; + border-radius: 1px; + bottom: 60px; + display: -webkit-box; + opacity: 1.0; + padding: 8px; + position: absolute; + right: 10px; + transition: opacity 500ms ease-in-out; +} + +.gallery .share-menu .bubble-point { + background-image: -webkit-image-set( + url('../images/gallery/bubble_point.png') 1x, + url('../images/gallery/2x/bubble_point.png') 2x); + background-position: center top; + background-repeat: no-repeat; + bottom: -8px; + height: 8px; + padding: 0; + position: absolute; + right: 20px; + width: 20px; +} + +.gallery .share-menu[hidden] { + bottom: -100%; /* Offscreen so that 'dimmed' attribute does not show it. */ + opacity: 0; + pointer-events: none; +} + +.gallery .share-menu > .item { + background-color: rgba(0, 0, 0, 0); + background-position: 5px center; + background-repeat: no-repeat; + cursor: pointer; + padding: 5px; + padding-left: 26px; +} + +.gallery .share-menu > .item:hover { + background-color: rgba(240, 240, 240, 1); +} + +.gallery .share-menu > div > img { + display: block; + margin-right: 5px; +} + +/* Load spinner and error banner. */ + +.gallery .spinner { + background-image: url(../images/common/spinner.svg); + background-size: 100%; + height: 16px; + left: 50%; + margin-left: -8px; + margin-top: -8px; + position: absolute; + top: 50%; + width: 16px; +} + +.gallery:not([spinner]) .spinner { + display: none; +} + +.gallery .error-banner { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + background-color: rgba(24, 24, 24, 1); + background-image: -webkit-image-set( + url('../images/media/error.png') 1x, + url('../images/media/2x/error.png') 2x); + background-position: 25px center; + background-repeat: no-repeat; + color: white; + display: -webkit-box; + height: 54px; + padding-left: 70px; + padding-right: 35px; +} + +.gallery:not([error]) .error-banner { + display: none; +} + +/* Video playback support. */ + +.gallery video { + height: 100%; + position: absolute; + width: 100%; +} + +.gallery .video-controls-spacer { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + bottom: 60px; /* Just above the toolbar */ + display: -webkit-box; + height: 30px; + left: 0; + opacity: 0; + pointer-events: none; + position: absolute; + right: 0; +} + +.gallery[video] .video-controls-spacer { + /* Animate opacity on 'tools' attribute toggle. */ + /* Change opacity immediately on 'video' attribute change. */ + transition: opacity 280ms ease; +} + +.gallery[video][tools] .video-controls-spacer { + opacity: 1; +} + +.gallery .video-controls { + display: none; + max-width: 800px; +} + +.gallery[video] .video-controls { + -webkit-box-flex: 1; + display: -webkit-box; +} + +.gallery[video] > .toolbar .edit-main { + display: none; +} + +/* Mosaic view. */ +.mosaic { + bottom: 55px; /* Toolbar height. */ + left: 0; + overflow-x: scroll; + overflow-y: hidden; + position: absolute; + right: 0; + top: 0; + + /* transition-duration is set in Javascript. */ + transition-property: -webkit-transform; + transition-timing-function: linear; +} + +.mosaic::-webkit-scrollbar { + background: transparent; +} + +.mosaic::-webkit-scrollbar-thumb { + background: rgb(31, 31, 31); +} + +.gallery:not([mode='mosaic']) .mosaic::-webkit-scrollbar-thumb { + background: transparent; +} + +.mosaic-tile { + position: absolute; + /* Tile's zoom factor is animated on hover. We apply the transform to + the entire tile so that the image outline is included into the animation. */ + transition: -webkit-transform 150ms linear; +} + +/* Mosaic tile's opacity is controlled by |visible| attribute which changes + separately from .gallery[mode] */ +.mosaic:not([visible]) .mosaic-tile .img-border { + opacity: 0; +} + +/* Animate tile's opacity, except for the selected tile which should show/hide + instantly (this looks better when zooming to/from the slide mode). */ +.mosaic-tile:not([selected]) .img-border { + transition: opacity 350ms linear; +} + +/* Must be in sync with mosaic_mode.js. + Mosaic.Layout.SPACING should be equal to + top + bottom + border-top-width + border-bottom-width AND + left + right + border-left-width + border-right-width */ +.mosaic-tile .img-border { + border: 1px solid black; /* Space between the outline and the image. */ + bottom: 4px; + left: 4px; + outline: 2px solid transparent; + overflow: hidden; + position: absolute; + right: 4px; + top: 4px; +} + +/* Selected and hover state are only visible when zoom transition is over. */ +.mosaic[visible='normal'] .mosaic-tile[selected] .img-border { + outline-color: rgb(51, 153, 255); +} + +.mosaic[visible='normal'].hover-visible .mosaic-tile:hover { + -webkit-transform: scale(1.05); + z-index: 50; +} + +.mosaic[visible='normal'].hover-visible + .mosaic-tile:hover:not([selected]) .img-border { + outline-color: rgb(182, 212, 252); +} + +.mosaic-tile .img-wrapper { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; +} + +.mosaic-tile .img-wrapper[generic-thumbnail], +.mosaic-tile .img-wrapper.animated:not([generic-thumbnail]) + canvas:not(.cached) { + -webkit-animation: fadeIn ease-in 1; + -webkit-animation-duration: 500ms; + -webkit-animation-fill-mode: forwards; +} + +@-webkit-keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* In order to do mode animated transitions smoothly we keep both mosaic and + image-container but transparent. */ +.gallery:not([mode='mosaic']) .mosaic, +.gallery:not([mode='slide']) .image-container { + pointer-events: none; +} + +.gallery:not([mode='slide']) .ribbon, +.gallery:not([mode='slide']) .arrow-box { + opacity: 0; + pointer-events: none; +} + +/* Temporary. Remove this along with the delete confirmation dialog + when Undo delete is implemented. */ +.cr-dialog-shield { + background-color: black; +} + +/* Slideshow controls */ + +.slideshow-toolbar { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + bottom: 0; + display: none; + left: 0; + padding-bottom: 6px; + pointer-events: none; + position: absolute; + right: 0; +} + +.gallery[tools][slideshow] .slideshow-toolbar { + display: -webkit-box; +} + +.slideshow-toolbar > div { + background-position: center; + background-repeat: no-repeat; + height: 68px; + opacity: 0.5; + pointer-events: auto; + width: 68px; +} + +.slideshow-toolbar > div:hover { + opacity: 1; +} + +.slideshow-toolbar > .slideshow-play { + background-image: -webkit-image-set( + url('../images/gallery/slideshow-play.png') 1x, + url('../images/gallery/2x/slideshow-play.png') 2x); + margin-right: -2px; +} + +.gallery[slideshow='playing'] .slideshow-toolbar > .slideshow-play { + background-image: -webkit-image-set( + url('../images/gallery/slideshow-pause.png') 1x, + url('../images/gallery/2x/slideshow-pause.png') 2x); +} + +.slideshow-toolbar > .slideshow-end { + background-image: -webkit-image-set( + url('../images/gallery/slideshow-end.png') 1x, + url('../images/gallery/2x/slideshow-end.png') 2x); + margin-left: -2px; +} + +.gallery > .header > button { + -webkit-margin-start: 10px; + cursor: default; + height: 32px; + min-width: 32px; + width: 32px; +} + +.gallery > .header > .maximize-button { + background: -webkit-image-set( + url('chrome://resources/images/apps/topbar_button_maximize.png') 1x, + url('chrome://resources/images/2x/apps/topbar_button_maximize.png') 2x) + center; +} + +.gallery > .header > .close-button { + background: -webkit-image-set( + url('chrome://resources/images/apps/topbar_button_close.png') 1x, + url('chrome://resources/images/2x/apps/topbar_button_close.png') 2x) + center; +} diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/list.css b/chromium/chrome/browser/resources/file_manager/foreground/css/list.css new file mode 100644 index 00000000000..d102d696dcd --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/css/list.css @@ -0,0 +1,73 @@ +/* Copyright 2013 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. */ + +/* Derived from /ui/webui/resources/css/list.css. */ + +list, +grid { + display: block; + outline: none; + overflow: auto; + position: relative; /* Make sure that item offsets are relative to the + list. */ +} + +list > *, +grid > * { + -webkit-user-select: none; + position: relative; /* to allow overlap */ + text-overflow: ellipsis; + white-space: pre; +} + +list > * { + display: block; +} + +grid > * { + display: inline-block; +} + +list:focus > [lead], +grid:focus > [lead] { + z-index: 2; +} + +list:not([disabled]) > :hover, +grid:not([disabled]) > :hover { + z-index: 1; +} + +list > [selected], +grid > [selected] { + z-index: 2; +} + +list > .spacer, +grid > .spacer { + box-sizing: border-box; + display: block; + overflow: hidden; + visibility: hidden; +} + +list :-webkit-any( + input[type='input'], + input[type='password'], + input[type='search'], + input[type='text'], + input[type='url']), +list :-webkit-any( + button, + input[type='button'], + input[type='submit'], + select):not(.custom-appearance):not(.link-button) { + line-height: normal; + vertical-align: middle; +} + +list > [hidden], +grid > [hidden] { + display: none; +} diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/media_controls.css b/chromium/chrome/browser/resources/file_manager/foreground/css/media_controls.css new file mode 100644 index 00000000000..ba167eec138 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/css/media_controls.css @@ -0,0 +1,605 @@ +/* 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. */ + +.media-button { + height: 28px; + position: relative; + width: 26px; +} + +.media-button > div { + height: 100%; + opacity: 0; + pointer-events: none; + position: absolute; + transition: opacity 100ms linear; + width: 100%; +} + +.media-button[state='default']:not(.disabled):not(:hover):not(:active) > + .default.normal, +.media-button[state='default']:not(.disabled):hover > .default.hover, +.media-button[state='default']:not(.disabled):active > .default.active, +.media-button[state='playing']:not(.disabled):not(:hover):not(:active) > + .playing.normal, +.media-button[state='playing']:not(.disabled):hover > .playing.hover, +.media-button[state='playing']:not(.disabled):active > .playing.active, +.media-button[state='ended']:not(.disabled):not(:hover):not(:active) > + .ended.normal, +.media-button[state='ended']:not(.disabled):hover > .ended.hover, +.media-button[state='ended']:not(.disabled):active > .ended.active, +.media-button.disabled > .disabled { + opacity: 1; +} + +/* Custom sliders for progress and volume. */ + +/* Customize the standard input[type='range']. */ +.custom-slider > input[type='range'] { + -webkit-appearance: none !important; /* Hide the default thumb icon. */ + background: transparent; /* Hide the standard slider bar */ + height: 100%; + left: -2px; /* Required to align the input element with the parent. */ + outline: none; + position: absolute; + top: -2px; + width: 100%; +} + +/* Custom thumb icon. */ +.custom-slider > input[type='range']::-webkit-slider-thumb { + -webkit-appearance: none; + background-position: center center; + background-repeat: no-repeat; + height: 24px; + position: relative; + z-index: 2; +} + +/* Custom slider bar (we hide the standard one). */ +.custom-slider > .bar { + /* In order to match the horizontal position of the standard slider bar + left and right must be equal to 1/2 of the thumb icon width. */ + border-bottom-style: solid; + border-top-style: solid; + border-width: 1px; + bottom: 11px; + pointer-events: none; /* Mouse events pass through to the standard input. */ + position: absolute; + top: 11px; +} + +.custom-slider > .bar > .filled, +.custom-slider > .bar > .cap { + border-style: solid; + border-width: 1px; + bottom: -1px; + position: absolute; + top: -1px; +} + +/* The filled portion of the slider bar to the left of the thumb. */ +.custom-slider > .bar > .filled { + border-left-style: none; + border-right-style: none; + left: 0; + width: 0; /* The element style.width is manipulated from the code. */ +} + +/* Rounded caps to the left and right of the slider bar. */ +.custom-slider > .bar > .cap { + width: 4px; +} + +/* Left cap is always filled, should be the same color as .filled. */ +.custom-slider > .bar > .cap.left { + border-bottom-left-radius: 4px; + border-right-style: none; + border-top-left-radius: 4px; + right: 100%; +} + +/* Right cap is always not filled. */ +.custom-slider > .bar > .cap.right { + border-bottom-right-radius: 4px; + border-left-style: none; + border-top-right-radius: 4px; + left: 100%; +} + +.custom-slider > .bar, +.custom-slider > .bar > .cap.right { + background-color: rgba(0, 0, 0, 0.5); + border-color: #808080; +} + +.custom-slider > .bar > .filled, +.custom-slider > .bar > .cap.left { + background-image: linear-gradient(#c3c3c3, #d9d9d9); + border-color: #d9d9d9; +} + +.custom-slider.disabled > .bar > .filled, +.custom-slider.disabled > .bar > .cap.left { + background-color: rgba(0, 0, 0, 0.5); + background-image: none; +} + +.custom-slider.disabled > .bar, +.custom-slider.disabled > .bar > .filled, +.custom-slider.disabled > .bar > .cap { + border-color: #404040; +} + +.media-button.disabled, +.custom-slider.disabled, +.custom-slider.readonly { + pointer-events: none; +} + +/* Progress seek marker (precise time shown on mouse hover. */ + +/* Thin vertical line across the slider bar */ +.custom-slider > .bar > .seek-mark { + background-color: #202020; + bottom: -1px; + left: 0; + position: absolute; + top: -1px; + width: 0; +} + +.custom-slider > .bar > .seek-mark.visible { + width: 1px; +} + +.custom-slider > .bar > .seek-mark.inverted { + background-color: #808080; +} + +/* Text label giving the precise time corresponding to the hover position. */ +.custom-slider > .bar > .seek-mark > .seek-label { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + background: #202020; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + bottom: 19px; + color: white; + display: -webkit-box; + font-size: 13px; + height: 15px; + left: 0; + opacity: 0; + overflow: hidden; + position: absolute; + transition: opacity 150ms ease; +} + +.custom-slider > .bar > .seek-mark.visible > .seek-label { + opacity: 1; +} + +/* Media controls in order of appearance. */ + +/* Play/pause button. */ + +.media-button.play { + margin-left: -7px; + margin-right: -7px; +} + +.media-button.play > .default.normal { + background-image: -webkit-image-set( + url('../images/media/media_play.png') 1x, + url('../images/media/2x/media_play.png') 2x); +} + +.media-button.play > .default.hover { + background-image: -webkit-image-set( + url('../images/media/media_play_hover.png') 1x, + url('../images/media/2x/media_play_hover.png') 2x); +} + +.media-button.play > .default.active { + background-image: -webkit-image-set( + url('../images/media/media_play_down.png') 1x, + url('../images/media/2x/media_play_down.png') 2x); +} + +.media-button.play > .playing.normal { + background-image: -webkit-image-set( + url('../images/media/media_pause.png') 1x, + url('../images/media/2x/media_pause.png') 2x); +} + +.media-button.play > .playing.hover { + background-image: -webkit-image-set( + url('../images/media/media_pause_hover.png') 1x, + url('../images/media/2x/media_pause_hover.png') 2x); +} + +.media-button.play > .playing.active { + background-image: -webkit-image-set( + url('../images/media/media_pause_down.png') 1x, + url('../images/media/2x/media_pause_down.png') 2x); +} + +.media-button.play > .ended.normal { + background-image: -webkit-image-set( + url('../images/media/media_loop.png') 1x, + url('../images/media/2x/media_loop.png') 2x); +} + +.media-button.play > .ended.hover { + background-image: -webkit-image-set( + url('../images/media/media_loop_hover.png') 1x, + url('../images/media/2x/media_loop_hover.png') 2x); +} + +.media-button.play > .ended.active { + background-image: -webkit-image-set( + url('../images/media/media_loop_down.png') 1x, + url('../images/media/2x/media_loop_down.png') 2x); +} + +.media-button.play > .disabled { + background-image: -webkit-image-set( + url('../images/media/media_play_disabled.png') 1x, + url('../images/media/2x/media_play_disabled.png') 2x); +} + +/* Time controls: a slider and a text time display. */ + +.time-controls { + -webkit-box-align: center; + -webkit-box-flex: 1; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + display: -webkit-box; + height: 100%; +} + +.custom-slider.progress { + -webkit-box-flex: 1; + display: -webkit-box; + height: 100%; + margin-left: -9px; /* Set the margins at the edges of the slider bar. */ + margin-right: -9px; + position: relative; +} + +.custom-slider.progress > input[type='range']::-webkit-slider-thumb { + background-image: -webkit-image-set( + url('../images/media/media_slider_thumb.png') 1x, + url('../images/media/2x/media_slider_thumb.png') 2x); + width: 28px; +} + +.custom-slider.progress > input[type='range']::-webkit-slider-thumb:hover { + background-image: -webkit-image-set( + url('../images/media/media_slider_thumb_hover.png') 1x, + url('../images/media/2x/media_slider_thumb_hover.png') 2x); +} + +.custom-slider.progress > input[type='range']::-webkit-slider-thumb:active { + background-image: -webkit-image-set( + url('../images/media/media_slider_thumb_down.png') 1x, + url('../images/media/2x/media_slider_thumb_down.png') 2x); +} + +.custom-slider.progress.disabled > input[type='range']::-webkit-slider-thumb { + background-image: none; +} + +.custom-slider.progress > .bar { + left: 14px; /* Exactly 1/2 of the thumb width */ + right: 14px; +} + +/* Time display. */ + +.time-controls > .time { + cursor: default; + height: 100%; + margin-left: 15px; + position: relative; +} + +.time-controls > .time.disabled { + opacity: 0; +} + +/* Invisible div used to compute the width required for the elapsed time. */ +.time-controls > .time > .duration { + color: transparent; +} + +.time-controls > .time > .current { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: end; + color: white; + display: -webkit-box; + height: 100%; + position: absolute; + right: 0; + top: -1px; +} + +/* Volume controls: sound button and volume slider */ + +.volume-controls { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + display: -webkit-box; + height: 100%; +} + +/* Sound button */ + +.media-button.sound { + margin-left: -4px; + width: 31px; +} + +.media-button.sound[level='0'] > .normal { + background-image: -webkit-image-set( + url('../images/media/media_sound_disabled.png') 1x, + url('../images/media/2x/media_sound_disabled.png') 2x); +} + +.media-button.sound[level='0'] > .hover { + background-image: -webkit-image-set( + url('../images/media/media_sound_disabled_hover.png') 1x, + url('../images/media/2x/media_sound_disabled_hover.png') 2x); +} + +.media-button.sound[level='0'] > .active { + background-image: -webkit-image-set( + url('../images/media/media_sound_disabled_down.png') 1x, + url('../images/media/2x/media_sound_disabled_down.png') 2x); +} + + +.media-button.sound[level='1'] > .normal { + background-image: -webkit-image-set( + url('../images/media/media_sound_level1.png') 1x, + url('../images/media/2x/media_sound_level1.png') 2x); +} + +.media-button.sound[level='1'] > .hover { + background-image: -webkit-image-set( + url('../images/media/media_sound_level1_hover.png') 1x, + url('../images/media/2x/media_sound_level1_hover.png') 2x); +} + +.media-button.sound[level='1'] > .active { + background-image: -webkit-image-set( + url('../images/media/media_sound_level1_down.png') 1x, + url('../images/media/2x/media_sound_level1_down.png') 2x); +} + + +.media-button.sound[level='2'] > .normal { + background-image: -webkit-image-set( + url('../images/media/media_sound_level2.png') 1x, + url('../images/media/2x/media_sound_level2.png') 2x); +} + +.media-button.sound[level='2'] > .hover { + background-image: -webkit-image-set( + url('../images/media/media_sound_level2_hover.png') 1x, + url('../images/media/2x/media_sound_level2_hover.png') 2x); +} + +.media-button.sound[level='2'] > .active { + background-image: -webkit-image-set( + url('../images/media/media_sound_level2_down.png') 1x, + url('../images/media/2x/media_sound_level2_down.png') 2x); +} + + +.media-button.sound[level='3'] > .normal { + background-image: -webkit-image-set( + url('../images/media/media_sound_full.png') 1x, + url('../images/media/2x/media_sound_full.png') 2x); +} + +.media-button.sound[level='3'] > .hover { + background-image: -webkit-image-set( + url('../images/media/media_sound_full_hover.png') 1x, + url('../images/media/2x/media_sound_full_hover.png') 2x); +} + +.media-button.sound[level='3'] > .active { + background-image: -webkit-image-set( + url('../images/media/media_sound_full_down.png') 1x, + url('../images/media/2x/media_sound_full_down.png') 2x); +} + + +.media-button.sound > .disabled { + background-image: -webkit-image-set( + url('../images/media/media_sound_full_disabled.png') 1x, + url('../images/media/2x/media_sound_full_disabled.png') 2x); +} + +/* Volume slider. */ + +.custom-slider.volume { + height: 100%; + margin-left: -4px; + margin-right: -4px; + position: relative; + width: 60px; +} + +.custom-slider.volume > input[type='range']::-webkit-slider-thumb { + background-image: -webkit-image-set( + url('../images/media/media_volume_slider_thumb.png') 1x, + url('../images/media/2x/media_volume_slider_thumb.png') 2x); + width: 20px; +} + +.custom-slider.volume > input[type='range']::-webkit-slider-thumb:hover { + background-image: -webkit-image-set( + url('../images/media/media_volume_slider_thumb_hover.png') 1x, + url('../images/media/2x/media_volume_slider_thumb_hover.png') 2x); +} + +.custom-slider.volume > input[type='range']::-webkit-slider-thumb:active { + background-image: -webkit-image-set( + url('../images/media/media_volume_slider_thumb_down.png') 1x, + url('../images/media/2x/media_volume_slider_thumb_down.png') 2x); +} + +.custom-slider.volume.disabled > input[type='range']::-webkit-slider-thumb { + background-image: none; +} + +.custom-slider.volume > .bar { + left: 10px; /* Exactly 1/2 of the thumb width */ + right: 10px; +} + +/* Horizontal video control bar, all controls in a row. */ + +.video-controls { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + background: #202020; + border-radius: 5px; + display: -webkit-box; + font-size: 15px; + height: 30px; + opacity: 0.8; + padding-left: 15px; + padding-right: 15px; + pointer-events: auto; +} + +.video-controls .time-controls, +.video-controls .volume-controls { + margin-left: 15px; +} + +/* Fullscreen button. */ +/* There is no final decision whether we need a separate icon when toggled. */ + +.media-button.fullscreen { + margin-left: 9px; /* 15px visible margin - 6px whitespace in the icon. */ + margin-right: -6px; +} + +.media-button.fullscreen > .normal { + background-image: -webkit-image-set( + url('../images/media/media_fullscreen.png') 1x, + url('../images/media/2x/media_fullscreen.png') 2x); +} + +.media-button.fullscreen > .hover { + background-image: -webkit-image-set( + url('../images/media/media_fullscreen_hover.png') 1x, + url('../images/media/2x/media_fullscreen_hover.png') 2x); +} + +.media-button.fullscreen > .active { + background-image: -webkit-image-set( + url('../images/media/media_fullscreen_down.png') 1x, + url('../images/media/2x/media_fullscreen_down.png') 2x); +} + +.media-button.fullscreen > .disabled { + background-image: -webkit-image-set( + url('../images/media/media_fullscreen_disabled.png') 1x, + url('../images/media/2x/media_fullscreen_disabled.png') 2x); +} + +.playback-state-icon { + background-color: #202020; + background-position: center center; + background-repeat: no-repeat; + border-radius: 2.5px; + height: 32px; + left: 50%; + margin-left: -16px; + margin-top: -16px; + opacity: 0; + pointer-events: none; + position: absolute; + top: 50%; + width: 32px; + z-index: 2; +} + +.text-banner { + background-color: black; + border-radius: 10px; + color: white; + font-size: 18px; + left: 50%; + margin-left: -250px; + opacity: 0; + padding: 10px; + pointer-events: none; + position: absolute; + text-align: center; + text-shadow: 0 0 10px black; + top: 20%; + width: 500px; + z-index: 2; +} + +.text-banner[visible] { + -webkit-animation: text-banner-blowup 3000ms; +} + +.playback-state-icon[state] { + -webkit-animation: blowup 500ms; +} + +@-webkit-keyframes blowup { + from { + opacity: 1; + } + to { + -webkit-transform: scale(3); + opacity: 0; + } +} + +@-webkit-keyframes text-banner-blowup { + from { + -webkit-transform: scale(0.5); + opacity: 0; + } + 20% { + -webkit-transform: scale(1); + opacity: 0.75; + } + 80% { + -webkit-transform: scale(1); + opacity: 0.75; + } + to { + -webkit-transform: scale(3); + opacity: 0; + } +} + +.playback-state-icon[state='play'] { + background-image: -webkit-image-set( + url('../images/media/media_play.png') 1x, + url('../images/media/2x/media_play.png') 2x); +} + +.playback-state-icon[state='pause'] { + background-image: -webkit-image-set( + url('../images/media/media_pause.png') 1x, + url('../images/media/2x/media_pause.png') 2x); +} diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/menu.css b/chromium/chrome/browser/resources/file_manager/foreground/css/menu.css new file mode 100644 index 00000000000..2f32d05752f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/css/menu.css @@ -0,0 +1,38 @@ +/* Copyright 2013 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. */ + +/* Derived from /ui/webui/resources/css/menu.css. */ + +menu { + position: fixed; + white-space: nowrap; + z-index: 3; +} + +menu:not(.decorated) { + display: none; +} + +menu > * { + box-sizing: border-box; + display: block; + text-align: start; + width: 100%; +} + +menu > hr { + border: 0; + height: 1px; +} + +menu > [hidden] { + display: none; +} + +menu > [shortcutText]::after { + -webkit-padding-start: 30px; + color: #999; + content: attr(shortcutText); + float: right; +} diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/table.css b/chromium/chrome/browser/resources/file_manager/foreground/css/table.css new file mode 100644 index 00000000000..83516993103 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/css/table.css @@ -0,0 +1,64 @@ +/* Copyright 2013 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. */ + +/* Derived from /ui/webui/resources/css/table.css. */ + +html.col-resize * { + cursor: col-resize !important; +} + +.table[hasElementFocus] > list > [lead] { + z-index: 2; +} + +.table-row { + display: -webkit-box; + text-align: start; + width: 100%; +} + +.table-row-cell { + display: -webkit-box; + overflow: hidden; +} + +.table-row-cell > * { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.table-header { + overflow: hidden; + position: relative; +} + +.table-header-inner { + -webkit-user-select: none; + cursor: default; + display: -webkit-box; + position: relative; + text-align: start; +} + +.table-header-cell { + font-weight: normal; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.table-header-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.table-header-splitter { + cursor: col-resize; + height: 100%; + left: 0; + position: absolute; + top: 0; +} diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/tree.css b/chromium/chrome/browser/resources/file_manager/foreground/css/tree.css new file mode 100644 index 00000000000..e151977f754 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/css/tree.css @@ -0,0 +1,83 @@ +/* Copyright 2013 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. */ + +/* Derived from /ui/webui/resources/css/table.css. */ + +tree { + display: block; + outline: none; + overflow: auto; +} + +.tree-item > .tree-row { + -webkit-user-select: none; + cursor: default; + position: relative; + white-space: nowrap; +} + +.expand-icon { + -webkit-transform: rotate(-90deg); + -webkit-transition: all 150ms; + background-image: -webkit-canvas(tree-triangle); + background-position: 50% 50%; + background-repeat: no-repeat; + background-size: 8px 5px; + display: inline-block; + height: 16px; + position: relative; + vertical-align: top; +} + +html[dir=rtl] .expand-icon { + -webkit-transform: rotate(90deg); +} + +.tree-item[expanded] > .tree-row > .expand-icon { + -webkit-transform: rotate(0); + background-image: -webkit-canvas(tree-triangle); +} + +.tree-row .expand-icon { + visibility: hidden; +} + +.tree-row[may-have-children] .expand-icon { + visibility: visible; +} + +.tree-row[has-children=false] .expand-icon { + visibility: hidden; +} + +.tree-row[selected] { + z-index: 2; +} + +.tree-children[expanded] { + display: block; +} + +.tree-children { + display: none; +} + +.tree-item > .tree-row > * { + display: inline-block; +} + +.tree-label { + white-space: pre; +} + +/* We need to ensure that even empty labels take up space */ +.tree-label:empty::after { + content: ' '; + white-space: pre; +} + +.tree-rename > .tree-row > .tree-label { + -webkit-user-modify: read-write-plaintext-only; + -webkit-user-select: auto; +} diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/video_player.css b/chromium/chrome/browser/resources/file_manager/foreground/css/video_player.css new file mode 100644 index 00000000000..decb63dfee8 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/css/video_player.css @@ -0,0 +1,103 @@ +/* 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. */ + +body { + -webkit-user-select: none; + background: black; + font-family: Noto Sans UI,Droid Sans Fallback,sans-serif; + font-size: 84%; + margin: 0; + overflow: hidden; +} + +#video-player { + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; +} + +#video-container { + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; +} + +video { + height: 100%; + left: 0; + pointer-events: none; + position: absolute; + top: 0; + width: 100%; +} + +#controls-wrapper { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + bottom: 0; + display: -webkit-box; + left: 0; + position: absolute; + right: 0; +} + +#controls { + -webkit-box-flex: 1; + display: -webkit-box; +} + +#video-player:not([tools]) .tool { + opacity: 0; +} + +#video-player:not([tools]) { + cursor: none; +} + +#video-player[disabled] .tool { + display: none; +} + +.tool { + transition: opacity 180ms linear; +} + +#error-wrapper { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + display: -webkit-box; + height: 100%; + left: 0; + pointer-events: none; + position: absolute; + top: 0; + width: 100%; +} + +#error { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + background-color: rgba(24, 24, 24, 1); + background-image: -webkit-image-set( + url('../images/media/error.png') 1x, + url('../images/media/2x/error.png') 2x); + background-position: 25px center; + background-repeat: no-repeat; + color: white; + display: -webkit-box; + height: 54px; + padding-left: 70px; + padding-right: 35px; +} + +#error:not([visible]) { + display: none; +} diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/bubble_point_white.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/bubble_point_white.png Binary files differnew file mode 100644 index 00000000000..643847baee3 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/bubble_point_white.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/check_no_box.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/check_no_box.png Binary files differnew file mode 100644 index 00000000000..8dddff8fa17 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/check_no_box.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/checkbox_white_checked.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/checkbox_white_checked.png Binary files differnew file mode 100644 index 00000000000..466fa2266b3 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/checkbox_white_checked.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/checkbox_white_unchecked.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/checkbox_white_unchecked.png Binary files differnew file mode 100644 index 00000000000..88d97d7dd15 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/checkbox_white_unchecked.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/close_x_gray.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/close_x_gray.png Binary files differnew file mode 100644 index 00000000000..8a2930af8bd --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/close_x_gray.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/disclosure_arrow_dk_grey.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/disclosure_arrow_dk_grey.png Binary files differnew file mode 100644 index 00000000000..8f3531966e3 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/disclosure_arrow_dk_grey.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/bubble_point_white.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/bubble_point_white.png Binary files differnew file mode 100644 index 00000000000..ec5dbe9580e --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/bubble_point_white.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/check_no_box.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/check_no_box.png Binary files differnew file mode 100644 index 00000000000..3006dcd62f9 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/check_no_box.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/checkbox_white_checked.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/checkbox_white_checked.png Binary files differnew file mode 100644 index 00000000000..b24de30f8c8 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/checkbox_white_checked.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/checkbox_white_unchecked.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/checkbox_white_unchecked.png Binary files differnew file mode 100644 index 00000000000..c92eaaa2d91 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/checkbox_white_unchecked.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/close_x_gray.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/close_x_gray.png Binary files differnew file mode 100644 index 00000000000..3ee375a32b5 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/close_x_gray.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/disclosure_arrow_dk_grey.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/disclosure_arrow_dk_grey.png Binary files differnew file mode 100644 index 00000000000..dd2f925b573 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/disclosure_arrow_dk_grey.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/spinner.svg b/chromium/chrome/browser/resources/file_manager/foreground/images/common/spinner.svg new file mode 100644 index 00000000000..66b05c38ad2 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/spinner.svg @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg viewBox="0 0 32 32" version="1.1" + xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + +<g> + <animateTransform attributeName="transform" type="translate" + values="0 0; -32 0; -64 0; -96 0; -128 0; -160 0; -192 0; -224 0; -256 0; -288 0; -320 0; -352 0; -384 0; -416 0; -448 0; -480 0; -512 0; -544 0; -576 0; -608 0; -640 0; -672 0; -704 0; -736 0; -768 0; -800 0; -832 0; -864 0; -896 0; -928 0; -960 0; -992 0; -1024 0; -1056 0; -1088 0; -1120 0" + dur="1s" calcMode="discrete" repeatCount="indefinite" /> + <image width="1152" height="32" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABIAAAAAgCAYAAABq+wOnAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAQa9JREFUeNrsnQd8FFX+wKduTTaFQAghhBZ6R5oCFkBUBJWTZgHP3u487Hf27p2eXTw8CzZAxIKCgBRBOJVDEAXpoff0um3K//dLXnAz+ybZ3ew87q87Hx5JdmfmO6/+yrz3e3xGRgsuwoOH5IBkgyRnDX8iN6XLpc8JkqM/fGXjdK1M1/X9nK7+xAnCMp6XFsF5FY3ddOuMNjHxIUn4We64D/o6W552B8+Lybqu/KyrgVW6pi2QnOkVkdy0qXzynQ5JgRTsMHFpji2tg86Ltq3kc6Z8SAFIvm43H9BPRf7r+JHkPcojwU/wE/wEP8FP8BP8BD9mPuhGUcNBTzql/GDVcZdoS7qe0/Ucb8HPn+9fMHkHS76/eKdNcmVcIMjuqYBtpVYXfLbzvSHvsOIHyg86IP8X8pJ9JOj63TlBbAs/m6PtoQWrPt332cRbfQWbqyNtf9E+g+ItTuYF4SLQ688C26YX2DgteU5Iwa90XS0B++cY/NwJ9bMJymamnNLW35B+HS1fC1Rm6Jx2MS/IZ/KC1IXj+RaQ/wz4SgRmqa4pe3UtuEFX/SvUQNVimyfH15CeHwO/GfAnAP8M4PerYfNCGhSzDv/KgJ8P9tdPuuJfrqmBL2V3ZnVj9kY0z6Ap3tZQxsCXzoG67w58MFx5d02b07ViKPvjkP8fdTW4QtfUzxqz//A5ouFDuXaCPI6H/J8Fdd8N2kBOyLdB+G432r6a4v8K6uYz0eYpidT2iuQ5IE9DOF0Zz/ES1L/QjeQ99IQCeIaNUAYroF7mCpLzYCz2n8mz8MAfDvyJwD+bF8Q8Mv6FPkAl2N2/6Jp/KZTNPOgnv8Rqf1KeAfnDgD8Fyv9syB/yBQPfD/zNcM438P0cOO+Hptq/oc8B5ToA/rsS8j+K5F80nK7C97vgvK/hnNlwztp42d/4HCh/2l78UVtny35vQ95OJzLI2Ejygb8K+JB/cWUkMliK4BnxnCRIzhCBw6V0ufR5aGTDfq0iIYPnORiQxP7w19XA9kKF/Bse5B/w9+EmCH4qH4+M/rdmuLPPmAvs5FoNwTaAk1zXANsHQmG25it9QkrO3tNExcOUb1BObJ2v3fIydPzJpMNuhXHhWV50oJDUWPBJwoFBP7zsVq3FoLsdsqfNflb5D+VD8kKqhMarNLEDNIlPFJNTlv8EP8FP8BP8BD/BT/CjN/xAPzil+Q+W7WstJWW93O2mfedoqv+Xyr1L7z207M+72fH354iujDtkV/OrQM9FhwPnzh5yS+64OZfu/3zKRkb8u+1pHa8AfurJRpGcPajD5OXF+XNHLrSSr3iLOgqS4x6bp/VEwHho5wiye3zOuTO+3fXBsDnx5qu+km5gzP5FcqZNMuPzvJAOPzpAIZxR84GLGwXt9sq48P1lfYB/u2BzT6p50U5/gEw4B9NgTnbfItpTyzXFuwiMwefAHvmhSfxA+WmCaL8P+BfQ+TzymwO7Odhfg8H+ukHUtTK0vxRfyTO25NZ7m8KH+5zFC9J9YGueE2701xwytMtMHpMg94LRaho6I+C6D8H+e7Kp9p+mVI+B+97Di/ahkEzGH16Gc7rCo3QVRQfYf/qrcN08Leh7SnKm72iK01tXfVPg9n8DO7p7uM8h9EwB62A0JvjrKV31L4bx6iGo/41N4UM7mswLtoeB36lBPscnwTmDeME1CP54ANreal0NPA59c0UT+ROBfz/cu0cjfDuccxqcA4m7HfjfA/9h4C9tSv2j/Ol8zeYHRHvKg42cKkIddIE20gV+vwns/826FngU2u38pspfeIYa+edo3uM1dAA30AY6AL8D/HZNrTMq8CgvOj5oyBEkNFSikHDAa14zpBmELwzK/RqpOydUyJ/hl3xITxMBHlXlN8THIzXvkjPqnD+GSx1Q8VdLya22a4HKf4IQt8fS+Brjhx65F83tX+f8qa0LEQSH4y1oCBuhIfa0mh96gGAelz3q5d2yp80+4H8H/B4s+eRcvKY5NF4PeXt2SvjkHgl+gp/gJ/gJfoL/u+ejA6axdKrzjzMuFG/hTR0vW31TcttRGaz5VUe+k1R/6X1ySu52UKovBoXOA8r8kOT2F7zMgu87sVlWAxUPyylttoEue1ud86fO2AFjYLKVfNSZCX8n8G8Ndf6ctEzcmcOt4uOMI9DdnwUDeivwrzVzvpwE2pLaxTX/VcddWrDqedGR9hPwr2uMb3BIXdis7w2tm8KvmXGj+t4Hw3MjtLsrTZ0/dEMQ2+oUUU5aXr7rs7xY9G/Ie3Nd9c8Dm2ZdTfuPjp8CZXCTLTl7K+TjHzh7KQbHSxbwP4f7fA38kY3YqmHOCLjuGrD/fgH2U9iWouVD2bfRteBXguRaCEb3sOjqkHfCddMkZ9pm4D+Hs9ei5/s7gu32LRrwtc6fqA4RyuxCqLv1UI6v41gaY/5XQzuaXev8iU524UwpqIPl2IZiqX9ss9AGXwD+3FrnT5QPIMiDgb8E+J9iW45F/qLt6kjv1Eq0e+6Jni/2hGf/CMpwJZRlbjzkPy/Z+0bxAHnQdt6DNrQWyiAvWgcQvnXBSksya/ia4ovUs4jOFyhAfR387Bip16sxPh6BykP7GikFWbAl3S4lZ/8XOkHnaLxukfDruaGdzXNMGkJvXpTXQSP4o5X8uiOl49gke3reP3EQJPzBwF/Pih96dLryu+u63rRnR9cb9+7QFO8k1nxybRK5l2Si6DLjn+r8J/gJfoKf4Cf4CX4jyreU1Obs7FPFB4OtJRi+GyRnxgxbaruXWo9+bYGr1SA7K36gdE+2K7Pf16I99XHjUgtetPWwmh8sP5Brb9Z5rWhLfihsqUedgaap1Zbxy/ZDA2hVxzc1XvVA5S4r+KirQ72vA939DtThI7hE9Rfv+CpefDVQ3k9yNfsFDMi/RNlv60rGW7X/6/JY+WAwDxFk189gwF3epP7HCymOjJ7nR80PVp0JxutPvGifEJ3jJewBHFCHd/Gy4/vKA197ouCfI4iOX4A/lmvSUcO/F9rS99HYfzWzfkT7Jl6QRzWRj/bndDmp5bpg5ZEOUfAvhHHmR7TdmsbnBEFyXYdjqeor7hpF/8/hBdtG4vhqWglAG4L63wB9qn808seZ2b8V9IGb4sC/GNryZjVQMTAm+SvYsP+JsXdB+Wwoyx+hTY9sqvzXg751MTiiToe2hI7AMWagMF8GpGYmHV/laqdW+mGQmgYF+zr8jm8BIvCw8jgLBtfF4YD0Y0O+lEj4KCcOLJy2pNtN+x6BXN7X0ACHUwMlR+p/YBA4T3KmNzYlMmI+9+vSLlENVH4OVfQI0CjeTt4Jg/mbuur3QIN80Qo+SXZP3kUdwpUG3lHLD+A0wX9YyedqZ3qJ7ScsOkdKzn6EiOGWvOCcA/nPgvy/wIIfelFqp0vS03pOHQqK5QZ7eqcDrPnkntguirjadfIJfoKf4Cf4Cf7vkN/QEihQfk9p/sFQGNT1pj1zeV7K1VXf+hPrX7i2aOOMIlb8QPnBtmCwLQPFtWOI06Vb1vAnRubPHfm51fxg5dE8OaXNcsh/GxPHywZyH0v4SvWJ7lJyqyXAb92Ag+EwpytPwi9HreDLnhyMI5LZiJNjvqZrfyd1Ecf2VzIYdPUva+PLNHjgvTC8QYGmeF/fv2Dy0rjwA+WjRFvyp2aON0NfxPvhMi8PuX9NEYIBfb+veKcSCx/sqrGCzT2vIceb4RnKCZ9qpAarj++NgT8/qhk/jVl+vNTBltR6Ioxt7zTGhzHnMjD8Z0Xo+IvUCO8rOdN+hLY1QnSkfdcI/wpBcr3F0WKsxM7vJblbrFW8RedLzmabGnb+eCcD//0InQ56JA5CjFkj2lPXRGL/ovzJm/r9lXBRs8b8RKTty8RR0VD9t4E+tVL1l40V7SnfRCJ/9WCFSNq3WT1gjKsS0udTG3RUwlgm2pJWQN++RLR5lkcj/32FW4Kqr/QdaDfXGM4r4GpjHNvJ+Y4G+GnQphdB35og2JI+j1X/KN4862/N+lwvgTwcAn9Wk2fA81BWNDBe8imC5PwU2hb6bOY05ACSTOB1A403RHnBgaXOQ4oV0I44gy6GdKZJw0ShggM1BjHabeKQapBf90HIczwM6RVIZ0NCj/VFVIcUNGgQLEuUisMDG1gXGjE/9CBxbg6RfD8HaTS1H4j256ESThgroal8oowo8BwojGGA0/cBrm043/a0rgWLYEB6M958UgbIL4dBxCknZ59Gyz8MsKW86JgVLz5pBwpJJ/l1QjH3orkD3K0Gv8XVrNPWgzgTiqyLjJlPUeBN+aHeeHLvwtC4SIYYSE2q/zo+UYJM+Zz5uvQEP8FP8BP8BP8U8Em8G1M+yA1TPlzbZL7vxOYMe/Ou8+qcHyAnB2b0u/n+oo0z/mzGJ/IrLvn3FWxpbs/oshL47cIqxt2iCvjHTeRmXPiBkvxWcmruauBnmZTz12BMXUP0rNBniA+/4lA7Oanlsgb4aLy9BjUzT3K1qCTBQePGBwM1T3SmL2/A+YMbmzwDCYwYvtjmyakLUBoXPhjo3cFAXFJ/uVu9A2OJvgrpY0g7T95YctbpZH54lpj5YKCdDobqAnxha8Ivg4ROjPmkLkL7AxpgvSDt2vXB8JKY+Djzxeb+uAHnRymk2ZAw7hKuwDhBnAAisb+GQsJZQyOwOJXqEy/sXzB5QxT8EY3wsWw/gfQFyf8xrjboNua1C7F/cKZ/2GwPNVhV3Shfqb5AkFzvNuD8wPzPg/QlpE2ErxHnFs6wOQvSZZAos10wJIl8B7SPyWZxSXHmDfBnNcBHBxraDjjbbDOkI2Rsxp2UukE6h5R/O4oTpKXkTF+jBirQwfi9SfmfKciudxrgo42Jz7cK0s+kLPHc5oQ/ivBbm9q/3qLBkrPZ7obknx6oKjX5fj3J/3LS/+qceVj/uDwJJ3dMg9SSUv4e0e75QvUVDxYd6dsak7/oQFWqC16Cce7ukO/XQML2sZr4D+pi2zgJ/0JIUyFlU/hJUO6fqYHyM0WbZ0M08j9/zlkPdLjsm9UwNnUm9b6SOF9Cr0P+BZCupZY/x9ugb82FMWakYEv6Nhb9I6P/rUcIg3Zg/7uEw/g/tX3ByJcFyfEOtLFjuKwydFA6eQYZxASKp+1EA8pPXcfEWT0vEkdMP9JJaUdzMoAbB9mm8LEjfARpIimID008cc1Ed4uPg5VHaQN8U/ihAvI8Ugb51CeQambidLOIT5QB/lzSUMPHIUF6VdeCfSzkozD2CnYP1dMOSuVM4Pe1ml93jatlv/trnT+1nYAX7W8CvzcrfujnLc94sHX2iOd6mqzJjhufnEu7RiCMBD/BT/AT/N81nxLzxpSPY3qEsXMa5dPGf4wT0eW6be92u3n/vq435n/b7pJPTzfy45V/Gr9g/fOCvVne+8aZL2Bcd2HGT+/4Ec35g7qMaE/91Eo+xtyRPTmfmThfjhG9Dg28vVbwMeaO7M5cYMIvI4r9AEg4O6Ey3vyaXbbsnvloqJro97dA6k0M0OJ488HYc4Nh9LGJ8wf7AMbhwFlhT4U6f+I1/mCcEDC+PzFx/gQJtxWk24gharx3CTFMj5jpfw3ylepW0Nc+NHG+oGPtCUi5pB4WQzoeYgCrxCCeRZwAzRVvcfrOWac9GQU/q/bFtCn/ScL/I3ECHSDOnzrH0H+Jc/A04ojZElK2yw4smvpNo/kXHWYzX7AcH4KEY9MNkBZwtbO//KRucCYcri55HBLGy8GlY2HBl3Vd9Zm1P9xlDPL/ngkfZ3rcCSmH1P8ikn+F1AHWBRrVD5A2OoU8X5gTghfEh2h8/AxssydMZl6hs+FqSG0JYwX5rG73vaPks3uJ8+lG0mfD7V+75zOTmEQn5d+h5bctgqL6b8h3O4lzZyCx8X/h6s/kKidt/17SRv5MxqwwJ5BgS1mAfT0S+Q/t90V/8c5xnK5NJ05FnGDyBjpZufqBjbF9oFPlb8T5cZsJ3w19fL5JTCRT+a94S46J9hRsG/cT30IB5QUQOsceIeU/3YTv5GXHnGj5Ecrf7WSM6kLqv4TuBHLOgf7YItTjVHfgQxk7fxU9I40e6J0dQ4TWDC58Rk4vUlh3W8BHAT2ZDBKzjGxekPtAJ3iSVBJnUf5XkYFwPvHG12sEHC++Db8MNjTiePJ3kc46m3gFQ/l2aGNvEWVCtYiPHu95ZCC+3PCVjedF7Ex9DG8C4sqHDqPXdAJdSzPmH/gfsOLDwK4KkiM576ofnq0LEu7pOHY+aaMRl38MW6ee5HP1p2giIxnuV04EjyX5r+OTPIbxidCwqv8l+Al+gp/gR8V3pHdyGpZu4KwTJnzRkfpXQXZPITpKrrNl33+n5I3rkj3qlWIW/LQeU2/hRcfocJtBXkJkmaX89J5XTQc+bYcTnDE+MUoHYNR8ObXtA7xoG0C5Dxo7FxCDz7r6d2Y8DmXd00SxR+Nrn5V8yZn+dM0uSuEHGn8XEp3SMr5gc78EfFqcFjSkcVb/T1bywfh+3WTmE26njasaotpNKVT/i4jPSzhLnRYs9wDh/xgFvgjqs252VqT8t3EnKRN7alyoQyeCA50BOAlgeKBkt7h7zjlbIuSnmbQ/zP+2KOp9IXGI/Puk/aFrBZX7lv/LnC/CuXwqzflM+JHuJoYzkuZytRMg3iVt12B/mco/mmNkJRn/iiLko/yaCWkJVztb5wzDeN5dcqQ9ThxaVPmLy57y546akHvh+/3klFwfeYZAhHw872VSB/NJOwjhi3mC7Ho8Uvvbnt7piyj7PToFX+JqZ6l9QRyCoe2srSA7MAzKTZHwY5D/WP4vECfh58QhE8pvEw0f9Z8obb+6+l9KnqGbQaBnQh28ROzPk94midL4vE1QvuoOXGo0iaOv+/wL9+tUJSv4c0jnC2u4oGjdoniLQyObW8EvJQPnf8KVKnGgpnjHW8zHjjuBdEQjv6+u+q60mK8Tr/USiie6u64Fr7OYX8f6wIR/PQs+Olo6TFo2vN4OcaL9UuBfxYpPUZ7dZLmB9eVPXzrhNjifE/wEP8FP8E8J31+4rUWX67fPbz95+Z6uN+avyR03B5VWbxOcP43yQ8ffin3LkkXZfaNBRqVmj3olnQUfl36BUfCIifHxkNX8QPGuLNGRSuPgW+jxFGMpvvyKQ+1EW9KdlGvR6XJmDM6fqPg1W53b3H+mXIuzOobH4PyJkl/c2STgKnJHxOD8iYqv+st6465JlGtxycuwGJw/UfExQGvtTlem/I2W8muCDttGmzifhkbp/Ila/8OlZyb8XYS/JQY02nwrbGkdv2ps/AX+ecA/l3KPLYS/LQY+Mq+ANChYfuDKo6vuHnZ4xfQ9JvyzgH8e5R7rSf3vibHe/8DVvoSvM4kUf9GON4zlT353K5XHjKFB0Il0fhTOH6PjdCRHWYmDY40WqBzakPwNlO2vkFNyPyP2WyAGPjoOz4K0jmJ/34rLTRuT/02Uv3tJ3YWthIGx5lpDUHAr+LvI2L2bwr8uEn4T9R8cu8+hjd286JioBspP40I6gXG3B5WjTSGL7fiM0zXcxnKG4XP0duG0zust5GPjvZ2rjRFUjy3IDpwudpXF+cd1r+i9xfWazerrdxJOl/vYYj7eZxoRoPXXJfIy8mdZzMeB41Ku9i1abn1PqHgn8VRqFre/J4kjsLeBfwf8+BcDPid7csKmVQP/IVL+qtV8ci9bmzHv9HVlDbgR4C61unAmg/ZXj8/9Or22bneM0gQ/wU/wf598ULBKiQJM5ccw67HeETK7scH8yyltHgelbFStXJbbu7IHzcy9+MNcVuXvatn/TwBuYbjmCEePkxh3vuxpfSvl7TsaHRhPw2c1X0zKBB2NN06Lx2VOk4gOZSlfcqTdS1n64yMGXJHVfEF2PUhZ+hEgumOB9XzHXyn8INHdDlnOlxyPceFLb5B/CXGCWMoH45/mfMSZBOM46lKeOPMF+X4T22Eco/w/ZuLAGEvGIav591KuwdlbF8ah/f931/tD0ZHTwrz8pdsp1x0n5d8UIxzl2BW6rsxXqgt7eY+u/+bQV7fsMJO/uz4Y9n77CYuOyJ42Z4u25PXwYG9yjW9a0NBRNwkA4zV1DTX+OEHECRhrLZb/FaQPI79VqMMD+jyMOTUTBKzkYxu6gDhQM+rxZQe2uT9azC8gDjx0IIfKN1GQ7NjmbrCYf5z0Ycy/s575KchY/n+QCNhJ6fw6F6+DF/6la+olvCAat9W7ovM1P9+1481eVvJfJY1wRH0vnHOS6i+9jfs1aJ1VfBxAHyTPEVIk8mm6GuhFPJRW8nEKOTq73q3PFzvrWnAQV/uWzUo+TmPD6X6fGNpEe2gTZ3G1a2et5KMgx7drqyl89JAvY9D+0RH5z3qdnBdyCP8rq/k4Hbn6yDqXq9XAj3Adck37T3GfA3z0kH/fEL+pRhg5dBKcMdTIcIYIV2vLv/ZeCX6Cn+BHyI9x2Y+ZI4bKh89r+B0mLRsvp7S5mtO1ssr9K58FJfn7ePLN8l/H73rTnnp6Cc9LrdythqDCeIwJ/8b8qyjX3NVE5T8ifvMB0/mM0/50HeWaR4gCaSk/9+IPRXfWoKmUa3CHqX1W8ztdtcEluTIup1zzHNe0mR8R8TtfuyUVjL1LKde8yDVh5kek/C7Xbk0XbO5JlGtwmcAGy/nXbWshyG7a7ItXiOFoKb/rDbs786JtKOWaJ1mUP/C7A3+wCX9TvPQ/mv5Vy9/ViRftp1Muw/AcO6zn72zDi47hlMumx8n51iAff+ly/Y4kSlC62+I0/usgTz7d9c7AJZHI3z0fjcFZlx9HGXOtoQMdiTi+rq/3qaYKIbGIrJT/GJ/oZg4ngoTa37Lr0mDl0VuIk8pKPjqw8WX/Owb7/1KluuBWUj5W6l/4EuevnGESCuT/csVbjJMgKi3mYx++n9ifIfl3jFUDFem4BMxB8T7Fq/H92gkEETuUZvjcqauBixjw76FY5ZBvfhyj/L/B/bpl56+FoivjGfFxGVT4NEZdY8XHzh8euE9XJjLiYwC6NRT+pYz4OIV1aThfncSIz7laDTq7zvlDDhHyP5UVnwg0Nef8f3fNm7ruhvYTFo2Q3S2drPjknqExl3jCTvAT/AQ/hB9H5bNRft609aPszTq/AQrZGWAIXuDpMOYj4MvM+JevOQ3jAhjO3RNH50fD/CvWDuAFOY+itM5jwU/tOvlMdHgZzi0mOovlfHtqh1GU2U+VxAFhOZ8X5YsoW35XEQeQ9f1P1y+mzD7Cc59mwdc51AHDthxnxudql+nIFKP1MSb515UplHOLjQaThfyJlHOPsOOrfzAxWt+0Qv8z8jXFN5QLDwqNm+m8x4KPKVi69z2Dwb0k3uNvQ3wG8hcdqf8KMTwqq47+dyZD/oJw+4tPFmTnRYz4WL+GZaQYkFsaw0j/wrI2vEnn3TxfMymFBR+dT4cNfBns/3E4A8hOGSisOLbpWnA1KDtn13eCaPj3cov5+CYDPaD1gvxBA8DB5wsG+cfpvLgW9I56fF4cxKj8NeIEeqB+GxCGMeLj4DqLq32rEcKXBjNsfzigD6ufffkshnws//pvunhxCEN+2FReyP8IhnwOjL2RsjuzZptLOSmL63DZ14O3/7vr/TR+nGYe1dcqZrTBe4c6wewmipJVR4Kf4P9P8V1ZA+XsUS/fIto9/VV/+Q9HVt7+PEs+KIGjuPozI3GpNMrlL5nw7ckjKOfN4eI7A8ucb0ui8XFprsKG7x5JOe9TzjonZD0+LzlHUc7D4JnlTPiijZZ/1AmLGPHPNcl/MRO+INP4+MKukA1fGkY5D4PHljDK/1km/b+KEZ+Wf9wNy8eEz4tnUM57k9X4EyjO3+PM6q8bjOAXuPDJApbJ3/x55y3NvWjuJfb0zqN4XtgrOtJeZTX+M5T/NwXL9n8B9nZexd6la45/9+Qxxvy3jPaXIDkxDu0SRvbnDOKICTG/bDjz7CsGfOxLr3G1u3SF8O0Yo2cVI/8Dbjx1f32+PBwdQJLhZL9V2p+uBecbHUDwGL0Z8T8LcwDV7vrAKv/LwhxAtbs+sOKvNDqAQPh2Zcj/JtwBIXRhyP+aC3+ADq6Wp7mqj/3Agr8mHC928nQY4ynPX6Qw4H9LyX9eWrcpaSVb5/hZ9H/Z3QKn9Z5c6y/Irmuh/J9kVP4195bdLVNaDLm3bfXhb0+UbJtHC25nKd+gAEgJfoJvPMcK5yceW2e0CePnjHnrcdHmub62P7pHt7nwXVz6dCOr/IMBkk057ygzvmjvTTlvBTO+5Ogfiaywii+INhp/FUN+n1Na/oLcj3LeV+z4Eq39LWbHt9F2HlvCsPxPo5y3iGH59zSxFViNfz0o533ETP7Qdz5jlv+9n16yK+/yNffKnpy7OZ736JryOrSJt1jL3/0LJuMEAUxFIH/9rPks9I9dHwzDcXXTKeJ/ThwhIUzdwdD+wzaNs6D4EPt/IEP+4jAHkCCz5C8McwDxUneaA0jhrDu+p7iF2jDih7Fh8G3DMP+bKQZ4luRMsyneEhZ8SiR/PjW53eiUir1LvQz4Wyl8e3LuOZ6K/St9DPi09cxCUtuRWdXHfjjKgI/rmXFqsyuU78oa2Ko8f9EBBnx8o4pe/3oBqZ2ZfXNKts7Zzab/8x5j/Xs6jsmB8j/Igp83bX2m5ExfBgNvp5RO45WMAbc/suvdwW8wHP+M95YiOCfB/43zs0e+0MGZ0bOn98SPmw6vvPM4S74gu84zKCWXW+gACuPzgtiKct52hvw2lPN2M+PzUjsTWcGm/fFiu8hktUV8gcrfzK78qfxt7Nqf1J5y3iaG7b8D5bwNLPio+8KAQ+v//2XB97Q/H/QRnrb1+EYWfGeL3k7K1u9BlvUfrDo+x5bcOsQJrK+GMmE2/hLHBM54el90pAqqr/Rot5sPqKdS/odsHvC75FvELuZ07RVo73+p4/iLd7/B0P4+wdUurc4J0XX6M7S/UaZhzKeUX60/IYth/sPjmQlCdl0Q6NBDs6r1cbp+mPKphxH/EMUBk8Yw/yeoDoics5JLd35awYBPndJrS22L69+9DPjYy3Bgr7fbgzNrYLOK/SsPM+ArYR0QDtmVgX8fZdL+a8vAVW8EdjXDNniAEb/A6AAS7J4UZv2fsqOC5MzA/B9kwZfdmegB71SHlpNaPdis381fFG2cEbrThGbhDAxj3iixBy0t/wQ/Aj7L+u84ZeU4W1pHnHIu2tLzFE/n8eiAmccs/7ruNZRCgG35CzQDzMuMz4vNKOeVs+PzKZHKaov4nlPJ5zkhlXJeEbv8C54IdbW4892thmAcStoyjKMs+J4OY5JMjMBjLPgpXaekmYzBTPLvzh6SbqKjFLPgOzP7uk3yrrBq/7vfO312h0lflcvJrc9Vg5W7ecH2pORMPyXyV/WVagn947fL3/pa29vbT1i0QXRmdPCe2LTm0JIbtjH1P9TO8Mr51R2hHFC8Jcblh5qFZY+KZc9fhZ+YyjD/ASLXM0L4yRLH8Kg68HVhcocLw9xCjPCU7UR1lWH2qYN69fGN1Yz41MCagZI9rPg2LnyrT4xGrzOsA0cYPljtZ8h3hTdBnWX+KQoHzzPkO8M+EW02hvxexj7hTO+MU6ALWMA7/XFTEgw59wi2pB6g7Kzbv2Dia4Gy/WHjgoUOCC5xmAwM6Z2kQPkBVVN8TLlySu4dIeMizgjB4KvzGD6CUeFIYloAPC+bjFNVrJ6A8pmHa9r2v03ly8yKnxNoDgg7w/qn6aBMZKLoSBNMvmKiEwh2j2jyFRO9VLS5xWh01bjnX3Ka2R9BFvxA1THvqap7PIo3zyrLPOOB3bwgdwz5eBVr2Zf/4bm45K5u2V2lVfpHQk9JHGSXs5Wngq0p3r/wvNiRF229dF054T22cTrjRzC8XOPdjoweNl/hliAjPupUGSG2p18iwjZUCRGsEkC2tE60t13Gt21W8WnGdwXD/GdR9JwKMACDhGk1vzlN0FbsW1bJiN+C2iIPrS1kVP/NaYotGH2ljPjJXP1tIGs1reqCYkZ8dLS0DtM0vYVFjPiobLY/hXw8csPK31dcxoovOdPegqGmZstfIcl5TrtLv8jY8WavB1mNv4Z+Hmpo/U/xrZoCDoplGF9OyhLbT1zyuOhIvRzkgU/X1Xu52oCBbPLP80YHaDvSVwNM+LpaYvA32IisOsqIX0Hxd+Bbuu1s+DrN0YTLUg4y4tOC3eIszV0s+DqnlfHhs7AyWfFryp/nkyj532s1X/WVqJyulZCZ4EZd5bDV/Iq9S6u4sLgYNQcq6UVW8ysPrqmk6L944JvpMqv53mMbzBiY/xOWl3/+l2WZg+81zkpPs9AAC2v/FfuWX5Oce87zvCDnasHqb4OVR6bb0zsx49PkL8jJ37X+8Xvn/1brf/vrnXEjnPOb9b42DexetL0DbMtfP1YPpWvlvsItfkb2Nx7u+sJfLRW4yNYFxqf2bUmU9cb8Pkb8vHD/j7qXYf67hfE1ZQ9Dfv9wvnpK+fAAR6uOfFfJiE8J9qiXFP30xnFGfEqwQ726ePOs/Qz5RmNTLd+9iFX/w4DjxhlAWsXe5azyn8sZlr9h24f8H2TEB6OCHx/6gWjzjGU5/na+5ufzu1y79dXOV//0WNbwJ9oQNjM+R1/vfEr5uRfPHys60q6q2RaTF5JBEX+ZtFUmfF3x/4dy3mBmfE2lOTqGMuPr2sHIxkqr+ArtdfcQdu1foxm6A5jxNe1IZLLSqvKntr++7Pg6bblbb4btj1b+3Vnwg5VH/aCD0RxdPVjwKw+sqkQdkHJeRxb8QNk+H5S/If+6Vcu/qPV/aMkNW7fNzBu79bW2vba/0e3G/Lkjq1jyf+/yP8H//fHB5ishM++Z8mG8fRjGuzp5owSrjj/EkI+2T4bB/j4ikQcINQxxloQl0yAFyUnZclD/xfCBVfzTw/0PwZ8Z5j9su1NdDWxgyD87PP9M+edSyp8lP3ztoaZsZMi/iML/OVBes+OAiwH//HC+uq3q8LdVjPjjKPydlQe+rmTEv4TGrz72QzXD/Nd7AwLGzyGW/U+0p+JODDWvIFK7TzkvULZ3IAjDKiPfql0wts5oY6coAJzsbulsN2Hhn+D5YIzQ0Sl/F2dNIN4wvuhIMc5Kwzq6mLMmEGwYX1P8Pwqy+zLD51M5yq6JVvB11f8zVMAkSl/5iAlf8W3iJNcfDJ+jo/R9NvkHHUAKG5tHc7VbEVtf/2rgF1F0GMfmMZCeY5J/0IF40WbUjS6A9BIbvrKNshMU1scMNvzABl5wGl8OngVpFgs+p6k/c6JkXPOCW9N/wqj+t/Ki3TgzeARnzU5cNP5m4GdR+N+y4CvVhY+C/PkXMbxU+Pt+ydXcKhuQKv9Y2V8JfoL//4FPdiv9zeV/17uDd3ran396eq+re3qP/7jv+HdP7mHIR71fC7VBdF0rkggs1ADCt/SWBEEUZOellOdaZvjACj5mfiLFAbOSUf6xnC+jOECWMeJjo5pC4X/NiO+klr+mLGXEd1PLXw0sZ8THKe5XnML6x9hH14R9qisrGPFxgLuBwl/IiI9rTG6i8Fcx4uOge2s4X1vNcPx9mAvdApOXWmX0vxUdQEsY8ft1vubn67Wgt7Bww0vvl/zyQXmdoGv7hwV3Sa4Wd5Hz8O07voHuwsV/KqxxBppfC1R9Kdo8dxk+x7byPCSf5Xxv0UouPOjmZEh3Qiq1nO+vWCY60p8wfI4OGSuWgYXx1WD1CuA/RnGA4DKwg5bnP1i9WrSnPEBxAOGM3a1W83XFt4qzee40fI4va3AWxG7L+agDye4bKQY4OiUOMOB/zUnOKyn5x2Vox63mwxj8H9LfjO0fx+tKy/OvKV/zot34cmoCJIwN5mXEN76cQ10Rl8IGGPBXUPjogH7MAhkUxt/1zsDZ2SOe3+Fq2f+06qPr1x9eecduC2PghLe/2qOe/AUDuPxU8jnrguAn+An+75pfvmdxFaTvTwH/qBqomCfakoms032qr/QViSi5oeuARfIQcRU+MKg5u928v4/hY1WQ7AtJAVjJv4ALm1aqV+matoBR/i/jQqKPE+OvDIwhVvxbuLDlL7qP0/X5jPg3c8bpZ5yOsY9Y8THYl9HKUjhe+IAR/3ZIYbvN8Lz4HiP+vRwtBhUvvMuI/ydiUBj5sxm2v/CF9bw0l8avHasOxJM/laMtK+ClTxjxcUZF2LIWQU7aFcrPPP3+rJROF0/WVZ+fFx2vx9EAHAZpuWhPtUHiWg59ZLz3xM/n+Qo21zhYREfqMMP5OFZfDenf8ZQ/XP1YD5hn3653B63uesPubbxoC132lUP67JNW83fPHfFj1xvzd/CC3DnkO3RYPwrpz1bzS3d+siGj/617gR+6HTY6bHF6cjy3g6fyy7Z/tC7jtD8d4nmptYGPBuBVVvMD5ftWy+7MEzAWhcaow754P0d5aRBvvh6sXg66QCnwUw38xymOibjzNTWwSOT0akCGKqGoF6JT7Dqr+SADP4dfA8APfQuKDvu7iRPEUn6tDqI/V7ME9NcjifT/Ry3PvyCiDvQMV392KOpK10N6kQF/Dvx8wsBvScbff1le/rU62ONc/UBgeRYZf7T65w6vmJ4PP/ZYKH8b5LOwvxL8BD/Bj44f52DkpzT/OKYJkuPOduM/+0pwpLXzHvth+aGvbllTFwTaa/BCeUIMg3jAMXMeTfFtFCRnqLK/ZsebvcpI5q3iyzRBCs/yieRMr9t+3cr8o+H/93C+d5aclOVlwM8hylx9frB6tmhPKWXAR2PuYVr5C7L7GAM+vsm9z/ihrvoXQ1s8yIDfx4S/nBftuxjwBxAHkMH/GFwPRt9GRvxHw/nqRuD/aMaHMcMHSlg8+L1ohjwufwPld63Z+BdHPiqzL1D4vwD/Wwb5R8P6NWr5i7Ytdfxmva/NhrQYFPK6+e84YwuXZuxvIh/v9x4XMs0VnS2tR77Yy5bWscbBpAYqNhAnUejxLCR8O9/UWRiCrivj8qau61a5b/nKo9/cV+fU8taVr+ItfhNkwbOG69ABgjPk1sVL/hk+PslXfcVviY70v1OclgvIM1jJ11VfyduiI83YR9EA/RLS53Fogw3zA+XvijbP3yhOUzSOF1rMV6D9vS/akm83fI/OFzSOv7CY79eCVe+BLPyT4XucMTubQflXa0r1PEFyGZ1t6ADAZXirLeYXgS60EGTxeMP3fyb532Ax/xjI4i9BFhuXAd4DCV8Q7LSYfwD4OAtnhOF71Jk+5eLjhG+Ivx/4K4E/0vD94yT/pRbzD0P9fxFa/9Afvt7+7658nOSfKT9Ev6HKXy5+u9FFzY+n/ZXgJ/gJ/u+XH+J/0fPnnbeUfFyN42ud17/SAEMvVGocHVB4L7F06+w7YLBfw9W8cdJRsb2KAR/jDhmDTwe1YHWowmsV306UKOPsmyq1uuBZBnxsVB+F30uvBv5DDPi46xvOcjDu8qHoivdhBvwWxIgybv+O2z0/wIDfmuTfGHxZ19Xggwz4bQjfHu6ACrDgtyV8Vzjfdw8DfntiQIXztcBjIUyr+DnEgEyh8J+g5T+pzdnOTn/c9FjXG/PR8YBvYJs10fmykKPswAf8p0L56T2mnRvi/MEjg5RdiybwPST/Ybuvie7MgrrfAyX5j1OCoeK1i2jXRnFgv1/A89KnclLWE2k9rliRe9Hc/qSsTy7v8B75diaFbyPP3i1e8i+0+EP5vsKtM6FCjMGA8fyPuVoHsqX8YPmh12p2QwpXWtAB0tdqvlJ++EXcEIfCR9nZz2o+yMLnanUSav4HWZ5/X8nz8JGfwp/FxScgb4N8Leh7mgsPRikQB0AHq/m6FnyKouxKRHfJtpyvBp+h8F3EAZnCgP8IhY/XvBZP/TtKPsqdSSz4wbIDN6uB8tmgk2xR/aXvH1/70F1W2B9mfFb2T4Kf4Cf4CX60fJyJ1JRkxsfv6hxAKPyN0eed8RB+APGQe3HH1j5yePvrnSdvnZGbBfoNvnHYbzW/+tgPlRTjc7bkbBa6vt4qPr5FOc/4oeov/7uc0vaQxXw04JbSFFg1UPkMAz46vdDJ19P4hRaofFF0pG+3mI/G/xqOsqOEplS/LtpTfrKYjzNPcAZDO0r7myPYkr6zmI+zN9ZylK3fddW/QJDdSyzmDzHnB74E/vLG+DB2NIWPgU2/pTkQwOBYLUjOOQ3xHRk9PMGqYxg36U8xGiF1+e9M4a8F/lwaP/vclx+TnOnX8YKMM6duIH0olinxPUn7603hfwP8+XV/d7v5gMJJjhMmbZg2OyeSAw3HVbTxB2NfibbkkzNrnJl9ipTqortNHIjrOUoA+QjHn6+4esHfeZezea9pWNY1eSZHcocLK1VvyQMmDjQcQ0bHwMfthF/QNXVD52u3vNKs1x9DFYp6fHfroWWKt+ivlHtg+1/NUQLYxyJ/zfiOFj0LVX/ZE5TL0QjGOHkXWMm3Z3Q9ATLxWZP8L4+x/EMdiWH8UIeHLbX9YZCJtKDLuBQPZ0GdH29+aP5tya33aoGqV03aEOZ/qJV8GG92gEycadKHvuaatitWo3zR5vkBZOK7lGvbkfbXw0o+yOL/AP9DkzH0Cy58++J489cAfy7l2vMp11rBXwv8D4wXBquOa3Ewfhrv/806H9/xRo+bts3sOHrHm73uKdk2ryIO8j/i/m+l/ZHgJ/gJfoL/v8oPFWw46AYpChDGTuFj8EbxkFCBMc7+QEZFiHcqKn4UimcNf98n44+o/tKTyoWuK+j4eIhySdz5YOiMoBj/q3xFW5+ymI8Fu5GmOILx/b2vcMvjFvLx3HHEcKRsPR/cqHoL77OQj236GpL/ThT+VtVfcaeFfJxtg8bc9xwl7g20v72a4r/FQn4yVxtTAB1MOVx4ARzRNeW6SPnQjtPJFMJID5wx8gpxfmRT+AVQBtdGw9eCVS2IERDJFonocHqJGM6ZFH4xdIJpDfFFR6rQdvzHs2V3yzfIvdBZeR9HmUlEObJI/r+h1T/yofwxtohO4wuy60zD52h8obPyWeKQaOxAx+8/IP1AdxzpJbT8q1Un5sHYtMKkPNEIe5GjxZEKP9DRgTMKNnPU2SN6GfDD6n/XOwNmQz3PM3HCLIb0NhfZbAQUdti/tlAdV4IgkbKud+yc1f9tMII/o9wvnfDfpPan8APftFxJ8n8bL4j9wMC9rPmgu18MlX8m/MUmAhyN0HeIQyyS8XcEtLOHgmX7LzOTv8aLijfPel5X/WtM6hNnkb0bYf3Xk7+R8kEmPgmyaZ2JEwSdMDMibP8n+eTaiPiat+hxkA2bTOof8/8cFxbHrtF6iJivBqseAP52yn0yiRPmMcq94sbXgr6/wrhMW+6JbR4d6RhHT7aMrwamE93MeKAMx3ZxdZQ6UFR8XVVuNeEP0zV1SAyKd7T5vwX4BwyyomjXOwPdUcrfGOvfi/zdIXriliMrpq+IQf7HxI+j/hFXfqz6f4Kf4Cf4Cf7/Oj/UoEKDpIQoOaGOIQcx6jAydaSBiZxEcRWNcoYwaGvb4sYnAS9P8ne82euvrUe9/Lac3CZDDZR+ldTm7HIWfF3x7+Ntct8Q4/9AoHT/ZHerIYqVfFDk7+FFew7N+QBGwXgL+VjeaERR31hj/oOVx/5gS2nrt4CPbysvI86fbib8o8AfY/PkVMeZLxBD/RLCb0WvGK1YC1ReJNpTS6Pl484QDQQmRCMNZ1qMJWWQZtIyytUg8G2eghj5WK64JAXjsuA22aWkr2NbQyX5PFIGdhO+VwtWXyzYko5Gys+7/Jupgux6mtwTFWScubMKfQZot5JBMpOU/xhSBiaGiu7XFO9kQXbvb4jfevS/ThMkZ+jWyDiIotP0LtK+VxAHQxlp85nE2YEzVUZHwA/b3QjX40IZl+hqcCcvSa0o4ykGRMW4MLhzHM7e+gXSCTKgY9vH2TqjSB2YvDXWg8C/UpBc+4zfOFr0DFYe+Hq8O3vIYl50GLeFRjmBMTlwd6ylJG0ifIHkvwthn9uAo0wF/hTgH6DlH/jXubPPyONFW19K/7qKq91JbylxCKCDpYAIszTSLtHhM5Eznb6r+zlNe4YWW4LwpwF/NfD7UIQpGqDTiDNoEXGwocHoJ0K6E2l742mOR6jzsyRnGqd4S6jyD/ne45uudDTvvoYX5K6Uh8eYOFNI3WMbxJlR2I4qSf7xmuFcbeya7hwvcHJKLtfpjz++vPPtvk83Jn+bD5iuBYp3TZLT2q3jeSmHkn90auHuRJ+QMkBnEfbjABkjOpPy76D6ir8hToOI5T/KpED5wclycta3wM+i1P9NpAwwLgrOXsPZbYUh3+MzVNPkfyR8GWSS4i2aKDnSvoOya0bhTydtAGdqfEScEpWx6D+09ie7M6sVb/HFkiP1v8D3UPofBqW+kTgisQzwBYsSLz7GQ1T9ZReLdg/UHZ9s+NpFHGB/4WrjCb7bFP2PxgeZVKIGyi8RbcnId1L4b5L8NsaOiQ8yqUgNVIwXbUnLgV+v/KuPfO/c//mUaAIDx5Z/f9k4yP9SqP9M0BXKfYXb7gxWHbNFIP+bzneklVcd+W6A7GwxRVcDwcMr71jmK9wSjFD/iFr/pwRYbVT/+P9ifyT4CX6Cn+D/f+DzGRlhIR7QeGnG0ae9quQh/EQYayEKkkSMNCcFXAcvoni5mPJBiAUNzhrL+LirTnrPaW/wor0Pvl3T/OWTREf6Oqv5Xa7d+jooNOPq+x6Cu5TqwgvkpKzdVvE7Xvnthbbk1vNNnC/7laqCUcDfFW9+pz9ubCc5m30Pzdn0DTE6f1RvybmSq/mWePG73rC7CxiLb3G1M53EBp1yyPeVjwVFe0NT+MltR6qtz5v5LBiJfyDGl5OLZJq4rpWCgj1OtKeuiYXf8Yr/TLV5cp6M0RuND1AJCvbFoOiuiJSf1nVictbZz/xs2CEmxkOv1gJVk6BfLGyMn3vR3JHu7NO/5OJ66D41UHkpKPiLaPy6rWeDlUc6SM6Mz6Bd9Ygvn0PnzxWC5Jxn5IduewtGQIYrs++XvOgYYAF/mmHpXdjhPbqhhaNFz+WQ/55xLn/g+8Lybzz8xTvb2FLbLYb+1S2udBj/tr3WPg/lj7HMQw+l4nB7MSlzJc9LuXEi+3d/MLxToGx/IFT+mvGDVce7SK5mK4DfKkIA3jesfwbKD/5t9/tnvGMm/03z7y3sJTnSlxl25TI7qgg/raaIteB/Cje8ckPB+udLI9E/aM+g+ooHwBi5BPjpjRUscUDtAoP5xW0zOy6IVP9paJtp1V86FMbIRRQnkPE4AW3qFmhTH0erfzXMLztTtHs+NzpB6p0TqLh8xxvdP4xV/2uQHyg/B8bILwy7kpEXW75V22Z2uqip+mc05a8Fq1bufLvvVAygGS/9tyF+6bYPkyV35sCy7R8dLNv9RaUV+ndDfBibftP6f4Kf4Cf4CX6CX3vQllQEiWKTxoW/ycYbJ3HRTUWuu2cJZ/7Gihk/gq3d4sY//u3jRyGN8XQYI5XnLzoSuu7ZSr6/JP8jZ2bvsXXGOiio/w2U5I/FWAtW8nlNoRotoLitCZTun4Brva3h89MbdP6o/jVK5bEr5JTcA3Hl8zwq/Y0uS4Hy36BUHh0fD37WWU/fDsbptKhMQC24TcO3m/XjLkXFtyVn3xWr80fX1F1asOIPYFhtjobvaNEnPR7OH5zargW9l4Fhsb6xc4mBvqzL9dsXC5LrfC4eh64d0YLVk4C/trFT5aRW+RX5C4e4c856VbAlTY0T/4SmeCcIsvubxk51txpSCPxz3G3OfgvOnxAnfjHwLzfEnaK/usjqf6LqyHfDnS16z45j+ZcA/7JI+Pb0Tgd8JzafaW+W9yEvOs6JV/nrin+a8eUD7ZCSs/cEy/YNlZJafcyLtoHxoGPEb9KnGpU/sjtze6Di0FD4+SmMM5HEfrHR23HLq7jaZWtRyX/JmfGz4i0eJtqTFwC/S2PNlaTa0ViQhzbrfd2jBeuf/3Os+geMketVX8lwAfm81NDYjmMhypzmWE+tz5s5+tCSG7Y3Vf+BMXItOmEEmxv5DSkrLXhe/KBZv5t3FG2cURQv/Uu0p6xWA+UjBNn1sRmf5/hLudpZmHHX/0SbZyXwzwT+p8CvFz9OUwMFVuufWP6Kt7CfrimXav7y0sPL/7KEOH+Y6L+pXSdheITVVurfjejAv2n9P8FP8BP8BD/B/9VzRDsU8hDG6NTRK5+19yiMMPO/SX55/qKDETp/4sLf+/HYld5jGyeDIjlLDVQ8AmlEhM6fJvFLt3+8Ateth2S/HJTp2ws3vnZWhM6fWPm8meGp+kvvrdy37OwInS8R89O6X+4Bg6MR549eCfx7Kvd9NShefDBio9gVSPdpgcq/ByuP9YvQ+UPlC5KD56KL/1DHD2rBqpdAoe4TofOnHv/o6r/9oqu+b5vQ/5D/glJd1DsS50+IE0ip2r9ybLDy6BW6FtzTBL6iKdVvqMGqnhhoM9KLMCjx9je6XVXD15XDTfJ9qb7ZmuLtGYnzpx7/310nKdUnpuHMtSZ5H9TAF5rq7x2J8yXECVVa9OPMC1Vf8W2U3ami5S/VtUCfaPgYFBnGrFHQd/9K2Z0qWv5iTfX1BX7E22ljgP7q4xvOgHH7IcruUFEdGPA/UH7weDTyB4MSK9WFQ7DvRCk3Q1teIFb5KznTd6q+0tOg77wWLZ+X7D2aKv9FR9ovMG72h77zToTyR3K26DMsXvqHaE/ZpAWr+2DfbZjP25JyhreLt/6DQZm1QFVv4L9H48N4st9K/Yvwe0L947IvrbY5Kccq8hc/z0L/k5wZ+bveGfiP/Lkj5/gKtwTirX9GsGtLQv9O8BP8BD/BT/At5UuNXIzrzqqJx8nJRT4DAK/1ErjShIf/zfAjmHkUV/7eTy7GHXA+idLx1CR+wYYXC6SkzLGeDhdcwYt2TVP8L6My33zAdEv5lfuWf5DaZcKVddO2MZgjKJBvBssOvILGHBi0ceeX7fi4ouWwR3fzgtwx3PYBfrD6XdzeGB1v8eSDUfYTGJPnNmx7Kce1oHe25i1+AR1PNltSk/ia4ktSvIUzJVeLOyO7UisEI+FDxVfyTzQm4XmjhpM4BeW+E5svklNz7xJEx2hetPWOkF8O/LeB/2KsfKgznG75QcH65z9O6375NaI99bqI+ZyO/E90xff3KB1vxvx/UJG/cIEza9C1oiP1JmhrnSLkB6DOvsDtlcGY2sCLMfPfBf4nruzTbxXsnluMb+Qb8jvoqn+5rilPoeMjlmljGJMGfrwE9T8b61+Uk66G/h1pEF5cDrQG0tOC5FrMxc5/2l+8c5acnH2fIDlxfIl4VwZgfw/pMeB/yXPRT2IjcdoeDZQffBfG0PsF2XU5dH9HFPw1uhp4RLQlr2hoyYepEyopC+XXdNVf+haM5Q9C/nHpTaRO4GCw+gTGHSpA+ROD7OMkV3Nc3nWz6it+iZecjwAfl7w22pJhfFwfD/kP/R3je12lBSpf50X5figDjO1luhuUUnl0ezz1D4wJAz8uVwPl/xRE+8PAHxPG17UCGJ82W6H/iPYUXEY3VQ1UvCqItrth7BsL7U+Gfr2x6MfX3rBa/yL8axVv0dOar6TLsbUP/1J58JtInaEN8iNsjwn9N8FP8BP8BD/B/83yaTGATM/lagMS2YnjSAp5IJ2AMKGQ9jXRc5Xg/z/n0wIN0o4ojIMG+a3OfiYzqe2oUbwgngiWHZiDQW2t5rce9XJecvvRT3OC1B6MrR/A4F8Hhvfq0m0f/ocYkHHnJ+UMT84ePeMRMAjHcJqGHRw4WglYvMd1NbhNC1avCJTvW2sS6Dtqfp3xiDtxdLrqh/N4QR4IqT0niNk8L6bXlolWpuMyF03ZDPzVmr98tT29U6ApfKPRivz2ExZ1kdwtzwNjsB8vSrkcL7aoKRddLSL8nbriX6MHq76S6YHGG+WbGcvI7zBxaXfRnQl8e18ogzyO51Mh2Thd98EzHNY19RdoB6s1NbAYg7pGw2/MSEd+3hVrBwi25NG8ZB/A8xLUgZDKc4JN19UKeIZjYPRvgTJYC93hc2LARsSPxEFQkb9QdGT2x6UpI8Ag7A9lnwf1n0KcAgG9Jv/KNniG1bwgfS5IriPxHHx8JzbLoitjKPDPF0THcGC043iheW39Q/vntGJo/5vg9zXw3UIwmPPjyQ9WHXdBvY8DQxj7QB9g5NWLU6Jrh3GHQSiHVVA3n8B525ta5vU8av4ybGsTgX0OL9j6wTjXjgt9gVPT/oObgP8d1MscyP+Ohtp6tHwtWIWBrSdAmZ8piPbB8LOlgQ99UN0E7X85PNtcXnTsa6iPR81XvNnAmALlPrJmaVot/+TXNcssVf+qsh3z7zi6+r4CElQ9orEtIo1K9eVC/4byl87moO/xgtCGBCtWVF/J27veG3IfjH3BSORvLA45yH9ryCPyh8HY2wM+OF51+LtHDyyctjla+R8TP1jVPFhxuNW+Ty45oAYqxKboH7HwyU5QCf0rwU/wE/wEP8FP8OPA/z8BBgB0NyAnicwKswAAAABJRU5ErkJggg==" /> +</g> + +</svg> diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/black_folder.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/black_folder.png Binary files differnew file mode 100644 index 00000000000..01ac4929616 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/black_folder.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/breadcrumb-separator.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/breadcrumb-separator.png Binary files differnew file mode 100644 index 00000000000..bbbfbffcf82 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/breadcrumb-separator.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_list_view.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_list_view.png Binary files differnew file mode 100644 index 00000000000..09aa2fd3a58 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_list_view.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_list_view_white.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_list_view_white.png Binary files differnew file mode 100644 index 00000000000..f89199a7e40 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_list_view_white.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_mosaic_view.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_mosaic_view.png Binary files differnew file mode 100644 index 00000000000..1961e46e121 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_mosaic_view.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_mosaic_view_white.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_mosaic_view_white.png Binary files differnew file mode 100644 index 00000000000..ad6f856c651 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_mosaic_view_white.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/close_bar.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/close_bar.png Binary files differnew file mode 100644 index 00000000000..3f9e4528983 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/close_bar.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/drive_logo.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/drive_logo.png Binary files differnew file mode 100644 index 00000000000..be900c88b2b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/drive_logo.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/eject.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/eject.png Binary files differnew file mode 100644 index 00000000000..f764c0c4c0f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/eject.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/hashed_bg.gif b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/hashed_bg.gif Binary files differnew file mode 100644 index 00000000000..03651e6ef82 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/hashed_bg.gif diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/icon_search.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/icon_search.png Binary files differnew file mode 100644 index 00000000000..7f2ff27ceef --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/icon_search.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/offline.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/offline.png Binary files differnew file mode 100644 index 00000000000..6fe2d25de02 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/offline.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/onbutton_trash.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/onbutton_trash.png Binary files differnew file mode 100644 index 00000000000..593f53fdba4 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/onbutton_trash.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed.png Binary files differnew file mode 100644 index 00000000000..92e9e784015 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed_hover.png Binary files differnew file mode 100644 index 00000000000..326f26f6ce2 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed_pressed.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed_pressed.png Binary files differnew file mode 100644 index 00000000000..98aca06f061 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed_pressed.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened.png Binary files differnew file mode 100644 index 00000000000..2e019075159 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened_hover.png Binary files differnew file mode 100644 index 00000000000..18481ce2989 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened_pressed.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened_pressed.png Binary files differnew file mode 100644 index 00000000000..6f4e637d152 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened_pressed.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear.png Binary files differnew file mode 100644 index 00000000000..a76574afb43 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear_hover.png Binary files differnew file mode 100644 index 00000000000..ed7155a8362 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear_pressed.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear_pressed.png Binary files differnew file mode 100644 index 00000000000..2237a085992 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear_pressed.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_icon_active.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_icon_active.png Binary files differnew file mode 100644 index 00000000000..d58cd100d67 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_icon_active.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_icon_inactive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_icon_inactive.png Binary files differnew file mode 100644 index 00000000000..553ebbb1a68 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_icon_inactive.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/select_checkbox.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/select_checkbox.png Binary files differnew file mode 100644 index 00000000000..e85e010f33b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/select_checkbox.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/select_checkbox_checked.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/select_checkbox_checked.png Binary files differnew file mode 100644 index 00000000000..e8792487a26 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/select_checkbox_checked.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/sort_asc.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/sort_asc.png Binary files differnew file mode 100644 index 00000000000..419b29ed735 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/sort_asc.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/sort_desc.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/sort_desc.png Binary files differnew file mode 100644 index 00000000000..e7e74c58769 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/sort_desc.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/vertical_separator.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/vertical_separator.png Binary files differnew file mode 100644 index 00000000000..9d14b688ebf --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/vertical_separator.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/warning_icon_square.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/warning_icon_square.png Binary files differnew file mode 100644 index 00000000000..52a8b5a4298 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/warning_icon_square.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/white_folder.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/white_folder.png Binary files differnew file mode 100644 index 00000000000..9124621632c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/white_folder.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/black_folder.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/black_folder.png Binary files differnew file mode 100644 index 00000000000..539f67759a5 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/black_folder.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/breadcrumb-separator.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/breadcrumb-separator.png Binary files differnew file mode 100644 index 00000000000..ed941270f61 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/breadcrumb-separator.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_list_view.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_list_view.png Binary files differnew file mode 100644 index 00000000000..c553781c5e1 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_list_view.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_list_view_white.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_list_view_white.png Binary files differnew file mode 100644 index 00000000000..798df51a9c7 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_list_view_white.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_mosaic_view.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_mosaic_view.png Binary files differnew file mode 100644 index 00000000000..5690834ac3b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_mosaic_view.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_mosaic_view_white.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_mosaic_view_white.png Binary files differnew file mode 100644 index 00000000000..9d4d518bfaa --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_mosaic_view_white.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/close_bar.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/close_bar.png Binary files differnew file mode 100644 index 00000000000..f7232012fc1 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/close_bar.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/drive_logo.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/drive_logo.png Binary files differnew file mode 100644 index 00000000000..b9ec5168cac --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/drive_logo.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/eject.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/eject.png Binary files differnew file mode 100644 index 00000000000..dbbdb7f6fcb --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/eject.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/hashed_bg.gif b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/hashed_bg.gif Binary files differnew file mode 100644 index 00000000000..1e5bcfa09f8 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/hashed_bg.gif diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/offline.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/offline.png Binary files differnew file mode 100644 index 00000000000..259d3b20e78 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/offline.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/onbutton_trash.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/onbutton_trash.png Binary files differnew file mode 100644 index 00000000000..20ab756dc43 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/onbutton_trash.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed.png Binary files differnew file mode 100644 index 00000000000..020e2e65f7c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed_hover.png Binary files differnew file mode 100644 index 00000000000..193a49b37c4 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed_pressed.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed_pressed.png Binary files differnew file mode 100644 index 00000000000..bfee4b01376 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed_pressed.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened.png Binary files differnew file mode 100644 index 00000000000..c03e33708fb --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened_hover.png Binary files differnew file mode 100644 index 00000000000..bf1aad74d5d --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened_pressed.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened_pressed.png Binary files differnew file mode 100644 index 00000000000..dbce527cd8b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened_pressed.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear.png Binary files differnew file mode 100644 index 00000000000..6755c4e8b17 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear_hover.png Binary files differnew file mode 100644 index 00000000000..bbfd15f4867 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear_pressed.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear_pressed.png Binary files differnew file mode 100644 index 00000000000..1825168c5c9 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear_pressed.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_icon_active.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_icon_active.png Binary files differnew file mode 100644 index 00000000000..f59dfe340a7 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_icon_active.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_icon_inactive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_icon_inactive.png Binary files differnew file mode 100644 index 00000000000..2f17e7c8c16 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_icon_inactive.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/select_checkbox.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/select_checkbox.png Binary files differnew file mode 100644 index 00000000000..4010b5c7212 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/select_checkbox.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/select_checkbox_checked.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/select_checkbox_checked.png Binary files differnew file mode 100644 index 00000000000..d63f1a44143 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/select_checkbox_checked.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/sort_asc.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/sort_asc.png Binary files differnew file mode 100644 index 00000000000..ed8c7d5b8af --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/sort_asc.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/sort_desc.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/sort_desc.png Binary files differnew file mode 100644 index 00000000000..f0b2af0a667 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/sort_desc.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/vertical_separator.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/vertical_separator.png Binary files differnew file mode 100644 index 00000000000..512f37f09d6 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/vertical_separator.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/view_thumbs_black.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/view_thumbs_black.png Binary files differnew file mode 100644 index 00000000000..54448aee6a0 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/view_thumbs_black.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/warning_icon_square.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/warning_icon_square.png Binary files differnew file mode 100644 index 00000000000..90d6b608fdb --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/warning_icon_square.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/white_folder.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/white_folder.png Binary files differnew file mode 100644 index 00000000000..07b6aa21f7f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/white_folder.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/arrow_left.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/arrow_left.png Binary files differnew file mode 100644 index 00000000000..6e4fb6659a7 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/arrow_left.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/arrow_right.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/arrow_right.png Binary files differnew file mode 100644 index 00000000000..22a6c739432 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/arrow_right.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/back_to_files.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/back_to_files.png Binary files differnew file mode 100644 index 00000000000..e04d9d317c2 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/back_to_files.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/bubble_point.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/bubble_point.png Binary files differnew file mode 100644 index 00000000000..161e4c06be4 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/bubble_point.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/butterbar_close_button.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/butterbar_close_button.png Binary files differnew file mode 100644 index 00000000000..aa6c816938d --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/butterbar_close_button.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_crop.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_crop.png Binary files differnew file mode 100644 index 00000000000..6202fa978a8 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_crop.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_leftright.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_leftright.png Binary files differnew file mode 100644 index 00000000000..a7ee09c632e --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_leftright.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_move.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_move.png Binary files differnew file mode 100644 index 00000000000..faa3c8ac398 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_move.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_nwse.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_nwse.png Binary files differnew file mode 100644 index 00000000000..0cd6399da7e --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_nwse.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_swne.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_swne.png Binary files differnew file mode 100644 index 00000000000..04d9dc02702 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_swne.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_updown.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_updown.png Binary files differnew file mode 100644 index 00000000000..1e9adfb5165 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_updown.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_1up.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_1up.png Binary files differnew file mode 100644 index 00000000000..58cbc28c4cb --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_1up.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_1up_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_1up_selected.png Binary files differnew file mode 100644 index 00000000000..a0ca726fa0d --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_1up_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_autofix.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_autofix.png Binary files differnew file mode 100644 index 00000000000..8ce4917d4d2 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_autofix.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_autofix_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_autofix_selected.png Binary files differnew file mode 100644 index 00000000000..8838d5ce5fd --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_autofix_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_brightness.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_brightness.png Binary files differnew file mode 100644 index 00000000000..ffcd3852947 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_brightness.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_brightness_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_brightness_selected.png Binary files differnew file mode 100644 index 00000000000..90bf03bbd82 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_brightness_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_contrast.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_contrast.png Binary files differnew file mode 100644 index 00000000000..eec931d56de --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_contrast.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_crop.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_crop.png Binary files differnew file mode 100644 index 00000000000..7c12fb54231 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_crop.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_crop_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_crop_selected.png Binary files differnew file mode 100644 index 00000000000..bb2e9e6cc0f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_crop_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_delete.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_delete.png Binary files differnew file mode 100644 index 00000000000..a55ac6c8086 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_delete.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_delete_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_delete_selected.png Binary files differnew file mode 100644 index 00000000000..af54168a2e6 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_delete_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_edit.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_edit.png Binary files differnew file mode 100644 index 00000000000..288bc5b7e47 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_edit.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_edit_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_edit_selected.png Binary files differnew file mode 100644 index 00000000000..bcf993396dc --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_edit_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_mosaic.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_mosaic.png Binary files differnew file mode 100644 index 00000000000..3e1a621b559 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_mosaic.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_mosaic_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_mosaic_selected.png Binary files differnew file mode 100644 index 00000000000..d9e329d9070 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_mosaic_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_print.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_print.png Binary files differnew file mode 100644 index 00000000000..b5a9be0c6ba --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_print.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_print_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_print_selected.png Binary files differnew file mode 100644 index 00000000000..048a34172ec --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_print_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_redo.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_redo.png Binary files differnew file mode 100644 index 00000000000..075275da898 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_redo.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_redo_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_redo_selected.png Binary files differnew file mode 100644 index 00000000000..beed584ffe3 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_redo_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate.png Binary files differnew file mode 100644 index 00000000000..db2c0b09f9f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_left.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_left.png Binary files differnew file mode 100644 index 00000000000..da57329d898 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_left.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_left_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_left_selected.png Binary files differnew file mode 100644 index 00000000000..d1b00a7e732 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_left_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_selected.png Binary files differnew file mode 100644 index 00000000000..b3a9bf60a28 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_share.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_share.png Binary files differnew file mode 100644 index 00000000000..b1da6d96d47 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_share.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_share_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_share_selected.png Binary files differnew file mode 100644 index 00000000000..b3cd00f1118 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_share_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_slideshow.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_slideshow.png Binary files differnew file mode 100644 index 00000000000..fec87c0ae9a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_slideshow.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_slideshow_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_slideshow_selected.png Binary files differnew file mode 100644 index 00000000000..4e1ed5aabfa --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_slideshow_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_undo.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_undo.png Binary files differnew file mode 100644 index 00000000000..c51fd6258ec --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_undo.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_undo_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_undo_selected.png Binary files differnew file mode 100644 index 00000000000..92d3a01d8e1 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_undo_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slider_thumb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slider_thumb.png Binary files differnew file mode 100644 index 00000000000..e100da62618 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slider_thumb.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-end.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-end.png Binary files differnew file mode 100644 index 00000000000..5e19f8d002b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-end.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-pause.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-pause.png Binary files differnew file mode 100644 index 00000000000..13457d999fa --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-pause.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-play.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-play.png Binary files differnew file mode 100644 index 00000000000..aa9bd88fc8d --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-play.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/arrow_left.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/arrow_left.png Binary files differnew file mode 100644 index 00000000000..85e687a1168 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/arrow_left.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/arrow_right.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/arrow_right.png Binary files differnew file mode 100644 index 00000000000..0361556bd20 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/arrow_right.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/back_to_files.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/back_to_files.png Binary files differnew file mode 100644 index 00000000000..96e420a657f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/back_to_files.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/bubble_point.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/bubble_point.png Binary files differnew file mode 100644 index 00000000000..a4a44e0cf09 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/bubble_point.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/butterbar_close_button.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/butterbar_close_button.png Binary files differnew file mode 100644 index 00000000000..3c65c230151 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/butterbar_close_button.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_crop.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_crop.png Binary files differnew file mode 100644 index 00000000000..6084188e0a7 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_crop.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_leftright.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_leftright.png Binary files differnew file mode 100644 index 00000000000..30eeb03accf --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_leftright.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_move.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_move.png Binary files differnew file mode 100644 index 00000000000..c5026d1b419 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_move.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_nwse.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_nwse.png Binary files differnew file mode 100644 index 00000000000..87fb564ba66 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_nwse.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_swne.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_swne.png Binary files differnew file mode 100644 index 00000000000..5e34475c863 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_swne.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_updown.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_updown.png Binary files differnew file mode 100644 index 00000000000..f3a422477c8 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_updown.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_1up.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_1up.png Binary files differnew file mode 100644 index 00000000000..546e87ae49f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_1up.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_1up_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_1up_selected.png Binary files differnew file mode 100644 index 00000000000..a3043a8a05a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_1up_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_autofix.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_autofix.png Binary files differnew file mode 100644 index 00000000000..0fb5b827d76 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_autofix.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_autofix_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_autofix_selected.png Binary files differnew file mode 100644 index 00000000000..fb5972d4d9d --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_autofix_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_brightness.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_brightness.png Binary files differnew file mode 100644 index 00000000000..ec9c114d245 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_brightness.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_brightness_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_brightness_selected.png Binary files differnew file mode 100644 index 00000000000..88ee72290aa --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_brightness_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_contrast.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_contrast.png Binary files differnew file mode 100644 index 00000000000..0188d483448 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_contrast.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_crop.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_crop.png Binary files differnew file mode 100644 index 00000000000..efff5ba58ff --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_crop.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_crop_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_crop_selected.png Binary files differnew file mode 100644 index 00000000000..18b8317cbac --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_crop_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_delete.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_delete.png Binary files differnew file mode 100644 index 00000000000..efb132aa722 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_delete.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_delete_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_delete_selected.png Binary files differnew file mode 100644 index 00000000000..f2f88d8d0f9 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_delete_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_edit.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_edit.png Binary files differnew file mode 100644 index 00000000000..fc72ecf3b60 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_edit.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_edit_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_edit_selected.png Binary files differnew file mode 100644 index 00000000000..61540b5b467 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_edit_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_mosaic.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_mosaic.png Binary files differnew file mode 100644 index 00000000000..6e49d3c929a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_mosaic.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_mosaic_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_mosaic_selected.png Binary files differnew file mode 100644 index 00000000000..86edb6e1ec0 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_mosaic_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_print.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_print.png Binary files differnew file mode 100644 index 00000000000..b2355367c25 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_print.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_print_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_print_selected.png Binary files differnew file mode 100644 index 00000000000..657b9c8e290 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_print_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_redo.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_redo.png Binary files differnew file mode 100644 index 00000000000..7b4703b6188 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_redo.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_redo_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_redo_selected.png Binary files differnew file mode 100644 index 00000000000..0022a136d09 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_redo_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate.png Binary files differnew file mode 100644 index 00000000000..c60f258f6be --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_left.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_left.png Binary files differnew file mode 100644 index 00000000000..ef2f21f4356 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_left.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_left_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_left_selected.png Binary files differnew file mode 100644 index 00000000000..1e4c1d6d62f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_left_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_selected.png Binary files differnew file mode 100644 index 00000000000..445350fcaf6 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_share.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_share.png Binary files differnew file mode 100644 index 00000000000..36bb2218a82 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_share.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_share_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_share_selected.png Binary files differnew file mode 100644 index 00000000000..438e8a2599a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_share_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_slideshow.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_slideshow.png Binary files differnew file mode 100644 index 00000000000..72763d44ed1 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_slideshow.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_slideshow_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_slideshow_selected.png Binary files differnew file mode 100644 index 00000000000..4f80a48d403 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_slideshow_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_undo.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_undo.png Binary files differnew file mode 100644 index 00000000000..79e3fddf26b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_undo.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_undo_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_undo_selected.png Binary files differnew file mode 100644 index 00000000000..d5d13a71a9f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_undo_selected.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slider_thumb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slider_thumb.png Binary files differnew file mode 100644 index 00000000000..cb2d712068f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slider_thumb.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-end.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-end.png Binary files differnew file mode 100644 index 00000000000..f4371115363 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-end.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-pause.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-pause.png Binary files differnew file mode 100644 index 00000000000..2170ce90cfc --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-pause.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-play.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-play.png Binary files differnew file mode 100644 index 00000000000..f949121d245 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-play.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/audio_player.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/audio_player.png Binary files differnew file mode 100644 index 00000000000..ce5fa1ab7a9 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/audio_player.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/drive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/drive.png Binary files differnew file mode 100644 index 00000000000..b2cef7191e7 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/drive.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/error.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/error.png Binary files differnew file mode 100644 index 00000000000..200baf571a3 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/error.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_close.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_close.png Binary files differnew file mode 100644 index 00000000000..2e19603f5cd --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_close.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_collapse.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_collapse.png Binary files differnew file mode 100644 index 00000000000..a9400612019 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_collapse.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_expand.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_expand.png Binary files differnew file mode 100644 index 00000000000..20c234003fd --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_expand.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen.png Binary files differnew file mode 100644 index 00000000000..090c8257ae6 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_disabled.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_disabled.png Binary files differnew file mode 100644 index 00000000000..96b3f635a83 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_disabled.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_down.png Binary files differnew file mode 100644 index 00000000000..14cbeecaa87 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_hover.png Binary files differnew file mode 100644 index 00000000000..25daa4fb6e6 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop.png Binary files differnew file mode 100644 index 00000000000..6bea029799a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop_down.png Binary files differnew file mode 100644 index 00000000000..0e4b6e26d00 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop_hover.png Binary files differnew file mode 100644 index 00000000000..b3a6c144a3e --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next.png Binary files differnew file mode 100644 index 00000000000..012af925e4a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next_down.png Binary files differnew file mode 100644 index 00000000000..58d5bf724e5 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next_hover.png Binary files differnew file mode 100644 index 00000000000..d8d199575dc --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause.png Binary files differnew file mode 100644 index 00000000000..acebff9d4a0 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio.png Binary files differnew file mode 100644 index 00000000000..7489a7ed43a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio_down.png Binary files differnew file mode 100644 index 00000000000..f9242360d7d --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio_hover.png Binary files differnew file mode 100644 index 00000000000..160d3d11145 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_down.png Binary files differnew file mode 100644 index 00000000000..9caaf7a0e19 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_hover.png Binary files differnew file mode 100644 index 00000000000..beb86f2e89a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play.png Binary files differnew file mode 100644 index 00000000000..28e8a25c032 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio.png Binary files differnew file mode 100644 index 00000000000..16f663260c9 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio_down.png Binary files differnew file mode 100644 index 00000000000..01dcc462306 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio_hover.png Binary files differnew file mode 100644 index 00000000000..c64070f4a24 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_disabled.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_disabled.png Binary files differnew file mode 100644 index 00000000000..77ea7c01091 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_disabled.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_down.png Binary files differnew file mode 100644 index 00000000000..7c68787119f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_hover.png Binary files differnew file mode 100644 index 00000000000..28ca18b0551 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous.png Binary files differnew file mode 100644 index 00000000000..399e5a00e43 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous_down.png Binary files differnew file mode 100644 index 00000000000..94741583314 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous_hover.png Binary files differnew file mode 100644 index 00000000000..0a8a919c330 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb.png Binary files differnew file mode 100644 index 00000000000..cd20aa2967f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb_down.png Binary files differnew file mode 100644 index 00000000000..931850f3f52 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb_hover.png Binary files differnew file mode 100644 index 00000000000..9fe30ef7442 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled.png Binary files differnew file mode 100644 index 00000000000..acbaa71be9b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled_down.png Binary files differnew file mode 100644 index 00000000000..f594f47e774 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled_hover.png Binary files differnew file mode 100644 index 00000000000..f4cc7b350f5 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full.png Binary files differnew file mode 100644 index 00000000000..a093e9f6cd0 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_disabled.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_disabled.png Binary files differnew file mode 100644 index 00000000000..56635f1d9b7 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_disabled.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_down.png Binary files differnew file mode 100644 index 00000000000..7d2a2395049 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_hover.png Binary files differnew file mode 100644 index 00000000000..f183edd7362 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1.png Binary files differnew file mode 100644 index 00000000000..25904c0d176 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1_down.png Binary files differnew file mode 100644 index 00000000000..0ba2886fbfb --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1_hover.png Binary files differnew file mode 100644 index 00000000000..be56fc17e6a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2.png Binary files differnew file mode 100644 index 00000000000..24f7ea805c1 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2_down.png Binary files differnew file mode 100644 index 00000000000..34fce547aa9 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2_hover.png Binary files differnew file mode 100644 index 00000000000..2e3f2e80cd5 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb.png Binary files differnew file mode 100644 index 00000000000..d5cf0c56e52 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb_down.png Binary files differnew file mode 100644 index 00000000000..034c7b807c3 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb_hover.png Binary files differnew file mode 100644 index 00000000000..4d4243d0f0c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/watch.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/watch.png Binary files differnew file mode 100644 index 00000000000..f47751be542 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/watch.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/audio_player.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/audio_player.png Binary files differnew file mode 100644 index 00000000000..6921db2f33d --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/audio_player.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/drive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/drive.png Binary files differnew file mode 100644 index 00000000000..8f4b64ee8bd --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/drive.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/error.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/error.png Binary files differnew file mode 100644 index 00000000000..125c0b75899 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/error.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_close.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_close.png Binary files differnew file mode 100644 index 00000000000..5980ea7504a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_close.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_collapse.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_collapse.png Binary files differnew file mode 100644 index 00000000000..3ab1add2a9c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_collapse.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_expand.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_expand.png Binary files differnew file mode 100644 index 00000000000..a228fbdf275 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_expand.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen.png Binary files differnew file mode 100644 index 00000000000..24f2ecf2f1d --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_disabled.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_disabled.png Binary files differnew file mode 100644 index 00000000000..bf74390888c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_disabled.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_down.png Binary files differnew file mode 100644 index 00000000000..dd318cec7d6 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_hover.png Binary files differnew file mode 100644 index 00000000000..42327397887 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop.png Binary files differnew file mode 100644 index 00000000000..f3e35dd9d4a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop_down.png Binary files differnew file mode 100644 index 00000000000..dda8af53979 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop_hover.png Binary files differnew file mode 100644 index 00000000000..a0ed21fccb8 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next.png Binary files differnew file mode 100644 index 00000000000..aec4ee676a8 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next_down.png Binary files differnew file mode 100644 index 00000000000..53cc1062da0 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next_hover.png Binary files differnew file mode 100644 index 00000000000..bea50b59347 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause.png Binary files differnew file mode 100644 index 00000000000..0a304e4bd04 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio.png Binary files differnew file mode 100644 index 00000000000..8e2e87be5c4 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio_down.png Binary files differnew file mode 100644 index 00000000000..11789840f5b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio_hover.png Binary files differnew file mode 100644 index 00000000000..52b6bbcbf37 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_down.png Binary files differnew file mode 100644 index 00000000000..6e65195cbea --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_hover.png Binary files differnew file mode 100644 index 00000000000..993ee50a4f3 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play.png Binary files differnew file mode 100644 index 00000000000..47bcdc29b17 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio.png Binary files differnew file mode 100644 index 00000000000..eceaa5bb82b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio_down.png Binary files differnew file mode 100644 index 00000000000..98ebeeaa120 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio_hover.png Binary files differnew file mode 100644 index 00000000000..f9d97d7714a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_disabled.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_disabled.png Binary files differnew file mode 100644 index 00000000000..6e96d4c9c67 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_disabled.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_down.png Binary files differnew file mode 100644 index 00000000000..1759ec39afa --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_hover.png Binary files differnew file mode 100644 index 00000000000..3942d462656 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous.png Binary files differnew file mode 100644 index 00000000000..4fded35ad47 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous_down.png Binary files differnew file mode 100644 index 00000000000..2c63184167e --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous_hover.png Binary files differnew file mode 100644 index 00000000000..45ea61b3b59 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb.png Binary files differnew file mode 100644 index 00000000000..e55b2c25128 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb_down.png Binary files differnew file mode 100644 index 00000000000..f0b2be7d653 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb_hover.png Binary files differnew file mode 100644 index 00000000000..e216ae6ffbd --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled.png Binary files differnew file mode 100644 index 00000000000..42126de9330 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled_down.png Binary files differnew file mode 100644 index 00000000000..2b494b93652 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled_hover.png Binary files differnew file mode 100644 index 00000000000..5040f8078d3 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full.png Binary files differnew file mode 100644 index 00000000000..4a034029d5c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_disabled.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_disabled.png Binary files differnew file mode 100644 index 00000000000..cef4bc4c6d0 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_disabled.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_down.png Binary files differnew file mode 100644 index 00000000000..55d77e4d496 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_hover.png Binary files differnew file mode 100644 index 00000000000..881e84305fb --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1.png Binary files differnew file mode 100644 index 00000000000..2f7ceea32e4 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1_down.png Binary files differnew file mode 100644 index 00000000000..9777c9b5a0b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1_hover.png Binary files differnew file mode 100644 index 00000000000..fdf3bc1331b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2.png Binary files differnew file mode 100644 index 00000000000..9379a038ba9 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2_down.png Binary files differnew file mode 100644 index 00000000000..422b43518be --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2_hover.png Binary files differnew file mode 100644 index 00000000000..8bf6157edf4 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb.png Binary files differnew file mode 100644 index 00000000000..a1bf0577905 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb_down.png Binary files differnew file mode 100644 index 00000000000..a4560f0ffc2 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb_down.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb_hover.png Binary files differnew file mode 100644 index 00000000000..159e2e71f3e --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb_hover.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/watch.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/watch.png Binary files differnew file mode 100644 index 00000000000..92b628497fb --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/watch.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/archive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/archive.png Binary files differnew file mode 100644 index 00000000000..b5821d4da5b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/archive.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_archive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_archive.png Binary files differnew file mode 100644 index 00000000000..6927bdd188e --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_archive.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_downloads.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_downloads.png Binary files differnew file mode 100644 index 00000000000..ab37f07104c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_downloads.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_drive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_drive.png Binary files differnew file mode 100644 index 00000000000..a17a0b19330 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_drive.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_hdd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_hdd.png Binary files differnew file mode 100644 index 00000000000..e0ec6965c2a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_hdd.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_offline.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_offline.png Binary files differnew file mode 100644 index 00000000000..87029a9355c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_offline.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_optical.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_optical.png Binary files differnew file mode 100644 index 00000000000..5077474e3b1 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_optical.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_phone.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_phone.png Binary files differnew file mode 100644 index 00000000000..c4521dec634 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_phone.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_recent.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_recent.png Binary files differnew file mode 100644 index 00000000000..e1b86212136 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_recent.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_sd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_sd.png Binary files differnew file mode 100644 index 00000000000..5679089adcf --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_sd.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_shared.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_shared.png Binary files differnew file mode 100644 index 00000000000..ec7c3e4779a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_shared.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_trash.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_trash.png Binary files differnew file mode 100644 index 00000000000..67f261f460a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_trash.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_usb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_usb.png Binary files differnew file mode 100644 index 00000000000..2cceca66e9c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_usb.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_hd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_hd.png Binary files differnew file mode 100644 index 00000000000..e0ec6965c2a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_hd.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_optical.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_optical.png Binary files differnew file mode 100644 index 00000000000..5077474e3b1 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_optical.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_sd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_sd.png Binary files differnew file mode 100644 index 00000000000..5679089adcf --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_sd.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_sd_large.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_sd_large.png Binary files differnew file mode 100644 index 00000000000..6fa0c756dce --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_sd_large.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_usb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_usb.png Binary files differnew file mode 100644 index 00000000000..70e9686f3a2 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_usb.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_usb_large.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_usb_large.png Binary files differnew file mode 100644 index 00000000000..17a8fee01f7 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_usb_large.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/downloads.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/downloads.png Binary files differnew file mode 100644 index 00000000000..22e66179190 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/downloads.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive.png Binary files differnew file mode 100644 index 00000000000..a17a0b19330 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_offline.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_offline.png Binary files differnew file mode 100644 index 00000000000..75981854cd6 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_offline.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_recent.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_recent.png Binary files differnew file mode 100644 index 00000000000..756e36a2e38 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_recent.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_shared.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_shared.png Binary files differnew file mode 100644 index 00000000000..c2eb5dafb30 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_shared.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/folder.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/folder.png Binary files differnew file mode 100644 index 00000000000..01ac4929616 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/folder.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_archive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_archive.png Binary files differnew file mode 100644 index 00000000000..481c40b89f1 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_archive.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_downloads.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_downloads.png Binary files differnew file mode 100644 index 00000000000..5262bb910a8 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_downloads.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_drive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_drive.png Binary files differnew file mode 100644 index 00000000000..5f43989d923 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_drive.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_hdd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_hdd.png Binary files differnew file mode 100644 index 00000000000..bc5e8c417d9 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_hdd.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_offline.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_offline.png Binary files differnew file mode 100644 index 00000000000..9d65b392094 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_offline.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_optical.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_optical.png Binary files differnew file mode 100644 index 00000000000..c67537714dd --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_optical.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_phone.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_phone.png Binary files differnew file mode 100644 index 00000000000..2dfb8f27d61 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_phone.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_recent.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_recent.png Binary files differnew file mode 100644 index 00000000000..a80eae2c8b3 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_recent.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_sd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_sd.png Binary files differnew file mode 100644 index 00000000000..282de9aa9af --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_sd.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_shared.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_shared.png Binary files differnew file mode 100644 index 00000000000..24ea1676b15 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_shared.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_trash.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_trash.png Binary files differnew file mode 100644 index 00000000000..f6c5ccb03f4 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_trash.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_usb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_usb.png Binary files differnew file mode 100644 index 00000000000..f296f4e0245 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_usb.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_archive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_archive.png Binary files differnew file mode 100644 index 00000000000..e4514206fbd --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_archive.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_downloads.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_downloads.png Binary files differnew file mode 100644 index 00000000000..e25c8756b32 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_downloads.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_drive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_drive.png Binary files differnew file mode 100644 index 00000000000..891ec480c47 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_drive.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_folder.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_folder.png Binary files differnew file mode 100644 index 00000000000..539f67759a5 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_folder.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_hdd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_hdd.png Binary files differnew file mode 100644 index 00000000000..53c02b4c48c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_hdd.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_offline.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_offline.png Binary files differnew file mode 100644 index 00000000000..a07ef0cb483 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_offline.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_optical.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_optical.png Binary files differnew file mode 100644 index 00000000000..30a905fbc9c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_optical.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_phone.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_phone.png Binary files differnew file mode 100644 index 00000000000..aa3aa9deb7c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_phone.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_recent.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_recent.png Binary files differnew file mode 100644 index 00000000000..6e5d3636bff --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_recent.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_sd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_sd.png Binary files differnew file mode 100644 index 00000000000..f24b4cefb74 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_sd.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_shared.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_shared.png Binary files differnew file mode 100644 index 00000000000..10a1553b9b0 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_shared.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_trash.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_trash.png Binary files differnew file mode 100644 index 00000000000..7708ae555a6 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_trash.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_usb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_usb.png Binary files differnew file mode 100644 index 00000000000..ba7ef472471 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_usb.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/device_sd_large.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/device_sd_large.png Binary files differnew file mode 100644 index 00000000000..39d2fe90a49 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/device_sd_large.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/device_usb_large.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/device_usb_large.png Binary files differnew file mode 100644 index 00000000000..6b68508f7d8 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/device_usb_large.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_archive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_archive.png Binary files differnew file mode 100644 index 00000000000..3b542deac45 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_archive.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_downloads.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_downloads.png Binary files differnew file mode 100644 index 00000000000..8b1cfdf7400 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_downloads.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_drive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_drive.png Binary files differnew file mode 100644 index 00000000000..0226fbd14ed --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_drive.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_folder.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_folder.png Binary files differnew file mode 100644 index 00000000000..07b6aa21f7f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_folder.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_hdd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_hdd.png Binary files differnew file mode 100644 index 00000000000..b7c4a485c94 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_hdd.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_offline.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_offline.png Binary files differnew file mode 100644 index 00000000000..503ed67f82f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_offline.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_optical.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_optical.png Binary files differnew file mode 100644 index 00000000000..3f13f723af4 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_optical.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_phone.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_phone.png Binary files differnew file mode 100644 index 00000000000..d187ef0951b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_phone.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_recent.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_recent.png Binary files differnew file mode 100644 index 00000000000..23c526ad8b2 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_recent.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_sd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_sd.png Binary files differnew file mode 100644 index 00000000000..f7f465c430b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_sd.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_shared.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_shared.png Binary files differnew file mode 100644 index 00000000000..051191e6de1 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_shared.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_trash.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_trash.png Binary files differnew file mode 100644 index 00000000000..f95d0215ee4 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_trash.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_usb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_usb.png Binary files differnew file mode 100644 index 00000000000..954a3c6dae3 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_usb.png diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/app_installer.js b/chromium/chrome/browser/resources/file_manager/foreground/js/app_installer.js new file mode 100644 index 00000000000..7914d6e458f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/app_installer.js @@ -0,0 +1,79 @@ +// Copyright 2013 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'; + +/** + * Manage the installation of apps. + * + * @param {string} itemId Item id to be installed. + * @constructor + * @extends {cr.EventType} + */ +function AppInstaller(itemId) { + this.itemId_ = itemId; + this.callback_ = null; + + Object.seal(this); +} + +AppInstaller.prototype = { +}; + +/** + * Type of result. + * + * @enum {string} + * @const + */ +AppInstaller.Result = { + SUCCESS: 'AppInstaller.success', + CANCELLED: 'AppInstaller.cancelled', + ERROR: 'AppInstaller.error' +}; +Object.freeze(AppInstaller.Result); + +/** + * Error message for user cancellation. This must be match with the constant + * 'kUserCancelledError' in C/B/extensions/webstore_standalone_installer.cc. + * @type {string} + * @const + * @private + */ +AppInstaller.USER_CANCELLED_ERROR_STR_ = 'User cancelled install'; + +/** + * Start an installation. + * @param {function(boolean, string)} callback Called when the installation is + * finished. + */ +AppInstaller.prototype.install = function(callback) { + this.callback_ = callback; + chrome.fileBrowserPrivate.installWebstoreItem( + this.itemId_, + function() { + this.onInstallCompleted_(chrome.runtime.lastError); + }.bind(this)); +}; + +/** + * Called when the installation is completed. + * + * @param {{message: string}?} error Null if the installation is success, + * otherwise an object which contains error message. + * @private + */ +AppInstaller.prototype.onInstallCompleted_ = function(error) { + var installerResult = AppInstaller.Result.SUCCESS; + var errorMessage = ''; + if (error) { + installerResult = + error.message == AppInstaller.USER_CANCELLED_ERROR_STR_ ? + AppInstaller.Result.CANCELLED : + AppInstaller.Result.ERROR; + errorMessage = error.message; + } + this.callback_(installerResult, errorMessage); + this.callback_ = null; +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/combobutton.js b/chromium/chrome/browser/resources/file_manager/foreground/js/combobutton.js new file mode 100644 index 00000000000..59ae9f2b6ed --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/combobutton.js @@ -0,0 +1,154 @@ +// 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'; + +/** + * @fileoverview This implements a combobutton control. + */ + +cr.define('cr.ui', function() { + /** + * Creates a new combobutton element. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {HTMLUListElement} + */ + var ComboButton = cr.ui.define(cr.ui.MenuButton); + + + ComboButton.prototype = { + __proto__: cr.ui.MenuButton.prototype, + + defaultItem_: null, + + /** + * Truncates drop-down list. + */ + clear: function() { + this.menu.clear(); + this.multiple = false; + }, + + addDropDownItem: function(item) { + this.multiple = true; + var menuitem = this.menu.addMenuItem(item); + menuitem.data = item; + if (item.iconType) { + menuitem.style.backgroundImage = ''; + menuitem.setAttribute('file-type-icon', item.iconType); + } + if (item.bold) { + menuitem.style.fontWeight = 'bold'; + } + return menuitem; + }, + + /** + * Adds separator to drop-down list. + */ + addSeparator: function() { + this.menu.addSeparator(); + }, + + /** + * Default item to fire on combobox click + */ + get defaultItem() { + return this.defaultItem_; + }, + set defaultItem(defaultItem) { + this.defaultItem_ = defaultItem; + + this.actionNode_.textContent = defaultItem.label || ''; + + if (defaultItem.iconType) { + this.actionNode_.style.backgroundImage = ''; + this.actionNode_.setAttribute('file-type-icon', defaultItem.iconType); + } else if (defaultItem.iconUrl) { + this.actionNode_.style.backgroundImage = + 'url(' + defaultItem.iconUrl + ')'; + } else { + this.actionNode_.style.backgroundImage = ''; + } + }, + + /** + * Initializes the element. + */ + decorate: function() { + cr.ui.MenuButton.prototype.decorate.call(this); + + this.classList.add('combobutton'); + + this.actionNode_ = this.ownerDocument.createElement('div'); + this.actionNode_.classList.add('action'); + this.appendChild(this.actionNode_); + + var triggerIcon = this.ownerDocument.createElement('span'); + triggerIcon.className = 'disclosureindicator'; + this.trigger_ = this.ownerDocument.createElement('div'); + this.trigger_.classList.add('trigger'); + this.trigger_.appendChild(triggerIcon); + + this.appendChild(this.trigger_); + + this.addEventListener('click', this.handleButtonClick_.bind(this)); + + this.trigger_.addEventListener('click', + this.handleTriggerClicked_.bind(this)); + + this.menu.addEventListener('activate', + this.handleMenuActivate_.bind(this)); + + // Remove mousedown event listener created by MenuButton::decorate, + // and move it down to trigger_. + this.removeEventListener('mousedown', this); + this.trigger_.addEventListener('mousedown', this); + }, + + /** + * Handles the keydown event for the menu button. + */ + handleKeyDown: function(e) { + switch (e.keyIdentifier) { + case 'Down': + case 'Up': + if (!this.isMenuShown()) + this.showMenu(); + e.preventDefault(); + break; + case 'Esc': + case 'U+001B': // Maybe this is remote desktop playing a prank? + this.hideMenu(); + break; + } + }, + + handleTriggerClicked_: function(event) { + event.stopPropagation(); + }, + + handleMenuActivate_: function(event) { + this.dispatchSelectEvent(event.target.data); + }, + + handleButtonClick_: function() { + this.dispatchSelectEvent(this.defaultItem_); + }, + + dispatchSelectEvent: function(item) { + var selectEvent = new Event('select'); + selectEvent.item = item; + this.dispatchEvent(selectEvent); + } + }; + + cr.defineProperty(ComboButton, 'disabled', cr.PropertyKind.BOOL_ATTR); + cr.defineProperty(ComboButton, 'multiple', cr.PropertyKind.BOOL_ATTR); + + return { + ComboButton: ComboButton + }; +}); diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/commandbutton.js b/chromium/chrome/browser/resources/file_manager/foreground/js/commandbutton.js new file mode 100644 index 00000000000..b6f2b916cd3 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/commandbutton.js @@ -0,0 +1,136 @@ +// 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'; + +/** + * @fileoverview This implements a common button control, bound to command. + */ + +/** + * Creates a new button element. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {HTMLDivElement} + */ + var CommandButton = cr.ui.define('button'); + +/** @override */ +CommandButton.prototype.__proto__ = HTMLButtonElement.prototype; + +/** + * Associated command. + * @type {Command} + * @private + */ +CommandButton.prototype.command_ = null; + +/** + * Initializes the menu item. + */ +CommandButton.prototype.decorate = function() { + var commandId; + if ((commandId = this.getAttribute('command'))) + this.setCommand(commandId); + + this.addEventListener('click', this.handleClick_.bind(this)); +}; + +/** + * Returns associated command. + * @return {cr.ui.Command} associated command. + */ +CommandButton.prototype.getCommand = function() { + return this.command_; +}; + +/** + * Associates command with this button. + * @param {string|cr.ui.Command} command Command id, or command object to + * associate with this button. + */ +CommandButton.prototype.setCommand = function(command) { + if (this.command_) { + this.command_.removeEventListener('labelChange', this); + this.command_.removeEventListener('disabledChange', this); + this.command_.removeEventListener('hiddenChange', this); + } + + if (typeof command == 'string' && command[0] == '#') { + command = this.ownerDocument.getElementById(command.slice(1)); + cr.ui.decorate(command, cr.ui.Command); + } + + this.command_ = command; + if (command) { + if (command.id) + this.setAttribute('command', '#' + command.id); + + this.setLabel(command.label); + this.disabled = command.disabled; + this.hidden = command.hidden; + + this.command_.addEventListener('labelChange', this); + this.command_.addEventListener('disabledChange', this); + this.command_.addEventListener('hiddenChange', this); + } +}; + +/** + * Returns button label + * @return {string} Button label. + */ +CommandButton.prototype.getLabel = function() { + return this.textContent; +}; + +/** + * Sets button label. + * @param {string} label New button label. + */ +CommandButton.prototype.setLabel = function(label) { + this.textContent = label; +}; + +/** + * Handles click event and dispatches associated command. + * @param {Event} e The mouseup event object. + * @private + */ +CommandButton.prototype.handleClick_ = function(e) { + if (!this.disabled && this.command_) + this.command_.execute(this); +}; + +/** + * Handles changes to the associated command. + * @param {Event} e The event object. + */ +CommandButton.prototype.handleEvent = function(e) { + switch (e.type) { + case 'disabledChange': + this.disabled = this.command_.disabled; + break; + case 'hiddenChange': + this.hidden = this.command_.hidden; + break; + case 'labelChange': + this.setLabel(this.command_.label); + break; + } +}; + +/** + * Whether the button is disabled or not. + * @type {boolean} + */ +cr.defineProperty(CommandButton, 'disabled', + cr.PropertyKind.BOOL_ATTR); + +/** + * Whether the button is hidden or not. + * @type {boolean} + */ +cr.defineProperty(CommandButton, 'hidden', + cr.PropertyKind.BOOL_ATTR); diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/cws_container_client.js b/chromium/chrome/browser/resources/file_manager/foreground/js/cws_container_client.js new file mode 100644 index 00000000000..fae6787a5ad --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/cws_container_client.js @@ -0,0 +1,249 @@ +// Copyright 2013 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'; + +/** + * @param {WebView} webView Web View tag. + * @param {?string} ext File extension. + * @param {?string} mime File mime type. + * @param {?string} searchQuery Search query. + * @param {number} width Width of the CWS widget. + * @param {number} height Height of the CWS widget. + * @param {string} url Share Url for an entry. + * @param {string} target Target (scheme + host + port) of the widget. + * @constructor + */ +function CWSContainerClient( + webView, ext, mime, searchQuery, width, height, url, target) { + this.webView_ = webView; + this.ext_ = (ext && ext[0] == '.') ? ext.substr(1) : ext; + this.mime_ = mime; + this.searchQuery_ = searchQuery; + this.width_ = width; + this.height_ = height; + this.url_ = url; + this.target_ = target; + + this.loaded_ = false; + this.loading_ = false; + + this.onMessageBound_ = this.onMessage_.bind(this); + this.onLoadStopBound_ = this.onLoadStop_.bind(this); + this.onLoadAbortBound_ = this.onLoadAbort_.bind(this); +} + +CWSContainerClient.prototype = { + __proto__: cr.EventTarget.prototype +}; + +/** + * Events CWSContainerClient fires + * + * @enum {string} + * @const + */ +CWSContainerClient.Events = { + LOADED: 'CWSContainerClient.Events.LOADED', + LOAD_FAILED: 'CWSContainerClient.Events.LOAD_FAILED', + REQUEST_INSTALL: 'CWSContainerClient.Events.REQUEST_INSTALL' +}; +Object.freeze(CWSContainerClient.Events); + +/** + * Handles messages from the widget + * @param {Event} event Message event. + * @private + */ +CWSContainerClient.prototype.onMessage_ = function(event) { + if (event.origin != this.target_) + return; + + var data = event.data; + switch (data['message']) { + case 'widget_loaded': + this.onWidgetLoaded_(); + break; + case 'widget_load_failed': + this.onWidgetLoadFailed_(); + break; + case 'before_install': + this.sendInstallRequest_(data['item_id']); + break; + default: + console.error('Unexpected message: ' + data['message'], data); + } +}; + +/** + * Called when receiving 'loadstop' event from the <wevview>. + * @param {Event} event Message event. + * @private + */ +CWSContainerClient.prototype.onLoadStop_ = function(event) { + if (this.url_ == this.webView_.src && !this.loaded_) { + this.loaded_ = true; + this.postInitializeMessage_(); + } +}; + +/** + * Called when the widget is loaded successfully. + * @private + */ +CWSContainerClient.prototype.onWidgetLoaded_ = function() { + cr.dispatchSimpleEvent(this, CWSContainerClient.Events.LOADED); +}; + +/** + * Called when the widget is failed to load. + * @private + */ +CWSContainerClient.prototype.onWidgetLoadFailed_ = function() { + this.sendWidgetLoadFailed_(); +}; + +/** + * Called when receiving the 'loadabort' event from <webview>. + * @param {Event} event Message event. + * @private + */ +CWSContainerClient.prototype.onLoadAbort_ = function(event) { + this.sendWidgetLoadFailed_(); +}; + +/** + * Called when the installation is completed from the suggest-app dialog. + * + * @param {boolean} result True if the installation is success, false if failed. + * @param {string} itemId Item id to be installed. + */ +CWSContainerClient.prototype.onInstallCompleted = function(result, itemId) { + if (result) + this.postInstallSuccessMessage_(itemId); + else + this.postInstallFailureMessage_(itemId); +}; + +/** + * Send the fail message to the suggest-app dialog. + * @private + */ +CWSContainerClient.prototype.sendWidgetLoadFailed_ = function() { + cr.dispatchSimpleEvent(this, CWSContainerClient.Events.LOAD_FAILED); +}; + +/** + * Send the install request to the suggest-app dialog. + * + * @param {string} itemId Item id to be installed. + * @private + */ +CWSContainerClient.prototype.sendInstallRequest_ = function(itemId) { + var event = new Event(CWSContainerClient.Events.REQUEST_INSTALL); + event.itemId = itemId; + this.dispatchEvent(event); +}; + +/** + * Send the 'install_failure' message to the widget. + * + * @param {string} itemId Item id to be installed. + * @private + */ +CWSContainerClient.prototype.postInstallFailureMessage_ = function(itemId) { + var message = { + message: 'install_failure', + item_id: itemId, + v: 1 + }; + + this.postMessage_(message); +}; + +/** + * Send the 'install_success' message to the widget. + * + * @param {string} itemId Item id to be installed. + * @private + */ +CWSContainerClient.prototype.postInstallSuccessMessage_ = function(itemId) { + var message = { + message: 'install_success', + item_id: itemId, + v: 1 + }; + + this.postMessage_(message); +}; + +/** + * Send the 'initialize' message to the widget. + * @private + */ +CWSContainerClient.prototype.postInitializeMessage_ = function() { + var message = { + message: 'initialize', + hl: util.getCurrentLocaleOrDefault(), + widgth: this.width_, + height: this.height_, + v: 1 + }; + + if (this.searchQuery_) { + message['search_query'] = this.searchQuery_; + } else { + message['file_extension'] = this.ext_; + message['mime_type'] = this.mime_; + } + + this.postMessage_(message); +}; + +/** + * Send a message to the widget. This method shouldn't be called directly, + * should from more specified posting function (eg. postXyzMessage_()). + * + * @param {object} message Message object to be posted. + * @private + */ +CWSContainerClient.prototype.postMessage_ = function(message) { + if (!this.webView_.contentWindow) + return; + + this.webView_.contentWindow.postMessage(message, this.target_); +}; + +/** + * Loads the page to <webview>. Can be called only once. + */ +CWSContainerClient.prototype.load = function() { + if (this.loading_ || this.loaded_) + throw new Error('Already loaded.'); + this.loading_ = true; + this.loaded_ = false; + + window.addEventListener('message', this.onMessageBound_); + this.webView_.addEventListener('loadstop', this.onLoadStopBound_); + this.webView_.addEventListener('loadabort', this.onLoadAbortBound_); + this.webView_.setAttribute('src', this.url_); +}; + +/** + * Aborts loading of the embedded dialog and performs cleanup. + */ +CWSContainerClient.prototype.abort = function() { + window.removeEventListener('message', this.onMessageBound_); + this.webView_.removeEventListener('loadstop', this.onLoadStopBound_); + this.webView_.removeEventListener( + 'loadabort', this.onLoadAbortBound_); + this.webView_.stop(); +}; + +/** + * Cleans the dialog by removing all handlers. + */ +CWSContainerClient.prototype.dispose = function() { + this.abort(); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/default_action_dialog.js b/chromium/chrome/browser/resources/file_manager/foreground/js/default_action_dialog.js new file mode 100644 index 00000000000..a5c8290a239 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/default_action_dialog.js @@ -0,0 +1,157 @@ +// 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'; + + +/** + * DefaultActionDialog contains a message, a list box, an ok button, and a + * cancel button. + * This dialog should be used as action picker for file operations. + */ +cr.define('cr.filebrowser', function() { + + /** + * Creates dialog in DOM tree. + * + * @param {HTMLElement} parentNode Node to be parent for this dialog. + * @constructor + * @extends {FileManagerDialogBase} + */ + function DefaultActionDialog(parentNode) { + FileManagerDialogBase.call(this, parentNode); + + this.frame_.id = 'default-action-dialog'; + + this.list_ = new cr.ui.List(); + this.list_.id = 'default-actions-list'; + this.frame_.insertBefore(this.list_, this.text_.nextSibling); + + this.selectionModel_ = this.list_.selectionModel = + new cr.ui.ListSingleSelectionModel(); + this.dataModel_ = this.list_.dataModel = new cr.ui.ArrayDataModel([]); + + // List has max-height defined at css, so that list grows automatically, + // but doesn't exceed predefined size. + this.list_.autoExpands = true; + this.list_.activateItemAtIndex = this.activateItemAtIndex_.bind(this); + + this.initialFocusElement_ = this.list_; + + var self = this; + + // Binding stuff doesn't work with constructors, so we have to create + // closure here. + this.list_.itemConstructor = function(item) { + return self.renderItem(item); + }; + } + + DefaultActionDialog.prototype = { + __proto__: FileManagerDialogBase.prototype + }; + + /** + * @override + */ + DefaultActionDialog.prototype.onInputFocus = function() { + this.list_.select(); + }; + + /** + * Renders item for list. + * @param {Object} item Item to render. + */ + DefaultActionDialog.prototype.renderItem = function(item) { + var result = this.document_.createElement('li'); + + var div = this.document_.createElement('div'); + div.textContent = item.label; + + if (item.iconType) { + div.setAttribute('file-type-icon', item.iconType); + } else if (item.iconUrl) { + div.style.backgroundImage = 'url(' + item.iconUrl + ')'; + } + + if (item.class) + div.classList.add(item.class); + + result.appendChild(div); + + cr.defineProperty(result, 'lead', cr.PropertyKind.BOOL_ATTR); + cr.defineProperty(result, 'selected', cr.PropertyKind.BOOL_ATTR); + + return result; + }; + + /** + * Shows dialog. + * + * @param {string} title Title in dialog caption. + * @param {string} message Message in dialog caption. + * @param {Array.<Object>} items Items to render in the list. + * @param {number} defaultIndex Item to select by default. + * @param {function(Object=)} opt_onOk OK callback with the selected item. + * @param {function()=} opt_onCancel Cancel callback. + */ + DefaultActionDialog.prototype.show = function(title, message, items, + defaultIndex, opt_onOk, opt_onCancel) { + + var show = FileManagerDialogBase.prototype.showOkCancelDialog.call( + this, title, message, opt_onOk, opt_onCancel); + + if (!show) { + console.error('DefaultActionDialog can\'t be shown.'); + return; + } + + if (!message) { + this.text_.setAttribute('hidden', 'hidden'); + } else { + this.text_.removeAttribute('hidden'); + } + + this.list_.startBatchUpdates(); + this.dataModel_.splice(0, this.dataModel_.length); + for (var i = 0; i < items.length; i++) { + this.dataModel_.push(items[i]); + } + this.selectionModel_.selectedIndex = defaultIndex; + this.list_.endBatchUpdates(); + }; + + /** + * List activation handler. Closes dialog and calls 'ok' callback. + * @param {number} index Activated index. + */ + DefaultActionDialog.prototype.activateItemAtIndex_ = function(index) { + this.hide(); + this.onOk_(this.dataModel_.item(index)); + }; + + /** + * Closes dialog and invokes callback with currently-selected item. + * @override + */ + DefaultActionDialog.prototype.onOkClick_ = function() { + this.activateItemAtIndex_(this.selectionModel_.selectedIndex); + }; + + /** + * @override + */ + DefaultActionDialog.prototype.onContainerKeyDown_ = function(event) { + // Handle Escape. + if (event.keyCode == 27) { + this.onCancelClick_(event); + event.preventDefault(); + } else if (event.keyCode == 32 || event.keyCode == 13) { + this.onOkClick_(); + event.preventDefault(); + } + }; + + return {DefaultActionDialog: DefaultActionDialog}; +}); diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/directory_contents.js b/chromium/chrome/browser/resources/file_manager/foreground/js/directory_contents.js new file mode 100644 index 00000000000..be69b66af90 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/directory_contents.js @@ -0,0 +1,770 @@ +// 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'; + +/** + * Scanner of the entries. + * @constructor + */ +function ContentScanner() { + this.cancelled_ = false; +} + +/** + * Starts to scan the entries. For example, starts to read the entries in a + * directory, or starts to search with some query on a file system. + * Derived classes must override this method. + * + * @param {function(Array.<Entry>)} entriesCallback Called when some chunk of + * entries are read. This can be called a couple of times until the + * completion. + * @param {function()} successCallback Called when the scan is completed + * successfully. + * @param {function(FileError)} errorCallback Called an error occurs. + */ +ContentScanner.prototype.scan = function( + entriesCallback, successCallback, errorCallback) { +}; + +/** + * Request cancelling of the running scan. When the cancelling is done, + * an error will be reported from errorCallback passed to scan(). + */ +ContentScanner.prototype.cancel = function() { + this.cancelled_ = true; +}; + +/** + * Scanner of the entries in a directory. + * @param {DirectoryEntry} entry The directory to be read. + * @constructor + * @extends {ContentScanner} + */ +function DirectoryContentScanner(entry) { + ContentScanner.call(this); + this.entry_ = entry; +} + +/** + * Extends ContentScanner. + */ +DirectoryContentScanner.prototype.__proto__ = ContentScanner.prototype; + +/** + * Starts to read the entries in the directory. + * @override + */ +DirectoryContentScanner.prototype.scan = function( + entriesCallback, successCallback, errorCallback) { + if (!this.entry_ || this.entry_ === DirectoryModel.fakeDriveEntry_) { + // If entry is not specified or a fake, we cannot read it. + errorCallback(util.createFileError(FileError.INVALID_MODIFICATION_ERR)); + return; + } + + metrics.startInterval('DirectoryScan'); + var reader = this.entry_.createReader(); + var readEntries = function() { + reader.readEntries( + function(entries) { + if (this.cancelled_) { + errorCallback(util.createFileError(FileError.ABORT_ERR)); + return; + } + + if (entries.length === 0) { + // All entries are read. + metrics.recordInterval('DirectoryScan'); + successCallback(); + return; + } + + entriesCallback(entries); + readEntries(); + }.bind(this), + errorCallback); + }.bind(this); + readEntries(); +}; + +/** + * Scanner of the entries for the search results on Drive File System. + * @param {string} query The query string. + * @constructor + * @extends {ContentScanner} + */ +function DriveSearchContentScanner(query) { + ContentScanner.call(this); + this.query_ = query; +} + +/** + * Extends ContentScanner. + */ +DriveSearchContentScanner.prototype.__proto__ = ContentScanner.prototype; + +/** + * Delay in milliseconds to be used for drive search scan, in order to reduce + * the number of server requests while user is typing the query. + * @type {number} + * @private + * @const + */ +DriveSearchContentScanner.SCAN_DELAY_ = 200; + +/** + * Maximum number of results which is shown on the search. + * @type {number} + * @private + * @const + */ +DriveSearchContentScanner.MAX_RESULTS_ = 100; + +/** + * Starts to search on Drive File System. + * @override + */ +DriveSearchContentScanner.prototype.scan = function( + entriesCallback, successCallback, errorCallback) { + var numReadEntries = 0; + var readEntries = function(nextFeed) { + chrome.fileBrowserPrivate.searchDrive( + {query: this.query_, nextFeed: nextFeed}, + function(entries, nextFeed) { + if (this.cancelled_) { + errorCallback(util.createFileError(FileError.ABORT_ERR)); + return; + } + + // TODO(tbarzic): Improve error handling. + if (!entries) { + console.error('Drive search encountered an error.'); + errorCallback(util.createFileError( + FileError.INVALID_MODIFICATION_ERR)); + return; + } + + var numRemainingEntries = + DriveSearchContentScanner.MAX_RESULTS_ - numReadEntries; + if (entries.length >= numRemainingEntries) { + // The limit is hit, so quit the scan here. + entries = entries.slice(0, numRemainingEntries); + nextFeed = ''; + } + + numReadEntries += entries.length; + if (entries.length > 0) + entriesCallback(entries); + + if (nextFeed === '') + successCallback(); + else + readEntries(nextFeed); + }.bind(this)); + }.bind(this); + + // Let's give another search a chance to cancel us before we begin. + setTimeout( + function() { + // Check cancelled state before read the entries. + if (this.cancelled_) { + errorCallback(util.createFileError(FileError.ABORT_ERR)); + return; + } + readEntries(''); + }.bind(this), + DriveSearchContentScanner.SCAN_DELAY_); +}; + +/** + * Scanner of the entries of the file name search on the directory tree, whose + * root is entry. + * @param {DirectoryEntry} entry The root of the search target directory tree. + * @param {string} query The query of the search. + * @constructor + * @extends {ContentScanner} + */ +function LocalSearchContentScanner(entry, query) { + ContentScanner.call(this); + this.entry_ = entry; + this.query_ = query.toLowerCase(); +} + +/** + * Extedns ContentScanner. + */ +LocalSearchContentScanner.prototype.__proto__ = ContentScanner.prototype; + +/** + * Starts the file name search. + * @override + */ +LocalSearchContentScanner.prototype.scan = function( + entriesCallback, successCallback, errorCallback) { + var numRunningTasks = 0; + var error = null; + var maybeRunCallback = function() { + if (numRunningTasks === 0) { + if (this.cancelled_) + errorCallback(util.createFileError(FileError.ABORT_ERR)); + else if (error) + errorCallback(error); + else + successCallback(); + } + }.bind(this); + + var processEntry = function(entry) { + numRunningTasks++; + var onError = function(fileError) { + if (!error) + error = fileError; + numRunningTasks--; + maybeRunCallback(); + }; + + var onSuccess = function(entries) { + if (this.cancelled_ || error || entries.length === 0) { + numRunningTasks--; + maybeRunCallback(); + return; + } + + // Filters by the query, and if found, run entriesCallback. + var foundEntries = entries.filter(function(entry) { + return entry.name.toLowerCase().indexOf(this.query_) >= 0; + }.bind(this)); + if (foundEntries.length > 0) + entriesCallback(foundEntries); + + // Start to process sub directories. + for (var i = 0; i < entries.length; i++) { + if (entries[i].isDirectory) + processEntry(entries[i]); + } + + // Read remaining entries. + reader.readEntries(onSuccess, onError); + }.bind(this); + + var reader = entry.createReader(); + reader.readEntries(onSuccess, onError); + }.bind(this); + + processEntry(this.entry_); +}; + +/** + * Scanner of the entries for the metadata search on Drive File System. + * @param {string} query The query of the search. + * @param {DriveMetadataSearchContentScanner.SearchType} searchType The option + * of the search. + * @constructor + * @extends {ContentScanner} + */ +function DriveMetadataSearchContentScanner(query, searchType) { + ContentScanner.call(this); + this.query_ = query; + this.searchType_ = searchType; +} + +/** + * Extends ContentScanner. + */ +DriveMetadataSearchContentScanner.prototype.__proto__ = + ContentScanner.prototype; + +/** + * The search types on the Drive File System. + * @enum {string} + */ +DriveMetadataSearchContentScanner.SearchType = Object.freeze({ + SEARCH_ALL: 'ALL', + SEARCH_SHARED_WITH_ME: 'SHARED_WITH_ME', + SEARCH_RECENT_FILES: 'EXCLUDE_DIRECTORIES', + SEARCH_OFFLINE: 'OFFLINE' +}); + +/** + * Starts to metadata-search on Drive File System. + * @override + */ +DriveMetadataSearchContentScanner.prototype.scan = function( + entriesCallback, successCallback, errorCallback) { + chrome.fileBrowserPrivate.searchDriveMetadata( + {query: this.query_, types: this.searchType_, maxResults: 500}, + function(results) { + if (this.cancelled_) { + errorCallback(util.createFileError(FileError.ABORT_ERR)); + return; + } + + if (!results) { + console.error('Drive search encountered an error.'); + errorCallback(util.createFileError( + FileError.INVALID_MODIFICATION_ERR)); + return; + } + + var entries = results.map(function(result) { return result.entry; }); + if (entries.length > 0) + entriesCallback(entries); + successCallback(); + }.bind(this)); +}; + +/** + * This class manages filters and determines a file should be shown or not. + * When filters are changed, a 'changed' event is fired. + * + * @param {MetadataCache} metadataCache Metadata cache service. + * @param {boolean} showHidden If files starting with '.' are shown. + * @constructor + * @extends {cr.EventTarget} + */ +function FileFilter(metadataCache, showHidden) { + /** + * @type {MetadataCache} + * @private + */ + this.metadataCache_ = metadataCache; + + /** + * @type Object.<string, Function> + * @private + */ + this.filters_ = {}; + this.setFilterHidden(!showHidden); + + // Do not show entries marked as 'deleted'. + this.addFilter('deleted', function(entry) { + var internal = this.metadataCache_.getCached(entry, 'internal'); + return !(internal && internal.deleted); + }.bind(this)); +} + +/* + * FileFilter extends cr.EventTarget. + */ +FileFilter.prototype = {__proto__: cr.EventTarget.prototype}; + +/** + * @param {string} name Filter identifier. + * @param {function(Entry)} callback A filter — a function receiving an Entry, + * and returning bool. + */ +FileFilter.prototype.addFilter = function(name, callback) { + this.filters_[name] = callback; + cr.dispatchSimpleEvent(this, 'changed'); +}; + +/** + * @param {string} name Filter identifier. + */ +FileFilter.prototype.removeFilter = function(name) { + delete this.filters_[name]; + cr.dispatchSimpleEvent(this, 'changed'); +}; + +/** + * @param {boolean} value If do not show hidden files. + */ +FileFilter.prototype.setFilterHidden = function(value) { + if (value) { + this.addFilter( + 'hidden', + function(entry) { return entry.name.substr(0, 1) !== '.'; } + ); + } else { + this.removeFilter('hidden'); + } +}; + +/** + * @return {boolean} If the files with names starting with "." are not shown. + */ +FileFilter.prototype.isFilterHiddenOn = function() { + return 'hidden' in this.filters_; +}; + +/** + * @param {Entry} entry File entry. + * @return {boolean} True if the file should be shown, false otherwise. + */ +FileFilter.prototype.filter = function(entry) { + for (var name in this.filters_) { + if (!this.filters_[name](entry)) + return false; + } + return true; +}; + +/** + * A context of DirectoryContents. + * TODO(yoshiki): remove this. crbug.com/224869. + * + * @param {FileFilter} fileFilter The file-filter context. + * @param {MetadataCache} metadataCache Metadata cache service. + * @constructor + */ +function FileListContext(fileFilter, metadataCache) { + /** + * @type {cr.ui.ArrayDataModel} + */ + this.fileList = new cr.ui.ArrayDataModel([]); + + /** + * @type {MetadataCache} + */ + this.metadataCache = metadataCache; + + /** + * @type {FileFilter} + */ + this.fileFilter = fileFilter; +} + +/** + * This class is responsible for scanning directory (or search results), + * and filling the fileList. Different descendants handle various types of + * directory contents shown: basic directory, drive search results, local search + * results. + * TODO(hidehiko): Remove EventTarget from this. + * + * @param {FileListContext} context The file list context. + * @param {boolean} isSearch True for search directory contents, otherwise + * false. + * @param {DirectoryEntry} directoryEntry The entry of the current directory. + * @param {DirectoryEntry} lastNonSearchDirectoryEntry The entry of the last + * non-search directory. + * @param {function():ContentScanner} scannerFactory The factory to create + * ContentScanner instance. + * @constructor + * @extends {cr.EventTarget} + */ +function DirectoryContents(context, isSearch, directoryEntry, + lastNonSearchDirectoryEntry, + scannerFactory) { + this.context_ = context; + this.fileList_ = context.fileList; + + this.isSearch_ = isSearch; + this.directoryEntry_ = directoryEntry; + this.lastNonSearchDirectoryEntry_ = lastNonSearchDirectoryEntry; + + this.scannerFactory_ = scannerFactory; + this.scanner_ = null; + this.prefetchMetadataQueue_ = new AsyncUtil.Queue(); + this.scanCancelled_ = false; +} + +/** + * DirectoryContents extends cr.EventTarget. + */ +DirectoryContents.prototype.__proto__ = cr.EventTarget.prototype; + +/** + * Create the copy of the object, but without scan started. + * @return {DirectoryContents} Object copy. + */ +DirectoryContents.prototype.clone = function() { + return new DirectoryContents( + this.context_, this.isSearch_, this.directoryEntry_, + this.lastNonSearchDirectoryEntry_, this.scannerFactory_); +}; + +/** + * Use a given fileList instead of the fileList from the context. + * @param {Array|cr.ui.ArrayDataModel} fileList The new file list. + */ +DirectoryContents.prototype.setFileList = function(fileList) { + if (fileList instanceof cr.ui.ArrayDataModel) + this.fileList_ = fileList; + else + this.fileList_ = new cr.ui.ArrayDataModel(fileList); + this.context_.metadataCache.setCacheSize(this.fileList_.length); +}; + +/** + * Use the filelist from the context and replace its contents with the entries + * from the current fileList. + */ +DirectoryContents.prototype.replaceContextFileList = function() { + if (this.context_.fileList !== this.fileList_) { + var spliceArgs = this.fileList_.slice(); + var fileList = this.context_.fileList; + spliceArgs.unshift(0, fileList.length); + fileList.splice.apply(fileList, spliceArgs); + this.fileList_ = fileList; + this.context_.metadataCache.setCacheSize(this.fileList_.length); + } +}; + +/** + * @return {boolean} If the scan is active. + */ +DirectoryContents.prototype.isScanning = function() { + return this.scanner_ || this.prefetchMetadataQueue_.isRunning(); +}; + +/** + * @return {boolean} True if search results (drive or local). + */ +DirectoryContents.prototype.isSearch = function() { + return this.isSearch_; +}; + +/** + * @return {DirectoryEntry} A DirectoryEntry for current directory. In case of + * search -- the top directory from which search is run. + */ +DirectoryContents.prototype.getDirectoryEntry = function() { + return this.directoryEntry_; +}; + +/** + * @return {DirectoryEntry} A DirectoryEntry for the last non search contents. + */ +DirectoryContents.prototype.getLastNonSearchDirectoryEntry = function() { + return this.lastNonSearchDirectoryEntry_; +}; + +/** + * Start directory scan/search operation. Either 'scan-completed' or + * 'scan-failed' event will be fired upon completion. + */ +DirectoryContents.prototype.scan = function() { + // TODO(hidehiko,mtomasz): this scan method must be called at most once. + // Remove such a limitation. + this.scanner_ = this.scannerFactory_(); + this.scanner_.scan(this.onNewEntries_.bind(this), + this.onScanCompleted_.bind(this), + this.onScanError_.bind(this)); +}; + +/** + * Cancels the running scan. + */ +DirectoryContents.prototype.cancelScan = function() { + if (this.scanCancelled_) + return; + this.scanCancelled_ = true; + if (this.scanner_) + this.scanner_.cancel(); + + this.prefetchMetadataQueue_.cancel(); + cr.dispatchSimpleEvent(this, 'scan-cancelled'); +}; + +/** + * Called when the scanning by scanner_ is done. + * @private + */ +DirectoryContents.prototype.onScanCompleted_ = function() { + this.scanner_ = null; + if (this.scanCancelled_) + return; + + this.prefetchMetadataQueue_.run(function(callback) { + // Call callback first, so isScanning() returns false in the event handlers. + callback(); + cr.dispatchSimpleEvent(this, 'scan-completed'); + }.bind(this)); +}; + +/** + * Called in case scan has failed. Should send the event. + * @private + */ +DirectoryContents.prototype.onScanError_ = function() { + this.scanner_ = null; + if (this.scanCancelled_) + return; + + this.prefetchMetadataQueue_.run(function(callback) { + // Call callback first, so isScanning() returns false in the event handlers. + callback(); + cr.dispatchSimpleEvent(this, 'scan-failed'); + }.bind(this)); +}; + +/** + * Called when some chunk of entries are read by scanner. + * @param {Array.<Entry>} entries The list of the scanned entries. + * @private + */ +DirectoryContents.prototype.onNewEntries_ = function(entries) { + if (this.scanCancelled_) + return; + + var entriesFiltered = [].filter.call( + entries, this.context_.fileFilter.filter.bind(this.context_.fileFilter)); + + // Update the filelist without waiting the metadata. + this.fileList_.push.apply(this.fileList_, entriesFiltered); + cr.dispatchSimpleEvent(this, 'scan-updated'); + + this.context_.metadataCache.setCacheSize(this.fileList_.length); + + // Because the prefetchMetadata can be slow, throttling by splitting entries + // into smaller chunks to reduce UI latency. + // TODO(hidehiko,mtomasz): This should be handled in MetadataCache. + var MAX_CHUNK_SIZE = 50; + for (var i = 0; i < entriesFiltered.length; i += MAX_CHUNK_SIZE) { + var chunk = entriesFiltered.slice(i, i + MAX_CHUNK_SIZE); + this.prefetchMetadataQueue_.run(function(chunk, callback) { + this.prefetchMetadata(chunk, function() { + if (this.scanCancelled_) { + // Do nothing if the scanning is cancelled. + callback(); + return; + } + + // TODO(yoshiki): Here we should fire the update event of changed + // items. Currently we have a method this.fileList_.updateIndex() to + // fire an event, but this method takes only 1 argument and invokes sort + // one by one. It is obviously time wasting. Instead, we call sort + // directory. + // In future, we should implement a good method like updateIndexes and + // use it here. + var status = this.fileList_.sortStatus; + this.fileList_.sort(status.field, status.direction); + + cr.dispatchSimpleEvent(this, 'scan-updated'); + callback(); + }.bind(this)); + }.bind(this, chunk)); + } +}; + +/** + * @param {Array.<Entry>} entries Files. + * @param {function(Object)} callback Callback on done. + */ +DirectoryContents.prototype.prefetchMetadata = function(entries, callback) { + this.context_.metadataCache.get(entries, 'filesystem', callback); +}; + +/** + * @param {Array.<Entry>} entries Files. + * @param {function(Object)} callback Callback on done. + */ +DirectoryContents.prototype.reloadMetadata = function(entries, callback) { + this.context_.metadataCache.clear(entries, '*'); + this.context_.metadataCache.get(entries, 'filesystem', callback); +}; + +/** + * @param {string} name Directory name. + * @param {function(DirectoryEntry)} successCallback Called on success. + * @param {function(FileError)} errorCallback On error. + */ +DirectoryContents.prototype.createDirectory = function( + name, successCallback, errorCallback) { + // TODO(hidehiko): createDirectory should not be the part of + // DirectoryContent. + if (this.isSearch_ || !this.directoryEntry_) { + errorCallback(util.createFileError(FileError.INVALID_MODIFICATION_ERR)); + return; + } + + var onSuccess = function(newEntry) { + this.reloadMetadata([newEntry], function() { + successCallback(newEntry); + }); + }; + + this.directoryEntry_.getDirectory(name, {create: true, exclusive: true}, + onSuccess.bind(this), errorCallback); +}; + +/** + * Creates a DirectoryContents instance to show entries in a directory. + * + * @param {FileListContext} context File list context. + * @param {DirectoryEntry} directoryEntry The current directory entry. + * @return {DirectoryContents} Created DirectoryContents instance. + */ +DirectoryContents.createForDirectory = function(context, directoryEntry) { + return new DirectoryContents( + context, + false, // Non search. + directoryEntry, + directoryEntry, + function() { + return new DirectoryContentScanner(directoryEntry); + }); +}; + +/** + * Creates a DirectoryContents instance to show the result of the search on + * Drive File System. + * + * @param {FileListContext} context File list context. + * @param {DirectoryEntry} directoryEntry The current directory entry. + * @param {DirectoryEntry} previousDirectoryEntry The DirectoryEntry that was + * current before the search. + * @param {string} query Search query. + * @return {DirectoryContents} Created DirectoryContents instance. + */ +DirectoryContents.createForDriveSearch = function( + context, directoryEntry, previousDirectoryEntry, query) { + return new DirectoryContents( + context, + true, // Search. + directoryEntry, + previousDirectoryEntry, + function() { + return new DriveSearchContentScanner(query); + }); +}; + +/** + * Creates a DirectoryContents instance to show the result of the search on + * Local File System. + * + * @param {FileListContext} context File list context. + * @param {DirectoryEntry} directoryEntry The current directory entry. + * @param {string} query Search query. + * @return {DirectoryContents} Created DirectoryContents instance. + */ +DirectoryContents.createForLocalSearch = function( + context, directoryEntry, query) { + return new DirectoryContents( + context, + true, // Search. + directoryEntry, + directoryEntry, + function() { + return new LocalSearchContentScanner(directoryEntry, query); + }); +}; + +/** + * Creates a DirectoryContents instance to show the result of metadata search + * on Drive File System. + * + * @param {FileListContext} context File list context. + * @param {DirectoryEntry} fakeDirectoryEntry Fake directory entry representing + * the set of result entries. This serves as a top directory for the + * search. + * @param {DirectoryEntry} driveDirectoryEntry Directory for the actual drive. + * @param {string} query Search query. + * @param {DriveMetadataSearchContentScanner.SearchType} searchType The type of + * the search. The scanner will restricts the entries based on the given + * type. + * @return {DirectoryContents} Created DirectoryContents instance. + */ +DirectoryContents.createForDriveMetadataSearch = function( + context, fakeDirectoryEntry, driveDirectoryEntry, query, searchType) { + return new DirectoryContents( + context, + true, // Search + fakeDirectoryEntry, + driveDirectoryEntry, + function() { + return new DriveMetadataSearchContentScanner(query, searchType); + }); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/directory_model.js b/chromium/chrome/browser/resources/file_manager/foreground/js/directory_model.js new file mode 100644 index 00000000000..fde41e8c321 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/directory_model.js @@ -0,0 +1,1186 @@ +// 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'; + +// If directory files changes too often, don't rescan directory more than once +// per specified interval +var SIMULTANEOUS_RESCAN_INTERVAL = 1000; +// Used for operations that require almost instant rescan. +var SHORT_RESCAN_INTERVAL = 100; + +/** + * Data model of the file manager. + * + * @param {boolean} singleSelection True if only one file could be selected + * at the time. + * @param {FileFilter} fileFilter Instance of FileFilter. + * @param {FileWatcher} fileWatcher Instance of FileWatcher. + * @param {MetadataCache} metadataCache The metadata cache service. + * @param {VolumeManagerWrapper} volumeManager The volume manager. + * @constructor + */ +function DirectoryModel(singleSelection, fileFilter, fileWatcher, + metadataCache, volumeManager) { + this.fileListSelection_ = singleSelection ? + new cr.ui.ListSingleSelectionModel() : new cr.ui.ListSelectionModel(); + + this.runningScan_ = null; + this.pendingScan_ = null; + this.rescanTime_ = null; + this.scanFailures_ = 0; + this.changeDirectorySequence_ = 0; + + this.fileFilter_ = fileFilter; + this.fileFilter_.addEventListener('changed', + this.onFilterChanged_.bind(this)); + + this.currentFileListContext_ = new FileListContext( + fileFilter, metadataCache); + this.currentDirContents_ = + DirectoryContents.createForDirectory(this.currentFileListContext_, null); + + this.volumeManager_ = volumeManager; + this.volumeManager_.volumeInfoList.addEventListener( + 'splice', this.onVolumeInfoListUpdated_.bind(this)); + + this.fileWatcher_ = fileWatcher; + this.fileWatcher_.addEventListener( + 'watcher-directory-changed', + this.onWatcherDirectoryChanged_.bind(this)); +} + +/** + * Fake entry to be used in currentDirEntry_ when current directory is + * unmounted DRIVE. TODO(haruki): Support "drive/root" and "drive/other". + * @type {Object} + * @const + * @private + */ +DirectoryModel.fakeDriveEntry_ = { + fullPath: RootDirectory.DRIVE + '/' + DriveSubRootDirectory.ROOT, + isDirectory: true, + rootType: RootType.DRIVE +}; + +/** + * Fake entry representing a psuedo directory, which contains Drive files + * available offline. This entry works as a trigger to start a search for + * offline files. + * @type {Object} + * @const + * @private + */ +DirectoryModel.fakeDriveOfflineEntry_ = { + fullPath: RootDirectory.DRIVE_OFFLINE, + isDirectory: true, + rootType: RootType.DRIVE_OFFLINE +}; + +/** + * Fake entry representing a pseudo directory, which contains shared-with-me + * Drive files. This entry works as a trigger to start a search for + * shared-with-me files. + * @type {Object} + * @const + * @private + */ +DirectoryModel.fakeDriveSharedWithMeEntry_ = { + fullPath: RootDirectory.DRIVE_SHARED_WITH_ME, + isDirectory: true, + rootType: RootType.DRIVE_SHARED_WITH_ME +}; + +/** + * Fake entry representing a pseudo directory, which contains Drive files + * accessed recently. This entry works as a trigger to start a metadata search + * implemented as DirectoryContentsDriveRecent. + * DirectoryModel is responsible to start the search when the UI tries to open + * this fake entry (e.g. changeDirectory()). + * @type {Object} + * @const + * @private + */ +DirectoryModel.fakeDriveRecentEntry_ = { + fullPath: RootDirectory.DRIVE_RECENT, + isDirectory: true, + rootType: RootType.DRIVE_RECENT +}; + +/** + * List of fake entries for special searches. + * + * @type {Array.<Object>} + * @const + */ +DirectoryModel.FAKE_DRIVE_SPECIAL_SEARCH_ENTRIES = [ + DirectoryModel.fakeDriveSharedWithMeEntry_, + DirectoryModel.fakeDriveRecentEntry_, + DirectoryModel.fakeDriveOfflineEntry_ +]; + +/** + * DirectoryModel extends cr.EventTarget. + */ +DirectoryModel.prototype.__proto__ = cr.EventTarget.prototype; + +/** + * Disposes the directory model by removing file watchers. + */ +DirectoryModel.prototype.dispose = function() { + this.fileWatcher_.dispose(); +}; + +/** + * @return {cr.ui.ArrayDataModel} Files in the current directory. + */ +DirectoryModel.prototype.getFileList = function() { + return this.currentFileListContext_.fileList; +}; + +/** + * Sort the file list. + * @param {string} sortField Sort field. + * @param {string} sortDirection "asc" or "desc". + */ +DirectoryModel.prototype.sortFileList = function(sortField, sortDirection) { + this.getFileList().sort(sortField, sortDirection); +}; + +/** + * @return {cr.ui.ListSelectionModel|cr.ui.ListSingleSelectionModel} Selection + * in the fileList. + */ +DirectoryModel.prototype.getFileListSelection = function() { + return this.fileListSelection_; +}; + +/** + * @return {RootType} Root type of current root. + */ +DirectoryModel.prototype.getCurrentRootType = function() { + var entry = this.currentDirContents_.getDirectoryEntry(); + return PathUtil.getRootType(entry ? entry.fullPath : ''); +}; + +/** + * @return {string} Root path. + */ +DirectoryModel.prototype.getCurrentRootPath = function() { + var entry = this.currentDirContents_.getDirectoryEntry(); + return entry ? PathUtil.getRootPath(entry.fullPath) : ''; +}; + +/** + * @return {string} Filesystem URL representing the mountpoint for the current + * contents. + */ +DirectoryModel.prototype.getCurrentMountPointUrl = function() { + var rootPath = this.getCurrentRootPath(); + // Special search roots are just showing a search results from DRIVE. + if (PathUtil.getRootType(rootPath) == RootType.DRIVE || + PathUtil.isSpecialSearchRoot(rootPath)) + return util.makeFilesystemUrl(RootDirectory.DRIVE); + + return util.makeFilesystemUrl(rootPath); +}; + +/** + * @return {boolean} on True if offline. + */ +DirectoryModel.prototype.isDriveOffline = function() { + var connection = this.volumeManager_.getDriveConnectionState(); + return connection.type == util.DriveConnectionType.OFFLINE; +}; + +/** + * TODO(haruki): This actually checks the current root. Fix the method name and + * related code. + * @return {boolean} True if the root for the current directory is read only. + */ +DirectoryModel.prototype.isReadOnly = function() { + return this.isPathReadOnly(this.getCurrentRootPath()); +}; + +/** + * @return {boolean} True if the a scan is active. + */ +DirectoryModel.prototype.isScanning = function() { + return this.currentDirContents_.isScanning(); +}; + +/** + * @return {boolean} True if search is in progress. + */ +DirectoryModel.prototype.isSearching = function() { + return this.currentDirContents_.isSearch(); +}; + +/** + * @param {string} path Path to check. + * @return {boolean} True if the |path| is read only. + */ +DirectoryModel.prototype.isPathReadOnly = function(path) { + // TODO(hidehiko): Migrate this into VolumeInfo. + switch (PathUtil.getRootType(path)) { + case RootType.REMOVABLE: + var volumeInfo = this.volumeManager_.getVolumeInfo(path); + // Returns true if the volume is actually read only, or if an error + // is found during the mounting. + // TODO(hidehiko): Remove "error" check here, by removing error'ed volume + // info from VolumeManager. + return volumeInfo && (volumeInfo.isReadOnly || !!volumeInfo.error); + case RootType.ARCHIVE: + return true; + case RootType.DOWNLOADS: + return false; + case RootType.DRIVE: + // TODO(haruki): Maybe add DRIVE_OFFLINE as well to allow renaming in the + // offline tab. + return this.isDriveOffline(); + default: + return true; + } +}; + +/** + * Updates the selection by using the updateFunc and publish the change event. + * If updateFunc returns true, it force to dispatch the change event even if the + * selection index is not changed. + * + * @param {cr.ui.ListSelectionModel|cr.ui.ListSingleSelectionModel} selection + * Selection to be updated. + * @param {function(): boolean} updateFunc Function updating the selection. + * @private + */ +DirectoryModel.prototype.updateSelectionAndPublishEvent_ = + function(selection, updateFunc) { + // Begin change. + selection.beginChange(); + + // If dispatchNeeded is true, we should ensure the change event is + // dispatched. + var dispatchNeeded = updateFunc(); + + // Check if the change event is dispatched in the endChange function + // or not. + var eventDispatched = function() { dispatchNeeded = false; }; + selection.addEventListener('change', eventDispatched); + selection.endChange(); + selection.removeEventListener('change', eventDispatched); + + // If the change event have been already dispatched, dispatchNeeded is false. + if (dispatchNeeded) { + var event = new Event('change'); + // The selection status (selected or not) is not changed because + // this event is caused by the change of selected item. + event.changes = []; + selection.dispatchEvent(event); + } +}; + +/** + * Invoked when a change in the directory is detected by the watcher. + * @private + */ +DirectoryModel.prototype.onWatcherDirectoryChanged_ = function() { + this.rescanSoon(); +}; + +/** + * Invoked when filters are changed. + * @private + */ +DirectoryModel.prototype.onFilterChanged_ = function() { + this.rescanSoon(); +}; + +/** + * Returns the filter. + * @return {FileFilter} The file filter. + */ +DirectoryModel.prototype.getFileFilter = function() { + return this.fileFilter_; +}; + +/** + * @return {DirectoryEntry} Current directory. + */ +DirectoryModel.prototype.getCurrentDirEntry = function() { + return this.currentDirContents_.getDirectoryEntry(); +}; + +/** + * @return {string} URL of the current directory. or null if unavailable. + */ +DirectoryModel.prototype.getCurrentDirectoryURL = function() { + var entry = this.currentDirContents_.getDirectoryEntry(); + if (!entry) + return null; + if (entry === DirectoryModel.fakeDriveOfflineEntry_) + return util.makeFilesystemUrl(entry.fullPath); + return entry.toURL(); +}; + +/** + * @return {string} Path for the current directory, or empty string if the + * current directory is not yet set. + */ +DirectoryModel.prototype.getCurrentDirPath = function() { + var entry = this.currentDirContents_.getDirectoryEntry(); + return entry ? entry.fullPath : ''; +}; + +/** + * @return {Array.<string>} File paths of selected files. + * @private + */ +DirectoryModel.prototype.getSelectedPaths_ = function() { + var indexes = this.fileListSelection_.selectedIndexes; + var fileList = this.getFileList(); + if (fileList) { + return indexes.map(function(i) { + return fileList.item(i).fullPath; + }); + } + return []; +}; + +/** + * @param {Array.<string>} value List of file paths of selected files. + * @private + */ +DirectoryModel.prototype.setSelectedPaths_ = function(value) { + var indexes = []; + var fileList = this.getFileList(); + + var safeKey = function(key) { + // The transformation must: + // 1. Never generate a reserved name ('__proto__') + // 2. Keep different keys different. + return '#' + key; + }; + + var hash = {}; + + for (var i = 0; i < value.length; i++) + hash[safeKey(value[i])] = 1; + + for (var i = 0; i < fileList.length; i++) { + if (hash.hasOwnProperty(safeKey(fileList.item(i).fullPath))) + indexes.push(i); + } + this.fileListSelection_.selectedIndexes = indexes; +}; + +/** + * @return {string} Lead item file path. + * @private + */ +DirectoryModel.prototype.getLeadPath_ = function() { + var index = this.fileListSelection_.leadIndex; + return index >= 0 && this.getFileList().item(index).fullPath; +}; + +/** + * @param {string} value The name of new lead index. + * @private + */ +DirectoryModel.prototype.setLeadPath_ = function(value) { + var fileList = this.getFileList(); + for (var i = 0; i < fileList.length; i++) { + if (fileList.item(i).fullPath === value) { + this.fileListSelection_.leadIndex = i; + return; + } + } +}; + +/** + * Schedule rescan with short delay. + */ +DirectoryModel.prototype.rescanSoon = function() { + this.scheduleRescan(SHORT_RESCAN_INTERVAL); +}; + +/** + * Schedule rescan with delay. Designed to handle directory change + * notification. + */ +DirectoryModel.prototype.rescanLater = function() { + this.scheduleRescan(SIMULTANEOUS_RESCAN_INTERVAL); +}; + +/** + * Schedule rescan with delay. If another rescan has been scheduled does + * nothing. File operation may cause a few notifications what should cause + * a single refresh. + * @param {number} delay Delay in ms after which the rescan will be performed. + */ +DirectoryModel.prototype.scheduleRescan = function(delay) { + if (this.rescanTime_) { + if (this.rescanTime_ <= Date.now() + delay) + return; + clearTimeout(this.rescanTimeoutId_); + } + + this.rescanTime_ = Date.now() + delay; + this.rescanTimeoutId_ = setTimeout(this.rescan.bind(this), delay); +}; + +/** + * Cancel a rescan on timeout if it is scheduled. + * @private + */ +DirectoryModel.prototype.clearRescanTimeout_ = function() { + this.rescanTime_ = null; + if (this.rescanTimeoutId_) { + clearTimeout(this.rescanTimeoutId_); + this.rescanTimeoutId_ = null; + } +}; + +/** + * Rescan current directory. May be called indirectly through rescanLater or + * directly in order to reflect user action. Will first cache all the directory + * contents in an array, then seamlessly substitute the fileList contents, + * preserving the select element etc. + * + * This should be to scan the contents of current directory (or search). + */ +DirectoryModel.prototype.rescan = function() { + this.clearRescanTimeout_(); + if (this.runningScan_) { + this.pendingRescan_ = true; + return; + } + + var dirContents = this.currentDirContents_.clone(); + dirContents.setFileList([]); + + var successCallback = (function() { + this.replaceDirectoryContents_(dirContents); + cr.dispatchSimpleEvent(this, 'rescan-completed'); + }).bind(this); + + this.scan_(dirContents, + successCallback, function() {}, function() {}, function() {}); +}; + +/** + * Run scan on the current DirectoryContents. The active fileList is cleared and + * the entries are added directly. + * + * This should be used when changing directory or initiating a new search. + * + * @param {DirectoryContentes} newDirContents New DirectoryContents instance to + * replace currentDirContents_. + * @param {function()=} opt_callback Called on success. + * @private + */ +DirectoryModel.prototype.clearAndScan_ = function(newDirContents, + opt_callback) { + if (this.currentDirContents_.isScanning()) + this.currentDirContents_.cancelScan(); + this.currentDirContents_ = newDirContents; + this.clearRescanTimeout_(); + + if (this.pendingScan_) + this.pendingScan_ = false; + + if (this.runningScan_) { + if (this.runningScan_.isScanning()) + this.runningScan_.cancelScan(); + this.runningScan_ = null; + } + + var onDone = function() { + cr.dispatchSimpleEvent(this, 'scan-completed'); + if (opt_callback) + opt_callback(); + }.bind(this); + + var onFailed = function() { + cr.dispatchSimpleEvent(this, 'scan-failed'); + }.bind(this); + + var onUpdated = function() { + cr.dispatchSimpleEvent(this, 'scan-updated'); + }.bind(this); + + var onCancelled = function() { + cr.dispatchSimpleEvent(this, 'scan-cancelled'); + }.bind(this); + + // Clear the table, and start scanning. + cr.dispatchSimpleEvent(this, 'scan-started'); + var fileList = this.getFileList(); + fileList.splice(0, fileList.length); + this.scan_(this.currentDirContents_, + onDone, onFailed, onUpdated, onCancelled); +}; + +/** + * Perform a directory contents scan. Should be called only from rescan() and + * clearAndScan_(). + * + * @param {DirectoryContents} dirContents DirectoryContents instance on which + * the scan will be run. + * @param {function()} successCallback Callback on success. + * @param {function()} failureCallback Callback on failure. + * @param {function()} updatedCallback Callback on update. Only on the last + * update, {@code successCallback} is called instead of this. + * @param {function()} cancelledCallback Callback on cancel. + * @private + */ +DirectoryModel.prototype.scan_ = function( + dirContents, + successCallback, failureCallback, updatedCallback, cancelledCallback) { + var self = this; + + /** + * Runs pending scan if there is one. + * + * @return {boolean} Did pending scan exist. + */ + var maybeRunPendingRescan = function() { + if (this.pendingRescan_) { + this.rescanSoon(); + this.pendingRescan_ = false; + return true; + } + return false; + }.bind(this); + + var onSuccess = function() { + // Record metric for Downloads directory. + if (!dirContents.isSearch()) { + var locationInfo = + this.volumeManager_.getLocationInfo(dirContents.getDirectoryEntry()); + if (locationInfo.volumeInfo.volumeType === util.VolumeType.DOWNLOADS && + locationInfo.isRootEntry) { + metrics.recordMediumCount('DownloadsCount', + dirContents.fileList_.length); + } + } + + this.runningScan_ = null; + successCallback(); + this.scanFailures_ = 0; + maybeRunPendingRescan(); + }.bind(this); + + var onFailure = function() { + this.runningScan_ = null; + this.scanFailures_++; + failureCallback(); + + if (maybeRunPendingRescan()) + return; + + if (this.scanFailures_ <= 1) + this.rescanLater(); + }.bind(this); + + this.runningScan_ = dirContents; + + dirContents.addEventListener('scan-completed', onSuccess); + dirContents.addEventListener('scan-updated', updatedCallback); + dirContents.addEventListener('scan-failed', onFailure); + dirContents.addEventListener('scan-cancelled', cancelledCallback); + dirContents.scan(); +}; + +/** + * @param {DirectoryContents} dirContents DirectoryContents instance. + * @private + */ +DirectoryModel.prototype.replaceDirectoryContents_ = function(dirContents) { + cr.dispatchSimpleEvent(this, 'begin-update-files'); + this.updateSelectionAndPublishEvent_(this.fileListSelection_, function() { + var selectedPaths = this.getSelectedPaths_(); + var selectedIndices = this.fileListSelection_.selectedIndexes; + + // Restore leadIndex in case leadName no longer exists. + var leadIndex = this.fileListSelection_.leadIndex; + var leadPath = this.getLeadPath_(); + + this.currentDirContents_ = dirContents; + dirContents.replaceContextFileList(); + + this.setSelectedPaths_(selectedPaths); + this.fileListSelection_.leadIndex = leadIndex; + this.setLeadPath_(leadPath); + + // If nothing is selected after update, then select file next to the + // latest selection + var forceChangeEvent = false; + if (this.fileListSelection_.selectedIndexes.length == 0 && + selectedIndices.length != 0) { + var maxIdx = Math.max.apply(null, selectedIndices); + this.selectIndex(Math.min(maxIdx - selectedIndices.length + 2, + this.getFileList().length) - 1); + forceChangeEvent = true; + } + return forceChangeEvent; + }.bind(this)); + + cr.dispatchSimpleEvent(this, 'end-update-files'); +}; + +/** + * Callback when an entry is changed. + * @param {util.EntryChangedKind} kind How the entry is changed. + * @param {Entry} entry The changed entry. + */ +DirectoryModel.prototype.onEntryChanged = function(kind, entry) { + // TODO(hidehiko): We should update directory model even the search result + // is shown. + var rootType = this.getCurrentRootType(); + if ((rootType === RootType.DRIVE || + rootType === RootType.DRIVE_SHARED_WITH_ME || + rootType === RootType.DRIVE_RECENT || + rootType === RootType.DRIVE_OFFLINE) && + this.isSearching()) + return; + + if (kind == util.EntryChangedKind.CREATED) { + entry.getParent(function(parentEntry) { + if (this.getCurrentDirEntry().fullPath != parentEntry.fullPath) { + // Do nothing if current directory changed during async operations. + return; + } + this.currentDirContents_.prefetchMetadata([entry], function() { + if (this.getCurrentDirEntry().fullPath != parentEntry.fullPath) { + // Do nothing if current directory changed during async operations. + return; + } + + var index = this.findIndexByEntry_(entry); + if (index >= 0) + this.getFileList().splice(index, 1, entry); + else + this.getFileList().push(entry); + }.bind(this)); + }.bind(this)); + } else { + // This is the delete event. + var index = this.findIndexByEntry_(entry); + if (index >= 0) + this.getFileList().splice(index, 1); + } +}; + +/** + * @param {Entry} entry The entry to be searched. + * @return {number} The index in the fileList, or -1 if not found. + * @private + */ +DirectoryModel.prototype.findIndexByEntry_ = function(entry) { + var fileList = this.getFileList(); + for (var i = 0; i < fileList.length; i++) { + if (util.isSameEntry(fileList.item(i), entry)) + return i; + } + return -1; +}; + +/** + * Called when rename is done successfully. + * Note: conceptually, DirectoryModel should work without this, because entries + * can be renamed by other systems anytime and Files.app should reflect it + * correctly. + * TODO(hidehiko): investigate more background, and remove this if possible. + * + * @param {Entry} oldEntry The old entry. + * @param {Entry} newEntry The new entry. + * @param {function()} opt_callback Called on completion. + */ +DirectoryModel.prototype.onRenameEntry = function( + oldEntry, newEntry, opt_callback) { + this.currentDirContents_.prefetchMetadata([newEntry], function() { + // If the current directory is the old entry, then quietly change to the + // new one. + if (util.isSameEntry(oldEntry, this.getCurrentDirEntry())) + this.changeDirectory(newEntry.fullPath); + + // Look for the old entry. + // If the entry doesn't exist in the list, it has been updated from + // outside (probably by directory rescan). + var index = this.findIndexByEntry_(oldEntry); + if (index >= 0) { + // Update the content list and selection status. + var wasSelected = this.fileListSelection_.getIndexSelected(index); + this.updateSelectionAndPublishEvent_(this.fileListSelection_, function() { + this.fileListSelection_.setIndexSelected(index, false); + this.getFileList().splice(index, 1, newEntry); + if (wasSelected) { + // We re-search the index, because splice may trigger sorting so that + // index may be stale. + this.fileListSelection_.setIndexSelected( + this.findIndexByEntry_(newEntry), true); + } + return true; + }.bind(this)); + } + + // Run callback, finally. + if (opt_callback) + opt_callback(); + }.bind(this)); +}; + +/** + * Creates directory and updates the file list. + * + * @param {string} name Directory name. + * @param {function(DirectoryEntry)} successCallback Callback on success. + * @param {function(FileError)} errorCallback Callback on failure. + */ +DirectoryModel.prototype.createDirectory = function(name, successCallback, + errorCallback) { + var entry = this.getCurrentDirEntry(); + if (!entry) { + errorCallback(util.createFileError(FileError.INVALID_MODIFICATION_ERR)); + return; + } + + var tracker = this.createDirectoryChangeTracker(); + tracker.start(); + + var onSuccess = function(newEntry) { + // Do not change anything or call the callback if current + // directory changed. + tracker.stop(); + if (tracker.hasChanged) + return; + + var existing = this.getFileList().slice().filter( + function(e) {return e.name == name;}); + + if (existing.length) { + this.selectEntry(newEntry); + successCallback(existing[0]); + } else { + this.fileListSelection_.beginChange(); + this.getFileList().splice(0, 0, newEntry); + this.selectEntry(newEntry); + this.fileListSelection_.endChange(); + successCallback(newEntry); + } + }; + + this.currentDirContents_.createDirectory(name, onSuccess.bind(this), + errorCallback); +}; + +/** + * Changes directory. Causes 'directory-change' event. + * + * The directory will not be changed, if another request is started before it is + * finished. The error callback will not be called, and the event for the first + * request will not be invoked. + * + * @param {string} path New current directory path. + * @param {function(FileError)=} opt_errorCallback Executed if the change + * directory failed. + */ +DirectoryModel.prototype.changeDirectory = function(path, opt_errorCallback) { + this.changeDirectorySequence_++; + + if (PathUtil.isSpecialSearchRoot(path)) { + this.specialSearch(path, ''); + return; + } + + this.resolveDirectory( + path, + function(sequence, directoryEntry) { + if (this.changeDirectorySequence_ === sequence) + this.changeDirectoryEntry(directoryEntry); + }.bind(this, this.changeDirectorySequence_), + function(error) { + console.error('Error changing directory to ' + path + ': ', error); + if (opt_errorCallback) + opt_errorCallback(error); + }); +}; + +/** + * Resolves absolute directory path. Handles Drive stub. If the drive is + * mounting, callbacks will be called after the mount is completed. + * + * @param {string} path Path to the directory. + * @param {function(DirectoryEntry)} successCallback Success callback. + * @param {function(FileError)} errorCallback Error callback. + */ +DirectoryModel.prototype.resolveDirectory = function( + path, successCallback, errorCallback) { + if (PathUtil.getRootType(path) == RootType.DRIVE) { + if (!this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE)) { + errorCallback(util.createFileError(FileError.NOT_FOUND_ERR)); + return; + } + } + + var onError = function(error) { + // Handle the special case, when in offline mode, and there are no cached + // contents on the C++ side. In such case, let's display the stub. + // The INVALID_STATE_ERR error code is returned from the drive filesystem + // in such situation. + // + // TODO(mtomasz, hashimoto): Consider rewriting this logic. + // crbug.com/253464. + if (PathUtil.getRootType(path) == RootType.DRIVE && + error.code == FileError.INVALID_STATE_ERR) { + successCallback(DirectoryModel.fakeDriveEntry_); + return; + } + errorCallback(error); + }.bind(this); + + // TODO(mtomasz): Use Entry instead of a path. + this.volumeManager_.resolveAbsolutePath( + path, + function(entry) { + if (entry.isFile) { + onError(util.createFileError(FileError.TYPE_MISMATCH_ERR)); + return; + } + successCallback(entry); + }, + onError); +}; + +/** + * @param {DirectoryEntry} dirEntry The absolute path to the new directory. + * @param {function()=} opt_callback Executed if the directory loads + * successfully. + * @private + */ +DirectoryModel.prototype.changeDirectoryEntrySilent_ = function(dirEntry, + opt_callback) { + var onScanComplete = function() { + if (opt_callback) + opt_callback(); + // For tests that open the dialog to empty directories, everything + // is loaded at this point. + chrome.test.sendMessage('directory-change-complete'); + }; + this.clearAndScan_( + DirectoryContents.createForDirectory(this.currentFileListContext_, + dirEntry), + onScanComplete.bind(this)); +}; + +/** + * Change the current directory to the directory represented by a + * DirectoryEntry. + * + * Dispatches the 'directory-changed' event when the directory is successfully + * changed. + * + * @param {DirectoryEntry} dirEntry The absolute path to the new directory. + * @param {function()=} opt_callback Executed if the directory loads + * successfully. + */ +DirectoryModel.prototype.changeDirectoryEntry = function( + dirEntry, opt_callback) { + this.fileWatcher_.changeWatchedDirectory(dirEntry, function(sequence) { + if (this.changeDirectorySequence_ !== sequence) + return; + var previous = this.currentDirContents_.getDirectoryEntry(); + this.clearSearch_(); + this.changeDirectoryEntrySilent_(dirEntry, opt_callback); + + var e = new Event('directory-changed'); + e.previousDirEntry = previous; + e.newDirEntry = dirEntry; + this.dispatchEvent(e); + }.bind(this, this.changeDirectorySequence_)); +}; + +/** + * Creates an object which could say whether directory has changed while it has + * been active or not. Designed for long operations that should be cancelled + * if the used change current directory. + * @return {Object} Created object. + */ +DirectoryModel.prototype.createDirectoryChangeTracker = function() { + var tracker = { + dm_: this, + active_: false, + hasChanged: false, + + start: function() { + if (!this.active_) { + this.dm_.addEventListener('directory-changed', + this.onDirectoryChange_); + this.active_ = true; + this.hasChanged = false; + } + }, + + stop: function() { + if (this.active_) { + this.dm_.removeEventListener('directory-changed', + this.onDirectoryChange_); + this.active_ = false; + } + }, + + onDirectoryChange_: function(event) { + tracker.stop(); + tracker.hasChanged = true; + } + }; + return tracker; +}; + +/** + * @param {Entry} entry Entry to be selected. + */ +DirectoryModel.prototype.selectEntry = function(entry) { + var fileList = this.getFileList(); + for (var i = 0; i < fileList.length; i++) { + if (fileList.item(i).toURL() === entry.toURL()) { + this.selectIndex(i); + return; + } + } +}; + +/** + * @param {Array.<string>} entries Array of entries. + */ +DirectoryModel.prototype.selectEntries = function(entries) { + // URLs are needed here, since we are comparing Entries by URLs. + var urls = util.entriesToURLs(entries); + var fileList = this.getFileList(); + this.fileListSelection_.beginChange(); + this.fileListSelection_.unselectAll(); + for (var i = 0; i < fileList.length; i++) { + if (urls.indexOf(fileList.item(i).toURL()) >= 0) + this.fileListSelection_.setIndexSelected(i, true); + } + this.fileListSelection_.endChange(); +}; + +/** + * @param {number} index Index of file. + */ +DirectoryModel.prototype.selectIndex = function(index) { + // this.focusCurrentList_(); + if (index >= this.getFileList().length) + return; + + // If a list bound with the model it will do scrollIndexIntoView(index). + this.fileListSelection_.selectedIndex = index; +}; + +/** + * Called when VolumeInfoList is updated. + * + * @param {Event} event Event of VolumeInfoList's 'splice'. + * @private + */ +DirectoryModel.prototype.onVolumeInfoListUpdated_ = function(event) { + var driveVolume = this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE); + if (driveVolume && !driveVolume.error) { + var currentDirEntry = this.getCurrentDirEntry(); + if (currentDirEntry) { + if (currentDirEntry === DirectoryModel.fakeDriveEntry_) { + // Replace the fake entry by real DirectoryEntry silently. + this.volumeManager_.resolveAbsolutePath( + DirectoryModel.fakeDriveEntry_.fullPath, + function(entry) { + // If the current entry is still fake drive entry, replace it. + if (this.getCurrentDirEntry() === DirectoryModel.fakeDriveEntry_) + this.changeDirectoryEntrySilent_(entry); + }, + function(error) {}); + } else if (PathUtil.isSpecialSearchRoot(currentDirEntry.fullPath)) { + for (var i = 0; i < event.added.length; i++) { + if (event.added[i].volumeType == util.VolumeType.DRIVE) { + // If the Drive volume is newly mounted, rescan it. + this.rescan(); + break; + } + } + } + } + } + + // When the volume where we are is unmounted, fallback to + // DEFAULT_MOUNT_POINT. If current directory path is empty, stop the fallback + // since the current directory is initializing now. + // TODO(mtomasz): DEFAULT_MOUNT_POINT is deprecated. Use VolumeManager:: + // getDefaultVolume() after it is implemented. + if (this.getCurrentDirPath() && + !this.volumeManager_.getVolumeInfo(this.getCurrentDirPath())) + this.changeDirectory(PathUtil.DEFAULT_MOUNT_POINT); +}; + +/** + * Check if the root of the given path is mountable or not. + * + * @param {string} path Path. + * @return {boolean} Return true, if the given path is under mountable root. + * Otherwise, return false. + */ +DirectoryModel.isMountableRoot = function(path) { + var rootType = PathUtil.getRootType(path); + switch (rootType) { + case RootType.DOWNLOADS: + return false; + case RootType.ARCHIVE: + case RootType.REMOVABLE: + case RootType.DRIVE: + return true; + default: + throw new Error('Unknown root type!'); + } +}; + +/** + * Performs search and displays results. The search type is dependent on the + * current directory. If we are currently on drive, server side content search + * over drive mount point. If the current directory is not on the drive, file + * name search over current directory will be performed. + * + * @param {string} query Query that will be searched for. + * @param {function(Event)} onSearchRescan Function that will be called when the + * search directory is rescanned (i.e. search results are displayed). + * @param {function()} onClearSearch Function to be called when search state + * gets cleared. + * TODO(olege): Change callbacks to events. + */ +DirectoryModel.prototype.search = function(query, + onSearchRescan, + onClearSearch) { + query = query.trimLeft(); + + this.clearSearch_(); + + var currentDirEntry = this.getCurrentDirEntry(); + if (!currentDirEntry) { + // Not yet initialized. Do nothing. + return; + } + + if (!query) { + if (this.isSearching()) { + var newDirContents = DirectoryContents.createForDirectory( + this.currentFileListContext_, + this.currentDirContents_.getLastNonSearchDirectoryEntry()); + this.clearAndScan_(newDirContents); + } + return; + } + + this.onSearchCompleted_ = onSearchRescan; + this.onClearSearch_ = onClearSearch; + + this.addEventListener('scan-completed', this.onSearchCompleted_); + + // If we are offline, let's fallback to file name search inside dir. + // A search initiated from directories in Drive or special search results + // should trigger Drive search. + var newDirContents; + if (!this.isDriveOffline() && + PathUtil.isDriveBasedPath(currentDirEntry.fullPath)) { + // Drive search is performed over the whole drive, so pass drive root as + // |directoryEntry|. + newDirContents = DirectoryContents.createForDriveSearch( + this.currentFileListContext_, + currentDirEntry, + this.currentDirContents_.getLastNonSearchDirectoryEntry(), + query); + } else { + newDirContents = DirectoryContents.createForLocalSearch( + this.currentFileListContext_, currentDirEntry, query); + } + this.clearAndScan_(newDirContents); +}; + +/** + * Performs special search and displays results. e.g. Drive files available + * offline, shared-with-me files, recently modified files. + * @param {string} path Path string representing special search. See fake + * entries in PathUtil.RootDirectory. + * @param {string=} opt_query Query string used for the search. + */ +DirectoryModel.prototype.specialSearch = function(path, opt_query) { + var query = opt_query || ''; + + this.clearSearch_(); + + this.onSearchCompleted_ = null; + this.onClearSearch_ = null; + + var onDriveDirectoryResolved = function(sequence, driveRoot) { + if (this.changeDirectorySequence_ !== sequence) + return; + if (!driveRoot || driveRoot == DirectoryModel.fakeDriveEntry_) { + // Drive root not available or not ready. onVolumeInfoListUpdated_() + // handles the rescan if necessary. + driveRoot = null; + } + + var specialSearchType = PathUtil.getRootType(path); + var searchOption; + var dirEntry; + if (specialSearchType == RootType.DRIVE_OFFLINE) { + dirEntry = DirectoryModel.fakeDriveOfflineEntry_; + searchOption = + DriveMetadataSearchContentScanner.SearchType.SEARCH_OFFLINE; + } else if (specialSearchType == RootType.DRIVE_SHARED_WITH_ME) { + dirEntry = DirectoryModel.fakeDriveSharedWithMeEntry_; + searchOption = + DriveMetadataSearchContentScanner.SearchType.SEARCH_SHARED_WITH_ME; + } else if (specialSearchType == RootType.DRIVE_RECENT) { + dirEntry = DirectoryModel.fakeDriveRecentEntry_; + searchOption = + DriveMetadataSearchContentScanner.SearchType.SEARCH_RECENT_FILES; + } else { + // Unknown path. + throw new Error('Unknown path for special search.'); + } + + var newDirContents = DirectoryContents.createForDriveMetadataSearch( + this.currentFileListContext_, + dirEntry, driveRoot, query, searchOption); + var previous = this.currentDirContents_.getDirectoryEntry(); + this.clearAndScan_(newDirContents); + + var e = new Event('directory-changed'); + e.previousDirEntry = previous; + e.newDirEntry = dirEntry; + this.dispatchEvent(e); + }.bind(this, this.changeDirectorySequence_); + + this.resolveDirectory(DirectoryModel.fakeDriveEntry_.fullPath, + onDriveDirectoryResolved /* success */, + function() {} /* failed */); +}; + +/** + * In case the search was active, remove listeners and send notifications on + * its canceling. + * @private + */ +DirectoryModel.prototype.clearSearch_ = function() { + if (!this.isSearching()) + return; + + if (this.onSearchCompleted_) { + this.removeEventListener('scan-completed', this.onSearchCompleted_); + this.onSearchCompleted_ = null; + } + + if (this.onClearSearch_) { + this.onClearSearch_(); + this.onClearSearch_ = null; + } +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/directory_tree.js b/chromium/chrome/browser/resources/file_manager/foreground/js/directory_tree.js new file mode 100644 index 00000000000..5c9de7aa0fe --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/directory_tree.js @@ -0,0 +1,676 @@ +// Copyright (c) 2013 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'; + +//////////////////////////////////////////////////////////////////////////////// +// DirectoryTreeUtil + +/** + * Utility methods. They are intended for use only in this file. + */ +var DirectoryTreeUtil = {}; + +/** + * Generate a list of the directory entries for the top level on the tree. + * @return {Array.<DirectoryEntry>} Entries for the top level on the tree. + */ +DirectoryTreeUtil.generateTopLevelEntries = function() { + var entries = [ + DirectoryModel.fakeDriveEntry_, + DirectoryModel.fakeDriveOfflineEntry_, + DirectoryModel.fakeDriveSharedWithMeEntry_, + DirectoryModel.fakeDriveRecentEntry_, + ]; + + for (var i = 0; i < entries.length; i++) { + entries[i]['label'] = PathUtil.getRootLabel(entries[i].fullPath); + } + + return entries; +}; + +/** + * Checks if the given directory can be on the tree or not. + * + * @param {string} path Path to be checked. + * @return {boolean} True if the path is eligible for the directory tree. + * Otherwise, false. + */ +DirectoryTreeUtil.isEligiblePathForDirectoryTree = function(path) { + return PathUtil.isDriveBasedPath(path); +}; + +Object.freeze(DirectoryTreeUtil); + +//////////////////////////////////////////////////////////////////////////////// +// DirectoryTreeBase + +/** + * Implementation of methods for DirectoryTree and DirectoryItem. These classes + * inherits cr.ui.Tree/TreeItem so we can't make them inherit this class. + * Instead, we separate their implementations to this separate object and call + * it with setting 'this' from DirectoryTree/Item. + */ +var DirectoryItemTreeBaseMethods = {}; + +/** + * Updates sub-elements of {@code this} reading {@code DirectoryEntry}. + * The list of {@code DirectoryEntry} are not updated by this method. + * + * @param {boolean} recursive True if the all visible sub-directories are + * updated recursively including left arrows. If false, the update walks + * only immediate child directories without arrows. + */ +DirectoryItemTreeBaseMethods.updateSubElementsFromList = function(recursive) { + var index = 0; + var tree = this.parentTree_ || this; // If no parent, 'this' itself is tree. + while (this.entries_[index]) { + var currentEntry = this.entries_[index]; + var currentElement = this.items[index]; + + if (index >= this.items.length) { + var item = new DirectoryItem(currentEntry, this, tree); + this.add(item); + index++; + } else if (currentEntry.fullPath == currentElement.fullPath) { + if (recursive && this.expanded) + currentElement.updateSubDirectories(true /* recursive */); + + index++; + } else if (currentEntry.fullPath < currentElement.fullPath) { + var item = new DirectoryItem(currentEntry, this, tree); + this.addAt(item, index); + index++; + } else if (currentEntry.fullPath > currentElement.fullPath) { + this.remove(currentElement); + } + } + + var removedChild; + while (removedChild = this.items[index]) { + this.remove(removedChild); + } + + if (index == 0) { + this.hasChildren = false; + this.expanded = false; + } else { + this.hasChildren = true; + } +}; + +/** + * Finds a parent directory of the {@code entry} in {@code this}, and + * invokes the DirectoryItem.selectByEntry() of the found directory. + * + * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be + * a fake. + * @return {boolean} True if the parent item is found. + */ +DirectoryItemTreeBaseMethods.searchAndSelectByEntry = function(entry) { + for (var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + if (util.isParentEntry(item.entry, entry)) { + item.selectByEntry(entry); + return true; + } + } + return false; +}; + +Object.freeze(DirectoryItemTreeBaseMethods); + +//////////////////////////////////////////////////////////////////////////////// +// DirectoryItem + +/** + * A directory in the tree. Each element represents one directory. + * + * @param {DirectoryEntry} dirEntry DirectoryEntry of this item. + * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item. + * @param {DirectoryTree} tree Current tree, which contains this item. + * @extends {cr.ui.TreeItem} + * @constructor + */ +function DirectoryItem(dirEntry, parentDirItem, tree) { + var item = cr.doc.createElement('div'); + DirectoryItem.decorate(item, dirEntry, parentDirItem, tree); + return item; +} + +/** + * @param {HTMLElement} el Element to be DirectoryItem. + * @param {DirectoryEntry} dirEntry DirectoryEntry of this item. + * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item. + * @param {DirectoryTree} tree Current tree, which contains this item. + */ +DirectoryItem.decorate = + function(el, dirEntry, parentDirItem, tree) { + el.__proto__ = DirectoryItem.prototype; + (/** @type {DirectoryItem} */ el).decorate( + dirEntry, parentDirItem, tree); +}; + +DirectoryItem.prototype = { + __proto__: cr.ui.TreeItem.prototype, + + /** + * The DirectoryEntry corresponding to this DirectoryItem. This may be + * a dummy DirectoryEntry. + * @type {DirectoryEntry|Object} + */ + get entry() { + return this.dirEntry_; + }, + + /** + * The element containing the label text and the icon. + * @type {!HTMLElement} + * @override + */ + get labelElement() { + return this.firstElementChild.querySelector('.label'); + } +}; + +/** + * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList(). + * + * @param {boolean} recursive True if the all visible sub-directories are + * updated recursively including left arrows. If false, the update walks + * only immediate child directories without arrows. + */ +DirectoryItem.prototype.updateSubElementsFromList = function(recursive) { + DirectoryItemTreeBaseMethods.updateSubElementsFromList.call(this, recursive); +}; + +/** + * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList(). + * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be + * a fake. + * @return {boolean} True if the parent item is found. + */ +DirectoryItem.prototype.searchAndSelectByEntry = function(entry) { + return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry); +}; + +/** + * @param {DirectoryEntry} dirEntry DirectoryEntry of this item. + * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item. + * @param {DirectoryTree} tree Current tree, which contains this item. + */ +DirectoryItem.prototype.decorate = function( + dirEntry, parentDirItem, tree) { + var path = dirEntry.fullPath; + var label; + label = dirEntry.label ? dirEntry.label : dirEntry.name; + + this.className = 'tree-item'; + this.innerHTML = + '<div class="tree-row">' + + ' <span class="expand-icon"></span>' + + ' <span class="icon"></span>' + + ' <span class="label"></span>' + + '</div>' + + '<div class="tree-children"></div>'; + this.setAttribute('role', 'treeitem'); + + this.parentTree_ = tree; + this.directoryModel_ = tree.directoryModel; + this.parent_ = parentDirItem; + this.label = label; + this.fullPath = path; + this.dirEntry_ = dirEntry; + this.fileFilter_ = this.directoryModel_.getFileFilter(); + + // Sets hasChildren=false tentatively. This will be overridden after + // scanning sub-directories in DirectoryTreeUtil.updateSubElementsFromList. + this.hasChildren = false; + + this.addEventListener('expand', this.onExpand_.bind(this), false); + var icon = this.querySelector('.icon'); + icon.classList.add('volume-icon'); + var iconType = PathUtil.getRootType(path); + if (iconType && PathUtil.isRootPath(path)) + icon.setAttribute('volume-type-icon', iconType); + else + icon.setAttribute('file-type-icon', 'folder'); + + if (this.parentTree_.contextMenuForSubitems) + this.setContextMenu(this.parentTree_.contextMenuForSubitems); + // Adds handler for future change. + this.parentTree_.addEventListener( + 'contextMenuForSubitemsChange', + function(e) { this.setContextMenu(e.newValue); }.bind(this)); + + if (parentDirItem.expanded) + this.updateSubDirectories(false /* recursive */); +}; + +/** + * Overrides WebKit's scrollIntoViewIfNeeded, which doesn't work well with + * a complex layout. This call is not necessary, so we are ignoring it. + * + * @param {boolean} unused Unused. + * @override + */ +DirectoryItem.prototype.scrollIntoViewIfNeeded = function(unused) { +}; + +/** + * Removes the child node, but without selecting the parent item, to avoid + * unintended changing of directories. Removing is done externally, and other + * code will navigate to another directory. + * + * @param {!cr.ui.TreeItem} child The tree item child to remove. + * @override + */ +DirectoryItem.prototype.remove = function(child) { + this.lastElementChild.removeChild(child); + if (this.items.length == 0) + this.hasChildren = false; +}; + +/** + * Invoked when the item is being expanded. + * @param {!UIEvent} e Event. + * @private + **/ +DirectoryItem.prototype.onExpand_ = function(e) { + this.updateSubDirectories( + true /* recursive */, + function() {}, + function() { + this.expanded = false; + }.bind(this)); + + e.stopPropagation(); +}; + +/** + * Retrieves the latest subdirectories and update them on the tree. + * @param {boolean} recursive True if the update is recursively. + * @param {function()=} opt_successCallback Callback called on success. + * @param {function()=} opt_errorCallback Callback called on error. + */ +DirectoryItem.prototype.updateSubDirectories = function( + recursive, opt_successCallback, opt_errorCallback) { + if (util.isFakeEntry(this.entry)) { + if (opt_errorCallback) + opt_errorCallback(); + return; + } + + var sortEntries = function(fileFilter, entries) { + entries.sort(function(a, b) { + return (a.name.toLowerCase() > b.name.toLowerCase()) ? 1 : -1; + }); + return entries.filter(fileFilter.filter.bind(fileFilter)); + }; + + var onSuccess = function(entries) { + this.entries_ = entries; + this.redrawSubDirectoryList_(recursive); + opt_successCallback && opt_successCallback(); + }.bind(this); + + var reader = this.entry.createReader(); + var entries = []; + var readEntry = function() { + reader.readEntries(function(results) { + if (!results.length) { + onSuccess(sortEntries(this.fileFilter_, entries)); + return; + } + + for (var i = 0; i < results.length; i++) { + var entry = results[i]; + if (entry.isDirectory) + entries.push(entry); + } + readEntry(); + }.bind(this)); + }.bind(this); + readEntry(); +}; + +/** + * Updates sub-elements of {@code parentElement} reading {@code DirectoryEntry} + * with calling {@code iterator}. + * + * @param {string} changedDirectryPath The path of the changed directory. + */ +DirectoryItem.prototype.updateItemByPath = function(changedDirectryPath) { + if (changedDirectryPath === this.entry.fullPath) { + this.updateSubDirectories(false /* recursive */); + return; + } + + for (var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + if (PathUtil.isParentPath(item.entry.fullPath, changedDirectryPath)) { + item.updateItemByPath(changedDirectryPath); + break; + } + } +}; + +/** + * Redraw subitems with the latest information. The items are sorted in + * alphabetical order, case insensitive. + * @param {boolean} recursive True if the update is recursively. + * @private + */ +DirectoryItem.prototype.redrawSubDirectoryList_ = function(recursive) { + this.updateSubElementsFromList(recursive); +}; + +/** + * Select the item corresponding to the given {@code entry}. + * @param {DirectoryEntry|Object} entry The entry to be selected. Can be a fake. + */ +DirectoryItem.prototype.selectByEntry = function(entry) { + if (util.isSameEntry(entry, this.entry)) { + this.selected = true; + return; + } + + if (this.searchAndSelectByEntry(entry)) + return; + + // If the path doesn't exist, updates sub directories and tryes again. + this.updateSubDirectories( + false /* recursive */, + this.searchAndSelectByEntry.bind(this, entry)); +}; + +/** + * Executes the assigned action as a drop target. + */ +DirectoryItem.prototype.doDropTargetAction = function() { + this.expanded = true; +}; + +/** + * Executes the assigned action. DirectoryItem performs changeDirectory. + */ +DirectoryItem.prototype.doAction = function() { + if (this.fullPath != this.directoryModel_.getCurrentDirPath()) + this.directoryModel_.changeDirectory(this.fullPath); +}; + +/** + * Sets the context menu for directory tree. + * @param {cr.ui.Menu} menu Menu to be set. + */ +DirectoryItem.prototype.setContextMenu = function(menu) { + if (this.entry && PathUtil.isEligibleForFolderShortcut(this.entry.fullPath)) + cr.ui.contextMenuHandler.setContextMenu(this, menu); +}; + +//////////////////////////////////////////////////////////////////////////////// +// DirectoryTree + +/** + * Tree of directories on the middle bar. This element is also the root of + * items, in other words, this is the parent of the top-level items. + * + * @constructor + * @extends {cr.ui.Tree} + */ +function DirectoryTree() {} + +/** + * Decorates an element. + * @param {HTMLElement} el Element to be DirectoryTree. + * @param {DirectoryModel} directoryModel Current DirectoryModel. + * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system. + */ +DirectoryTree.decorate = function(el, directoryModel, volumeManager) { + el.__proto__ = DirectoryTree.prototype; + (/** @type {DirectoryTree} */ el).decorate(directoryModel, volumeManager); +}; + +DirectoryTree.prototype = { + __proto__: cr.ui.Tree.prototype, + + // DirectoryTree is always expanded. + get expanded() { return true; }, + /** + * @param {boolean} value Not used. + */ + set expanded(value) {}, + + /** + * The DirectoryEntry corresponding to this DirectoryItem. This may be + * a dummy DirectoryEntry. + * @type {DirectoryEntry|Object} + * @override + **/ + get entry() { + return this.dirEntry_; + }, + + /** + * The DirectoryModel this tree corresponds to. + * @type {DirectoryModel} + */ + get directoryModel() { + return this.directoryModel_; + }, + + /** + * The VolumeManager instance of the system. + * @type {VolumeManager} + */ + get volumeManager() { + return this.volumeManager_; + }, +}; + +cr.defineProperty(DirectoryTree, 'contextMenuForSubitems', cr.PropertyKind.JS); + +/** + * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList(). + * + * @param {boolean} recursive True if the all visible sub-directories are + * updated recursively including left arrows. If false, the update walks + * only immediate child directories without arrows. + */ +DirectoryTree.prototype.updateSubElementsFromList = function(recursive) { + DirectoryItemTreeBaseMethods.updateSubElementsFromList.call(this, recursive); +}; + +/** + * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList(). + * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be + * a fake. + * @return {boolean} True if the parent item is found. + */ +DirectoryTree.prototype.searchAndSelectByEntry = function(entry) { + return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry); +}; + +/** + * Decorates an element. + * @param {DirectoryModel} directoryModel Current DirectoryModel. + * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system. + */ +DirectoryTree.prototype.decorate = function(directoryModel, volumeManager) { + cr.ui.Tree.prototype.decorate.call(this); + + this.directoryModel_ = directoryModel; + this.volumeManager_ = volumeManager; + this.entries_ = DirectoryTreeUtil.generateTopLevelEntries(); + + this.fileFilter_ = this.directoryModel_.getFileFilter(); + this.fileFilter_.addEventListener('changed', + this.onFilterChanged_.bind(this)); + + this.directoryModel_.addEventListener('directory-changed', + this.onCurrentDirectoryChanged_.bind(this)); + + // Add a handler for directory change. + this.addEventListener('change', function() { + if (this.selectedItem && + (!this.currentEntry_ || + !util.isSameEntry(this.currentEntry_, this.selectedItem.entry))) { + this.currentEntry_ = this.selectedItem.entry; + this.selectedItem.doAction(); + return; + } + }.bind(this)); + + this.privateOnDirectoryChangedBound_ = + this.onDirectoryContentChanged_.bind(this); + chrome.fileBrowserPrivate.onDirectoryChanged.addListener( + this.privateOnDirectoryChangedBound_); + + this.scrollBar_ = MainPanelScrollBar(); + this.scrollBar_.initialize(this.parentNode, this); + + // Once, draws the list with the fake '/drive/' entry. + this.redraw(false /* recursive */); + // Resolves 'My Drive' entry and replaces the fake with the true one. + this.maybeResolveMyDriveRoot_(function() { + // After the true entry is resolved, draws the list again. + this.redraw(true /* recursive */); + }.bind(this)); +}; + +/** + * Select the item corresponding to the given entry. + * @param {DirectoryEntry|Object} entry The directory entry to be selected. Can + * be a fake. + */ +DirectoryTree.prototype.selectByEntry = function(entry) { + // If the target directory is not in the tree, do nothing. + if (!DirectoryTreeUtil.isEligiblePathForDirectoryTree(entry.fullPath)) + return; + + this.maybeResolveMyDriveRoot_(function() { + if (this.selectedItem && util.isSameEntry(entry, this.selectedItem.entry)) + return; + + if (this.searchAndSelectByEntry(entry)) + return; + + this.selectedItem = null; + this.updateSubDirectories( + false /* recursive */, + // Success callback, failure is not handled. + function() { + if (!this.searchAndSelectByEntry(entry)) + this.selectedItem = null; + }.bind(this)); + }.bind(this)); +}; + +/** + * Resolves the My Drive root's entry, if it is a fake. If the entry is already + * resolved to a DirectoryEntry, completionCallback() will be called + * immediately. + * @param {function()} completionCallback Called when the resolving is + * done (or the entry is already resolved), regardless if it is + * successfully done or not. + * @private + */ +DirectoryTree.prototype.maybeResolveMyDriveRoot_ = function( + completionCallback) { + var myDriveItem = this.items[0]; + if (!util.isFakeEntry(myDriveItem.entry)) { + // The entry is already resolved. Don't need to try again. + completionCallback(); + return; + } + + // The entry is a fake. + this.directoryModel_.resolveDirectory( + myDriveItem.fullPath, + function(entry) { + if (!util.isFakeEntry(entry)) + myDriveItem.dirEntry_ = entry; + + completionCallback(); + }, + completionCallback); +}; + +/** + * Retrieves the latest subdirectories and update them on the tree. + * @param {boolean} recursive True if the update is recursively. + * @param {function()=} opt_successCallback Callback called on success. + * @param {function()=} opt_errorCallback Callback called on error. + */ +DirectoryTree.prototype.updateSubDirectories = function( + recursive, opt_successCallback, opt_errorCallback) { + this.entries_ = DirectoryTreeUtil.generateTopLevelEntries(); + this.redraw(recursive); + if (opt_successCallback) + opt_successCallback(); +}; + +/** + * Redraw the list. + * @param {boolean} recursive True if the update is recursively. False if the + * only root items are updated. + */ +DirectoryTree.prototype.redraw = function(recursive) { + this.updateSubElementsFromList(recursive); +}; + +/** + * Invoked when the filter is changed. + * @private + */ +DirectoryTree.prototype.onFilterChanged_ = function() { + // Returns immediately, if the tree is hidden. + if (this.hidden) + return; + + this.redraw(true /* recursive */); +}; + +/** + * Invoked when a directory is changed. + * @param {!UIEvent} event Event. + * @private + */ +DirectoryTree.prototype.onDirectoryContentChanged_ = function(event) { + if (event.eventType == 'changed') { + // TODO: Use Entry instead of urls. This will stop working once migrating + // to separate file systems. See: crbug.com/325052. + if (!DirectoryTreeUtil.isEligiblePathForDirectoryTree(event.entry.fullPath)) + return; + + var myDriveItem = this.items[0]; + myDriveItem.updateItemByPath(event.entry.fullPath); + } +}; + +/** + * Invoked when the current directory is changed. + * @param {!UIEvent} event Event. + * @private + */ +DirectoryTree.prototype.onCurrentDirectoryChanged_ = function(event) { + this.selectByEntry(event.newDirEntry); +}; + +/** + * Sets the margin height for the transparent preview panel at the bottom. + * @param {number} margin Margin to be set in px. + */ +DirectoryTree.prototype.setBottomMarginForPanel = function(margin) { + this.style.paddingBottom = margin + 'px'; + this.scrollBar_.setBottomMarginForPanel(margin); +}; + +/** + * Updates the UI after the layout has changed. + */ +DirectoryTree.prototype.relayout = function() { + cr.dispatchSimpleEvent(this, 'relayout'); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/drag_selector.js b/chromium/chrome/browser/resources/file_manager/foreground/js/drag_selector.js new file mode 100644 index 00000000000..edc55165dfb --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/drag_selector.js @@ -0,0 +1,244 @@ +// Copyright 2013 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'; + +/** + * Drag selector used on the file list or the grid table. + * TODO(hirono): Support drag selection for grid view. crbug.com/224832 + * @constructor + */ +function DragSelector() { + /** + * Target list of drag selection. + * @type {cr.ui.List} + * @private + */ + this.target_ = null; + + /** + * Border element of drag handle. + * @type {HtmlElement} + * @private + */ + this.border_ = null; + + /** + * Start point of dragging. + * @type {number?} + * @private + */ + this.startX_ = null; + + /** + * Start point of dragging. + * @type {number?} + * @private + */ + this.startY_ = null; + + /** + * Indexes of selected items by dragging at the last update. + * @type {Array.<number>!} + * @private + */ + this.lastSelection_ = []; + + /** + * Indexes of selected items at the start of dragging. + * @type {Array.<number>!} + * @private + */ + this.originalSelection_ = []; + + // Bind handlers to make them removable. + this.onMouseMoveBound_ = this.onMouseMove_.bind(this); + this.onMouseUpBound_ = this.onMouseUp_.bind(this); + + Object.seal(this); +} + +/** + * Flag that shows whether the item is included in the selection or not. + * @enum {number} + * @private + */ +DragSelector.SelectionFlag_ = { + IN_LAST_SELECTION: 1 << 0, + IN_CURRENT_SELECTION: 1 << 1 +}; + +/** + * Obtains the scrolled position in the element of mouse pointer from the mouse + * event. + * + * @param {HTMLElement} element Element that has the scroll bars. + * @param {Event} event The mouse event. + * @return {object} Scrolled position. + */ +DragSelector.getScrolledPosition = function(element, event) { + if (!element.cachedBounds) { + element.cachedBounds = element.getBoundingClientRect(); + if (!element.cachedBounds) + return null; + } + var rect = element.cachedBounds; + return { + x: event.clientX - rect.left + element.scrollLeft, + y: event.clientY - rect.top + element.scrollTop + }; +}; + +/** + * Starts drag selection by reacting dragstart event. + * This function must be called from handlers of dragstart event. + * + * @this {DragSelector} + * @param {cr.ui.List} list List where the drag selection starts. + * @param {Event} event The dragstart event. + */ +DragSelector.prototype.startDragSelection = function(list, event) { + // Precondition check + if (!list.selectionModel_.multiple || this.target_) + return; + + // Set the target of the drag selection + this.target_ = list; + + // Prevent the default action. + event.preventDefault(); + + // Save the start state. + var startPos = DragSelector.getScrolledPosition(list, event); + if (!startPos) + return; + this.startX_ = startPos.x; + this.startY_ = startPos.y; + this.lastSelection_ = []; + this.originalSelection_ = this.target_.selectionModel_.selectedIndexes; + + // Create and add the border element + if (!this.border_) { + this.border_ = this.target_.ownerDocument.createElement('div'); + this.border_.className = 'drag-selection-border'; + } + this.border_.style.left = this.startX_ + 'px'; + this.border_.style.top = this.startY_ + 'px'; + this.border_.style.width = '0'; + this.border_.style.height = '0'; + list.appendChild(this.border_); + + // If no modifier key is pressed, clear the original selection. + if (!event.shiftKey && !event.ctrlKey) + this.target_.selectionModel_.unselectAll(); + + // Register event handlers. + // The handlers are bounded at the constructor. + this.target_.ownerDocument.addEventListener( + 'mousemove', this.onMouseMoveBound_, true); + this.target_.ownerDocument.addEventListener( + 'mouseup', this.onMouseUpBound_, true); +}; + +/** + * Handles the mousemove event. + * @private + * @param {MouseEvent} event The mousemove event. + */ +DragSelector.prototype.onMouseMove_ = function(event) { + // Get the selection bounds. + var pos = DragSelector.getScrolledPosition(this.target_, event); + var borderBounds = { + left: Math.max(Math.min(this.startX_, pos.x), 0), + top: Math.max(Math.min(this.startY_, pos.y), 0), + right: Math.min(Math.max(this.startX_, pos.x), this.target_.scrollWidth), + bottom: Math.min(Math.max(this.startY_, pos.y), this.target_.scrollHeight) + }; + borderBounds.width = borderBounds.right - borderBounds.left; + borderBounds.height = borderBounds.bottom - borderBounds.top; + + // Collect items within the selection rect. + var currentSelection = this.target_.getHitElements( + borderBounds.left, + borderBounds.top, + borderBounds.width, + borderBounds.height); + var pointedElements = this.target_.getHitElements(pos.x, pos.y); + var leadIndex = pointedElements.length ? pointedElements[0] : -1; + + // Diff the selection between currentSelection and this.lastSelection_. + var selectionFlag = []; + for (var i = 0; i < this.lastSelection_.length; i++) { + var index = this.lastSelection_[i]; + // Bit operator can be used for undefined value. + selectionFlag[index] = + selectionFlag[index] | DragSelector.SelectionFlag_.IN_LAST_SELECTION; + } + for (var i = 0; i < currentSelection.length; i++) { + var index = currentSelection[i]; + // Bit operator can be used for undefined value. + selectionFlag[index] = + selectionFlag[index] | DragSelector.SelectionFlag_.IN_CURRENT_SELECTION; + } + + // Update the selection + this.target_.selectionModel_.beginChange(); + for (var name in selectionFlag) { + var index = parseInt(name); + var flag = selectionFlag[name]; + // The flag may be one of followings: + // - IN_LAST_SELECTION | IN_CURRENT_SELECTION + // - IN_LAST_SELECTION + // - IN_CURRENT_SELECTION + // - undefined + + // If the flag equals to (IN_LAST_SELECTION | IN_CURRENT_SELECTION), + // this is included in both the last selection and the current selection. + // We have nothing to do for this item. + + if (flag == DragSelector.SelectionFlag_.IN_LAST_SELECTION) { + // If the flag equals to IN_LAST_SELECTION, + // then the item is included in lastSelection but not in currentSelection. + // Revert the selection state to this.originalSelection_. + this.target_.selectionModel_.setIndexSelected( + index, this.originalSelection_.indexOf(index) != -1); + } else if (flag == DragSelector.SelectionFlag_.IN_CURRENT_SELECTION) { + // If the flag equals to IN_CURRENT_SELECTION, + // this is included in currentSelection but not in lastSelection. + this.target_.selectionModel_.setIndexSelected(index, true); + } + } + if (leadIndex != -1) { + this.target_.selectionModel_.leadIndex = leadIndex; + this.target_.selectionModel_.anchorIndex = leadIndex; + } + this.target_.selectionModel_.endChange(); + this.lastSelection_ = currentSelection; + + // Update the size of border + this.border_.style.left = borderBounds.left + 'px'; + this.border_.style.top = borderBounds.top + 'px'; + this.border_.style.width = borderBounds.width + 'px'; + this.border_.style.height = borderBounds.height + 'px'; +}; + +/** + * Handle the mouseup event. + * @private + * @param {MouseEvent} event The mouseup event. + */ +DragSelector.prototype.onMouseUp_ = function(event) { + this.onMouseMove_(event); + this.target_.removeChild(this.border_); + this.target_.ownerDocument.removeEventListener( + 'mousemove', this.onMouseMoveBound_, true); + this.target_.ownerDocument.removeEventListener( + 'mouseup', this.onMouseUpBound_, true); + cr.dispatchSimpleEvent(this.target_, 'dragselectionend'); + this.target_.cachedBounds = null; + this.target_ = null; + // The target may select an item by reacting to the mouseup event. + // This suppress to the selecting behavior. + event.stopPropagation(); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/drive_banners.js b/chromium/chrome/browser/resources/file_manager/foreground/js/drive_banners.js new file mode 100644 index 00000000000..dc1cd924d7a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/drive_banners.js @@ -0,0 +1,660 @@ +// 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'; + +/** + * Responsible for showing following banners in the file list. + * - WelcomeBanner + * - AuthFailBanner + * @param {DirectoryModel} directoryModel The model. + * @param {VolumeManagerWrapper} volumeManager The manager. + * @param {DOMDocument} document HTML document. + * @param {boolean} showOffers True if we should show offer banners. + * @constructor + */ +function FileListBannerController( + directoryModel, volumeManager, document, showOffers) { + this.directoryModel_ = directoryModel; + this.volumeManager_ = volumeManager; + this.document_ = document; + this.showOffers_ = showOffers; + this.driveEnabled_ = false; + + this.initializeWelcomeBanner_(); + this.privateOnDirectoryChangedBound_ = + this.privateOnDirectoryChanged_.bind(this); + + var handler = this.checkSpaceAndMaybeShowWelcomeBanner_.bind(this); + this.directoryModel_.addEventListener('scan-completed', handler); + this.directoryModel_.addEventListener('rescan-completed', handler); + this.directoryModel_.addEventListener('directory-changed', + this.onDirectoryChanged_.bind(this)); + + this.unmountedPanel_ = this.document_.querySelector('#unmounted-panel'); + this.volumeManager_.volumeInfoList.addEventListener( + 'splice', this.onVolumeInfoListSplice_.bind(this)); + this.volumeManager_.addEventListener('drive-connection-changed', + this.onDriveConnectionChanged_.bind(this)); + + chrome.storage.onChanged.addListener(this.onStorageChange_.bind(this)); + this.welcomeHeaderCounter_ = WELCOME_HEADER_COUNTER_LIMIT; + this.warningDismissedCounter_ = 0; + chrome.storage.local.get([WELCOME_HEADER_COUNTER_KEY, WARNING_DISMISSED_KEY], + function(values) { + this.welcomeHeaderCounter_ = + parseInt(values[WELCOME_HEADER_COUNTER_KEY]) || 0; + this.warningDismissedCounter_ = + parseInt(values[WARNING_DISMISSED_KEY]) || 0; + }.bind(this)); + + this.authFailedBanner_ = + this.document_.querySelector('#drive-auth-failed-warning'); + var authFailedText = this.authFailedBanner_.querySelector('.drive-text'); + authFailedText.innerHTML = util.htmlUnescape(str('DRIVE_NOT_REACHED')); + authFailedText.querySelector('a').addEventListener('click', function(e) { + chrome.fileBrowserPrivate.logoutUserForReauthentication(); + e.preventDefault(); + }); + this.maybeShowAuthFailBanner_(); +} + +/** + * FileListBannerController extends cr.EventTarget. + */ +FileListBannerController.prototype.__proto__ = cr.EventTarget.prototype; + +/** + * Key in localStorage to keep number of times the Drive Welcome + * banner has shown. + */ +var WELCOME_HEADER_COUNTER_KEY = 'driveWelcomeHeaderCounter'; + +// If the warning was dismissed before, this key stores the quota value +// (as of the moment of dismissal). +// If the warning was never dismissed or was reset this key stores 0. +var WARNING_DISMISSED_KEY = 'driveSpaceWarningDismissed'; + +/** + * Maximum times Drive Welcome banner could have shown. + */ +var WELCOME_HEADER_COUNTER_LIMIT = 25; + +/** + * Initializes the banner to promote DRIVE. + * This method must be called before any of showing banner functions, and + * also before registering them as callbacks. + * @private + */ +FileListBannerController.prototype.initializeWelcomeBanner_ = function() { + this.usePromoWelcomeBanner_ = !util.boardIs('x86-mario') && + !util.boardIs('x86-zgb') && + !util.boardIs('x86-alex'); +}; + +/** + * @param {number} value How many times the Drive Welcome header banner + * has shown. + * @private + */ +FileListBannerController.prototype.setWelcomeHeaderCounter_ = function(value) { + var values = {}; + values[WELCOME_HEADER_COUNTER_KEY] = value; + chrome.storage.local.set(values); +}; + +/** + * @param {number} value How many times the low space warning has dismissed. + * @private + */ +FileListBannerController.prototype.setWarningDismissedCounter_ = + function(value) { + var values = {}; + values[WARNING_DISMISSED_KEY] = value; + chrome.storage.local.set(values); +}; + +/** + * chrome.storage.onChanged event handler. + * @param {Object.<string, Object>} changes Changes values. + * @param {string} areaName "local" or "sync". + * @private + */ +FileListBannerController.prototype.onStorageChange_ = function(changes, + areaName) { + if (areaName == 'local' && WELCOME_HEADER_COUNTER_KEY in changes) { + this.welcomeHeaderCounter_ = changes[WELCOME_HEADER_COUNTER_KEY].newValue; + } + if (areaName == 'local' && WARNING_DISMISSED_KEY in changes) { + this.warningDismissedCounter_ = changes[WARNING_DISMISSED_KEY].newValue; + } +}; + +/** + * Invoked when the drive connection status is change in the volume manager. + * @private + */ +FileListBannerController.prototype.onDriveConnectionChanged_ = function() { + this.maybeShowAuthFailBanner_(); +}; + +/** + * @param {string} type 'none'|'page'|'header'. + * @param {string} messageId Resource ID of the message. + * @private + */ +FileListBannerController.prototype.prepareAndShowWelcomeBanner_ = + function(type, messageId) { + this.showWelcomeBanner_(type); + + var container = this.document_.querySelector('.drive-welcome.' + type); + if (container.firstElementChild) + return; // Do not re-create. + + if (!this.document_.querySelector('link[drive-welcome-style]')) { + var style = this.document_.createElement('link'); + style.rel = 'stylesheet'; + style.href = 'foreground/css/drive_welcome.css'; + style.setAttribute('drive-welcome-style', ''); + this.document_.head.appendChild(style); + } + + var wrapper = util.createChild(container, 'drive-welcome-wrapper'); + util.createChild(wrapper, 'drive-welcome-icon'); + + var close = util.createChild(wrapper, 'cr-dialog-close'); + close.addEventListener('click', this.closeWelcomeBanner_.bind(this)); + + var message = util.createChild(wrapper, 'drive-welcome-message'); + + var title = util.createChild(message, 'drive-welcome-title'); + + var text = util.createChild(message, 'drive-welcome-text'); + text.innerHTML = str(messageId); + + var links = util.createChild(message, 'drive-welcome-links'); + + var more; + if (this.usePromoWelcomeBanner_) { + var welcomeTitle = str('DRIVE_WELCOME_TITLE_ALTERNATIVE'); + if (util.boardIs('link')) + welcomeTitle = str('DRIVE_WELCOME_TITLE_ALTERNATIVE_1TB'); + title.textContent = welcomeTitle; + more = util.createChild(links, + 'drive-welcome-button drive-welcome-start', 'a'); + more.textContent = str('DRIVE_WELCOME_CHECK_ELIGIBILITY'); + more.href = str('GOOGLE_DRIVE_REDEEM_URL'); + } else { + title.textContent = str('DRIVE_WELCOME_TITLE'); + more = util.createChild(links, 'plain-link', 'a'); + more.textContent = str('DRIVE_LEARN_MORE'); + more.href = str('GOOGLE_DRIVE_OVERVIEW_URL'); + } + more.tabIndex = '13'; // See: go/filesapp-tabindex. + more.target = '_blank'; + + var dismiss; + if (this.usePromoWelcomeBanner_) + dismiss = util.createChild(links, 'drive-welcome-button'); + else + dismiss = util.createChild(links, 'plain-link'); + + dismiss.classList.add('drive-welcome-dismiss'); + dismiss.textContent = str('DRIVE_WELCOME_DISMISS'); + dismiss.addEventListener('click', this.closeWelcomeBanner_.bind(this)); + + this.previousDirWasOnDrive_ = false; +}; + +/** + * Show or hide the "Low Google Drive space" warning. + * @param {boolean} show True if the box need to be shown. + * @param {Object} sizeStats Size statistics. Should be defined when showing the + * warning. + * @private + */ +FileListBannerController.prototype.showLowDriveSpaceWarning_ = + function(show, sizeStats) { + var box = this.document_.querySelector('#volume-space-warning'); + + // Avoid showing two banners. + // TODO(kaznacheev): Unify the low space warning and the promo header. + if (show) + this.cleanupWelcomeBanner_(); + + if (box.hidden == !show) + return; + + if (this.warningDismissedCounter_) { + if (this.warningDismissedCounter_ == + sizeStats.totalSize && // Quota had not changed + sizeStats.remainingSize / sizeStats.totalSize < 0.15) { + // Since the last dismissal decision the quota has not changed AND + // the user did not free up significant space. Obey the dismissal. + show = false; + } else { + // Forget the dismissal. Warning will be shown again. + this.setWarningDismissedCounter_(0); + } + } + + box.textContent = ''; + if (show) { + var icon = this.document_.createElement('div'); + icon.className = 'drive-icon'; + box.appendChild(icon); + + var text = this.document_.createElement('div'); + text.className = 'drive-text'; + text.textContent = strf('DRIVE_SPACE_AVAILABLE_LONG', + util.bytesToString(sizeStats.remainingSize)); + box.appendChild(text); + + var link = this.document_.createElement('a'); + link.className = 'plain-link'; + link.textContent = str('DRIVE_BUY_MORE_SPACE_LINK'); + link.href = str('GOOGLE_DRIVE_BUY_STORAGE_URL'); + link.target = '_blank'; + box.appendChild(link); + + var close = this.document_.createElement('div'); + close.className = 'cr-dialog-close'; + box.appendChild(close); + close.addEventListener('click', function(total) { + window.localStorage[WARNING_DISMISSED_KEY] = total; + box.hidden = true; + this.requestRelayout_(100); + }.bind(this, sizeStats.totalSize)); + } + + if (box.hidden != !show) { + box.hidden = !show; + this.requestRelayout_(100); + } +}; +/** + * Closes the Drive Welcome banner. + * @private + */ +FileListBannerController.prototype.closeWelcomeBanner_ = function() { + this.cleanupWelcomeBanner_(); + // Stop showing the welcome banner. + this.setWelcomeHeaderCounter_(WELCOME_HEADER_COUNTER_LIMIT); +}; + +/** + * Shows or hides the welcome banner for drive. + * @private + */ +FileListBannerController.prototype.checkSpaceAndMaybeShowWelcomeBanner_ = + function() { + if (!this.isOnCurrentProfileDrive()) { + // We are not on the drive file system. Do not show (close) the welcome + // banner. + this.cleanupWelcomeBanner_(); + this.previousDirWasOnDrive_ = false; + return; + } + + var driveVolume = this.volumeManager_.getCurrentProfileVolumeInfo( + util.VolumeType.DRIVE); + if (this.welcomeHeaderCounter_ >= WELCOME_HEADER_COUNTER_LIMIT || + !driveVolume || driveVolume.error) { + // The banner is already shown enough times or the drive FS is not mounted. + // So, do nothing here. + return; + } + + if (!this.showOffers_) { + // Because it is not necessary to show the offer, set + // |usePromoWelcomeBanner_| false here. Note that it probably should be able + // to do this in the constructor, but there remains non-trivial path, + // which may be causes |usePromoWelcomeBanner_| == true's behavior even + // if |showOffers_| is false. + // TODO(hidehiko): Make sure if it is expected or not, and simplify + // |showOffers_| if possible. + this.usePromoWelcomeBanner_ = false; + } + + // Perform asynchronous tasks in parallel. + var group = new AsyncUtil.Group(); + + // Choose the offer basing on the board name. The default one is 100 GB. + var offerSize = 100; // In GB. + var offerServiceId = 'drive.cros.echo.1'; + + if (util.boardIs('link')) { + offerSize = 1024; // 1 TB. + offerServiceId = 'drive.cros.echo.2'; + } + + // If the offer has been checked, then do not show the promo anymore. + group.add(function(onCompleted) { + chrome.echoPrivate.getOfferInfo(offerServiceId, function(offerInfo) { + // If the offer has not been checked, then an error is raised. + if (!chrome.runtime.lastError) + this.usePromoWelcomeBanner_ = false; + onCompleted(); + }.bind(this)); + }.bind(this)); + + if (this.usePromoWelcomeBanner_) { + // getSizeStats for Drive file system accesses to the server, so we should + // minimize the invocation. + group.add(function(onCompleted) { + chrome.fileBrowserPrivate.getSizeStats( + util.makeFilesystemUrl(this.directoryModel_.getCurrentRootPath()), + function(result) { + if (result && result.totalSize >= offerSize * 1024 * 1024 * 1024) + this.usePromoWelcomeBanner_ = false; + onCompleted(); + }.bind(this)); + }.bind(this)); + } + + group.run(this.maybeShowWelcomeBanner_.bind(this)); +}; + +/** + * Decides which banner should be shown, and show it. This method is designed + * to be called only from checkSpaceAndMaybeShowWelcomeBanner_. + * @private + */ +FileListBannerController.prototype.maybeShowWelcomeBanner_ = function() { + if (this.directoryModel_.getFileList().length == 0 && + this.welcomeHeaderCounter_ == 0) { + // Only show the full page banner if the header banner was never shown. + // Do not increment the counter. + // The timeout below is required because sometimes another + // 'rescan-completed' event arrives shortly with non-empty file list. + setTimeout(function() { + if (this.isOnCurrentProfileDrive() && this.welcomeHeaderCounter_ == 0) { + this.prepareAndShowWelcomeBanner_('page', 'DRIVE_WELCOME_TEXT_LONG'); + } + }.bind(this), 2000); + } else { + // We do not want to increment the counter when the user navigates + // between different directories on Drive, but we increment the counter + // once anyway to prevent the full page banner from showing. + if (!this.previousDirWasOnDrive_ || this.welcomeHeaderCounter_ == 0) { + this.setWelcomeHeaderCounter_(this.welcomeHeaderCounter_ + 1); + this.prepareAndShowWelcomeBanner_('header', 'DRIVE_WELCOME_TEXT_SHORT'); + } + } + this.previousDirWasOnDrive_ = true; +}; + +/** + * @return {boolean} True if current directory is on Drive root of current + * profile. + */ +FileListBannerController.prototype.isOnCurrentProfileDrive = function() { + var entry = this.directoryModel_.getCurrentDirEntry(); + if (!entry || util.isFakeEntry(entry)) + return false; + var locationInfo = this.volumeManager_.getLocationInfo(entry); + return locationInfo && + locationInfo.rootType === RootType.DRIVE && + locationInfo.volumeInfo.profile.isCurrentProfile; +}; + +/** + * Shows the Drive Welcome banner. + * @param {string} type 'page'|'head'|'none'. + * @private + */ +FileListBannerController.prototype.showWelcomeBanner_ = function(type) { + var container = this.document_.querySelector('.dialog-container'); + if (container.getAttribute('drive-welcome') != type) { + container.setAttribute('drive-welcome', type); + this.requestRelayout_(200); // Resize only after the animation is done. + } +}; + +/** + * Update the UI when the current directory changes. + * + * @param {Event} event The directory-changed event. + * @private + */ +FileListBannerController.prototype.onDirectoryChanged_ = function(event) { + var rootVolume = this.volumeManager_.getVolumeInfo(event.newDirEntry); + var previousRootVolume = event.previousDirEntry ? + this.volumeManager_.getVolumeInfo(event.previousDirEntry) : null; + + // Show (or hide) the low space warning. + this.maybeShowLowSpaceWarning_(rootVolume); + + // Add or remove listener to show low space warning, if necessary. + var isLowSpaceWarningTarget = this.isLowSpaceWarningTarget_(rootVolume); + if (isLowSpaceWarningTarget !== + this.isLowSpaceWarningTarget_(previousRootVolume)) { + if (isLowSpaceWarningTarget) { + chrome.fileBrowserPrivate.onDirectoryChanged.addListener( + this.privateOnDirectoryChangedBound_); + } else { + chrome.fileBrowserPrivate.onDirectoryChanged.removeListener( + this.privateOnDirectoryChangedBound_); + } + } + + if (!this.isOnCurrentProfileDrive()) { + this.cleanupWelcomeBanner_(); + this.authFailedBanner_.hidden = true; + } + + this.updateDriveUnmountedPanel_(); + if (this.isOnCurrentProfileDrive()) { + this.unmountedPanel_.classList.remove('retry-enabled'); + this.maybeShowAuthFailBanner_(); + } +}; + +/** + * @param {VolumeInfo} volumeInfo Volume info to be checked. + * @return {boolean} true if the file system specified by |root| is a target + * to show low space warning. Otherwise false. + * @private + */ +FileListBannerController.prototype.isLowSpaceWarningTarget_ = + function(volumeInfo) { + return volumeInfo && + volumeInfo.profile.isCurrentProfile && + (volumeInfo.volumeType === util.VolumeType.DOWNLOADS || + volumeInfo.volumeType === util.VolumeType.DRIVE); +}; + +/** + * Callback which is invoked when the file system has been changed. + * @param {Object} event chrome.fileBrowserPrivate.onDirectoryChanged event. + * @private + */ +FileListBannerController.prototype.privateOnDirectoryChanged_ = function( + event) { + if (!this.directoryModel_.getCurrentDirEntry()) + return; + + var currentDirEntry = this.directoryModel_.getCurrentDirEntry(); + var currentVolume = currentDirEntry && + this.volumeManager_.getVolumeInfo(currentDirEntry); + var eventVolume = this.volumeManager_.getVolumeInfo(event.entry); + if (currentVolume === eventVolume) { + // The file system we are currently on is changed. + // So, check the free space. + this.maybeShowLowSpaceWarning_(currentVolume); + } +}; + +/** + * Shows or hides the low space warning. + * @param {VolumeInfo} volume Type of volume, which we are interested in. + * @private + */ +FileListBannerController.prototype.maybeShowLowSpaceWarning_ = function( + volume) { + // TODO(kaznacheev): Unify the two low space warning. + var threshold = 0; + switch (volume.volumeType) { + case util.VolumeType.DOWNLOADS: + this.showLowDriveSpaceWarning_(false); + threshold = 0.2; + break; + case util.VolumeType.DRIVE: + this.showLowDownloadsSpaceWarning_(false); + threshold = 0.1; + break; + default: + // If the current file system is neither the DOWNLOAD nor the DRIVE, + // just hide the warning. + this.showLowDownloadsSpaceWarning_(false); + this.showLowDriveSpaceWarning_(false); + return; + } + + chrome.fileBrowserPrivate.getSizeStats( + volume.getDisplayRootDirectoryURL(), + function(sizeStats) { + var currentVolume = this.volumeManager_.getVolumeInfo( + this.directoryModel_.getCurrentDirEntry()); + if (volume !== currentVolume) { + // This happens when the current directory is moved during requesting + // the file system size. Just ignore it. + return; + } + // sizeStats is undefined, if some error occurs. + if (!sizeStats || sizeStats.totalSize == 0) + return; + + var remainingRatio = sizeStats.remainingSize / sizeStats.totalSize; + var isLowDiskSpace = remainingRatio < threshold; + if (volume.volumeType === util.VolumeType.DOWNLOADS) + this.showLowDownloadsSpaceWarning_(isLowDiskSpace); + else + this.showLowDriveSpaceWarning_(isLowDiskSpace, sizeStats); + }.bind(this)); +}; + +/** + * removes the Drive Welcome banner. + * @private + */ +FileListBannerController.prototype.cleanupWelcomeBanner_ = function() { + this.showWelcomeBanner_('none'); +}; + +/** + * Notifies the file manager what layout must be recalculated. + * @param {number} delay In milliseconds. + * @private + */ +FileListBannerController.prototype.requestRelayout_ = function(delay) { + var self = this; + setTimeout(function() { + cr.dispatchSimpleEvent(self, 'relayout'); + }, delay); +}; + +/** + * Show or hide the "Low disk space" warning. + * @param {boolean} show True if the box need to be shown. + * @private + */ +FileListBannerController.prototype.showLowDownloadsSpaceWarning_ = + function(show) { + var box = this.document_.querySelector('.downloads-warning'); + + if (box.hidden == !show) return; + + if (show) { + var html = util.htmlUnescape(str('DOWNLOADS_DIRECTORY_WARNING')); + box.innerHTML = html; + var link = box.querySelector('a'); + link.href = str('DOWNLOADS_LOW_SPACE_WARNING_HELP_URL'); + link.target = '_blank'; + } else { + box.innerHTML = ''; + } + + box.hidden = !show; + this.requestRelayout_(100); +}; + +/** + * Creates contents for the DRIVE unmounted panel. + * @private + */ +FileListBannerController.prototype.ensureDriveUnmountedPanelInitialized_ = + function() { + var panel = this.unmountedPanel_; + if (panel.firstElementChild) + return; + + var create = function(parent, tag, className, opt_textContent) { + var div = panel.ownerDocument.createElement(tag); + div.className = className; + div.textContent = opt_textContent || ''; + parent.appendChild(div); + return div; + }; + + var loading = create(panel, 'div', 'loading', str('DRIVE_LOADING')); + var spinnerBox = create(loading, 'div', 'spinner-box'); + create(spinnerBox, 'div', 'spinner'); + create(panel, 'div', 'error', str('DRIVE_CANNOT_REACH')); + + var learnMore = create(panel, 'a', 'learn-more plain-link', + str('DRIVE_LEARN_MORE')); + learnMore.href = str('GOOGLE_DRIVE_ERROR_HELP_URL'); + learnMore.target = '_blank'; +}; + +/** + * Called when volume info list is updated. + * @param {Event} event Splice event data on volume info list. + * @private + */ +FileListBannerController.prototype.onVolumeInfoListSplice_ = function(event) { + var isDriveVolume = function(volumeInfo) { + return volumeInfo.volumeType === util.VolumeType.DRIVE; + }; + if (event.removed.some(isDriveVolume) || event.added.some(isDriveVolume)) + this.updateDriveUnmountedPanel_(); +}; + +/** + * Shows the panel when current directory is DRIVE and it's unmounted. + * Hides it otherwise. The panel shows spinner if DRIVE is mounting or + * an error message if it failed. + * @private + */ +FileListBannerController.prototype.updateDriveUnmountedPanel_ = function() { + var node = this.document_.body; + if (this.isOnCurrentProfileDrive()) { + var driveVolume = this.volumeManager_.getCurrentProfileVolumeInfo( + util.VolumeType.DRIVE); + if (driveVolume && driveVolume.error) { + this.ensureDriveUnmountedPanelInitialized_(); + this.unmountedPanel_.classList.add('retry-enabled'); + } else { + this.unmountedPanel_.classList.remove('retry-enabled'); + } + node.setAttribute('drive', status); + } else { + node.removeAttribute('drive'); + } +}; + +/** + * Updates the visibility of Drive Connection Warning banner, retrieving the + * current connection information. + * @private + */ +FileListBannerController.prototype.maybeShowAuthFailBanner_ = function() { + var connection = this.volumeManager_.getDriveConnectionState(); + var showDriveNotReachedMessage = + this.isOnCurrentProfileDrive() && + connection.type == util.DriveConnectionType.OFFLINE && + connection.reason == util.DriveConnectionReason.NOT_READY; + this.authFailedBanner_.hidden = !showDriveNotReachedMessage; +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/error_counter.js b/chromium/chrome/browser/resources/file_manager/foreground/js/error_counter.js new file mode 100644 index 00000000000..8cec3bba03f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/error_counter.js @@ -0,0 +1,16 @@ +// Copyright 2013 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'; + +/** + * This variable is checked in SelectFileDialogExtensionBrowserTest. + * @type {number} + */ +window.JSErrorCount = 0; + +/** + * Count uncaught exceptions. + */ +window.onerror = function() { window.JSErrorCount++; }; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/error_dialog.js b/chromium/chrome/browser/resources/file_manager/foreground/js/error_dialog.js new file mode 100644 index 00000000000..5dd3545d3c7 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/error_dialog.js @@ -0,0 +1,29 @@ +// Copyright 2013 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'; + +/** + * @param {HTMLElement} parentNode Node to be parent for this dialog. + * @constructor + */ +function ErrorDialog(parentNode) { + cr.ui.dialogs.BaseDialog.call(this, parentNode); +} + +ErrorDialog.prototype = { + __proto__: cr.ui.dialogs.BaseDialog.prototype +}; + +/** + * One-time initialization of DOM. + * @private + */ +ErrorDialog.prototype.initDom_ = function() { + cr.ui.dialogs.BaseDialog.prototype.initDom_.call(this); + this.frame_.classList.add('error-dialog-frame'); + var img = this.document_.createElement('div'); + img.className = 'error-dialog-img'; + this.frame_.insertBefore(img, this.text_); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_grid.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_grid.js new file mode 100644 index 00000000000..b275cd0e9f5 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_grid.js @@ -0,0 +1,302 @@ +// 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'; + +/** + * FileGrid constructor. + * + * Represents grid for the Grid Vew in the File Manager. + * @constructor + * @extends {cr.ui.Grid} + */ + +function FileGrid() { + throw new Error('Use FileGrid.decorate'); +} + +/** + * Thumbnail quality. + * @enum {number} + */ +FileGrid.ThumbnailQuality = { + LOW: 0, + HIGH: 1 +}; + +/** + * Inherits from cr.ui.Grid. + */ +FileGrid.prototype.__proto__ = cr.ui.Grid.prototype; + +/** + * Decorates an HTML element to be a FileGrid. + * @param {HTMLElement} self The grid to decorate. + * @param {MetadataCache} metadataCache Metadata cache to find entries + * metadata. + */ +FileGrid.decorate = function(self, metadataCache) { + cr.ui.Grid.decorate(self); + self.__proto__ = FileGrid.prototype; + self.metadataCache_ = metadataCache; + + self.scrollBar_ = new MainPanelScrollBar(); + self.scrollBar_.initialize(self.parentNode, self); + self.setBottomMarginForPanel(0); + + self.itemConstructor = function(entry) { + var item = self.ownerDocument.createElement('LI'); + FileGrid.Item.decorate(item, entry, self); + return item; + }; + + self.relayoutAggregation_ = + new AsyncUtil.Aggregation(self.relayoutImmediately_.bind(self)); +}; + +/** + * Updates items to reflect metadata changes. + * @param {string} type Type of metadata changed. + * @param {Object.<string, Object>} props Map from entry URLs to metadata props. + */ +FileGrid.prototype.updateListItemsMetadata = function(type, props) { + var boxes = this.querySelectorAll('.img-container'); + for (var i = 0; i < boxes.length; i++) { + var box = boxes[i]; + var entry = this.dataModel.item(this.getListItemAncestor(box)); + if (!entry || !(entry.toURL() in props)) + continue; + + FileGrid.decorateThumbnailBox(box, + entry, + this.metadataCache_, + ThumbnailLoader.FillMode.FIT, + FileGrid.ThumbnailQuality.HIGH); + } +}; + +/** + * Redraws the UI. Skips multiple consecutive calls. + */ +FileGrid.prototype.relayout = function() { + this.relayoutAggregation_.run(); +}; + +/** + * Redraws the UI immediately. + * @private + */ +FileGrid.prototype.relayoutImmediately_ = function() { + this.startBatchUpdates(); + this.columns = 0; + this.redraw(); + this.endBatchUpdates(); + cr.dispatchSimpleEvent(this, 'relayout'); +}; + +/** + * Decorates thumbnail. + * @param {HTMLElement} li List item. + * @param {Entry} entry Entry to render a thumbnail for. + * @param {MetadataCache} metadataCache To retrieve metadata. + */ +FileGrid.decorateThumbnail = function(li, entry, metadataCache) { + li.className = 'thumbnail-item'; + if (entry) + filelist.decorateListItem(li, entry, metadataCache); + + var frame = li.ownerDocument.createElement('div'); + frame.className = 'thumbnail-frame'; + li.appendChild(frame); + + var box = li.ownerDocument.createElement('div'); + if (entry) { + FileGrid.decorateThumbnailBox(box, + entry, + metadataCache, + ThumbnailLoader.FillMode.AUTO, + FileGrid.ThumbnailQuality.HIGH); + } + frame.appendChild(box); + + var bottom = li.ownerDocument.createElement('div'); + bottom.className = 'thumbnail-bottom'; + bottom.appendChild(filelist.renderFileNameLabel(li.ownerDocument, entry)); + frame.appendChild(bottom); +}; + +/** + * Decorates the box containing a centered thumbnail image. + * + * @param {HTMLDivElement} box Box to decorate. + * @param {Entry} entry Entry which thumbnail is generating for. + * @param {MetadataCache} metadataCache To retrieve metadata. + * @param {ThumbnailLoader.FillMode} fillMode Fill mode. + * @param {FileGrid.ThumbnailQuality} quality Thumbnail quality. + * @param {function(HTMLElement)=} opt_imageLoadCallback Callback called when + * the image has been loaded before inserting it into the DOM. + */ +FileGrid.decorateThumbnailBox = function( + box, entry, metadataCache, fillMode, quality, opt_imageLoadCallback) { + box.className = 'img-container'; + if (entry.isDirectory) { + box.setAttribute('generic-thumbnail', 'folder'); + if (opt_imageLoadCallback) + setTimeout(opt_imageLoadCallback, 0, null /* callback parameter */); + return; + } + + var metadataTypes = 'thumbnail|filesystem'; + + if (FileType.isOnDrive(entry)) { + metadataTypes += '|drive'; + } else { + // TODO(dgozman): If we ask for 'media' for a Drive file we fall into an + // infinite loop. + metadataTypes += '|media'; + } + + // Drive provides high quality thumbnails via USE_EMBEDDED, however local + // images usually provide very tiny thumbnails, therefore USE_EMBEDDE can't + // be used to obtain high quality output. + var useEmbedded; + switch (quality) { + case FileGrid.ThumbnailQuality.LOW: + useEmbedded = ThumbnailLoader.UseEmbedded.USE_EMBEDDED; + break; + case FileGrid.ThumbnailQuality.HIGH: + useEmbedded = FileType.isOnDrive(entry) ? + ThumbnailLoader.UseEmbedded.USE_EMBEDDED : + ThumbnailLoader.UseEmbedded.NO_EMBEDDED; + break; + } + + metadataCache.get(entry, metadataTypes, + function(metadata) { + new ThumbnailLoader(entry.toURL(), + ThumbnailLoader.LoaderType.IMAGE, + metadata, + undefined, // opt_mediaType + useEmbedded). + load(box, + fillMode, + ThumbnailLoader.OptimizationMode.DISCARD_DETACHED, + opt_imageLoadCallback); + }); +}; + +/** + * Item for the Grid View. + * @constructor + */ +FileGrid.Item = function() { + throw new Error(); +}; + +/** + * Inherits from cr.ui.ListItem. + */ +FileGrid.Item.prototype.__proto__ = cr.ui.ListItem.prototype; + +Object.defineProperty(FileGrid.Item.prototype, 'label', { + /** + * @this {FileGrid.Item} + * @return {string} Label of the item. + */ + get: function() { + return this.querySelector('filename-label').textContent; + } +}); + +/** + * @param {Element} li List item element. + * @param {Entry} entry File entry. + * @param {FileGrid} grid Owner. + */ +FileGrid.Item.decorate = function(li, entry, grid) { + li.__proto__ = FileGrid.Item.prototype; + FileGrid.decorateThumbnail(li, entry, grid.metadataCache_, true); + + // Override the default role 'listitem' to 'option' to match the parent's + // role (listbox). + li.setAttribute('role', 'option'); +}; + +/** + * Sets the margin height for the transparent preview panel at the bottom. + * @param {number} margin Margin to be set in px. + */ +FileGrid.prototype.setBottomMarginForPanel = function(margin) { + // +20 bottom margin is needed to match the bottom margin size with the + // margin between its items. + this.style.paddingBottom = (margin + 20) + 'px'; + this.scrollBar_.setBottomMarginForPanel(margin); +}; + +/** + * Obtains if the drag selection should be start or not by referring the mouse + * event. + * @param {MouseEvent} event Drag start event. + * @return {boolean} True if the mouse is hit to the background of the list. + */ +FileGrid.prototype.shouldStartDragSelection = function(event) { + var pos = DragSelector.getScrolledPosition(this, event); + return this.getHitElements(pos.x, pos.y).length == 0; +}; + +/** + * Obtains the column/row index that the coordinate points. + * @param {number} coordinate Vertical/horizontal coodinate value that points + * column/row. + * @param {number} step Length from a column/row to the next one. + * @param {number} threshold Threshold that determinds whether 1 offset is added + * to the return value or not. This is used in order to handle the margin of + * column/row. + * @return {number} Index of hit column/row. + * @private + */ +FileGrid.prototype.getHitIndex_ = function(coordinate, step, threshold) { + var index = ~~(coordinate / step); + return (coordinate % step >= threshold) ? index + 1 : index; +}; + +/** + * Obtains the index list of elements that are hit by the point or the + * rectangle. + * + * We should match its argument interface with FileList.getHitElements. + * + * @param {number} x X coordinate value. + * @param {number} y Y coordinate value. + * @param {=number} opt_width Width of the coordinate. + * @param {=number} opt_height Height of the coordinate. + * @return {Array.<number>} Index list of hit elements. + */ +FileGrid.prototype.getHitElements = function(x, y, opt_width, opt_height) { + var currentSelection = []; + var right = x + (opt_width || 0); + var bottom = y + (opt_height || 0); + var itemMetrics = this.measureItem(); + var horizontalStartIndex = this.getHitIndex_( + x, itemMetrics.width, itemMetrics.width - itemMetrics.marginRight); + var horizontalEndIndex = Math.min(this.columns, this.getHitIndex_( + right, itemMetrics.width, itemMetrics.marginLeft)); + var verticalStartIndex = this.getHitIndex_( + y, itemMetrics.height, itemMetrics.height - itemMetrics.bottom); + var verticalEndIndex = this.getHitIndex_( + bottom, itemMetrics.height, itemMetrics.marginTop); + for (var verticalIndex = verticalStartIndex; + verticalIndex < verticalEndIndex; + verticalIndex++) { + var indexBase = this.getFirstItemInRow(verticalIndex); + for (var horizontalIndex = horizontalStartIndex; + horizontalIndex < horizontalEndIndex; + horizontalIndex++) { + var index = indexBase + horizontalIndex; + if (0 <= index && index < this.dataModel.length) + currentSelection.push(index); + } + } + return currentSelection; +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_manager.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_manager.js new file mode 100644 index 00000000000..201a4f6ad2d --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_manager.js @@ -0,0 +1,3688 @@ +// 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'; + +/** + * FileManager constructor. + * + * FileManager objects encapsulate the functionality of the file selector + * dialogs, as well as the full screen file manager application (though the + * latter is not yet implemented). + * + * @constructor + */ +function FileManager() { + this.initializeQueue_ = new AsyncUtil.Group(); + + /** + * Current list type. + * @type {ListType} + * @private + */ + this.listType_ = null; + + /** + * Whether to suppress the focus moving or not. + * This is used to filter out focusing by mouse. + * @type {boolean} + * @private + */ + this.suppressFocus_ = false; + + /** + * SelectionHandler. + * @type {SelectionHandler} + * @private + */ + this.selectionHandler_ = null; +} + +/** + * Maximum delay in milliseconds for updating thumbnails in the bottom panel + * to mitigate flickering. If images load faster then the delay they replace + * old images smoothly. On the other hand we don't want to keep old images + * too long. + * + * @type {number} + * @const + */ +FileManager.THUMBNAIL_SHOW_DELAY = 100; + +FileManager.prototype = { + __proto__: cr.EventTarget.prototype, + get directoryModel() { + return this.directoryModel_; + }, + get navigationList() { + return this.navigationList_; + }, + get document() { + return this.document_; + }, + get fileTransferController() { + return this.fileTransferController_; + }, + get backgroundPage() { + return this.backgroundPage_; + }, + get volumeManager() { + return this.volumeManager_; + } +}; + +/** + * Unload the file manager. + * Used by background.js (when running in the packaged mode). + */ +function unload() { + fileManager.onBeforeUnload_(); + fileManager.onUnload_(); +} + +/** + * List of dialog types. + * + * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except + * FULL_PAGE which is specific to this code. + * + * @enum {string} + */ +var DialogType = { + SELECT_FOLDER: 'folder', + SELECT_UPLOAD_FOLDER: 'upload-folder', + SELECT_SAVEAS_FILE: 'saveas-file', + SELECT_OPEN_FILE: 'open-file', + SELECT_OPEN_MULTI_FILE: 'open-multi-file', + FULL_PAGE: 'full-page' +}; + +/** + * @param {string} type Dialog type. + * @return {boolean} Whether the type is modal. + */ +DialogType.isModal = function(type) { + return type == DialogType.SELECT_FOLDER || + type == DialogType.SELECT_UPLOAD_FOLDER || + type == DialogType.SELECT_SAVEAS_FILE || + type == DialogType.SELECT_OPEN_FILE || + type == DialogType.SELECT_OPEN_MULTI_FILE; +}; + +/** + * @param {string} type Dialog type. + * @return {boolean} Whether the type is open dialog. + */ +DialogType.isOpenDialog = function(type) { + return type == DialogType.SELECT_OPEN_FILE || + type == DialogType.SELECT_OPEN_MULTI_FILE; +}; + +/** + * @param {string} type Dialog type. + * @return {boolean} Whether the type is folder selection dialog. + */ +DialogType.isFolderDialog = function(type) { + return type == DialogType.SELECT_FOLDER || + type == DialogType.SELECT_UPLOAD_FOLDER; +}; + +/** + * Bottom margin of the list and tree for transparent preview panel. + * @const + */ +var BOTTOM_MARGIN_FOR_PREVIEW_PANEL_PX = 52; + +// Anonymous "namespace". +(function() { + + // Private variables and helper functions. + + /** + * Number of milliseconds in a day. + */ + var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; + + /** + * Some UI elements react on a single click and standard double click handling + * leads to confusing results. We ignore a second click if it comes soon + * after the first. + */ + var DOUBLE_CLICK_TIMEOUT = 200; + + /** + * Update the element to display the information about remaining space for + * the storage. + * @param {!Element} spaceInnerBar Block element for a percentage bar + * representing the remaining space. + * @param {!Element} spaceInfoLabel Inline element to contain the message. + * @param {!Element} spaceOuterBar Block element around the percentage bar. + */ + var updateSpaceInfo = function( + sizeStatsResult, spaceInnerBar, spaceInfoLabel, spaceOuterBar) { + spaceInnerBar.removeAttribute('pending'); + if (sizeStatsResult) { + var sizeStr = util.bytesToString(sizeStatsResult.remainingSize); + spaceInfoLabel.textContent = strf('SPACE_AVAILABLE', sizeStr); + + var usedSpace = + sizeStatsResult.totalSize - sizeStatsResult.remainingSize; + spaceInnerBar.style.width = + (100 * usedSpace / sizeStatsResult.totalSize) + '%'; + + spaceOuterBar.hidden = false; + } else { + spaceOuterBar.hidden = true; + spaceInfoLabel.textContent = str('FAILED_SPACE_INFO'); + } + }; + + // Public statics. + + FileManager.ListType = { + DETAIL: 'detail', + THUMBNAIL: 'thumb' + }; + + FileManager.prototype.initPreferences_ = function(callback) { + var group = new AsyncUtil.Group(); + + // DRIVE preferences should be initialized before creating DirectoryModel + // to rebuild the roots list. + group.add(this.getPreferences_.bind(this)); + + // Get startup preferences. + this.viewOptions_ = {}; + group.add(function(done) { + util.platform.getPreference(this.startupPrefName_, function(value) { + // Load the global default options. + try { + this.viewOptions_ = JSON.parse(value); + } catch (ignore) {} + // Override with window-specific options. + if (window.appState && window.appState.viewOptions) { + for (var key in window.appState.viewOptions) { + if (window.appState.viewOptions.hasOwnProperty(key)) + this.viewOptions_[key] = window.appState.viewOptions[key]; + } + } + done(); + }.bind(this)); + }.bind(this)); + + // Get the command line option. + group.add(function(done) { + chrome.commandLinePrivate.hasSwitch( + 'file-manager-show-checkboxes', function(flag) { + this.showCheckboxes_ = flag; + done(); + }.bind(this)); + }.bind(this)); + + // TODO(yoshiki): Remove the flag when the feature is launched. + this.enableExperimentalWebstoreIntegration_ = true; + + group.run(callback); + }; + + /** + * One time initialization for the file system and related things. + * + * @param {function()} callback Completion callback. + * @private + */ + FileManager.prototype.initFileSystemUI_ = function(callback) { + this.table_.startBatchUpdates(); + this.grid_.startBatchUpdates(); + + this.initFileList_(); + this.setupCurrentDirectory_(); + + // PyAuto tests monitor this state by polling this variable + this.__defineGetter__('workerInitialized_', function() { + return this.metadataCache_.isInitialized(); + }.bind(this)); + + this.initDateTimeFormatters_(); + + var self = this; + + // Get the 'allowRedeemOffers' preference before launching + // FileListBannerController. + this.getPreferences_(function(pref) { + /** @type {boolean} */ + var showOffers = pref['allowRedeemOffers']; + self.bannersController_ = new FileListBannerController( + self.directoryModel_, self.volumeManager_, self.document_, + showOffers); + self.bannersController_.addEventListener('relayout', + self.onResize_.bind(self)); + }); + + var dm = this.directoryModel_; + dm.addEventListener('directory-changed', + this.onDirectoryChanged_.bind(this)); + dm.addEventListener('begin-update-files', function() { + self.currentList_.startBatchUpdates(); + }); + dm.addEventListener('end-update-files', function() { + self.restoreItemBeingRenamed_(); + self.currentList_.endBatchUpdates(); + }); + dm.addEventListener('scan-started', this.onScanStarted_.bind(this)); + dm.addEventListener('scan-completed', this.onScanCompleted_.bind(this)); + dm.addEventListener('scan-failed', this.onScanCancelled_.bind(this)); + dm.addEventListener('scan-cancelled', this.onScanCancelled_.bind(this)); + dm.addEventListener('scan-updated', this.onScanUpdated_.bind(this)); + dm.addEventListener('rescan-completed', + this.onRescanCompleted_.bind(this)); + + this.directoryTree_.addEventListener('change', function() { + this.ensureDirectoryTreeItemNotBehindPreviewPanel_(); + }.bind(this)); + + var stateChangeHandler = + this.onPreferencesChanged_.bind(this); + chrome.fileBrowserPrivate.onPreferencesChanged.addListener( + stateChangeHandler); + stateChangeHandler(); + + var driveConnectionChangedHandler = + this.onDriveConnectionChanged_.bind(this); + this.volumeManager_.addEventListener('drive-connection-changed', + driveConnectionChangedHandler); + driveConnectionChangedHandler(); + + // Set the initial focus. + this.refocus(); + // Set it as a fallback when there is no focus. + this.document_.addEventListener('focusout', function(e) { + setTimeout(function() { + // When there is no focus, the active element is the <body>. + if (this.document_.activeElement == this.document_.body) + this.refocus(); + }.bind(this), 0); + }.bind(this)); + + this.initDataTransferOperations_(); + + this.initContextMenus_(); + this.initCommands_(); + + this.updateFileTypeFilter_(); + + this.selectionHandler_.onFileSelectionChanged(); + + this.table_.endBatchUpdates(); + this.grid_.endBatchUpdates(); + + callback(); + }; + + /** + * If |item| in the directory tree is behind the preview panel, scrolls up the + * parent view and make the item visible. This should be called when: + * - the selected item is changed in the directory tree. + * - the visibility of the the preview panel is changed. + * + * @private + */ + FileManager.prototype.ensureDirectoryTreeItemNotBehindPreviewPanel_ = + function() { + var selectedSubTree = this.directoryTree_.selectedItem; + if (!selectedSubTree) + return; + var item = selectedSubTree.rowElement; + var parentView = this.directoryTree_; + + var itemRect = item.getBoundingClientRect(); + if (!itemRect) + return; + + var listRect = parentView.getBoundingClientRect(); + if (!listRect) + return; + + var previewPanel = this.dialogDom_.querySelector('.preview-panel'); + var previewPanelRect = previewPanel.getBoundingClientRect(); + var panelHeight = previewPanelRect ? previewPanelRect.height : 0; + + var itemBottom = itemRect.bottom; + var listBottom = listRect.bottom - panelHeight; + + if (itemBottom > listBottom) { + var scrollOffset = itemBottom - listBottom; + parentView.scrollTop += scrollOffset; + } + }; + + /** + * @private + */ + FileManager.prototype.initDateTimeFormatters_ = function() { + var use12hourClock = !this.preferences_['use24hourClock']; + this.table_.setDateTimeFormat(use12hourClock); + }; + + /** + * @private + */ + FileManager.prototype.initDataTransferOperations_ = function() { + this.fileOperationManager_ = FileOperationManagerWrapper.getInstance( + this.backgroundPage_); + + // 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; + + // TODO(hidehiko): Extract FileOperationManager related code from + // FileManager to simplify it. + this.onCopyProgressBound_ = this.onCopyProgress_.bind(this); + this.fileOperationManager_.addEventListener( + 'copy-progress', this.onCopyProgressBound_); + + this.onEntryChangedBound_ = this.onEntryChanged_.bind(this); + this.fileOperationManager_.addEventListener( + 'entry-changed', this.onEntryChangedBound_); + + var controller = this.fileTransferController_ = + new FileTransferController(this.document_, + this.fileOperationManager_, + this.metadataCache_, + this.directoryModel_); + controller.attachDragSource(this.table_.list); + controller.attachFileListDropTarget(this.table_.list); + controller.attachDragSource(this.grid_); + controller.attachFileListDropTarget(this.grid_); + controller.attachTreeDropTarget(this.directoryTree_); + controller.attachNavigationListDropTarget(this.navigationList_, true); + controller.attachCopyPasteHandlers(); + controller.addEventListener('selection-copied', + this.blinkSelection.bind(this)); + controller.addEventListener('selection-cut', + this.blinkSelection.bind(this)); + }; + + /** + * One-time initialization of context menus. + * @private + */ + FileManager.prototype.initContextMenus_ = function() { + this.fileContextMenu_ = this.dialogDom_.querySelector('#file-context-menu'); + cr.ui.Menu.decorate(this.fileContextMenu_); + + cr.ui.contextMenuHandler.setContextMenu(this.grid_, this.fileContextMenu_); + cr.ui.contextMenuHandler.setContextMenu(this.table_.querySelector('.list'), + this.fileContextMenu_); + cr.ui.contextMenuHandler.setContextMenu( + this.document_.querySelector('.drive-welcome.page'), + this.fileContextMenu_); + + this.rootsContextMenu_ = + this.dialogDom_.querySelector('#roots-context-menu'); + cr.ui.Menu.decorate(this.rootsContextMenu_); + this.navigationList_.setContextMenu(this.rootsContextMenu_); + + this.directoryTreeContextMenu_ = + this.dialogDom_.querySelector('#directory-tree-context-menu'); + cr.ui.Menu.decorate(this.directoryTreeContextMenu_); + this.directoryTree_.contextMenuForSubitems = this.directoryTreeContextMenu_; + + this.textContextMenu_ = + this.dialogDom_.querySelector('#text-context-menu'); + cr.ui.Menu.decorate(this.textContextMenu_); + + this.gearButton_ = this.dialogDom_.querySelector('#gear-button'); + this.gearButton_.addEventListener('menushow', + this.refreshRemainingSpace_.bind(this, + false /* Without loading caption. */)); + this.dialogDom_.querySelector('#gear-menu').menuItemSelector = + 'menuitem, hr'; + cr.ui.decorate(this.gearButton_, cr.ui.MenuButton); + + if (this.dialogType == DialogType.FULL_PAGE) { + // This is to prevent the buttons from stealing focus on mouse down. + var preventFocus = function(event) { + event.preventDefault(); + }; + + var maximizeButton = this.dialogDom_.querySelector('#maximize-button'); + maximizeButton.addEventListener('click', this.onMaximize.bind(this)); + maximizeButton.addEventListener('mousedown', preventFocus); + + var closeButton = this.dialogDom_.querySelector('#close-button'); + closeButton.addEventListener('click', this.onClose.bind(this)); + closeButton.addEventListener('mousedown', preventFocus); + } + + this.syncButton.checkable = true; + this.hostedButton.checkable = true; + this.detailViewButton_.checkable = true; + this.thumbnailViewButton_.checkable = true; + + if (util.platform.runningInBrowser()) { + // Suppresses the default context menu. + this.dialogDom_.addEventListener('contextmenu', function(e) { + e.preventDefault(); + e.stopPropagation(); + }); + } + }; + + FileManager.prototype.onMaximize = function() { + var appWindow = chrome.app.window.current(); + if (appWindow.isMaximized()) + appWindow.restore(); + else + appWindow.maximize(); + }; + + FileManager.prototype.onClose = function() { + window.close(); + }; + + /** + * One-time initialization of commands. + * @private + */ + FileManager.prototype.initCommands_ = function() { + this.commandHandler = new CommandHandler(this); + + // 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.dialogDom_.querySelectorAll( + 'input[type=text], input[type=search], textarea'); + for (var i = 0; i < inputs.length; i++) { + cr.ui.contextMenuHandler.setContextMenu(inputs[i], this.textContextMenu_); + this.registerInputCommands_(inputs[i]); + } + + cr.ui.contextMenuHandler.setContextMenu(this.renameInput_, + this.textContextMenu_); + this.registerInputCommands_(this.renameInput_); + this.document_.addEventListener('command', + this.setNoHover_.bind(this, true)); + }; + + /** + * 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.initializeQueue_.add(this.initGeneral_.bind(this), [], 'initGeneral'); + this.initializeQueue_.add(this.initBackgroundPage_.bind(this), + [], 'initBackgroundPage'); + this.initializeQueue_.add(this.initPreferences_.bind(this), + ['initGeneral'], 'initPreferences'); + this.initializeQueue_.add(this.initVolumeManager_.bind(this), + ['initGeneral', 'initBackgroundPage'], + 'initVolumeManager'); + + this.initializeQueue_.run(); + }; + + FileManager.prototype.initializeUI = function(dialogDom, callback) { + this.dialogDom_ = dialogDom; + this.document_ = this.dialogDom_.ownerDocument; + + this.initializeQueue_.add( + this.initEssentialUI_.bind(this), + ['initGeneral', 'initBackgroundPage'], + 'initEssentialUI'); + this.initializeQueue_.add(this.initAdditionalUI_.bind(this), + ['initEssentialUI'], 'initAdditionalUI'); + this.initializeQueue_.add( + this.initFileSystemUI_.bind(this), + ['initAdditionalUI', 'initPreferences'], 'initFileSystemUI'); + + // Run again just in case if all pending closures have completed and the + // queue has stopped and monitor the completion. + this.initializeQueue_.run(callback); + }; + + /** + * Initializes general purpose basic things, which are used by other + * initializing methods. + * + * @param {function()} callback Completion callback. + * @private + */ + FileManager.prototype.initGeneral_ = function(callback) { + // Initialize the application state. + if (window.appState) { + this.params_ = window.appState.params || {}; + this.defaultPath = window.appState.defaultPath; + } else { + this.params_ = location.search ? + JSON.parse(decodeURIComponent(location.search.substr(1))) : + {}; + this.defaultPath = this.params_.defaultPath; + } + + // Initialize the member variables that depend this.params_. + this.dialogType = this.params_.type || DialogType.FULL_PAGE; + this.startupPrefName_ = 'file-manager-' + this.dialogType; + this.fileTypes_ = this.params_.typeList || []; + + callback(); + }; + + /** + * Initialize the background page. + * @param {function()} callback Completion callback. + * @private + */ + FileManager.prototype.initBackgroundPage_ = function(callback) { + chrome.runtime.getBackgroundPage(function(backgroundPage) { + this.backgroundPage_ = backgroundPage; + this.backgroundPage_.background.ready(function() { + loadTimeData.data = this.backgroundPage_.background.stringData; + callback(); + }.bind(this)); + }.bind(this)); + }; + + /** + * Initializes the VolumeManager instance. + * @param {function()} callback Completion callback. + * @private + */ + FileManager.prototype.initVolumeManager_ = function(callback) { + // Auto resolving to local path does not work for folders (e.g., dialog for + // loading unpacked extensions). + var noLocalPathResolution = DialogType.isFolderDialog(this.params_.type); + + // If this condition is false, VolumeManagerWrapper hides all drive + // related event and data, even if Drive is enabled on preference. + // In other words, even if Drive is disabled on preference but 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. + var driveEnabled = + !noLocalPathResolution || !this.params_.shouldReturnLocalPath; + this.volumeManager_ = new VolumeManagerWrapper( + driveEnabled, this.backgroundPage_); + callback(); + }; + + /** + * One time initialization of the Files.app's essential UI elements. These + * elements will be shown to the user. Only visible elements should be + * initialized here. Any heavy operation should be avoided. Files.app's + * window is shown at the end of this routine. + * + * @param {function()} callback Completion callback. + * @private + */ + FileManager.prototype.initEssentialUI_ = function(callback) { + // Optional list of file types. + 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. + this.metadataCache_ = MetadataCache.createFull(); + + // Create the root view of FileManager. + this.ui_ = new FileManagerUI(this.dialogDom_, this.dialogType); + this.fileTypeSelector_ = this.ui_.fileTypeSelector; + this.okButton_ = this.ui_.okButton; + this.cancelButton_ = this.ui_.cancelButton; + + // Show the window as soon as the UI pre-initialization is done. + if (this.dialogType == DialogType.FULL_PAGE && + !util.platform.runningInBrowser()) { + chrome.app.window.current().show(); + setTimeout(callback, 100); // Wait until the animation is finished. + } else { + callback(); + } + }; + + /** + * One-time initialization of dialogs. + * @private + */ + FileManager.prototype.initDialogs_ = function() { + // Initialize the dialog. + this.ui_.initDialogs(); + FileManagerDialogBase.setFileManager(this); + + // Obtains the dialog instances from FileManagerUI. + // TODO(hirono): Remove the properties from the FileManager class. + this.error = this.ui_.errorDialog; + this.alert = this.ui_.alertDialog; + this.confirm = this.ui_.confirmDialog; + this.prompt = this.ui_.promptDialog; + this.shareDialog_ = this.ui_.shareDialog; + this.defaultTaskPicker = this.ui_.defaultTaskPicker; + this.suggestAppsDialog = this.ui_.suggestAppsDialog; + }; + + /** + * 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. + * + * @param {function()} callback Completion callback. + * @private + */ + FileManager.prototype.initAdditionalUI_ = function(callback) { + this.initDialogs_(); + this.ui_.initAdditionalUI(); + + this.dialogDom_.addEventListener('drop', function(e) { + // Prevent opening an URL by dropping it onto the page. + e.preventDefault(); + }); + + this.dialogDom_.addEventListener('click', + this.onExternalLinkClick_.bind(this)); + // Cache nodes we'll be manipulating. + var dom = this.dialogDom_; + + this.filenameInput_ = dom.querySelector('#filename-input-box input'); + this.taskItems_ = dom.querySelector('#tasks'); + + this.table_ = dom.querySelector('.detail-table'); + this.grid_ = dom.querySelector('.thumbnail-grid'); + this.spinner_ = dom.querySelector('#list-container > .spinner-layer'); + this.showSpinner_(true); + + // Check the option to hide the selecting checkboxes. + this.table_.showCheckboxes = this.showCheckboxes_; + + var fullPage = this.dialogType == DialogType.FULL_PAGE; + FileTable.decorate(this.table_, this.metadataCache_, fullPage); + FileGrid.decorate(this.grid_, this.metadataCache_); + + this.previewPanel_ = new PreviewPanel( + dom.querySelector('.preview-panel'), + DialogType.isOpenDialog(this.dialogType) ? + PreviewPanel.VisibilityType.ALWAYS_VISIBLE : + PreviewPanel.VisibilityType.AUTO, + this.metadataCache_, + this.volumeManager_); + this.previewPanel_.addEventListener( + PreviewPanel.Event.VISIBILITY_CHANGE, + this.onPreviewPanelVisibilityChange_.bind(this)); + this.previewPanel_.initialize(); + + this.previewPanel_.breadcrumbs.addEventListener( + 'pathclick', this.onBreadcrumbClick_.bind(this)); + + // Initialize progress center panel. + this.progressCenterPanel_ = new ProgressCenterPanel( + dom.querySelector('#progress-center')); + this.backgroundPage_.background.progressCenter.addPanel( + this.progressCenterPanel_); + + this.document_.addEventListener('keydown', this.onKeyDown_.bind(this)); + + // This capturing event is only used to distinguish focusing using + // keyboard from focusing using mouse. + this.document_.addEventListener('mousedown', function() { + this.suppressFocus_ = true; + }.bind(this), true); + + this.renameInput_ = this.document_.createElement('input'); + this.renameInput_.className = 'rename'; + + this.renameInput_.addEventListener( + 'keydown', this.onRenameInputKeyDown_.bind(this)); + this.renameInput_.addEventListener( + 'blur', this.onRenameInputBlur_.bind(this)); + + // TODO(hirono): Rename the handler after creating the DialogFooter class. + this.filenameInput_.addEventListener( + 'input', this.onFilenameInputInput_.bind(this)); + this.filenameInput_.addEventListener( + 'keydown', this.onFilenameInputKeyDown_.bind(this)); + this.filenameInput_.addEventListener( + 'focus', this.onFilenameInputFocus_.bind(this)); + + this.listContainer_ = this.dialogDom_.querySelector('#list-container'); + this.listContainer_.addEventListener( + 'keydown', this.onListKeyDown_.bind(this)); + this.listContainer_.addEventListener( + 'keypress', this.onListKeyPress_.bind(this)); + this.listContainer_.addEventListener( + 'mousemove', this.onListMouseMove_.bind(this)); + + this.okButton_.addEventListener('click', this.onOk_.bind(this)); + this.onCancelBound_ = this.onCancel_.bind(this); + this.cancelButton_.addEventListener('click', this.onCancelBound_); + + this.decorateSplitter( + this.dialogDom_.querySelector('#navigation-list-splitter')); + this.decorateSplitter( + this.dialogDom_.querySelector('#middlebar-splitter')); + + this.dialogContainer_ = this.dialogDom_.querySelector('.dialog-container'); + + this.syncButton = this.dialogDom_.querySelector('#drive-sync-settings'); + this.syncButton.addEventListener('click', this.onDrivePrefClick_.bind( + this, 'cellularDisabled', false /* not inverted */)); + + this.hostedButton = this.dialogDom_.querySelector('#drive-hosted-settings'); + this.hostedButton.addEventListener('click', this.onDrivePrefClick_.bind( + this, 'hostedFilesDisabled', true /* inverted */)); + + this.detailViewButton_ = + this.dialogDom_.querySelector('#detail-view'); + this.detailViewButton_.addEventListener('activate', + this.onDetailViewButtonClick_.bind(this)); + + this.thumbnailViewButton_ = + this.dialogDom_.querySelector('#thumbnail-view'); + this.thumbnailViewButton_.addEventListener('activate', + this.onThumbnailViewButtonClick_.bind(this)); + + cr.ui.ComboButton.decorate(this.taskItems_); + this.taskItems_.showMenu = function(shouldSetFocus) { + // Prevent the empty menu from opening. + if (!this.menu.length) + return; + cr.ui.ComboButton.prototype.showMenu.call(this, shouldSetFocus); + }; + this.taskItems_.addEventListener('select', + this.onTaskItemClicked_.bind(this)); + + this.dialogDom_.ownerDocument.defaultView.addEventListener( + 'resize', this.onResize_.bind(this)); + + this.filePopup_ = null; + + this.searchBoxWrapper_ = this.ui_.searchBox.element; + this.searchBox_ = this.ui_.searchBox.inputElement; + this.searchBox_.addEventListener( + 'input', this.onSearchBoxUpdate_.bind(this)); + this.ui_.searchBox.clearButton.addEventListener( + 'click', this.onSearchClearButtonClick_.bind(this)); + + this.lastSearchQuery_ = ''; + + this.autocompleteList_ = this.ui_.searchBox.autocompleteList; + this.autocompleteList_.requestSuggestions = + this.requestAutocompleteSuggestions_.bind(this); + + // Instead, open the suggested item when Enter key is pressed or + // mouse-clicked. + this.autocompleteList_.handleEnterKeydown = function(event) { + this.openAutocompleteSuggestion_(); + this.lastAutocompleteQuery_ = ''; + this.autocompleteList_.suggestions = []; + }.bind(this); + this.autocompleteList_.addEventListener('mousedown', function(event) { + this.openAutocompleteSuggestion_(); + this.lastAutocompleteQuery_ = ''; + this.autocompleteList_.suggestions = []; + }.bind(this)); + + this.defaultActionMenuItem_ = + this.dialogDom_.querySelector('#default-action'); + + this.openWithCommand_ = + this.dialogDom_.querySelector('#open-with'); + + this.driveBuyMoreStorageCommand_ = + this.dialogDom_.querySelector('#drive-buy-more-space'); + + this.defaultActionMenuItem_.addEventListener('click', + this.dispatchSelectionAction_.bind(this)); + + this.initFileTypeFilter_(); + + util.addIsFocusedMethod(); + + // Populate the static localized strings. + i18nTemplate.process(this.document_, loadTimeData); + + // Arrange the file list. + this.table_.normalizeColumns(); + this.table_.redraw(); + + callback(); + }; + + /** + * @private + */ + FileManager.prototype.onBreadcrumbClick_ = function(event) { + // TODO(hirono): Use directoryModel#changeDirectoryEntry after implementing + // it. + if (event.entry === RootType.DRIVE_SHARED_WITH_ME) + this.directoryModel_.changeDirectory(RootDirectory.DRIVE_SHARED_WITH_ME); + else + this.directoryModel_.changeDirectory(event.entry.fullPath); + }; + + /** + * Constructs table and grid (heavy operation). + * @private + **/ + FileManager.prototype.initFileList_ = function() { + // Always sharing the data model between the detail/thumb views confuses + // them. Instead we maintain this bogus data model, and hook it up to the + // view that is not in use. + this.emptyDataModel_ = new cr.ui.ArrayDataModel([]); + this.emptySelectionModel_ = new cr.ui.ListSelectionModel(); + + 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; + + this.fileFilter_ = new FileFilter( + this.metadataCache_, + false /* Don't show dot files by default. */); + + this.fileWatcher_ = new FileWatcher(this.metadataCache_); + this.fileWatcher_.addEventListener( + 'watcher-metadata-changed', + this.onWatcherMetadataChanged_.bind(this)); + + this.directoryModel_ = new DirectoryModel( + singleSelection, + this.fileFilter_, + this.fileWatcher_, + this.metadataCache_, + this.volumeManager_); + + this.folderShortcutsModel_ = new FolderShortcutsDataModel(); + + this.selectionHandler_ = new FileSelectionHandler(this); + + var dataModel = this.directoryModel_.getFileList(); + + this.table_.setupCompareFunctions(dataModel); + + dataModel.addEventListener('permuted', + this.updateStartupPrefs_.bind(this)); + + this.directoryModel_.getFileListSelection().addEventListener('change', + this.selectionHandler_.onFileSelectionChanged.bind( + this.selectionHandler_)); + + this.initList_(this.grid_); + this.initList_(this.table_.list); + + var fileListFocusBound = this.onFileListFocus_.bind(this); + var fileListBlurBound = this.onFileListBlur_.bind(this); + + this.table_.list.addEventListener('focus', fileListFocusBound); + this.grid_.addEventListener('focus', fileListFocusBound); + + this.table_.list.addEventListener('blur', fileListBlurBound); + this.grid_.addEventListener('blur', fileListBlurBound); + + var dragStartBound = this.onDragStart_.bind(this); + this.table_.list.addEventListener('dragstart', dragStartBound); + this.grid_.addEventListener('dragstart', dragStartBound); + + var dragEndBound = this.onDragEnd_.bind(this); + this.table_.list.addEventListener('dragend', dragEndBound); + this.grid_.addEventListener('dragend', dragEndBound); + // This event is published by DragSelector because drag end event is not + // published at the end of drag selection. + this.table_.list.addEventListener('dragselectionend', dragEndBound); + this.grid_.addEventListener('dragselectionend', dragEndBound); + + // TODO(mtomasz, yoshiki): Create navigation list earlier, and here just + // attach the directory model. + this.initNavigationList_(); + + this.table_.addEventListener('column-resize-end', + this.updateStartupPrefs_.bind(this)); + + // Restore preferences. + this.directoryModel_.sortFileList( + this.viewOptions_.sortField || 'modificationTime', + this.viewOptions_.sortDirection || 'desc'); + if (this.viewOptions_.columns) { + var cm = this.table_.columnModel; + for (var i = 0; i < cm.totalSize; i++) { + if (this.viewOptions_.columns[i] > 0) + cm.setWidth(i, this.viewOptions_.columns[i]); + } + } + this.setListType(this.viewOptions_.listType || FileManager.ListType.DETAIL); + + this.textSearchState_ = {text: '', date: new Date()}; + this.closeOnUnmount_ = (this.params_.action == 'auto-open'); + + if (this.closeOnUnmount_) { + this.volumeManager_.addEventListener('externally-unmounted', + this.onExternallyUnmounted_.bind(this)); + } + + // Update metadata to change 'Today' and 'Yesterday' dates. + var today = new Date(); + today.setHours(0); + today.setMinutes(0); + today.setSeconds(0); + today.setMilliseconds(0); + setTimeout(this.dailyUpdateModificationTime_.bind(this), + today.getTime() + MILLISECONDS_IN_DAY - Date.now() + 1000); + }; + + /** + * @private + */ + FileManager.prototype.initNavigationList_ = function() { + this.directoryTree_ = this.dialogDom_.querySelector('#directory-tree'); + DirectoryTree.decorate(this.directoryTree_, this.directoryModel_); + + this.navigationList_ = this.dialogDom_.querySelector('#navigation-list'); + NavigationList.decorate(this.navigationList_, + this.volumeManager_, + this.directoryModel_); + this.navigationList_.fileManager = this; + this.navigationList_.dataModel = new NavigationListModel( + this.volumeManager_, this.folderShortcutsModel_); + }; + + /** + * @private + */ + FileManager.prototype.updateMiddleBarVisibility_ = function() { + var entry = this.directoryModel_.getCurrentDirEntry(); + if (!entry) + return; + + var driveVolume = this.volumeManager_.getVolumeInfo(entry); + var visible = + DirectoryTreeUtil.isEligiblePathForDirectoryTree(entry.fullPath) && + driveVolume && !driveVolume.error; + this.dialogDom_. + querySelector('.dialog-middlebar-contents').hidden = !visible; + this.dialogDom_.querySelector('#middlebar-splitter').hidden = !visible; + this.onResize_(); + }; + + /** + * @private + */ + FileManager.prototype.updateStartupPrefs_ = function() { + var sortStatus = this.directoryModel_.getFileList().sortStatus; + var prefs = { + sortField: sortStatus.field, + sortDirection: sortStatus.direction, + columns: [], + listType: this.listType_ + }; + var cm = this.table_.columnModel; + for (var i = 0; i < cm.totalSize; i++) { + prefs.columns.push(cm.getWidth(i)); + } + // Save the global default. + util.platform.setPreference(this.startupPrefName_, JSON.stringify(prefs)); + + // Save the window-specific preference. + if (window.appState) { + window.appState.viewOptions = prefs; + util.saveAppState(); + } + }; + + FileManager.prototype.refocus = function() { + var targetElement; + if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) + targetElement = this.filenameInput_; + else + targetElement = this.currentList_; + + // Hack: if the tabIndex is disabled, we can assume a modal dialog is + // shown. Focus to a button on the dialog instead. + if (!targetElement.hasAttribute('tabIndex') || targetElement.tabIndex == -1) + targetElement = document.querySelector('button:not([tabIndex="-1"])'); + + if (targetElement) + targetElement.focus(); + }; + + /** + * File list focus handler. Used to select the top most element on the list + * if nothing was selected. + * + * @private + */ + FileManager.prototype.onFileListFocus_ = function() { + // Do not select default item if focused using mouse. + if (this.suppressFocus_) + return; + + var selection = this.getSelection(); + if (!selection || selection.totalCount != 0) + return; + + this.directoryModel_.selectIndex(0); + }; + + /** + * File list blur handler. + * + * @private + */ + FileManager.prototype.onFileListBlur_ = function() { + this.suppressFocus_ = false; + }; + + /** + * Index of selected item in the typeList of the dialog params. + * + * @return {number} 1-based index of selected type or 0 if no type selected. + * @private + */ + FileManager.prototype.getSelectedFilterIndex_ = function() { + var index = Number(this.fileTypeSelector_.selectedIndex); + if (index < 0) // Nothing selected. + return 0; + if (this.params_.includeAllFiles) // Already 1-based. + return index; + return index + 1; // Convert to 1-based; + }; + + FileManager.prototype.setListType = function(type) { + if (type && type == this.listType_) + return; + + this.table_.list.startBatchUpdates(); + this.grid_.startBatchUpdates(); + + // TODO(dzvorygin): style.display and dataModel setting order shouldn't + // cause any UI bugs. Currently, the only right way is first to set display + // style and only then set dataModel. + + if (type == FileManager.ListType.DETAIL) { + this.table_.dataModel = this.directoryModel_.getFileList(); + this.table_.selectionModel = this.directoryModel_.getFileListSelection(); + this.table_.hidden = false; + this.grid_.hidden = true; + this.grid_.selectionModel = this.emptySelectionModel_; + this.grid_.dataModel = this.emptyDataModel_; + this.table_.hidden = false; + /** @type {cr.ui.List} */ + this.currentList_ = this.table_.list; + this.detailViewButton_.setAttribute('checked', ''); + this.thumbnailViewButton_.removeAttribute('checked'); + this.detailViewButton_.setAttribute('disabled', ''); + this.thumbnailViewButton_.removeAttribute('disabled'); + } else if (type == FileManager.ListType.THUMBNAIL) { + this.grid_.dataModel = this.directoryModel_.getFileList(); + this.grid_.selectionModel = this.directoryModel_.getFileListSelection(); + this.grid_.hidden = false; + this.table_.hidden = true; + this.table_.selectionModel = this.emptySelectionModel_; + this.table_.dataModel = this.emptyDataModel_; + this.grid_.hidden = false; + /** @type {cr.ui.List} */ + this.currentList_ = this.grid_; + this.thumbnailViewButton_.setAttribute('checked', ''); + this.detailViewButton_.removeAttribute('checked'); + this.thumbnailViewButton_.setAttribute('disabled', ''); + this.detailViewButton_.removeAttribute('disabled'); + } else { + throw new Error('Unknown list type: ' + type); + } + + this.listType_ = type; + this.updateStartupPrefs_(); + this.onResize_(); + + this.table_.list.endBatchUpdates(); + this.grid_.endBatchUpdates(); + }; + + /** + * Initialize the file list table or grid. + * + * @param {cr.ui.List} list The list. + * @private + */ + FileManager.prototype.initList_ = function(list) { + // Overriding the default role 'list' to 'listbox' for better accessibility + // on ChromeOS. + list.setAttribute('role', 'listbox'); + list.addEventListener('click', this.onDetailClick_.bind(this)); + list.id = 'file-list'; + }; + + /** + * @private + */ + FileManager.prototype.onCopyProgress_ = function(event) { + if (event.reason == 'ERROR' && + event.error.code == util.FileOperationErrorType.FILESYSTEM_ERROR && + event.error.data.toDrive && + event.error.data.code == FileError.QUOTA_EXCEEDED_ERR) { + this.alert.showHtml( + strf('DRIVE_SERVER_OUT_OF_SPACE_HEADER'), + strf('DRIVE_SERVER_OUT_OF_SPACE_MESSAGE', + decodeURIComponent( + event.error.data.sourceFileUrl.split('/').pop()), + str('GOOGLE_DRIVE_BUY_STORAGE_URL'))); + } + }; + + /** + * Handler of file manager operations. Called when an entry has been + * changed. + * This updates directory model to reflect operation result immediately (not + * waiting for directory update event). Also, preloads thumbnails for the + * images of new entries. + * See also FileOperationManager.EventRouter. + * + * @param {Event} event An event for the entry change. + * @private + */ + FileManager.prototype.onEntryChanged_ = function(event) { + var kind = event.kind; + var entry = event.entry; + this.directoryModel_.onEntryChanged(kind, entry); + this.selectionHandler_.onFileSelectionChanged(); + + if (kind == util.EntryChangedKind.CREATE && FileType.isImage(entry)) { + // Preload a thumbnail if the new copied entry an image. + var metadata = entry.getMetadata(function(metadata) { + var url = entry.toURL(); + var thumbnailLoader_ = new ThumbnailLoader( + url, + ThumbnailLoader.LoaderType.CANVAS, + metadata, + undefined, // Media type. + FileType.isOnDrive(url) ? + ThumbnailLoader.UseEmbedded.USE_EMBEDDED : + ThumbnailLoader.UseEmbedded.NO_EMBEDDED, + 10); // Very low priority. + thumbnailLoader_.loadDetachedImage(function(success) {}); + }); + } + }; + + /** + * Fills the file type list or hides it. + * @private + */ + FileManager.prototype.initFileTypeFilter_ = function() { + if (this.params_.includeAllFiles) { + var option = this.document_.createElement('option'); + option.innerText = str('ALL_FILES_FILTER'); + this.fileTypeSelector_.appendChild(option); + option.value = 0; + } + + for (var i = 0; i < this.fileTypes_.length; i++) { + var fileType = this.fileTypes_[i]; + var option = this.document_.createElement('option'); + var description = fileType.description; + if (!description) { + // See if all the extensions in the group have the same description. + for (var j = 0; j != fileType.extensions.length; j++) { + var currentDescription = + FileType.getTypeString('.' + fileType.extensions[j]); + if (!description) // Set the first time. + description = currentDescription; + else if (description != currentDescription) { + // No single description, fall through to the extension list. + description = null; + break; + } + } + + if (!description) + // Convert ['jpg', 'png'] to '*.jpg, *.png'. + description = fileType.extensions.map(function(s) { + return '*.' + s; + }).join(', '); + } + option.innerText = description; + + option.value = i + 1; + + if (fileType.selected) + option.selected = true; + + this.fileTypeSelector_.appendChild(option); + } + + var options = this.fileTypeSelector_.querySelectorAll('option'); + if (options.length >= 2) { + // There is in fact no choice, show the selector. + this.fileTypeSelector_.hidden = false; + + this.fileTypeSelector_.addEventListener('change', + this.updateFileTypeFilter_.bind(this)); + } + }; + + /** + * Filters file according to the selected file type. + * @private + */ + FileManager.prototype.updateFileTypeFilter_ = function() { + this.fileFilter_.removeFilter('fileType'); + var selectedIndex = this.getSelectedFilterIndex_(); + if (selectedIndex > 0) { // Specific filter selected. + var regexp = new RegExp('.*(' + + this.fileTypes_[selectedIndex - 1].extensions.join('|') + ')$', 'i'); + var filter = function(entry) { + return entry.isDirectory || regexp.test(entry.name); + }; + this.fileFilter_.addFilter('fileType', filter); + } + }; + + /** + * Resize details and thumb views to fit the new window size. + * @private + */ + FileManager.prototype.onResize_ = function() { + if (this.listType_ == FileManager.ListType.THUMBNAIL) + this.grid_.relayout(); + else + this.table_.relayout(); + + // May not be available during initialization. + if (this.directoryTree_) + this.directoryTree_.relayout(); + + // TODO(mtomasz, yoshiki): Initialize navigation list earlier, before + // file system is available. + if (this.navigationList_) + this.navigationList_.redraw(); + + this.ui_.searchBox.updateSizeRelatedStyle(); + + this.previewPanel_.breadcrumbs.truncate(); + }; + + /** + * Handles local metadata changes in the currect directory. + * @param {Event} event Change event. + * @private + */ + FileManager.prototype.onWatcherMetadataChanged_ = function(event) { + this.updateMetadataInUI_( + event.metadataType, event.entries, event.properties); + }; + + /** + * Resize details and thumb views to fit the new window size. + * @private + */ + FileManager.prototype.onPreviewPanelVisibilityChange_ = function() { + // This method may be called on initialization. Some object may not be + // initialized. + + var panelHeight = this.previewPanel_.visible ? + this.previewPanel_.height : 0; + if (this.grid_) + this.grid_.setBottomMarginForPanel(panelHeight); + if (this.table_) + this.table_.setBottomMarginForPanel(panelHeight); + + if (this.directoryTree_) { + this.directoryTree_.setBottomMarginForPanel(panelHeight); + this.ensureDirectoryTreeItemNotBehindPreviewPanel_(); + } + }; + + /** + * Invoked when the drag is started on the list or the grid. + * @private + */ + FileManager.prototype.onDragStart_ = function() { + // On open file dialog, the preview panel is always shown. + if (DialogType.isOpenDialog(this.dialogType)) + return; + this.previewPanel_.visibilityType = + PreviewPanel.VisibilityType.ALWAYS_HIDDEN; + }; + + /** + * Invoked when the drag is ended on the list or the grid. + * @private + */ + FileManager.prototype.onDragEnd_ = function() { + // On open file dialog, the preview panel is always shown. + if (DialogType.isOpenDialog(this.dialogType)) + return; + this.previewPanel_.visibilityType = PreviewPanel.VisibilityType.AUTO; + }; + + /** + * Restores current directory and may be a selected item after page load (or + * reload) or popping a state (after click on back/forward). defaultPath + * primarily is used with save/open dialogs. + * Default path may also contain a file name. Freshly opened file manager + * window has neither. + * + * @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)); + + // Resolve the default path. + var defaultFullPath; + var candidateFullPath; + var candidateEntry; + queue.run(function(callback) { + // Cancel this sequence if the current directory has already changed. + if (tracker.hasChanged) { + callback(); + return; + } + + // Resolve the absolute path in case only the file name or an empty string + // is passed. + if (!this.defaultPath) { + defaultFullPath = PathUtil.DEFAULT_MOUNT_POINT; + } else if (this.defaultPath.indexOf('/') === -1) { + // Path is a file name. + defaultFullPath = PathUtil.DEFAULT_MOUNT_POINT + '/' + this.defaultPath; + } else { + defaultFullPath = this.defaultPath; + } + + // If Drive is disabled but the path points to Drive's entry, fallback to + // DEFAULT_MOUNT_POINT. + if (PathUtil.isDriveBasedPath(defaultFullPath) && + !this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE)) { + candidateFullPath = PathUtil.DEFAULT_MOUNT_POINT + '/' + + PathUtil.basename(defaultFullPath); + } else { + candidateFullPath = defaultFullPath; + } + + // If the path points a fake entry, use the entry directly. + var fakeEntries = DirectoryModel.FAKE_DRIVE_SPECIAL_SEARCH_ENTRIES; + for (var i = 0; i < fakeEntries.length; i++) { + if (candidateFullPath === fakeEntries[i].fullPath) { + candidateEntry = fakeEntries[i]; + callback(); + return; + } + } + + // Convert the path to the directory entry and an optional selection + // entry. + // TODO(hirono): There may be a race here. The path on Drive, may not + // be available yet. + this.volumeManager_.resolveAbsolutePath(candidateFullPath, + function(inEntry) { + candidateEntry = inEntry; + callback(); + }, function() { + callback(); + }); + }.bind(this)); + + // Check the obtained entry. + var nextCurrentDirEntry; + var selectionEntry = null; + var suggestedName = null; + var error = null; + queue.run(function(callback) { + // Cancel this sequence if the current directory has already changed. + if (tracker.hasChanged) { + callback(); + return; + } + + if (candidateEntry) { + // The entry is directory. Use it. + if (candidateEntry.isDirectory) { + nextCurrentDirEntry = candidateEntry; + callback(); + return; + } + // The entry exists, but it is not a directory. Therefore use a + // parent. + candidateEntry.getParent(function(parentEntry) { + nextCurrentDirEntry = parentEntry; + selectionEntry = candidateEntry; + callback(); + }, function() { + error = new Error('Unable to resolve parent for: ' + + candidateEntry.fullPath); + callback(); + }); + return; + } + + // If the entry doesn't exist, most probably because the path contains a + // suggested name. Therefore try to open its parent. However, the parent + // may also not exist. In such situation, fallback. + var pathNodes = candidateFullPath.split('/'); + var baseName = pathNodes.pop(); + var parentPath = pathNodes.join('/'); + this.volumeManager_.resolveAbsolutePath( + parentPath, + function(parentEntry) { + nextCurrentDirEntry = parentEntry; + suggestedName = baseName; + callback(); + }, + callback); // In case of an error, continue. + }.bind(this)); + + // If the directory is not set at this stage, fallback to the default + // mount point. + queue.run(function(callback) { + // Cancel this sequence if the current directory has already changed, + // or the next current directory is already set. + if (tracker.hasChanged || nextCurrentDirEntry) { + callback(); + return; + } + this.volumeManager_.resolveAbsolutePath( + PathUtil.DEFAULT_MOUNT_POINT, + function(fallbackEntry) { + nextCurrentDirEntry = fallbackEntry; + callback(); + }, + function() { + // Fallback directory not available? Throw an error. + error = new Error('Unable to resolve the fallback directory: ' + + PathUtil.DEFAULT_MOUNT_POINT); + callback(); + }); + }.bind(this)); + + queue.run(function(callback) { + // Check error. + if (error) { + callback(); + throw error; + } + // Check directory change. + tracker.stop(); + if (tracker.hasChanged) { + callback(); + return; + } + // Finish setup current directory. + this.finishSetupCurrentDirectory_( + nextCurrentDirEntry, selectionEntry, suggestedName); + 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). + if (util.isFakeEntry(directoryEntry)) { + this.directoryModel_.specialSearch(directoryEntry.fullPath, ''); + } else { + 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 path points to a file we might + // have to invoke a task after selecting it. + if (this.params_.action == 'select') + return; + + var task = null; + if (opt_suggestedName) { + // Non-existent file or a directory. + if (this.params_.gallery) { + // Reloading while the Gallery is open with empty or multiple + // selection. Open the Gallery when the directory is scanned. + task = function() { + new FileTasks(this, this.params_).openGallery([]); + }.bind(this); + } + } else if (opt_selectionEntry) { + // There is a file to be selected. It means, that we are recovering + // the Files app. + // We call the appropriate methods of FileTasks directly as we do + // not need any of the preparations that |execute| method does. + // TODO(mtomasz): Change Entry.fullPath to Entry. + var mediaType = FileType.getMediaType(opt_selectionEntry.fullPath); + if (mediaType == 'image' || mediaType == 'video') { + task = function() { + // TODO(mtomasz): Replace the url with an entry. + new FileTasks(this, this.params_).openGallery([opt_selectionEntry]); + }.bind(this); + } else if (mediaType == 'archive') { + task = function() { + new FileTasks(this, this.params_).mountArchives( + [opt_selectionEntry]); + }.bind(this); + } + } + + // If there is a task to be run, run it after the scan is completed. + if (task) { + var listener = function() { + this.directoryModel_.removeEventListener( + 'scan-completed', listener); + task(); + }.bind(this); + this.directoryModel_.addEventListener('scan-completed', listener); + } + } else if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) { + this.filenameInput_.value = opt_suggestedName || ''; + this.selectDefaultPathInFilenameInput_(); + } + }; + + /** + * Unmounts device. + * @param {string} path Path to a volume to unmount. + */ + FileManager.prototype.unmountVolume = function(path) { + var onError = function(error) { + this.alert.showHtml('', str('UNMOUNT_FAILED')); + }; + this.volumeManager_.unmount(path, function() {}, onError.bind(this)); + }; + + /** + * @private + */ + FileManager.prototype.refreshCurrentDirectoryMetadata_ = function() { + var entries = this.directoryModel_.getFileList().slice(); + var directoryEntry = this.directoryModel_.getCurrentDirEntry(); + if (!directoryEntry) + return; + // We don't pass callback here. When new metadata arrives, we have an + // observer registered to update the UI. + + // TODO(dgozman): refresh content metadata only when modificationTime + // changed. + var isFakeEntry = util.isFakeEntry(directoryEntry); + var getEntries = (isFakeEntry ? [] : [directoryEntry]).concat(entries); + if (!isFakeEntry) + this.metadataCache_.clearRecursively(directoryEntry, '*'); + this.metadataCache_.get(getEntries, 'filesystem', null); + + if (this.isOnDrive()) + this.metadataCache_.get(getEntries, 'drive', null); + + var visibleItems = this.currentList_.items; + var visibleEntries = []; + for (var i = 0; i < visibleItems.length; i++) { + var index = this.currentList_.getIndexOfListItem(visibleItems[i]); + var entry = this.directoryModel_.getFileList().item(index); + // The following check is a workaround for the bug in list: sometimes item + // does not have listIndex, and therefore is not found in the list. + if (entry) visibleEntries.push(entry); + } + this.metadataCache_.get(visibleEntries, 'thumbnail', null); + }; + + /** + * @private + */ + FileManager.prototype.dailyUpdateModificationTime_ = function() { + var fileList = this.directoryModel_.getFileList(); + var entries = []; + for (var i = 0; i < fileList.length; i++) { + entries.push(fileList.item(i)); + } + this.metadataCache_.get( + entries, + 'filesystem', + this.updateMetadataInUI_.bind(this, 'filesystem', entries)); + + setTimeout(this.dailyUpdateModificationTime_.bind(this), + MILLISECONDS_IN_DAY); + }; + + /** + * @param {string} type Type of metadata changed. + * @param {Array.<Entry>} entries Array of entries. + * @param {Object.<string, Object>} props Map from entry URLs to metadata + * props. + * @private + */ + FileManager.prototype.updateMetadataInUI_ = function( + type, entries, properties) { + if (this.listType_ == FileManager.ListType.DETAIL) + this.table_.updateListItemsMetadata(type, properties); + else + this.grid_.updateListItemsMetadata(type, properties); + // TODO: update bottom panel thumbnails. + }; + + /** + * Restore the item which is being renamed while refreshing the file list. Do + * nothing if no item is being renamed or such an item disappeared. + * + * While refreshing file list it gets repopulated with new file entries. + * There is not a big difference whether DOM items stay the same or not. + * Except for the item that the user is renaming. + * + * @private + */ + FileManager.prototype.restoreItemBeingRenamed_ = function() { + if (!this.isRenamingInProgress()) + return; + + var dm = this.directoryModel_; + var leadIndex = dm.getFileListSelection().leadIndex; + if (leadIndex < 0) + return; + + var leadEntry = dm.getFileList().item(leadIndex); + if (this.renameInput_.currentEntry.fullPath != leadEntry.fullPath) + return; + + var leadListItem = this.findListItemForNode_(this.renameInput_); + if (this.currentList_ == this.table_.list) { + this.table_.updateFileMetadata(leadListItem, leadEntry); + } + this.currentList_.restoreLeadItem(leadListItem); + }; + + /** + * @return {boolean} True if the current directory content is from Google + * Drive. + */ + FileManager.prototype.isOnDrive = function() { + var rootType = this.directoryModel_.getCurrentRootType(); + return rootType === RootType.DRIVE || + rootType === RootType.DRIVE_SHARED_WITH_ME || + rootType === RootType.DRIVE_RECENT || + rootType === RootType.DRIVE_OFFLINE; + }; + + /** + * Overrides default handling for clicks on hyperlinks. + * In a packaged apps links with targer='_blank' open in a new tab by + * default, other links do not open at all. + * + * @param {Event} event Click event. + * @private + */ + FileManager.prototype.onExternalLinkClick_ = function(event) { + if (event.target.tagName != 'A' || !event.target.href) + return; + + if (this.dialogType != DialogType.FULL_PAGE) + this.onCancel_(); + }; + + /** + * Task combobox handler. + * + * @param {Object} event Event containing task which was clicked. + * @private + */ + FileManager.prototype.onTaskItemClicked_ = function(event) { + var selection = this.getSelection(); + if (!selection.tasks) return; + + if (event.item.task) { + // Task field doesn't exist on change-default dropdown item. + selection.tasks.execute(event.item.task.taskId); + } else { + var extensions = []; + + for (var i = 0; i < selection.entries.length; i++) { + var match = /\.(\w+)$/g.exec(selection.entries[i].toURL()); + if (match) { + var ext = match[1].toUpperCase(); + if (extensions.indexOf(ext) == -1) { + extensions.push(ext); + } + } + } + + var format = ''; + + if (extensions.length == 1) { + format = extensions[0]; + } + + // Change default was clicked. We should open "change default" dialog. + selection.tasks.showTaskPicker(this.defaultTaskPicker, + loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM'), + strf('CHANGE_DEFAULT_CAPTION', format), + this.onDefaultTaskDone_.bind(this)); + } + }; + + /** + * Sets the given task as default, when this task is applicable. + * + * @param {Object} task Task to set as default. + * @private + */ + FileManager.prototype.onDefaultTaskDone_ = function(task) { + // TODO(dgozman): move this method closer to tasks. + var selection = this.getSelection(); + chrome.fileBrowserPrivate.setDefaultTask( + task.taskId, + util.entriesToURLs(selection.entries), + selection.mimeTypes); + selection.tasks = new FileTasks(this); + selection.tasks.init(selection.entries, selection.mimeTypes); + selection.tasks.display(this.taskItems_); + this.refreshCurrentDirectoryMetadata_(); + this.selectionHandler_.onFileSelectionChanged(); + }; + + /** + * @private + */ + FileManager.prototype.onPreferencesChanged_ = function() { + var self = this; + this.getPreferences_(function(prefs) { + self.initDateTimeFormatters_(); + self.refreshCurrentDirectoryMetadata_(); + + if (prefs.cellularDisabled) + self.syncButton.setAttribute('checked', ''); + else + self.syncButton.removeAttribute('checked'); + + if (self.hostedButton.hasAttribute('checked') != + prefs.hostedFilesDisabled && self.isOnDrive()) { + self.directoryModel_.rescan(); + } + + if (!prefs.hostedFilesDisabled) + self.hostedButton.setAttribute('checked', ''); + else + self.hostedButton.removeAttribute('checked'); + }, + true /* refresh */); + }; + + FileManager.prototype.onDriveConnectionChanged_ = function() { + var connection = this.volumeManager_.getDriveConnectionState(); + if (this.commandHandler) + this.commandHandler.updateAvailability(); + if (this.dialogContainer_) + this.dialogContainer_.setAttribute('connection', connection.type); + this.shareDialog_.hideWithResult(ShareDialog.Result.NETWORK_ERROR); + this.suggestAppsDialog.onDriveConnectionChanged(connection.type); + }; + + /** + * Get the metered status of Drive connection. + * + * @return {boolean} Returns true if drive should limit the traffic because + * the connection is metered and the 'disable-sync-on-metered' setting is + * enabled. Otherwise, returns false. + */ + FileManager.prototype.isDriveOnMeteredConnection = function() { + var connection = this.volumeManager_.getDriveConnectionState(); + return connection.type == util.DriveConnectionType.METERED; + }; + + /** + * Get the online/offline status of drive. + * + * @return {boolean} Returns true if the connection is offline. Otherwise, + * returns false. + */ + FileManager.prototype.isDriveOffline = function() { + var connection = this.volumeManager_.getDriveConnectionState(); + return connection.type == util.DriveConnectionType.OFFLINE; + }; + + FileManager.prototype.isOnReadonlyDirectory = function() { + return this.directoryModel_.isReadOnly(); + }; + + /** + * @param {Event} Unmount event. + * @private + */ + FileManager.prototype.onExternallyUnmounted_ = function(event) { + if (event.mountPath == this.directoryModel_.getCurrentRootPath()) { + if (this.closeOnUnmount_) { + // If the file manager opened automatically when a usb drive inserted, + // user have never changed current volume (that implies the current + // directory is still on the device) then close this window. + window.close(); + } + } + }; + + /** + * Show a modal-like file viewer/editor on top of the File Manager UI. + * + * @param {HTMLElement} popup Popup element. + * @param {function()} closeCallback Function to call after the popup is + * closed. + */ + FileManager.prototype.openFilePopup = function(popup, closeCallback) { + this.closeFilePopup(); + this.filePopup_ = popup; + this.filePopupCloseCallback_ = closeCallback; + this.dialogDom_.insertBefore( + this.filePopup_, this.dialogDom_.querySelector('#iframe-drag-area')); + this.filePopup_.focus(); + this.document_.body.setAttribute('overlay-visible', ''); + this.document_.querySelector('#iframe-drag-area').hidden = false; + }; + + /** + * Closes the modal-like file viewer/editor popup. + */ + FileManager.prototype.closeFilePopup = function() { + if (this.filePopup_) { + this.document_.body.removeAttribute('overlay-visible'); + this.document_.querySelector('#iframe-drag-area').hidden = true; + // The window resize would not be processed properly while the relevant + // divs had 'display:none', force resize after the layout fired. + setTimeout(this.onResize_.bind(this), 0); + if (this.filePopup_.contentWindow && + this.filePopup_.contentWindow.unload) { + this.filePopup_.contentWindow.unload(); + } + + if (this.filePopupCloseCallback_) { + this.filePopupCloseCallback_(); + this.filePopupCloseCallback_ = null; + } + + // These operations have to be in the end, otherwise v8 crashes on an + // assert. See: crbug.com/224174. + this.dialogDom_.removeChild(this.filePopup_); + this.filePopup_ = null; + } + }; + + /** + * Updates visibility of the draggable app region in the modal-like file + * viewer/editor. + * + * @param {boolean} visible True for visible, false otherwise. + */ + FileManager.prototype.onFilePopupAppRegionChanged = function(visible) { + if (!this.filePopup_) + return; + + this.document_.querySelector('#iframe-drag-area').hidden = !visible; + }; + + /** + * @return {Array.<Entry>} List of all entries in the current directory. + */ + FileManager.prototype.getAllEntriesInCurrentDirectory = function() { + return this.directoryModel_.getFileList().slice(); + }; + + FileManager.prototype.isRenamingInProgress = function() { + return !!this.renameInput_.currentEntry; + }; + + /** + * @private + */ + FileManager.prototype.focusCurrentList_ = function() { + if (this.listType_ == FileManager.ListType.DETAIL) + this.table_.focus(); + else // this.listType_ == FileManager.ListType.THUMBNAIL) + this.grid_.focus(); + }; + + /** + * Return full path of the current directory or null. + * @return {?string} The full path of the current directory. + */ + FileManager.prototype.getCurrentDirectory = function() { + return this.directoryModel_ && this.directoryModel_.getCurrentDirPath(); + }; + + /** + * Return URL of the current directory or null. + * @return {string} URL representing the current directory. + */ + FileManager.prototype.getCurrentDirectoryURL = function() { + return this.directoryModel_ && + this.directoryModel_.getCurrentDirectoryURL(); + }; + + /** + * Return DirectoryEntry of the current directory or null. + * @return {DirectoryEntry} 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(); + }; + + /** + * Deletes the selected file and directories recursively. + */ + FileManager.prototype.deleteSelection = function() { + // TODO(mtomasz): Remove this temporary dialog. crbug.com/167364 + var entries = this.getSelection().entries; + var message = entries.length == 1 ? + strf('GALLERY_CONFIRM_DELETE_ONE', entries[0].name) : + strf('GALLERY_CONFIRM_DELETE_SOME', entries.length); + this.confirm.show(message, function() { + this.fileOperationManager_.deleteEntries(entries); + }.bind(this)); + }; + + /** + * Shows the share dialog for the selected file or directory. + */ + FileManager.prototype.shareSelection = function() { + var entries = this.getSelection().entries; + if (entries.length != 1) { + console.warn('Unable to share multiple items at once.'); + return; + } + // Add the overlapped class to prevent the applicaiton window from + // captureing mouse events. + this.shareDialog_.show(entries[0], function(result) { + if (result == ShareDialog.Result.NETWORK_ERROR) + this.error.show(str('SHARE_ERROR')); + }.bind(this)); + }; + + /** + * Creates a folder shortcut. + * @param {string} path A shortcut which refers to |path| to be created. + */ + FileManager.prototype.createFolderShortcut = function(path) { + // Duplicate entry. + if (this.folderShortcutExists(path)) + return; + + this.folderShortcutsModel_.add(path); + }; + + /** + * Checkes if the shortcut which refers to the given folder exists or not. + * @param {string} path Path of the folder to be checked. + */ + FileManager.prototype.folderShortcutExists = function(path) { + return this.folderShortcutsModel_.exists(path); + }; + + /** + * Removes the folder shortcut. + * @param {string} path The shortcut which refers to |path| is to be removed. + */ + FileManager.prototype.removeFolderShortcut = function(path) { + this.folderShortcutsModel_.remove(path); + }; + + /** + * Blinks the selection. Used to give feedback when copying or cutting the + * selection. + */ + FileManager.prototype.blinkSelection = function() { + var selection = this.getSelection(); + if (!selection || selection.totalCount == 0) + return; + + for (var i = 0; i < selection.entries.length; i++) { + var selectedIndex = selection.indexes[i]; + var listItem = this.currentList_.getListItemByIndex(selectedIndex); + if (listItem) + this.blinkListItem_(listItem); + } + }; + + /** + * @param {Element} listItem List item element. + * @private + */ + FileManager.prototype.blinkListItem_ = function(listItem) { + listItem.classList.add('blink'); + setTimeout(function() { + listItem.classList.remove('blink'); + }, 100); + }; + + /** + * @private + */ + FileManager.prototype.selectDefaultPathInFilenameInput_ = function() { + var input = this.filenameInput_; + input.focus(); + var selectionEnd = input.value.lastIndexOf('.'); + if (selectionEnd == -1) { + input.select(); + } else { + input.selectionStart = 0; + input.selectionEnd = selectionEnd; + } + // Clear, so we never do this again. + this.defaultPath = ''; + }; + + /** + * Handles mouse click or tap. + * + * @param {Event} event The click event. + * @private + */ + FileManager.prototype.onDetailClick_ = function(event) { + if (this.isRenamingInProgress()) { + // Don't pay attention to clicks during a rename. + return; + } + + var listItem = this.findListItemForEvent_(event); + var selection = this.getSelection(); + if (!listItem || !listItem.selected || selection.totalCount != 1) { + return; + } + + // React on double click, but only if both clicks hit the same item. + // TODO(mtomasz): Simplify it, and use a double click handler if possible. + var clickNumber = (this.lastClickedItem_ == listItem) ? 2 : undefined; + this.lastClickedItem_ = listItem; + + if (event.detail != clickNumber) + return; + + var entry = selection.entries[0]; + if (entry.isDirectory) { + this.onDirectoryAction_(entry); + } else { + this.dispatchSelectionAction_(); + } + }; + + /** + * @private + */ + FileManager.prototype.dispatchSelectionAction_ = function() { + if (this.dialogType == DialogType.FULL_PAGE) { + var selection = this.getSelection(); + var tasks = selection.tasks; + var urls = selection.urls; + var mimeTypes = selection.mimeTypes; + if (tasks) + tasks.executeDefault(); + return true; + } + if (!this.okButton_.disabled) { + this.onOk_(); + return true; + } + return false; + }; + + /** + * Opens the suggest file dialog. + * + * @param {Entry} entry Entry of the file. + * @param {function()} onSuccess Success callback. + * @param {function()} onCancelled User-cancelled callback. + * @param {function()} onFailure Failure callback. + * @private + */ + FileManager.prototype.openSuggestAppsDialog = + function(entry, onSuccess, onCancelled, onFailure) { + if (!url) { + onFailure(); + return; + } + + this.metadataCache_.get([entry], 'drive', function(props) { + if (!props || !props[0] || !props[0].contentMimeType) { + onFailure(); + return; + } + + var basename = entry.name; + var splitted = PathUtil.splitExtension(basename); + var filename = splitted[0]; + var extension = splitted[1]; + var mime = props[0].contentMimeType; + + // Returns with failure if the file has neither extension nor mime. + if (!extension || !mime) { + onFailure(); + return; + } + + var onDialogClosed = function(result) { + switch (result) { + case SuggestAppsDialog.Result.INSTALL_SUCCESSFUL: + onSuccess(); + break; + case SuggestAppsDialog.Result.FAILED: + onFailure(); + break; + default: + onCancelled(); + } + }; + + if (FileTasks.EXECUTABLE_EXTENSIONS.indexOf(extension) !== -1) { + this.suggestAppsDialog.showByFilename(filename, onDialogClosed); + } else { + this.suggestAppsDialog.showByExtensionAndMime( + extension, mime, onDialogClosed); + } + }.bind(this)); + }; + + /** + * Called when a dialog is shown or hidden. + * @param {boolean} flag True if a dialog is shown, false if hidden. */ + FileManager.prototype.onDialogShownOrHidden = function(show) { + // Set/unset a flag to disable dragging on the title area. + this.dialogContainer_.classList.toggle('disable-header-drag', show); + }; + + /** + * Executes directory action (i.e. changes directory). + * + * @param {DirectoryEntry} entry Directory entry to which directory should be + * changed. + * @private + */ + FileManager.prototype.onDirectoryAction_ = function(entry) { + return this.directoryModel_.changeDirectory(entry.fullPath); + }; + + /** + * Update the window title. + * @private + */ + FileManager.prototype.updateTitle_ = function() { + if (this.dialogType != DialogType.FULL_PAGE) + return; + + var path = this.getCurrentDirectory(); + var rootPath = PathUtil.getRootPath(path); + this.document_.title = PathUtil.getRootLabel(rootPath) + + path.substring(rootPath.length); + }; + + /** + * Update the gear menu. + * @private + */ + FileManager.prototype.updateGearMenu_ = function() { + var hideItemsForDrive = !this.isOnDrive(); + this.syncButton.hidden = hideItemsForDrive; + this.hostedButton.hidden = hideItemsForDrive; + this.document_.getElementById('drive-separator').hidden = + hideItemsForDrive; + + // If volume has changed, then fetch remaining space data. + if (this.previousRootUrl_ != this.directoryModel_.getCurrentMountPointUrl()) + this.refreshRemainingSpace_(true); // Show loading caption. + + this.previousRootUrl_ = this.directoryModel_.getCurrentMountPointUrl(); + }; + + /** + * Refreshes space info of the current volume. + * @param {boolean} showLoadingCaption Whether show loading caption or not. + * @private + */ + FileManager.prototype.refreshRemainingSpace_ = function(showLoadingCaption) { + var volumeSpaceInfoLabel = + this.dialogDom_.querySelector('#volume-space-info-label'); + var volumeSpaceInnerBar = + this.dialogDom_.querySelector('#volume-space-info-bar'); + var volumeSpaceOuterBar = + this.dialogDom_.querySelector('#volume-space-info-bar').parentNode; + + volumeSpaceInnerBar.setAttribute('pending', ''); + + if (showLoadingCaption) { + volumeSpaceInfoLabel.innerText = str('WAITING_FOR_SPACE_INFO'); + volumeSpaceInnerBar.style.width = '100%'; + } + + var currentMountPointUrl = this.directoryModel_.getCurrentMountPointUrl(); + chrome.fileBrowserPrivate.getSizeStats( + currentMountPointUrl, function(result) { + if (this.directoryModel_.getCurrentMountPointUrl() != + currentMountPointUrl) + return; + updateSpaceInfo(result, + volumeSpaceInnerBar, + volumeSpaceInfoLabel, + volumeSpaceOuterBar); + }.bind(this)); + }; + + /** + * Update the UI when the current directory changes. + * + * @param {Event} event The directory-changed event. + * @private + */ + FileManager.prototype.onDirectoryChanged_ = function(event) { + this.selectionHandler_.onFileSelectionChanged(); + this.ui_.searchBox.clear(); + util.updateAppState(this.getCurrentDirectory()); + + // If the current directory is moved from the device's volume, do not + // automatically close the window on device removal. + if (event.previousDirEntry && + PathUtil.getRootPath(event.previousDirEntry.fullPath) != + PathUtil.getRootPath(event.newDirEntry.fullPath)) + this.closeOnUnmount_ = false; + + if (this.commandHandler) + this.commandHandler.updateAvailability(); + this.updateUnformattedVolumeStatus_(); + this.updateTitle_(); + this.updateGearMenu_(); + var currentEntry = this.getCurrentDirectoryEntry(); + this.previewPanel_.currentEntry = util.isFakeEntry(currentEntry) ? + null : currentEntry; + }; + + FileManager.prototype.updateUnformattedVolumeStatus_ = function() { + var volumeInfo = this.volumeManager_.getVolumeInfo( + this.directoryModel_.getCurrentDirEntry()); + + if (volumeInfo && volumeInfo.error) { + this.dialogDom_.setAttribute('unformatted', ''); + + var errorNode = this.dialogDom_.querySelector('#format-panel > .error'); + if (volumeInfo.error == util.VolumeError.UNSUPPORTED_FILESYSTEM) { + errorNode.textContent = str('UNSUPPORTED_FILESYSTEM_WARNING'); + } else { + errorNode.textContent = str('UNKNOWN_FILESYSTEM_WARNING'); + } + + // Update 'canExecute' for format command so the format button's disabled + // property is properly set. + if (this.commandHandler) + this.commandHandler.updateAvailability(); + } else { + this.dialogDom_.removeAttribute('unformatted'); + } + }; + + FileManager.prototype.findListItemForEvent_ = function(event) { + return this.findListItemForNode_(event.touchedElement || event.srcElement); + }; + + FileManager.prototype.findListItemForNode_ = function(node) { + var item = this.currentList_.getListItemAncestor(node); + // TODO(serya): list should check that. + return item && this.currentList_.isItem(item) ? item : null; + }; + + /** + * Unload handler for the page. May be called manually for the file picker + * dialog, because it closes by calling extension API functions that do not + * return. + * + * TODO(hirono): This method is not called when Files.app is opend as a dialog + * and is closed by the close button in the dialog frame. crbug.com/309967 + * @private + */ + FileManager.prototype.onUnload_ = function() { + if (this.directoryModel_) + this.directoryModel_.dispose(); + if (this.volumeManager_) + this.volumeManager_.dispose(); + if (this.filePopup_ && + this.filePopup_.contentWindow && + this.filePopup_.contentWindow.unload) + this.filePopup_.contentWindow.unload(true /* exiting */); + if (this.progressCenterPanel_) + this.backgroundPage_.background.progressCenter.removePanel( + this.progressCenterPanel_); + if (this.fileOperationManager_) { + if (this.onCopyProgressBound_) { + this.fileOperationManager_.removeEventListener( + 'copy-progress', this.onCopyProgressBound_); + } + if (this.onEntryChangedBound_) { + this.fileOperationManager_.removeEventListener( + 'entry-changed', this.onEntryChangedBound_); + } + } + window.closing = true; + if (this.backgroundPage_ && util.platform.runningInBrowser()) + this.backgroundPage_.background.tryClose(); + }; + + FileManager.prototype.initiateRename = function() { + var item = this.currentList_.ensureLeadItemExists(); + if (!item) + return; + var label = item.querySelector('.filename-label'); + var input = this.renameInput_; + + input.value = label.textContent; + label.parentNode.setAttribute('renaming', ''); + label.parentNode.appendChild(input); + input.focus(); + var selectionEnd = input.value.lastIndexOf('.'); + if (selectionEnd == -1) { + input.select(); + } else { + input.selectionStart = 0; + input.selectionEnd = selectionEnd; + } + + // This has to be set late in the process so we don't handle spurious + // blur events. + input.currentEntry = this.currentList_.dataModel.item(item.listIndex); + }; + + /** + * @type {Event} Key event. + * @private + */ + FileManager.prototype.onRenameInputKeyDown_ = function(event) { + if (!this.isRenamingInProgress()) + return; + + // Do not move selection or lead item in list during rename. + if (event.keyIdentifier == 'Up' || event.keyIdentifier == 'Down') { + event.stopPropagation(); + } + + switch (util.getKeyModifiers(event) + event.keyCode) { + case '27': // Escape + this.cancelRename_(); + event.preventDefault(); + break; + + case '13': // Enter + this.commitRename_(); + event.preventDefault(); + break; + } + }; + + /** + * @type {Event} Blur event. + * @private + */ + FileManager.prototype.onRenameInputBlur_ = function(event) { + if (this.isRenamingInProgress() && !this.renameInput_.validation_) + this.commitRename_(); + }; + + /** + * @private + */ + FileManager.prototype.commitRename_ = function() { + var input = this.renameInput_; + var entry = input.currentEntry; + var newName = input.value; + + if (newName == entry.name) { + this.cancelRename_(); + return; + } + + var nameNode = this.findListItemForNode_(this.renameInput_). + querySelector('.filename-label'); + + input.validation_ = true; + var validationDone = function(valid) { + input.validation_ = false; + // Alert dialog restores focus unless the item removed from DOM. + if (this.document_.activeElement != input) + this.cancelRename_(); + if (!valid) + return; + + // Validation succeeded. Do renaming. + + this.cancelRename_(); + // Optimistically apply new name immediately to avoid flickering in + // case of success. + nameNode.textContent = newName; + + util.rename( + entry, newName, + function(newEntry) { + this.directoryModel_.onRenameEntry(entry, newEntry); + }.bind(this), + function(error) { + // Write back to the old name. + nameNode.textContent = entry.name; + + // Show error dialog. + var message; + if (error.code == FileError.PATH_EXISTS_ERR || + error.code == FileError.TYPE_MISMATCH_ERR) { + // Check the existing entry is file or not. + // 1) If the entry is a file: + // a) If we get PATH_EXISTS_ERR, a file exists. + // b) If we get TYPE_MISMATCH_ERR, a directory exists. + // 2) If the entry is a directory: + // a) If we get PATH_EXISTS_ERR, a directory exists. + // b) If we get TYPE_MISMATCH_ERR, a file exists. + message = strf( + (entry.isFile && error.code == FileError.PATH_EXISTS_ERR) || + (!entry.isFile && error.code == FileError.TYPE_MISMATCH_ERR) ? + 'FILE_ALREADY_EXISTS' : + 'DIRECTORY_ALREADY_EXISTS', + newName); + } else { + message = strf('ERROR_RENAMING', entry.name, + util.getFileErrorString(err.code)); + } + + this.alert.show(message); + }.bind(this)); + }; + + // TODO(haruki): this.getCurrentDirectoryURL() might not return the actual + // parent if the directory content is a search result. Fix it to do proper + // validation. + this.validateFileName_(this.getCurrentDirectoryURL(), + newName, + validationDone.bind(this)); + }; + + /** + * @private + */ + FileManager.prototype.cancelRename_ = function() { + this.renameInput_.currentEntry = null; + + var parent = this.renameInput_.parentNode; + if (parent) { + parent.removeAttribute('renaming'); + parent.removeChild(this.renameInput_); + } + }; + + /** + * @param {Event} Key event. + * @private + */ + FileManager.prototype.onFilenameInputInput_ = function() { + this.selectionHandler_.updateOkButton(); + }; + + /** + * @param {Event} Key event. + * @private + */ + FileManager.prototype.onFilenameInputKeyDown_ = function(event) { + if ((util.getKeyModifiers(event) + event.keyCode) === '13' /* Enter */) + this.okButton_.click(); + }; + + /** + * @param {Event} Focus event. + * @private + */ + FileManager.prototype.onFilenameInputFocus_ = function(event) { + var input = this.filenameInput_; + + // On focus we want to select everything but the extension, but + // Chrome will select-all after the focus event completes. We + // schedule a timeout to alter the focus after that happens. + setTimeout(function() { + var selectionEnd = input.value.lastIndexOf('.'); + if (selectionEnd == -1) { + input.select(); + } else { + input.selectionStart = 0; + input.selectionEnd = selectionEnd; + } + }, 0); + }; + + /** + * @private + */ + FileManager.prototype.onScanStarted_ = function() { + if (this.scanInProgress_) { + this.table_.list.endBatchUpdates(); + this.grid_.endBatchUpdates(); + } + + if (this.commandHandler) + this.commandHandler.updateAvailability(); + this.table_.list.startBatchUpdates(); + this.grid_.startBatchUpdates(); + this.scanInProgress_ = true; + + this.scanUpdatedAtLeastOnceOrCompleted_ = false; + if (this.scanCompletedTimer_) { + clearTimeout(this.scanCompletedTimer_); + this.scanCompletedTimer_ = null; + } + + if (this.scanUpdatedTimer_) { + clearTimeout(this.scanUpdatedTimer_); + this.scanUpdatedTimer_ = null; + } + + if (this.spinner_.hidden) { + this.cancelSpinnerTimeout_(); + this.showSpinnerTimeout_ = + setTimeout(this.showSpinner_.bind(this, true), 500); + } + }; + + /** + * @private + */ + FileManager.prototype.onScanCompleted_ = function() { + if (!this.scanInProgress_) { + console.error('Scan-completed event recieved. But scan is not started.'); + return; + } + + if (this.commandHandler) + this.commandHandler.updateAvailability(); + this.hideSpinnerLater_(); + + if (this.scanUpdatedTimer_) { + clearTimeout(this.scanUpdatedTimer_); + this.scanUpdatedTimer_ = null; + } + + // To avoid flickering postpone updating the ui by a small amount of time. + // There is a high chance, that metadata will be received within 50 ms. + this.scanCompletedTimer_ = setTimeout(function() { + // Check if batch updates are already finished by onScanUpdated_(). + if (!this.scanUpdatedAtLeastOnceOrCompleted_) { + this.scanUpdatedAtLeastOnceOrCompleted_ = true; + this.updateMiddleBarVisibility_(); + } + + this.scanInProgress_ = false; + this.table_.list.endBatchUpdates(); + this.grid_.endBatchUpdates(); + this.scanCompletedTimer_ = null; + }.bind(this), 50); + }; + + /** + * @private + */ + FileManager.prototype.onScanUpdated_ = function() { + if (!this.scanInProgress_) { + console.error('Scan-updated event recieved. But scan is not started.'); + return; + } + + if (this.scanUpdatedTimer_ || this.scanCompletedTimer_) + return; + + // Show contents incrementally by finishing batch updated, but only after + // 200ms elapsed, to avoid flickering when it is not necessary. + this.scanUpdatedTimer_ = setTimeout(function() { + // We need to hide the spinner only once. + if (!this.scanUpdatedAtLeastOnceOrCompleted_) { + this.scanUpdatedAtLeastOnceOrCompleted_ = true; + this.hideSpinnerLater_(); + this.updateMiddleBarVisibility_(); + } + + // Update the UI. + if (this.scanInProgress_) { + this.table_.list.endBatchUpdates(); + this.grid_.endBatchUpdates(); + this.table_.list.startBatchUpdates(); + this.grid_.startBatchUpdates(); + } + this.scanUpdatedTimer_ = null; + }.bind(this), 200); + }; + + /** + * @private + */ + FileManager.prototype.onScanCancelled_ = function() { + if (!this.scanInProgress_) { + console.error('Scan-cancelled event recieved. But scan is not started.'); + return; + } + + if (this.commandHandler) + this.commandHandler.updateAvailability(); + this.hideSpinnerLater_(); + if (this.scanCompletedTimer_) { + clearTimeout(this.scanCompletedTimer_); + this.scanCompletedTimer_ = null; + } + if (this.scanUpdatedTimer_) { + clearTimeout(this.scanUpdatedTimer_); + this.scanUpdatedTimer_ = null; + } + // Finish unfinished batch updates. + if (!this.scanUpdatedAtLeastOnceOrCompleted_) { + this.scanUpdatedAtLeastOnceOrCompleted_ = true; + this.updateMiddleBarVisibility_(); + } + + this.scanInProgress_ = false; + this.table_.list.endBatchUpdates(); + this.grid_.endBatchUpdates(); + }; + + /** + * Handle the 'rescan-completed' from the DirectoryModel. + * @private + */ + FileManager.prototype.onRescanCompleted_ = function() { + this.selectionHandler_.onFileSelectionChanged(); + }; + + /** + * @private + */ + FileManager.prototype.cancelSpinnerTimeout_ = function() { + if (this.showSpinnerTimeout_) { + clearTimeout(this.showSpinnerTimeout_); + this.showSpinnerTimeout_ = null; + } + }; + + /** + * @private + */ + FileManager.prototype.hideSpinnerLater_ = function() { + this.cancelSpinnerTimeout_(); + this.showSpinner_(false); + }; + + /** + * @param {boolean} on True to show, false to hide. + * @private + */ + FileManager.prototype.showSpinner_ = function(on) { + if (on && this.directoryModel_ && this.directoryModel_.isScanning()) + this.spinner_.hidden = false; + + if (!on && (!this.directoryModel_ || + !this.directoryModel_.isScanning() || + this.directoryModel_.getFileList().length != 0)) { + this.spinner_.hidden = true; + } + }; + + FileManager.prototype.createNewFolder = function() { + var defaultName = str('DEFAULT_NEW_FOLDER_NAME'); + + // Find a name that doesn't exist in the data model. + var files = this.directoryModel_.getFileList(); + var hash = {}; + for (var i = 0; i < files.length; i++) { + var name = files.item(i).name; + // Filtering names prevents from conflicts with prototype's names + // and '__proto__'. + if (name.substring(0, defaultName.length) == defaultName) + hash[name] = 1; + } + + var baseName = defaultName; + var separator = ''; + var suffix = ''; + var index = ''; + + var advance = function() { + separator = ' ('; + suffix = ')'; + index++; + }; + + var current = function() { + return baseName + separator + index + suffix; + }; + + // Accessing hasOwnProperty is safe since hash properties filtered. + while (hash.hasOwnProperty(current())) { + advance(); + } + + var self = this; + var list = self.currentList_; + var tryCreate = function() { + self.directoryModel_.createDirectory(current(), + onSuccess, onError); + }; + + var onSuccess = function(entry) { + metrics.recordUserAction('CreateNewFolder'); + list.selectedItem = entry; + self.initiateRename(); + }; + + var onError = function(error) { + self.alert.show(strf('ERROR_CREATING_FOLDER', current(), + util.getFileErrorString(error.code))); + }; + + tryCreate(); + }; + + /** + * @param {Event} event Click event. + * @private + */ + FileManager.prototype.onDetailViewButtonClick_ = function(event) { + // Stop propagate and hide the menu manually, in order to prevent the focus + // from being back to the button. (cf. http://crbug.com/248479) + event.stopPropagation(); + this.gearButton_.hideMenu(); + + this.setListType(FileManager.ListType.DETAIL); + this.currentList_.focus(); + }; + + /** + * @param {Event} event Click event. + * @private + */ + FileManager.prototype.onThumbnailViewButtonClick_ = function(event) { + // Stop propagate and hide the menu manually, in order to prevent the focus + // from being back to the button. (cf. http://crbug.com/248479) + event.stopPropagation(); + this.gearButton_.hideMenu(); + + this.setListType(FileManager.ListType.THUMBNAIL); + this.currentList_.focus(); + }; + + /** + * KeyDown event handler for the document. + * @param {Event} event Key event. + * @private + */ + FileManager.prototype.onKeyDown_ = function(event) { + if (event.srcElement === this.renameInput_) { + // Ignore keydown handler in the rename input box. + return; + } + + switch (util.getKeyModifiers(event) + event.keyCode) { + case 'Ctrl-190': // Ctrl-. => Toggle filter files. + this.fileFilter_.setFilterHidden( + !this.fileFilter_.isFilterHiddenOn()); + event.preventDefault(); + return; + + case '27': // Escape => Cancel dialog. + if (this.dialogType != DialogType.FULL_PAGE) { + // If there is nothing else for ESC to do, then cancel the dialog. + event.preventDefault(); + this.cancelButton_.click(); + } + break; + } + }; + + /** + * KeyDown event handler for the div#list-container element. + * @param {Event} event Key event. + * @private + */ + FileManager.prototype.onListKeyDown_ = function(event) { + if (event.srcElement.tagName == 'INPUT') { + // Ignore keydown handler in the rename input box. + return; + } + + switch (util.getKeyModifiers(event) + event.keyCode) { + case '8': // Backspace => Up one directory. + event.preventDefault(); + var path = this.getCurrentDirectory(); + if (path && !PathUtil.isRootPath(path)) { + var path = path.replace(/\/[^\/]+$/, ''); + this.directoryModel_.changeDirectory(path); + } + break; + + case '13': // Enter => Change directory or perform default action. + // TODO(dgozman): move directory action to dispatchSelectionAction. + var selection = this.getSelection(); + if (selection.totalCount == 1 && + selection.entries[0].isDirectory && + !DialogType.isFolderDialog(this.dialogType)) { + event.preventDefault(); + this.onDirectoryAction_(selection.entries[0]); + } else if (this.dispatchSelectionAction_()) { + event.preventDefault(); + } + break; + } + + switch (event.keyIdentifier) { + case 'Home': + case 'End': + case 'Up': + case 'Down': + case 'Left': + case 'Right': + // When navigating with keyboard we hide the distracting mouse hover + // highlighting until the user moves the mouse again. + this.setNoHover_(true); + break; + } + }; + + /** + * Suppress/restore hover highlighting in the list container. + * @param {boolean} on True to temporarity hide hover state. + * @private + */ + FileManager.prototype.setNoHover_ = function(on) { + if (on) { + this.listContainer_.classList.add('nohover'); + } else { + this.listContainer_.classList.remove('nohover'); + } + }; + + /** + * KeyPress event handler for the div#list-container element. + * @param {Event} event Key event. + * @private + */ + FileManager.prototype.onListKeyPress_ = function(event) { + if (event.srcElement.tagName == 'INPUT') { + // Ignore keypress handler in the rename input box. + return; + } + + if (event.ctrlKey || event.metaKey || event.altKey) + return; + + var now = new Date(); + var char = String.fromCharCode(event.charCode).toLowerCase(); + var text = now - this.textSearchState_.date > 1000 ? '' : + this.textSearchState_.text; + this.textSearchState_ = {text: text + char, date: now}; + + this.doTextSearch_(); + }; + + /** + * Mousemove event handler for the div#list-container element. + * @param {Event} event Mouse event. + * @private + */ + FileManager.prototype.onListMouseMove_ = function(event) { + // The user grabbed the mouse, restore the hover highlighting. + this.setNoHover_(false); + }; + + /** + * Performs a 'text search' - selects a first list entry with name + * starting with entered text (case-insensitive). + * @private + */ + FileManager.prototype.doTextSearch_ = function() { + var text = this.textSearchState_.text; + if (!text) + return; + + var dm = this.directoryModel_.getFileList(); + for (var index = 0; index < dm.length; ++index) { + var name = dm.item(index).name; + if (name.substring(0, text.length).toLowerCase() == text) { + this.currentList_.selectionModel.selectedIndexes = [index]; + return; + } + } + + this.textSearchState_.text = ''; + }; + + /** + * Handle a click of the cancel button. Closes the window. + * TODO(jamescook): Make unload handler work automatically, crbug.com/104811 + * + * @param {Event} event The click event. + * @private + */ + FileManager.prototype.onCancel_ = function(event) { + chrome.fileBrowserPrivate.cancelDialog(); + this.onUnload_(); + window.close(); + }; + + /** + * Resolves selected file urls returned from an Open dialog. + * + * For drive files this involves some special treatment. + * Starts getting drive files if needed. + * + * @param {Array.<string>} fileUrls Drive URLs. + * @param {function(Array.<string>)} callback To be called with fixed URLs. + * @private + */ + FileManager.prototype.resolveSelectResults_ = function(fileUrls, callback) { + if (this.isOnDrive()) { + chrome.fileBrowserPrivate.getDriveFiles( + fileUrls, + function(localPaths) { + callback(fileUrls); + }); + } else { + callback(fileUrls); + } + }; + + /** + * Closes this modal dialog with some files selected. + * TODO(jamescook): Make unload handler work automatically, crbug.com/104811 + * @param {Object} selection Contains urls, filterIndex and multiple fields. + * @private + */ + FileManager.prototype.callSelectFilesApiAndClose_ = function(selection) { + var self = this; + function callback() { + self.onUnload_(); + window.close(); + } + if (selection.multiple) { + chrome.fileBrowserPrivate.selectFiles( + selection.urls, this.params_.shouldReturnLocalPath, callback); + } else { + var forOpening = (this.dialogType != DialogType.SELECT_SAVEAS_FILE); + chrome.fileBrowserPrivate.selectFile( + selection.urls[0], selection.filterIndex, forOpening, + this.params_.shouldReturnLocalPath, callback); + } + }; + + /** + * Tries to close this modal dialog with some files selected. + * Performs preprocessing if needed (e.g. for Drive). + * @param {Object} selection Contains urls, filterIndex and multiple fields. + * @private + */ + FileManager.prototype.selectFilesAndClose_ = function(selection) { + if (!this.isOnDrive() || + this.dialogType == DialogType.SELECT_SAVEAS_FILE) { + setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0); + return; + } + + var shade = this.document_.createElement('div'); + shade.className = 'shade'; + var footer = this.dialogDom_.querySelector('.button-panel'); + var progress = footer.querySelector('.progress-track'); + progress.style.width = '0%'; + var cancelled = false; + + var progressMap = {}; + var filesStarted = 0; + var filesTotal = selection.urls.length; + for (var index = 0; index < selection.urls.length; index++) { + progressMap[selection.urls[index]] = -1; + } + var lastPercent = 0; + var bytesTotal = 0; + var bytesDone = 0; + + var onFileTransfersUpdated = function(statusList) { + for (var index = 0; index < statusList.length; index++) { + var status = statusList[index]; + var escaped = encodeURI(status.fileUrl); + if (!(escaped in progressMap)) continue; + if (status.total == -1) continue; + + var old = progressMap[escaped]; + if (old == -1) { + // -1 means we don't know file size yet. + bytesTotal += status.total; + filesStarted++; + old = 0; + } + bytesDone += status.processed - old; + progressMap[escaped] = status.processed; + } + + var percent = bytesTotal == 0 ? 0 : bytesDone / bytesTotal; + // For files we don't have information about, assume the progress is zero. + percent = percent * filesStarted / filesTotal * 100; + // Do not decrease the progress. This may happen, if first downloaded + // file is small, and the second one is large. + lastPercent = Math.max(lastPercent, percent); + progress.style.width = lastPercent + '%'; + }.bind(this); + + var setup = function() { + this.document_.querySelector('.dialog-container').appendChild(shade); + setTimeout(function() { shade.setAttribute('fadein', 'fadein') }, 100); + footer.setAttribute('progress', 'progress'); + this.cancelButton_.removeEventListener('click', this.onCancelBound_); + this.cancelButton_.addEventListener('click', onCancel); + chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener( + onFileTransfersUpdated); + }.bind(this); + + var cleanup = function() { + shade.parentNode.removeChild(shade); + footer.removeAttribute('progress'); + this.cancelButton_.removeEventListener('click', onCancel); + this.cancelButton_.addEventListener('click', this.onCancelBound_); + chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener( + onFileTransfersUpdated); + }.bind(this); + + var onCancel = function() { + cancelled = true; + // According to API cancel may fail, but there is no proper UI to reflect + // this. So, we just silently assume that everything is cancelled. + chrome.fileBrowserPrivate.cancelFileTransfers( + selection.urls, function(response) {}); + cleanup(); + }.bind(this); + + var onResolved = function(resolvedUrls) { + if (cancelled) return; + cleanup(); + selection.urls = resolvedUrls; + // Call next method on a timeout, as it's unsafe to + // close a window from a callback. + setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0); + }.bind(this); + + var onProperties = function(properties) { + for (var i = 0; i < properties.length; i++) { + if (!properties[i] || properties[i].present) { + // For files already in GCache, we don't get any transfer updates. + filesTotal--; + } + } + this.resolveSelectResults_(selection.urls, onResolved); + }.bind(this); + + setup(); + + // TODO(mtomasz): Use Entry instead of URLs, if possible. + util.URLsToEntries(selection.urls, function(entries) { + this.metadataCache_.get(entries, 'drive', onProperties); + }.bind(this)); + }; + + /** + * Handle a click of the ok button. + * + * The ok button has different UI labels depending on the type of dialog, but + * in code it's always referred to as 'ok'. + * + * @param {Event} event The click event. + * @private + */ + FileManager.prototype.onOk_ = function(event) { + if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) { + // Save-as doesn't require a valid selection from the list, since + // we're going to take the filename from the text input. + var filename = this.filenameInput_.value; + if (!filename) + throw new Error('Missing filename!'); + + var directory = this.getCurrentDirectoryEntry(); + var currentDirUrl = directory.toURL(); + if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/') + currentDirUrl += '/'; + this.validateFileName_(currentDirUrl, filename, function(isValid) { + if (!isValid) + return; + + if (util.isFakeEntry(directory)) { + // Can't save a file into a fake directory. + return; + } + + var selectFileAndClose = function() { + this.selectFilesAndClose_({ + urls: [currentDirUrl + encodeURIComponent(filename)], + multiple: false, + filterIndex: this.getSelectedFilterIndex_(filename) + }); + }.bind(this); + + directory.getFile( + filename, {create: false}, + function(entry) { + // An existing file is found. Show confirmation dialog to + // overwrite it. If the user select "OK" on the dialog, save it. + this.confirm.show(strf('CONFIRM_OVERWRITE_FILE', filename), + selectFileAndClose); + }.bind(this), + function(error) { + if (error.code == FileError.NOT_FOUND_ERR) { + // The file does not exist, so it should be ok to create a + // new file. + selectFileAndClose(); + return; + } + if (error.code == FileError.TYPE_MISMATCH_ERR) { + // An directory is found. + // Do not allow to overwrite directory. + this.alert.show(strf('DIRECTORY_ALREADY_EXISTS', filename)); + return; + } + + // Unexpected error. + console.error('File save failed: ' + error.code); + }.bind(this)); + }.bind(this)); + return; + } + + var files = []; + var selectedIndexes = this.currentList_.selectionModel.selectedIndexes; + + if (DialogType.isFolderDialog(this.dialogType) && + selectedIndexes.length == 0) { + var url = this.getCurrentDirectoryURL(); + var singleSelection = { + urls: [url], + multiple: false, + filterIndex: this.getSelectedFilterIndex_() + }; + this.selectFilesAndClose_(singleSelection); + return; + } + + // All other dialog types require at least one selected list item. + // The logic to control whether or not the ok button is enabled should + // prevent us from ever getting here, but we sanity check to be sure. + if (!selectedIndexes.length) + throw new Error('Nothing selected!'); + + var dm = this.directoryModel_.getFileList(); + for (var i = 0; i < selectedIndexes.length; i++) { + var entry = dm.item(selectedIndexes[i]); + if (!entry) { + console.error('Error locating selected file at index: ' + i); + continue; + } + + files.push(entry.toURL()); + } + + // Multi-file selection has no other restrictions. + if (this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE) { + var multipleSelection = { + urls: files, + multiple: true + }; + this.selectFilesAndClose_(multipleSelection); + return; + } + + // Everything else must have exactly one. + if (files.length > 1) + throw new Error('Too many files selected!'); + + var selectedEntry = dm.item(selectedIndexes[0]); + + if (DialogType.isFolderDialog(this.dialogType)) { + if (!selectedEntry.isDirectory) + throw new Error('Selected entry is not a folder!'); + } else if (this.dialogType == DialogType.SELECT_OPEN_FILE) { + if (!selectedEntry.isFile) + throw new Error('Selected entry is not a file!'); + } + + var singleSelection = { + urls: [files[0]], + multiple: false, + filterIndex: this.getSelectedFilterIndex_() + }; + this.selectFilesAndClose_(singleSelection); + }; + + /** + * Verifies the user entered name for file or folder to be created or + * renamed to. Name restrictions must correspond to File API restrictions + * (see DOMFilePath::isValidPath). Curernt WebKit implementation is + * out of date (spec is + * http://dev.w3.org/2009/dap/file-system/file-dir-sys.html, 8.3) and going to + * be fixed. Shows message box if the name is invalid. + * + * It also verifies if the name length is in the limit of the filesystem. + * + * @param {string} parentUrl The URL of the parent directory entry. + * @param {string} name New file or folder name. + * @param {function} onDone Function to invoke when user closes the + * warning box or immediatelly if file name is correct. If the name was + * valid it is passed true, and false otherwise. + * @private + */ + FileManager.prototype.validateFileName_ = function(parentUrl, name, onDone) { + var msg; + var testResult = /[\/\\\<\>\:\?\*\"\|]/.exec(name); + if (testResult) { + msg = strf('ERROR_INVALID_CHARACTER', testResult[0]); + } else if (/^\s*$/i.test(name)) { + msg = str('ERROR_WHITESPACE_NAME'); + } else if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(name)) { + msg = str('ERROR_RESERVED_NAME'); + } else if (this.fileFilter_.isFilterHiddenOn() && name[0] == '.') { + msg = str('ERROR_HIDDEN_NAME'); + } + + if (msg) { + this.alert.show(msg, function() { + onDone(false); + }); + return; + } + + var self = this; + chrome.fileBrowserPrivate.validatePathNameLength( + parentUrl, name, function(valid) { + if (!valid) { + self.alert.show(str('ERROR_LONG_NAME'), + function() { onDone(false); }); + } else { + onDone(true); + } + }); + }; + + /** + * Handler invoked on preference setting in drive context menu. + * + * @param {string} pref The preference to alter. + * @param {boolean} inverted Invert the value if true. + * @param {Event} event The click event. + * @private + */ + FileManager.prototype.onDrivePrefClick_ = function(pref, inverted, event) { + var newValue = !event.target.hasAttribute('checked'); + if (newValue) + event.target.setAttribute('checked', 'checked'); + else + event.target.removeAttribute('checked'); + + var changeInfo = {}; + changeInfo[pref] = inverted ? !newValue : newValue; + chrome.fileBrowserPrivate.setPreferences(changeInfo); + }; + + /** + * Invoked when the search box is changed. + * + * @param {Event} event The changed event. + * @private + */ + FileManager.prototype.onSearchBoxUpdate_ = function(event) { + var searchString = this.searchBox_.value; + + if (this.isOnDrive()) { + // When the search text is changed, finishes the search and showes back + // the last directory by passing an empty string to + // {@code DirectoryModel.search()}. + if (this.directoryModel_.isSearching() && + this.lastSearchQuery_ != searchString) { + this.doSearch(''); + } + + // On drive, incremental search is not invoked since we have an auto- + // complete suggestion instead. + return; + } + + this.search_(searchString); + }; + + /** + * Handle the search clear button click. + * @private + */ + FileManager.prototype.onSearchClearButtonClick_ = function() { + this.ui_.searchBox.clear(); + this.onSearchBoxUpdate_(); + }; + + /** + * Search files and update the list with the search result. + * + * @param {string} searchString String to be searched with. + * @private + */ + FileManager.prototype.search_ = function(searchString) { + var noResultsDiv = this.document_.getElementById('no-search-results'); + + var reportEmptySearchResults = function() { + if (this.directoryModel_.getFileList().length === 0) { + // The string 'SEARCH_NO_MATCHING_FILES_HTML' may contain HTML tags, + // hence we escapes |searchString| here. + var html = strf('SEARCH_NO_MATCHING_FILES_HTML', + util.htmlEscape(searchString)); + noResultsDiv.innerHTML = html; + noResultsDiv.setAttribute('show', 'true'); + } else { + noResultsDiv.removeAttribute('show'); + } + }; + + var hideNoResultsDiv = function() { + noResultsDiv.removeAttribute('show'); + }; + + this.doSearch(searchString, + reportEmptySearchResults.bind(this), + hideNoResultsDiv.bind(this)); + }; + + /** + * Performs search and displays results. + * + * @param {string} query Query that will be searched for. + * @param {function()=} opt_onSearchRescan Function that will be called when + * the search directory is rescanned (i.e. search results are displayed). + * @param {function()=} opt_onClearSearch Function to be called when search + * state gets cleared. + */ + FileManager.prototype.doSearch = function( + searchString, opt_onSearchRescan, opt_onClearSearch) { + var onSearchRescan = opt_onSearchRescan || function() {}; + var onClearSearch = opt_onClearSearch || function() {}; + + this.lastSearchQuery_ = searchString; + this.directoryModel_.search(searchString, onSearchRescan, onClearSearch); + }; + + /** + * Requests autocomplete suggestions for files on Drive. + * Once the suggestions are returned, the autocomplete popup will show up. + * + * @param {string} query The text to autocomplete from. + * @private + */ + FileManager.prototype.requestAutocompleteSuggestions_ = function(query) { + query = query.trimLeft(); + + // Only Drive supports auto-compelete + if (!this.isOnDrive()) + return; + + // Remember the most recent query. If there is an other request in progress, + // then it's result will be discarded and it will call a new request for + // this query. + this.lastAutocompleteQuery_ = query; + if (this.autocompleteSuggestionsBusy_) + return; + + // The autocomplete list should be resized and repositioned here as the + // search box is resized when it's focused. + this.autocompleteList_.syncWidthAndPositionToInput(); + + if (!query) { + this.autocompleteList_.suggestions = []; + return; + } + + var headerItem = {isHeaderItem: true, searchQuery: query}; + if (!this.autocompleteList_.dataModel || + this.autocompleteList_.dataModel.length == 0) + this.autocompleteList_.suggestions = [headerItem]; + else + // Updates only the head item to prevent a flickering on typing. + this.autocompleteList_.dataModel.splice(0, 1, headerItem); + + this.autocompleteSuggestionsBusy_ = true; + + var searchParams = { + 'query': query, + 'types': 'ALL', + 'maxResults': 4 + }; + chrome.fileBrowserPrivate.searchDriveMetadata( + searchParams, + function(suggestions) { + this.autocompleteSuggestionsBusy_ = false; + + // Discard results for previous requests and fire a new search + // for the most recent query. + if (query != this.lastAutocompleteQuery_) { + this.requestAutocompleteSuggestions_(this.lastAutocompleteQuery_); + return; + } + + // Keeps the items in the suggestion list. + this.autocompleteList_.suggestions = [headerItem].concat(suggestions); + }.bind(this)); + }; + + /** + * Opens the currently selected suggestion item. + * @private + */ + FileManager.prototype.openAutocompleteSuggestion_ = function() { + var selectedItem = this.autocompleteList_.selectedItem; + + // If the entry is the search item or no entry is selected, just change to + // the search result. + if (!selectedItem || selectedItem.isHeaderItem) { + var query = selectedItem ? + selectedItem.searchQuery : this.searchBox_.value; + this.search_(query); + return; + } + + var entry = selectedItem.entry; + // If the entry is a directory, just change the directory. + if (entry.isDirectory) { + this.onDirectoryAction_(entry); + return; + } + + var entries = [entry]; + var self = this; + + // To open a file, first get the mime type. + this.metadataCache_.get(entries, 'drive', function(props) { + var mimeType = props[0].contentMimeType || ''; + var mimeTypes = [mimeType]; + var openIt = function() { + if (self.dialogType == DialogType.FULL_PAGE) { + var tasks = new FileTasks(self); + tasks.init(entries, mimeTypes); + tasks.executeDefault(); + } else { + self.onOk_(); + } + }; + + // Change the current directory to the directory that contains the + // selected file. Note that this is necessary for an image or a video, + // which should be opened in the gallery mode, as the gallery mode + // requires the entry to be in the current directory model. For + // consistency, the current directory is always changed regardless of + // the file type. + entry.getParent(function(parent) { + var onDirectoryChanged = function(event) { + self.directoryModel_.removeEventListener('scan-completed', + onDirectoryChanged); + self.directoryModel_.selectEntry(entry); + openIt(); + }; + // changeDirectory() returns immediately. We should wait until the + // directory scan is complete. + self.directoryModel_.addEventListener('scan-completed', + onDirectoryChanged); + self.directoryModel_.changeDirectory( + parent.fullPath, + function() { + // Remove the listner if the change directory failed. + self.directoryModel_.removeEventListener('scan-completed', + onDirectoryChanged); + }); + }); + }); + }; + + FileManager.prototype.decorateSplitter = function(splitterElement) { + var self = this; + + var Splitter = cr.ui.Splitter; + + var customSplitter = cr.ui.define('div'); + + customSplitter.prototype = { + __proto__: Splitter.prototype, + + handleSplitterDragStart: function(e) { + Splitter.prototype.handleSplitterDragStart.apply(this, arguments); + this.ownerDocument.documentElement.classList.add('col-resize'); + }, + + handleSplitterDragMove: function(deltaX) { + Splitter.prototype.handleSplitterDragMove.apply(this, arguments); + self.onResize_(); + }, + + handleSplitterDragEnd: function(e) { + Splitter.prototype.handleSplitterDragEnd.apply(this, arguments); + this.ownerDocument.documentElement.classList.remove('col-resize'); + } + }; + + customSplitter.decorate(splitterElement); + }; + + /** + * Updates default action menu item to match passed taskItem (icon, + * label and action). + * + * @param {Object} defaultItem - taskItem to match. + * @param {boolean} isMultiple - if multiple tasks available. + */ + FileManager.prototype.updateContextMenuActionItems = function(defaultItem, + isMultiple) { + if (defaultItem) { + if (defaultItem.iconType) { + this.defaultActionMenuItem_.style.backgroundImage = ''; + this.defaultActionMenuItem_.setAttribute('file-type-icon', + defaultItem.iconType); + } else if (defaultItem.iconUrl) { + this.defaultActionMenuItem_.style.backgroundImage = + 'url(' + defaultItem.iconUrl + ')'; + } else { + this.defaultActionMenuItem_.style.backgroundImage = ''; + } + + this.defaultActionMenuItem_.label = defaultItem.title; + this.defaultActionMenuItem_.disabled = !!defaultItem.disabled; + this.defaultActionMenuItem_.taskId = defaultItem.taskId; + } + + var defaultActionSeparator = + this.dialogDom_.querySelector('#default-action-separator'); + + this.openWithCommand_.canExecuteChange(); + this.openWithCommand_.setHidden(!(defaultItem && isMultiple)); + this.openWithCommand_.disabled = defaultItem && !!defaultItem.disabled; + + this.defaultActionMenuItem_.hidden = !defaultItem; + defaultActionSeparator.hidden = !defaultItem; + }; + + /** + * Window beforeunload handler. + * @return {string} Message to show. Ignored when running as a packaged app. + * @private + */ + FileManager.prototype.onBeforeUnload_ = function() { + if (this.filePopup_ && + this.filePopup_.contentWindow && + this.filePopup_.contentWindow.beforeunload) { + // The gallery might want to prevent the unload if it is busy. + return this.filePopup_.contentWindow.beforeunload(); + } + return null; + }; + + /** + * @return {FileSelection} Selection object. + */ + FileManager.prototype.getSelection = function() { + return this.selectionHandler_.selection; + }; + + /** + * @return {ArrayDataModel} File list. + */ + FileManager.prototype.getFileList = function() { + return this.directoryModel_.getFileList(); + }; + + /** + * @return {cr.ui.List} Current list object. + */ + FileManager.prototype.getCurrentList = function() { + return this.currentList_; + }; + + /** + * Retrieve the preferences of the files.app. This method caches the result + * and returns it unless opt_update is true. + * @param {function(Object.<string, *>)} callback Callback to get the + * preference. + * @param {boolean=} opt_update If is's true, don't use the cache and + * retrieve latest preference. Default is false. + * @private + */ + FileManager.prototype.getPreferences_ = function(callback, opt_update) { + if (!opt_update && this.preferences_ !== undefined) { + callback(this.preferences_); + return; + } + + chrome.fileBrowserPrivate.getPreferences(function(prefs) { + this.preferences_ = prefs; + callback(prefs); + }.bind(this)); + }; +})(); diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_manager_commands.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_manager_commands.js new file mode 100644 index 00000000000..52a26b92a64 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_manager_commands.js @@ -0,0 +1,828 @@ +// 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'; + +/** + * TODO(dzvorygin): Here we use this hack, since 'hidden' is standard + * attribute and we can't use it's setter as usual. + * @param {boolean} value New value of hidden property. + */ +cr.ui.Command.prototype.setHidden = function(value) { + this.__lookupSetter__('hidden').call(this, value); +}; + +/** + * A command. + * @interface + */ +var Command = function() {}; + +/** + * Handles the execute event. + * @param {Event} event Command event. + * @param {FileManager} fileManager FileManager. + */ +Command.prototype.execute = function(event, fileManager) {}; + +/** + * Handles the can execute event. + * @param {Event} event Can execute event. + * @param {FileManager} fileManager FileManager. + */ +Command.prototype.canExecute = function(event, fileManager) {}; + +/** + * Utility for commands. + */ +var CommandUtil = {}; + +/** + * Extracts entry on which command event was dispatched. + * + * @param {DirectoryTree|DirectoryItem|NavigationList|HTMLLIElement|cr.ui.List} + * element Directory to extract a path from. + * @return {Entry} Entry of the found node. + */ +CommandUtil.getCommandEntry = function(element) { + if (element instanceof NavigationList) { + // element is a NavigationList. + + /** @type {NavigationModelItem} */ + var selectedItem = element.selectedItem; + return selectedItem && selectedItem.getCachedEntry(); + } else if (element instanceof NavigationListItem) { + // element is a subitem of NavigationList. + /** @type {NavigationList} */ + var navigationList = element.parentElement; + var index = navigationList.getIndexOfListItem(element); + /** @type {NavigationModelItem} */ + var item = (index != -1) ? navigationList.dataModel.item(index) : null; + return item && item.getCachedEntry(); + } else if (element instanceof DirectoryTree) { + // element is a DirectoryTree. + return element.selectedItem; + } else if (element instanceof DirectoryItem) { + // element is a sub item in DirectoryTree. + + // DirectoryItem.fullPath is set on initialization, but entry is lazily. + // We may use fullPath just in case that the entry has not been set yet. + return element.entry; + } else if (element instanceof cr.ui.List) { + // element is a normal List (eg. the file list on the right panel). + var entry = element.selectedItem; + // Check if it is Entry or not by referring the fullPath member variable. + return entry && entry.fullPath ? entry : null; + } else { + console.warn('Unsupported element'); + return null; + } +}; + +/** + * @param {NavigationList} navigationList navigation list to extract root node. + * @return {?RootType} Type of the found root. + */ +CommandUtil.getCommandRootType = function(navigationList) { + var root = CommandUtil.getCommandEntry(navigationList); + return root && + PathUtil.isRootPath(root.fullPath) && + PathUtil.getRootType(root.fullPath); +}; + +/** + * Checks if command can be executed on drive. + * @param {Event} event Command event to mark. + * @param {FileManager} fileManager FileManager to use. + */ +CommandUtil.canExecuteEnabledOnDriveOnly = function(event, fileManager) { + event.canExecute = fileManager.isOnDrive(); +}; + +/** + * Checks if command should be visible on drive. + * @param {Event} event Command event to mark. + * @param {FileManager} fileManager FileManager to use. + */ +CommandUtil.canExecuteVisibleOnDriveOnly = function(event, fileManager) { + event.canExecute = fileManager.isOnDrive(); + event.command.setHidden(!fileManager.isOnDrive()); +}; + +/** + * Sets as the command as always enabled. + * @param {Event} event Command event to mark. + */ +CommandUtil.canExecuteAlways = function(event) { + event.canExecute = true; +}; + +/** + * Returns a single selected/passed entry or null. + * @param {Event} event Command event. + * @param {FileManager} fileManager FileManager to use. + * @return {FileEntry} The entry or null. + */ +CommandUtil.getSingleEntry = function(event, fileManager) { + if (event.target.entry) { + return event.target.entry; + } + var selection = fileManager.getSelection(); + if (selection.totalCount == 1) { + return selection.entries[0]; + } + return null; +}; + +/** + * Obtains target entries that can be pinned from the selection. + * If directories are included in the selection, it just returns an empty + * array to avoid confusing because pinning directory is not supported + * currently. + * + * @return {Array.<Entry>} Target entries. + */ +CommandUtil.getPinTargetEntries = function() { + var hasDirectory = false; + var results = fileManager.getSelection().entries.filter(function(entry) { + hasDirectory = hasDirectory || entry.isDirectory; + if (!entry || hasDirectory) + return false; + var metadata = fileManager.metadataCache_.getCached(entry, 'drive'); + if (!metadata || metadata.hosted) + return false; + entry.pinned = metadata.pinned; + return true; + }); + return hasDirectory ? [] : results; +}; + +/** + * Sets the default handler for the commandId and prevents handling + * the keydown events for this command. Not doing that breaks relationship + * of original keyboard event and the command. WebKit would handle it + * differently in some cases. + * @param {Node} node to register command handler on. + * @param {string} commandId Command id to respond to. + */ +CommandUtil.forceDefaultHandler = function(node, commandId) { + var doc = node.ownerDocument; + var command = doc.querySelector('command[id="' + commandId + '"]'); + node.addEventListener('keydown', function(e) { + if (command.matchesEvent(e)) { + // Prevent cr.ui.CommandManager of handling it and leave it + // for the default handler. + e.stopPropagation(); + } + }); + node.addEventListener('command', function(event) { + if (event.command.id !== commandId) + return; + document.execCommand(event.command.id); + event.cancelBubble = true; + }); + node.addEventListener('canExecute', function(event) { + if (event.command.id === commandId) + event.canExecute = document.queryCommandEnabled(event.command.id); + }); +}; + +/** + * Default command. + * @type {Command} + */ +CommandUtil.defaultCommand = { + execute: function(event, fileManager) { + fileManager.document.execCommand(event.command.id); + }, + canExecute: function(event, fileManager) { + event.canExecute = fileManager.document.queryCommandEnabled( + event.command.id); + } +}; + +/** + * Creates the volume switch command with index. + * @param {number} index Volume index from 1 to 9. + * @return {Command} Volume switch command. + */ +CommandUtil.createVolumeSwitchCommand = function(index) { + return { + execute: function(event, fileManager) { + fileManager.navigationList.selectByIndex(index - 1); + }, + canExecute: function(event, fileManager) { + event.canExecute = index > 0 && + index <= fileManager.navigationList.dataModel.length; + } + }; +}; + +/** + * Handle of the command events. + * @param {FileManager} fileManager FileManager. + * @constructor + */ +var CommandHandler = function(fileManager) { + /** + * FileManager. + * @type {FileManager} + * @private + */ + this.fileManager_ = fileManager; + + /** + * Command elements. + * @type {Object.<string, cr.ui.Command>} + * @private + */ + this.commands_ = {}; + + /** + * Whether the ctrl key is pressed or not. + * @type {boolean} + * @private + */ + this.ctrlKeyPressed_ = false; + + Object.seal(this); + + // Decorate command tags in the document. + var commands = fileManager.document.querySelectorAll('command'); + for (var i = 0; i < commands.length; i++) { + cr.ui.Command.decorate(commands[i]); + this.commands_[commands[i].id] = commands[i]; + } + + // Register events. + fileManager.document.addEventListener('command', this.onCommand_.bind(this)); + fileManager.document.addEventListener('canExecute', + this.onCanExecute_.bind(this)); + fileManager.document.addEventListener('keydown', this.onKeyDown_.bind(this)); + fileManager.document.addEventListener('keyup', this.onKeyUp_.bind(this)); +}; + +/** + * Updates the availability of all commands. + */ +CommandHandler.prototype.updateAvailability = function() { + for (var id in this.commands_) { + this.commands_[id].canExecuteChange(); + } +}; + +/** + * Checks if the handler should ignore the current event, eg. since there is + * a popup dialog currently opened. + * + * @return {boolean} True if the event should be ignored, false otherwise. + * @private + */ +CommandHandler.prototype.shouldIgnoreEvents_ = function() { + // Do not handle commands, when a dialog is shown. + if (this.fileManager_.document.querySelector('.cr-dialog-container.shown')) + return true; + + return false; // Do not ignore. +}; + +/** + * Handles command events. + * @param {Event} event Command event. + * @private + */ +CommandHandler.prototype.onCommand_ = function(event) { + if (this.shouldIgnoreEvents_()) + return; + var handler = CommandHandler.COMMANDS_[event.command.id]; + handler.execute.call(this, event, this.fileManager_); +}; + +/** + * Handles canExecute events. + * @param {Event} event Can execute event. + * @private + */ +CommandHandler.prototype.onCanExecute_ = function(event) { + if (this.shouldIgnoreEvents_()) + return; + var handler = CommandHandler.COMMANDS_[event.command.id]; + handler.canExecute.call(this, event, this.fileManager_); +}; + +/** + * Handle key down event. + * @param {Event} event Key down event. + * @private + */ +CommandHandler.prototype.onKeyDown_ = function(event) { + // 17 is the keycode of Ctrl key and it means the event is not for other keys + // with Ctrl modifier but for ctrl key itself. + if (util.getKeyModifiers(event) + event.keyCode == 'Ctrl-17') { + this.ctrlKeyPressed_ = true; + this.updateAvailability(); + } +}; + +/** + * Handle key up event. + * @param {Event} event Key up event. + * @private + */ +CommandHandler.prototype.onKeyUp_ = function(event) { + // 17 is the keycode of Ctrl key and it means the event is not for other keys + // with Ctrl modifier but for ctrl key itself. + if (util.getKeyModifiers(event) + event.keyCode == '17') { + this.ctrlKeyPressed_ = false; + this.updateAvailability(); + } +}; + +/** + * Commands. + * @type {Object.<string, Command>} + * @const + * @private + */ +CommandHandler.COMMANDS_ = {}; + +/** + * Unmounts external drive. + * @type {Command} + */ +CommandHandler.COMMANDS_['unmount'] = { + /** + * @param {Event} event Command event. + * @param {FileManager} fileManager The file manager instance. + */ + execute: function(event, fileManager) { + var root = CommandUtil.getCommandEntry(event.target); + if (root) + fileManager.unmountVolume(PathUtil.getRootPath(root.fullPath)); + }, + /** + * @param {Event} event Command event. + */ + canExecute: function(event, fileManager) { + var rootType = CommandUtil.getCommandRootType(event.target); + + event.canExecute = (rootType == RootType.ARCHIVE || + rootType == RootType.REMOVABLE); + event.command.setHidden(!event.canExecute); + event.command.label = rootType == RootType.ARCHIVE ? + str('CLOSE_ARCHIVE_BUTTON_LABEL') : + str('UNMOUNT_DEVICE_BUTTON_LABEL'); + } +}; + +/** + * Formats external drive. + * @type {Command} + */ +CommandHandler.COMMANDS_['format'] = { + /** + * @param {Event} event Command event. + * @param {FileManager} fileManager The file manager instance. + */ + execute: function(event, fileManager) { + var directoryModel = fileManager.directoryModel; + var root = CommandUtil.getCommandEntry(event.target); + // If an entry is not found from the event target, use the current + // directory. This can happen for the format button for unsupported and + // unrecognized volumes. + if (!root) + root = directoryModel.getCurrentDirEntry(); + + // TODO(satorux): Stop assuming fullPath to be unique. crbug.com/320967 + var mountPath = root.fullPath; + var volumeInfo = fileManager.volumeManager.getVolumeInfo(mountPath); + if (volumeInfo) { + fileManager.confirm.show( + loadTimeData.getString('FORMATTING_WARNING'), + chrome.fileBrowserPrivate.formatVolume.bind(null, + volumeInfo.volumeId)); + } + }, + /** + * @param {Event} event Command event. + * @param {FileManager} fileManager The file manager instance. + */ + canExecute: function(event, fileManager) { + var directoryModel = fileManager.directoryModel; + var root = CommandUtil.getCommandEntry(event.target); + // See the comment in execute() for why doing this. + if (!root) + root = directoryModel.getCurrentDirEntry(); + var removable = root && + PathUtil.getRootType(root.fullPath) == RootType.REMOVABLE; + // Don't check if the volume is read-only. Unformatted volume is + // considered read-only per directoryModel.isPathReadOnly(), but can be + // formatted. An error will be raised if formatting failed anyway. + event.canExecute = removable; + event.command.setHidden(!removable); + } +}; + +/** + * Initiates new folder creation. + * @type {Command} + */ +CommandHandler.COMMANDS_['new-folder'] = { + execute: function(event, fileManager) { + fileManager.createNewFolder(); + }, + canExecute: function(event, fileManager) { + var directoryModel = fileManager.directoryModel; + event.canExecute = !fileManager.isOnReadonlyDirectory() && + !fileManager.isRenamingInProgress() && + !directoryModel.isSearching() && + !directoryModel.isScanning(); + } +}; + +/** + * Initiates new window creation. + * @type {Command} + */ +CommandHandler.COMMANDS_['new-window'] = { + execute: function(event, fileManager) { + fileManager.backgroundPage.launchFileManager({ + defaultPath: fileManager.getCurrentDirectory() + }); + }, + canExecute: function(event, fileManager) { + event.canExecute = + fileManager.getCurrentDirectoryEntry() && + (fileManager.dialogType === DialogType.FULL_PAGE); + } +}; + +/** + * Deletes selected files. + * @type {Command} + */ +CommandHandler.COMMANDS_['delete'] = { + execute: function(event, fileManager) { + fileManager.deleteSelection(); + }, + canExecute: function(event, fileManager) { + var allowDeletingWhileOffline = + fileManager.directoryModel.getCurrentRootType() === RootType.DRIVE; + var selection = fileManager.getSelection(); + event.canExecute = (!fileManager.isOnReadonlyDirectory() || + allowDeletingWhileOffline) && + selection && + selection.totalCount > 0; + } +}; + +/** + * Pastes files from clipboard. + * @type {Command} + */ +CommandHandler.COMMANDS_['paste'] = { + execute: function() { + document.execCommand(event.command.id); + }, + canExecute: function(event, fileManager) { + var document = fileManager.document; + var fileTransferController = fileManager.fileTransferController; + event.canExecute = (fileTransferController && + fileTransferController.queryPasteCommandEnabled()); + } +}; + +CommandHandler.COMMANDS_['cut'] = CommandUtil.defaultCommand; +CommandHandler.COMMANDS_['copy'] = CommandUtil.defaultCommand; + +/** + * Initiates file renaming. + * @type {Command} + */ +CommandHandler.COMMANDS_['rename'] = { + execute: function(event, fileManager) { + fileManager.initiateRename(); + }, + canExecute: function(event, fileManager) { + var allowRenamingWhileOffline = + fileManager.directoryModel.getCurrentRootType() === RootType.DRIVE; + var selection = fileManager.getSelection(); + event.canExecute = + !fileManager.isRenamingInProgress() && + (!fileManager.isOnReadonlyDirectory() || allowRenamingWhileOffline) && + selection && + selection.totalCount == 1; + } +}; + +/** + * Opens drive help. + * @type {Command} + */ +CommandHandler.COMMANDS_['volume-help'] = { + execute: function(event, fileManager) { + if (fileManager.isOnDrive()) + util.visitURL(str('GOOGLE_DRIVE_HELP_URL')); + else + util.visitURL(str('FILES_APP_HELP_URL')); + }, + canExecute: CommandUtil.canExecuteAlways +}; + +/** + * Opens drive buy-more-space url. + * @type {Command} + */ +CommandHandler.COMMANDS_['drive-buy-more-space'] = { + execute: function(event, fileManager) { + util.visitURL(str('GOOGLE_DRIVE_BUY_STORAGE_URL')); + }, + canExecute: CommandUtil.canExecuteVisibleOnDriveOnly +}; + +/** + * Opens drive.google.com. + * @type {Command} + */ +CommandHandler.COMMANDS_['drive-go-to-drive'] = { + execute: function(event, fileManager) { + util.visitURL(str('GOOGLE_DRIVE_ROOT_URL')); + }, + canExecute: CommandUtil.canExecuteVisibleOnDriveOnly +}; + +/** + * Displays open with dialog for current selection. + * @type {Command} + */ +CommandHandler.COMMANDS_['open-with'] = { + execute: function(event, fileManager) { + var tasks = fileManager.getSelection().tasks; + if (tasks) { + tasks.showTaskPicker(fileManager.defaultTaskPicker, + str('OPEN_WITH_BUTTON_LABEL'), + null, + function(task) { + tasks.execute(task.taskId); + }); + } + }, + canExecute: function(event, fileManager) { + var tasks = fileManager.getSelection().tasks; + event.canExecute = tasks && tasks.size() > 1; + } +}; + +/** + * Focuses search input box. + * @type {Command} + */ +CommandHandler.COMMANDS_['search'] = { + execute: function(event, fileManager) { + var element = fileManager.document.querySelector('#search-box input'); + element.focus(); + element.select(); + }, + canExecute: function(event, fileManager) { + event.canExecute = !fileManager.isRenamingInProgress(); + } +}; + +/** + * Activates the n-th volume. + * @type {Command} + */ +CommandHandler.COMMANDS_['volume-switch-1'] = + CommandUtil.createVolumeSwitchCommand(1); +CommandHandler.COMMANDS_['volume-switch-2'] = + CommandUtil.createVolumeSwitchCommand(2); +CommandHandler.COMMANDS_['volume-switch-3'] = + CommandUtil.createVolumeSwitchCommand(3); +CommandHandler.COMMANDS_['volume-switch-4'] = + CommandUtil.createVolumeSwitchCommand(4); +CommandHandler.COMMANDS_['volume-switch-5'] = + CommandUtil.createVolumeSwitchCommand(5); +CommandHandler.COMMANDS_['volume-switch-6'] = + CommandUtil.createVolumeSwitchCommand(6); +CommandHandler.COMMANDS_['volume-switch-7'] = + CommandUtil.createVolumeSwitchCommand(7); +CommandHandler.COMMANDS_['volume-switch-8'] = + CommandUtil.createVolumeSwitchCommand(8); +CommandHandler.COMMANDS_['volume-switch-9'] = + CommandUtil.createVolumeSwitchCommand(9); + +/** + * Flips 'available offline' flag on the file. + * @type {Command} + */ +CommandHandler.COMMANDS_['toggle-pinned'] = { + execute: function(event, fileManager) { + var pin = !event.command.checked; + event.command.checked = pin; + var entries = CommandUtil.getPinTargetEntries(); + var currentEntry; + var error = false; + var steps = { + // Pick an entry and pin it. + start: function() { + // Check if all the entries are pinned or not. + if (entries.length == 0) + return; + currentEntry = entries.shift(); + chrome.fileBrowserPrivate.pinDriveFile( + currentEntry.toURL(), + pin, + steps.entryPinned); + }, + + // Check the result of pinning + entryPinned: function() { + // Convert to boolean. + error = !!chrome.runtime.lastError; + if (error && pin) { + fileManager.metadataCache_.get( + currentEntry, 'filesystem', steps.showError); + } + fileManager.metadataCache_.clear(currentEntry, 'drive'); + fileManager.metadataCache_.get( + currentEntry, 'drive', steps.updateUI.bind(this)); + }, + + // Update the user interface accoding to the cache state. + updateUI: function(drive) { + fileManager.updateMetadataInUI_( + 'drive', [currentEntry.toURL()], [drive]); + if (!error) + steps.start(); + }, + + // Show the error + showError: function(filesystem) { + fileManager.alert.showHtml(str('DRIVE_OUT_OF_SPACE_HEADER'), + strf('DRIVE_OUT_OF_SPACE_MESSAGE', + unescape(currentEntry.name), + util.bytesToString(filesystem.size))); + } + }; + steps.start(); + }, + + canExecute: function(event, fileManager) { + var entries = CommandUtil.getPinTargetEntries(); + var checked = true; + for (var i = 0; i < entries.length; i++) { + checked = checked && entries[i].pinned; + } + if (entries.length > 0) { + event.canExecute = true; + event.command.setHidden(false); + event.command.checked = checked; + } else { + event.canExecute = false; + event.command.setHidden(true); + } + } +}; + +/** + * Creates zip file for current selection. + * @type {Command} + */ +CommandHandler.COMMANDS_['zip-selection'] = { + execute: function(event, fileManager) { + var dirEntry = fileManager.getCurrentDirectoryEntry(); + var selectionEntries = fileManager.getSelection().entries; + fileManager.fileOperationManager_.zipSelection(dirEntry, selectionEntries); + }, + canExecute: function(event, fileManager) { + var dirEntry = fileManager.getCurrentDirectoryEntry(); + var selection = fileManager.getSelection(); + event.canExecute = + dirEntry && + !fileManager.isOnReadonlyDirectory() && + !fileManager.isOnDrive() && + selection && selection.totalCount > 0; + } +}; + +/** + * Shows the share dialog for the current selection (single only). + * @type {Command} + */ +CommandHandler.COMMANDS_['share'] = { + execute: function(event, fileManager) { + fileManager.shareSelection(); + }, + canExecute: function(event, fileManager) { + var selection = fileManager.getSelection(); + event.canExecute = fileManager.isOnDrive() && + !fileManager.isDriveOffline() && + selection && selection.totalCount == 1; + event.command.setHidden(!fileManager.isOnDrive()); + } +}; + +/** + * Creates a shortcut of the selected folder (single only). + * @type {Command} + */ +CommandHandler.COMMANDS_['create-folder-shortcut'] = { + /** + * @param {Event} event Command event. + * @param {FileManager} fileManager The file manager instance. + */ + execute: function(event, fileManager) { + var entry = CommandUtil.getCommandEntry(event.target); + if (entry) + fileManager.createFolderShortcut(entry.fullPath); + }, + + /** + * @param {Event} event Command event. + * @param {FileManager} fileManager The file manager instance. + */ + canExecute: function(event, fileManager) { + var entry = CommandUtil.getCommandEntry(event.target); + var folderShortcutExists = entry && + fileManager.folderShortcutExists(entry.fullPath); + + var onlyOneFolderSelected = true; + // Only on list, user can select multiple files. The command is enabled only + // when a single file is selected. + if (event.target instanceof cr.ui.List && + !(event.target instanceof NavigationList)) { + var items = event.target.selectedItems; + onlyOneFolderSelected = (items.length == 1 && items[0].isDirectory); + } + + var eligible = entry && + PathUtil.isEligibleForFolderShortcut(entry.fullPath); + event.canExecute = + eligible && onlyOneFolderSelected && !folderShortcutExists; + event.command.setHidden(!eligible || !onlyOneFolderSelected); + } +}; + +/** + * Removes the folder shortcut. + * @type {Command} + */ +CommandHandler.COMMANDS_['remove-folder-shortcut'] = { + /** + * @param {Event} event Command event. + * @param {FileManager} fileManager The file manager instance. + */ + execute: function(event, fileManager) { + var entry = CommandUtil.getCommandEntry(event.target); + if (entry && entry.fullPath) + fileManager.removeFolderShortcut(entry.fullPath); + }, + + /** + * @param {Event} event Command event. + * @param {FileManager} fileManager The file manager instance. + */ + canExecute: function(event, fileManager) { + var entry = CommandUtil.getCommandEntry(event.target); + var path = entry && entry.fullPath; + + var eligible = path && PathUtil.isEligibleForFolderShortcut(path); + var isShortcut = path && fileManager.folderShortcutExists(path); + event.canExecute = isShortcut && eligible; + event.command.setHidden(!event.canExecute); + } +}; + +/** + * Zoom in to the Files.app. + * @type {Command} + */ +CommandHandler.COMMANDS_['zoom-in'] = { + execute: function(event, fileManager) { + chrome.fileBrowserPrivate.zoom('in'); + }, + canExecute: CommandUtil.canExecuteAlways +}; + +/** + * Zoom out from the Files.app. + * @type {Command} + */ +CommandHandler.COMMANDS_['zoom-out'] = { + execute: function(event, fileManager) { + chrome.fileBrowserPrivate.zoom('out'); + }, + canExecute: CommandUtil.canExecuteAlways +}; + +/** + * Reset the zoom factor. + * @type {Command} + */ +CommandHandler.COMMANDS_['zoom-reset'] = { + execute: function(event, fileManager) { + chrome.fileBrowserPrivate.zoom('reset'); + }, + canExecute: CommandUtil.canExecuteAlways +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_operation_manager_wrapper.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_operation_manager_wrapper.js new file mode 100644 index 00000000000..583b9bf549e --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_operation_manager_wrapper.js @@ -0,0 +1,56 @@ +// Copyright 2013 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'; + +/** + * While FileOperationManager is run in the background page, this class is + * used to communicate with it. + * @param {DOMWindow} backgroundPage Window object of the background page. + * @constructor + */ +function FileOperationManagerWrapper(backgroundPage) { + this.fileOperationManager_ = + backgroundPage.FileOperationManager.getInstance(); +} + +/** + * Create a new instance or get existing instance of FCMW. + * @param {DOMWindow} backgroundPage Window object of the background page. + * @return {FileOperationManagerWrapper} FileOperationManagerWrapper instance. + */ +FileOperationManagerWrapper.getInstance = function(backgroundPage) { + if (!FileOperationManagerWrapper.instance_) + FileOperationManagerWrapper.instance_ = + new FileOperationManagerWrapper(backgroundPage); + + return FileOperationManagerWrapper.instance_; +}; + +/** + * @return {boolean} True if there is a running task. + */ +FileOperationManagerWrapper.prototype.isRunning = function() { + return this.fileOperationManager_.hasQueuedTasks(); +}; + +/** + * Decorates a FileOperationManager method, so it will be executed after + * initializing the FileOperationManager instance in background page. + * @param {string} method The method name. + */ +FileOperationManagerWrapper.decorateAsyncMethod = function(method) { + FileOperationManagerWrapper.prototype[method] = function() { + this.fileOperationManager_[method].apply( + this.fileOperationManager_, arguments); + }; +}; + +FileOperationManagerWrapper.decorateAsyncMethod('paste'); +FileOperationManagerWrapper.decorateAsyncMethod('deleteEntries'); +FileOperationManagerWrapper.decorateAsyncMethod('forceDeleteTask'); +FileOperationManagerWrapper.decorateAsyncMethod('cancelDeleteTask'); +FileOperationManagerWrapper.decorateAsyncMethod('zipSelection'); +FileOperationManagerWrapper.decorateAsyncMethod('addEventListener'); +FileOperationManagerWrapper.decorateAsyncMethod('removeEventListener'); diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_selection.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_selection.js new file mode 100644 index 00000000000..945a48bfa0e --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_selection.js @@ -0,0 +1,360 @@ +// 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.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; + this.mimeTypes = null; + + // 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); + + 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_); + + Object.seal(this); +} + +/** + * 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.entries); + callback(); + return; + } + + this.fileManager_.metadataCache_.get(this.entries, '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.entries, 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.previewPanel_; + this.taskItems_ = fileManager.taskItems_; +} + +/** + * Create the temporary disabled action menu item. + * @return {Object} Created disabled item. + * @private + */ +FileSelectionHandler.createTemporaryDisabledActionMenuItem_ = function() { + if (!FileSelectionHandler.cachedDisabledActionMenuItem_) { + FileSelectionHandler.cachedDisabledActionMenuItem_ = { + label: str('ACTION_OPEN'), + disabled: true + }; + } + + return FileSelectionHandler.cachedDisabledActionMenuItem_; +}; + +/** + * Cached the temporary disabled action menu item. Used inside + * FileSelectionHandler.createTemporaryDisabledActionMenuItem_(). + * @private + */ +FileSelectionHandler.cachedDisabledActionMenuItem_ = 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 {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; + } + + // 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; + + if (this.fileManager_.dialogType === DialogType.FULL_PAGE && + selection.directoryCount === 0 && selection.fileCount > 0) { + // Show disabled items for position calculation of the menu. They will be + // overridden in this.updateFileSelectionAsync(). + this.fileManager_.updateContextMenuActionItems( + FileSelectionHandler.createTemporaryDisabledActionMenuItem_(), true); + } else { + // Update context menu. + this.fileManager_.updateContextMenuActionItems(null, false); + } + + this.selectionUpdateTimer_ = setTimeout(function() { + this.selectionUpdateTimer_ = null; + if (this.selection == selection) + this.updateFileSelectionAsync(selection); + }.bind(this), updateDelay); +}; + +/** + * 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.isFolderDialog(dialogType)) { + // 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; +}; + +/** + * 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.previewPanel_.visible; + this.previewPanel_.setSelection(selection); + + // Scroll to item + if (!wasVisible && this.selection.totalCount == 1) { + var list = this.fileManager_.getCurrentList(); + list.scrollIndexIntoView(list.selectionModel.selectedIndex); + } + + // Sync the commands availability. + if (this.fileManager_.commandHandler) + this.fileManager_.commandHandler.updateAvailability(); + + // Inform tests it's OK to click buttons now. + if (selection.totalCount > 0) { + chrome.test.sendMessage('selection-change-complete'); + } +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_table.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_table.js new file mode 100644 index 00000000000..3280de32953 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_table.js @@ -0,0 +1,1036 @@ +// 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'; + +/** + * Namespace for utility functions. + */ +var filelist = {}; + +/** + * Custom column model for advanced auto-resizing. + * + * @param {Array.<cr.ui.table.TableColumn>} tableColumns Table columns. + * @extends {cr.ui.table.TableColumnModel} + * @constructor + */ +function FileTableColumnModel(tableColumns) { + cr.ui.table.TableColumnModel.call(this, tableColumns); +} + +/** + * The columns whose index is less than the constant are resizable. + * @const + * @type {number} + * @private + */ +FileTableColumnModel.RESIZABLE_LENGTH_ = 4; + +/** + * Inherits from cr.ui.TableColumnModel. + */ +FileTableColumnModel.prototype.__proto__ = + cr.ui.table.TableColumnModel.prototype; + +/** + * Minimum width of column. + * @const + * @type {number} + * @private + */ +FileTableColumnModel.MIN_WIDTH_ = 10; + +/** + * Sets column width so that the column dividers move to the specified position. + * This function also check the width of each column and keep the width larger + * than MIN_WIDTH_. + * + * @private + * @param {Array.<number>} newPos Positions of each column dividers. + */ +FileTableColumnModel.prototype.applyColumnPositions_ = function(newPos) { + // Check the minimum width and adjust the positions. + for (var i = 0; i < newPos.length - 2; i++) { + if (newPos[i + 1] - newPos[i] < FileTableColumnModel.MIN_WIDTH_) { + newPos[i + 1] = newPos[i] + FileTableColumnModel.MIN_WIDTH_; + } + } + for (var i = newPos.length - 1; i >= 2; i--) { + if (newPos[i] - newPos[i - 1] < FileTableColumnModel.MIN_WIDTH_) { + newPos[i - 1] = newPos[i] - FileTableColumnModel.MIN_WIDTH_; + } + } + // Set the new width of columns + for (var i = 0; i < FileTableColumnModel.RESIZABLE_LENGTH_; i++) { + this.columns_[i].width = newPos[i + 1] - newPos[i]; + } +}; + +/** + * Normalizes widths to make their sum 100% if possible. Uses the proportional + * approach with some additional constraints. + * + * @param {number} contentWidth Target width. + * @override + */ +FileTableColumnModel.prototype.normalizeWidths = function(contentWidth) { + var totalWidth = 0; + var fixedWidth = 0; + // Some columns have fixed width. + for (var i = 0; i < this.columns_.length; i++) { + if (i < FileTableColumnModel.RESIZABLE_LENGTH_) + totalWidth += this.columns_[i].width; + else + fixedWidth += this.columns_[i].width; + } + var newTotalWidth = Math.max(contentWidth - fixedWidth, 0); + var positions = [0]; + var sum = 0; + for (var i = 0; i < FileTableColumnModel.RESIZABLE_LENGTH_; i++) { + var column = this.columns_[i]; + sum += column.width; + // Faster alternative to Math.floor for non-negative numbers. + positions[i + 1] = ~~(newTotalWidth * sum / totalWidth); + } + this.applyColumnPositions_(positions); +}; + +/** + * Handles to the start of column resizing by splitters. + */ +FileTableColumnModel.prototype.handleSplitterDragStart = function() { + this.columnPos_ = [0]; + for (var i = 0; i < this.columns_.length; i++) { + this.columnPos_[i + 1] = this.columns_[i].width + this.columnPos_[i]; + } +}; + +/** + * Handles to the end of column resizing by splitters. + */ +FileTableColumnModel.prototype.handleSplitterDragEnd = function() { + this.columnPos_ = null; +}; + +/** + * Sets the width of column with keeping the total width of table. + * @param {number} columnIndex Index of column that is resized. + * @param {number} columnWidth New width of the column. + */ +FileTableColumnModel.prototype.setWidthAndKeepTotal = function( + columnIndex, columnWidth) { + // Skip to resize 'selection' column + if (columnIndex < 0 || + columnIndex >= FileTableColumnModel.RESIZABLE_LENGTH_ || + !this.columnPos_) { + return; + } + + // Calculate new positions of column splitters. + var newPosStart = + this.columnPos_[columnIndex] + Math.max(columnWidth, + FileTableColumnModel.MIN_WIDTH_); + var newPos = []; + var posEnd = this.columnPos_[FileTableColumnModel.RESIZABLE_LENGTH_]; + for (var i = 0; i < columnIndex + 1; i++) { + newPos[i] = this.columnPos_[i]; + } + for (var i = columnIndex + 1; + i < FileTableColumnModel.RESIZABLE_LENGTH_; + i++) { + var posStart = this.columnPos_[columnIndex + 1]; + newPos[i] = (posEnd - newPosStart) * + (this.columnPos_[i] - posStart) / + (posEnd - posStart) + + newPosStart; + // Faster alternative to Math.floor for non-negative numbers. + newPos[i] = ~~newPos[i]; + } + newPos[columnIndex] = this.columnPos_[columnIndex]; + newPos[FileTableColumnModel.RESIZABLE_LENGTH_] = posEnd; + this.applyColumnPositions_(newPos); + + // Notifiy about resizing + cr.dispatchSimpleEvent(this, 'resize'); +}; + +/** + * Custom splitter that resizes column with retaining the sum of all the column + * width. + */ +var FileTableSplitter = cr.ui.define('div'); + +/** + * Inherits from cr.ui.TableSplitter. + */ +FileTableSplitter.prototype.__proto__ = cr.ui.TableSplitter.prototype; + +/** + * Handles the drag start event. + */ +FileTableSplitter.prototype.handleSplitterDragStart = function() { + cr.ui.TableSplitter.prototype.handleSplitterDragStart.call(this); + this.table_.columnModel.handleSplitterDragStart(); +}; + +/** + * Handles the drag move event. + * @param {number} deltaX Horizontal mouse move offset. + */ +FileTableSplitter.prototype.handleSplitterDragMove = function(deltaX) { + this.table_.columnModel.setWidthAndKeepTotal(this.columnIndex, + this.columnWidth_ + deltaX, + true); +}; + +/** + * Handles the drag end event. + */ +FileTableSplitter.prototype.handleSplitterDragEnd = function() { + cr.ui.TableSplitter.prototype.handleSplitterDragEnd.call(this); + this.table_.columnModel.handleSplitterDragEnd(); +}; + +/** + * File list Table View. + * @constructor + */ +function FileTable() { + throw new Error('Designed to decorate elements'); +} + +/** + * Inherits from cr.ui.Table. + */ +FileTable.prototype.__proto__ = cr.ui.Table.prototype; + +/** + * Decorates the element. + * @param {HTMLElement} self Table to decorate. + * @param {MetadataCache} metadataCache To retrieve metadata. + * @param {boolean} fullPage True if it's full page File Manager, + * False if a file open/save dialog. + */ +FileTable.decorate = function(self, metadataCache, fullPage) { + cr.ui.Table.decorate(self); + self.__proto__ = FileTable.prototype; + self.metadataCache_ = metadataCache; + self.collator_ = Intl.Collator([], {numeric: true, sensitivity: 'base'}); + + var columns = [ + new cr.ui.table.TableColumn('name', str('NAME_COLUMN_LABEL'), + fullPage ? 386 : 324), + new cr.ui.table.TableColumn('size', str('SIZE_COLUMN_LABEL'), + 110, true), + new cr.ui.table.TableColumn('type', str('TYPE_COLUMN_LABEL'), + fullPage ? 110 : 110), + new cr.ui.table.TableColumn('modificationTime', + str('DATE_COLUMN_LABEL'), + fullPage ? 150 : 210) + ]; + + columns[0].renderFunction = self.renderName_.bind(self); + columns[1].renderFunction = self.renderSize_.bind(self); + columns[1].defaultOrder = 'desc'; + columns[2].renderFunction = self.renderType_.bind(self); + columns[3].renderFunction = self.renderDate_.bind(self); + columns[3].defaultOrder = 'desc'; + + var tableColumnModelClass; + tableColumnModelClass = FileTableColumnModel; + if (self.showCheckboxes) { + columns.push(new cr.ui.table.TableColumn('selection', + '', + 50, true)); + columns[4].renderFunction = self.renderSelection_.bind(self); + columns[4].headerRenderFunction = + self.renderSelectionColumnHeader_.bind(self); + columns[4].fixed = true; + } + + var columnModel = Object.create(tableColumnModelClass.prototype, { + /** + * The number of columns. + * @type {number} + */ + size: { + /** + * @this {FileTableColumnModel} + * @return {number} Number of columns. + */ + get: function() { + return this.totalSize; + } + }, + + /** + * The number of columns. + * @type {number} + */ + totalSize: { + /** + * @this {FileTableColumnModel} + * @return {number} Number of columns. + */ + get: function() { + return columns.length; + } + }, + + /** + * Obtains a column by the specified horizontal position. + */ + getHitColumn: { + /** + * @this {FileTableColumnModel} + * @param {number} x Horizontal position. + * @return {object} The object that contains column index, column width, + * and hitPosition where the horizontal position is hit in the column. + */ + value: function(x) { + for (var i = 0; x >= this.columns_[i].width; i++) { + x -= this.columns_[i].width; + } + if (i >= this.columns_.length) + return null; + return {index: i, hitPosition: x, width: this.columns_[i].width}; + } + } + }); + + tableColumnModelClass.call(columnModel, columns); + self.columnModel = columnModel; + self.setDateTimeFormat(true); + self.setRenderFunction(self.renderTableRow_.bind(self, + self.getRenderFunction())); + + self.scrollBar_ = MainPanelScrollBar(); + self.scrollBar_.initialize(self, self.list); + // Keep focus on the file list when clicking on the header. + self.header.addEventListener('mousedown', function(e) { + self.list.focus(); + e.preventDefault(); + }); + + var handleSelectionChange = function() { + var selectAll = self.querySelector('#select-all-checkbox'); + if (selectAll) + self.updateSelectAllCheckboxState_(selectAll); + }; + + self.relayoutAggregation_ = + new AsyncUtil.Aggregation(self.relayoutImmediately_.bind(self)); + + Object.defineProperty(self.list_, 'selectionModel', { + /** + * @this {cr.ui.List} + * @return {cr.ui.ListSelectionModel} The current selection model. + */ + get: function() { + return this.selectionModel_; + }, + /** + * @this {cr.ui.List} + */ + set: function(value) { + var sm = this.selectionModel; + if (sm) + sm.removeEventListener('change', handleSelectionChange); + + util.callInheritedSetter(this, 'selectionModel', value); + sm = value; + + if (sm) + sm.addEventListener('change', handleSelectionChange); + handleSelectionChange(); + } + }); + + // Override header#redraw to use FileTableSplitter. + self.header_.redraw = function() { + this.__proto__.redraw.call(this); + // Extend table splitters + var splitters = this.querySelectorAll('.table-header-splitter'); + for (var i = 0; i < splitters.length; i++) { + if (splitters[i] instanceof FileTableSplitter) + continue; + FileTableSplitter.decorate(splitters[i]); + } + }; + + // Save the last selection. This is used by shouldStartDragSelection. + self.list.addEventListener('mousedown', function(e) { + this.lastSelection_ = this.selectionModel.selectedIndexes; + }.bind(self), true); + self.list.shouldStartDragSelection = + self.shouldStartDragSelection_.bind(self); + + /** + * Obtains the index list of elements that are hit by the point or the + * rectangle. + * + * @param {number} x X coordinate value. + * @param {number} y Y coordinate value. + * @param {=number} opt_width Width of the coordinate. + * @param {=number} opt_height Height of the coordinate. + * @return {Array.<number>} Index list of hit elements. + */ + self.list.getHitElements = function(x, y, opt_width, opt_height) { + var currentSelection = []; + var bottom = y + (opt_height || 0); + for (var i = 0; i < this.selectionModel_.length; i++) { + var itemMetrics = this.getHeightsForIndex_(i); + if (itemMetrics.top < bottom && itemMetrics.top + itemMetrics.height >= y) + currentSelection.push(i); + } + return currentSelection; + }; +}; + +/** + * Sets date and time format. + * @param {boolean} use12hourClock True if 12 hours clock, False if 24 hours. + */ +FileTable.prototype.setDateTimeFormat = function(use12hourClock) { + this.timeFormatter_ = Intl.DateTimeFormat( + [] /* default locale */, + {hour: 'numeric', minute: 'numeric', + hour12: use12hourClock}); + this.dateFormatter_ = Intl.DateTimeFormat( + [] /* default locale */, + {year: 'numeric', month: 'short', day: 'numeric', + hour: 'numeric', minute: 'numeric', + hour12: use12hourClock}); +}; + +/** + * Obtains if the drag selection should be start or not by referring the mouse + * event. + * @param {MouseEvent} event Drag start event. + * @return {boolean} True if the mouse is hit to the background of the list. + * @private + */ +FileTable.prototype.shouldStartDragSelection_ = function(event) { + // If the shift key is pressed, it should starts drag selection. + if (event.shiftKey) + return true; + + // If the position values are negative, it points the out of list. + // It should start the drag selection. + var pos = DragSelector.getScrolledPosition(this.list, event); + if (!pos) + return false; + if (pos.x < 0 || pos.y < 0) + return true; + + // If the item index is out of range, it should start the drag selection. + var itemHeight = this.list.measureItem().height; + // Faster alternative to Math.floor for non-negative numbers. + var itemIndex = ~~(pos.y / itemHeight); + if (itemIndex >= this.list.dataModel.length) + return true; + + // If the pointed item is already selected, it should not start the drag + // selection. + if (this.lastSelection_.indexOf(itemIndex) != -1) + return false; + + // If the horizontal value is not hit to column, it should start the drag + // selection. + var hitColumn = this.columnModel.getHitColumn(pos.x); + if (!hitColumn) + return true; + + // Check if the point is on the column contents or not. + var item = this.list.getListItemByIndex(itemIndex); + switch (this.columnModel.columns_[hitColumn.index].id) { + case 'name': + var spanElement = item.querySelector('.filename-label span'); + var spanRect = spanElement.getBoundingClientRect(); + // The this.list.cachedBounds_ object is set by + // DragSelector.getScrolledPosition. + if (!this.list.cachedBounds) + return true; + var textRight = + spanRect.left - this.list.cachedBounds.left + spanRect.width; + return textRight <= hitColumn.hitPosition; + default: + return true; + } +}; + +/** + * Update check and disable states of the 'Select all' checkbox. + * @param {HTMLInputElement} checkbox The checkbox. If not passed, using + * the default one. + * @private + */ +FileTable.prototype.updateSelectAllCheckboxState_ = function(checkbox) { + // TODO(serya): introduce this.selectionModel.selectedCount. + checkbox.checked = this.dataModel.length > 0 && + this.dataModel.length == this.selectionModel.selectedIndexes.length; + checkbox.disabled = this.dataModel.length == 0; +}; + +/** + * Prepares the data model to be sorted by columns. + * @param {cr.ui.ArrayDataModel} dataModel Data model to prepare. + */ +FileTable.prototype.setupCompareFunctions = function(dataModel) { + dataModel.setCompareFunction('name', + this.compareName_.bind(this)); + dataModel.setCompareFunction('modificationTime', + this.compareMtime_.bind(this)); + dataModel.setCompareFunction('size', + this.compareSize_.bind(this)); + dataModel.setCompareFunction('type', + this.compareType_.bind(this)); +}; + +/** + * Render the Name column of the detail table. + * + * Invoked by cr.ui.Table when a file needs to be rendered. + * + * @param {Entry} entry The Entry object to render. + * @param {string} columnId The id of the column to be rendered. + * @param {cr.ui.Table} table The table doing the rendering. + * @return {HTMLDivElement} Created element. + * @private + */ +FileTable.prototype.renderName_ = function(entry, columnId, table) { + var label = this.ownerDocument.createElement('div'); + label.appendChild(this.renderIconType_(entry, columnId, table)); + label.entry = entry; + label.className = 'detail-name'; + label.appendChild(filelist.renderFileNameLabel(this.ownerDocument, entry)); + return label; +}; + +/** + * Render the Selection column of the detail table. + * + * Invoked by cr.ui.Table when a file needs to be rendered. + * + * @param {Entry} entry The Entry object to render. + * @param {string} columnId The id of the column to be rendered. + * @param {cr.ui.Table} table The table doing the rendering. + * @return {HTMLDivElement} Created element. + * @private + */ +FileTable.prototype.renderSelection_ = function(entry, columnId, table) { + var label = this.ownerDocument.createElement('div'); + label.className = 'selection-label'; + if (this.selectionModel.multiple) { + var checkBox = this.ownerDocument.createElement('input'); + filelist.decorateSelectionCheckbox(checkBox, entry, this.list); + label.appendChild(checkBox); + } + return label; +}; + +/** + * Render the Size column of the detail table. + * + * @param {Entry} entry The Entry object to render. + * @param {string} columnId The id of the column to be rendered. + * @param {cr.ui.Table} table The table doing the rendering. + * @return {HTMLDivElement} Created element. + * @private + */ +FileTable.prototype.renderSize_ = function(entry, columnId, table) { + var div = this.ownerDocument.createElement('div'); + div.className = 'size'; + this.updateSize_( + div, entry, this.metadataCache_.getCached(entry, 'filesystem')); + + return div; +}; + +/** + * Sets up or updates the size cell. + * + * @param {HTMLDivElement} div The table cell. + * @param {Entry} entry The corresponding entry. + * @param {Object} filesystemProps Metadata. + * @private + */ +FileTable.prototype.updateSize_ = function(div, entry, filesystemProps) { + if (!filesystemProps) { + div.textContent = '...'; + } else if (filesystemProps.size == -1) { + div.textContent = '--'; + } else if (filesystemProps.size == 0 && + FileType.isHosted(entry)) { + div.textContent = '--'; + } else { + div.textContent = util.bytesToString(filesystemProps.size); + } +}; + +/** + * Render the Type column of the detail table. + * + * @param {Entry} entry The Entry object to render. + * @param {string} columnId The id of the column to be rendered. + * @param {cr.ui.Table} table The table doing the rendering. + * @return {HTMLDivElement} Created element. + * @private + */ +FileTable.prototype.renderType_ = function(entry, columnId, table) { + var div = this.ownerDocument.createElement('div'); + div.className = 'type'; + div.textContent = FileType.getTypeString(entry); + return div; +}; + +/** + * Render the Date column of the detail table. + * + * @param {Entry} entry The Entry object to render. + * @param {string} columnId The id of the column to be rendered. + * @param {cr.ui.Table} table The table doing the rendering. + * @return {HTMLDivElement} Created element. + * @private + */ +FileTable.prototype.renderDate_ = function(entry, columnId, table) { + var div = this.ownerDocument.createElement('div'); + div.className = 'date'; + + this.updateDate_(div, + this.metadataCache_.getCached(entry, 'filesystem')); + return div; +}; + +/** + * Sets up or updates the date cell. + * + * @param {HTMLDivElement} div The table cell. + * @param {Object} filesystemProps Metadata. + * @private + */ +FileTable.prototype.updateDate_ = function(div, filesystemProps) { + if (!filesystemProps) { + div.textContent = '...'; + return; + } + + var modTime = filesystemProps.modificationTime; + var today = new Date(); + today.setHours(0); + today.setMinutes(0); + today.setSeconds(0); + today.setMilliseconds(0); + + /** + * Number of milliseconds in a day. + */ + var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; + + if (modTime >= today && + modTime < today.getTime() + MILLISECONDS_IN_DAY) { + div.textContent = strf('TIME_TODAY', this.timeFormatter_.format(modTime)); + } else if (modTime >= today - MILLISECONDS_IN_DAY && modTime < today) { + div.textContent = strf('TIME_YESTERDAY', + this.timeFormatter_.format(modTime)); + } else { + div.textContent = + this.dateFormatter_.format(filesystemProps.modificationTime); + } +}; + +/** + * Updates the file metadata in the table item. + * + * @param {Element} item Table item. + * @param {Entry} entry File entry. + */ +FileTable.prototype.updateFileMetadata = function(item, entry) { + var props = this.metadataCache_.getCached(entry, 'filesystem'); + this.updateDate_(item.querySelector('.date'), props); + this.updateSize_(item.querySelector('.size'), entry, props); +}; + +/** + * Updates list items 'in place' on metadata change. + * @param {string} type Type of metadata change. + * @param {Object.<sting, Object>} propsMap Map from entry URLs to metadata + * properties. + */ +FileTable.prototype.updateListItemsMetadata = function(type, propsMap) { + var forEachCell = function(selector, callback) { + var cells = this.querySelectorAll(selector); + for (var i = 0; i < cells.length; i++) { + var cell = cells[i]; + var listItem = this.list_.getListItemAncestor(cell); + var entry = this.dataModel.item(listItem.listIndex); + if (entry) { + var props = propsMap[entry.toURL()]; + if (props) + callback.call(this, cell, entry, props, listItem); + } + } + }.bind(this); + if (type == 'filesystem') { + forEachCell('.table-row-cell > .date', function(item, entry, props) { + this.updateDate_(item, props); + }); + forEachCell('.table-row-cell > .size', function(item, entry, props) { + this.updateSize_(item, entry, props); + }); + } else if (type == 'drive') { + // The cell name does not matter as the entire list item is needed. + forEachCell('.table-row-cell > .date', + function(item, entry, props, listItem) { + filelist.updateListItemDriveProps(listItem, props); + }); + } +}; + +/** + * Compare by mtime first, then by name. + * @param {Entry} a First entry. + * @param {Entry} b Second entry. + * @return {number} Compare result. + * @private + */ +FileTable.prototype.compareName_ = function(a, b) { + return this.collator_.compare(a.name, b.name); +}; + +/** + * Compare by mtime first, then by name. + * @param {Entry} a First entry. + * @param {Entry} b Second entry. + * @return {number} Compare result. + * @private + */ +FileTable.prototype.compareMtime_ = function(a, b) { + var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem'); + var aTime = aCachedFilesystem ? aCachedFilesystem.modificationTime : 0; + + var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem'); + var bTime = bCachedFilesystem ? bCachedFilesystem.modificationTime : 0; + + if (aTime > bTime) + return 1; + + if (aTime < bTime) + return -1; + + return this.collator_.compare(a.name, b.name); +}; + +/** + * Compare by size first, then by name. + * @param {Entry} a First entry. + * @param {Entry} b Second entry. + * @return {number} Compare result. + * @private + */ +FileTable.prototype.compareSize_ = function(a, b) { + var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem'); + var aSize = aCachedFilesystem ? aCachedFilesystem.size : 0; + + var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem'); + var bSize = bCachedFilesystem ? bCachedFilesystem.size : 0; + + if (aSize != bSize) return aSize - bSize; + return this.collator_.compare(a.name, b.name); +}; + +/** + * Compare by type first, then by subtype and then by name. + * @param {Entry} a First entry. + * @param {Entry} b Second entry. + * @return {number} Compare result. + * @private + */ +FileTable.prototype.compareType_ = function(a, b) { + // Directories precede files. + if (a.isDirectory != b.isDirectory) + return Number(b.isDirectory) - Number(a.isDirectory); + + var aType = FileType.getTypeString(a); + var bType = FileType.getTypeString(b); + + var result = this.collator_.compare(aType, bType); + if (result != 0) + return result; + + return this.collator_.compare(a.name, b.name); +}; + +/** + * Renders table row. + * @param {function(Entry, cr.ui.Table)} baseRenderFunction Base renderer. + * @param {Entry} entry Corresponding entry. + * @return {HTMLLiElement} Created element. + * @private + */ +FileTable.prototype.renderTableRow_ = function(baseRenderFunction, entry) { + var item = baseRenderFunction(entry, this); + filelist.decorateListItem(item, entry, this.metadataCache_); + return item; +}; + +/** + * Renders the name column header. + * @param {string} name Localized column name. + * @return {HTMLLiElement} Created element. + * @private + */ +FileTable.prototype.renderNameColumnHeader_ = function(name) { + if (!this.selectionModel.multiple) + return this.ownerDocument.createTextNode(name); + + var input = this.ownerDocument.createElement('input'); + input.setAttribute('type', 'checkbox'); + input.setAttribute('tabindex', -1); + input.id = 'select-all-checkbox'; + input.className = 'common'; + + this.updateSelectAllCheckboxState_(input); + + input.addEventListener('click', function(event) { + if (input.checked) + this.selectionModel.selectAll(); + else + this.selectionModel.unselectAll(); + event.stopPropagation(); + }.bind(this)); + + var fragment = this.ownerDocument.createDocumentFragment(); + fragment.appendChild(input); + fragment.appendChild(this.ownerDocument.createTextNode(name)); + return fragment; +}; + +/** + * Renders the selection column header. + * @param {string} name Localized column name. + * @return {HTMLLiElement} Created element. + * @private + */ +FileTable.prototype.renderSelectionColumnHeader_ = function(name) { + if (!this.selectionModel.multiple) + return this.ownerDocument.createTextNode(''); + + var input = this.ownerDocument.createElement('input'); + input.setAttribute('type', 'checkbox'); + input.setAttribute('tabindex', -1); + input.id = 'select-all-checkbox'; + input.className = 'common'; + + this.updateSelectAllCheckboxState_(input); + + input.addEventListener('click', function(event) { + if (input.checked) + this.selectionModel.selectAll(); + else + this.selectionModel.unselectAll(); + event.stopPropagation(); + }.bind(this)); + + var fragment = this.ownerDocument.createDocumentFragment(); + fragment.appendChild(input); + return fragment; +}; + +/** + * Render the type column of the detail table. + * + * Invoked by cr.ui.Table when a file needs to be rendered. + * + * @param {Entry} entry The Entry object to render. + * @param {string} columnId The id of the column to be rendered. + * @param {cr.ui.Table} table The table doing the rendering. + * @return {HTMLDivElement} Created element. + * @private + */ +FileTable.prototype.renderIconType_ = function(entry, columnId, table) { + var icon = this.ownerDocument.createElement('div'); + icon.className = 'detail-icon'; + icon.setAttribute('file-type-icon', FileType.getIcon(entry)); + return icon; +}; + +/** + * Sets the margin height for the transparent preview panel at the bottom. + * @param {number} margin Margin to be set in px. + */ +FileTable.prototype.setBottomMarginForPanel = function(margin) { + this.list_.style.paddingBottom = margin + 'px'; + this.scrollBar_.setBottomMarginForPanel(margin); +}; + +/** + * Redraws the UI. Skips multiple consecutive calls. + */ +FileTable.prototype.relayout = function() { + this.relayoutAggregation_.run(); +}; + +/** + * Redraws the UI immediately. + * @private + */ +FileTable.prototype.relayoutImmediately_ = function() { + if (this.clientWidth > 0) + this.normalizeColumns(); + this.redraw(); + cr.dispatchSimpleEvent(this.list, 'relayout'); +}; + +/** + * Decorates (and wire up) a checkbox to be used in either a detail or a + * thumbnail list item. + * @param {HTMLInputElement} input Element to decorate. + */ +filelist.decorateCheckbox = function(input) { + var stopEventPropagation = function(event) { + if (!event.shiftKey) + event.stopPropagation(); + }; + input.setAttribute('type', 'checkbox'); + input.setAttribute('tabindex', -1); + input.classList.add('common'); + input.addEventListener('mousedown', stopEventPropagation); + input.addEventListener('mouseup', stopEventPropagation); + + input.addEventListener( + 'click', + /** + * @this {HTMLInputElement} + */ + function(event) { + // Revert default action and swallow the event + // if this is a multiple click or Shift is pressed. + if (event.detail > 1 || event.shiftKey) { + this.checked = !this.checked; + stopEventPropagation(event); + } + }); +}; + +/** + * Decorates selection checkbox. + * @param {HTMLInputElement} input Element to decorate. + * @param {Entry} entry Corresponding entry. + * @param {cr.ui.List} list Owner list. + */ +filelist.decorateSelectionCheckbox = function(input, entry, list) { + filelist.decorateCheckbox(input); + input.classList.add('file-checkbox'); + input.addEventListener('click', function(e) { + var sm = list.selectionModel; + var listIndex = list.getListItemAncestor(this).listIndex; + sm.setIndexSelected(listIndex, this.checked); + sm.leadIndex = listIndex; + if (sm.anchorIndex == -1) + sm.anchorIndex = listIndex; + + }); + // Since we do not want to open the item when tap on checkbox, we need to + // stop propagation of TAP event dispatched by checkbox ideally. But all + // touch events from touch_handler are dispatched to the list control. So we + // have to stop propagation of native touchstart event to prevent list + // control from generating TAP event here. The synthetic click event will + // select the touched checkbox/item. + input.addEventListener('touchstart', + function(e) { e.stopPropagation() }); + + var index = list.dataModel.indexOf(entry); + // Our DOM nodes get discarded as soon as we're scrolled out of view, + // so we have to make sure the check state is correct when we're brought + // back to life. + input.checked = list.selectionModel.getIndexSelected(index); +}; + +/** + * Common item decoration for table's and grid's items. + * @param {ListItem} li List item. + * @param {Entry} entry The entry. + * @param {MetadataCache} metadataCache Cache to retrieve metadada. + */ +filelist.decorateListItem = function(li, entry, metadataCache) { + li.classList.add(entry.isDirectory ? 'directory' : 'file'); + if (FileType.isOnDrive(entry)) { + // The metadata may not yet be ready. In that case, the list item will be + // updated when the metadata is ready via updateListItemsMetadata. + var driveProps = metadataCache.getCached(entry, 'drive'); + if (driveProps) + filelist.updateListItemDriveProps(li, driveProps); + } + + // Overriding the default role 'list' to 'listbox' for better + // accessibility on ChromeOS. + li.setAttribute('role', 'option'); + + Object.defineProperty(li, 'selected', { + /** + * @this {ListItem} + * @return {boolean} True if the list item is selected. + */ + get: function() { + return this.hasAttribute('selected'); + }, + + /** + * @this {ListItem} + */ + set: function(v) { + if (v) + this.setAttribute('selected', ''); + else + this.removeAttribute('selected'); + var checkBox = this.querySelector('input.file-checkbox'); + if (checkBox) + checkBox.checked = !!v; + } + }); +}; + +/** + * Render filename label for grid and list view. + * @param {HTMLDocument} doc Owner document. + * @param {Entry} entry The Entry object to render. + * @return {HTMLDivElement} The label. + */ +filelist.renderFileNameLabel = function(doc, entry) { + // Filename need to be in a '.filename-label' container for correct + // work of inplace renaming. + var box = doc.createElement('div'); + box.className = 'filename-label'; + var fileName = doc.createElement('span'); + fileName.textContent = entry.name; + box.appendChild(fileName); + + return box; +}; + +/** + * Updates grid item or table row for the driveProps. + * @param {cr.ui.ListItem} li List item. + * @param {Object} driveProps Metadata. + */ +filelist.updateListItemDriveProps = function(li, driveProps) { + if (li.classList.contains('file')) { + if (driveProps.availableOffline) + li.classList.remove('dim-offline'); + else + li.classList.add('dim-offline'); + // TODO(mtomasz): Consider adding some vidual indication for files which + // are not cached on LTE. Currently we show them as normal files. + // crbug.com/246611. + } + + if (driveProps.customIconUrl) { + var iconDiv = li.querySelector('.detail-icon'); + if (!iconDiv) + return; + iconDiv.style.backgroundImage = 'url(' + driveProps.customIconUrl + ')'; + } +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_tasks.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_tasks.js new file mode 100644 index 00000000000..7bde88ca9c2 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_tasks.js @@ -0,0 +1,834 @@ +// 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'; + +/** + * This object encapsulates everything related to tasks execution. + * + * @param {FileManager} fileManager FileManager instance. + * @param {Object=} opt_params File manager load parameters. + * @constructor + */ +function FileTasks(fileManager, opt_params) { + this.fileManager_ = fileManager; + this.params_ = opt_params; + this.tasks_ = null; + this.defaultTask_ = null; + this.entries_ = null; + + /** + * List of invocations to be called once tasks are available. + * + * @private + * @type {Array.<Object>} + */ + this.pendingInvocations_ = []; +} + +/** + * Location of the FAQ about the file actions. + * + * @const + * @type {string} + */ +FileTasks.NO_ACTION_FOR_FILE_URL = 'http://support.google.com/chromeos/bin/' + + 'answer.py?answer=1700055&topic=29026&ctx=topic'; + +/** + * Location of the Chrome Web Store. + * + * @const + * @type {string} + */ +FileTasks.CHROME_WEB_STORE_URL = 'https://chrome.google.com/webstore'; + +/** + * Base URL of apps list in the Chrome Web Store. This constant is used in + * FileTasks.createWebStoreLink(). + * + * @const + * @type {string} + */ +FileTasks.WEB_STORE_HANDLER_BASE_URL = + 'https://chrome.google.com/webstore/category/collection/file_handlers'; + +/** + * Returns URL of the Chrome Web Store which show apps supporting the given + * file-extension and mime-type. + * + * @param {string} extension Extension of the file (with the first dot). + * @param {string} mimeType Mime type of the file. + * @return {string} URL + */ +FileTasks.createWebStoreLink = function(extension, mimeType) { + if (!extension) + return FileTasks.CHROME_WEB_STORE_URL; + + if (extension[0] === '.') + extension = extension.substr(1); + else + console.warn('Please pass an extension with a dot to createWebStoreLink.'); + + var url = FileTasks.WEB_STORE_HANDLER_BASE_URL; + url += '?_fe=' + extension.toLowerCase().replace(/[^\w]/g, ''); + + // If a mime is given, add it into the URL. + if (mimeType) + url += '&_fmt=' + mimeType.replace(/[^-\w\/]/g, ''); + return url; +}; + +/** + * Complete the initialization. + * + * @param {Array.<Entry>} entries List of file entries. + * @param {Array.<string>=} opt_mimeTypes List of MIME types for each + * of the files. + */ +FileTasks.prototype.init = function(entries, opt_mimeTypes) { + this.entries_ = entries; + this.mimeTypes_ = opt_mimeTypes || []; + + // TODO(mtomasz): Move conversion from entry to url to custom bindings. + var urls = util.entriesToURLs(entries); + if (urls.length > 0) { + chrome.fileBrowserPrivate.getFileTasks(urls, this.mimeTypes_, + this.onTasks_.bind(this)); + } +}; + +/** + * Returns amount of tasks. + * + * @return {number} amount of tasks. + */ +FileTasks.prototype.size = function() { + return (this.tasks_ && this.tasks_.length) || 0; +}; + +/** + * Callback when tasks found. + * + * @param {Array.<Object>} tasks The tasks. + * @private + */ +FileTasks.prototype.onTasks_ = function(tasks) { + this.processTasks_(tasks); + for (var index = 0; index < this.pendingInvocations_.length; index++) { + var name = this.pendingInvocations_[index][0]; + var args = this.pendingInvocations_[index][1]; + this[name].apply(this, args); + } + this.pendingInvocations_ = []; +}; + +/** + * The list of known extensions to record UMA. + * Note: Because the data is recorded by the index, so new item shouldn't be + * inserted. + * + * @const + * @type {Array.<string>} + * @private + */ +FileTasks.knownExtensions_ = [ + 'other', '.3ga', '.3gp', '.aac', '.alac', '.asf', '.avi', '.bmp', '.csv', + '.doc', '.docx', '.flac', '.gif', '.ico', '.jpeg', '.jpg', '.log', '.m3u', + '.m3u8', '.m4a', '.m4v', '.mid', '.mkv', '.mov', '.mp3', '.mp4', '.mpg', + '.odf', '.odp', '.ods', '.odt', '.oga', '.ogg', '.ogv', '.pdf', '.png', + '.ppt', '.pptx', '.ra', '.ram', '.rar', '.rm', '.rtf', '.wav', '.webm', + '.webp', '.wma', '.wmv', '.xls', '.xlsx', +]; + +/** + * The list of excutable file extensions. + * + * @const + * @type {Array.<string>} + */ +FileTasks.EXECUTABLE_EXTENSIONS = Object.freeze([ + '.exe', '.lnk', '.deb', '.dmg', '.jar', '.msi', +]); + +/** + * The list of extensions to skip the suggest app dialog. + * @const + * @type {Array.<string>} + * @private + */ +FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_ = Object.freeze([ + '.crdownload', '.dsc', '.inf', '.crx', +]); + +/** + * Records trial of opening file grouped by extensions. + * + * @param {Array.<Entry>} entries The entries to be opened. + * @private + */ +FileTasks.recordViewingFileTypeUMA_ = function(entries) { + for (var i = 0; i < entries.length; i++) { + var entry = entries[i]; + var extension = FileType.getExtension(entry).toLowerCase(); + if (FileTasks.knownExtensions_.indexOf(extension) < 0) { + extension = 'other'; + } + metrics.recordEnum( + 'ViewingFileType', extension, FileTasks.knownExtensions_); + } +}; + +/** + * Returns true if the taskId is for an internal task. + * + * @param {string} taskId Task identifier. + * @return {boolean} True if the task ID is for an internal task. + * @private + */ +FileTasks.isInternalTask_ = function(taskId) { + var taskParts = taskId.split('|'); + var appId = taskParts[0]; + var taskType = taskParts[1]; + var actionId = taskParts[2]; + // The action IDs here should match ones used in executeInternalTask_(). + return (appId === chrome.runtime.id && + taskType === 'file' && + (actionId === 'play' || + actionId === 'watch' || + actionId === 'mount-archive' || + actionId === 'gallery')); +}; + +/** + * Processes internal tasks. + * + * @param {Array.<Object>} tasks The tasks. + * @private + */ +FileTasks.prototype.processTasks_ = function(tasks) { + this.tasks_ = []; + var id = chrome.runtime.id; + var isOnDrive = false; + for (var index = 0; index < this.entries_.length; ++index) { + if (FileType.isOnDrive(this.entries_[index])) { + isOnDrive = true; + break; + } + } + + for (var i = 0; i < tasks.length; i++) { + var task = tasks[i]; + var taskParts = task.taskId.split('|'); + + // Skip internal Files.app's handlers. + if (taskParts[0] === id && (taskParts[2] === 'auto-open' || + taskParts[2] === 'select' || taskParts[2] === 'open')) { + continue; + } + + // Tweak images, titles of internal tasks. + if (taskParts[0] === id && taskParts[1] === 'file') { + if (taskParts[2] === 'play') { + // TODO(serya): This hack needed until task.iconUrl is working + // (see GetFileTasksFileBrowserFunction::RunImpl). + task.iconType = 'audio'; + task.title = loadTimeData.getString('ACTION_LISTEN'); + } else if (taskParts[2] === 'mount-archive') { + task.iconType = 'archive'; + task.title = loadTimeData.getString('MOUNT_ARCHIVE'); + } else if (taskParts[2] === 'gallery') { + task.iconType = 'image'; + task.title = loadTimeData.getString('ACTION_OPEN'); + } else if (taskParts[2] === 'watch') { + task.iconType = 'video'; + task.title = loadTimeData.getString('ACTION_WATCH'); + } else if (taskParts[2] === 'open-hosted-generic') { + if (this.entries_.length > 1) + task.iconType = 'generic'; + else // Use specific icon. + task.iconType = FileType.getIcon(this.entries_[0]); + task.title = loadTimeData.getString('ACTION_OPEN'); + } else if (taskParts[2] === 'open-hosted-gdoc') { + task.iconType = 'gdoc'; + task.title = loadTimeData.getString('ACTION_OPEN_GDOC'); + } else if (taskParts[2] === 'open-hosted-gsheet') { + task.iconType = 'gsheet'; + task.title = loadTimeData.getString('ACTION_OPEN_GSHEET'); + } else if (taskParts[2] === 'open-hosted-gslides') { + task.iconType = 'gslides'; + task.title = loadTimeData.getString('ACTION_OPEN_GSLIDES'); + } else if (taskParts[2] === 'view-swf') { + // Do not render this task if disabled. + if (!loadTimeData.getBoolean('SWF_VIEW_ENABLED')) + continue; + task.iconType = 'generic'; + task.title = loadTimeData.getString('ACTION_VIEW'); + } else if (taskParts[2] === 'view-pdf') { + // Do not render this task if disabled. + if (!loadTimeData.getBoolean('PDF_VIEW_ENABLED')) + continue; + task.iconType = 'pdf'; + task.title = loadTimeData.getString('ACTION_VIEW'); + } else if (taskParts[2] === 'view-in-browser') { + task.iconType = 'generic'; + task.title = loadTimeData.getString('ACTION_VIEW'); + } + } + + if (!task.iconType && taskParts[1] === 'web-intent') { + task.iconType = 'generic'; + } + + this.tasks_.push(task); + if (this.defaultTask_ === null && task.isDefault) { + this.defaultTask_ = task; + } + } + if (!this.defaultTask_ && this.tasks_.length > 0) { + // If we haven't picked a default task yet, then just pick the first one. + // This is not the preferred way we want to pick this, but better this than + // no default at all if the C++ code didn't set one. + this.defaultTask_ = this.tasks_[0]; + } +}; + +/** + * Executes default task. + * + * @param {function(boolean, Array.<string>)=} opt_callback Called when the + * default task is executed, or the error is occurred. + * @private + */ +FileTasks.prototype.executeDefault_ = function(opt_callback) { + FileTasks.recordViewingFileTypeUMA_(this.entries_); + this.executeDefaultInternal_(this.entries_, opt_callback); +}; + +/** + * Executes default task. + * + * @param {Array.<Entry>} entries Entries to execute. + * @param {function(boolean, Array.<Entry>)=} opt_callback Called when the + * default task is executed, or the error is occurred. + * @private + */ +FileTasks.prototype.executeDefaultInternal_ = function(entries, opt_callback) { + var callback = opt_callback || function(arg1, arg2) {}; + + if (this.defaultTask_ !== null) { + this.executeInternal_(this.defaultTask_.taskId, entries); + callback(true, entries); + return; + } + + // We don't have tasks, so try to show a file in a browser tab. + // We only do that for single selection to avoid confusion. + if (entries.length !== 1 || !entries[0]) + return; + + var filename = entries[0].name; + var extension = PathUtil.splitExtension(filename)[1]; + var mimeType = this.mimeTypes_[0]; + + var showAlert = function() { + var textMessageId; + var titleMessageId; + switch (extension) { + case '.exe': + textMessageId = 'NO_ACTION_FOR_EXECUTABLE'; + break; + case '.crx': + textMessageId = 'NO_ACTION_FOR_CRX'; + titleMessageId = 'NO_ACTION_FOR_CRX_TITLE'; + break; + default: + textMessageId = 'NO_ACTION_FOR_FILE'; + } + + var webStoreUrl = FileTasks.createWebStoreLink(extension, mimeType); + var text = strf(textMessageId, + webStoreUrl, + FileTasks.NO_ACTION_FOR_FILE_URL); + var title = titleMessageId ? str(titleMessageId) : filename; + this.fileManager_.alert.showHtml(title, text, function() {}); + callback(false, urls); + }.bind(this); + + var onViewFilesFailure = function() { + var fm = this.fileManager_; + if (fm.enableExperimentalWebStoreIntegration_) { + showAlert(); + return; + } + + if (!fm.isOnDrive() || + !entries[0] || + FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_.indexOf(extension) !== -1) { + showAlert(); + return; + } + + fm.openSuggestAppsDialog( + entries[0], + function() { + var newTasks = new FileTasks(fm); + newTasks.init(entries, this.mimeTypes_); + newTasks.executeDefault(); + callback(true, entries); + }.bind(this), + // Cancelled callback. + function() { + callback(false, entries); + }, + showAlert); + }.bind(this); + + var onViewFiles = function(success) { + if (success) + callback(success, entries); + else + onViewFilesFailure(); + }.bind(this); + + this.checkAvailability_(function() { + // TODO(mtomasz): Pass entries instead. + var urls = util.entriesToURLs(entries); + util.viewFilesInBrowser(urls, onViewFiles); + }.bind(this)); +}; + +/** + * Executes a single task. + * + * @param {string} taskId Task identifier. + * @param {Array.<Entry>=} opt_entries Entries to xecute on instead of + * this.entries_|. + * @private + */ +FileTasks.prototype.execute_ = function(taskId, opt_entries) { + var entries = opt_entries || this.entries_; + FileTasks.recordViewingFileTypeUMA_(entries); + this.executeInternal_(taskId, entries); +}; + +/** + * The core implementation to execute a single task. + * + * @param {string} taskId Task identifier. + * @param {Array.<Entry>} entries Entries to execute. + * @private + */ +FileTasks.prototype.executeInternal_ = function(taskId, entries) { + this.checkAvailability_(function() { + if (FileTasks.isInternalTask_(taskId)) { + var taskParts = taskId.split('|'); + this.executeInternalTask_(taskParts[2], entries); + } else { + // TODO(mtomasz): Pass entries instead. + var urls = util.entriesToURLs(entries); + chrome.fileBrowserPrivate.executeTask(taskId, urls); + } + }.bind(this)); +}; + +/** + * Checks whether the remote files are available right now. + * + * @param {function} callback The callback. + * @private + */ +FileTasks.prototype.checkAvailability_ = function(callback) { + var areAll = function(props, name) { + var isOne = function(e) { + // If got no properties, we safely assume that item is unavailable. + return e && e[name]; + }; + return props.filter(isOne).length === props.length; + }; + + var fm = this.fileManager_; + var entries = this.entries_; + + if (fm.isOnDrive() && fm.isDriveOffline()) { + fm.metadataCache_.get(entries, 'drive', function(props) { + if (areAll(props, 'availableOffline')) { + callback(); + return; + } + + fm.alert.showHtml( + loadTimeData.getString('OFFLINE_HEADER'), + props[0].hosted ? + loadTimeData.getStringF( + entries.length === 1 ? + 'HOSTED_OFFLINE_MESSAGE' : + 'HOSTED_OFFLINE_MESSAGE_PLURAL') : + loadTimeData.getStringF( + entries.length === 1 ? + 'OFFLINE_MESSAGE' : + 'OFFLINE_MESSAGE_PLURAL', + loadTimeData.getString('OFFLINE_COLUMN_LABEL'))); + }); + return; + } + + if (fm.isOnDrive() && fm.isDriveOnMeteredConnection()) { + fm.metadataCache_.get(entries, 'drive', function(driveProps) { + if (areAll(driveProps, 'availableWhenMetered')) { + callback(); + return; + } + + fm.metadataCache_.get(entries, 'filesystem', function(fileProps) { + var sizeToDownload = 0; + for (var i = 0; i !== entries.length; i++) { + if (!driveProps[i].availableWhenMetered) + sizeToDownload += fileProps[i].size; + } + fm.confirm.show( + loadTimeData.getStringF( + entries.length === 1 ? + 'CONFIRM_MOBILE_DATA_USE' : + 'CONFIRM_MOBILE_DATA_USE_PLURAL', + util.bytesToString(sizeToDownload)), + callback); + }); + }); + return; + } + + callback(); +}; + +/** + * Executes an internal task. + * + * @param {string} id The short task id. + * @param {Array.<Entry>} entries The entries to execute on. + * @private + */ +FileTasks.prototype.executeInternalTask_ = function(id, entries) { + var fm = this.fileManager_; + + if (id === 'play') { + var position = 0; + if (entries.length === 1) { + // If just a single audio file is selected pass along every audio file + // in the directory. + var selectedEntries = entries[0]; + entries = fm.getAllEntriesInCurrentDirectory().filter(FileType.isAudio); + position = entries.indexOf(selectedEntries); + } + // TODO(mtomasz): Pass entries instead. + var urls = util.entriesToURLs(entries); + fm.backgroundPage.launchAudioPlayer({items: urls, position: position}); + return; + } + + if (id === 'watch') { + console.assert(entries.length === 1, 'Cannot open multiple videos'); + // TODO(mtomasz): Pass an entry instead. + fm.backgroundPage.launchVideoPlayer(entries[0].toURL()); + return; + } + + if (id === 'mount-archive') { + this.mountArchivesInternal_(entries); + return; + } + + if (id === 'gallery') { + this.openGalleryInternal_(entries); + return; + } + + console.error('Unexpected action ID: ' + id); +}; + +/** + * Mounts archives. + * + * @param {Array.<Entry>} entries Mount file entries list. + */ +FileTasks.prototype.mountArchives = function(entries) { + FileTasks.recordViewingFileTypeUMA_(entries); + this.mountArchivesInternal_(entries); +}; + +/** + * The core implementation of mounts archives. + * + * @param {Array.<Entry>} entries Mount file entries list. + * @private + */ +FileTasks.prototype.mountArchivesInternal_ = function(entries) { + var fm = this.fileManager_; + + var tracker = fm.directoryModel.createDirectoryChangeTracker(); + tracker.start(); + + // TODO(mtomasz): Pass Entries instead of URLs. + var urls = util.entriesToURLs(entries); + fm.resolveSelectResults_(urls, function(resolvedURLs) { + for (var index = 0; index < resolvedURLs.length; ++index) { + // TODO(mtomasz): Pass Entry instead of URL. + fm.volumeManager.mountArchive(resolvedURLs[index], + function(mountPath) { + tracker.stop(); + if (!tracker.hasChanged) + fm.directoryModel.changeDirectory(mountPath); + }, function(url, error) { + tracker.stop(); + var path = util.extractFilePath(url); + var namePos = path.lastIndexOf('/'); + fm.alert.show(strf('ARCHIVE_MOUNT_FAILED', + path.substr(namePos + 1), error)); + }.bind(null, resolvedURLs[index])); + } + }); +}; + +/** + * Open the Gallery. + * + * @param {Array.<Entry>} entries List of selected entries. + */ +FileTasks.prototype.openGallery = function(entries) { + FileTasks.recordViewingFileTypeUMA_(entries); + this.openGalleryInternal_(entries); +}; + +/** + * The core implementation to open the Gallery. + * + * @param {Array.<Entry>} entries List of selected entries. + * @private + */ +FileTasks.prototype.openGalleryInternal_ = function(entries) { + var fm = this.fileManager_; + + var allEntries = + fm.getAllEntriesInCurrentDirectory().filter(FileType.isImageOrVideo); + + var galleryFrame = fm.document_.createElement('iframe'); + galleryFrame.className = 'overlay-pane'; + galleryFrame.scrolling = 'no'; + galleryFrame.setAttribute('webkitallowfullscreen', true); + + if (this.params_ && this.params_.gallery) { + // Remove the Gallery state from the location, we do not need it any more. + util.updateAppState(null /* keep path */, '' /* remove search. */); + } + + var savedAppState = window.appState; + var savedTitle = document.title; + + // Push a temporary state which will be replaced every time the selection + // changes in the Gallery and popped when the Gallery is closed. + util.updateAppState(); + + var onBack = function(selectedEntries) { + fm.directoryModel.selectEntries(selectedEntries); + fm.closeFilePopup(); // Will call Gallery.unload. + window.appState = savedAppState; + util.saveAppState(); + document.title = savedTitle; + }; + + var onClose = function() { + fm.onClose(); + }; + + var onMaximize = function() { + fm.onMaximize(); + }; + + var onAppRegionChanged = function(visible) { + fm.onFilePopupAppRegionChanged(visible); + }; + + galleryFrame.onload = function() { + galleryFrame.contentWindow.ImageUtil.metrics = metrics; + + // TODO(haruki): isOnReadonlyDirectory() only checks the permission for the + // root. We should check more granular permission to know whether the file + // is writable or not. + var readonly = fm.isOnReadonlyDirectory(); + var currentDir = fm.getCurrentDirectoryEntry(); + var downloadsVolume = + fm.volumeManager.getCurrentProfileVolumeInfo(RootType.DOWNLOADS); + var downloadsDir = downloadsVolume && downloadsVolume.root; + var readonlyDirName = null; + if (readonly && currentDir) { + var rootPath = PathUtil.getRootPath(currentDir.fullPath); + readonlyDirName = fm.isOnDrive() ? + PathUtil.getRootLabel(rootPath) : + PathUtil.basename(rootPath); + } + + var context = { + // We show the root label in readonly warning (e.g. archive name). + readonlyDirName: readonlyDirName, + curDirEntry: currentDir, + saveDirEntry: readonly ? downloadsDir : null, + searchResults: fm.directoryModel.isSearching(), + metadataCache: fm.metadataCache_, + pageState: this.params_, + appWindow: chrome.app.window.current(), + onBack: onBack, + onClose: onClose, + onMaximize: onMaximize, + onAppRegionChanged: onAppRegionChanged, + displayStringFunction: strf + }; + galleryFrame.contentWindow.Gallery.open( + context, fm.volumeManager, allEntries, entries); + }.bind(this); + + galleryFrame.src = 'gallery.html'; + fm.openFilePopup(galleryFrame, fm.updateTitle_.bind(fm)); +}; + +/** + * Displays the list of tasks in a task picker combobutton. + * + * @param {cr.ui.ComboButton} combobutton The task picker element. + * @private + */ +FileTasks.prototype.display_ = function(combobutton) { + if (this.tasks_.length === 0) { + combobutton.hidden = true; + return; + } + + combobutton.clear(); + combobutton.hidden = false; + combobutton.defaultItem = this.createCombobuttonItem_(this.defaultTask_); + + var items = this.createItems_(); + + if (items.length > 1) { + var defaultIdx = 0; + + for (var j = 0; j < items.length; j++) { + combobutton.addDropDownItem(items[j]); + if (items[j].task.taskId === this.defaultTask_.taskId) + defaultIdx = j; + } + + combobutton.addSeparator(); + var changeDefaultMenuItem = combobutton.addDropDownItem({ + label: loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM') + }); + changeDefaultMenuItem.classList.add('change-default'); + } +}; + +/** + * Creates sorted array of available task descriptions such as title and icon. + * + * @return {Array} created array can be used to feed combobox, menus and so on. + * @private + */ +FileTasks.prototype.createItems_ = function() { + var items = []; + var title = this.defaultTask_.title + ' ' + + loadTimeData.getString('DEFAULT_ACTION_LABEL'); + items.push(this.createCombobuttonItem_(this.defaultTask_, title, true)); + + for (var index = 0; index < this.tasks_.length; index++) { + var task = this.tasks_[index]; + if (task !== this.defaultTask_) + items.push(this.createCombobuttonItem_(task)); + } + + items.sort(function(a, b) { + return a.label.localeCompare(b.label); + }); + + return items; +}; + +/** + * Updates context menu with default item. + * @private + */ + +FileTasks.prototype.updateMenuItem_ = function() { + this.fileManager_.updateContextMenuActionItems(this.defaultTask_, + this.tasks_.length > 1); +}; + +/** + * Creates combobutton item based on task. + * + * @param {Object} task Task to convert. + * @param {string=} opt_title Title. + * @param {boolean=} opt_bold Make a menu item bold. + * @return {Object} Item appendable to combobutton drop-down list. + * @private + */ +FileTasks.prototype.createCombobuttonItem_ = function(task, opt_title, + opt_bold) { + return { + label: opt_title || task.title, + iconUrl: task.iconUrl, + iconType: task.iconType, + task: task, + bold: opt_bold || false + }; +}; + + +/** + * Decorates a FileTasks method, so it will be actually executed after the tasks + * are available. + * This decorator expects an implementation called |method + '_'|. + * + * @param {string} method The method name. + */ +FileTasks.decorate = function(method) { + var privateMethod = method + '_'; + FileTasks.prototype[method] = function() { + if (this.tasks_) { + this[privateMethod].apply(this, arguments); + } else { + this.pendingInvocations_.push([privateMethod, arguments]); + } + return this; + }; +}; + +/** + * Shows modal action picker dialog with currently available list of tasks. + * + * @param {DefaultActionDialog} actionDialog Action dialog to show and update. + * @param {string} title Title to use. + * @param {string} message Message to use. + * @param {function(Object)} onSuccess Callback to pass selected task. + */ +FileTasks.prototype.showTaskPicker = function(actionDialog, title, message, + onSuccess) { + var items = this.createItems_(); + + var defaultIdx = 0; + for (var j = 0; j < items.length; j++) { + if (items[j].task.taskId === this.defaultTask_.taskId) + defaultIdx = j; + } + + actionDialog.show( + title, + message, + items, defaultIdx, + function(item) { + onSuccess(item.task); + }); +}; + +FileTasks.decorate('display'); +FileTasks.decorate('updateMenuItem'); +FileTasks.decorate('execute'); +FileTasks.decorate('executeDefault'); diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_transfer_controller.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_transfer_controller.js new file mode 100644 index 00000000000..3f8900c88d1 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_transfer_controller.js @@ -0,0 +1,860 @@ +// 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'; + +/** + * Global (placed in the window object) variable name to hold internal + * file dragging information. Needed to show visual feedback while dragging + * since DataTransfer object is in protected state. Reachable from other + * file manager instances. + */ +var DRAG_AND_DROP_GLOBAL_DATA = '__drag_and_drop_global_data'; + +/** + * @param {HTMLDocument} doc Owning document. + * @param {FileOperationManager} fileOperationManager File operation manager + * instance. + * @param {MetadataCache} metadataCache Metadata cache service. + * @param {DirectoryModel} directoryModel Directory model instance. + * @constructor + */ +function FileTransferController(doc, + fileOperationManager, + metadataCache, + directoryModel) { + this.document_ = doc; + this.fileOperationManager_ = fileOperationManager; + this.metadataCache_ = metadataCache; + this.directoryModel_ = directoryModel; + + this.directoryModel_.getFileListSelection().addEventListener('change', + this.onSelectionChanged_.bind(this)); + + /** + * DOM element to represent selected file in drag operation. Used if only + * one element is selected. + * @type {HTMLElement} + * @private + */ + this.preloadedThumbnailImageNode_ = null; + + /** + * File objects for selected files. + * + * @type {Array.<File>} + * @private + */ + this.selectedFileObjects_ = []; + + /** + * Drag selector. + * @type {DragSelector} + * @private + */ + this.dragSelector_ = new DragSelector(); + + /** + * Whether a user is touching the device or not. + * @type {boolean} + * @private + */ + this.touching_ = false; +} + +FileTransferController.prototype = { + __proto__: cr.EventTarget.prototype, + + /** + * @this {FileTransferController} + * @param {cr.ui.List} list Items in the list will be draggable. + */ + attachDragSource: function(list) { + list.style.webkitUserDrag = 'element'; + list.addEventListener('dragstart', this.onDragStart_.bind(this, list)); + list.addEventListener('dragend', this.onDragEnd_.bind(this, list)); + list.addEventListener('touchstart', this.onTouchStart_.bind(this)); + list.addEventListener('touchend', this.onTouchEnd_.bind(this)); + }, + + /** + * @this {FileTransferController} + * @param {cr.ui.List} list List itself and its directory items will could + * be drop target. + * @param {boolean=} opt_onlyIntoDirectories If true only directory list + * items could be drop targets. Otherwise any other place of the list + * accetps files (putting it into the current directory). + */ + attachFileListDropTarget: function(list, opt_onlyIntoDirectories) { + list.addEventListener('dragover', this.onDragOver_.bind(this, + !!opt_onlyIntoDirectories, list)); + list.addEventListener('dragenter', + this.onDragEnterFileList_.bind(this, list)); + list.addEventListener('dragleave', this.onDragLeave_.bind(this, list)); + list.addEventListener('drop', + this.onDrop_.bind(this, !!opt_onlyIntoDirectories)); + }, + + /** + * @this {FileTransferController} + * @param {DirectoryTree} tree Its sub items will could be drop target. + */ + attachTreeDropTarget: function(tree) { + tree.addEventListener('dragover', this.onDragOver_.bind(this, true, tree)); + tree.addEventListener('dragenter', this.onDragEnterTree_.bind(this, tree)); + tree.addEventListener('dragleave', this.onDragLeave_.bind(this, tree)); + tree.addEventListener('drop', this.onDrop_.bind(this, true)); + }, + + /** + * @this {FileTransferController} + * @param {NavigationList} tree Its sub items will could be drop target. + */ + attachNavigationListDropTarget: function(list) { + list.addEventListener('dragover', + this.onDragOver_.bind(this, true /* onlyIntoDirectories */, list)); + list.addEventListener('dragenter', + this.onDragEnterVolumesList_.bind(this, list)); + list.addEventListener('dragleave', this.onDragLeave_.bind(this, list)); + list.addEventListener('drop', + this.onDrop_.bind(this, true /* onlyIntoDirectories */)); + }, + + /** + * Attach handlers of copy, cut and paste operations to the document. + * + * @this {FileTransferController} + */ + attachCopyPasteHandlers: function() { + this.document_.addEventListener('beforecopy', + this.onBeforeCopy_.bind(this)); + this.document_.addEventListener('copy', + this.onCopy_.bind(this)); + this.document_.addEventListener('beforecut', + this.onBeforeCut_.bind(this)); + this.document_.addEventListener('cut', + this.onCut_.bind(this)); + this.document_.addEventListener('beforepaste', + this.onBeforePaste_.bind(this)); + this.document_.addEventListener('paste', + this.onPaste_.bind(this)); + this.copyCommand_ = this.document_.querySelector('command#copy'); + }, + + /** + * Write the current selection to system clipboard. + * + * @this {FileTransferController} + * @param {DataTransfer} dataTransfer DataTransfer from the event. + * @param {string} effectAllowed Value must be valid for the + * |dataTransfer.effectAllowed| property ('move', 'copy', 'copyMove'). + */ + cutOrCopy_: function(dataTransfer, effectAllowed) { + // Tag to check it's filemanager data. + dataTransfer.setData('fs/tag', 'filemanager-data'); + dataTransfer.setData('fs/sourceRoot', + this.directoryModel_.getCurrentRootPath()); + var sourcePaths = + this.selectedEntries_.map(function(e) { return e.fullPath; }); + dataTransfer.setData('fs/sources', sourcePaths.join('\n')); + dataTransfer.effectAllowed = effectAllowed; + dataTransfer.setData('fs/effectallowed', effectAllowed); + + for (var i = 0; i < this.selectedFileObjects_.length; i++) { + dataTransfer.items.add(this.selectedFileObjects_[i]); + } + }, + + /** + * Extracts source root from the |dataTransfer| object. + * + * @this {FileTransferController} + * @param {DataTransfer} dataTransfer DataTransfer object from the event. + * @return {string} Path or empty string (if unknown). + */ + getSourceRoot_: function(dataTransfer) { + var sourceRoot = dataTransfer.getData('fs/sourceRoot'); + if (sourceRoot) + return sourceRoot; + + // |dataTransfer| in protected mode. + if (window[DRAG_AND_DROP_GLOBAL_DATA]) + return window[DRAG_AND_DROP_GLOBAL_DATA].sourceRoot; + + // Dragging from other tabs/windows. + var views = chrome && chrome.extension ? chrome.extension.getViews() : []; + for (var i = 0; i < views.length; i++) { + if (views[i][DRAG_AND_DROP_GLOBAL_DATA]) + return views[i][DRAG_AND_DROP_GLOBAL_DATA].sourceRoot; + } + + // Unknown source. + return ''; + }, + + /** + * Queue up a file copy operation based on the current system clipboard. + * + * @this {FileTransferController} + * @param {DataTransfer} dataTransfer System data transfer object. + * @param {string=} opt_destinationPath Paste destination. + * @param {string=} opt_effect Desired drop/paste effect. Could be + * 'move'|'copy' (default is copy). Ignored if conflicts with + * |dataTransfer.effectAllowed|. + * @return {string} Either "copy" or "move". + */ + paste: function(dataTransfer, opt_destinationPath, opt_effect) { + var sourcePaths = (dataTransfer.getData('fs/sources') || '').split('\n'); + var destinationPath = opt_destinationPath || + this.currentDirectoryContentPath; + // effectAllowed set in copy/paste handlers stay uninitialized. DnD handlers + // work fine. + var effectAllowed = dataTransfer.effectAllowed != 'uninitialized' ? + dataTransfer.effectAllowed : dataTransfer.getData('fs/effectallowed'); + var toMove = effectAllowed == 'move' || + (effectAllowed == 'copyMove' && opt_effect == 'move'); + + // Start the pasting operation. + this.fileOperationManager_.paste(sourcePaths, destinationPath, toMove); + return toMove ? 'move' : 'copy'; + }, + + /** + * Preloads an image thumbnail for the specified file entry. + * + * @this {FileTransferController} + * @param {Entry} entry Entry to preload a thumbnail for. + */ + preloadThumbnailImage_: function(entry) { + var metadataTypes = 'thumbnail|filesystem'; + var thumbnailContainer = this.document_.createElement('div'); + this.preloadedThumbnailImageNode_ = thumbnailContainer; + this.preloadedThumbnailImageNode_.className = 'img-container'; + this.metadataCache_.get( + entry, + metadataTypes, + function(metadata) { + new ThumbnailLoader(entry.toURL(), + ThumbnailLoader.LoaderType.IMAGE, + metadata). + load(thumbnailContainer, + ThumbnailLoader.FillMode.FILL); + }.bind(this)); + }, + + /** + * Renders a drag-and-drop thumbnail. + * + * @this {FileTransferController} + * @return {HTMLElement} Element containing the thumbnail. + */ + renderThumbnail_: function() { + var length = this.selectedEntries_.length; + + var container = this.document_.querySelector('#drag-container'); + var contents = this.document_.createElement('div'); + contents.className = 'drag-contents'; + container.appendChild(contents); + + var thumbnailImage; + if (this.preloadedThumbnailImageNode_) + thumbnailImage = this.preloadedThumbnailImageNode_.querySelector('img'); + + // Option 1. Multiple selection, render only a label. + if (length > 1) { + var label = this.document_.createElement('div'); + label.className = 'label'; + label.textContent = strf('DRAGGING_MULTIPLE_ITEMS', length); + contents.appendChild(label); + return container; + } + + // Option 2. Thumbnail image available, then render it without + // a label. + if (thumbnailImage) { + thumbnailImage.classList.add('drag-thumbnail'); + contents.classList.add('for-image'); + contents.appendChild(this.preloadedThumbnailImageNode_); + return container; + } + + // Option 3. Thumbnail not available. Render an icon and a label. + var entry = this.selectedEntries_[0]; + var icon = this.document_.createElement('div'); + icon.className = 'detail-icon'; + icon.setAttribute('file-type-icon', FileType.getIcon(entry)); + contents.appendChild(icon); + var label = this.document_.createElement('div'); + label.className = 'label'; + label.textContent = entry.name; + contents.appendChild(label); + return container; + }, + + /** + * @this {FileTransferController} + * @param {cr.ui.List} list Drop target list + * @param {Event} event A dragstart event of DOM. + */ + onDragStart_: function(list, event) { + // If a user is touching, Files.app does not receive drag operations. + if (this.touching_) { + event.preventDefault(); + return; + } + + // Check if a drag selection should be initiated or not. + if (list.shouldStartDragSelection(event)) { + this.dragSelector_.startDragSelection(list, event); + return; + } + + // Nothing selected. + if (!this.selectedEntries_.length) { + event.preventDefault(); + return; + } + + var dt = event.dataTransfer; + + if (this.canCopyOrDrag_(dt)) { + if (this.canCutOrDrag_(dt)) + this.cutOrCopy_(dt, 'copyMove'); + else + this.cutOrCopy_(dt, 'copy'); + } else { + event.preventDefault(); + return; + } + + var dragThumbnail = this.renderThumbnail_(); + dt.setDragImage(dragThumbnail, 1000, 1000); + + window[DRAG_AND_DROP_GLOBAL_DATA] = { + sourceRoot: this.directoryModel_.getCurrentRootPath() + }; + }, + + /** + * @this {FileTransferController} + * @param {cr.ui.List} list Drop target list. + * @param {Event} event A dragend event of DOM. + */ + onDragEnd_: function(list, event) { + var container = this.document_.querySelector('#drag-container'); + container.textContent = ''; + this.clearDropTarget_(); + delete window[DRAG_AND_DROP_GLOBAL_DATA]; + }, + + /** + * @this {FileTransferController} + * @param {boolean} onlyIntoDirectories True if the drag is only into + * directories. + * @param {cr.ui.List} list Drop target list. + * @param {Event} event A dragover event of DOM. + */ + onDragOver_: function(onlyIntoDirectories, list, event) { + event.preventDefault(); + var path = this.destinationPath_ || + (!onlyIntoDirectories && this.currentDirectoryContentPath); + event.dataTransfer.dropEffect = this.selectDropEffect_(event, path); + event.preventDefault(); + }, + + /** + * @this {FileTransferController} + * @param {cr.ui.List} list Drop target list. + * @param {Event} event A dragenter event of DOM. + */ + onDragEnterFileList_: function(list, event) { + event.preventDefault(); // Required to prevent the cursor flicker. + this.lastEnteredTarget_ = event.target; + var item = list.getListItemAncestor(event.target); + item = item && list.isItem(item) ? item : null; + if (item == this.dropTarget_) + return; + + var entry = item && list.dataModel.item(item.listIndex); + if (entry) { + this.setDropTarget_(item, entry.isDirectory, event.dataTransfer, + entry.fullPath); + } else { + this.clearDropTarget_(); + } + }, + + /** + * @this {FileTransferController} + * @param {DirectoryTree} tree Drop target tree. + * @param {Event} event A dragenter event of DOM. + */ + onDragEnterTree_: function(tree, event) { + event.preventDefault(); // Required to prevent the cursor flicker. + this.lastEnteredTarget_ = event.target; + var item = event.target; + while (item && !(item instanceof DirectoryItem)) { + item = item.parentNode; + } + + if (item == this.dropTarget_) + return; + + var entry = item && item.entry; + if (entry) { + this.setDropTarget_(item, entry.isDirectory, event.dataTransfer, + entry.fullPath); + } else { + this.clearDropTarget_(); + } + }, + + /** + * @this {FileTransferController} + * @param {NavigationList} list Drop target list. + * @param {Event} event A dragenter event of DOM. + */ + onDragEnterVolumesList_: function(list, event) { + event.preventDefault(); // Required to prevent the cursor flicker. + this.lastEnteredTarget_ = event.target; + var item = list.getListItemAncestor(event.target); + item = item && list.isItem(item) ? item : null; + if (item == this.dropTarget_) + return; + + var path = item && list.dataModel.item(item.listIndex).path; + if (path) + this.setDropTarget_(item, true /* directory */, event.dataTransfer, path); + else + this.clearDropTarget_(); + }, + + /** + * @this {FileTransferController} + * @param {cr.ui.List} list Drop target list. + * @param {Event} event A dragleave event of DOM. + */ + onDragLeave_: function(list, event) { + // If mouse moves from one element to another the 'dragenter' + // event for the new element comes before the 'dragleave' event for + // the old one. In this case event.target != this.lastEnteredTarget_ + // and handler of the 'dragenter' event has already caried of + // drop target. So event.target == this.lastEnteredTarget_ + // could only be if mouse goes out of listened element. + if (event.target == this.lastEnteredTarget_) { + this.clearDropTarget_(); + this.lastEnteredTarget_ = null; + } + }, + + /** + * @this {FileTransferController} + * @param {boolean} onlyIntoDirectories True if the drag is only into + * directories. + * @param {Event} event A dragleave event of DOM. + */ + onDrop_: function(onlyIntoDirectories, event) { + if (onlyIntoDirectories && !this.dropTarget_) + return; + var destinationPath = this.destinationPath_ || + this.currentDirectoryContentPath; + if (!this.canPasteOrDrop_(event.dataTransfer, destinationPath)) + return; + event.preventDefault(); + this.paste(event.dataTransfer, destinationPath, + this.selectDropEffect_(event, destinationPath)); + this.clearDropTarget_(); + }, + + /** + * Sets the drop target. + * @this {FileTransferController} + * @param {Element} domElement Target of the drop. + * @param {boolean} isDirectory If the target is a directory. + * @param {DataTransfer} dataTransfer Data transfer object. + * @param {string} destinationPath Destination path. + */ + setDropTarget_: function(domElement, isDirectory, dataTransfer, + destinationPath) { + if (this.dropTarget_ == domElement) + return; + + // Remove the old drop target. + this.clearDropTarget_(); + + // Set the new drop target. + this.dropTarget_ = domElement; + + if (!domElement || + !isDirectory || + !this.canPasteOrDrop_(dataTransfer, destinationPath)) { + return; + } + + // Add accept class if the domElement can accept the drag. + domElement.classList.add('accepts'); + this.destinationPath_ = destinationPath; + + // Start timer changing the directory. + this.navigateTimer_ = setTimeout(function() { + if (domElement instanceof DirectoryItem) + // Do custom action. + (/** @type {DirectoryItem} */ domElement).doDropTargetAction(); + this.directoryModel_.changeDirectory(destinationPath); + }.bind(this), 2000); + }, + + /** + * Handles touch start. + */ + onTouchStart_: function() { + this.touching_ = true; + }, + + /** + * Handles touch end. + */ + onTouchEnd_: function(event) { + if (event.touches.length === 0) + this.touching_ = false; + }, + + /** + * Clears the drop target. + * @this {FileTransferController} + */ + clearDropTarget_: function() { + if (this.dropTarget_ && this.dropTarget_.classList.contains('accepts')) + this.dropTarget_.classList.remove('accepts'); + this.dropTarget_ = null; + this.destinationPath_ = null; + if (this.navigateTimer_ !== undefined) { + clearTimeout(this.navigateTimer_); + this.navigateTimer_ = undefined; + } + }, + + /** + * @this {FileTransferController} + * @return {boolean} Returns false if {@code <input type="text">} element is + * currently active. Otherwise, returns true. + */ + isDocumentWideEvent_: function() { + return this.document_.activeElement.nodeName.toLowerCase() != 'input' || + this.document_.activeElement.type.toLowerCase() != 'text'; + }, + + /** + * @this {FileTransferController} + */ + onCopy_: function(event) { + if (!this.isDocumentWideEvent_() || + !this.canCopyOrDrag_()) { + return; + } + event.preventDefault(); + this.cutOrCopy_(event.clipboardData, 'copy'); + this.notify_('selection-copied'); + }, + + /** + * @this {FileTransferController} + */ + onBeforeCopy_: function(event) { + if (!this.isDocumentWideEvent_()) + return; + + // queryCommandEnabled returns true if event.defaultPrevented is true. + if (this.canCopyOrDrag_()) + event.preventDefault(); + }, + + /** + * @this {FileTransferController} + * @return {boolean} Returns true if some files are selected and all the file + * on drive is available to be copied. Otherwise, returns false. + */ + canCopyOrDrag_: function() { + if (this.isOnDrive && + this.directoryModel_.isDriveOffline() && + !this.allDriveFilesAvailable) + return false; + return this.selectedEntries_.length > 0; + }, + + /** + * @this {FileTransferController} + */ + onCut_: function(event) { + if (!this.isDocumentWideEvent_() || + !this.canCutOrDrag_()) { + return; + } + event.preventDefault(); + this.cutOrCopy_(event.clipboardData, 'move'); + this.notify_('selection-cut'); + }, + + /** + * @this {FileTransferController} + */ + onBeforeCut_: function(event) { + if (!this.isDocumentWideEvent_()) + return; + // queryCommandEnabled returns true if event.defaultPrevented is true. + if (this.canCutOrDrag_()) + event.preventDefault(); + }, + + /** + * @this {FileTransferController} + * @return {boolean} Returns true if some files are selected and all the file + * on drive is available to be cut. Otherwise, returns false. + */ + canCutOrDrag_: function() { + return !this.readonly && this.canCopyOrDrag_(); + }, + + /** + * @this {FileTransferController} + */ + onPaste_: function(event) { + // Need to update here since 'beforepaste' doesn't fire. + if (!this.isDocumentWideEvent_() || + !this.canPasteOrDrop_(event.clipboardData, + this.currentDirectoryContentPath)) { + return; + } + event.preventDefault(); + var effect = this.paste(event.clipboardData); + + // On cut, we clear the clipboard after the file is pasted/moved so we don't + // try to move/delete the original file again. + if (effect == 'move') { + this.simulateCommand_('cut', function(event) { + event.preventDefault(); + event.clipboardData.setData('fs/clear', ''); + }); + } + }, + + /** + * @this {FileTransferController} + */ + onBeforePaste_: function(event) { + if (!this.isDocumentWideEvent_()) + return; + // queryCommandEnabled returns true if event.defaultPrevented is true. + if (this.canPasteOrDrop_(event.clipboardData, + this.currentDirectoryContentPath)) { + event.preventDefault(); + } + }, + + /** + * @this {FileTransferController} + * @param {DataTransfer} dataTransfer Data transfer object. + * @param {string?} destinationPath Destination path. + * @return {boolean} Returns true if items stored in {@code dataTransfer} can + * be pasted to {@code destinationPath}. Otherwise, returns false. + */ + canPasteOrDrop_: function(dataTransfer, destinationPath) { + if (!destinationPath) { + return false; + } + if (this.directoryModel_.isPathReadOnly(destinationPath)) { + return false; + } + if (!dataTransfer.types || dataTransfer.types.indexOf('fs/tag') == -1) { + return false; // Unsupported type of content. + } + if (dataTransfer.getData('fs/tag') == '') { + // Data protected. Other checks are not possible but it makes sense to + // let the user try. + return true; + } + + var directories = dataTransfer.getData('fs/directories').split('\n'). + filter(function(d) { return d != ''; }); + + for (var i = 0; i < directories.length; i++) { + if (destinationPath.substr(0, directories[i].length) == directories[i]) + return false; // recursive paste. + } + + return true; + }, + + /** + * Execute paste command. + * + * @this {FileTransferController} + * @return {boolean} Returns true, the paste is success. Otherwise, returns + * false. + */ + queryPasteCommandEnabled: function() { + if (!this.isDocumentWideEvent_()) { + return false; + } + + // HACK(serya): return this.document_.queryCommandEnabled('paste') + // should be used. + var result; + this.simulateCommand_('paste', function(event) { + result = this.canPasteOrDrop_(event.clipboardData, + this.currentDirectoryContentPath); + }.bind(this)); + return result; + }, + + /** + * Allows to simulate commands to get access to clipboard. + * + * @this {FileTransferController} + * @param {string} command 'copy', 'cut' or 'paste'. + * @param {function} handler Event handler. + */ + simulateCommand_: function(command, handler) { + var iframe = this.document_.querySelector('#command-dispatcher'); + var doc = iframe.contentDocument; + doc.addEventListener(command, handler); + doc.execCommand(command); + doc.removeEventListener(command, handler); + }, + + /** + * @this {FileTransferController} + */ + onSelectionChanged_: function(event) { + var entries = this.selectedEntries_; + var files = this.selectedFileObjects_ = []; + this.preloadedThumbnailImageNode_ = null; + + var fileEntries = []; + for (var i = 0; i < entries.length; i++) { + if (entries[i].isFile) + fileEntries.push(entries[i]); + } + + if (entries.length == 1) { + // For single selection, the dragged element is created in advance, + // otherwise an image may not be loaded at the time the 'dragstart' event + // comes. + this.preloadThumbnailImage_(entries[0]); + } + + // File object must be prepeared in advance for clipboard operations + // (copy, paste and drag). DataTransfer object closes for write after + // returning control from that handlers so they may not have + // asynchronous operations. + var prepareFileObjects = function() { + for (var i = 0; i < fileEntries.length; i++) { + fileEntries[i].file(function(file) { files.push(file); }); + } + }; + + if (this.isOnDrive) { + this.allDriveFilesAvailable = false; + this.metadataCache_.get( + entries, 'drive', function(props) { + // We consider directories not available offline for the purposes of + // file transfer since we cannot afford to recursive traversal. + this.allDriveFilesAvailable = + entries.filter(function(e) {return e.isDirectory}).length == 0 && + props.filter(function(p) {return !p.availableOffline}).length == 0; + // |Copy| is the only menu item affected by allDriveFilesAvailable. + // It could be open right now, update its UI. + this.copyCommand_.disabled = !this.canCopyOrDrag_(); + + if (this.allDriveFilesAvailable) + prepareFileObjects(); + }.bind(this)); + } else { + prepareFileObjects(); + } + }, + + /** + * Path of directory that is displaying now. + * If search result is displaying now, this is null. + * @this {FileTransferController} + * @return {string} Path of directry that is displaying now. + */ + get currentDirectoryContentPath() { + return this.directoryModel_.isSearching() ? + null : this.directoryModel_.getCurrentDirPath(); + }, + + /** + * @this {FileTransferController} + * @return {boolean} True if the current directory is read only. + */ + get readonly() { + return this.directoryModel_.isReadOnly(); + }, + + /** + * @this {FileTransferController} + * @return {boolean} True if the current directory is on Drive. + */ + get isOnDrive() { + return PathUtil.isDriveBasedPath(this.directoryModel_.getCurrentRootPath()); + }, + + /** + * @this {FileTransferController} + */ + notify_: function(eventName) { + var self = this; + // Set timeout to avoid recursive events. + setTimeout(function() { + cr.dispatchSimpleEvent(self, eventName); + }, 0); + }, + + /** + * @this {FileTransferController} + * @return {Array.<Entry>} Array of the selected entries. + */ + get selectedEntries_() { + var list = this.directoryModel_.getFileList(); + var selectedIndexes = this.directoryModel_.getFileListSelection(). + selectedIndexes; + var entries = selectedIndexes.map(function(index) { + return list.item(index); + }); + + // TODO(serya): Diagnostics for http://crbug/129642 + if (entries.indexOf(undefined) != -1) { + var index = entries.indexOf(undefined); + entries = entries.filter(function(e) { return !!e; }); + console.error('Invalid selection found: list items: ', list.length, + 'wrong indexe value: ', selectedIndexes[index], + 'Stack trace: ', new Error().stack); + } + return entries; + }, + + /** + * @this {FileTransferController} + * @return {string} Returns the appropriate drop query type ('none', 'move' + * or copy') to the current modifiers status and the destination. + */ + selectDropEffect_: function(event, destinationPath) { + if (!destinationPath || + this.directoryModel_.isPathReadOnly(destinationPath)) + return 'none'; + if (event.dataTransfer.effectAllowed == 'copyMove' && + this.getSourceRoot_(event.dataTransfer) == + PathUtil.getRootPath(destinationPath) && + !event.ctrlKey) { + return 'move'; + } + if (event.dataTransfer.effectAllowed == 'copyMove' && + event.shiftKey) { + return 'move'; + } + return 'copy'; + }, +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_type.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_type.js new file mode 100644 index 00000000000..ea0ae9278ee --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_type.js @@ -0,0 +1,294 @@ +// 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'; + +/** + * Namespace object for file type utility functions. + */ +var FileType = {}; + +/** + * Description of known file types. + * Pair type-subtype defines order when sorted by file type. + */ +FileType.types = [ + // Images + {type: 'image', name: 'IMAGE_FILE_TYPE', subtype: 'JPEG', + pattern: /\.jpe?g$/i}, + {type: 'image', name: 'IMAGE_FILE_TYPE', subtype: 'BMP', + pattern: /\.bmp$/i}, + {type: 'image', name: 'IMAGE_FILE_TYPE', subtype: 'GIF', + pattern: /\.gif$/i}, + {type: 'image', name: 'IMAGE_FILE_TYPE', subtype: 'ICO', + pattern: /\.ico$/i}, + {type: 'image', name: 'IMAGE_FILE_TYPE', subtype: 'PNG', + pattern: /\.png$/i}, + {type: 'image', name: 'IMAGE_FILE_TYPE', subtype: 'WebP', + pattern: /\.webp$/i}, + {type: 'image', name: 'IMAGE_FILE_TYPE', subtype: 'TIFF', + pattern: /\.tiff?$/i}, + + // Video + {type: 'video', name: 'VIDEO_FILE_TYPE', subtype: '3GP', + pattern: /\.3gp$/i}, + {type: 'video', name: 'VIDEO_FILE_TYPE', subtype: 'AVI', + pattern: /\.avi$/i}, + {type: 'video', name: 'VIDEO_FILE_TYPE', subtype: 'QuickTime', + pattern: /\.mov$/i}, + {type: 'video', name: 'VIDEO_FILE_TYPE', subtype: 'MKV', + pattern: /\.mkv$/i}, + {type: 'video', name: 'VIDEO_FILE_TYPE', subtype: 'MPEG', + pattern: /\.m(p4|4v|pg|peg|pg4|peg4)$/i}, + {type: 'video', name: 'VIDEO_FILE_TYPE', subtype: 'OGG', + pattern: /\.og(m|v|x)$/i}, + {type: 'video', name: 'VIDEO_FILE_TYPE', subtype: 'WebM', + pattern: /\.webm$/i}, + + // Audio + {type: 'audio', name: 'AUDIO_FILE_TYPE', subtype: 'AMR', + pattern: /\.amr$/i}, + {type: 'audio', name: 'AUDIO_FILE_TYPE', subtype: 'FLAC', + pattern: /\.flac$/i}, + {type: 'audio', name: 'AUDIO_FILE_TYPE', subtype: 'MP3', + pattern: /\.mp3$/i}, + {type: 'audio', name: 'AUDIO_FILE_TYPE', subtype: 'MPEG', + pattern: /\.m4a$/i}, + {type: 'audio', name: 'AUDIO_FILE_TYPE', subtype: 'OGG', + pattern: /\.og(a|g)$/i}, + {type: 'audio', name: 'AUDIO_FILE_TYPE', subtype: 'WAV', + pattern: /\.wav$/i}, + + // Text + {type: 'text', name: 'PLAIN_TEXT_FILE_TYPE', subtype: 'TXT', + pattern: /\.txt$/i}, + + // Archive + {type: 'archive', name: 'ZIP_ARCHIVE_FILE_TYPE', subtype: 'ZIP', + pattern: /\.zip$/i}, + {type: 'archive', name: 'RAR_ARCHIVE_FILE_TYPE', subtype: 'RAR', + pattern: /\.rar$/i}, + {type: 'archive', name: 'TAR_ARCHIVE_FILE_TYPE', subtype: 'TAR', + pattern: /\.tar$/i}, + {type: 'archive', name: 'TAR_BZIP2_ARCHIVE_FILE_TYPE', subtype: 'TBZ2', + pattern: /\.(tar\.bz2|tbz|tbz2)$/i}, + {type: 'archive', name: 'TAR_GZIP_ARCHIVE_FILE_TYPE', subtype: 'TGZ', + pattern: /\.(tar\.|t)gz$/i}, + + // Hosted docs. + {type: 'hosted', icon: 'gdoc', name: 'GDOC_DOCUMENT_FILE_TYPE', + subtype: 'doc', pattern: /\.gdoc$/i}, + {type: 'hosted', icon: 'gsheet', name: 'GSHEET_DOCUMENT_FILE_TYPE', + subtype: 'sheet', pattern: /\.gsheet$/i}, + {type: 'hosted', icon: 'gslides', name: 'GSLIDES_DOCUMENT_FILE_TYPE', + subtype: 'slides', pattern: /\.gslides$/i}, + {type: 'hosted', icon: 'gdraw', name: 'GDRAW_DOCUMENT_FILE_TYPE', + subtype: 'draw', pattern: /\.gdraw$/i}, + {type: 'hosted', icon: 'gtable', name: 'GTABLE_DOCUMENT_FILE_TYPE', + subtype: 'table', pattern: /\.gtable$/i}, + {type: 'hosted', icon: 'glink', name: 'GLINK_DOCUMENT_FILE_TYPE', + subtype: 'glink', pattern: /\.glink$/i}, + {type: 'hosted', icon: 'gform', name: 'GFORM_DOCUMENT_FILE_TYPE', + subtype: 'form', pattern: /\.gform$/i}, + + // Others + {type: 'document', icon: 'pdf', name: 'PDF_DOCUMENT_FILE_TYPE', + subtype: 'PDF', pattern: /\.pdf$/i}, + {type: 'document', name: 'HTML_DOCUMENT_FILE_TYPE', + subtype: 'HTML', pattern: /\.(html?|mht|mhtml)$/i}, + {type: 'document', icon: 'word', name: 'WORD_DOCUMENT_FILE_TYPE', + subtype: 'Word', pattern: /\.(doc|docx)$/i}, + {type: 'document', icon: 'ppt', name: 'POWERPOINT_PRESENTATION_FILE_TYPE', + subtype: 'PPT', pattern: /\.(ppt|pptx)$/i}, + {type: 'document', icon: 'excel', name: 'EXCEL_FILE_TYPE', + subtype: 'Excel', pattern: /\.(xls|xlsx)$/i} +]; + +/** + * A special type for directory. + */ +FileType.DIRECTORY = {name: 'FOLDER', type: '.folder', icon: 'folder'}; + +/** + * Returns the file path extension for a given file. + * + * @param {string|Entry} file Reference to the file. + * Can be a name, a path, a url or a File API Entry. + * @return {string} The extension including a leading '.', or empty string if + * not found. + */ +FileType.getExtension = function(file) { + var fileName; + if (typeof file == 'object') { + if (file.isDirectory) { + // No extension for a directory. + return ''; + } else { + fileName = file.name; + } + } else { + fileName = file; + } + + var extensionStartIndex = fileName.lastIndexOf('.'); + if (extensionStartIndex == -1 || extensionStartIndex == fileName.length - 1) { + return ''; + } + return fileName.substr(extensionStartIndex); +}; + +/** + * Get the file type object for a given file. + * + * @param {string|Entry} file Reference to the file. + * Can be a name, a path, a url or a File API Entry. + * @return {Object} The matching file type object or an empty object. + */ +FileType.getType = function(file) { + if (typeof file == 'object') { + if (file.isDirectory) + return FileType.DIRECTORY; + else + file = file.name; + } + var types = FileType.types; + for (var i = 0; i < types.length; i++) { + if (types[i].pattern.test(file)) { + return types[i]; + } + } + + // Unknown file type. + var extension = FileType.getExtension(file); + if (extension == '') { + return { name: 'NO_EXTENSION_FILE_TYPE', type: 'UNKNOWN', icon: '' }; + } + // subtype is the extension excluding the first dot. + return { name: 'GENERIC_FILE_TYPE', type: 'UNKNOWN', + subtype: extension.substr(1).toUpperCase(), icon: '' }; +}; + +/** + * @param {string|Entry} file Reference to the file. + * Can be a name, a path, a url or a File API Entry. + * @return {string} Localized string representation of file type. + */ +FileType.getTypeString = function(file) { + var fileType = FileType.getType(file); + if (fileType.subtype) + return strf(fileType.name, fileType.subtype); + else + return str(fileType.name); +}; + +/** + * Pattern for urls pointing to Google Drive files. + */ +FileType.DRIVE_URL_PATTERN = + new RegExp('^filesystem:[^/]*://[^/]*/[^/]*/drive/(.*)'); + +/** + * Pattern for file paths pointing to Google Drive files. + */ +FileType.DRIVE_PATH_PATTERN = + new RegExp('^/drive/'); + +/** + * @param {string|Entry} file The url string or entry. + * @return {boolean} Whether this provider supports the url. + */ +FileType.isOnDrive = function(file) { + return typeof file == 'string' ? + FileType.DRIVE_URL_PATTERN.test(file) : + FileType.DRIVE_PATH_PATTERN.test(file.fullPath); +}; + + +/** + * Get the media type for a given file. + * + * @param {string|Entry} file Reference to the file. + * @return {string} The value of 'type' property from one of the elements in + * FileType.types or undefined. + */ +FileType.getMediaType = function(file) { + return FileType.getType(file).type; +}; + +/** + * @param {string|Entry} file Reference to the file. + * @return {boolean} True if audio file. + */ +FileType.isAudio = function(file) { + return FileType.getMediaType(file) == 'audio'; +}; + +/** + * @param {string|Entry} file Reference to the file. + * @return {boolean} True if image file. + */ +FileType.isImage = function(file) { + return FileType.getMediaType(file) == 'image'; +}; + +/** + * @param {string|Entry} file Reference to the file. + * @return {boolean} True if video file. + */ +FileType.isVideo = function(file) { + return FileType.getMediaType(file) == 'video'; +}; + + +/** + * Files with more pixels won't have preview. + * @param {string|Entry} file Reference to the file. + * @return {boolean} True if image or video. + */ +FileType.isImageOrVideo = function(file) { + var type = FileType.getMediaType(file); + return type == 'image' || type == 'video'; +}; + +/** + * @param {string|Entry} file Reference to the file. + * @return {boolean} Returns true if the file is hosted. + */ +FileType.isHosted = function(file) { + return FileType.getType(file).type === 'hosted'; +}; + +/** + * @param {string|Entry} file Reference to the file. + * @return {boolean} Returns true if the file is not hidden, and we should + * display it. + */ +FileType.isVisible = function(file) { + if (typeof file == 'object') { + file = file.name; + } + + var path = util.extractFilePath(file); + if (path) file = path; + + file = file.split('/').pop(); + return file.indexOf('.') != 0 && !(file in FileType.HIDDEN_NAMES); +}; + +/** + * File/directory names that we know are usually hidden. + */ +FileType.HIDDEN_NAMES = { + 'RECYCLED': true +}; + +/** + * @param {string|Entry} file Reference to the file. + * @return {string} Returns string that represents the file icon. + * It refers to a file 'images/filetype_' + icon + '.png'. + */ +FileType.getIcon = function(file) { + var fileType = FileType.getType(file); + return fileType.icon || fileType.type || 'unknown'; +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_watcher.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_watcher.js new file mode 100644 index 00000000000..a6414e61f4a --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_watcher.js @@ -0,0 +1,227 @@ +// Copyright 2013 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'; + +/** + * Watches for changes in the tracked directory, including local metadata + * changes. + * + * @param {MetadataCache} metadataCache Instance of MetadataCache. + * @extends {cr.EventTarget} + * @constructor + */ +function FileWatcher(metadataCache) { + this.queue_ = new AsyncUtil.Queue(); + this.metadataCache_ = metadataCache; + this.watchedDirectoryEntry_ = null; + + this.onDirectoryChangedBound_ = this.onDirectoryChanged_.bind(this); + chrome.fileBrowserPrivate.onDirectoryChanged.addListener( + this.onDirectoryChangedBound_); + + this.filesystemMetadataObserverId_ = null; + this.thumbnailMetadataObserverId_ = null; + this.driveMetadataObserverId_ = null; +} + +/** + * FileWatcher extends cr.EventTarget. + */ +FileWatcher.prototype.__proto__ = cr.EventTarget.prototype; + +/** + * Stops watching (must be called before page unload). + */ +FileWatcher.prototype.dispose = function() { + chrome.fileBrowserPrivate.onDirectoryChanged.removeListener( + this.onDirectoryChangedBound_); + if (this.watchedDirectoryEntry_) + this.resetWatchedEntry_(function() {}, function() {}); +}; + +/** + * Called when a file in the watched directory is changed. + * @param {Event} event Change event. + * @private + */ +FileWatcher.prototype.onDirectoryChanged_ = function(event) { + if (this.watchedDirectoryEntry_ && + event.entry.toURL() === this.watchedDirectoryEntry_.toURL()) { + var e = new Event('watcher-directory-changed'); + this.dispatchEvent(e); + } +}; + +/** + * Called when general metadata in the watched directory has been changed. + * + * @param {Array.<Entry>} entries Array of entries. + * @param {Object.<string, Object>} properties Map from entry URLs to metadata + * properties. + * @private + */ +FileWatcher.prototype.onFilesystemMetadataChanged_ = function( + entries, properties) { + this.dispatchMetadataEvent_('filesystem', entries, properties); +}; + +/** + * Called when thumbnail metadata in the watched directory has been changed. + * + * @param {Array.<Entry>} entries Arrray of entries. + * @param {Object.<string, Object>} properties Map from entry URLs to metadata + * properties. + * @private + */ +FileWatcher.prototype.onThumbnailMetadataChanged_ = function( + entries, properties) { + this.dispatchMetadataEvent_('thumbnail', entries, properties); +}; + +/** + * Called when drive metadata in the watched directory has been changed. + * + * @param {Array.<Entry>} entries Array of entries. + * @param {Object.<string, Object>} properties Map from entry URLs to metadata + * properties. + * @private + */ +FileWatcher.prototype.onDriveMetadataChanged_ = function( + entries, properties) { + this.dispatchMetadataEvent_('drive', entries, properties); +}; + +/** + * Dispatches an event about detected change in metadata within the tracked + * directory. + * + * @param {string} type Type of the metadata change. + * @param {Array.<Entry>} entries Array of entries. + * @param {Object.<string, Object>} properties Map from entry URLs to metadata + * properties. + * @private + */ +FileWatcher.prototype.dispatchMetadataEvent_ = function( + type, entries, properties) { + var e = new Event('watcher-metadata-changed'); + e.metadataType = type; + e.entries = entries; + e.properties = properties; + this.dispatchEvent(e); +}; + +/** + * Changes the watched directory. In case of a fake entry, the watch is + * just released, since there is no reason to track a fake directory. + * + * @param {!DirectoryEntry|!Object} entry Directory entry to be tracked, or the + * fake entry. + * @param {function()} callback Completion callback. + */ +FileWatcher.prototype.changeWatchedDirectory = function(entry, callback) { + if (entry && entry.toURL) { + this.changeWatchedEntry_( + entry, + callback, + function() { + console.error( + 'Unable to change the watched directory to: ' + entry.toURL()); + callback(); + }); + } else { + this.resetWatchedEntry_( + callback, + function() { + console.error('Unable to reset the watched directory.'); + callback(); + }); + } +}; + +/** + * Resets the watched entry to the passed directory. + * + * @param {function()} onSuccess Success callback. + * @param {function()} onError Error callback. + * @private + */ +FileWatcher.prototype.resetWatchedEntry_ = function(onSuccess, onError) { + // Run the tasks in the queue to avoid races. + this.queue_.run(function(callback) { + // Release the watched directory. + if (this.watchedDirectoryEntry_) { + chrome.fileBrowserPrivate.removeFileWatch( + this.watchedDirectoryEntry_.toURL(), + function(result) { + this.watchedDirectoryEntry_ = null; + if (result) + onSuccess(); + else + onError(); + callback(); + }.bind(this)); + this.metadataCache_.removeObserver(this.filesystemMetadataObserverId_); + this.metadataCache_.removeObserver(this.thumbnailMetadataObserverId_); + this.metadataCache_.removeObserver(this.driveMetadataObserverId_); + } else { + onSuccess(); + callback(); + } + }.bind(this)); +}; + +/** + * Sets the watched entry to the passed directory. + * + * @param {!DirectoryEntry} entry Directory to be watched. + * @param {function()} onSuccess Success callback. + * @param {function()} onError Error callback. + * @private + */ +FileWatcher.prototype.changeWatchedEntry_ = function( + entry, onSuccess, onError) { + var setEntryClosure = function() { + // Run the tasks in the queue to avoid races. + this.queue_.run(function(callback) { + chrome.fileBrowserPrivate.addFileWatch( + entry.toURL(), + function(result) { + if (!result) { + this.watchedDirectoryEntry_ = null; + onError(); + } else { + this.watchedDirectoryEntry_ = entry; + onSuccess(); + } + callback(); + }.bind(this)); + this.filesystemMetadataObserverId_ = this.metadataCache_.addObserver( + entry, + MetadataCache.CHILDREN, + 'filesystem', + this.onFilesystemMetadataChanged_.bind(this)); + this.thumbnailMetadataObserverId_ = this.metadataCache_.addObserver( + entry, + MetadataCache.CHILDREN, + 'thumbnail', + this.onThumbnailMetadataChanged_.bind(this)); + this.driveMetadataObserverId_ = this.metadataCache_.addObserver( + entry, + MetadataCache.CHILDREN, + 'drive', + this.onDriveMetadataChanged_.bind(this)); + }.bind(this)); + }.bind(this); + + // Reset the watched directory first, then set the new watched directory. + this.resetWatchedEntry_(setEntryClosure, onError); +}; + +/** + * @return {DirectoryEntry} Current watched directory entry. + */ +FileWatcher.prototype.getWatchedDirectoryEntry = function() { + return this.watchedDirectoryEntry_; +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/folder_shortcuts_data_model.js b/chromium/chrome/browser/resources/file_manager/foreground/js/folder_shortcuts_data_model.js new file mode 100644 index 00000000000..9467f5eec07 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/folder_shortcuts_data_model.js @@ -0,0 +1,293 @@ +// Copyright (c) 2013 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. + +/** + * Model for the folder shortcuts. This object is cr.ui.ArrayDataModel-like + * object with additional methods for the folder shortcut feature. + * This uses chrome.storage as backend. Items are always sorted by file path. + * + * @constructor + * @extends {cr.EventTarget} + */ +function FolderShortcutsDataModel() { + this.array_ = []; + + /** + * Eliminate unsupported folders from the list. + * + * @param {Array.<string>} array Folder array which may contain the + * unsupported folders. + * @return {Array.<string>} Folder list without unsupported folder. + */ + var filter = function(array) { + return array.filter(PathUtil.isEligibleForFolderShortcut); + }; + + // Loads the contents from the storage to initialize the array. + chrome.storage.sync.get(FolderShortcutsDataModel.NAME, function(value) { + if (!(FolderShortcutsDataModel.NAME in value)) + return; + + // Since the value comes from outer resource, we have to check it just in + // case. + var list = value[FolderShortcutsDataModel.NAME]; + if (list instanceof Array) { + list = filter(list); + + // Record metrics. + metrics.recordSmallCount('FolderShortcut.Count', list.length); + + var permutation = this.calculatePermutation_(this.array_, list); + this.array_ = list; + this.firePermutedEvent_(permutation); + } + }.bind(this)); + + // Listening for changes in the storage. + chrome.storage.onChanged.addListener(function(changes, namespace) { + if (!(FolderShortcutsDataModel.NAME in changes) || namespace != 'sync') + return; + + var list = changes[FolderShortcutsDataModel.NAME].newValue; + // Since the value comes from outer resource, we have to check it just in + // case. + if (list instanceof Array) { + list = filter(list); + + // If the list is not changed, do nothing and just return. + if (this.array_.length == list.length) { + var changed = false; + for (var i = 0; i < this.array_.length; i++) { + // Same item check: must be exact match. + if (this.array_[i] != list[i]) { + changed = true; + break; + } + } + if (!changed) + return; + } + + var permutation = this.calculatePermutation_(this.array_, list); + this.array_ = list; + this.firePermutedEvent_(permutation); + } + }.bind(this)); +} + +/** + * Key name in chrome.storage. The array are stored with this name. + * @type {string} + * @const + */ +FolderShortcutsDataModel.NAME = 'folder-shortcuts-list'; + +FolderShortcutsDataModel.prototype = { + __proto__: cr.EventTarget.prototype, + + /** + * @return {number} Number of elements in the array. + */ + get length() { + return this.array_.length; + }, + + /** + * Returns the paths in the given range as a new array instance. The + * arguments and return value are compatible with Array.slice(). + * + * @param {number} start Where to start the selection. + * @param {number=} opt_end Where to end the selection. + * @return {Array.<string>} Paths in the selected range. + */ + slice: function(begin, opt_end) { + return this.array_.slice(begin, opt_end); + }, + + /** + * @param {number} index Index of the element to be retrieved. + * @return {string} The value of the |index|-th element. + */ + item: function(index) { + return this.array_[index]; + }, + + /** + * @param {string} value Value of the element to be retrieved. + * @return {number} Index of the element with the specified |value|. + */ + getIndex: function(value) { + for (var i = 0; i < this.length; i++) { + // Same item check: must be exact match. + if (this.array_[i] == value) { + return i; + } + } + return -1; + }, + + /** + * Compares 2 strings and returns a number indicating one string comes before + * or after or is the same as the other string in sort order. + * + * @param {string} a String1. + * @param {string} b String2. + * @return {boolean} Return -1, if String1 < String2. Return 0, if String1 == + * String2. Otherwise, return 1. + */ + compare: function(a, b) { + return a.localeCompare(b, + undefined, // locale parameter, use default locale. + {usage: 'sort', numeric: true}); + }, + + /** + * Adds the given item to the array. If there were already same item in the + * list, return the index of the existing item without adding a duplicate + * item. + * + * @param {string} value Value to be added into the array. + * @return {number} Index in the list which the element added to. + */ + add: function(value) { + var oldArray = this.array_.slice(0); // Shallow copy. + var addedIndex = -1; + for (var i = 0; i < this.length; i++) { + // Same item check: must be exact match. + if (this.array_[i] == value) + return i; + + // Since the array is sorted, new item will be added just before the first + // larger item. + if (this.compare(this.array_[i], value) >= 0) { + this.array_.splice(i, 0, value); + addedIndex = i; + break; + } + } + // If value is not added yet, add it at the last. + if (addedIndex == -1) { + this.array_.push(value); + addedIndex = this.length; + } + + this.firePermutedEvent_( + this.calculatePermutation_(oldArray, this.array_)); + this.save_(); + metrics.recordUserAction('FolderShortcut.Add'); + return addedIndex; + }, + + /** + * Removes the given item from the array. + * @param {string} value Value to be removed from the array. + * @return {number} Index in the list which the element removed from. + */ + remove: function(value) { + var removedIndex = -1; + var oldArray = this.array_.slice(0); // Shallow copy. + for (var i = 0; i < this.length; i++) { + // Same item check: must be exact match. + if (this.array_[i] == value) { + this.array_.splice(i, 1); + removedIndex = i; + break; + } + } + + if (removedIndex != -1) { + this.firePermutedEvent_( + this.calculatePermutation_(oldArray, this.array_)); + this.save_(); + metrics.recordUserAction('FolderShortcut.Remove'); + return removedIndex; + } + + // No item is removed. + return -1; + }, + + /** + * @param {string} path Path to be checked. + * @return {boolean} True if the given |path| exists in the array. False + * otherwise. + */ + exists: function(path) { + var index = this.getIndex(path); + return (index >= 0); + }, + + /** + * Saves the current array to chrome.storage. + * @private + */ + save_: function() { + var obj = {}; + obj[FolderShortcutsDataModel.NAME] = this.array_; + chrome.storage.sync.set(obj, function() {}); + }, + + /** + * Creates a permutation array for 'permuted' event, which is compatible with + * a permutation array used in cr/ui/array_data_model.js. + * + * @param {array} oldArray Previous array before changing. + * @param {array} newArray New array after changing. + * @return {Array.<number>} Created permutation array. + * @private + */ + calculatePermutation_: function(oldArray, newArray) { + var oldIndex = 0; // Index of oldArray. + var newIndex = 0; // Index of newArray. + + // Note that both new and old arrays are sorted. + var permutation = []; + for (; oldIndex < oldArray.length; oldIndex++) { + if (newIndex >= newArray.length) { + // oldArray[oldIndex] is deleted, which is not in the new array. + permutation[oldIndex] = -1; + continue; + } + + while (newIndex < newArray.length) { + // Unchanged item, which exists in both new and old array. But the + // index may be changed. + if (oldArray[oldIndex] == newArray[newIndex]) { + permutation[oldIndex] = newIndex; + newIndex++; + break; + } + + // oldArray[oldIndex] is deleted, which is not in the new array. + if (this.compare(oldArray[oldIndex], newArray[newIndex]) < 0) { + permutation[oldIndex] = -1; + break; + } + + // In the case of this.compare(oldArray[oldIndex]) > 0: + // newArray[newIndex] is added, which is not in the old array. + newIndex++; + } + } + return permutation; + }, + + /** + * Fires a 'permuted' event, which is compatible with cr.ui.ArrayDataModel. + * @param {Array.<number>} Permutation array. + */ + firePermutedEvent_: function(permutation) { + var permutedEvent = new Event('permuted'); + permutedEvent.newLength = this.length; + permutedEvent.permutation = permutation; + this.dispatchEvent(permutedEvent); + + // Note: This model only fires 'permuted' event, because: + // 1) 'change' event is not necessary to fire since it is covered by + // 'permuted' event. + // 2) 'splice' and 'sorted' events are not implemented. These events are + // not used in NavigationListModel. We have to implement them when + // necessary. + } +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/commands.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/commands.js new file mode 100644 index 00000000000..f05f0f227d6 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/commands.js @@ -0,0 +1,455 @@ +// 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'; + +/** + * Command queue is the only way to modify images. + * Supports undo/redo. + * Command execution is asynchronous (callback-based). + * + * @param {Document} document Document to create canvases in. + * @param {HTMLCanvasElement} canvas The canvas with the original image. + * @param {function(callback)} saveFunction Function to save the image. + * @constructor + */ +function CommandQueue(document, canvas, saveFunction) { + this.document_ = document; + this.undo_ = []; + this.redo_ = []; + this.subscribers_ = []; + this.currentImage_ = canvas; + + // Current image may be null or not-null but with width = height = 0. + // Copying an image with zero dimensions causes js errors. + if (this.currentImage_) { + this.baselineImage_ = document.createElement('canvas'); + this.baselineImage_.width = this.currentImage_.width; + this.baselineImage_.height = this.currentImage_.height; + if (this.currentImage_.width > 0 && this.currentImage_.height > 0) { + var context = this.baselineImage_.getContext('2d'); + context.drawImage(this.currentImage_, 0, 0); + } + } else { + this.baselineImage_ = null; + } + + this.previousImage_ = document.createElement('canvas'); + this.previousImageAvailable_ = false; + + this.saveFunction_ = saveFunction; + this.busy_ = false; + this.UIContext_ = {}; +} + +/** + * Attach the UI elements to the command queue. + * Once the UI is attached the results of image manipulations are displayed. + * + * @param {ImageView} imageView The ImageView object to display the results. + * @param {ImageEditor.Prompt} prompt Prompt to use with this CommandQueue. + * @param {function(boolean)} lock Function to enable/disable buttons etc. + */ +CommandQueue.prototype.attachUI = function(imageView, prompt, lock) { + this.UIContext_ = { + imageView: imageView, + prompt: prompt, + lock: lock + }; +}; + +/** + * Execute the action when the queue is not busy. + * @param {function} callback Callback. + */ +CommandQueue.prototype.executeWhenReady = function(callback) { + if (this.isBusy()) + this.subscribers_.push(callback); + else + setTimeout(callback, 0); +}; + +/** + * @return {boolean} True if the command queue is busy. + */ +CommandQueue.prototype.isBusy = function() { return this.busy_ }; + +/** + * Set the queue state to busy. Lock the UI. + * @private + */ +CommandQueue.prototype.setBusy_ = function() { + if (this.busy_) + throw new Error('CommandQueue already busy'); + + this.busy_ = true; + + if (this.UIContext_.lock) + this.UIContext_.lock(true); + + ImageUtil.trace.resetTimer('command-busy'); +}; + +/** + * Set the queue state to not busy. Unlock the UI and execute pending actions. + * @private + */ +CommandQueue.prototype.clearBusy_ = function() { + if (!this.busy_) + throw new Error('Inconsistent CommandQueue already not busy'); + + this.busy_ = false; + + // Execute the actions requested while the queue was busy. + while (this.subscribers_.length) + this.subscribers_.shift()(); + + if (this.UIContext_.lock) + this.UIContext_.lock(false); + + ImageUtil.trace.reportTimer('command-busy'); +}; + +/** + * Commit the image change: save and unlock the UI. + * @param {number=} opt_delay Delay in ms (to avoid disrupting the animation). + * @private + */ +CommandQueue.prototype.commit_ = function(opt_delay) { + setTimeout(this.saveFunction_.bind(null, this.clearBusy_.bind(this)), + opt_delay || 0); +}; + +/** + * Internal function to execute the command in a given context. + * + * @param {Command} command The command to execute. + * @param {Object} uiContext The UI context. + * @param {function} callback Completion callback. + * @private + */ +CommandQueue.prototype.doExecute_ = function(command, uiContext, callback) { + if (!this.currentImage_) + throw new Error('Cannot operate on null image'); + + // Remember one previous image so that the first undo is as fast as possible. + this.previousImage_.width = this.currentImage_.width; + this.previousImage_.height = this.currentImage_.height; + this.previousImageAvailable_ = true; + var context = this.previousImage_.getContext('2d'); + context.drawImage(this.currentImage_, 0, 0); + + command.execute( + this.document_, + this.currentImage_, + function(result, opt_delay) { + this.currentImage_ = result; + callback(opt_delay); + }.bind(this), + uiContext); +}; + +/** + * Executes the command. + * + * @param {Command} command Command to execute. + * @param {boolean=} opt_keep_redo True if redo stack should not be cleared. + */ +CommandQueue.prototype.execute = function(command, opt_keep_redo) { + this.setBusy_(); + + if (!opt_keep_redo) + this.redo_ = []; + + this.undo_.push(command); + + this.doExecute_(command, this.UIContext_, this.commit_.bind(this)); +}; + +/** + * @return {boolean} True if Undo is applicable. + */ +CommandQueue.prototype.canUndo = function() { + return this.undo_.length != 0; +}; + +/** + * Undo the most recent command. + */ +CommandQueue.prototype.undo = function() { + if (!this.canUndo()) + throw new Error('Cannot undo'); + + this.setBusy_(); + + var command = this.undo_.pop(); + this.redo_.push(command); + + var self = this; + + function complete() { + var delay = command.revertView( + self.currentImage_, self.UIContext_.imageView); + self.commit_(delay); + } + + if (this.previousImageAvailable_) { + // First undo after an execute call. + this.currentImage_.width = this.previousImage_.width; + this.currentImage_.height = this.previousImage_.height; + var context = this.currentImage_.getContext('2d'); + context.drawImage(this.previousImage_, 0, 0); + + // Free memory. + this.previousImage_.width = 0; + this.previousImage_.height = 0; + this.previousImageAvailable_ = false; + + complete(); + // TODO(kaznacheev) Consider recalculating previousImage_ right here + // by replaying the commands in the background. + } else { + this.currentImage_.width = this.baselineImage_.width; + this.currentImage_.height = this.baselineImage_.height; + var context = this.currentImage_.getContext('2d'); + context.drawImage(this.baselineImage_, 0, 0); + + var replay = function(index) { + if (index < self.undo_.length) + self.doExecute_(self.undo_[index], {}, replay.bind(null, index + 1)); + else { + complete(); + } + }; + + replay(0); + } +}; + +/** + * @return {boolean} True if Redo is applicable. + */ +CommandQueue.prototype.canRedo = function() { + return this.redo_.length != 0; +}; + +/** + * Repeat the command that was recently un-done. + */ +CommandQueue.prototype.redo = function() { + if (!this.canRedo()) + throw new Error('Cannot redo'); + + this.execute(this.redo_.pop(), true); +}; + +/** + * Closes internal buffers. Call to ensure, that internal buffers are freed + * as soon as possible. + */ +CommandQueue.prototype.close = function() { + // Free memory used by the undo buffer. + this.previousImage_.width = 0; + this.previousImage_.height = 0; + this.previousImageAvailable_ = false; + + if (this.baselineImage_) { + this.baselineImage_.width = 0; + this.baselineImage_.height = 0; + } +}; + +/** + * Command object encapsulates an operation on an image and a way to visualize + * its result. + * + * @param {string} name Command name. + * @constructor + */ +function Command(name) { + this.name_ = name; +} + +/** + * @return {string} String representation of the command. + */ +Command.prototype.toString = function() { + return 'Command ' + this.name_; +}; + +/** + * Execute the command and visualize its results. + * + * The two actions are combined into one method because sometimes it is nice + * to be able to show partial results for slower operations. + * + * @param {Document} document Document on which to execute command. + * @param {HTMLCanvasElement} srcCanvas Canvas to execute on. + * @param {function(HTMLCanvasElement, number)} callback Callback to call on + * completion. + * @param {Object} uiContext Context to work in. + */ +Command.prototype.execute = function(document, srcCanvas, callback, uiContext) { + console.error('Command.prototype.execute not implemented'); +}; + +/** + * Visualize reversion of the operation. + * + * @param {HTMLCanvasElement} canvas Image data to use. + * @param {ImageView} imageView ImageView to revert. + * @return {number} Animation duration in ms. + */ +Command.prototype.revertView = function(canvas, imageView) { + imageView.replace(canvas); + return 0; +}; + +/** + * Creates canvas to render on. + * + * @param {Document} document Document to create canvas in. + * @param {HTMLCanvasElement} srcCanvas to copy optional dimensions from. + * @param {number=} opt_width new canvas width. + * @param {number=} opt_height new canvas height. + * @return {HTMLCanvasElement} Newly created canvas. + * @private + */ +Command.prototype.createCanvas_ = function( + document, srcCanvas, opt_width, opt_height) { + var result = document.createElement('canvas'); + result.width = opt_width || srcCanvas.width; + result.height = opt_height || srcCanvas.height; + return result; +}; + + +/** + * Rotate command + * @param {number} rotate90 Rotation angle in 90 degree increments (signed). + * @constructor + * @extends {Command} + */ +Command.Rotate = function(rotate90) { + Command.call(this, 'rotate(' + rotate90 * 90 + 'deg)'); + this.rotate90_ = rotate90; +}; + +Command.Rotate.prototype = { __proto__: Command.prototype }; + +/** @override */ +Command.Rotate.prototype.execute = function( + document, srcCanvas, callback, uiContext) { + var result = this.createCanvas_( + document, + srcCanvas, + (this.rotate90_ & 1) ? srcCanvas.height : srcCanvas.width, + (this.rotate90_ & 1) ? srcCanvas.width : srcCanvas.height); + ImageUtil.drawImageTransformed( + result, srcCanvas, 1, 1, this.rotate90_ * Math.PI / 2); + var delay; + if (uiContext.imageView) { + delay = uiContext.imageView.replaceAndAnimate(result, null, this.rotate90_); + } + setTimeout(callback, 0, result, delay); +}; + +/** @override */ +Command.Rotate.prototype.revertView = function(canvas, imageView) { + return imageView.replaceAndAnimate(canvas, null, -this.rotate90_); +}; + + +/** + * Crop command. + * + * @param {Rect} imageRect Crop rectangle in image coordinates. + * @constructor + * @extends {Command} + */ +Command.Crop = function(imageRect) { + Command.call(this, 'crop' + imageRect.toString()); + this.imageRect_ = imageRect; +}; + +Command.Crop.prototype = { __proto__: Command.prototype }; + +/** @override */ +Command.Crop.prototype.execute = function( + document, srcCanvas, callback, uiContext) { + var result = this.createCanvas_( + document, srcCanvas, this.imageRect_.width, this.imageRect_.height); + Rect.drawImage(result.getContext('2d'), srcCanvas, null, this.imageRect_); + var delay; + if (uiContext.imageView) { + delay = uiContext.imageView.replaceAndAnimate(result, this.imageRect_, 0); + } + setTimeout(callback, 0, result, delay); +}; + +/** @override */ +Command.Crop.prototype.revertView = function(canvas, imageView) { + return imageView.animateAndReplace(canvas, this.imageRect_); +}; + + +/** + * Filter command. + * + * @param {string} name Command name. + * @param {function(ImageData,ImageData,number,number)} filter Filter function. + * @param {string} message Message to display when done. + * @constructor + * @extends {Command} + */ +Command.Filter = function(name, filter, message) { + Command.call(this, name); + this.filter_ = filter; + this.message_ = message; +}; + +Command.Filter.prototype = { __proto__: Command.prototype }; + +/** @override */ +Command.Filter.prototype.execute = function( + document, srcCanvas, callback, uiContext) { + var result = this.createCanvas_(document, srcCanvas); + + var self = this; + + var previousRow = 0; + + function onProgressVisible(updatedRow, rowCount) { + if (updatedRow == rowCount) { + uiContext.imageView.replace(result); + if (self.message_) + uiContext.prompt.show(self.message_, 2000); + callback(result); + } else { + var viewport = uiContext.imageView.viewport_; + + var imageStrip = new Rect(viewport.getImageBounds()); + imageStrip.top = previousRow; + imageStrip.height = updatedRow - previousRow; + + var screenStrip = new Rect(viewport.getImageBoundsOnScreen()); + screenStrip.top = Math.round(viewport.imageToScreenY(previousRow)); + screenStrip.height = + Math.round(viewport.imageToScreenY(updatedRow)) - screenStrip.top; + + uiContext.imageView.paintDeviceRect( + viewport.screenToDeviceRect(screenStrip), result, imageStrip); + previousRow = updatedRow; + } + } + + function onProgressInvisible(updatedRow, rowCount) { + if (updatedRow == rowCount) { + callback(result); + } + } + + filter.applyByStrips(result, srcCanvas, this.filter_, + uiContext.imageView ? onProgressVisible : onProgressInvisible); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/exif_encoder.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/exif_encoder.js new file mode 100644 index 00000000000..e81b8fba899 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/exif_encoder.js @@ -0,0 +1,569 @@ +// 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'; + +// TODO:(kaznacheev) Share the EXIF constants with exif_parser.js +var EXIF_MARK_SOS = 0xffda; // Start of "stream" (the actual image data). +var EXIF_MARK_SOI = 0xffd8; // Start of image data. +var EXIF_MARK_EOI = 0xffd9; // End of image data. + +var EXIF_MARK_APP0 = 0xffe0; // APP0 block, most commonly JFIF data. +var EXIF_MARK_EXIF = 0xffe1; // Start of exif block. + +var EXIF_ALIGN_LITTLE = 0x4949; // Indicates little endian exif data. +var EXIF_ALIGN_BIG = 0x4d4d; // Indicates big endian exif data. + +var EXIF_TAG_TIFF = 0x002a; // First directory containing TIFF data. +var EXIF_TAG_GPSDATA = 0x8825; // Pointer from TIFF to the GPS directory. +var EXIF_TAG_EXIFDATA = 0x8769; // Pointer from TIFF to the EXIF IFD. + +var EXIF_TAG_JPG_THUMB_OFFSET = 0x0201; // Pointer from TIFF to thumbnail. +var EXIF_TAG_JPG_THUMB_LENGTH = 0x0202; // Length of thumbnail data. + +var EXIF_TAG_IMAGE_WIDTH = 0x0100; +var EXIF_TAG_IMAGE_HEIGHT = 0x0101; + +var EXIF_TAG_ORIENTATION = 0x0112; +var EXIF_TAG_X_DIMENSION = 0xA002; +var EXIF_TAG_Y_DIMENSION = 0xA003; + +/** + * The Exif metadata encoder. + * Uses the metadata format as defined by ExifParser. + * @param {Object} original_metadata Metadata to encode. + * @constructor + * @extends {ImageEncoder.MetadataEncoder} + */ +function ExifEncoder(original_metadata) { + ImageEncoder.MetadataEncoder.apply(this, arguments); + + this.ifd_ = this.metadata_.ifd; + if (!this.ifd_) + this.ifd_ = this.metadata_.ifd = {}; +} + +ExifEncoder.prototype = {__proto__: ImageEncoder.MetadataEncoder.prototype}; + +ImageEncoder.registerMetadataEncoder(ExifEncoder, 'image/jpeg'); + +/** + * @param {HTMLCanvasElement|Object} canvas Canvas or anything with + * width and height properties. + */ +ExifEncoder.prototype.setImageData = function(canvas) { + var image = this.ifd_.image; + if (!image) + image = this.ifd_.image = {}; + + // Only update width/height in this directory if they are present. + if (image[EXIF_TAG_IMAGE_WIDTH] && image[EXIF_TAG_IMAGE_HEIGHT]) { + image[EXIF_TAG_IMAGE_WIDTH].value = canvas.width; + image[EXIF_TAG_IMAGE_HEIGHT].value = canvas.height; + } + + var exif = this.ifd_.exif; + if (!exif) + exif = this.ifd_.exif = {}; + ExifEncoder.findOrCreateTag(image, EXIF_TAG_EXIFDATA); + ExifEncoder.findOrCreateTag(exif, EXIF_TAG_X_DIMENSION).value = canvas.width; + ExifEncoder.findOrCreateTag(exif, EXIF_TAG_Y_DIMENSION).value = canvas.height; + + this.metadata_.width = canvas.width; + this.metadata_.height = canvas.height; + + // Always save in default orientation. + delete this.metadata_.imageTransform; + ExifEncoder.findOrCreateTag(image, EXIF_TAG_ORIENTATION).value = 1; +}; + + +/** + * @param {HTMLCanvasElement} canvas Thumbnail canvas. + * @param {number} quality (0..1] Thumbnail encoding quality. + */ +ExifEncoder.prototype.setThumbnailData = function(canvas, quality) { + // Empirical formula with reasonable behavior: + // 10K for 1Mpix, 30K for 5Mpix, 50K for 9Mpix and up. + var pixelCount = this.metadata_.width * this.metadata_.height; + var maxEncodedSize = 5000 * Math.min(10, 1 + pixelCount / 1000000); + + var DATA_URL_PREFIX = 'data:' + this.mimeType + ';base64,'; + var BASE64_BLOAT = 4 / 3; + var maxDataURLLength = + DATA_URL_PREFIX.length + Math.ceil(maxEncodedSize * BASE64_BLOAT); + + for (;; quality *= 0.8) { + ImageEncoder.MetadataEncoder.prototype.setThumbnailData.call( + this, canvas, quality); + if (this.metadata_.thumbnailURL.length <= maxDataURLLength || quality < 0.2) + break; + } + + if (this.metadata_.thumbnailURL.length <= maxDataURLLength) { + var thumbnail = this.ifd_.thumbnail; + if (!thumbnail) + thumbnail = this.ifd_.thumbnail = {}; + + ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_IMAGE_WIDTH).value = + canvas.width; + + ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_IMAGE_HEIGHT).value = + canvas.height; + + // The values for these tags will be set in ExifWriter.encode. + ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_JPG_THUMB_OFFSET); + ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_JPG_THUMB_LENGTH); + + // Always save in default orientation. + ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_ORIENTATION).value = 1; + } else { + console.warn( + 'Thumbnail URL too long: ' + this.metadata_.thumbnailURL.length); + // Delete thumbnail ifd so that it is not written out to a file, but + // keep thumbnailURL for display purposes. + if (this.ifd_.thumbnail) { + delete this.ifd_.thumbnail; + } + } + delete this.metadata_.thumbnailTransform; +}; + +/** + * Return a range where the metadata is (or should be) located. + * @param {string} encodedImage Raw image data to look for metadata. + * @return {Object} An object with from and to properties. + */ +ExifEncoder.prototype.findInsertionRange = function(encodedImage) { + function getWord(pos) { + if (pos + 2 > encodedImage.length) + throw 'Reading past the buffer end @' + pos; + return encodedImage.charCodeAt(pos) << 8 | encodedImage.charCodeAt(pos + 1); + } + + if (getWord(0) != EXIF_MARK_SOI) + throw new Error('Jpeg data starts from 0x' + getWord(0).toString(16)); + + var sectionStart = 2; + + // Default: an empty range right after SOI. + // Will be returned in absence of APP0 or Exif sections. + var range = {from: sectionStart, to: sectionStart}; + + for (;;) { + var tag = getWord(sectionStart); + + if (tag == EXIF_MARK_SOS) + break; + + var nextSectionStart = sectionStart + 2 + getWord(sectionStart + 2); + if (nextSectionStart <= sectionStart || + nextSectionStart > encodedImage.length) + throw new Error('Invalid section size in jpeg data'); + + if (tag == EXIF_MARK_APP0) { + // Assert that we have not seen the Exif section yet. + if (range.from != range.to) + throw new Error('APP0 section found after EXIF section'); + // An empty range right after the APP0 segment. + range.from = range.to = nextSectionStart; + } else if (tag == EXIF_MARK_EXIF) { + // A range containing the existing EXIF section. + range.from = sectionStart; + range.to = nextSectionStart; + } + sectionStart = nextSectionStart; + } + + return range; +}; + +/** + * @return {ArrayBuffer} serialized metadata ready to write to an image file. + */ +ExifEncoder.prototype.encode = function() { + var HEADER_SIZE = 10; + + // Allocate the largest theoretically possible size. + var bytes = new Uint8Array(0x10000); + + // Serialize header + var hw = new ByteWriter(bytes.buffer, 0, HEADER_SIZE); + hw.writeScalar(EXIF_MARK_EXIF, 2); + hw.forward('size', 2); + hw.writeString('Exif\0\0'); // Magic string. + + // First serialize the content of the exif section. + // Use a ByteWriter starting at HEADER_SIZE offset so that tell() positions + // can be directly mapped to offsets as encoded in the dictionaries. + var bw = new ByteWriter(bytes.buffer, HEADER_SIZE); + + if (this.metadata_.littleEndian) { + bw.setByteOrder(ByteWriter.LITTLE_ENDIAN); + bw.writeScalar(EXIF_ALIGN_LITTLE, 2); + } else { + bw.setByteOrder(ByteWriter.BIG_ENDIAN); + bw.writeScalar(EXIF_ALIGN_BIG, 2); + } + + bw.writeScalar(EXIF_TAG_TIFF, 2); + + bw.forward('image-dir', 4); // The pointer should point right after itself. + bw.resolveOffset('image-dir'); + + ExifEncoder.encodeDirectory(bw, this.ifd_.image, + [EXIF_TAG_EXIFDATA, EXIF_TAG_GPSDATA], 'thumb-dir'); + + if (this.ifd_.exif) { + bw.resolveOffset(EXIF_TAG_EXIFDATA); + ExifEncoder.encodeDirectory(bw, this.ifd_.exif); + } else { + if (EXIF_TAG_EXIFDATA in this.ifd_.image) + throw new Error('Corrupt exif dictionary reference'); + } + + if (this.ifd_.gps) { + bw.resolveOffset(EXIF_TAG_GPSDATA); + ExifEncoder.encodeDirectory(bw, this.ifd_.gps); + } else { + if (EXIF_TAG_GPSDATA in this.ifd_.image) + throw new Error('Missing gps dictionary reference'); + } + + if (this.ifd_.thumbnail) { + bw.resolveOffset('thumb-dir'); + ExifEncoder.encodeDirectory( + bw, + this.ifd_.thumbnail, + [EXIF_TAG_JPG_THUMB_OFFSET, EXIF_TAG_JPG_THUMB_LENGTH]); + + var thumbnailDecoded = + ImageEncoder.decodeDataURL(this.metadata_.thumbnailURL); + bw.resolveOffset(EXIF_TAG_JPG_THUMB_OFFSET); + bw.resolve(EXIF_TAG_JPG_THUMB_LENGTH, thumbnailDecoded.length); + bw.writeString(thumbnailDecoded); + } else { + bw.resolve('thumb-dir', 0); + } + + bw.checkResolved(); + + var totalSize = HEADER_SIZE + bw.tell(); + hw.resolve('size', totalSize - 2); // The marker is excluded. + hw.checkResolved(); + + var subarray = new Uint8Array(totalSize); + for (var i = 0; i != totalSize; i++) { + subarray[i] = bytes[i]; + } + return subarray.buffer; +}; + +/* + * Static methods. + */ + +/** + * Write the contents of an IFD directory. + * @param {ByteWriter} bw ByteWriter to use. + * @param {Object} directory A directory map as created by ExifParser. + * @param {Array} resolveLater An array of tag ids for which the values will be + * resolved later. + * @param {string} nextDirPointer A forward key for the pointer to the next + * directory. If omitted the pointer is set to 0. + */ +ExifEncoder.encodeDirectory = function( + bw, directory, resolveLater, nextDirPointer) { + + var longValues = []; + + bw.forward('dir-count', 2); + var count = 0; + + for (var key in directory) { + var tag = directory[key]; + bw.writeScalar(tag.id, 2); + bw.writeScalar(tag.format, 2); + bw.writeScalar(tag.componentCount, 4); + + var width = ExifEncoder.getComponentWidth(tag) * tag.componentCount; + + if (resolveLater && (resolveLater.indexOf(tag.id) >= 0)) { + // The actual value depends on further computations. + if (tag.componentCount != 1 || width > 4) + throw new Error('Cannot forward the pointer for ' + tag.id); + bw.forward(tag.id, width); + } else if (width <= 4) { + // The value fits into 4 bytes, write it immediately. + ExifEncoder.writeValue(bw, tag); + } else { + // The value does not fit, forward the 4 byte offset to the actual value. + width = 4; + bw.forward(tag.id, width); + longValues.push(tag); + } + bw.skip(4 - width); // Align so that the value take up exactly 4 bytes. + count++; + } + + bw.resolve('dir-count', count); + + if (nextDirPointer) { + bw.forward(nextDirPointer, 4); + } else { + bw.writeScalar(0, 4); + } + + // Write out the long values and resolve pointers. + for (var i = 0; i != longValues.length; i++) { + var longValue = longValues[i]; + bw.resolveOffset(longValue.id); + ExifEncoder.writeValue(bw, longValue); + } +}; + +/** + * @param {{format:number, id:number}} tag EXIF tag object. + * @return {number} Width in bytes of the data unit associated with this tag. + * TODO(kaznacheev): Share with ExifParser? + */ +ExifEncoder.getComponentWidth = function(tag) { + switch (tag.format) { + case 1: // Byte + case 2: // String + case 7: // Undefined + return 1; + + case 3: // Short + return 2; + + case 4: // Long + case 9: // Signed Long + return 4; + + case 5: // Rational + case 10: // Signed Rational + return 8; + + default: // ??? + console.warn('Unknown tag format 0x' + + Number(tag.id).toString(16) + ': ' + tag.format); + return 4; + } +}; + +/** + * Writes out the tag value. + * @param {ByteWriter} bw Writer to use. + * @param {Object} tag Tag, which value to write. + */ +ExifEncoder.writeValue = function(bw, tag) { + if (tag.format == 2) { // String + if (tag.componentCount != tag.value.length) { + throw new Error( + 'String size mismatch for 0x' + Number(tag.id).toString(16)); + } + bw.writeString(tag.value); + } else { // Scalar or rational + var width = ExifEncoder.getComponentWidth(tag); + + var writeComponent = function(value, signed) { + if (width == 8) { + bw.writeScalar(value[0], 4, signed); + bw.writeScalar(value[1], 4, signed); + } else { + bw.writeScalar(value, width, signed); + } + }; + + var signed = (tag.format == 9 || tag.format == 10); + if (tag.componentCount == 1) { + writeComponent(tag.value, signed); + } else { + for (var i = 0; i != tag.componentCount; i++) { + writeComponent(tag.value[i], signed); + } + } + } +}; + +/** + * @param {{Object.<number,Object>}} directory EXIF directory. + * @param {number} id Tag id. + * @param {number} format Tag format + * (used in {@link ExifEncoder#getComponentWidth}). + * @param {number} componentCount Number of components in this tag. + * @return {{id:number, format:number, componentCount:number}} + * Tag found or created. + */ +ExifEncoder.findOrCreateTag = function(directory, id, format, componentCount) { + if (!(id in directory)) { + directory[id] = { + id: id, + format: format || 3, // Short + componentCount: componentCount || 1 + }; + } + return directory[id]; +}; + +/** + * ByteWriter class. + * @param {ArrayBuffer} arrayBuffer Underlying buffer to use. + * @param {number} offset Offset at which to start writing. + * @param {number} length Maximum length to use. + * @class + * @constructor + */ +function ByteWriter(arrayBuffer, offset, length) { + length = length || (arrayBuffer.byteLength - offset); + this.view_ = new DataView(arrayBuffer, offset, length); + this.littleEndian_ = false; + this.pos_ = 0; + this.forwards_ = {}; +} + +/** + * Little endian byte order. + * @type {number} + */ +ByteWriter.LITTLE_ENDIAN = 0; + +/** + * Bug endian byte order. + * @type {number} + */ +ByteWriter.BIG_ENDIAN = 1; + +/** + * Set the byte ordering for future writes. + * @param {number} order ByteOrder to use {ByteWriter.LITTLE_ENDIAN} + * or {ByteWriter.BIG_ENDIAN}. + */ +ByteWriter.prototype.setByteOrder = function(order) { + this.littleEndian_ = (order == ByteWriter.LITTLE_ENDIAN); +}; + +/** + * @return {number} the current write position. + */ +ByteWriter.prototype.tell = function() { return this.pos_ }; + +/** + * Skips desired amount of bytes in output stream. + * @param {number} count Byte count to skip. + */ +ByteWriter.prototype.skip = function(count) { + this.validateWrite(count); + this.pos_ += count; +}; + +/** + * Check if the buffer has enough room to read 'width' bytes. Throws an error + * if it has not. + * @param {number} width Amount of bytes to check. + */ +ByteWriter.prototype.validateWrite = function(width) { + if (this.pos_ + width > this.view_.byteLength) + throw new Error('Writing past the end of the buffer'); +}; + +/** + * Writes scalar value to output stream. + * @param {number} value Value to write. + * @param {number} width Desired width of written value. + * @param {boolean=} opt_signed True if value represents signed number. + */ +ByteWriter.prototype.writeScalar = function(value, width, opt_signed) { + var method; +// The below switch is so verbose for two reasons: +// 1. V8 is faster on method names which are 'symbols'. +// 2. Method names are discoverable by full text search. + switch (width) { + case 1: + method = opt_signed ? 'setInt8' : 'setUint8'; + break; + + case 2: + method = opt_signed ? 'setInt16' : 'setUint16'; + break; + + case 4: + method = opt_signed ? 'setInt32' : 'setUint32'; + break; + + case 8: + method = opt_signed ? 'setInt64' : 'setUint64'; + break; + + default: + throw new Error('Invalid width: ' + width); + break; + } + + this.validateWrite(width); + this.view_[method](this.pos_, value, this.littleEndian_); + this.pos_ += width; +}; + +/** + * Writes string. + * @param {string} str String to write. + */ +ByteWriter.prototype.writeString = function(str) { + this.validateWrite(str.length); + for (var i = 0; i != str.length; i++) { + this.view_.setUint8(this.pos_++, str.charCodeAt(i)); + } +}; + +/** + * Allocate the space for 'width' bytes for the value that will be set later. + * To be followed by a 'resolve' call with the same key. + * @param {string} key A key to identify the value. + * @param {number} width Width of the value in bytes. + */ +ByteWriter.prototype.forward = function(key, width) { + if (key in this.forwards_) + throw new Error('Duplicate forward key ' + key); + this.validateWrite(width); + this.forwards_[key] = { + pos: this.pos_, + width: width + }; + this.pos_ += width; +}; + +/** + * Set the value previously allocated with a 'forward' call. + * @param {string} key A key to identify the value. + * @param {number} value value to write in pre-allocated space. + */ +ByteWriter.prototype.resolve = function(key, value) { + if (!(key in this.forwards_)) + throw new Error('Undeclared forward key ' + key.toString(16)); + var forward = this.forwards_[key]; + var curPos = this.pos_; + this.pos_ = forward.pos; + this.writeScalar(value, forward.width); + this.pos_ = curPos; + delete this.forwards_[key]; +}; + +/** + * A shortcut to resolve the value to the current write position. + * @param {string} key A key to identify pre-allocated position. + */ +ByteWriter.prototype.resolveOffset = function(key) { + this.resolve(key, this.tell()); +}; + +/** + * Check if every forward has been resolved, throw and error if not. + */ +ByteWriter.prototype.checkResolved = function() { + for (var key in this.forwards_) { + throw new Error('Unresolved forward pointer ' + key.toString(16)); + } +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/filter.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/filter.js new file mode 100644 index 00000000000..e06d4ef2dd8 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/filter.js @@ -0,0 +1,612 @@ +// 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'; + +/** + * A namespace for image filter utilities. + */ +var filter = {}; + +/** + * Create a filter from name and options. + * + * @param {string} name Maps to a filter method name. + * @param {Object} options A map of filter-specific options. + * @return {function(ImageData,ImageData,number,number)} created function. + */ +filter.create = function(name, options) { + var filterFunc = filter[name](options); + return function() { + var time = Date.now(); + filterFunc.apply(null, arguments); + var dst = arguments[0]; + var mPixPerSec = dst.width * dst.height / 1000 / (Date.now() - time); + ImageUtil.trace.report(name, Math.round(mPixPerSec * 10) / 10 + 'Mps'); + } +}; + +/** + * Apply a filter to a image by splitting it into strips. + * + * To be used with large images to avoid freezing up the UI. + * + * @param {HTMLCanvasElement} dstCanvas Destination canvas. + * @param {HTMLCanvasElement} srcCanvas Source canvas. + * @param {function(ImageData,ImageData,number,number)} filterFunc Filter. + * @param {function(number, number)} progressCallback Progress callback. + * @param {number} maxPixelsPerStrip Pixel number to process at once. + */ +filter.applyByStrips = function( + dstCanvas, srcCanvas, filterFunc, progressCallback, maxPixelsPerStrip) { + var dstContext = dstCanvas.getContext('2d'); + var srcContext = srcCanvas.getContext('2d'); + var source = srcContext.getImageData(0, 0, srcCanvas.width, srcCanvas.height); + + var stripCount = Math.ceil(srcCanvas.width * srcCanvas.height / + (maxPixelsPerStrip || 1000000)); // 1 Mpix is a reasonable default. + + var strip = srcContext.getImageData(0, 0, + srcCanvas.width, Math.ceil(srcCanvas.height / stripCount)); + + var offset = 0; + + function filterStrip() { + // If the strip overlaps the bottom of the source image we cannot shrink it + // and we cannot fill it partially (since canvas.putImageData always draws + // the entire buffer). + // Instead we move the strip up several lines (converting those lines + // twice is a small price to pay). + if (offset > source.height - strip.height) { + offset = source.height - strip.height; + } + + filterFunc(strip, source, 0, offset); + dstContext.putImageData(strip, 0, offset); + + offset += strip.height; + + if (offset < source.height) { + setTimeout(filterStrip, 0); + } else { + ImageUtil.trace.reportTimer('filter-commit'); + } + + progressCallback(offset, source.height); + } + + ImageUtil.trace.resetTimer('filter-commit'); + filterStrip(); +}; + +/** + * Return a color histogram for an image. + * + * @param {HTMLCanvasElement|ImageData} source Image data to analyze. + * @return {{r: Array.<number>, g: Array.<number>, b: Array.<number>}} + * histogram. + */ +filter.getHistogram = function(source) { + var imageData; + if (source.constructor.name == 'HTMLCanvasElement') { + imageData = source.getContext('2d'). + getImageData(0, 0, source.width, source.height); + } else { + imageData = source; + } + + var r = []; + var g = []; + var b = []; + + for (var i = 0; i != 256; i++) { + r.push(0); + g.push(0); + b.push(0); + } + + var data = imageData.data; + var maxIndex = 4 * imageData.width * imageData.height; + for (var index = 0; index != maxIndex;) { + r[data[index++]]++; + g[data[index++]]++; + b[data[index++]]++; + index++; + } + + return { r: r, g: g, b: b }; +}; + +/** + * Compute the function for every integer value from 0 up to maxArg. + * + * Rounds and clips the results to fit the [0..255] range. + * Useful to speed up pixel manipulations. + * + * @param {number} maxArg Maximum argument value (inclusive). + * @param {function(number): number} func Function to precompute. + * @return {Uint8Array} Computed results. + */ +filter.precompute = function(maxArg, func) { + var results = new Uint8Array(maxArg + 1); + for (var arg = 0; arg <= maxArg; arg++) { + results[arg] = Math.max(0, Math.min(0xFF, Math.round(func(arg)))); + } + return results; +}; + +/** + * Convert pixels by applying conversion tables to each channel individually. + * + * @param {Array.<number>} rMap Red channel conversion table. + * @param {Array.<number>} gMap Green channel conversion table. + * @param {Array.<number>} bMap Blue channel conversion table. + * @param {ImageData} dst Destination image data. Can be smaller than the + * source, must completely fit inside the source. + * @param {ImageData} src Source image data. + * @param {number} offsetX Horizontal offset of dst relative to src. + * @param {number} offsetY Vertical offset of dst relative to src. + */ +filter.mapPixels = function(rMap, gMap, bMap, dst, src, offsetX, offsetY) { + var dstData = dst.data; + var dstWidth = dst.width; + var dstHeight = dst.height; + + var srcData = src.data; + var srcWidth = src.width; + var srcHeight = src.height; + + if (offsetX < 0 || offsetX + dstWidth > srcWidth || + offsetY < 0 || offsetY + dstHeight > srcHeight) + throw new Error('Invalid offset'); + + var dstIndex = 0; + for (var y = 0; y != dstHeight; y++) { + var srcIndex = (offsetX + (offsetY + y) * srcWidth) * 4; + for (var x = 0; x != dstWidth; x++) { + dstData[dstIndex++] = rMap[srcData[srcIndex++]]; + dstData[dstIndex++] = gMap[srcData[srcIndex++]]; + dstData[dstIndex++] = bMap[srcData[srcIndex++]]; + dstIndex++; + srcIndex++; + } + } +}; + +/** + * Number of digits after period(in binary form) to preserve. + * @type {number} + */ +filter.FIXED_POINT_SHIFT = 16; + +/** + * Maximum value that can be represented in fixed point without overflow. + * @type {number} + */ +filter.MAX_FLOAT_VALUE = 0x7FFFFFFF >> filter.FIXED_POINT_SHIFT; + +/** + * Converts floating point to fixed. + * @param {number} x Number to convert. + * @return {number} Converted number. + */ +filter.floatToFixedPoint = function(x) { + // Math.round on negative arguments causes V8 to deoptimize the calling + // function, so we are using >> 0 instead. + return (x * (1 << filter.FIXED_POINT_SHIFT)) >> 0; +}; + +/** + * Perform an image convolution with a symmetrical 5x5 matrix: + * + * 0 0 w3 0 0 + * 0 w2 w1 w2 0 + * w3 w1 w0 w1 w3 + * 0 w2 w1 w2 0 + * 0 0 w3 0 0 + * + * @param {Array.<number>} weights See the picture above. + * @param {ImageData} dst Destination image data. Can be smaller than the + * source, must completely fit inside the source. + * @param {ImageData} src Source image data. + * @param {number} offsetX Horizontal offset of dst relative to src. + * @param {number} offsetY Vertical offset of dst relative to src. + */ +filter.convolve5x5 = function(weights, dst, src, offsetX, offsetY) { + var w0 = filter.floatToFixedPoint(weights[0]); + var w1 = filter.floatToFixedPoint(weights[1]); + var w2 = filter.floatToFixedPoint(weights[2]); + var w3 = filter.floatToFixedPoint(weights[3]); + + var dstData = dst.data; + var dstWidth = dst.width; + var dstHeight = dst.height; + var dstStride = dstWidth * 4; + + var srcData = src.data; + var srcWidth = src.width; + var srcHeight = src.height; + var srcStride = srcWidth * 4; + var srcStride2 = srcStride * 2; + + if (offsetX < 0 || offsetX + dstWidth > srcWidth || + offsetY < 0 || offsetY + dstHeight > srcHeight) + throw new Error('Invalid offset'); + + // Javascript is not very good at inlining constants. + // We inline manually and assert that the constant is equal to the variable. + if (filter.FIXED_POINT_SHIFT != 16) + throw new Error('Wrong fixed point shift'); + + var margin = 2; + + var startX = Math.max(0, margin - offsetX); + var endX = Math.min(dstWidth, srcWidth - margin - offsetX); + + var startY = Math.max(0, margin - offsetY); + var endY = Math.min(dstHeight, srcHeight - margin - offsetY); + + for (var y = startY; y != endY; y++) { + var dstIndex = y * dstStride + startX * 4; + var srcIndex = (y + offsetY) * srcStride + (startX + offsetX) * 4; + + for (var x = startX; x != endX; x++) { + for (var c = 0; c != 3; c++) { + var sum = w0 * srcData[srcIndex] + + w1 * (srcData[srcIndex - 4] + + srcData[srcIndex + 4] + + srcData[srcIndex - srcStride] + + srcData[srcIndex + srcStride]) + + w2 * (srcData[srcIndex - srcStride - 4] + + srcData[srcIndex + srcStride - 4] + + srcData[srcIndex - srcStride + 4] + + srcData[srcIndex + srcStride + 4]) + + w3 * (srcData[srcIndex - 8] + + srcData[srcIndex + 8] + + srcData[srcIndex - srcStride2] + + srcData[srcIndex + srcStride2]); + if (sum < 0) + dstData[dstIndex++] = 0; + else if (sum > 0xFF0000) + dstData[dstIndex++] = 0xFF; + else + dstData[dstIndex++] = sum >> 16; + srcIndex++; + } + srcIndex++; + dstIndex++; + } + } +}; + +/** + * Compute the average color for the image. + * + * @param {ImageData} imageData Image data to analyze. + * @return {{r: number, g: number, b: number}} average color. + */ +filter.getAverageColor = function(imageData) { + var data = imageData.data; + var width = imageData.width; + var height = imageData.height; + + var total = 0; + var r = 0; + var g = 0; + var b = 0; + + var maxIndex = 4 * width * height; + for (var i = 0; i != maxIndex;) { + total++; + r += data[i++]; + g += data[i++]; + b += data[i++]; + i++; + } + if (total == 0) return { r: 0, g: 0, b: 0 }; + return { r: r / total, g: g / total, b: b / total }; +}; + +/** + * Compute the average color with more weight given to pixes at the center. + * + * @param {ImageData} imageData Image data to analyze. + * @return {{r: number, g: number, b: number}} weighted average color. + */ +filter.getWeightedAverageColor = function(imageData) { + var data = imageData.data; + var width = imageData.width; + var height = imageData.height; + + var total = 0; + var r = 0; + var g = 0; + var b = 0; + + var center = Math.floor(width / 2); + var maxDist = center * Math.sqrt(2); + maxDist *= 2; // Weaken the effect of distance + + var i = 0; + for (var x = 0; x != width; x++) { + for (var y = 0; y != height; y++) { + var dist = Math.sqrt( + (x - center) * (x - center) + (y - center) * (y - center)); + var weight = (maxDist - dist) / maxDist; + + total += weight; + r += data[i++] * weight; + g += data[i++] * weight; + b += data[i++] * weight; + i++; + } + } + if (total == 0) return { r: 0, g: 0, b: 0 }; + return { r: r / total, g: g / total, b: b / total }; +}; + +/** + * Copy part of src image to dst, applying matrix color filter on-the-fly. + * + * The copied part of src should completely fit into dst (there is no clipping + * on either side). + * + * @param {Array.<number>} matrix 3x3 color matrix. + * @param {ImageData} dst Destination image data. + * @param {ImageData} src Source image data. + * @param {number} offsetX X offset in source to start processing. + * @param {number} offsetY Y offset in source to start processing. + */ +filter.colorMatrix3x3 = function(matrix, dst, src, offsetX, offsetY) { + var c11 = filter.floatToFixedPoint(matrix[0]); + var c12 = filter.floatToFixedPoint(matrix[1]); + var c13 = filter.floatToFixedPoint(matrix[2]); + var c21 = filter.floatToFixedPoint(matrix[3]); + var c22 = filter.floatToFixedPoint(matrix[4]); + var c23 = filter.floatToFixedPoint(matrix[5]); + var c31 = filter.floatToFixedPoint(matrix[6]); + var c32 = filter.floatToFixedPoint(matrix[7]); + var c33 = filter.floatToFixedPoint(matrix[8]); + + var dstData = dst.data; + var dstWidth = dst.width; + var dstHeight = dst.height; + + var srcData = src.data; + var srcWidth = src.width; + var srcHeight = src.height; + + if (offsetX < 0 || offsetX + dstWidth > srcWidth || + offsetY < 0 || offsetY + dstHeight > srcHeight) + throw new Error('Invalid offset'); + + // Javascript is not very good at inlining constants. + // We inline manually and assert that the constant is equal to the variable. + if (filter.FIXED_POINT_SHIFT != 16) + throw new Error('Wrong fixed point shift'); + + var dstIndex = 0; + for (var y = 0; y != dstHeight; y++) { + var srcIndex = (offsetX + (offsetY + y) * srcWidth) * 4; + for (var x = 0; x != dstWidth; x++) { + var r = srcData[srcIndex++]; + var g = srcData[srcIndex++]; + var b = srcData[srcIndex++]; + srcIndex++; + + var rNew = r * c11 + g * c12 + b * c13; + var gNew = r * c21 + g * c22 + b * c23; + var bNew = r * c31 + g * c32 + b * c33; + + if (rNew < 0) { + dstData[dstIndex++] = 0; + } else if (rNew > 0xFF0000) { + dstData[dstIndex++] = 0xFF; + } else { + dstData[dstIndex++] = rNew >> 16; + } + + if (gNew < 0) { + dstData[dstIndex++] = 0; + } else if (gNew > 0xFF0000) { + dstData[dstIndex++] = 0xFF; + } else { + dstData[dstIndex++] = gNew >> 16; + } + + if (bNew < 0) { + dstData[dstIndex++] = 0; + } else if (bNew > 0xFF0000) { + dstData[dstIndex++] = 0xFF; + } else { + dstData[dstIndex++] = bNew >> 16; + } + + dstIndex++; + } + } +}; + +/** + * Return a convolution filter function bound to specific weights. + * + * @param {Array.<number>} weights Weights for the convolution matrix + * (not normalized). + * @return {function(ImageData,ImageData,number,number)} Convolution filter. + */ +filter.createConvolutionFilter = function(weights) { + // Normalize the weights to sum to 1. + var total = 0; + for (var i = 0; i != weights.length; i++) { + total += weights[i] * (i ? 4 : 1); + } + + var normalized = []; + for (i = 0; i != weights.length; i++) { + normalized.push(weights[i] / total); + } + for (; i < 4; i++) { + normalized.push(0); + } + + var maxWeightedSum = 0xFF * + Math.abs(normalized[0]) + + Math.abs(normalized[1]) * 4 + + Math.abs(normalized[2]) * 4 + + Math.abs(normalized[3]) * 4; + if (maxWeightedSum > filter.MAX_FLOAT_VALUE) + throw new Error('convolve5x5 cannot convert the weights to fixed point'); + + return filter.convolve5x5.bind(null, normalized); +}; + +/** + * Creates matrix filter. + * @param {Array.<number>} matrix Color transformation matrix. + * @return {function(ImageData,ImageData,number,number)} Matrix filter. + */ +filter.createColorMatrixFilter = function(matrix) { + for (var r = 0; r != 3; r++) { + var maxRowSum = 0; + for (var c = 0; c != 3; c++) { + maxRowSum += 0xFF * Math.abs(matrix[r * 3 + c]); + } + if (maxRowSum > filter.MAX_FLOAT_VALUE) + throw new Error( + 'colorMatrix3x3 cannot convert the matrix to fixed point'); + } + return filter.colorMatrix3x3.bind(null, matrix); +}; + +/** + * Return a blur filter. + * @param {Object} options Blur options. + * @return {function(ImageData,ImageData,number,number)} Blur filter. + */ +filter.blur = function(options) { + if (options.radius == 1) + return filter.createConvolutionFilter( + [1, options.strength]); + else if (options.radius == 2) + return filter.createConvolutionFilter( + [1, options.strength, options.strength]); + else + return filter.createConvolutionFilter( + [1, options.strength, options.strength, options.strength]); +}; + +/** + * Return a sharpen filter. + * @param {Object} options Sharpen options. + * @return {function(ImageData,ImageData,number,number)} Sharpen filter. + */ +filter.sharpen = function(options) { + if (options.radius == 1) + return filter.createConvolutionFilter( + [5, -options.strength]); + else if (options.radius == 2) + return filter.createConvolutionFilter( + [10, -options.strength, -options.strength]); + else + return filter.createConvolutionFilter( + [15, -options.strength, -options.strength, -options.strength]); +}; + +/** + * Return an exposure filter. + * @param {Object} options exposure options. + * @return {function(ImageData,ImageData,number,number)} Exposure filter. + */ +filter.exposure = function(options) { + var pixelMap = filter.precompute( + 255, + function(value) { + if (options.brightness > 0) { + value *= (1 + options.brightness); + } else { + value += (0xFF - value) * options.brightness; + } + return 0x80 + + (value - 0x80) * Math.tan((options.contrast + 1) * Math.PI / 4); + }); + + return filter.mapPixels.bind(null, pixelMap, pixelMap, pixelMap); +}; + +/** + * Return a color autofix filter. + * @param {Object} options Histogram for autofix. + * @return {function(ImageData,ImageData,number,number)} Autofix filter. + */ +filter.autofix = function(options) { + return filter.mapPixels.bind(null, + filter.autofix.stretchColors(options.histogram.r), + filter.autofix.stretchColors(options.histogram.g), + filter.autofix.stretchColors(options.histogram.b)); +}; + +/** + * Return a conversion table that stretches the range of colors used + * in the image to 0..255. + * @param {Array.<number>} channelHistogram Histogram to calculate range. + * @return {Uint8Array} Color mapping array. + */ +filter.autofix.stretchColors = function(channelHistogram) { + var range = filter.autofix.getRange(channelHistogram); + return filter.precompute( + 255, + function(x) { + return (x - range.first) / (range.last - range.first) * 255; + } + ); +}; + +/** + * Return a range that encloses non-zero elements values in a histogram array. + * @param {Array.<number>} channelHistogram Histogram to analyze. + * @return {{first: number, last: number}} Channel range in histogram. + */ +filter.autofix.getRange = function(channelHistogram) { + var first = 0; + while (first < channelHistogram.length && channelHistogram[first] == 0) + first++; + + var last = channelHistogram.length - 1; + while (last >= 0 && channelHistogram[last] == 0) + last--; + + if (first >= last) // Stretching does not make sense + return {first: 0, last: channelHistogram.length - 1}; + else + return {first: first, last: last}; +}; + +/** + * Minimum channel offset that makes visual difference. If autofix calculated + * offset is less than SENSITIVITY, probably autofix is not needed. + * Reasonable empirical value. + * @type {number} + */ +filter.autofix.SENSITIVITY = 8; + +/** + * @param {Array.<number>} channelHistogram Histogram to analyze. + * @return {boolean} True if stretching this range to 0..255 would make + * a visible difference. + */ +filter.autofix.needsStretching = function(channelHistogram) { + var range = filter.autofix.getRange(channelHistogram); + return (range.first >= filter.autofix.SENSITIVITY || + range.last <= 255 - filter.autofix.SENSITIVITY); +}; + +/** + * @param {{r: Array.<number>, g: Array.<number>, b: Array.<number>}} histogram + * @return {boolean} True if the autofix would make a visible difference. + */ +filter.autofix.isApplicable = function(histogram) { + return filter.autofix.needsStretching(histogram.r) || + filter.autofix.needsStretching(histogram.g) || + filter.autofix.needsStretching(histogram.b); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_adjust.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_adjust.js new file mode 100644 index 00000000000..2abb10ed2f5 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_adjust.js @@ -0,0 +1,248 @@ +// 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 base class for simple filters that only modify the image content + * but do not modify the image dimensions. + * @constructor + * @extends ImageEditor.Mode + */ +ImageEditor.Mode.Adjust = function() { + ImageEditor.Mode.apply(this, arguments); + this.implicitCommit = true; + this.doneMessage_ = null; + this.viewportGeneration_ = 0; +}; + +ImageEditor.Mode.Adjust.prototype = {__proto__: ImageEditor.Mode.prototype}; + +/** @override */ +ImageEditor.Mode.Adjust.prototype.getCommand = function() { + if (!this.filter_) return null; + + return new Command.Filter(this.name, this.filter_, this.doneMessage_); +}; + +/** @override */ +ImageEditor.Mode.Adjust.prototype.cleanUpUI = function() { + ImageEditor.Mode.prototype.cleanUpUI.apply(this, arguments); + this.hidePreview(); +}; + +/** + * TODO(JSDOC) + */ +ImageEditor.Mode.Adjust.prototype.hidePreview = function() { + if (this.canvas_) { + this.canvas_.parentNode.removeChild(this.canvas_); + this.canvas_ = null; + } +}; + +/** + * TODO(JSDOC) + */ +ImageEditor.Mode.Adjust.prototype.cleanUpCaches = function() { + this.filter_ = null; + this.previewImageData_ = null; +}; + +/** + * TODO(JSDOC) + */ +ImageEditor.Mode.Adjust.prototype.reset = function() { + ImageEditor.Mode.prototype.reset.call(this); + this.hidePreview(); + this.cleanUpCaches(); +}; + +/** + * TODO(JSDOC) + * @param {Object} options // TODO(JSDOC). + */ +ImageEditor.Mode.Adjust.prototype.update = function(options) { + ImageEditor.Mode.prototype.update.apply(this, arguments); + + // We assume filter names are used in the UI directly. + // This will have to change with i18n. + this.filter_ = this.createFilter(options); + this.updatePreviewImage(); + ImageUtil.trace.resetTimer('preview'); + this.filter_(this.previewImageData_, this.originalImageData, 0, 0); + ImageUtil.trace.reportTimer('preview'); + this.canvas_.getContext('2d').putImageData( + this.previewImageData_, 0, 0); +}; + +/** + * Copy the source image data for the preview. + * Use the cached copy if the viewport has not changed. + */ +ImageEditor.Mode.Adjust.prototype.updatePreviewImage = function() { + if (!this.previewImageData_ || + this.viewportGeneration_ != this.getViewport().getCacheGeneration()) { + this.viewportGeneration_ = this.getViewport().getCacheGeneration(); + + if (!this.canvas_) { + this.canvas_ = this.getImageView().createOverlayCanvas(); + } + + this.getImageView().setupDeviceBuffer(this.canvas_); + + this.originalImageData = this.getImageView().copyScreenImageData(); + this.previewImageData_ = this.getImageView().copyScreenImageData(); + } +}; + +/* + * Own methods + */ + +/** + * TODO(JSDOC) + * @param {Object} options // TODO(JSDOC). + * @return {function(ImageData,ImageData,number,number)} Created function. + */ +ImageEditor.Mode.Adjust.prototype.createFilter = function(options) { + return filter.create(this.name, options); +}; + +/** + * A base class for color filters that are scale independent. + * @constructor + */ +ImageEditor.Mode.ColorFilter = function() { + ImageEditor.Mode.Adjust.apply(this, arguments); +}; + +ImageEditor.Mode.ColorFilter.prototype = + {__proto__: ImageEditor.Mode.Adjust.prototype}; + +/** + * TODO(JSDOC) + * @return {{r: Array.<number>, g: Array.<number>, b: Array.<number>}} + * histogram. + */ +ImageEditor.Mode.ColorFilter.prototype.getHistogram = function() { + return filter.getHistogram(this.getImageView().getThumbnail()); +}; + +/** + * Exposure/contrast filter. + * @constructor + */ +ImageEditor.Mode.Exposure = function() { + ImageEditor.Mode.ColorFilter.call(this, 'exposure', 'GALLERY_EXPOSURE'); +}; + +ImageEditor.Mode.Exposure.prototype = + {__proto__: ImageEditor.Mode.ColorFilter.prototype}; + +/** + * TODO(JSDOC) + * @param {ImageEditor.Toolbar} toolbar The toolbar to populate. + */ +ImageEditor.Mode.Exposure.prototype.createTools = function(toolbar) { + toolbar.addRange('brightness', 'GALLERY_BRIGHTNESS', -1, 0, 1, 100); + toolbar.addRange('contrast', 'GALLERY_CONTRAST', -1, 0, 1, 100); +}; + +/** + * Autofix. + * @constructor + */ +ImageEditor.Mode.Autofix = function() { + ImageEditor.Mode.ColorFilter.call(this, 'autofix', 'GALLERY_AUTOFIX'); + this.doneMessage_ = 'GALLERY_FIXED'; +}; + +ImageEditor.Mode.Autofix.prototype = + {__proto__: ImageEditor.Mode.ColorFilter.prototype}; + +/** + * TODO(JSDOC) + * @param {ImageEditor.Toolbar} toolbar The toolbar to populate. + */ +ImageEditor.Mode.Autofix.prototype.createTools = function(toolbar) { + var self = this; + toolbar.addButton('Apply', this.apply.bind(this)); +}; + +/** + * TODO(JSDOC) + * @return {boolean} // TODO(JSDOC). + */ +ImageEditor.Mode.Autofix.prototype.isApplicable = function() { + return this.getImageView().hasValidImage() && + filter.autofix.isApplicable(this.getHistogram()); +}; + +/** + * TODO(JSDOC) + */ +ImageEditor.Mode.Autofix.prototype.apply = function() { + this.update({histogram: this.getHistogram()}); +}; + +/** + * Instant Autofix. + * @constructor + */ +ImageEditor.Mode.InstantAutofix = function() { + ImageEditor.Mode.Autofix.apply(this, arguments); + this.instant = true; +}; + +ImageEditor.Mode.InstantAutofix.prototype = + {__proto__: ImageEditor.Mode.Autofix.prototype}; + +/** + * TODO(JSDOC) + */ +ImageEditor.Mode.InstantAutofix.prototype.setUp = function() { + ImageEditor.Mode.Autofix.prototype.setUp.apply(this, arguments); + this.apply(); +}; + +/** + * Blur filter. + * @constructor + */ +ImageEditor.Mode.Blur = function() { + ImageEditor.Mode.Adjust.call(this, 'blur'); +}; + +ImageEditor.Mode.Blur.prototype = + {__proto__: ImageEditor.Mode.Adjust.prototype}; + +/** + * TODO(JSDOC) + * @param {ImageEditor.Toolbar} toolbar The toolbar to populate. + */ +ImageEditor.Mode.Blur.prototype.createTools = function(toolbar) { + toolbar.addRange('strength', 'GALLERY_STRENGTH', 0, 0, 1, 100); + toolbar.addRange('radius', 'GALLERY_RADIUS', 1, 1, 3); +}; + +/** + * Sharpen filter. + * @constructor + */ +ImageEditor.Mode.Sharpen = function() { + ImageEditor.Mode.Adjust.call(this, 'sharpen'); +}; + +ImageEditor.Mode.Sharpen.prototype = + {__proto__: ImageEditor.Mode.Adjust.prototype}; + +/** + * TODO(JSDOC) + * @param {ImageEditor.Toolbar} toolbar The toolbar to populate. + */ +ImageEditor.Mode.Sharpen.prototype.createTools = function(toolbar) { + toolbar.addRange('strength', 'GALLERY_STRENGTH', 0, 0, 1, 100); + toolbar.addRange('radius', 'GALLERY_RADIUS', 1, 1, 3); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_buffer.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_buffer.js new file mode 100644 index 00000000000..8e894a8a0f2 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_buffer.js @@ -0,0 +1,184 @@ +// 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'; + +/** + * A stack of overlays that display itself and handle mouse events. + * TODO(kaznacheev) Consider disbanding this class and moving + * the functionality to individual objects that display anything or handle + * mouse events. + * @constructor + */ +function ImageBuffer() { + this.overlays_ = []; +} + +/** + * TODO(JSDOC). + * @param {ImageBuffer.Overlay} overlay // TODO(JSDOC). + */ +ImageBuffer.prototype.addOverlay = function(overlay) { + var zIndex = overlay.getZIndex(); + // Store the overlays in the ascending Z-order. + var i; + for (i = 0; i != this.overlays_.length; i++) { + if (zIndex < this.overlays_[i].getZIndex()) break; + } + this.overlays_.splice(i, 0, overlay); +}; + +/** + * TODO(JSDOC). + * @param {ImageBuffer.Overlay} overlay // TODO(JSDOC). + */ +ImageBuffer.prototype.removeOverlay = function(overlay) { + for (var i = 0; i != this.overlays_.length; i++) { + if (this.overlays_[i] == overlay) { + this.overlays_.splice(i, 1); + return; + } + } + throw new Error('Cannot remove overlay ' + overlay); +}; + +/** + * Draws overlays in the ascending Z-order. + */ +ImageBuffer.prototype.draw = function() { + for (var i = 0; i != this.overlays_.length; i++) { + this.overlays_[i].draw(); + } +}; + +/** + * Searches for a cursor style in the descending Z-order. + * @param {number} x X coordinate for cursor. + * @param {number} y Y coordinate for cursor. + * @param {boolean} mouseDown If mouse button is down. + * @return {string} A value for style.cursor CSS property. + */ +ImageBuffer.prototype.getCursorStyle = function(x, y, mouseDown) { + for (var i = this.overlays_.length - 1; i >= 0; i--) { + var style = this.overlays_[i].getCursorStyle(x, y, mouseDown); + if (style) return style; + } + return 'default'; +}; + +/** + * Searches for a click handler in the descending Z-order. + * @param {number} x X coordinate for click event. + * @param {number} y Y coordinate for click event. + * @return {boolean} True if handled. + */ +ImageBuffer.prototype.onClick = function(x, y) { + for (var i = this.overlays_.length - 1; i >= 0; i--) { + if (this.overlays_[i].onClick(x, y)) return true; + } + return false; +}; + +/** + * Searches for a drag handler in the descending Z-order. + * @param {number} x Event X coordinate. + * @param {number} y Event Y coordinate. + * @param {boolean} touch True if it's a touch event, false if mouse. + * @return {function(number,number)} A function to be called on mouse drag. + */ +ImageBuffer.prototype.getDragHandler = function(x, y, touch) { + for (var i = this.overlays_.length - 1; i >= 0; i--) { + var handler = this.overlays_[i].getDragHandler(x, y, touch); + if (handler) + return handler; + } + return null; +}; + +/** + * Searches for an action for the double tap enumerating + * layers in the descending Z-order. + * @param {number} x X coordinate of the event. + * @param {number} y Y coordinate of the event. + * @return {ImageBuffer.DoubleTapAction} Action to perform as result. + */ +ImageBuffer.prototype.getDoubleTapAction = function(x, y) { + for (var i = this.overlays_.length - 1; i >= 0; i--) { + var action = this.overlays_[i].getDoubleTapAction(x, y); + if (action != ImageBuffer.DoubleTapAction.NOTHING) + return action; + } + return ImageBuffer.DoubleTapAction.NOTHING; +}; + +/** + * Possible double tap actions. + * @enum + */ +ImageBuffer.DoubleTapAction = { + NOTHING: 0, + COMMIT: 1, + CANCEL: 2 +}; + +/** + * ImageBuffer.Overlay is a pluggable extension that modifies the outlook + * and the behavior of the ImageBuffer instance. + * @class + */ +ImageBuffer.Overlay = function() {}; + +/** + * TODO(JSDOC). + * @return {number} // TODO(JSDOC). + */ +ImageBuffer.Overlay.prototype.getZIndex = function() { return 0 }; + +/** + * TODO(JSDOC). + */ +ImageBuffer.Overlay.prototype.draw = function() {}; + +/** + * TODO(JSDOC). + * @param {number} x X coordinate for cursor. + * @param {number} y Y coordinate for cursor. + * @param {boolean} mouseDown If mouse button is down. + * @return {?string} A value for style.cursor CSS property or null for + * default. + */ +ImageBuffer.Overlay.prototype.getCursorStyle = function(x, y, mouseDown) { + return null; +}; + +/** + * TODO(JSDOC). + * @param {number} x // TODO(JSDOC). + * @param {number} y // TODO(JSDOC). + * @return {boolean} // TODO(JSDOC). + */ +ImageBuffer.Overlay.prototype.onClick = function(x, y) { + return false; +}; + +/** + * TODO(JSDOC). + * @param {number} x Event X coordinate. + * @param {number} y Event Y coordinate. + * @param {boolean} touch True if it's a touch event, false if mouse. + * @return {function(number,number)} A function to be called on mouse drag. + */ +ImageBuffer.Overlay.prototype.getDragHandler = function(x, y, touch) { + return null; +}; + +/** + * TODO(JSDOC). + * @param {number} x // TODO(JSDOC). + * @param {number} y // TODO(JSDOC). + * @return {ImageBuffer.DoubleTapAction} // TODO(JSDOC). + */ +ImageBuffer.Overlay.prototype.getDoubleTapAction = function(x, y) { + return ImageBuffer.DoubleTapAction.NOTHING; +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_editor.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_editor.js new file mode 100644 index 00000000000..090155494bd --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_editor.js @@ -0,0 +1,1177 @@ +// 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'; + +/** + * ImageEditor is the top level object that holds together and connects + * everything needed for image editing. + * + * @param {Viewport} viewport The viewport. + * @param {ImageView} imageView The ImageView containing the images to edit. + * @param {ImageEditor.Prompt} prompt Prompt instance. + * @param {Object} DOMContainers Various DOM containers required for the editor. + * @param {Array.<ImageEditor.Mode>} modes Available editor modes. + * @param {function} displayStringFunction String formatting function. + * @param {function()} onToolsVisibilityChanged Callback to be called, when + * some of the UI elements have been dimmed or revealed. + * @constructor + */ +function ImageEditor( + viewport, imageView, prompt, DOMContainers, modes, displayStringFunction, + onToolsVisibilityChanged) { + this.rootContainer_ = DOMContainers.root; + this.container_ = DOMContainers.image; + this.modes_ = modes; + this.displayStringFunction_ = displayStringFunction; + this.onToolsVisibilityChanged_ = onToolsVisibilityChanged; + + ImageUtil.removeChildren(this.container_); + + var document = this.container_.ownerDocument; + + this.viewport_ = viewport; + this.viewport_.sizeByFrame(this.container_); + + this.buffer_ = new ImageBuffer(); + this.viewport_.addRepaintCallback(this.buffer_.draw.bind(this.buffer_)); + + this.imageView_ = imageView; + this.imageView_.addContentCallback(this.onContentUpdate_.bind(this)); + this.buffer_.addOverlay(this.imageView_); + + this.panControl_ = new ImageEditor.MouseControl( + this.rootContainer_, this.container_, this.getBuffer()); + + this.panControl_.setDoubleTapCallback(this.onDoubleTap_.bind(this)); + + this.mainToolbar_ = new ImageEditor.Toolbar( + DOMContainers.toolbar, displayStringFunction); + + this.modeToolbar_ = new ImageEditor.Toolbar( + DOMContainers.mode, displayStringFunction, + this.onOptionsChange.bind(this)); + + this.prompt_ = prompt; + + this.createToolButtons(); + + this.commandQueue_ = null; +} + +/** + * @return {boolean} True if no user commands are to be accepted. + */ +ImageEditor.prototype.isLocked = function() { + return !this.commandQueue_ || this.commandQueue_.isBusy(); +}; + +/** + * @return {boolean} True if the command queue is busy. + */ +ImageEditor.prototype.isBusy = function() { + return this.commandQueue_ && this.commandQueue_.isBusy(); +}; + +/** + * Reflect the locked state of the editor in the UI. + * @param {boolean} on True if locked. + */ +ImageEditor.prototype.lockUI = function(on) { + ImageUtil.setAttribute(this.rootContainer_, 'locked', on); +}; + +/** + * Report the tool use to the metrics subsystem. + * @param {string} name Action name. + */ +ImageEditor.prototype.recordToolUse = function(name) { + ImageUtil.metrics.recordEnum( + ImageUtil.getMetricName('Tool'), name, this.actionNames_); +}; + +/** + * Content update handler. + * @private + */ +ImageEditor.prototype.onContentUpdate_ = function() { + for (var i = 0; i != this.modes_.length; i++) { + var mode = this.modes_[i]; + ImageUtil.setAttribute(mode.button_, 'disabled', !mode.isApplicable()); + } +}; + +/** + * Open the editing session for a new image. + * + * @param {string} url Image url. + * @param {Object} metadata Metadata. + * @param {Object} effect Transition effect object. + * @param {function(function)} saveFunction Image save function. + * @param {function} displayCallback Display callback. + * @param {function} loadCallback Load callback. + */ +ImageEditor.prototype.openSession = function( + url, metadata, effect, saveFunction, displayCallback, loadCallback) { + if (this.commandQueue_) + throw new Error('Session not closed'); + + this.lockUI(true); + + var self = this; + this.imageView_.load( + url, metadata, effect, displayCallback, function(loadType, delay, error) { + self.lockUI(false); + self.commandQueue_ = new CommandQueue( + self.container_.ownerDocument, + self.imageView_.getCanvas(), + saveFunction); + self.commandQueue_.attachUI( + self.getImageView(), self.getPrompt(), self.lockUI.bind(self)); + self.updateUndoRedo(); + loadCallback(loadType, delay, error); + }); +}; + +/** + * Close the current image editing session. + * @param {function} callback Callback. + */ +ImageEditor.prototype.closeSession = function(callback) { + this.getPrompt().hide(); + if (this.imageView_.isLoading()) { + if (this.commandQueue_) { + console.warn('Inconsistent image editor state'); + this.commandQueue_ = null; + } + this.imageView_.cancelLoad(); + this.lockUI(false); + callback(); + return; + } + if (!this.commandQueue_) { + // Session is already closed. + callback(); + return; + } + + this.executeWhenReady(callback); + this.commandQueue_.close(); + this.commandQueue_ = null; +}; + +/** + * Commit the current operation and execute the action. + * + * @param {function} callback Callback. + */ +ImageEditor.prototype.executeWhenReady = function(callback) { + if (this.commandQueue_) { + this.leaveModeGently(); + this.commandQueue_.executeWhenReady(callback); + } else { + if (!this.imageView_.isLoading()) + console.warn('Inconsistent image editor state'); + callback(); + } +}; + +/** + * @return {boolean} True if undo queue is not empty. + */ +ImageEditor.prototype.canUndo = function() { + return this.commandQueue_ && this.commandQueue_.canUndo(); +}; + +/** + * Undo the recently executed command. + */ +ImageEditor.prototype.undo = function() { + if (this.isLocked()) return; + this.recordToolUse('undo'); + + // First undo click should dismiss the uncommitted modifications. + if (this.currentMode_ && this.currentMode_.isUpdated()) { + this.currentMode_.reset(); + return; + } + + this.getPrompt().hide(); + this.leaveMode(false); + this.commandQueue_.undo(); + this.updateUndoRedo(); +}; + +/** + * Redo the recently un-done command. + */ +ImageEditor.prototype.redo = function() { + if (this.isLocked()) return; + this.recordToolUse('redo'); + this.getPrompt().hide(); + this.leaveMode(false); + this.commandQueue_.redo(); + this.updateUndoRedo(); +}; + +/** + * Update Undo/Redo buttons state. + */ +ImageEditor.prototype.updateUndoRedo = function() { + var canUndo = this.commandQueue_ && this.commandQueue_.canUndo(); + var canRedo = this.commandQueue_ && this.commandQueue_.canRedo(); + ImageUtil.setAttribute(this.undoButton_, 'disabled', !canUndo); + this.redoButton_.hidden = !canRedo; +}; + +/** + * @return {HTMLCanvasElement} The current image canvas. + */ +ImageEditor.prototype.getCanvas = function() { + return this.getImageView().getCanvas(); +}; + +/** + * @return {ImageBuffer} ImageBuffer instance. + */ +ImageEditor.prototype.getBuffer = function() { return this.buffer_ }; + +/** + * @return {ImageView} ImageView instance. + */ +ImageEditor.prototype.getImageView = function() { return this.imageView_ }; + +/** + * @return {Viewport} Viewport instance. + */ +ImageEditor.prototype.getViewport = function() { return this.viewport_ }; + +/** + * @return {ImageEditor.Prompt} Prompt instance. + */ +ImageEditor.prototype.getPrompt = function() { return this.prompt_ }; + +/** + * Handle the toolbar controls update. + * @param {Object} options A map of options. + */ +ImageEditor.prototype.onOptionsChange = function(options) { + ImageUtil.trace.resetTimer('update'); + if (this.currentMode_) { + this.currentMode_.update(options); + } + ImageUtil.trace.reportTimer('update'); +}; + +/** + * ImageEditor.Mode represents a modal state dedicated to a specific operation. + * Inherits from ImageBuffer. Overlay to simplify the drawing of mode-specific + * tools. + * + * @param {string} name The mode name. + * @param {string} title The mode title. + * @constructor + */ + +ImageEditor.Mode = function(name, title) { + this.name = name; + this.title = title; + this.message_ = 'GALLERY_ENTER_WHEN_DONE'; +}; + +ImageEditor.Mode.prototype = {__proto__: ImageBuffer.Overlay.prototype }; + +/** + * @return {Viewport} Viewport instance. + */ +ImageEditor.Mode.prototype.getViewport = function() { return this.viewport_ }; + +/** + * @return {ImageView} ImageView instance. + */ +ImageEditor.Mode.prototype.getImageView = function() { return this.imageView_ }; + +/** + * @return {string} The mode-specific message to be displayed when entering. + */ +ImageEditor.Mode.prototype.getMessage = function() { return this.message_ }; + +/** + * @return {boolean} True if the mode is applicable in the current context. + */ +ImageEditor.Mode.prototype.isApplicable = function() { return true }; + +/** + * Called once after creating the mode button. + * + * @param {ImageEditor} editor The editor instance. + * @param {HTMLElement} button The mode button. + */ + +ImageEditor.Mode.prototype.bind = function(editor, button) { + this.editor_ = editor; + this.editor_.registerAction_(this.name); + this.button_ = button; + this.viewport_ = editor.getViewport(); + this.imageView_ = editor.getImageView(); +}; + +/** + * Called before entering the mode. + */ +ImageEditor.Mode.prototype.setUp = function() { + this.editor_.getBuffer().addOverlay(this); + this.updated_ = false; +}; + +/** + * Create mode-specific controls here. + * @param {ImageEditor.Toolbar} toolbar The toolbar to populate. + */ +ImageEditor.Mode.prototype.createTools = function(toolbar) {}; + +/** + * Called before exiting the mode. + */ +ImageEditor.Mode.prototype.cleanUpUI = function() { + this.editor_.getBuffer().removeOverlay(this); +}; + +/** + * Called after exiting the mode. + */ +ImageEditor.Mode.prototype.cleanUpCaches = function() {}; + +/** + * Called when any of the controls changed its value. + * @param {Object} options A map of options. + */ +ImageEditor.Mode.prototype.update = function(options) { + this.markUpdated(); +}; + +/** + * Mark the editor mode as updated. + */ +ImageEditor.Mode.prototype.markUpdated = function() { + this.updated_ = true; +}; + +/** + * @return {boolean} True if the mode controls changed. + */ +ImageEditor.Mode.prototype.isUpdated = function() { return this.updated_ }; + +/** + * Resets the mode to a clean state. + */ +ImageEditor.Mode.prototype.reset = function() { + this.editor_.modeToolbar_.reset(); + this.updated_ = false; +}; + +/** + * One-click editor tool, requires no interaction, just executes the command. + * + * @param {string} name The mode name. + * @param {string} title The mode title. + * @param {Command} command The command to execute on click. + * @constructor + */ +ImageEditor.Mode.OneClick = function(name, title, command) { + ImageEditor.Mode.call(this, name, title); + this.instant = true; + this.command_ = command; +}; + +ImageEditor.Mode.OneClick.prototype = {__proto__: ImageEditor.Mode.prototype}; + +/** + * @return {Command} command. + */ +ImageEditor.Mode.OneClick.prototype.getCommand = function() { + return this.command_; +}; + +/** + * Register the action name. Required for metrics reporting. + * @param {string} name Button name. + * @private + */ +ImageEditor.prototype.registerAction_ = function(name) { + this.actionNames_.push(name); +}; + +/** + * Populate the toolbar. + */ +ImageEditor.prototype.createToolButtons = function() { + this.mainToolbar_.clear(); + this.actionNames_ = []; + + var self = this; + function createButton(name, title, handler) { + return self.mainToolbar_.addButton(name, + title, + handler, + name /* opt_className */); + } + + for (var i = 0; i != this.modes_.length; i++) { + var mode = this.modes_[i]; + mode.bind(this, createButton(mode.name, + mode.title, + this.enterMode.bind(this, mode))); + } + + this.undoButton_ = createButton('undo', + 'GALLERY_UNDO', + this.undo.bind(this)); + this.registerAction_('undo'); + + this.redoButton_ = createButton('redo', + 'GALLERY_REDO', + this.redo.bind(this)); + this.registerAction_('redo'); +}; + +/** + * @return {ImageEditor.Mode} The current mode. + */ +ImageEditor.prototype.getMode = function() { return this.currentMode_ }; + +/** + * The user clicked on the mode button. + * + * @param {ImageEditor.Mode} mode The new mode. + */ +ImageEditor.prototype.enterMode = function(mode) { + if (this.isLocked()) return; + + if (this.currentMode_ == mode) { + // Currently active editor tool clicked, commit if modified. + this.leaveMode(this.currentMode_.updated_); + return; + } + + this.recordToolUse(mode.name); + + this.leaveModeGently(); + // The above call could have caused a commit which might have initiated + // an asynchronous command execution. Wait for it to complete, then proceed + // with the mode set up. + this.commandQueue_.executeWhenReady(this.setUpMode_.bind(this, mode)); +}; + +/** + * Set up the new editing mode. + * + * @param {ImageEditor.Mode} mode The mode. + * @private + */ +ImageEditor.prototype.setUpMode_ = function(mode) { + this.currentTool_ = mode.button_; + + ImageUtil.setAttribute(this.currentTool_, 'pressed', true); + + this.currentMode_ = mode; + this.currentMode_.setUp(); + + if (this.currentMode_.instant) { // Instant tool. + this.leaveMode(true); + return; + } + + this.getPrompt().show(this.currentMode_.getMessage()); + + this.modeToolbar_.clear(); + this.currentMode_.createTools(this.modeToolbar_); + this.modeToolbar_.show(true); +}; + +/** + * The user clicked on 'OK' or 'Cancel' or on a different mode button. + * @param {boolean} commit True if commit is required. + */ +ImageEditor.prototype.leaveMode = function(commit) { + if (!this.currentMode_) return; + + if (!this.currentMode_.instant) { + this.getPrompt().hide(); + } + + this.modeToolbar_.show(false); + + this.currentMode_.cleanUpUI(); + if (commit) { + var self = this; + var command = this.currentMode_.getCommand(); + if (command) { // Could be null if the user did not do anything. + this.commandQueue_.execute(command); + this.updateUndoRedo(); + } + } + this.currentMode_.cleanUpCaches(); + this.currentMode_ = null; + + ImageUtil.setAttribute(this.currentTool_, 'pressed', false); + this.currentTool_ = null; +}; + +/** + * Leave the mode, commit only if required by the current mode. + */ +ImageEditor.prototype.leaveModeGently = function() { + this.leaveMode(this.currentMode_ && + this.currentMode_.updated_ && + this.currentMode_.implicitCommit); +}; + +/** + * Enter the editor mode with the given name. + * + * @param {string} name Mode name. + * @private + */ +ImageEditor.prototype.enterModeByName_ = function(name) { + for (var i = 0; i != this.modes_.length; i++) { + var mode = this.modes_[i]; + if (mode.name == name) { + if (!mode.button_.hasAttribute('disabled')) + this.enterMode(mode); + return; + } + } + console.error('Mode "' + name + '" not found.'); +}; + +/** + * Key down handler. + * @param {Event} event The keydown event. + * @return {boolean} True if handled. + */ +ImageEditor.prototype.onKeyDown = function(event) { + switch (util.getKeyModifiers(event) + event.keyIdentifier) { + case 'U+001B': // Escape + case 'Enter': + if (this.getMode()) { + this.leaveMode(event.keyIdentifier == 'Enter'); + return true; + } + break; + + case 'Ctrl-U+005A': // Ctrl+Z + if (this.commandQueue_.canUndo()) { + this.undo(); + return true; + } + break; + + case 'Ctrl-U+0059': // Ctrl+Y + if (this.commandQueue_.canRedo()) { + this.redo(); + return true; + } + break; + + case 'U+0041': // 'a' + this.enterModeByName_('autofix'); + return true; + + case 'U+0042': // 'b' + this.enterModeByName_('exposure'); + return true; + + case 'U+0043': // 'c' + this.enterModeByName_('crop'); + return true; + + case 'U+004C': // 'l' + this.enterModeByName_('rotate_left'); + return true; + + case 'U+0052': // 'r' + this.enterModeByName_('rotate_right'); + return true; + } + return false; +}; + +/** + * Double tap handler. + * @param {number} x X coordinate of the event. + * @param {number} y Y coordinate of the event. + * @private + */ +ImageEditor.prototype.onDoubleTap_ = function(x, y) { + if (this.getMode()) { + var action = this.buffer_.getDoubleTapAction(x, y); + if (action == ImageBuffer.DoubleTapAction.COMMIT) + this.leaveMode(true); + else if (action == ImageBuffer.DoubleTapAction.CANCEL) + this.leaveMode(false); + } +}; + +/** + * Hide the tools that overlap the given rectangular frame. + * + * @param {Rect} frame Hide the tool that overlaps this rect. + * @param {Rect} transparent But do not hide the tool that is completely inside + * this rect. + */ +ImageEditor.prototype.hideOverlappingTools = function(frame, transparent) { + var tools = this.rootContainer_.ownerDocument.querySelectorAll('.dimmable'); + var changed = false; + for (var i = 0; i != tools.length; i++) { + var tool = tools[i]; + var toolRect = tool.getBoundingClientRect(); + var overlapping = + (frame && frame.intersects(toolRect)) && + !(transparent && transparent.contains(toolRect)); + if (overlapping && !tool.hasAttribute('dimmed') || + !overlapping && tool.hasAttribute('dimmed')) { + ImageUtil.setAttribute(tool, 'dimmed', overlapping); + changed = true; + } + } + if (changed) + this.onToolsVisibilityChanged_(); +}; + +/** + * A helper object for panning the ImageBuffer. + * + * @param {HTMLElement} rootContainer The top-level container. + * @param {HTMLElement} container The container for mouse events. + * @param {ImageBuffer} buffer Image buffer. + * @constructor + */ +ImageEditor.MouseControl = function(rootContainer, container, buffer) { + this.rootContainer_ = rootContainer; + this.container_ = container; + this.buffer_ = buffer; + + var handlers = { + 'touchstart': this.onTouchStart, + 'touchend': this.onTouchEnd, + 'touchcancel': this.onTouchCancel, + 'touchmove': this.onTouchMove, + 'mousedown': this.onMouseDown, + 'mouseup': this.onMouseUp + }; + + for (var eventName in handlers) { + container.addEventListener( + eventName, handlers[eventName].bind(this), false); + } + + // Mouse move handler has to be attached to the window to receive events + // from outside of the window. See: http://crbug.com/155705 + window.addEventListener('mousemove', this.onMouseMove.bind(this), false); +}; + +/** + * Maximum movement for touch to be detected as a tap (in pixels). + * @private + */ +ImageEditor.MouseControl.MAX_MOVEMENT_FOR_TAP_ = 8; + +/** + * Maximum time for touch to be detected as a tap (in milliseconds). + * @private + */ +ImageEditor.MouseControl.MAX_TAP_DURATION_ = 500; + +/** + * Maximum distance from the first tap to the second tap to be considered + * as a double tap. + * @private + */ +ImageEditor.MouseControl.MAX_DISTANCE_FOR_DOUBLE_TAP_ = 32; + +/** + * Maximum time for touch to be detected as a double tap (in milliseconds). + * @private + */ +ImageEditor.MouseControl.MAX_DOUBLE_TAP_DURATION_ = 1000; + +/** + * Returns an event's position. + * + * @param {MouseEvent|Touch} e Pointer position. + * @return {Object} A pair of x,y in page coordinates. + * @private + */ +ImageEditor.MouseControl.getPosition_ = function(e) { + return { + x: e.pageX, + y: e.pageY + }; +}; + +/** + * Returns touch position or null if there is more than one touch position. + * + * @param {TouchEvent} e Event. + * @return {object?} A pair of x,y in page coordinates. + * @private + */ +ImageEditor.MouseControl.prototype.getTouchPosition_ = function(e) { + if (e.targetTouches.length == 1) + return ImageEditor.MouseControl.getPosition_(e.targetTouches[0]); + else + return null; +}; + +/** + * Touch start handler. + * @param {TouchEvent} e Event. + */ +ImageEditor.MouseControl.prototype.onTouchStart = function(e) { + var position = this.getTouchPosition_(e); + if (position) { + this.touchStartInfo_ = { + x: position.x, + y: position.y, + time: Date.now() + }; + this.dragHandler_ = this.buffer_.getDragHandler(position.x, position.y, + true /* touch */); + this.dragHappened_ = false; + } +}; + +/** + * Touch end handler. + * @param {TouchEvent} e Event. + */ +ImageEditor.MouseControl.prototype.onTouchEnd = function(e) { + if (!this.dragHappened_ && Date.now() - this.touchStartInfo_.time <= + ImageEditor.MouseControl.MAX_TAP_DURATION_) { + this.buffer_.onClick(this.touchStartInfo_.x, this.touchStartInfo_.y); + if (this.previousTouchStartInfo_ && + Date.now() - this.previousTouchStartInfo_.time < + ImageEditor.MouseControl.MAX_DOUBLE_TAP_DURATION_) { + var prevTouchCircle = new Circle( + this.previousTouchStartInfo_.x, + this.previousTouchStartInfo_.y, + ImageEditor.MouseControl.MAX_DISTANCE_FOR_DOUBLE_TAP_); + if (prevTouchCircle.inside(this.touchStartInfo_.x, + this.touchStartInfo_.y)) { + this.doubleTapCallback_(this.touchStartInfo_.x, this.touchStartInfo_.y); + } + } + this.previousTouchStartInfo_ = this.touchStartInfo_; + } else { + this.previousTouchStartInfo_ = null; + } + this.onTouchCancel(e); +}; + +/** + * Default double tap handler. + * @param {number} x X coordinate of the event. + * @param {number} y Y coordinate of the event. + * @private + */ +ImageEditor.MouseControl.prototype.doubleTapCallback_ = function(x, y) {}; + +/** + * Sets callback to be called when double tap detected. + * @param {function(number, number)} callback New double tap callback. + */ +ImageEditor.MouseControl.prototype.setDoubleTapCallback = function(callback) { + this.doubleTapCallback_ = callback; +}; + +/** + * Touch chancel handler. + */ +ImageEditor.MouseControl.prototype.onTouchCancel = function() { + this.dragHandler_ = null; + this.dragHappened_ = false; + this.touchStartInfo_ = null; + this.lockMouse_(false); +}; + +/** + * Touch move handler. + * @param {TouchEvent} e Event. + */ +ImageEditor.MouseControl.prototype.onTouchMove = function(e) { + var position = this.getTouchPosition_(e); + if (!position) + return; + + if (this.touchStartInfo_ && !this.dragHappened_) { + var tapCircle = new Circle(this.touchStartInfo_.x, this.touchStartInfo_.y, + ImageEditor.MouseControl.MAX_MOVEMENT_FOR_TAP_); + this.dragHappened_ = !tapCircle.inside(position.x, position.y); + } + if (this.dragHandler_ && this.dragHappened_) { + this.dragHandler_(position.x, position.y); + this.lockMouse_(true); + } +}; + +/** + * Mouse down handler. + * @param {MouseEvent} e Event. + */ +ImageEditor.MouseControl.prototype.onMouseDown = function(e) { + var position = ImageEditor.MouseControl.getPosition_(e); + + this.dragHandler_ = this.buffer_.getDragHandler(position.x, position.y, + false /* mouse */); + this.dragHappened_ = false; + this.updateCursor_(position); +}; + +/** + * Mouse up handler. + * @param {MouseEvent} e Event. + */ +ImageEditor.MouseControl.prototype.onMouseUp = function(e) { + var position = ImageEditor.MouseControl.getPosition_(e); + + if (!this.dragHappened_) { + this.buffer_.onClick(position.x, position.y); + } + this.dragHandler_ = null; + this.dragHappened_ = false; + this.lockMouse_(false); +}; + +/** + * Mouse move handler. + * @param {MouseEvent} e Event. + */ +ImageEditor.MouseControl.prototype.onMouseMove = function(e) { + var position = ImageEditor.MouseControl.getPosition_(e); + + if (this.dragHandler_ && !e.which) { + // mouseup must have happened while the mouse was outside our window. + this.dragHandler_ = null; + this.lockMouse_(false); + } + + this.updateCursor_(position); + if (this.dragHandler_) { + this.dragHandler_(position.x, position.y); + this.dragHappened_ = true; + this.lockMouse_(true); + } +}; + +/** + * Update the UI to reflect mouse drag state. + * @param {boolean} on True if dragging. + * @private + */ +ImageEditor.MouseControl.prototype.lockMouse_ = function(on) { + ImageUtil.setAttribute(this.rootContainer_, 'mousedrag', on); +}; + +/** + * Update the cursor. + * + * @param {Object} position An object holding x and y properties. + * @private + */ +ImageEditor.MouseControl.prototype.updateCursor_ = function(position) { + var oldCursor = this.container_.getAttribute('cursor'); + var newCursor = this.buffer_.getCursorStyle( + position.x, position.y, !!this.dragHandler_); + if (newCursor != oldCursor) // Avoid flicker. + this.container_.setAttribute('cursor', newCursor); +}; + +/** + * A toolbar for the ImageEditor. + * @param {HTMLElement} parent The parent element. + * @param {function} displayStringFunction A string formatting function. + * @param {function} updateCallback The callback called when controls change. + * @constructor + */ +ImageEditor.Toolbar = function(parent, displayStringFunction, updateCallback) { + this.wrapper_ = parent; + this.displayStringFunction_ = displayStringFunction; + this.updateCallback_ = updateCallback; +}; + +/** + * Clear the toolbar. + */ +ImageEditor.Toolbar.prototype.clear = function() { + ImageUtil.removeChildren(this.wrapper_); +}; + +/** + * Create a control. + * @param {string} tagName The element tag name. + * @return {HTMLElement} The created control element. + * @private + */ +ImageEditor.Toolbar.prototype.create_ = function(tagName) { + return this.wrapper_.ownerDocument.createElement(tagName); +}; + +/** + * Add a control. + * @param {HTMLElement} element The control to add. + * @return {HTMLElement} The added element. + */ +ImageEditor.Toolbar.prototype.add = function(element) { + this.wrapper_.appendChild(element); + return element; +}; + +/** + * Add a text label. + * @param {string} name Label name. + * @return {HTMLElement} The added label. + */ +ImageEditor.Toolbar.prototype.addLabel = function(name) { + var label = this.create_('span'); + label.textContent = this.displayStringFunction_(name); + return this.add(label); +}; + +/** + * Add a button. + * + * @param {string} name Button name. + * @param {string} title Button title. + * @param {function} handler onClick handler. + * @param {string=} opt_class Extra class name. + * @return {HTMLElement} The added button. + */ +ImageEditor.Toolbar.prototype.addButton = function( + name, title, handler, opt_class) { + var button = this.create_('button'); + if (opt_class) button.classList.add(opt_class); + var label = this.create_('span'); + label.textContent = this.displayStringFunction_(title); + button.appendChild(label); + button.label = this.displayStringFunction_(title); + button.addEventListener('click', handler, false); + return this.add(button); +}; + +/** + * Add a range control (scalar value picker). + * + * @param {string} name An option name. + * @param {string} title An option title. + * @param {number} min Min value of the option. + * @param {number} value Default value of the option. + * @param {number} max Max value of the options. + * @param {number} scale A number to multiply by when setting + * min/value/max in DOM. + * @param {boolean=} opt_showNumeric True if numeric value should be displayed. + * @return {HTMLElement} Range element. + */ +ImageEditor.Toolbar.prototype.addRange = function( + name, title, min, value, max, scale, opt_showNumeric) { + var self = this; + + scale = scale || 1; + + var range = this.create_('input'); + + range.className = 'range'; + range.type = 'range'; + range.name = name; + range.min = Math.ceil(min * scale); + range.max = Math.floor(max * scale); + + var numeric = this.create_('div'); + numeric.className = 'numeric'; + function mirror() { + numeric.textContent = Math.round(range.getValue() * scale) / scale; + } + + range.setValue = function(newValue) { + range.value = Math.round(newValue * scale); + mirror(); + }; + + range.getValue = function() { + return Number(range.value) / scale; + }; + + range.reset = function() { + range.setValue(value); + }; + + range.addEventListener('change', + function() { + mirror(); + self.updateCallback_(self.getOptions()); + }, + false); + + range.setValue(value); + + var label = this.create_('div'); + label.textContent = this.displayStringFunction_(title); + label.className = 'label ' + name; + this.add(label); + this.add(range); + + if (opt_showNumeric) + this.add(numeric); + + // Swallow the left and right keys, so they are not handled by other + // listeners. + range.addEventListener('keydown', function(e) { + if (e.keyIdentifier === 'Left' || e.keyIdentifier === 'Right') + e.stopPropagation(); + }); + + return range; +}; + +/** + * @return {Object} options A map of options. + */ +ImageEditor.Toolbar.prototype.getOptions = function() { + var values = {}; + for (var child = this.wrapper_.firstChild; child; child = child.nextSibling) { + if (child.name) + values[child.name] = child.getValue(); + } + return values; +}; + +/** + * Reset the toolbar. + */ +ImageEditor.Toolbar.prototype.reset = function() { + for (var child = this.wrapper_.firstChild; child; child = child.nextSibling) { + if (child.reset) child.reset(); + } +}; + +/** + * Show/hide the toolbar. + * @param {boolean} on True if show. + */ +ImageEditor.Toolbar.prototype.show = function(on) { + if (!this.wrapper_.firstChild) + return; // Do not show empty toolbar; + + this.wrapper_.hidden = !on; +}; + +/** A prompt panel for the editor. + * + * @param {HTMLElement} container Container element. + * @param {function} displayStringFunction A formatting function. + * @constructor + */ +ImageEditor.Prompt = function(container, displayStringFunction) { + this.container_ = container; + this.displayStringFunction_ = displayStringFunction; +}; + +/** + * Reset the prompt. + */ +ImageEditor.Prompt.prototype.reset = function() { + this.cancelTimer(); + if (this.wrapper_) { + this.container_.removeChild(this.wrapper_); + this.wrapper_ = null; + this.prompt_ = null; + } +}; + +/** + * Cancel the delayed action. + */ +ImageEditor.Prompt.prototype.cancelTimer = function() { + if (this.timer_) { + clearTimeout(this.timer_); + this.timer_ = null; + } +}; + +/** + * Schedule the delayed action. + * @param {function} callback Callback. + * @param {number} timeout Timeout. + */ +ImageEditor.Prompt.prototype.setTimer = function(callback, timeout) { + this.cancelTimer(); + var self = this; + this.timer_ = setTimeout(function() { + self.timer_ = null; + callback(); + }, timeout); +}; + +/** + * Show the prompt. + * + * @param {string} text The prompt text. + * @param {number} timeout Timeout in ms. + * @param {Object} formatArgs varArgs for the formatting fuction. + */ +ImageEditor.Prompt.prototype.show = function(text, timeout, formatArgs) { + this.showAt.apply(this, + ['center'].concat(Array.prototype.slice.call(arguments))); +}; + +/** + * + * @param {string} pos The 'pos' attribute value. + * @param {string} text The prompt text. + * @param {number} timeout Timeout in ms. + * @param {Object} formatArgs varArgs for the formatting function. + */ +ImageEditor.Prompt.prototype.showAt = function(pos, text, timeout, formatArgs) { + this.reset(); + if (!text) return; + + var document = this.container_.ownerDocument; + this.wrapper_ = document.createElement('div'); + this.wrapper_.className = 'prompt-wrapper'; + this.wrapper_.setAttribute('pos', pos); + this.container_.appendChild(this.wrapper_); + + this.prompt_ = document.createElement('div'); + this.prompt_.className = 'prompt'; + + // Create an extra wrapper which opacity can be manipulated separately. + var tool = document.createElement('div'); + tool.className = 'dimmable'; + this.wrapper_.appendChild(tool); + tool.appendChild(this.prompt_); + + var args = [text].concat(Array.prototype.slice.call(arguments, 3)); + this.prompt_.textContent = this.displayStringFunction_.apply(null, args); + + var close = document.createElement('div'); + close.className = 'close'; + close.addEventListener('click', this.hide.bind(this)); + this.prompt_.appendChild(close); + + setTimeout( + this.prompt_.setAttribute.bind(this.prompt_, 'state', 'fadein'), 0); + + if (timeout) + this.setTimer(this.hide.bind(this), timeout); +}; + +/** + * Hide the prompt. + */ +ImageEditor.Prompt.prototype.hide = function() { + if (!this.prompt_) return; + this.prompt_.setAttribute('state', 'fadeout'); + // Allow some time for the animation to play out. + this.setTimer(this.reset.bind(this), 500); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_encoder.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_encoder.js new file mode 100644 index 00000000000..1c96b1fe326 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_encoder.js @@ -0,0 +1,228 @@ +// 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'; + +/** + * A namespace class for image encoding functions. All methods are static. + */ +function ImageEncoder() {} + +/** + * @type {Array.<Object>} + */ +ImageEncoder.metadataEncoders = {}; + +/** + * @param {function(new:ImageEncoder.MetadataEncoder)} constructor + * // TODO(JSDOC). + * @param {string} mimeType // TODO(JSDOC). + */ +ImageEncoder.registerMetadataEncoder = function(constructor, mimeType) { + ImageEncoder.metadataEncoders[mimeType] = constructor; +}; + +/** + * Create a metadata encoder. + * + * The encoder will own and modify a copy of the original metadata. + * + * @param {Object} metadata Original metadata. + * @return {ImageEncoder.MetadataEncoder} Created metadata encoder. + */ +ImageEncoder.createMetadataEncoder = function(metadata) { + var constructor = ImageEncoder.metadataEncoders[metadata.mimeType] || + ImageEncoder.MetadataEncoder; + return new constructor(metadata); +}; + + +/** + * Create a metadata encoder object holding a copy of metadata + * modified according to the properties of the supplied image. + * + * @param {Object} metadata Original metadata. + * @param {HTMLCanvasElement} canvas Canvas to use for metadata. + * @param {number} quality Encoding quality (defaults to 1). + * @return {ImageEncoder.MetadataEncoder} Encoder with encoded metadata. + */ +ImageEncoder.encodeMetadata = function(metadata, canvas, quality) { + var encoder = ImageEncoder.createMetadataEncoder(metadata); + encoder.setImageData(canvas); + encoder.setThumbnailData(ImageEncoder.createThumbnail(canvas), quality || 1); + return encoder; +}; + + +/** + * Return a blob with the encoded image with metadata inserted. + * @param {HTMLCanvasElement} canvas The canvas with the image to be encoded. + * @param {ImageEncoder.MetadataEncoder} metadataEncoder Encoder to use. + * @param {number} quality (0..1], Encoding quality, defaults to 0.9. + * @return {Blob} encoded data. + */ +ImageEncoder.getBlob = function(canvas, metadataEncoder, quality) { + // Contrary to what one might think 1.0 is not a good default. Opening and + // saving an typical photo taken with consumer camera increases its file size + // by 50-100%. + // Experiments show that 0.9 is much better. It shrinks some photos a bit, + // keeps others about the same size, but does not visibly lower the quality. + quality = quality || 0.9; + + ImageUtil.trace.resetTimer('dataurl'); + // WebKit does not support canvas.toBlob yet so canvas.toDataURL is + // the only way to use the Chrome built-in image encoder. + var dataURL = + canvas.toDataURL(metadataEncoder.getMetadata().mimeType, quality); + ImageUtil.trace.reportTimer('dataurl'); + + var encodedImage = ImageEncoder.decodeDataURL(dataURL); + + var encodedMetadata = metadataEncoder.encode(); + + var slices = []; + + // TODO(kaznacheev): refactor |stringToArrayBuffer| and |encode| to return + // arrays instead of array buffers. + function appendSlice(arrayBuffer) { + slices.push(new DataView(arrayBuffer)); + } + + ImageUtil.trace.resetTimer('blob'); + if (encodedMetadata.byteLength != 0) { + var metadataRange = metadataEncoder.findInsertionRange(encodedImage); + appendSlice(ImageEncoder.stringToArrayBuffer( + encodedImage, 0, metadataRange.from)); + + appendSlice(metadataEncoder.encode()); + + appendSlice(ImageEncoder.stringToArrayBuffer( + encodedImage, metadataRange.to, encodedImage.length)); + } else { + appendSlice(ImageEncoder.stringToArrayBuffer( + encodedImage, 0, encodedImage.length)); + } + var blob = new Blob(slices, {type: metadataEncoder.getMetadata().mimeType}); + ImageUtil.trace.reportTimer('blob'); + return blob; +}; + +/** + * Decode a dataURL into a binary string containing the encoded image. + * + * Why return a string? Calling atob and having the rest of the code deal + * with a string is several times faster than decoding base64 in Javascript. + * + * @param {string} dataURL Data URL to decode. + * @return {string} A binary string (char codes are the actual byte values). + */ +ImageEncoder.decodeDataURL = function(dataURL) { + // Skip the prefix ('data:image/<type>;base64,') + var base64string = dataURL.substring(dataURL.indexOf(',') + 1); + return atob(base64string); +}; + +/** + * Return a thumbnail for an image. + * @param {HTMLCanvasElement} canvas Original image. + * @param {number=} opt_shrinkage Thumbnail should be at least this much smaller + * than the original image (in each dimension). + * @return {HTMLCanvasElement} Thumbnail canvas. + */ +ImageEncoder.createThumbnail = function(canvas, opt_shrinkage) { + var MAX_THUMBNAIL_DIMENSION = 320; + + opt_shrinkage = Math.max(opt_shrinkage || 4, + canvas.width / MAX_THUMBNAIL_DIMENSION, + canvas.height / MAX_THUMBNAIL_DIMENSION); + + var thumbnailCanvas = canvas.ownerDocument.createElement('canvas'); + thumbnailCanvas.width = Math.round(canvas.width / opt_shrinkage); + thumbnailCanvas.height = Math.round(canvas.height / opt_shrinkage); + + var context = thumbnailCanvas.getContext('2d'); + context.drawImage(canvas, + 0, 0, canvas.width, canvas.height, + 0, 0, thumbnailCanvas.width, thumbnailCanvas.height); + + return thumbnailCanvas; +}; + +/** + * TODO(JSDOC) + * @param {string} string // TODO(JSDOC). + * @param {number} from // TODO(JSDOC). + * @param {number} to // TODO(JSDOC). + * @return {ArrayBuffer} // TODO(JSDOC). + */ +ImageEncoder.stringToArrayBuffer = function(string, from, to) { + var size = to - from; + var array = new Uint8Array(size); + for (var i = 0; i != size; i++) { + array[i] = string.charCodeAt(from + i); + } + return array.buffer; +}; + +/** + * A base class for a metadata encoder. + * + * Serves as a default metadata encoder for images that none of the metadata + * parsers recognized. + * + * @param {Object} original_metadata Starting metadata. + * @constructor + */ +ImageEncoder.MetadataEncoder = function(original_metadata) { + this.metadata_ = MetadataCache.cloneMetadata(original_metadata) || {}; + if (this.metadata_.mimeType != 'image/jpeg') { + // Chrome can only encode JPEG and PNG. Force PNG mime type so that we + // can save to file and generate a thumbnail. + this.metadata_.mimeType = 'image/png'; + } +}; + +/** + * TODO(JSDOC) + * @return {Object} // TODO(JSDOC). + */ +ImageEncoder.MetadataEncoder.prototype.getMetadata = function() { + return this.metadata_; +}; + +/** + * @param {HTMLCanvasElement|Object} canvas Canvas or or anything with + * width and height properties. + */ +ImageEncoder.MetadataEncoder.prototype.setImageData = function(canvas) { + this.metadata_.width = canvas.width; + this.metadata_.height = canvas.height; +}; + +/** + * @param {HTMLCanvasElement} canvas Canvas to use as thumbnail. + * @param {number} quality Thumbnail quality. + */ +ImageEncoder.MetadataEncoder.prototype.setThumbnailData = + function(canvas, quality) { + this.metadata_.thumbnailURL = + canvas.toDataURL(this.metadata_.mimeType, quality); +}; + +/** + * Return a range where the metadata is (or should be) located. + * @param {string} encodedImage // TODO(JSDOC). + * @return {Object} An object with from and to properties. + */ +ImageEncoder.MetadataEncoder.prototype. + findInsertionRange = function(encodedImage) { return {from: 0, to: 0} }; + +/** + * Return serialized metadata ready to write to an image file. + * The return type is optimized for passing to Blob.append. + * @return {ArrayBuffer} // TODO(JSDOC). + */ +ImageEncoder.MetadataEncoder.prototype.encode = function() { + return new Uint8Array(0).buffer; +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_transform.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_transform.js new file mode 100644 index 00000000000..6b194baf0fc --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_transform.js @@ -0,0 +1,493 @@ +// 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'; + +/** + * Crop mode. + * @constructor + */ +ImageEditor.Mode.Crop = function() { + ImageEditor.Mode.call(this, 'crop', 'GALLERY_CROP'); +}; + +ImageEditor.Mode.Crop.prototype = {__proto__: ImageEditor.Mode.prototype}; + +/** + * TODO(JSDOC). + */ +ImageEditor.Mode.Crop.prototype.setUp = function() { + ImageEditor.Mode.prototype.setUp.apply(this, arguments); + + var container = this.getImageView().container_; + var doc = container.ownerDocument; + + this.domOverlay_ = doc.createElement('div'); + this.domOverlay_.className = 'crop-overlay'; + container.appendChild(this.domOverlay_); + + this.shadowTop_ = doc.createElement('div'); + this.shadowTop_.className = 'shadow'; + this.domOverlay_.appendChild(this.shadowTop_); + + this.middleBox_ = doc.createElement('div'); + this.middleBox_.className = 'middle-box'; + this.domOverlay_.appendChild(this.middleBox_); + + this.shadowLeft_ = doc.createElement('div'); + this.shadowLeft_.className = 'shadow'; + this.middleBox_.appendChild(this.shadowLeft_); + + this.cropFrame_ = doc.createElement('div'); + this.cropFrame_.className = 'crop-frame'; + this.middleBox_.appendChild(this.cropFrame_); + + this.shadowRight_ = doc.createElement('div'); + this.shadowRight_.className = 'shadow'; + this.middleBox_.appendChild(this.shadowRight_); + + this.shadowBottom_ = doc.createElement('div'); + this.shadowBottom_.className = 'shadow'; + this.domOverlay_.appendChild(this.shadowBottom_); + + var cropFrame = this.cropFrame_; + function addCropFrame(className) { + var div = doc.createElement('div'); + div.className = className; + cropFrame.appendChild(div); + } + + addCropFrame('left top corner'); + addCropFrame('top horizontal'); + addCropFrame('right top corner'); + addCropFrame('left vertical'); + addCropFrame('right vertical'); + addCropFrame('left bottom corner'); + addCropFrame('bottom horizontal'); + addCropFrame('right bottom corner'); + + this.onResizedBound_ = this.onResized_.bind(this); + window.addEventListener('resize', this.onResizedBound_); + + this.createDefaultCrop(); +}; + +/** + * Handles resizing of the window and updates the crop rectangle. + * @private + */ +ImageEditor.Mode.Crop.prototype.onResized_ = function() { + this.positionDOM(); +}; + +/** + * TODO(JSDOC). + */ +ImageEditor.Mode.Crop.prototype.reset = function() { + ImageEditor.Mode.prototype.reset.call(this); + this.createDefaultCrop(); +}; + +/** + * TODO(JSDOC). + */ +ImageEditor.Mode.Crop.prototype.positionDOM = function() { + var screenClipped = this.viewport_.getScreenClipped(); + + var screenCrop = this.viewport_.imageToScreenRect(this.cropRect_.getRect()); + var delta = ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS; + this.editor_.hideOverlappingTools( + screenCrop.inflate(delta, delta), + screenCrop.inflate(-delta, -delta)); + + this.domOverlay_.style.left = screenClipped.left + 'px'; + this.domOverlay_.style.top = screenClipped.top + 'px'; + this.domOverlay_.style.width = screenClipped.width + 'px'; + this.domOverlay_.style.height = screenClipped.height + 'px'; + + this.shadowLeft_.style.width = screenCrop.left - screenClipped.left + 'px'; + + this.shadowTop_.style.height = screenCrop.top - screenClipped.top + 'px'; + + this.shadowRight_.style.width = screenClipped.left + screenClipped.width - + (screenCrop.left + screenCrop.width) + 'px'; + + this.shadowBottom_.style.height = screenClipped.top + screenClipped.height - + (screenCrop.top + screenCrop.height) + 'px'; +}; + +/** + * TODO(JSDOC). + */ +ImageEditor.Mode.Crop.prototype.cleanUpUI = function() { + ImageEditor.Mode.prototype.cleanUpUI.apply(this, arguments); + this.domOverlay_.parentNode.removeChild(this.domOverlay_); + this.domOverlay_ = null; + this.editor_.hideOverlappingTools(); + window.removeEventListener(this.onResizedBound_); + this.onResizedBound_ = null; +}; + +/** + * @const + * @type {number} + */ +ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS = 6; +/** + * @const + * @type {number} + */ +ImageEditor.Mode.Crop.TOUCH_GRAB_RADIUS = 20; + +/** + * TODO(JSDOC). + * @return {Command.Crop} // TODO(JSDOC). + */ +ImageEditor.Mode.Crop.prototype.getCommand = function() { + var cropImageRect = this.cropRect_.getRect(); + return new Command.Crop(cropImageRect); +}; + +/** + * TODO(JSDOC). + */ +ImageEditor.Mode.Crop.prototype.createDefaultCrop = function() { + var rect = new Rect(this.getViewport().getImageClipped()); + rect = rect.inflate( + -Math.round(rect.width / 6), -Math.round(rect.height / 6)); + this.cropRect_ = new DraggableRect(rect, this.getViewport()); + this.positionDOM(); +}; + +/** + * TODO(JSDOC). + * @param {number} x X coordinate for cursor. + * @param {number} y Y coordinate for cursor. + * @param {boolean} mouseDown If mouse button is down. + * @return {string} A value for style.cursor CSS property. + */ +ImageEditor.Mode.Crop.prototype.getCursorStyle = function(x, y, mouseDown) { + return this.cropRect_.getCursorStyle(x, y, mouseDown); +}; + +/** + * TODO(JSDOC). + * @param {number} x Event X coordinate. + * @param {number} y Event Y coordinate. + * @param {boolean} touch True if it's a touch event, false if mouse. + * @return {function(number,number)} A function to be called on mouse drag. + */ +ImageEditor.Mode.Crop.prototype.getDragHandler = function(x, y, touch) { + var cropDragHandler = this.cropRect_.getDragHandler(x, y, touch); + if (!cropDragHandler) return null; + + var self = this; + return function(x, y) { + cropDragHandler(x, y); + self.markUpdated(); + self.positionDOM(); + }; +}; + +/** + * TODO(JSDOC). + * @param {number} x X coordinate of the event. + * @param {number} y Y coordinate of the event. + * @return {ImageBuffer.DoubleTapAction} Action to perform as result. + */ +ImageEditor.Mode.Crop.prototype.getDoubleTapAction = function(x, y) { + return this.cropRect_.getDoubleTapAction(x, y); +}; + +/* + * A draggable rectangle over the image. + * @param {Rect} rect // TODO(JSDOC). + * @param {Viewport} viewport // TODO(JSDOC). + * @constructor + */ +function DraggableRect(rect, viewport) { + // The bounds are not held in a regular rectangle (with width/height). + // left/top/right/bottom held instead for convenience. + this.bounds_ = {}; + this.bounds_[DraggableRect.LEFT] = rect.left; + this.bounds_[DraggableRect.RIGHT] = rect.left + rect.width; + this.bounds_[DraggableRect.TOP] = rect.top; + this.bounds_[DraggableRect.BOTTOM] = rect.top + rect.height; + + this.viewport_ = viewport; + + this.oppositeSide_ = {}; + this.oppositeSide_[DraggableRect.LEFT] = DraggableRect.RIGHT; + this.oppositeSide_[DraggableRect.RIGHT] = DraggableRect.LEFT; + this.oppositeSide_[DraggableRect.TOP] = DraggableRect.BOTTOM; + this.oppositeSide_[DraggableRect.BOTTOM] = DraggableRect.TOP; + + // Translation table to form CSS-compatible cursor style. + this.cssSide_ = {}; + this.cssSide_[DraggableRect.LEFT] = 'w'; + this.cssSide_[DraggableRect.TOP] = 'n'; + this.cssSide_[DraggableRect.RIGHT] = 'e'; + this.cssSide_[DraggableRect.BOTTOM] = 's'; + this.cssSide_[DraggableRect.NONE] = ''; +} + +// Static members to simplify reflective access to the bounds. +/** + * @const + * @type {string} + */ +DraggableRect.LEFT = 'left'; +/** + * @const + * @type {string} + */ +DraggableRect.RIGHT = 'right'; +/** + * @const + * @type {string} + */ +DraggableRect.TOP = 'top'; +/** + * @const + * @type {string} + */ +DraggableRect.BOTTOM = 'bottom'; +/** + * @const + * @type {string} + */ +DraggableRect.NONE = 'none'; + +/** + * TODO(JSDOC) + * @return {number} // TODO(JSDOC). + */ +DraggableRect.prototype.getLeft = function() { + return this.bounds_[DraggableRect.LEFT]; +}; + +/** + * TODO(JSDOC) + * @return {number} // TODO(JSDOC). + */ +DraggableRect.prototype.getRight = function() { + return this.bounds_[DraggableRect.RIGHT]; +}; + +/** + * TODO(JSDOC) + * @return {number} // TODO(JSDOC). + */ +DraggableRect.prototype.getTop = function() { + return this.bounds_[DraggableRect.TOP]; +}; + +/** + * TODO(JSDOC) + * @return {number} // TODO(JSDOC). + */ +DraggableRect.prototype.getBottom = function() { + return this.bounds_[DraggableRect.BOTTOM]; +}; + +/** + * TODO(JSDOC) + * @return {Rect} // TODO(JSDOC). + */ +DraggableRect.prototype.getRect = function() { + return new Rect(this.bounds_); +}; + +/** + * TODO(JSDOC) + * @param {number} x X coordinate for cursor. + * @param {number} y Y coordinate for cursor. + * @param {boolean} touch // TODO(JSDOC). + * @return {Object} // TODO(JSDOC). + */ +DraggableRect.prototype.getDragMode = function(x, y, touch) { + var result = { + xSide: DraggableRect.NONE, + ySide: DraggableRect.NONE + }; + + var bounds = this.bounds_; + var R = this.viewport_.screenToImageSize( + touch ? ImageEditor.Mode.Crop.TOUCH_GRAB_RADIUS : + ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS); + + var circle = new Circle(x, y, R); + + var xBetween = ImageUtil.between(bounds.left, x, bounds.right); + var yBetween = ImageUtil.between(bounds.top, y, bounds.bottom); + + if (circle.inside(bounds.left, bounds.top)) { + result.xSide = DraggableRect.LEFT; + result.ySide = DraggableRect.TOP; + } else if (circle.inside(bounds.left, bounds.bottom)) { + result.xSide = DraggableRect.LEFT; + result.ySide = DraggableRect.BOTTOM; + } else if (circle.inside(bounds.right, bounds.top)) { + result.xSide = DraggableRect.RIGHT; + result.ySide = DraggableRect.TOP; + } else if (circle.inside(bounds.right, bounds.bottom)) { + result.xSide = DraggableRect.RIGHT; + result.ySide = DraggableRect.BOTTOM; + } else if (yBetween && Math.abs(x - bounds.left) <= R) { + result.xSide = DraggableRect.LEFT; + } else if (yBetween && Math.abs(x - bounds.right) <= R) { + result.xSide = DraggableRect.RIGHT; + } else if (xBetween && Math.abs(y - bounds.top) <= R) { + result.ySide = DraggableRect.TOP; + } else if (xBetween && Math.abs(y - bounds.bottom) <= R) { + result.ySide = DraggableRect.BOTTOM; + } else if (xBetween && yBetween) { + result.whole = true; + } else { + result.newcrop = true; + result.xSide = DraggableRect.RIGHT; + result.ySide = DraggableRect.BOTTOM; + } + + return result; +}; + +/** + * TODO(JSDOC) + * @param {number} x X coordinate for cursor. + * @param {number} y Y coordinate for cursor. + * @param {boolean} mouseDown If mouse button is down. + * @return {string} // TODO(JSDOC). + */ +DraggableRect.prototype.getCursorStyle = function(x, y, mouseDown) { + var mode; + if (mouseDown) { + mode = this.dragMode_; + } else { + mode = this.getDragMode( + this.viewport_.screenToImageX(x), this.viewport_.screenToImageY(y)); + } + if (mode.whole) return 'move'; + if (mode.newcrop) return 'crop'; + return this.cssSide_[mode.ySide] + this.cssSide_[mode.xSide] + '-resize'; +}; + +/** + * TODO(JSDOC) + * @param {number} x X coordinate for cursor. + * @param {number} y Y coordinate for cursor. + * @param {boolean} touch // TODO(JSDOC). + * @return {function(number,number)} // TODO(JSDOC). + */ +DraggableRect.prototype.getDragHandler = function(x, y, touch) { + x = this.viewport_.screenToImageX(x); + y = this.viewport_.screenToImageY(y); + + var clipRect = this.viewport_.getImageClipped(); + if (!clipRect.inside(x, y)) return null; + + this.dragMode_ = this.getDragMode(x, y, touch); + + var self = this; + + var mouseBiasX; + var mouseBiasY; + + var fixedWidth = 0; + var fixedHeight = 0; + + var resizeFuncX; + var resizeFuncY; + + if (this.dragMode_.whole) { + mouseBiasX = this.bounds_.left - x; + fixedWidth = this.bounds_.right - this.bounds_.left; + resizeFuncX = function(x) { + self.bounds_.left = x; + self.bounds_.right = self.bounds_.left + fixedWidth; + }; + mouseBiasY = this.bounds_.top - y; + fixedHeight = this.bounds_.bottom - this.bounds_.top; + resizeFuncY = function(y) { + self.bounds_.top = y; + self.bounds_.bottom = self.bounds_.top + fixedHeight; + }; + } else { + var checkNewCrop = function() { + if (self.dragMode_.newcrop) { + self.dragMode_.newcrop = false; + self.bounds_.left = self.bounds_.right = x; + self.bounds_.top = self.bounds_.bottom = y; + mouseBiasX = 0; + mouseBiasY = 0; + } + }; + + var flipSide = function(side) { + var opposite = self.oppositeSide_[side]; + var temp = self.bounds_[side]; + self.bounds_[side] = self.bounds_[opposite]; + self.bounds_[opposite] = temp; + return opposite; + }; + + if (this.dragMode_.xSide != DraggableRect.NONE) { + mouseBiasX = self.bounds_[this.dragMode_.xSide] - x; + resizeFuncX = function(x) { + checkNewCrop(); + self.bounds_[self.dragMode_.xSide] = x; + if (self.bounds_.left > self.bounds_.right) { + self.dragMode_.xSide = flipSide(self.dragMode_.xSide); + } + }; + } + if (this.dragMode_.ySide != DraggableRect.NONE) { + mouseBiasY = self.bounds_[this.dragMode_.ySide] - y; + resizeFuncY = function(y) { + checkNewCrop(); + self.bounds_[self.dragMode_.ySide] = y; + if (self.bounds_.top > self.bounds_.bottom) { + self.dragMode_.ySide = flipSide(self.dragMode_.ySide); + } + }; + } + } + + function convertX(x) { + return ImageUtil.clamp( + clipRect.left, + self.viewport_.screenToImageX(x) + mouseBiasX, + clipRect.left + clipRect.width - fixedWidth); + } + + function convertY(y) { + return ImageUtil.clamp( + clipRect.top, + self.viewport_.screenToImageY(y) + mouseBiasY, + clipRect.top + clipRect.height - fixedHeight); + } + + return function(x, y) { + if (resizeFuncX) resizeFuncX(convertX(x)); + if (resizeFuncY) resizeFuncY(convertY(y)); + }; +}; + +/** + * TODO(JSDOC) + * @param {number} x X coordinate for cursor. + * @param {number} y Y coordinate for cursor. + * @param {boolean} touch // TODO(JSDOC). + * @return {ImageBuffer.DoubleTapAction} // TODO(JSDOC). + */ +DraggableRect.prototype.getDoubleTapAction = function(x, y, touch) { + x = this.viewport_.screenToImageX(x); + y = this.viewport_.screenToImageY(y); + + var clipRect = this.viewport_.getImageClipped(); + if (clipRect.inside(x, y)) + return ImageBuffer.DoubleTapAction.COMMIT; + else + return ImageBuffer.DoubleTapAction.NOTHING; +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_util.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_util.js new file mode 100644 index 00000000000..f088f7c6c83 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_util.js @@ -0,0 +1,701 @@ +// 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'; + + +// Namespace object for the utilities. +function ImageUtil() {} + +/** + * Performance trace. + */ +ImageUtil.trace = (function() { + function PerformanceTrace() { + this.lines_ = {}; + this.timers_ = {}; + this.container_ = null; + } + + PerformanceTrace.prototype.bindToDOM = function(container) { + this.container_ = container; + }; + + PerformanceTrace.prototype.report = function(key, value) { + if (!(key in this.lines_)) { + if (this.container_) { + var div = this.lines_[key] = document.createElement('div'); + this.container_.appendChild(div); + } else { + this.lines_[key] = {}; + } + } + this.lines_[key].textContent = key + ': ' + value; + if (ImageUtil.trace.log) this.dumpLine(key); + }; + + PerformanceTrace.prototype.resetTimer = function(key) { + this.timers_[key] = Date.now(); + }; + + PerformanceTrace.prototype.reportTimer = function(key) { + this.report(key, (Date.now() - this.timers_[key]) + 'ms'); + }; + + PerformanceTrace.prototype.dump = function() { + for (var key in this.lines_) + this.dumpLine(key); + }; + + PerformanceTrace.prototype.dumpLine = function(key) { + console.log('trace.' + this.lines_[key].textContent); + }; + + return new PerformanceTrace(); +})(); + +/** + * @param {number} min Minimum value. + * @param {number} value Value to adjust. + * @param {number} max Maximum value. + * @return {number} The closest to the |value| number in span [min, max]. + */ +ImageUtil.clamp = function(min, value, max) { + return Math.max(min, Math.min(max, value)); +}; + +/** + * @param {number} min Minimum value. + * @param {number} value Value to check. + * @param {number} max Maximum value. + * @return {boolean} True if value is between. + */ +ImageUtil.between = function(min, value, max) { + return (value - min) * (value - max) <= 0; +}; + +/** + * Rectangle class. + */ + +/** + * Rectangle constructor takes 0, 1, 2 or 4 arguments. + * Supports following variants: + * new Rect(left, top, width, height) + * new Rect(width, height) + * new Rect(rect) // anything with left, top, width, height properties + * new Rect(bounds) // anything with left, top, right, bottom properties + * new Rect(canvas|image) // anything with width and height properties. + * new Rect() // empty rectangle. + * @constructor + */ +function Rect() { + switch (arguments.length) { + case 4: + this.left = arguments[0]; + this.top = arguments[1]; + this.width = arguments[2]; + this.height = arguments[3]; + return; + + case 2: + this.left = 0; + this.top = 0; + this.width = arguments[0]; + this.height = arguments[1]; + return; + + case 1: { + var source = arguments[0]; + if ('left' in source && 'top' in source) { + this.left = source.left; + this.top = source.top; + if ('right' in source && 'bottom' in source) { + this.width = source.right - source.left; + this.height = source.bottom - source.top; + return; + } + } else { + this.left = 0; + this.top = 0; + } + if ('width' in source && 'height' in source) { + this.width = source.width; + this.height = source.height; + return; + } + break; // Fall through to the error message. + } + + case 0: + this.left = 0; + this.top = 0; + this.width = 0; + this.height = 0; + return; + } + console.error('Invalid Rect constructor arguments:', + Array.apply(null, arguments)); +} + +/** + * @param {number} factor Factor to scale. + * @return {Rect} A rectangle with every dimension scaled. + */ +Rect.prototype.scale = function(factor) { + return new Rect( + this.left * factor, + this.top * factor, + this.width * factor, + this.height * factor); +}; + +/** + * @param {number} dx Difference in X. + * @param {number} dy Difference in Y. + * @return {Rect} A rectangle shifted by (dx,dy), same size. + */ +Rect.prototype.shift = function(dx, dy) { + return new Rect(this.left + dx, this.top + dy, this.width, this.height); +}; + +/** + * @param {number} x Coordinate of the left top corner. + * @param {number} y Coordinate of the left top corner. + * @return {Rect} A rectangle with left==x and top==y, same size. + */ +Rect.prototype.moveTo = function(x, y) { + return new Rect(x, y, this.width, this.height); +}; + +/** + * @param {number} dx Difference in X. + * @param {number} dy Difference in Y. + * @return {Rect} A rectangle inflated by (dx, dy), same center. + */ +Rect.prototype.inflate = function(dx, dy) { + return new Rect( + this.left - dx, this.top - dy, this.width + 2 * dx, this.height + 2 * dy); +}; + +/** + * @param {number} x Coordinate of the point. + * @param {number} y Coordinate of the point. + * @return {boolean} True if the point lies inside the rectangle. + */ +Rect.prototype.inside = function(x, y) { + return this.left <= x && x < this.left + this.width && + this.top <= y && y < this.top + this.height; +}; + +/** + * @param {Rect} rect Rectangle to check. + * @return {boolean} True if this rectangle intersects with the |rect|. + */ +Rect.prototype.intersects = function(rect) { + return (this.left + this.width) > rect.left && + (rect.left + rect.width) > this.left && + (this.top + this.height) > rect.top && + (rect.top + rect.height) > this.top; +}; + +/** + * @param {Rect} rect Rectangle to check. + * @return {boolean} True if this rectangle containing the |rect|. + */ +Rect.prototype.contains = function(rect) { + return (this.left <= rect.left) && + (rect.left + rect.width) <= (this.left + this.width) && + (this.top <= rect.top) && + (rect.top + rect.height) <= (this.top + this.height); +}; + +/** + * @return {boolean} True if rectangle is empty. + */ +Rect.prototype.isEmpty = function() { + return this.width === 0 || this.height === 0; +}; + +/** + * Clamp the rectangle to the bounds by moving it. + * Decrease the size only if necessary. + * @param {Rect} bounds Bounds. + * @return {Rect} Calculated rectangle. + */ +Rect.prototype.clamp = function(bounds) { + var rect = new Rect(this); + + if (rect.width > bounds.width) { + rect.left = bounds.left; + rect.width = bounds.width; + } else if (rect.left < bounds.left) { + rect.left = bounds.left; + } else if (rect.left + rect.width > + bounds.left + bounds.width) { + rect.left = bounds.left + bounds.width - rect.width; + } + + if (rect.height > bounds.height) { + rect.top = bounds.top; + rect.height = bounds.height; + } else if (rect.top < bounds.top) { + rect.top = bounds.top; + } else if (rect.top + rect.height > + bounds.top + bounds.height) { + rect.top = bounds.top + bounds.height - rect.height; + } + + return rect; +}; + +/** + * @return {string} String representation. + */ +Rect.prototype.toString = function() { + return '(' + this.left + ',' + this.top + '):' + + '(' + (this.left + this.width) + ',' + (this.top + this.height) + ')'; +}; +/* + * Useful shortcuts for drawing (static functions). + */ + +/** + * Draw the image in context with appropriate scaling. + * @param {CanvasRenderingContext2D} context Context to draw. + * @param {Image} image Image to draw. + * @param {Rect=} opt_dstRect Rectangle in the canvas (whole canvas by default). + * @param {Rect=} opt_srcRect Rectangle in the image (whole image by default). + */ +Rect.drawImage = function(context, image, opt_dstRect, opt_srcRect) { + opt_dstRect = opt_dstRect || new Rect(context.canvas); + opt_srcRect = opt_srcRect || new Rect(image); + if (opt_dstRect.isEmpty() || opt_srcRect.isEmpty()) + return; + context.drawImage(image, + opt_srcRect.left, opt_srcRect.top, opt_srcRect.width, opt_srcRect.height, + opt_dstRect.left, opt_dstRect.top, opt_dstRect.width, opt_dstRect.height); +}; + +/** + * Draw a box around the rectangle. + * @param {CanvasRenderingContext2D} context Context to draw. + * @param {Rect} rect Rectangle. + */ +Rect.outline = function(context, rect) { + context.strokeRect( + rect.left - 0.5, rect.top - 0.5, rect.width + 1, rect.height + 1); +}; + +/** + * Fill the rectangle. + * @param {CanvasRenderingContext2D} context Context to draw. + * @param {Rect} rect Rectangle. + */ +Rect.fill = function(context, rect) { + context.fillRect(rect.left, rect.top, rect.width, rect.height); +}; + +/** + * Fills the space between the two rectangles. + * @param {CanvasRenderingContext2D} context Context to draw. + * @param {Rect} inner Inner rectangle. + * @param {Rect} outer Outer rectangle. + */ +Rect.fillBetween = function(context, inner, outer) { + var innerRight = inner.left + inner.width; + var innerBottom = inner.top + inner.height; + var outerRight = outer.left + outer.width; + var outerBottom = outer.top + outer.height; + if (inner.top > outer.top) { + context.fillRect( + outer.left, outer.top, outer.width, inner.top - outer.top); + } + if (inner.left > outer.left) { + context.fillRect( + outer.left, inner.top, inner.left - outer.left, inner.height); + } + if (inner.width < outerRight) { + context.fillRect( + innerRight, inner.top, outerRight - innerRight, inner.height); + } + if (inner.height < outerBottom) { + context.fillRect( + outer.left, innerBottom, outer.width, outerBottom - innerBottom); + } +}; + +/** + * Circle class. + * @param {number} x X coordinate of circle center. + * @param {number} y Y coordinate of circle center. + * @param {number} r Radius. + * @constructor + */ +function Circle(x, y, r) { + this.x = x; + this.y = y; + this.squaredR = r * r; +} + +/** + * Check if the point is inside the circle. + * @param {number} x X coordinate of the point. + * @param {number} y Y coordinate of the point. + * @return {boolean} True if the point is inside. + */ +Circle.prototype.inside = function(x, y) { + x -= this.x; + y -= this.y; + return x * x + y * y <= this.squaredR; +}; + +/** + * Copy an image applying scaling and rotation. + * + * @param {HTMLCanvasElement} dst Destination. + * @param {HTMLCanvasElement|HTMLImageElement} src Source. + * @param {number} scaleX Y scale transformation. + * @param {number} scaleY X scale transformation. + * @param {number} angle (in radians). + */ +ImageUtil.drawImageTransformed = function(dst, src, scaleX, scaleY, angle) { + var context = dst.getContext('2d'); + context.save(); + context.translate(context.canvas.width / 2, context.canvas.height / 2); + context.rotate(angle); + context.scale(scaleX, scaleY); + context.drawImage(src, -src.width / 2, -src.height / 2); + context.restore(); +}; + +/** + * Adds or removes an attribute to/from an HTML element. + * @param {HTMLElement} element To be applied to. + * @param {string} attribute Name of attribute. + * @param {boolean} on True if add, false if remove. + */ +ImageUtil.setAttribute = function(element, attribute, on) { + if (on) + element.setAttribute(attribute, ''); + else + element.removeAttribute(attribute); +}; + +/** + * Adds or removes CSS class to/from an HTML element. + * @param {HTMLElement} element To be applied to. + * @param {string} className Name of CSS class. + * @param {boolean} on True if add, false if remove. + */ +ImageUtil.setClass = function(element, className, on) { + var cl = element.classList; + if (on) + cl.add(className); + else + cl.remove(className); +}; + +/** + * ImageLoader loads an image from a given Entry into a canvas in two steps: + * 1. Loads the image into an HTMLImageElement. + * 2. Copies pixels from HTMLImageElement to HTMLCanvasElement. This is done + * stripe-by-stripe to avoid freezing up the UI. The transform is taken into + * account. + * + * @param {HTMLDocument} document Owner document. + * @param {MetadataCache=} opt_metadataCache Metadata cache. Required for + * caching. If not passed, caching will be disabled. + * @constructor + */ +ImageUtil.ImageLoader = function(document, opt_metadataCache) { + this.document_ = document; + this.metadataCache_ = opt_metadataCache || null; + this.image_ = new Image(); + this.generation_ = 0; +}; + +/** + * Max size of image to be displayed (in pixels) + */ +ImageUtil.ImageLoader.IMAGE_SIZE_LIMIT = 25 * 1000 * 1000; + +/** + * @param {number} width Width of the image. + * @param {number} height Height of the image. + * @return {boolean} True if the image is too large to be loaded. + */ +ImageUtil.ImageLoader.isTooLarge = function(width, height) { + return width * height > ImageUtil.ImageLoader.IMAGE_SIZE_LIMIT; +}; + +/** + * Loads an image. + * TODO(mtomasz): Simplify, or even get rid of this class and merge with the + * ThumbnaiLoader class. + * + * @param {FileEntry} entry Image entry to be loaded. + * @param {function(function(object))} transformFetcher function to get + * the image transform (which we need for the image orientation). + * @param {function(HTMLCanvasElement, string=)} callback Callback to be + * called when loaded. The second optional argument is an error identifier. + * @param {number=} opt_delay Load delay in milliseconds, useful to let the + * animations play out before the computation heavy image loading starts. + */ +ImageUtil.ImageLoader.prototype.load = function( + entry, transformFetcher, callback, opt_delay) { + this.cancel(); + + this.entry_ = entry; + this.callback_ = callback; + + // The transform fetcher is not cancellable so we need a generation counter. + var generation = ++this.generation_; + var onTransform = function(image, transform) { + if (generation === this.generation_) { + this.convertImage_( + image, transform || { scaleX: 1, scaleY: 1, rotate90: 0}); + } + }; + + var onError = function(opt_error) { + this.image_.onerror = null; + this.image_.onload = null; + var tmpCallback = this.callback_; + this.callback_ = null; + var emptyCanvas = this.document_.createElement('canvas'); + emptyCanvas.width = 0; + emptyCanvas.height = 0; + tmpCallback(emptyCanvas, opt_error); + }.bind(this); + + var loadImage = function(opt_metadata) { + ImageUtil.metrics.startInterval(ImageUtil.getMetricName('LoadTime')); + this.timeout_ = null; + + this.image_.onload = function(e) { + this.image_.onerror = null; + this.image_.onload = null; + if (ImageUtil.ImageLoader.isTooLarge(this.image_.width, + this.image_.height)) { + onError('GALLERY_IMAGE_TOO_BIG_ERROR'); + return; + } + transformFetcher(entry, onTransform.bind(this, e.target)); + }.bind(this); + + // The error callback has an optional error argument, which in case of a + // general error should not be specified + this.image_.onerror = onError.bind(this, 'GALLERY_IMAGE_ERROR'); + + // Extract the last modification date to determine if the cached image + // is outdated. + var modificationTime = opt_metadata && + opt_metadata.modificationTime && + opt_metadata.modificationTime.getTime(); + + // Load the image directly. + this.image_.src = entry.toURL(); + }.bind(this); + + // Loads the image. If already loaded, then forces a reload. + var startLoad = this.resetImage_.bind(this, function() { + // Fetch metadata to detect last modification time for the caching purpose. + if (this.metadataCache_) + this.metadataCache_.get(entry, 'filesystem', loadImage); + else + loadImage(); + }.bind(this), onError); + + if (opt_delay) { + this.timeout_ = setTimeout(startLoad, opt_delay); + } else { + startLoad(); + } +}; + +/** + * Resets the image by forcing the garbage collection and clearing the src + * attribute. + * + * @param {function()} onSuccess Success callback. + * @param {function(opt_string)} onError Failure callback with an optional + * error identifier. + * @private + */ +ImageUtil.ImageLoader.prototype.resetImage_ = function(onSuccess, onError) { + var clearSrc = function() { + this.image_.onload = onSuccess; + this.image_.onerror = onSuccess; + this.image_.src = ''; + }.bind(this); + + var emptyImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAA' + + 'AAABAAEAAAICTAEAOw=='; + + if (this.image_.src !== emptyImage) { + // Load an empty image, then clear src. + this.image_.onload = clearSrc; + this.image_.onerror = onError.bind(this, 'GALLERY_IMAGE_ERROR'); + this.image_.src = emptyImage; + } else { + // Empty image already loaded, so clear src immediately. + clearSrc(); + } +}; + +/** + * @return {boolean} True if an image is loading. + */ +ImageUtil.ImageLoader.prototype.isBusy = function() { + return !!this.callback_; +}; + +/** + * @param {Entry} entry Image entry. + * @return {boolean} True if loader loads this image. + */ +ImageUtil.ImageLoader.prototype.isLoading = function(entry) { + return this.isBusy() && util.isSameEntry(this.entry_, entry); +}; + +/** + * @param {function} callback To be called when the image loaded. + */ +ImageUtil.ImageLoader.prototype.setCallback = function(callback) { + this.callback_ = callback; +}; + +/** + * Stops loading image. + */ +ImageUtil.ImageLoader.prototype.cancel = function() { + if (!this.callback_) return; + this.callback_ = null; + if (this.timeout_) { + clearTimeout(this.timeout_); + this.timeout_ = null; + } + if (this.image_) { + this.image_.onload = function() {}; + this.image_.onerror = function() {}; + this.image_.src = ''; + } + this.generation_++; // Silence the transform fetcher if it is in progress. +}; + +/** + * @param {HTMLImageElement} image Image to be transformed. + * @param {Object} transform transformation description to apply to the image. + * @private + */ +ImageUtil.ImageLoader.prototype.convertImage_ = function(image, transform) { + var canvas = this.document_.createElement('canvas'); + + if (transform.rotate90 & 1) { // Rotated +/-90deg, swap the dimensions. + canvas.width = image.height; + canvas.height = image.width; + } else { + canvas.width = image.width; + canvas.height = image.height; + } + + var context = canvas.getContext('2d'); + context.save(); + context.translate(canvas.width / 2, canvas.height / 2); + context.rotate(transform.rotate90 * Math.PI / 2); + context.scale(transform.scaleX, transform.scaleY); + + var stripCount = Math.ceil(image.width * image.height / (1 << 21)); + var step = Math.max(16, Math.ceil(image.height / stripCount)) & 0xFFFFF0; + + this.copyStrip_(context, image, 0, step); +}; + +/** + * @param {CanvasRenderingContext2D} context Context to draw. + * @param {HTMLImageElement} image Image to draw. + * @param {number} firstRow Number of the first pixel row to draw. + * @param {number} rowCount Count of pixel rows to draw. + * @private + */ +ImageUtil.ImageLoader.prototype.copyStrip_ = function( + context, image, firstRow, rowCount) { + var lastRow = Math.min(firstRow + rowCount, image.height); + + context.drawImage( + image, 0, firstRow, image.width, lastRow - firstRow, + -image.width / 2, firstRow - image.height / 2, + image.width, lastRow - firstRow); + + if (lastRow === image.height) { + context.restore(); + if (this.entry_.toURL().substr(0, 5) !== 'data:') { // Ignore data urls. + ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('LoadTime')); + } + try { + setTimeout(this.callback_, 0, context.canvas); + } catch (e) { + console.error(e); + } + this.callback_ = null; + } else { + var self = this; + this.timeout_ = setTimeout( + function() { + self.timeout_ = null; + self.copyStrip_(context, image, lastRow, rowCount); + }, 0); + } +}; + +/** + * @param {HTMLElement} element To remove children from. + */ +ImageUtil.removeChildren = function(element) { + element.textContent = ''; +}; + +/** + * @param {string} name File name (with extension). + * @return {string} File name without extension. + */ +ImageUtil.getDisplayNameFromName = function(name) { + var index = name.lastIndexOf('.'); + if (index !== -1) + return name.substr(0, index); + else + return name; +}; + +/** + * @param {string} name File name. + * @return {string} File extension. + */ +ImageUtil.getExtensionFromFullName = function(name) { + var index = name.lastIndexOf('.'); + if (index !== -1) + return name.substring(index); + else + return ''; +}; + +/** + * Metrics (from metrics.js) itnitialized by the File Manager from owner frame. + * @type {Object?} + */ +ImageUtil.metrics = null; + +/** + * @param {string} name Local name. + * @return {string} Full name. + */ +ImageUtil.getMetricName = function(name) { + return 'PhotoEditor.' + name; +}; + +/** + * Used for metrics reporting, keep in sync with the histogram description. + */ +ImageUtil.FILE_TYPES = ['jpg', 'png', 'gif', 'bmp', 'webp']; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_view.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_view.js new file mode 100644 index 00000000000..24d70045798 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_view.js @@ -0,0 +1,1065 @@ +// 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 overlay displaying the image. + * + * @param {HTMLElement} container The container element. + * @param {Viewport} viewport The viewport. + * @param {MetadataCache} metadataCache The metadataCache. + * @constructor + */ +function ImageView(container, viewport, metadataCache) { + this.container_ = container; + this.viewport_ = viewport; + this.document_ = container.ownerDocument; + this.contentGeneration_ = 0; + this.displayedContentGeneration_ = 0; + this.displayedViewportGeneration_ = 0; + + this.imageLoader_ = new ImageUtil.ImageLoader(this.document_, metadataCache); + // We have a separate image loader for prefetch which does not get cancelled + // when the selection changes. + this.prefetchLoader_ = new ImageUtil.ImageLoader( + this.document_, metadataCache); + + // The content cache is used for prefetching the next image when going + // through the images sequentially. The real life photos can be large + // (18Mpix = 72Mb pixel array) so we want only the minimum amount of caching. + this.contentCache_ = new ImageView.Cache(2); + + // We reuse previously generated screen-scale images so that going back to + // a recently loaded image looks instant even if the image is not in + // the content cache any more. Screen-scale images are small (~1Mpix) + // so we can afford to cache more of them. + this.screenCache_ = new ImageView.Cache(5); + this.contentCallbacks_ = []; + + /** + * The element displaying the current content. + * + * @type {HTMLCanvasElement|HTMLVideoElement} + * @private + */ + this.screenImage_ = null; + + this.localImageTransformFetcher_ = function(entry, callback) { + metadataCache.get(entry, 'fetchedMedia', function(fetchedMedia) { + callback(fetchedMedia.imageTransform); + }); + }; +} + +/** + * Duration of transition between modes in ms. + */ +ImageView.MODE_TRANSITION_DURATION = 350; + +/** + * If the user flips though images faster than this interval we do not apply + * the slide-in/slide-out transition. + */ +ImageView.FAST_SCROLL_INTERVAL = 300; + +/** + * Image load type: full resolution image loaded from cache. + */ +ImageView.LOAD_TYPE_CACHED_FULL = 0; + +/** + * Image load type: screen resolution preview loaded from cache. + */ +ImageView.LOAD_TYPE_CACHED_SCREEN = 1; + +/** + * Image load type: image read from file. + */ +ImageView.LOAD_TYPE_IMAGE_FILE = 2; + +/** + * Image load type: video loaded. + */ +ImageView.LOAD_TYPE_VIDEO_FILE = 3; + +/** + * Image load type: error occurred. + */ +ImageView.LOAD_TYPE_ERROR = 4; + +/** + * Image load type: the file contents is not available offline. + */ +ImageView.LOAD_TYPE_OFFLINE = 5; + +/** + * The total number of load types. + */ +ImageView.LOAD_TYPE_TOTAL = 6; + +ImageView.prototype = {__proto__: ImageBuffer.Overlay.prototype}; + +/** + * Draws below overlays with the default zIndex. + * @return {number} Z-index. + */ +ImageView.prototype.getZIndex = function() { return -1 }; + +/** + * Draws the image on screen. + */ +ImageView.prototype.draw = function() { + if (!this.contentCanvas_) // Do nothing if the image content is not set. + return; + + var forceRepaint = false; + + if (this.displayedViewportGeneration_ !== + this.viewport_.getCacheGeneration()) { + this.displayedViewportGeneration_ = this.viewport_.getCacheGeneration(); + + this.setupDeviceBuffer(this.screenImage_); + + forceRepaint = true; + } + + if (forceRepaint || + this.displayedContentGeneration_ !== this.contentGeneration_) { + this.displayedContentGeneration_ = this.contentGeneration_; + + ImageUtil.trace.resetTimer('paint'); + this.paintDeviceRect(this.viewport_.getDeviceClipped(), + this.contentCanvas_, this.viewport_.getImageClipped()); + ImageUtil.trace.reportTimer('paint'); + } +}; + +/** + * @param {number} x X pointer position. + * @param {number} y Y pointer position. + * @param {boolean} mouseDown True if mouse is down. + * @return {string} CSS cursor style. + */ +ImageView.prototype.getCursorStyle = function(x, y, mouseDown) { + // Indicate that the image is draggable. + if (this.viewport_.isClipped() && + this.viewport_.getScreenClipped().inside(x, y)) + return 'move'; + + return null; +}; + +/** + * @param {number} x X pointer position. + * @param {number} y Y pointer position. + * @return {function} The closure to call on drag. + */ +ImageView.prototype.getDragHandler = function(x, y) { + var cursor = this.getCursorStyle(x, y); + if (cursor === 'move') { + // Return the handler that drags the entire image. + return this.viewport_.createOffsetSetter(x, y); + } + + return null; +}; + +/** + * @return {number} The cache generation. + */ +ImageView.prototype.getCacheGeneration = function() { + return this.contentGeneration_; +}; + +/** + * Invalidates the caches to force redrawing the screen canvas. + */ +ImageView.prototype.invalidateCaches = function() { + this.contentGeneration_++; +}; + +/** + * @return {HTMLCanvasElement} The content canvas element. + */ +ImageView.prototype.getCanvas = function() { return this.contentCanvas_ }; + +/** + * @return {boolean} True if the a valid image is currently loaded. + */ +ImageView.prototype.hasValidImage = function() { + return !this.preview_ && this.contentCanvas_ && this.contentCanvas_.width; +}; + +/** + * @return {HTMLVideoElement} The video element. + */ +ImageView.prototype.getVideo = function() { return this.videoElement_ }; + +/** + * @return {HTMLCanvasElement} The cached thumbnail image. + */ +ImageView.prototype.getThumbnail = function() { return this.thumbnailCanvas_ }; + +/** + * @return {number} The content revision number. + */ +ImageView.prototype.getContentRevision = function() { + return this.contentRevision_; +}; + +/** + * Copies an image fragment from a full resolution canvas to a device resolution + * canvas. + * + * @param {Rect} deviceRect Rectangle in the device coordinates. + * @param {HTMLCanvasElement} canvas Full resolution canvas. + * @param {Rect} imageRect Rectangle in the full resolution canvas. + */ +ImageView.prototype.paintDeviceRect = function(deviceRect, canvas, imageRect) { + // Map screen canvas (0,0) to (deviceBounds.left, deviceBounds.top) + var deviceBounds = this.viewport_.getDeviceClipped(); + deviceRect = deviceRect.shift(-deviceBounds.left, -deviceBounds.top); + + // The source canvas may have different physical size than the image size + // set at the viewport. Adjust imageRect accordingly. + var bounds = this.viewport_.getImageBounds(); + var scaleX = canvas.width / bounds.width; + var scaleY = canvas.height / bounds.height; + imageRect = new Rect(imageRect.left * scaleX, imageRect.top * scaleY, + imageRect.width * scaleX, imageRect.height * scaleY); + Rect.drawImage( + this.screenImage_.getContext('2d'), canvas, deviceRect, imageRect); +}; + +/** + * Creates an overlay canvas with properties similar to the screen canvas. + * Useful for showing quick feedback when editing. + * + * @return {HTMLCanvasElement} Overlay canvas. + */ +ImageView.prototype.createOverlayCanvas = function() { + var canvas = this.document_.createElement('canvas'); + canvas.className = 'image'; + this.container_.appendChild(canvas); + return canvas; +}; + +/** + * Sets up the canvas as a buffer in the device resolution. + * + * @param {HTMLCanvasElement} canvas The buffer canvas. + */ +ImageView.prototype.setupDeviceBuffer = function(canvas) { + var deviceRect = this.viewport_.getDeviceClipped(); + + // Set the canvas position and size in device pixels. + if (canvas.width !== deviceRect.width) + canvas.width = deviceRect.width; + + if (canvas.height !== deviceRect.height) + canvas.height = deviceRect.height; + + canvas.style.left = deviceRect.left + 'px'; + canvas.style.top = deviceRect.top + 'px'; + + // Scale the canvas down to screen pixels. + this.setTransform(canvas); +}; + +/** + * @return {ImageData} A new ImageData object with a copy of the content. + */ +ImageView.prototype.copyScreenImageData = function() { + return this.screenImage_.getContext('2d').getImageData( + 0, 0, this.screenImage_.width, this.screenImage_.height); +}; + +/** + * @return {boolean} True if the image is currently being loaded. + */ +ImageView.prototype.isLoading = function() { + return this.imageLoader_.isBusy(); +}; + +/** + * Cancels the current image loading operation. The callbacks will be ignored. + */ +ImageView.prototype.cancelLoad = function() { + this.imageLoader_.cancel(); +}; + +/** + * Loads and display a new image. + * + * Loads the thumbnail first, then replaces it with the main image. + * Takes into account the image orientation encoded in the metadata. + * + * @param {FileEntry} entry Image entry. + * @param {Object} metadata Metadata. + * @param {Object} effect Transition effect object. + * @param {function(number} displayCallback Called when the image is displayed + * (possibly as a prevew). + * @param {function(number} loadCallback Called when the image is fully loaded. + * The parameter is the load type. + */ +ImageView.prototype.load = function(entry, metadata, effect, + displayCallback, loadCallback) { + if (effect) { + // Skip effects when reloading repeatedly very quickly. + var time = Date.now(); + if (this.lastLoadTime_ && + (time - this.lastLoadTime_) < ImageView.FAST_SCROLL_INTERVAL) { + effect = null; + } + this.lastLoadTime_ = time; + } + + metadata = metadata || {}; + + ImageUtil.metrics.startInterval(ImageUtil.getMetricName('DisplayTime')); + + var self = this; + + this.contentEntry_ = entry; + this.contentRevision_ = -1; + + var loadingVideo = FileType.getMediaType(entry) === 'video'; + if (loadingVideo) { + var video = this.document_.createElement('video'); + var videoPreview = !!(metadata.thumbnail && metadata.thumbnail.url); + if (videoPreview) { + var thumbnailLoader = new ThumbnailLoader( + metadata.thumbnail.url, + ThumbnailLoader.LoaderType.CANVAS, + metadata); + thumbnailLoader.loadDetachedImage(function(success) { + if (success) { + var canvas = thumbnailLoader.getImage(); + video.setAttribute('poster', canvas.toDataURL('image/jpeg')); + this.replace(video, effect); // Show the poster immediately. + if (displayCallback) displayCallback(); + } + }.bind(this)); + } + + var onVideoLoad = function(error) { + video.removeEventListener('loadedmetadata', onVideoLoadSuccess); + video.removeEventListener('error', onVideoLoadError); + displayMainImage(ImageView.LOAD_TYPE_VIDEO_FILE, videoPreview, video, + error); + }; + var onVideoLoadError = onVideoLoad.bind(this, 'GALLERY_VIDEO_ERROR'); + var onVideoLoadSuccess = onVideoLoad.bind(this, null); + + video.addEventListener('loadedmetadata', onVideoLoadSuccess); + video.addEventListener('error', onVideoLoadError); + + video.src = entry.toURL(); + video.load(); + return; + } + + // Cache has to be evicted in advance, so the returned cached image is not + // evicted later by the prefetched image. + this.contentCache_.evictLRU(); + + var cached = this.contentCache_.getItem(this.contentEntry_); + if (cached) { + displayMainImage(ImageView.LOAD_TYPE_CACHED_FULL, + false /* no preview */, cached); + } else { + var cachedScreen = this.screenCache_.getItem(this.contentEntry_); + var imageWidth = metadata.media && metadata.media.width || + metadata.drive && metadata.drive.imageWidth; + var imageHeight = metadata.media && metadata.media.height || + metadata.drive && metadata.drive.imageHeight; + if (cachedScreen) { + // We have a cached screen-scale canvas, use it instead of a thumbnail. + displayThumbnail(ImageView.LOAD_TYPE_CACHED_SCREEN, cachedScreen); + // As far as the user can tell the image is loaded. We still need to load + // the full res image to make editing possible, but we can report now. + ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime')); + } else if ((!effect || (effect.constructor.name === 'Slide')) && + metadata.thumbnail && metadata.thumbnail.url && + !(imageWidth && imageHeight && + ImageUtil.ImageLoader.isTooLarge(imageWidth, imageHeight))) { + // Only show thumbnails if there is no effect or the effect is Slide. + // Also no thumbnail if the image is too large to be loaded. + var thumbnailLoader = new ThumbnailLoader( + metadata.thumbnail.url, + ThumbnailLoader.LoaderType.CANVAS, + metadata); + thumbnailLoader.loadDetachedImage(function(success) { + displayThumbnail(ImageView.LOAD_TYPE_IMAGE_FILE, + success ? thumbnailLoader.getImage() : null); + }); + } else { + loadMainImage(ImageView.LOAD_TYPE_IMAGE_FILE, entry, + false /* no preview*/, 0 /* delay */); + } + } + + function displayThumbnail(loadType, canvas) { + if (canvas) { + self.replace( + canvas, + effect, + metadata.media.width || metadata.drive.imageWidth, + metadata.media.height || metadata.drive.imageHeight, + true /* preview */); + if (displayCallback) displayCallback(); + } + loadMainImage(loadType, entry, !!canvas, + (effect && canvas) ? effect.getSafeInterval() : 0); + } + + function loadMainImage(loadType, contentEntry, previewShown, delay) { + if (self.prefetchLoader_.isLoading(contentEntry)) { + // The image we need is already being prefetched. Initiating another load + // would be a waste. Hijack the load instead by overriding the callback. + self.prefetchLoader_.setCallback( + displayMainImage.bind(null, loadType, previewShown)); + + // Swap the loaders so that the self.isLoading works correctly. + var temp = self.prefetchLoader_; + self.prefetchLoader_ = self.imageLoader_; + self.imageLoader_ = temp; + return; + } + self.prefetchLoader_.cancel(); // The prefetch was doing something useless. + + self.imageLoader_.load( + contentEntry, + self.localImageTransformFetcher_, + displayMainImage.bind(null, loadType, previewShown), + delay); + } + + function displayMainImage(loadType, previewShown, content, opt_error) { + if (opt_error) + loadType = ImageView.LOAD_TYPE_ERROR; + + // If we already displayed the preview we should not replace the content if: + // 1. The full content failed to load. + // or + // 2. We are loading a video (because the full video is displayed in the + // same HTML element as the preview). + var animationDuration = 0; + if (!(previewShown && + (loadType === ImageView.LOAD_TYPE_ERROR || + loadType === ImageView.LOAD_TYPE_VIDEO_FILE))) { + var replaceEffect = previewShown ? null : effect; + animationDuration = replaceEffect ? replaceEffect.getSafeInterval() : 0; + self.replace(content, replaceEffect); + if (!previewShown && displayCallback) displayCallback(); + } + + if (loadType !== ImageView.LOAD_TYPE_ERROR && + loadType !== ImageView.LOAD_TYPE_CACHED_SCREEN) { + ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime')); + } + ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('LoadMode'), + loadType, ImageView.LOAD_TYPE_TOTAL); + + if (loadType === ImageView.LOAD_TYPE_ERROR && + !navigator.onLine && metadata.streaming) { + // |streaming| is set only when the file is not locally cached. + loadType = ImageView.LOAD_TYPE_OFFLINE; + } + if (loadCallback) loadCallback(loadType, animationDuration, opt_error); + } +}; + +/** + * Prefetches an image. + * @param {FileEntry} entry The image entry. + * @param {number} delay Image load delay in ms. + */ +ImageView.prototype.prefetch = function(entry, delay) { + var self = this; + function prefetchDone(canvas) { + if (canvas.width) + self.contentCache_.putItem(entry, canvas); + } + + var cached = this.contentCache_.getItem(entry); + if (cached) { + prefetchDone(cached); + } else if (FileType.getMediaType(entry) === 'image') { + // Evict the LRU item before we allocate the new canvas to avoid unneeded + // strain on memory. + this.contentCache_.evictLRU(); + + this.prefetchLoader_.load( + entry, + this.localImageTransformFetcher_, + prefetchDone, + delay); + } +}; + +/** + * Renames the current image. + * @param {FileEntry} newEntry The new image Entry. + */ +ImageView.prototype.changeEntry = function(newEntry) { + this.contentCache_.renameItem(this.contentEntry_, newEntry); + this.screenCache_.renameItem(this.contentEntry_, newEntry); + this.contentEntry_ = newEntry; +}; + +/** + * Unloads content. + * @param {Rect} zoomToRect Target rectangle for zoom-out-effect. + */ +ImageView.prototype.unload = function(zoomToRect) { + if (this.unloadTimer_) { + clearTimeout(this.unloadTimer_); + this.unloadTimer_ = null; + } + if (zoomToRect && this.screenImage_) { + var effect = this.createZoomEffect(zoomToRect); + this.setTransform(this.screenImage_, effect); + this.screenImage_.setAttribute('fade', true); + this.unloadTimer_ = setTimeout(function() { + this.unloadTimer_ = null; + this.unload(null /* force unload */); + }.bind(this), + effect.getSafeInterval()); + return; + } + this.container_.textContent = ''; + this.contentCanvas_ = null; + this.screenImage_ = null; + this.videoElement_ = null; +}; + +/** + * @param {HTMLCanvasElement|HTMLVideoElement} content The image element. + * @param {number=} opt_width Image width. + * @param {number=} opt_height Image height. + * @param {boolean=} opt_preview True if the image is a preview (not full res). + * @private + */ +ImageView.prototype.replaceContent_ = function( + content, opt_width, opt_height, opt_preview) { + + if (this.contentCanvas_ && this.contentCanvas_.parentNode === this.container_) + this.container_.removeChild(this.contentCanvas_); + + if (content.constructor.name === 'HTMLVideoElement') { + this.contentCanvas_ = null; + this.videoElement_ = content; + this.screenImage_ = content; + this.screenImage_.className = 'image'; + this.container_.appendChild(this.screenImage_); + this.videoElement_.play(); + return; + } + + this.screenImage_ = this.document_.createElement('canvas'); + this.screenImage_.className = 'image'; + + this.videoElement_ = null; + this.contentCanvas_ = content; + this.invalidateCaches(); + this.viewport_.setImageSize( + opt_width || this.contentCanvas_.width, + opt_height || this.contentCanvas_.height); + this.viewport_.fitImage(); + this.viewport_.update(); + this.draw(); + + this.container_.appendChild(this.screenImage_); + + this.preview_ = opt_preview; + // If this is not a thumbnail, cache the content and the screen-scale image. + if (this.hasValidImage()) { + // Insert the full resolution canvas into DOM so that it can be printed. + this.container_.appendChild(this.contentCanvas_); + this.contentCanvas_.classList.add('fullres'); + + this.contentCache_.putItem(this.contentEntry_, this.contentCanvas_, true); + this.screenCache_.putItem(this.contentEntry_, this.screenImage_); + + // TODO(kaznacheev): It is better to pass screenImage_ as it is usually + // much smaller than contentCanvas_ and still contains the entire image. + // Once we implement zoom/pan we should pass contentCanvas_ instead. + this.updateThumbnail_(this.screenImage_); + + this.contentRevision_++; + for (var i = 0; i !== this.contentCallbacks_.length; i++) { + try { + this.contentCallbacks_[i](); + } catch (e) { + console.error(e); + } + } + } +}; + +/** + * Adds a listener for content changes. + * @param {function} callback Callback. + */ +ImageView.prototype.addContentCallback = function(callback) { + this.contentCallbacks_.push(callback); +}; + +/** + * Updates the cached thumbnail image. + * + * @param {HTMLCanvasElement} canvas The source canvas. + * @private + */ +ImageView.prototype.updateThumbnail_ = function(canvas) { + ImageUtil.trace.resetTimer('thumb'); + var pixelCount = 10000; + var downScale = + Math.max(1, Math.sqrt(canvas.width * canvas.height / pixelCount)); + + this.thumbnailCanvas_ = canvas.ownerDocument.createElement('canvas'); + this.thumbnailCanvas_.width = Math.round(canvas.width / downScale); + this.thumbnailCanvas_.height = Math.round(canvas.height / downScale); + Rect.drawImage(this.thumbnailCanvas_.getContext('2d'), canvas); + ImageUtil.trace.reportTimer('thumb'); +}; + +/** + * Replaces the displayed image, possibly with slide-in animation. + * + * @param {HTMLCanvasElement|HTMLVideoElement} content The image element. + * @param {Object=} opt_effect Transition effect object. + * @param {number=} opt_width Image width. + * @param {number=} opt_height Image height. + * @param {boolean=} opt_preview True if the image is a preview (not full res). + */ +ImageView.prototype.replace = function( + content, opt_effect, opt_width, opt_height, opt_preview) { + var oldScreenImage = this.screenImage_; + + this.replaceContent_(content, opt_width, opt_height, opt_preview); + if (!opt_effect) { + if (oldScreenImage) + oldScreenImage.parentNode.removeChild(oldScreenImage); + return; + } + + var newScreenImage = this.screenImage_; + + if (oldScreenImage) + ImageUtil.setAttribute(newScreenImage, 'fade', true); + this.setTransform(newScreenImage, opt_effect, 0 /* instant */); + + setTimeout(function() { + this.setTransform(newScreenImage, null, + opt_effect && opt_effect.getDuration()); + if (oldScreenImage) { + ImageUtil.setAttribute(newScreenImage, 'fade', false); + ImageUtil.setAttribute(oldScreenImage, 'fade', true); + console.assert(opt_effect.getReverse, 'Cannot revert an effect.'); + var reverse = opt_effect.getReverse(); + this.setTransform(oldScreenImage, reverse); + setTimeout(function() { + if (oldScreenImage.parentNode) + oldScreenImage.parentNode.removeChild(oldScreenImage); + }, reverse.getSafeInterval()); + } + }.bind(this), 0); +}; + +/** + * @param {HTMLCanvasElement|HTMLVideoElement} element The element to transform. + * @param {ImageView.Effect=} opt_effect The effect to apply. + * @param {number=} opt_duration Transition duration. + */ +ImageView.prototype.setTransform = function(element, opt_effect, opt_duration) { + if (!opt_effect) + opt_effect = new ImageView.Effect.None(); + if (typeof opt_duration !== 'number') + opt_duration = opt_effect.getDuration(); + element.style.webkitTransitionDuration = opt_duration + 'ms'; + element.style.webkitTransitionTimingFunction = opt_effect.getTiming(); + element.style.webkitTransform = opt_effect.transform(element, this.viewport_); +}; + +/** + * @param {Rect} screenRect Target rectangle in screen coordinates. + * @return {ImageView.Effect.Zoom} Zoom effect object. + */ +ImageView.prototype.createZoomEffect = function(screenRect) { + return new ImageView.Effect.Zoom( + this.viewport_.screenToDeviceRect(screenRect), + null /* use viewport */, + ImageView.MODE_TRANSITION_DURATION); +}; + +/** + * Visualizes crop or rotate operation. Hide the old image instantly, animate + * the new image to visualize the operation. + * + * @param {HTMLCanvasElement} canvas New content canvas. + * @param {Rect} imageCropRect The crop rectangle in image coordinates. + * Null for rotation operations. + * @param {number} rotate90 Rotation angle in 90 degree increments. + * @return {number} Animation duration. + */ +ImageView.prototype.replaceAndAnimate = function( + canvas, imageCropRect, rotate90) { + var oldScale = this.viewport_.getScale(); + var deviceCropRect = imageCropRect && this.viewport_.screenToDeviceRect( + this.viewport_.imageToScreenRect(imageCropRect)); + + var oldScreenImage = this.screenImage_; + this.replaceContent_(canvas); + var newScreenImage = this.screenImage_; + + // Display the new canvas, initially transformed. + var deviceFullRect = this.viewport_.getDeviceClipped(); + + var effect = rotate90 ? + new ImageView.Effect.Rotate( + oldScale / this.viewport_.getScale(), -rotate90) : + new ImageView.Effect.Zoom(deviceCropRect, deviceFullRect); + + this.setTransform(newScreenImage, effect, 0 /* instant */); + + oldScreenImage.parentNode.appendChild(newScreenImage); + oldScreenImage.parentNode.removeChild(oldScreenImage); + + // Let the layout fire, then animate back to non-transformed state. + setTimeout( + this.setTransform.bind( + this, newScreenImage, null, effect.getDuration()), + 0); + + return effect.getSafeInterval(); +}; + +/** + * Visualizes "undo crop". Shrink the current image to the given crop rectangle + * while fading in the new image. + * + * @param {HTMLCanvasElement} canvas New content canvas. + * @param {Rect} imageCropRect The crop rectangle in image coordinates. + * @return {number} Animation duration. + */ +ImageView.prototype.animateAndReplace = function(canvas, imageCropRect) { + var deviceFullRect = this.viewport_.getDeviceClipped(); + var oldScale = this.viewport_.getScale(); + + var oldScreenImage = this.screenImage_; + this.replaceContent_(canvas); + var newScreenImage = this.screenImage_; + + var deviceCropRect = this.viewport_.screenToDeviceRect( + this.viewport_.imageToScreenRect(imageCropRect)); + + var setFade = ImageUtil.setAttribute.bind(null, newScreenImage, 'fade'); + setFade(true); + oldScreenImage.parentNode.insertBefore(newScreenImage, oldScreenImage); + + var effect = new ImageView.Effect.Zoom(deviceCropRect, deviceFullRect); + // Animate to the transformed state. + this.setTransform(oldScreenImage, effect); + + setTimeout(setFade.bind(null, false), 0); + + setTimeout(function() { + if (oldScreenImage.parentNode) + oldScreenImage.parentNode.removeChild(oldScreenImage); + }, effect.getSafeInterval()); + + return effect.getSafeInterval(); +}; + + +/** + * Generic cache with a limited capacity and LRU eviction. + * @param {number} capacity Maximum number of cached item. + * @constructor + */ +ImageView.Cache = function(capacity) { + this.capacity_ = capacity; + this.map_ = {}; + this.order_ = []; +}; + +/** + * Fetches the item from the cache. + * @param {FileEntry} entry The entry. + * @return {Object} The cached item. + */ +ImageView.Cache.prototype.getItem = function(entry) { + return this.map_[entry.toURL()]; +}; + +/** + * Puts the item into the cache. + * + * @param {FileEntry} entry The entry. + * @param {Object} item The item object. + * @param {boolean=} opt_keepLRU True if the LRU order should not be modified. + */ +ImageView.Cache.prototype.putItem = function(entry, item, opt_keepLRU) { + var pos = this.order_.indexOf(entry.toURL()); + + if ((pos >= 0) !== (entry.toURL() in this.map_)) + throw new Error('Inconsistent cache state'); + + if (entry.toURL() in this.map_) { + if (!opt_keepLRU) { + // Move to the end (most recently used). + this.order_.splice(pos, 1); + this.order_.push(entry.toURL()); + } + } else { + this.evictLRU(); + this.order_.push(entry.toURL()); + } + + if ((pos >= 0) && (item !== this.map_[entry.toURL()])) + this.deleteItem_(this.map_[entry.toURL()]); + this.map_[entry.toURL()] = item; + + if (this.order_.length > this.capacity_) + throw new Error('Exceeded cache capacity'); +}; + +/** + * Evicts the least recently used items. + */ +ImageView.Cache.prototype.evictLRU = function() { + if (this.order_.length === this.capacity_) { + var url = this.order_.shift(); + this.deleteItem_(this.map_[url]); + delete this.map_[url]; + } +}; + +/** + * Changes the Entry. + * @param {FileEntry} oldEntry The old Entry. + * @param {FileEntry} newEntry The new Entry. + */ +ImageView.Cache.prototype.renameItem = function(oldEntry, newEntry) { + if (util.isSameEntry(oldEntry, newEntry)) + return; // No need to rename. + + var pos = this.order_.indexOf(oldEntry.toURL()); + if (pos < 0) + return; // Not cached. + + this.order_[pos] = newEntry.toURL(); + this.map_[newEntry.toURL()] = this.map_[oldEntry.toURL()]; + delete this.map_[oldEntry.toURL()]; +}; + +/** + * Disposes an object. + * + * @param {Object} item The item object. + * @private + */ +ImageView.Cache.prototype.deleteItem_ = function(item) { + // Trick to reduce memory usage without waiting for gc. + if (item instanceof HTMLCanvasElement) { + // If the canvas is being used somewhere else (eg. displayed on the screen), + // it will be cleared. + item.width = 0; + item.height = 0; + } +}; + +/* Transition effects */ + +/** + * Base class for effects. + * + * @param {number} duration Duration in ms. + * @param {string=} opt_timing CSS transition timing function name. + * @constructor + */ +ImageView.Effect = function(duration, opt_timing) { + this.duration_ = duration; + this.timing_ = opt_timing || 'linear'; +}; + +/** + * + */ +ImageView.Effect.DEFAULT_DURATION = 180; + +/** + * + */ +ImageView.Effect.MARGIN = 100; + +/** + * @return {number} Effect duration in ms. + */ +ImageView.Effect.prototype.getDuration = function() { return this.duration_ }; + +/** + * @return {number} Delay in ms since the beginning of the animation after which + * it is safe to perform CPU-heavy operations without disrupting the animation. + */ +ImageView.Effect.prototype.getSafeInterval = function() { + return this.getDuration() + ImageView.Effect.MARGIN; +}; + +/** + * @return {string} CSS transition timing function name. + */ +ImageView.Effect.prototype.getTiming = function() { return this.timing_ }; + +/** + * @param {HTMLCanvasElement|HTMLVideoElement} element Element. + * @return {number} Preferred pixel ration to use with this element. + * @private + */ +ImageView.Effect.getPixelRatio_ = function(element) { + if (element.constructor.name === 'HTMLCanvasElement') + return Viewport.getDevicePixelRatio(); + else + return 1; +}; + +/** + * Default effect. It is not a no-op as it needs to adjust a canvas scale + * for devicePixelRatio. + * + * @constructor + */ +ImageView.Effect.None = function() { + ImageView.Effect.call(this, 0); +}; + +/** + * Inherits from ImageView.Effect. + */ +ImageView.Effect.None.prototype = { __proto__: ImageView.Effect.prototype }; + +/** + * @param {HTMLCanvasElement|HTMLVideoElement} element Element. + * @return {string} Transform string. + */ +ImageView.Effect.None.prototype.transform = function(element) { + var ratio = ImageView.Effect.getPixelRatio_(element); + return 'scale(' + (1 / ratio) + ')'; +}; + +/** + * Slide effect. + * + * @param {number} direction -1 for left, 1 for right. + * @param {boolean=} opt_slow True if slow (as in slideshow). + * @constructor + */ +ImageView.Effect.Slide = function Slide(direction, opt_slow) { + ImageView.Effect.call(this, + opt_slow ? 800 : ImageView.Effect.DEFAULT_DURATION, 'ease-in-out'); + this.direction_ = direction; + this.slow_ = opt_slow; + this.shift_ = opt_slow ? 100 : 40; + if (this.direction_ < 0) this.shift_ = -this.shift_; +}; + +/** + * Inherits from ImageView.Effect. + */ +ImageView.Effect.Slide.prototype = { __proto__: ImageView.Effect.prototype }; + +/** + * @return {ImageView.Effect.Slide} Reverse Slide effect. + */ +ImageView.Effect.Slide.prototype.getReverse = function() { + return new ImageView.Effect.Slide(-this.direction_, this.slow_); +}; + +/** + * @param {HTMLCanvasElement|HTMLVideoElement} element Element. + * @return {string} Transform string. + */ +ImageView.Effect.Slide.prototype.transform = function(element) { + var ratio = ImageView.Effect.getPixelRatio_(element); + return 'scale(' + (1 / ratio) + ') translate(' + this.shift_ + 'px, 0px)'; +}; + +/** + * Zoom effect. + * + * Animates the original rectangle to the target rectangle. Both parameters + * should be given in device coordinates (accounting for devicePixelRatio). + * + * @param {Rect} deviceTargetRect Target rectangle. + * @param {Rect=} opt_deviceOriginalRect Original rectangle. If omitted, + * the full viewport will be used at the time of |transform| call. + * @param {number=} opt_duration Duration in ms. + * @constructor + */ +ImageView.Effect.Zoom = function( + deviceTargetRect, opt_deviceOriginalRect, opt_duration) { + ImageView.Effect.call(this, + opt_duration || ImageView.Effect.DEFAULT_DURATION); + this.target_ = deviceTargetRect; + this.original_ = opt_deviceOriginalRect; +}; + +/** + * Inherits from ImageView.Effect. + */ +ImageView.Effect.Zoom.prototype = { __proto__: ImageView.Effect.prototype }; + +/** + * @param {HTMLCanvasElement|HTMLVideoElement} element Element. + * @param {Viewport} viewport Viewport. + * @return {string} Transform string. + */ +ImageView.Effect.Zoom.prototype.transform = function(element, viewport) { + if (!this.original_) + this.original_ = viewport.getDeviceClipped(); + + var ratio = ImageView.Effect.getPixelRatio_(element); + + var dx = (this.target_.left + this.target_.width / 2) - + (this.original_.left + this.original_.width / 2); + var dy = (this.target_.top + this.target_.height / 2) - + (this.original_.top + this.original_.height / 2); + + var scaleX = this.target_.width / this.original_.width; + var scaleY = this.target_.height / this.original_.height; + + return 'translate(' + (dx / ratio) + 'px,' + (dy / ratio) + 'px) ' + + 'scaleX(' + (scaleX / ratio) + ') scaleY(' + (scaleY / ratio) + ')'; +}; + +/** + * Rotate effect. + * + * @param {number} scale Scale. + * @param {number} rotate90 Rotation in 90 degrees increments. + * @constructor + */ +ImageView.Effect.Rotate = function(scale, rotate90) { + ImageView.Effect.call(this, ImageView.Effect.DEFAULT_DURATION); + this.scale_ = scale; + this.rotate90_ = rotate90; +}; + +/** + * Inherits from ImageView.Effect. + */ +ImageView.Effect.Rotate.prototype = { __proto__: ImageView.Effect.prototype }; + +/** + * @param {HTMLCanvasElement|HTMLVideoElement} element Element. + * @return {string} Transform string. + */ +ImageView.Effect.Rotate.prototype.transform = function(element) { + var ratio = ImageView.Effect.getPixelRatio_(element); + return 'rotate(' + (this.rotate90_ * 90) + 'deg) ' + + 'scale(' + (this.scale_ / ratio) + ')'; +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/viewport.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/viewport.js new file mode 100644 index 00000000000..4d6b8ea0874 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/viewport.js @@ -0,0 +1,430 @@ +// 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'; + +/** + * Viewport class controls the way the image is displayed (scale, offset etc). + * @constructor + */ +function Viewport() { + this.imageBounds_ = new Rect(); + this.screenBounds_ = new Rect(); + + this.scale_ = 1; + this.offsetX_ = 0; + this.offsetY_ = 0; + + this.generation_ = 0; + + this.scaleControl_ = null; + this.repaintCallbacks_ = []; + this.update(); +} + +/* + * Viewport modification. + */ + +/** + * @param {Object} scaleControl The UI object responsible for scaling. + */ +Viewport.prototype.setScaleControl = function(scaleControl) { + this.scaleControl_ = scaleControl; +}; + +/** + * @param {number} width Image width. + * @param {number} height Image height. + */ +Viewport.prototype.setImageSize = function(width, height) { + this.imageBounds_ = new Rect(width, height); + if (this.scaleControl_) this.scaleControl_.displayImageSize(width, height); + this.invalidateCaches(); +}; + +/** + * @param {number} width Screen width. + * @param {number} height Screen height. + */ +Viewport.prototype.setScreenSize = function(width, height) { + this.screenBounds_ = new Rect(width, height); + if (this.scaleControl_) + this.scaleControl_.setMinScale(this.getFittingScale()); + this.invalidateCaches(); +}; + +/** + * Set the size by an HTML element. + * + * @param {HTMLElement} frame The element acting as the "screen". + */ +Viewport.prototype.sizeByFrame = function(frame) { + this.setScreenSize(frame.clientWidth, frame.clientHeight); +}; + +/** + * Set the size and scale to fit an HTML element. + * + * @param {HTMLElement} frame The element acting as the "screen". + */ +Viewport.prototype.sizeByFrameAndFit = function(frame) { + var wasFitting = this.getScale() == this.getFittingScale(); + this.sizeByFrame(frame); + var minScale = this.getFittingScale(); + if (wasFitting || (this.getScale() < minScale)) { + this.setScale(minScale, true); + } +}; + +/** + * @return {number} Scale. + */ +Viewport.prototype.getScale = function() { return this.scale_ }; + +/** + * @param {number} scale The new scale. + * @param {boolean} notify True if the change should be reflected in the UI. + */ +Viewport.prototype.setScale = function(scale, notify) { + if (this.scale_ == scale) return; + this.scale_ = scale; + if (notify && this.scaleControl_) this.scaleControl_.displayScale(scale); + this.invalidateCaches(); +}; + +/** + * @return {number} Best scale to fit the current image into the current screen. + */ +Viewport.prototype.getFittingScale = function() { + var scaleX = this.screenBounds_.width / this.imageBounds_.width; + var scaleY = this.screenBounds_.height / this.imageBounds_.height; + // Scales > (1 / this.getDevicePixelRatio()) do not look good. Also they are + // not really useful as we do not have any pixel-level operations. + return Math.min(1 / Viewport.getDevicePixelRatio(), scaleX, scaleY); +}; + +/** + * Set the scale to fit the image into the screen. + */ +Viewport.prototype.fitImage = function() { + var scale = this.getFittingScale(); + if (this.scaleControl_) this.scaleControl_.setMinScale(scale); + this.setScale(scale, true); +}; + +/** + * @return {number} X-offset of the viewport. + */ +Viewport.prototype.getOffsetX = function() { return this.offsetX_ }; + +/** + * @return {number} Y-offset of the viewport. + */ +Viewport.prototype.getOffsetY = function() { return this.offsetY_ }; + +/** + * Set the image offset in the viewport. + * @param {number} x X-offset. + * @param {number} y Y-offset. + * @param {boolean} ignoreClipping True if no clipping should be applied. + */ +Viewport.prototype.setOffset = function(x, y, ignoreClipping) { + if (!ignoreClipping) { + x = this.clampOffsetX_(x); + y = this.clampOffsetY_(y); + } + if (this.offsetX_ == x && this.offsetY_ == y) return; + this.offsetX_ = x; + this.offsetY_ = y; + this.invalidateCaches(); +}; + +/** + * Return a closure that can be called to pan the image. + * Useful for implementing non-trivial variants of panning (overview etc). + * @param {number} originalX The x coordinate on the screen canvas that + * corresponds to zero change to offsetX. + * @param {number} originalY The y coordinate on the screen canvas that + * corresponds to zero change to offsetY. + * @param {function():number} scaleFunc returns the image to screen scale. + * @param {function(number,number):boolean} hitFunc returns true if (x,y) is + * in the valid region. + * @return {function} The closure to pan the image. + */ +Viewport.prototype.createOffsetSetter = function( + originalX, originalY, scaleFunc, hitFunc) { + var originalOffsetX = this.offsetX_; + var originalOffsetY = this.offsetY_; + if (!hitFunc) hitFunc = function() { return true }; + if (!scaleFunc) scaleFunc = this.getScale.bind(this); + + var self = this; + return function(x, y) { + if (hitFunc(x, y)) { + var scale = scaleFunc(); + self.setOffset( + originalOffsetX + (x - originalX) / scale, + originalOffsetY + (y - originalY) / scale); + self.repaint(); + } + }; +}; + +/* + * Access to the current viewport state. + */ + +/** + * @return {Rect} The image bounds in image coordinates. + */ +Viewport.prototype.getImageBounds = function() { return this.imageBounds_ }; + +/** +* @return {Rect} The screen bounds in screen coordinates. +*/ +Viewport.prototype.getScreenBounds = function() { return this.screenBounds_ }; + +/** + * @return {Rect} The visible part of the image, in image coordinates. + */ +Viewport.prototype.getImageClipped = function() { return this.imageClipped_ }; + +/** + * @return {Rect} The visible part of the image, in screen coordinates. + */ +Viewport.prototype.getScreenClipped = function() { return this.screenClipped_ }; + +/** + * A counter that is incremented with each viewport state change. + * Clients that cache anything that depends on the viewport state should keep + * track of this counter. + * @return {number} counter. + */ +Viewport.prototype.getCacheGeneration = function() { return this.generation_ }; + +/** + * Called on event view port state change (even if repaint has not been called). + */ +Viewport.prototype.invalidateCaches = function() { this.generation_++ }; + +/** + * @return {Rect} The image bounds in screen coordinates. + */ +Viewport.prototype.getImageBoundsOnScreen = function() { + return this.imageOnScreen_; +}; + +/* + * Conversion between the screen and image coordinate spaces. + */ + +/** + * @param {number} size Size in screen coordinates. + * @return {number} Size in image coordinates. + */ +Viewport.prototype.screenToImageSize = function(size) { + return size / this.getScale(); +}; + +/** + * @param {number} x X in screen coordinates. + * @return {number} X in image coordinates. + */ +Viewport.prototype.screenToImageX = function(x) { + return Math.round((x - this.imageOnScreen_.left) / this.getScale()); +}; + +/** + * @param {number} y Y in screen coordinates. + * @return {number} Y in image coordinates. + */ +Viewport.prototype.screenToImageY = function(y) { + return Math.round((y - this.imageOnScreen_.top) / this.getScale()); +}; + +/** + * @param {Rect} rect Rectangle in screen coordinates. + * @return {Rect} Rectangle in image coordinates. + */ +Viewport.prototype.screenToImageRect = function(rect) { + return new Rect( + this.screenToImageX(rect.left), + this.screenToImageY(rect.top), + this.screenToImageSize(rect.width), + this.screenToImageSize(rect.height)); +}; + +/** + * @param {number} size Size in image coordinates. + * @return {number} Size in screen coordinates. + */ +Viewport.prototype.imageToScreenSize = function(size) { + return size * this.getScale(); +}; + +/** + * @param {number} x X in image coordinates. + * @return {number} X in screen coordinates. + */ +Viewport.prototype.imageToScreenX = function(x) { + return Math.round(this.imageOnScreen_.left + x * this.getScale()); +}; + +/** + * @param {number} y Y in image coordinates. + * @return {number} Y in screen coordinates. + */ +Viewport.prototype.imageToScreenY = function(y) { + return Math.round(this.imageOnScreen_.top + y * this.getScale()); +}; + +/** + * @param {Rect} rect Rectangle in image coordinates. + * @return {Rect} Rectangle in screen coordinates. + */ +Viewport.prototype.imageToScreenRect = function(rect) { + return new Rect( + this.imageToScreenX(rect.left), + this.imageToScreenY(rect.top), + Math.round(this.imageToScreenSize(rect.width)), + Math.round(this.imageToScreenSize(rect.height))); +}; + +/** + * @return {number} The number of physical pixels in one CSS pixel. + */ +Viewport.getDevicePixelRatio = function() { return window.devicePixelRatio }; + +/** + * Convert a rectangle from screen coordinates to 'device' coordinates. + * + * This conversion enlarges the original rectangle devicePixelRatio times + * with the screen center as a fixed point. + * + * @param {Rect} rect Rectangle in screen coordinates. + * @return {Rect} Rectangle in device coordinates. + */ +Viewport.prototype.screenToDeviceRect = function(rect) { + var ratio = Viewport.getDevicePixelRatio(); + var screenCenterX = Math.round( + this.screenBounds_.left + this.screenBounds_.width / 2); + var screenCenterY = Math.round( + this.screenBounds_.top + this.screenBounds_.height / 2); + return new Rect(screenCenterX + (rect.left - screenCenterX) * ratio, + screenCenterY + (rect.top - screenCenterY) * ratio, + rect.width * ratio, + rect.height * ratio); +}; + +/** + * @return {Rect} The visible part of the image, in device coordinates. + */ +Viewport.prototype.getDeviceClipped = function() { + return this.screenToDeviceRect(this.getScreenClipped()); +}; + +/** + * @return {boolean} True if some part of the image is clipped by the screen. + */ +Viewport.prototype.isClipped = function() { + return this.getMarginX_() < 0 || this.getMarginY_() < 0; +}; + +/** + * @return {number} Horizontal margin. + * Negative if the image is clipped horizontally. + * @private + */ +Viewport.prototype.getMarginX_ = function() { + return Math.round( + (this.screenBounds_.width - this.imageBounds_.width * this.scale_) / 2); +}; + +/** + * @return {number} Vertical margin. + * Negative if the image is clipped vertically. + * @private + */ +Viewport.prototype.getMarginY_ = function() { + return Math.round( + (this.screenBounds_.height - this.imageBounds_.height * this.scale_) / 2); +}; + +/** + * @param {number} x X-offset. + * @return {number} X-offset clamped to the valid range. + * @private + */ +Viewport.prototype.clampOffsetX_ = function(x) { + var limit = Math.round(Math.max(0, -this.getMarginX_() / this.getScale())); + return ImageUtil.clamp(-limit, x, limit); +}; + +/** + * @param {number} y Y-offset. + * @return {number} Y-offset clamped to the valid range. + * @private + */ +Viewport.prototype.clampOffsetY_ = function(y) { + var limit = Math.round(Math.max(0, -this.getMarginY_() / this.getScale())); + return ImageUtil.clamp(-limit, y, limit); +}; + +/** + * Recalculate the viewport parameters. + */ +Viewport.prototype.update = function() { + var scale = this.getScale(); + + // Image bounds in screen coordinates. + this.imageOnScreen_ = new Rect( + this.getMarginX_(), + this.getMarginY_(), + Math.round(this.imageBounds_.width * scale), + Math.round(this.imageBounds_.height * scale)); + + // A visible part of the image in image coordinates. + this.imageClipped_ = new Rect(this.imageBounds_); + + // A visible part of the image in screen coordinates. + this.screenClipped_ = new Rect(this.screenBounds_); + + // Adjust for the offset. + if (this.imageOnScreen_.left < 0) { + this.imageOnScreen_.left += + Math.round(this.clampOffsetX_(this.offsetX_) * scale); + this.imageClipped_.left = Math.round(-this.imageOnScreen_.left / scale); + this.imageClipped_.width = Math.round(this.screenBounds_.width / scale); + } else { + this.screenClipped_.left = this.imageOnScreen_.left; + this.screenClipped_.width = this.imageOnScreen_.width; + } + + if (this.imageOnScreen_.top < 0) { + this.imageOnScreen_.top += + Math.round(this.clampOffsetY_(this.offsetY_) * scale); + this.imageClipped_.top = Math.round(-this.imageOnScreen_.top / scale); + this.imageClipped_.height = Math.round(this.screenBounds_.height / scale); + } else { + this.screenClipped_.top = this.imageOnScreen_.top; + this.screenClipped_.height = this.imageOnScreen_.height; + } +}; + +/** + * @param {function} callback Repaint callback. + */ +Viewport.prototype.addRepaintCallback = function(callback) { + this.repaintCallbacks_.push(callback); +}; + +/** + * Repaint all clients. + */ +Viewport.prototype.repaint = function() { + this.update(); + for (var i = 0; i != this.repaintCallbacks_.length; i++) + this.repaintCallbacks_[i](); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/main.js b/chromium/chrome/browser/resources/file_manager/foreground/js/main.js new file mode 100644 index 00000000000..f32c0a1ef47 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/main.js @@ -0,0 +1,41 @@ +// 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'; + +/** + * @type {FileManager} + */ +var fileManager; + +/** + * Indicates if the DOM and scripts have been already loaded. + * @type {boolean} + */ +var pageLoaded = false; + +/** + * Kick off the file manager dialog. + * Called by main.html after the DOM has been parsed. + */ +function init() { + // Initializes UI and starts the File Manager dialog. + fileManager.initializeUI(document.body, function() { + chrome.test.sendMessage('ready'); + metrics.recordInterval('Load.Total'); + }); +} + +// Create the File Manager object. Note, that the DOM, nor any external +// scripts may not be ready yet. +fileManager = new FileManager(); + +// Initialize the core stuff, which doesn't require access to DOM nor to +// additional scripts. +fileManager.initializeCore(); + +// Final initialization is performed after all scripts and Dom is loaded. +util.addPageLoadHandler(init); + +metrics.recordInterval('Load.Script'); // Must be the last line. diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/main_scripts.js b/chromium/chrome/browser/resources/file_manager/foreground/js/main_scripts.js new file mode 100644 index 00000000000..768d35ce8fd --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/main_scripts.js @@ -0,0 +1,132 @@ +// 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. + +// The include directives are put into Javascript-style comments to prevent +// parsing errors in non-flattened mode. The flattener still sees them. +// Note that this makes the flattener to comment out the first line of the +// included file but that's all right since any javascript file should start +// with a copyright comment anyway. + +// If you add a new dependency, you should update build files by rerunning +// gyp. Otherwise, you'll be bitten by a dependency issue like: +// +// 1) You add a new dependency to "whatever.js" +// 2) You make changes in "whatever.js" +// 3) Rebuild "resources.pak" and open Files.app +// 4) You don't see the changes in "whatever.js". Why is that? +// +// Because the dependencies are computed at gyp time, the existing build +// files don't know that "resources.pak" now has a dependency to +// "whatever.js". You should rerun gyp to let the build files know. +// +// //metrics.js initiates load performance tracking +// //so we want to parse it as early as possible. +//<include src="metrics.js"/> +// +//<include src="../../../image_loader/image_loader_client.js"/> +// +//<include src="../../../../../../ui/webui/resources/js/load_time_data.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr.js"/> +//<include src="../../../../../../ui/webui/resources/js/util.js"/> +//<include src="../../../../../../ui/webui/resources/js/i18n_template_no_process.js"/> +// +//<include src="../../../../../../ui/webui/resources/js/event_tracker.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/event_target.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/touch_handler.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/array_data_model.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/dialogs.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/list_item.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/list_selection_model.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/list_single_selection_model.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/list_selection_controller.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/list.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/tree.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/autocomplete_list.js"/> +// +//<include src="../../../../../../ui/webui/resources/js/cr/ui/splitter.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/table/table_splitter.js"/> +// +//<include src="../../../../../../ui/webui/resources/js/cr/ui/table/table_column.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/table/table_column_model.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/table/table_header.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/table/table_list.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/table.js"/> +// +//<include src="../../../../../../ui/webui/resources/js/cr/ui/grid.js"/> +// +//<include src="../../../../../../ui/webui/resources/js/cr/ui/command.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/position_util.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/menu_item.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/menu.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/menu_button.js"/> +//<include src="../../../../../../ui/webui/resources/js/cr/ui/context_menu_handler.js"/> + +(function() { +// 'strict mode' is invoked for this scope. + +// // This script must be loaded before all other Files.app's scripts. +//<include src="error_counter.js"/> +// +//<include src="../../common/js/async_util.js"/> +//<include src="../../common/js/path_util.js"/> +//<include src="../../common/js/util.js"/> +//<include src="../../common/js/progress_center_common.js"> +// +//<include src="combobutton.js"/> +//<include src="commandbutton.js"/> +//<include src="ui/file_manager_dialog_base.js"/> +// +//<include src="app_installer.js"/> +//<include src="cws_container_client.js"/> +//<include src="directory_contents.js"/> +//<include src="directory_model.js"/> +//<include src="directory_tree.js"/> +//<include src="drag_selector.js"/> +//<include src="drive_banners.js" /> +//<include src="error_dialog.js"/> +//<include src="file_operation_manager_wrapper.js"/> +//<include src="file_grid.js"/> +//<include src="file_manager.js"/> +//<include src="file_selection.js"/> +//<include src="file_table.js"/> +//<include src="file_tasks.js"/> +//<include src="file_transfer_controller.js"/> +//<include src="file_type.js"/> +//<include src="file_watcher.js"/> +//<include src="folder_shortcuts_data_model.js"/> +//<include src="navigation_list_model.js"/> +//<include src="scrollbar.js"/> +//<include src="share_client.js"/> +//<include src="share_dialog.js"/> +//<include src="suggest_apps_dialog.js"/> +//<include src="text_measure.js"/> +//<include src="tree.css.js"/> +//<include src="ui/breadcrumbs_controller.js"/> +//<include src="ui/conflict_dialog.js"/> +//<include src="ui/file_manager_ui.js"/> +//<include src="ui/navigation_list.js"/> +//<include src="ui/preview_panel.js"/> +//<include src="ui/progress_center_panel.js"/> +//<include src="ui/search_box.js"/> +//<include src="volume_manager_wrapper.js"/> +//<include src="media/media_util.js"/> +//<include src="metadata/metadata_cache.js"/> +//<include src="default_action_dialog.js"/> +//<include src="file_manager_commands.js"/> + +// // For accurate load performance tracking place main.js should be +// // the last include to include. +//<include src="main.js"/> + +// Global fileManager reference useful for poking at from the console. +window.fileManager = fileManager; + +// Exports +window.util = util; +window.FileOperationManagerWrapper = FileOperationManagerWrapper; + +window.unload = unload; + +})(); diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/media/audio_player.js b/chromium/chrome/browser/resources/file_manager/foreground/js/media/audio_player.js new file mode 100644 index 00000000000..4791c31f92b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/media/audio_player.js @@ -0,0 +1,628 @@ +// 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'; + +/** + * TODO(mtomasz): Rewrite the entire audio player. + * + * @param {HTMLElement} container Container element. + * @constructor + */ +function AudioPlayer(container) { + this.container_ = container; + this.metadataCache_ = MetadataCache.createFull(); + this.currentTrack_ = -1; + this.playlistGeneration_ = 0; + this.selectedEntry_ = null; + this.volumeManager_ = new VolumeManagerWrapper( + VolumeManagerWrapper.DriveEnabledStatus.DRIVE_ENABLED); + + this.container_.classList.add('collapsed'); + + function createChild(opt_className, opt_tag) { + var child = container.ownerDocument.createElement(opt_tag || 'div'); + if (opt_className) + child.className = opt_className; + container.appendChild(child); + return child; + } + + // We create two separate containers (for expanded and compact view) and keep + // two sets of TrackInfo instances. We could fiddle with a single set instead + // but it would make keeping the list scroll position very tricky. + this.trackList_ = createChild('track-list'); + this.trackStack_ = createChild('track-stack'); + + createChild('title-button collapse').addEventListener( + 'click', this.onExpandCollapse_.bind(this)); + + this.audioControls_ = new FullWindowAudioControls( + createChild(), this.advance_.bind(this), this.onError_.bind(this)); + + this.audioControls_.attachMedia(createChild('', 'audio')); + + chrome.fileBrowserPrivate.getStrings(function(strings) { + container.ownerDocument.title = strings['AUDIO_PLAYER_TITLE']; + this.errorString_ = strings['AUDIO_ERROR']; + this.offlineString_ = strings['AUDIO_OFFLINE']; + AudioPlayer.TrackInfo.DEFAULT_ARTIST = + strings['AUDIO_PLAYER_DEFAULT_ARTIST']; + }.bind(this)); + + this.volumeManager_.addEventListener('externally-unmounted', + this.onExternallyUnmounted_.bind(this)); + + window.addEventListener('resize', this.onResize_.bind(this)); + + // Show the window after DOM is processed. + var currentWindow = chrome.app.window.current(); + setTimeout(currentWindow.show.bind(currentWindow), 0); +} + +/** + * Initial load method (static). + */ +AudioPlayer.load = function() { + document.ondragstart = function(e) { e.preventDefault() }; + + // TODO(mtomasz): Consider providing an exact size icon, instead of relying + // on downsampling by ash. + chrome.app.window.current().setIcon( + 'foreground/images/media/2x/audio_player.png'); + + AudioPlayer.instance = + new AudioPlayer(document.querySelector('.audio-player')); + reload(); +}; + +util.addPageLoadHandler(AudioPlayer.load); + +/** + * Unload the player. + */ +function unload() { + if (AudioPlayer.instance) + AudioPlayer.instance.onUnload(); +} + +/** + * Reload the player. + */ +function reload() { + if (window.appState) { + util.saveAppState(); + AudioPlayer.instance.load(window.appState); + return; + } +} + +/** + * Load a new playlist. + * @param {Playlist} playlist Playlist object passed via mediaPlayerPrivate. + */ +AudioPlayer.prototype.load = function(playlist) { + this.playlistGeneration_++; + this.audioControls_.pause(); + this.currentTrack_ = -1; + + // Save the app state, in case of restart. + window.appState = playlist; + util.saveAppState(); + + util.URLsToEntries(playlist.items, function(entries) { + this.entries_ = entries; + this.invalidTracks_ = {}; + this.cancelAutoAdvance_(); + + if (this.entries_.length <= 1) + this.container_.classList.add('single-track'); + else + this.container_.classList.remove('single-track'); + + this.syncHeight_(); + + this.trackList_.textContent = ''; + this.trackStack_.textContent = ''; + + this.trackListItems_ = []; + this.trackStackItems_ = []; + + if (this.entries_.length == 0) + return; + + for (var i = 0; i != this.entries_.length; i++) { + var entry = this.entries_[i]; + var onClick = this.select_.bind(this, i, false /* no restore */); + this.trackListItems_.push( + new AudioPlayer.TrackInfo(this.trackList_, entry, onClick)); + this.trackStackItems_.push( + new AudioPlayer.TrackInfo(this.trackStack_, entry, onClick)); + } + + this.select_(playlist.position, !!playlist.time); + + // This class will be removed if at least one track has art. + this.container_.classList.add('noart'); + + // Load the selected track metadata first, then load the rest. + this.loadMetadata_(playlist.position); + for (i = 0; i != this.entries_.length; i++) { + if (i != playlist.position) + this.loadMetadata_(i); + } + }.bind(this)); +}; + +/** + * Load metadata for a track. + * @param {number} track Track number. + * @private + */ +AudioPlayer.prototype.loadMetadata_ = function(track) { + this.fetchMetadata_( + this.entries_[track], this.displayMetadata_.bind(this, track)); +}; + +/** + * Display track's metadata. + * @param {number} track Track number. + * @param {Object} metadata Metadata object. + * @param {string=} opt_error Error message. + * @private + */ +AudioPlayer.prototype.displayMetadata_ = function(track, metadata, opt_error) { + this.trackListItems_[track]. + setMetadata(metadata, this.container_, opt_error); + this.trackStackItems_[track]. + setMetadata(metadata, this.container_, opt_error); +}; + +/** + * Closes audio player when a volume containing the selected item is unmounted. + * @param {Event} event The unmount event. + * @private + */ +AudioPlayer.prototype.onExternallyUnmounted_ = function(event) { + if (!this.selectedEntry_) + return; + + if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) === + event.volumeInfo) { + window.close(); + } +}; + +/** + * Called on window is being unloaded. + */ +AudioPlayer.prototype.onUnload = function() { + this.audioControls_.cleanup(); + this.volumeManager_.dispose(); +}; + +/** + * Select a new track to play. + * @param {number} newTrack New track number. + * @param {boolean=} opt_restoreState True if restoring the play state from URL. + * @private + */ +AudioPlayer.prototype.select_ = function(newTrack, opt_restoreState) { + if (this.currentTrack_ == newTrack) return; + + this.changeSelectionInList_(this.currentTrack_, newTrack); + this.changeSelectionInStack_(this.currentTrack_, newTrack); + + this.currentTrack_ = newTrack; + + if (window.appState) { + window.appState.position = this.currentTrack_; + window.appState.time = 0; + util.saveAppState(); + } else { + util.platform.setPreference(AudioPlayer.TRACK_KEY, this.currentTrack_); + } + + this.scrollToCurrent_(false); + + var currentTrack = this.currentTrack_; + var entry = this.entries_[currentTrack]; + this.fetchMetadata_(entry, function(metadata) { + if (this.currentTrack_ != currentTrack) + return; + this.audioControls_.load(entry, opt_restoreState); + + // Resolve real filesystem path of the current audio file. + this.selectedEntry_ = entry; + }.bind(this)); +}; + +/** + * @param {Entry} entry Track file entry. + * @param {function(object)} callback Callback. + * @private + */ +AudioPlayer.prototype.fetchMetadata_ = function(entry, callback) { + this.metadataCache_.get(entry, 'thumbnail|media|streaming', + function(generation, metadata) { + // Do nothing if another load happened since the metadata request. + if (this.playlistGeneration_ == generation) + callback(metadata); + }.bind(this, this.playlistGeneration_)); +}; + +/** + * @param {number} oldTrack Old track number. + * @param {number} newTrack New track number. + * @private + */ +AudioPlayer.prototype.changeSelectionInList_ = function(oldTrack, newTrack) { + this.trackListItems_[newTrack].getBox().classList.add('selected'); + + if (oldTrack >= 0) { + this.trackListItems_[oldTrack].getBox().classList.remove('selected'); + } +}; + +/** + * @param {number} oldTrack Old track number. + * @param {number} newTrack New track number. + * @private + */ +AudioPlayer.prototype.changeSelectionInStack_ = function(oldTrack, newTrack) { + var newBox = this.trackStackItems_[newTrack].getBox(); + newBox.classList.add('selected'); // Put on top immediately. + newBox.classList.add('visible'); // Start fading in. + + if (oldTrack >= 0) { + var oldBox = this.trackStackItems_[oldTrack].getBox(); + oldBox.classList.remove('selected'); // Put under immediately. + setTimeout(function() { + if (!oldBox.classList.contains('selected')) { + // This will start fading out which is not really necessary because + // oldBox is already completely obscured by newBox. + oldBox.classList.remove('visible'); + } + }, 300); + } +}; + +/** + * Scrolls the current track into the viewport. + * + * @param {boolean} keepAtBottom If true, make the selected track the last + * of the visible (if possible). If false, perform minimal scrolling. + * @private + */ +AudioPlayer.prototype.scrollToCurrent_ = function(keepAtBottom) { + var box = this.trackListItems_[this.currentTrack_].getBox(); + this.trackList_.scrollTop = Math.max( + keepAtBottom ? 0 : Math.min(box.offsetTop, this.trackList_.scrollTop), + box.offsetTop + box.offsetHeight - this.trackList_.clientHeight); +}; + +/** + * @return {boolean} True if the player is be displayed in compact mode. + * @private + */ +AudioPlayer.prototype.isCompact_ = function() { + return this.container_.classList.contains('collapsed') || + this.container_.classList.contains('single-track'); +}; + +/** + * Go to the previous or the next track. + * @param {boolean} forward True if next, false if previous. + * @param {boolean=} opt_onlyIfValid True if invalid tracks should be selected. + * @private + */ +AudioPlayer.prototype.advance_ = function(forward, opt_onlyIfValid) { + this.cancelAutoAdvance_(); + + var newTrack = this.currentTrack_ + (forward ? 1 : -1); + if (newTrack < 0) newTrack = this.entries_.length - 1; + if (newTrack == this.entries_.length) newTrack = 0; + if (opt_onlyIfValid && this.invalidTracks_[newTrack]) + return; + this.select_(newTrack); +}; + +/** + * Media error handler. + * @private + */ +AudioPlayer.prototype.onError_ = function() { + var track = this.currentTrack_; + + this.invalidTracks_[track] = true; + + this.fetchMetadata_( + this.entries_[track], + function(metadata) { + var error = (!navigator.onLine && metadata.streaming) ? + this.offlineString_ : this.errorString_; + this.displayMetadata_(track, metadata, error); + this.scheduleAutoAdvance_(); + }.bind(this)); +}; + +/** + * Schedule automatic advance to the next track after a timeout. + * @private + */ +AudioPlayer.prototype.scheduleAutoAdvance_ = function() { + this.cancelAutoAdvance_(); + this.autoAdvanceTimer_ = setTimeout( + function() { + this.autoAdvanceTimer_ = null; + // We are advancing only if the next track is not known to be invalid. + // This prevents an endless auto-advancing in the case when all tracks + // are invalid (we will only visit each track once). + this.advance_(true /* forward */, true /* only if valid */); + }.bind(this), + 3000); +}; + +/** + * Cancel the scheduled auto advance. + * @private + */ +AudioPlayer.prototype.cancelAutoAdvance_ = function() { + if (this.autoAdvanceTimer_) { + clearTimeout(this.autoAdvanceTimer_); + this.autoAdvanceTimer_ = null; + } +}; + +/** + * Expand/collapse button click handler. Toggles the mode and updates the + * height of the window. + * + * @private + */ +AudioPlayer.prototype.onExpandCollapse_ = function() { + if (!this.isCompact_()) { + this.setExpanded_(false); + this.lastExpandedHeight_ = window.innerHeight; + } else { + this.setExpanded_(true); + } + this.syncHeight_(); +}; + +/** + * Toggles the current expand mode. + * + * @param {boolean} on True if on, false otherwise. + * @private + */ +AudioPlayer.prototype.setExpanded_ = function(on) { + if (on) { + this.container_.classList.remove('collapsed'); + this.scrollToCurrent_(true); + } else { + this.container_.classList.add('collapsed'); + } +}; + +/** + * Toggles the expanded mode when resizing. + * + * @param {Event} event Resize event. + * @private + */ +AudioPlayer.prototype.onResize_ = function(event) { + if (this.isCompact_() && + window.innerHeight >= AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) { + this.setExpanded_(true); + } else if (!this.isCompact_() && + window.innerHeight < AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) { + this.setExpanded_(false); + } +}; + +/* Keep the below constants in sync with the CSS. */ + +/** + * Window header size in pixels. + * @type {number} + * @const + */ +AudioPlayer.HEADER_HEIGHT = 28; + +/** + * Track height in pixels. + * @type {number} + * @const + */ +AudioPlayer.TRACK_HEIGHT = 58; + +/** + * Controls bar height in pixels. + * @type {number} + * @const + */ +AudioPlayer.CONTROLS_HEIGHT = 35; + +/** + * Default number of items in the expanded mode. + * @type {number} + * @const + */ +AudioPlayer.DEFAULT_EXPANDED_ITEMS = 5; + +/** + * Minimum size of the window in the expanded mode in pixels. + * @type {number} + * @const + */ +AudioPlayer.EXPANDED_MODE_MIN_HEIGHT = AudioPlayer.CONTROLS_HEIGHT + + AudioPlayer.TRACK_HEIGHT * 2; + +/** + * Set the correct player window height. + * @private + */ +AudioPlayer.prototype.syncHeight_ = function() { + var targetHeight; + + if (!this.isCompact_()) { + // Expanded. + if (this.lastExpandedHeight_) { + targetHeight = this.lastExpandedHeight_; + } else { + var expandedListHeight = + Math.min(this.entries_.length, AudioPlayer.DEFAULT_EXPANDED_ITEMS) * + AudioPlayer.TRACK_HEIGHT; + targetHeight = AudioPlayer.CONTROLS_HEIGHT + expandedListHeight; + } + } else { + // Not expaned. + targetHeight = AudioPlayer.CONTROLS_HEIGHT + AudioPlayer.TRACK_HEIGHT; + } + + window.resizeTo(window.innerWidth, targetHeight + AudioPlayer.HEADER_HEIGHT); +}; + +/** + * Create a TrackInfo object encapsulating the information about one track. + * + * @param {HTMLElement} container Container element. + * @param {Entry} entry Track entry. + * @param {function} onClick Click handler. + * @constructor + */ +AudioPlayer.TrackInfo = function(container, entry, onClick) { + this.entry_ = entry; + + var doc = container.ownerDocument; + + this.box_ = doc.createElement('div'); + this.box_.className = 'track'; + this.box_.addEventListener('click', onClick); + container.appendChild(this.box_); + + this.art_ = doc.createElement('div'); + this.art_.className = 'art blank'; + this.box_.appendChild(this.art_); + + this.img_ = doc.createElement('img'); + this.art_.appendChild(this.img_); + + this.data_ = doc.createElement('div'); + this.data_.className = 'data'; + this.box_.appendChild(this.data_); + + this.title_ = doc.createElement('div'); + this.title_.className = 'data-title'; + this.data_.appendChild(this.title_); + + this.artist_ = doc.createElement('div'); + this.artist_.className = 'data-artist'; + this.data_.appendChild(this.artist_); +}; + +/** + * @return {HTMLDivElement} The wrapper element for the track. + */ +AudioPlayer.TrackInfo.prototype.getBox = function() { return this.box_ }; + +/** + * @return {string} Default track title (file name extracted from the entry). + */ +AudioPlayer.TrackInfo.prototype.getDefaultTitle = function() { + // TODO(mtomasz): Reuse ImageUtil.getDisplayNameFromName(). + var name = this.entry_.name; + var dotIndex = name.lastIndexOf('.'); + var title = dotIndex >= 0 ? name.substr(0, dotIndex) : name; + return title; +}; + +/** + * TODO(kaznacheev): Localize. + */ +AudioPlayer.TrackInfo.DEFAULT_ARTIST = 'Unknown Artist'; + +/** + * @return {string} 'Unknown artist' string. + */ +AudioPlayer.TrackInfo.prototype.getDefaultArtist = function() { + return AudioPlayer.TrackInfo.DEFAULT_ARTIST; +}; + +/** + * @param {Object} metadata The metadata object. + * @param {HTMLElement} container The container for the tracks. + * @param {string} error Error string. + */ +AudioPlayer.TrackInfo.prototype.setMetadata = function( + metadata, container, error) { + if (error) { + this.art_.classList.add('blank'); + this.art_.classList.add('error'); + container.classList.remove('noart'); + } else if (metadata.thumbnail && metadata.thumbnail.url) { + this.img_.onload = function() { + // Only display the image if the thumbnail loaded successfully. + this.art_.classList.remove('blank'); + container.classList.remove('noart'); + }.bind(this); + this.img_.src = metadata.thumbnail.url; + } + this.title_.textContent = (metadata.media && metadata.media.title) || + this.getDefaultTitle(); + this.artist_.textContent = error || + (metadata.media && metadata.media.artist) || this.getDefaultArtist(); +}; + +/** + * Audio controls specific for the Audio Player. + * + * @param {HTMLElement} container Parent container. + * @param {function(boolean)} advanceTrack Parameter: true=forward. + * @param {function} onError Error handler. + * @constructor + */ +function FullWindowAudioControls(container, advanceTrack, onError) { + AudioControls.apply(this, arguments); + + document.addEventListener('keydown', function(e) { + if (e.keyIdentifier == 'U+0020') { + this.togglePlayState(); + e.preventDefault(); + } + }.bind(this)); +} + +FullWindowAudioControls.prototype = { __proto__: AudioControls.prototype }; + +/** + * Enable play state restore from the location hash. + * @param {FileEntry} entry Source Entry. + * @param {boolean} restore True if need to restore the play state. + */ +FullWindowAudioControls.prototype.load = function(entry, restore) { + this.media_.src = entry.toURL(); + this.media_.load(); + this.restoreWhenLoaded_ = restore; +}; + +/** + * Save the current state so that it survives page/app reload. + */ +FullWindowAudioControls.prototype.onPlayStateChanged = function() { + this.encodeState(); +}; + +/** + * Restore the state after page/app reload. + */ +FullWindowAudioControls.prototype.restorePlayState = function() { + if (this.restoreWhenLoaded_) { + this.restoreWhenLoaded_ = false; // This should only work once. + if (this.decodeState()) + return; + } + this.play(); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/media/media_controls.js b/chromium/chrome/browser/resources/file_manager/foreground/js/media/media_controls.js new file mode 100644 index 00000000000..a335bdc4c52 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/media/media_controls.js @@ -0,0 +1,1245 @@ +// 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'; + +/** + * @fileoverview MediaControls class implements media playback controls + * that exist outside of the audio/video HTML element. + */ + +/** + * @param {HTMLElement} containerElement The container for the controls. + * @param {function} onMediaError Function to display an error message. + * @constructor + */ +function MediaControls(containerElement, onMediaError) { + this.container_ = containerElement; + this.document_ = this.container_.ownerDocument; + this.media_ = null; + + this.onMediaPlayBound_ = this.onMediaPlay_.bind(this, true); + this.onMediaPauseBound_ = this.onMediaPlay_.bind(this, false); + this.onMediaDurationBound_ = this.onMediaDuration_.bind(this); + this.onMediaProgressBound_ = this.onMediaProgress_.bind(this); + this.onMediaError_ = onMediaError || function() {}; +} + +/** + * Button's state types. Values are used as CSS class names. + * @enum {string} + */ +MediaControls.ButtonStateType = { + DEFAULT: 'default', + PLAYING: 'playing', + ENDED: 'ended' +}; + +/** + * @return {HTMLAudioElement|HTMLVideoElement} The media element. + */ +MediaControls.prototype.getMedia = function() { return this.media_ }; + +/** + * Format the time in hh:mm:ss format (omitting redundant leading zeros). + * + * @param {number} timeInSec Time in seconds. + * @return {string} Formatted time string. + * @private + */ +MediaControls.formatTime_ = function(timeInSec) { + var seconds = Math.floor(timeInSec % 60); + var minutes = Math.floor((timeInSec / 60) % 60); + var hours = Math.floor(timeInSec / 60 / 60); + var result = ''; + if (hours) result += hours + ':'; + if (hours && (minutes < 10)) result += '0'; + result += minutes + ':'; + if (seconds < 10) result += '0'; + result += seconds; + return result; +}; + +/** + * Create a custom control. + * + * @param {string} className Class name. + * @param {HTMLElement=} opt_parent Parent element or container if undefined. + * @return {HTMLElement} The new control element. + */ +MediaControls.prototype.createControl = function(className, opt_parent) { + var parent = opt_parent || this.container_; + var control = this.document_.createElement('div'); + control.className = className; + parent.appendChild(control); + return control; +}; + +/** + * Create a custom button. + * + * @param {string} className Class name. + * @param {function(Event)} handler Click handler. + * @param {HTMLElement=} opt_parent Parent element or container if undefined. + * @param {number=} opt_numStates Number of states, default: 1. + * @return {HTMLElement} The new button element. + */ +MediaControls.prototype.createButton = function( + className, handler, opt_parent, opt_numStates) { + opt_numStates = opt_numStates || 1; + + var button = this.createControl(className, opt_parent); + button.classList.add('media-button'); + button.addEventListener('click', handler); + + var stateTypes = Object.keys(MediaControls.ButtonStateType); + for (var state = 0; state != opt_numStates; state++) { + var stateClass = MediaControls.ButtonStateType[stateTypes[state]]; + this.createControl('normal ' + stateClass, button); + this.createControl('hover ' + stateClass, button); + this.createControl('active ' + stateClass, button); + } + this.createControl('disabled', button); + + button.setAttribute('state', MediaControls.ButtonStateType.DEFAULT); + button.addEventListener('click', handler); + return button; +}; + +/** + * Enable/disable controls matching a given selector. + * + * @param {string} selector CSS selector. + * @param {boolean} on True if enable, false if disable. + * @private + */ +MediaControls.prototype.enableControls_ = function(selector, on) { + var controls = this.container_.querySelectorAll(selector); + for (var i = 0; i != controls.length; i++) { + var classList = controls[i].classList; + if (on) + classList.remove('disabled'); + else + classList.add('disabled'); + } +}; + +/* + * Playback control. + */ + +/** + * Play the media. + */ +MediaControls.prototype.play = function() { + this.media_.play(); +}; + +/** + * Pause the media. + */ +MediaControls.prototype.pause = function() { + this.media_.pause(); +}; + +/** + * @return {boolean} True if the media is currently playing. + */ +MediaControls.prototype.isPlaying = function() { + return !this.media_.paused && !this.media_.ended; +}; + +/** + * Toggle play/pause. + */ +MediaControls.prototype.togglePlayState = function() { + if (this.isPlaying()) + this.pause(); + else + this.play(); +}; + +/** + * Toggle play/pause state on a mouse click on the play/pause button. Can be + * called externally. TODO(mtomasz): Remove it. http://www.crbug.com/254318. + * + * @param {Event=} opt_event Mouse click event. + */ +MediaControls.prototype.onPlayButtonClicked = function(opt_event) { + this.togglePlayState(); +}; + +/** + * @param {HTMLElement=} opt_parent Parent container. + */ +MediaControls.prototype.initPlayButton = function(opt_parent) { + this.playButton_ = this.createButton('play media-control', + this.onPlayButtonClicked.bind(this), opt_parent, 3 /* States. */); +}; + +/* + * Time controls + */ + +/** + * The default range of 100 is too coarse for the media progress slider. + */ +MediaControls.PROGRESS_RANGE = 5000; + +/** + * @param {boolean=} opt_seekMark True if the progress slider should have + * a seek mark. + * @param {HTMLElement=} opt_parent Parent container. + */ +MediaControls.prototype.initTimeControls = function(opt_seekMark, opt_parent) { + var timeControls = this.createControl('time-controls', opt_parent); + + var sliderConstructor = + opt_seekMark ? MediaControls.PreciseSlider : MediaControls.Slider; + + this.progressSlider_ = new sliderConstructor( + this.createControl('progress media-control', timeControls), + 0, /* value */ + MediaControls.PROGRESS_RANGE, + this.onProgressChange_.bind(this), + this.onProgressDrag_.bind(this)); + + var timeBox = this.createControl('time media-control', timeControls); + + this.duration_ = this.createControl('duration', timeBox); + // Set the initial width to the minimum to reduce the flicker. + this.duration_.textContent = MediaControls.formatTime_(0); + + this.currentTime_ = this.createControl('current', timeBox); +}; + +/** + * @param {number} current Current time is seconds. + * @param {number} duration Duration in seconds. + * @private + */ +MediaControls.prototype.displayProgress_ = function(current, duration) { + var ratio = current / duration; + this.progressSlider_.setValue(ratio); + this.currentTime_.textContent = MediaControls.formatTime_(current); +}; + +/** + * @param {number} value Progress [0..1]. + * @private + */ +MediaControls.prototype.onProgressChange_ = function(value) { + if (!this.media_.seekable || !this.media_.duration) { + console.error('Inconsistent media state'); + return; + } + + var current = this.media_.duration * value; + this.media_.currentTime = current; + this.currentTime_.textContent = MediaControls.formatTime_(current); +}; + +/** + * @param {boolean} on True if dragging. + * @private + */ +MediaControls.prototype.onProgressDrag_ = function(on) { + if (on) { + this.resumeAfterDrag_ = this.isPlaying(); + this.media_.pause(); + } else { + if (this.resumeAfterDrag_) { + if (this.media_.ended) + this.onMediaPlay_(false); + else + this.media_.play(); + } + this.updatePlayButtonState_(this.isPlaying()); + } +}; + +/* + * Volume controls + */ + +/** + * @param {HTMLElement=} opt_parent Parent element for the controls. + */ +MediaControls.prototype.initVolumeControls = function(opt_parent) { + var volumeControls = this.createControl('volume-controls', opt_parent); + + this.soundButton_ = this.createButton('sound media-control', + this.onSoundButtonClick_.bind(this), volumeControls); + this.soundButton_.setAttribute('level', 3); // max level. + + this.volume_ = new MediaControls.AnimatedSlider( + this.createControl('volume media-control', volumeControls), + 1, /* value */ + 100 /* range */, + this.onVolumeChange_.bind(this), + this.onVolumeDrag_.bind(this)); +}; + +/** + * Click handler for the sound level button. + * @private + */ +MediaControls.prototype.onSoundButtonClick_ = function() { + if (this.media_.volume == 0) { + this.volume_.setValue(this.savedVolume_ || 1); + } else { + this.savedVolume_ = this.media_.volume; + this.volume_.setValue(0); + } + this.onVolumeChange_(this.volume_.getValue()); +}; + +/** + * @param {number} value Volume [0..1]. + * @return {number} The rough level [0..3] used to pick an icon. + * @private + */ +MediaControls.getVolumeLevel_ = function(value) { + if (value == 0) return 0; + if (value <= 1 / 3) return 1; + if (value <= 2 / 3) return 2; + return 3; +}; + +/** + * @param {number} value Volume [0..1]. + * @private + */ +MediaControls.prototype.onVolumeChange_ = function(value) { + this.media_.volume = value; + this.soundButton_.setAttribute('level', MediaControls.getVolumeLevel_(value)); +}; + +/** + * @param {boolean} on True if dragging is in progress. + * @private + */ +MediaControls.prototype.onVolumeDrag_ = function(on) { + if (on && (this.media_.volume != 0)) { + this.savedVolume_ = this.media_.volume; + } +}; + +/* + * Media event handlers. + */ + +/** + * Attach a media element. + * + * @param {HTMLMediaElement} mediaElement The media element to control. + */ +MediaControls.prototype.attachMedia = function(mediaElement) { + this.media_ = mediaElement; + + this.media_.addEventListener('play', this.onMediaPlayBound_); + this.media_.addEventListener('pause', this.onMediaPauseBound_); + this.media_.addEventListener('durationchange', this.onMediaDurationBound_); + this.media_.addEventListener('timeupdate', this.onMediaProgressBound_); + this.media_.addEventListener('error', this.onMediaError_); + + // Reflect the media state in the UI. + this.onMediaDuration_(); + this.onMediaPlay_(this.isPlaying()); + this.onMediaProgress_(); + if (this.volume_) { + /* Copy the user selected volume to the new media element. */ + this.media_.volume = this.volume_.getValue(); + } +}; + +/** + * Detach media event handlers. + */ +MediaControls.prototype.detachMedia = function() { + if (!this.media_) + return; + + this.media_.removeEventListener('play', this.onMediaPlayBound_); + this.media_.removeEventListener('pause', this.onMediaPauseBound_); + this.media_.removeEventListener('durationchange', this.onMediaDurationBound_); + this.media_.removeEventListener('timeupdate', this.onMediaProgressBound_); + this.media_.removeEventListener('error', this.onMediaError_); + + this.media_ = null; +}; + +/** + * Force-empty the media pipeline. This is a workaround for crbug.com/149957. + * The document is not going to be GC-ed until the last Files app window closes, + * but we want the media pipeline to deinitialize ASAP to minimize leakage. + */ +MediaControls.prototype.cleanup = function() { + this.media_.src = ''; + this.media_.load(); + this.detachMedia(); +}; + +/** + * 'play' and 'pause' event handler. + * @param {boolean} playing True if playing. + * @private + */ +MediaControls.prototype.onMediaPlay_ = function(playing) { + if (this.progressSlider_.isDragging()) + return; + + this.updatePlayButtonState_(playing); + this.onPlayStateChanged(); +}; + +/** + * 'durationchange' event handler. + * @private + */ +MediaControls.prototype.onMediaDuration_ = function() { + if (!this.media_.duration) { + this.enableControls_('.media-control', false); + return; + } + + this.enableControls_('.media-control', true); + + var sliderContainer = this.progressSlider_.getContainer(); + if (this.media_.seekable) + sliderContainer.classList.remove('readonly'); + else + sliderContainer.classList.add('readonly'); + + var valueToString = function(value) { + return MediaControls.formatTime_(this.media_.duration * value); + }.bind(this); + + this.duration_.textContent = valueToString(1); + + if (this.progressSlider_.setValueToStringFunction) + this.progressSlider_.setValueToStringFunction(valueToString); + + if (this.media_.seekable) + this.restorePlayState(); +}; + +/** + * 'timeupdate' event handler. + * @private + */ +MediaControls.prototype.onMediaProgress_ = function() { + if (!this.media_.duration) { + this.displayProgress_(0, 1); + return; + } + + var current = this.media_.currentTime; + var duration = this.media_.duration; + + if (this.progressSlider_.isDragging()) + return; + + this.displayProgress_(current, duration); + + if (current == duration) { + this.onMediaComplete(); + } + this.onPlayStateChanged(); +}; + +/** + * Called when the media playback is complete. + */ +MediaControls.prototype.onMediaComplete = function() {}; + +/** + * Called when play/pause state is changed or on playback progress. + * This is the right moment to save the play state. + */ +MediaControls.prototype.onPlayStateChanged = function() {}; + +/** + * Updates the play button state. + * @param {boolean} playing If the video is playing. + * @private + */ +MediaControls.prototype.updatePlayButtonState_ = function(playing) { + if (playing) { + this.playButton_.setAttribute('state', + MediaControls.ButtonStateType.PLAYING); + } else if (!this.media_.ended) { + this.playButton_.setAttribute('state', + MediaControls.ButtonStateType.DEFAULT); + } else { + this.playButton_.setAttribute('state', + MediaControls.ButtonStateType.ENDED); + } +}; + +/** + * Restore play state. Base implementation is empty. + */ +MediaControls.prototype.restorePlayState = function() {}; + +/** + * Encode current state into the page URL or the app state. + */ +MediaControls.prototype.encodeState = function() { + if (!this.media_.duration) + return; + + if (window.appState) { + window.appState.time = this.media_.currentTime; + util.saveAppState(); + return; + } + + var playState = JSON.stringify({ + play: this.isPlaying(), + time: this.media_.currentTime + }); + + var newLocation = document.location.origin + document.location.pathname + + document.location.search + '#' + playState; + + document.location.href = newLocation; +}; + +/** + * Decode current state from the page URL or the app state. + * @return {boolean} True if decode succeeded. + */ +MediaControls.prototype.decodeState = function() { + if (window.appState) { + if (!('time' in window.appState)) + return false; + // There is no page reload for apps v2, only app restart. + // Always restart in paused state. + this.media_.currentTime = appState.time; + this.pause(); + return true; + } + + var hash = document.location.hash.substring(1); + if (hash) { + try { + var playState = JSON.parse(hash); + if (!('time' in playState)) + return false; + + this.media_.currentTime = playState.time; + + if (playState.play) + this.play(); + else + this.pause(); + + return true; + } catch (e) { + console.warn('Cannot decode play state'); + } + } + return false; +}; + +/** + * Remove current state from the page URL or the app state. + */ +MediaControls.prototype.clearState = function() { + if (window.appState) { + if ('time' in window.appState) + delete window.appState.time; + util.saveAppState(); + return; + } + + var newLocation = document.location.origin + document.location.pathname + + document.location.search + '#'; + + document.location.href = newLocation; +}; + +/** + * Create a customized slider control. + * + * @param {HTMLElement} container The containing div element. + * @param {number} value Initial value [0..1]. + * @param {number} range Number of distinct slider positions to be supported. + * @param {function(number)} onChange Value change handler. + * @param {function(boolean)} onDrag Drag begin/end handler. + * @constructor + */ + +MediaControls.Slider = function(container, value, range, onChange, onDrag) { + this.container_ = container; + this.onChange_ = onChange; + this.onDrag_ = onDrag; + + var document = this.container_.ownerDocument; + + this.container_.classList.add('custom-slider'); + + this.input_ = document.createElement('input'); + this.input_.type = 'range'; + this.input_.min = 0; + this.input_.max = range; + this.input_.value = value * range; + this.container_.appendChild(this.input_); + + this.input_.addEventListener( + 'change', this.onInputChange_.bind(this)); + this.input_.addEventListener( + 'mousedown', this.onInputDrag_.bind(this, true)); + this.input_.addEventListener( + 'mouseup', this.onInputDrag_.bind(this, false)); + + this.bar_ = document.createElement('div'); + this.bar_.className = 'bar'; + this.container_.appendChild(this.bar_); + + this.filled_ = document.createElement('div'); + this.filled_.className = 'filled'; + this.bar_.appendChild(this.filled_); + + var leftCap = document.createElement('div'); + leftCap.className = 'cap left'; + this.bar_.appendChild(leftCap); + + var rightCap = document.createElement('div'); + rightCap.className = 'cap right'; + this.bar_.appendChild(rightCap); + + this.value_ = value; + this.setFilled_(value); +}; + +/** + * @return {HTMLElement} The container element. + */ +MediaControls.Slider.prototype.getContainer = function() { + return this.container_; +}; + +/** + * @return {HTMLElement} The standard input element. + * @private + */ +MediaControls.Slider.prototype.getInput_ = function() { + return this.input_; +}; + +/** + * @return {HTMLElement} The slider bar element. + */ +MediaControls.Slider.prototype.getBar = function() { + return this.bar_; +}; + +/** + * @return {number} [0..1] The current value. + */ +MediaControls.Slider.prototype.getValue = function() { + return this.value_; +}; + +/** + * @param {number} value [0..1]. + */ +MediaControls.Slider.prototype.setValue = function(value) { + this.value_ = value; + this.setValueToUI_(value); +}; + +/** + * Fill the given proportion the slider bar (from the left). + * + * @param {number} proportion [0..1]. + * @private + */ +MediaControls.Slider.prototype.setFilled_ = function(proportion) { + this.filled_.style.width = proportion * 100 + '%'; +}; + +/** + * Get the value from the input element. + * + * @return {number} Value [0..1]. + * @private + */ +MediaControls.Slider.prototype.getValueFromUI_ = function() { + return this.input_.value / this.input_.max; +}; + +/** + * Update the UI with the current value. + * + * @param {number} value [0..1]. + * @private + */ +MediaControls.Slider.prototype.setValueToUI_ = function(value) { + this.input_.value = value * this.input_.max; + this.setFilled_(value); +}; + +/** + * Compute the proportion in which the given position divides the slider bar. + * + * @param {number} position in pixels. + * @return {number} [0..1] proportion. + */ +MediaControls.Slider.prototype.getProportion = function(position) { + var rect = this.bar_.getBoundingClientRect(); + return Math.max(0, Math.min(1, (position - rect.left) / rect.width)); +}; + +/** + * 'change' event handler. + * @private + */ +MediaControls.Slider.prototype.onInputChange_ = function() { + this.value_ = this.getValueFromUI_(); + this.setFilled_(this.value_); + this.onChange_(this.value_); +}; + +/** + * @return {boolean} True if dragging is in progress. + */ +MediaControls.Slider.prototype.isDragging = function() { + return this.isDragging_; +}; + +/** + * Mousedown/mouseup handler. + * @param {boolean} on True if the mouse is down. + * @private + */ +MediaControls.Slider.prototype.onInputDrag_ = function(on) { + this.isDragging_ = on; + this.onDrag_(on); +}; + +/** + * Create a customized slider with animated thumb movement. + * + * @param {HTMLElement} container The containing div element. + * @param {number} value Initial value [0..1]. + * @param {number} range Number of distinct slider positions to be supported. + * @param {function(number)} onChange Value change handler. + * @param {function(boolean)} onDrag Drag begin/end handler. + * @param {function(number):string} formatFunction Value formatting function. + * @constructor + */ +MediaControls.AnimatedSlider = function( + container, value, range, onChange, onDrag, formatFunction) { + MediaControls.Slider.apply(this, arguments); +}; + +MediaControls.AnimatedSlider.prototype = { + __proto__: MediaControls.Slider.prototype +}; + +/** + * Number of animation steps. + */ +MediaControls.AnimatedSlider.STEPS = 10; + +/** + * Animation duration. + */ +MediaControls.AnimatedSlider.DURATION = 100; + +/** + * @param {number} value [0..1]. + * @private + */ +MediaControls.AnimatedSlider.prototype.setValueToUI_ = function(value) { + if (this.animationInterval_) { + clearInterval(this.animationInterval_); + } + var oldValue = this.getValueFromUI_(); + var step = 0; + this.animationInterval_ = setInterval(function() { + step++; + var currentValue = oldValue + + (value - oldValue) * (step / MediaControls.AnimatedSlider.STEPS); + MediaControls.Slider.prototype.setValueToUI_.call(this, currentValue); + if (step == MediaControls.AnimatedSlider.STEPS) { + clearInterval(this.animationInterval_); + } + }.bind(this), + MediaControls.AnimatedSlider.DURATION / MediaControls.AnimatedSlider.STEPS); +}; + +/** + * Create a customized slider with a precise time feedback. + * + * The time value is shown above the slider bar at the mouse position. + * + * @param {HTMLElement} container The containing div element. + * @param {number} value Initial value [0..1]. + * @param {number} range Number of distinct slider positions to be supported. + * @param {function(number)} onChange Value change handler. + * @param {function(boolean)} onDrag Drag begin/end handler. + * @param {function(number):string} formatFunction Value formatting function. + * @constructor + */ +MediaControls.PreciseSlider = function( + container, value, range, onChange, onDrag, formatFunction) { + MediaControls.Slider.apply(this, arguments); + + var doc = this.container_.ownerDocument; + + /** + * @type {function(number):string} + * @private + */ + this.valueToString_ = null; + + this.seekMark_ = doc.createElement('div'); + this.seekMark_.className = 'seek-mark'; + this.getBar().appendChild(this.seekMark_); + + this.seekLabel_ = doc.createElement('div'); + this.seekLabel_.className = 'seek-label'; + this.seekMark_.appendChild(this.seekLabel_); + + this.getContainer().addEventListener( + 'mousemove', this.onMouseMove_.bind(this)); + this.getContainer().addEventListener( + 'mouseout', this.onMouseOut_.bind(this)); +}; + +MediaControls.PreciseSlider.prototype = { + __proto__: MediaControls.Slider.prototype +}; + +/** + * Show the seek mark after a delay. + */ +MediaControls.PreciseSlider.SHOW_DELAY = 200; + +/** + * Hide the seek mark for this long after changing the position with a click. + */ +MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY = 2500; + +/** + * Hide the seek mark for this long after changing the position with a drag. + */ +MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY = 750; + +/** + * Default hide timeout (no hiding). + */ +MediaControls.PreciseSlider.NO_AUTO_HIDE = 0; + +/** + * @param {function(number):string} func Value formatting function. + */ +MediaControls.PreciseSlider.prototype.setValueToStringFunction = + function(func) { + this.valueToString_ = func; + + /* It is not completely accurate to assume that the max value corresponds + to the longest string, but generous CSS padding will compensate for that. */ + var labelWidth = this.valueToString_(1).length / 2 + 1; + this.seekLabel_.style.width = labelWidth + 'em'; + this.seekLabel_.style.marginLeft = -labelWidth / 2 + 'em'; +}; + +/** + * Show the time above the slider. + * + * @param {number} ratio [0..1] The proportion of the duration. + * @param {number} timeout Timeout in ms after which the label should be hidden. + * MediaControls.PreciseSlider.NO_AUTO_HIDE means show until the next call. + * @private + */ +MediaControls.PreciseSlider.prototype.showSeekMark_ = + function(ratio, timeout) { + // Do not update the seek mark for the first 500ms after the drag is finished. + if (this.latestMouseUpTime_ && (this.latestMouseUpTime_ + 500 > Date.now())) + return; + + this.seekMark_.style.left = ratio * 100 + '%'; + + if (ratio < this.getValue()) { + this.seekMark_.classList.remove('inverted'); + } else { + this.seekMark_.classList.add('inverted'); + } + this.seekLabel_.textContent = this.valueToString_(ratio); + + this.seekMark_.classList.add('visible'); + + if (this.seekMarkTimer_) { + clearTimeout(this.seekMarkTimer_); + this.seekMarkTimer_ = null; + } + if (timeout != MediaControls.PreciseSlider.NO_AUTO_HIDE) { + this.seekMarkTimer_ = setTimeout(this.hideSeekMark_.bind(this), timeout); + } +}; + +/** + * @private + */ +MediaControls.PreciseSlider.prototype.hideSeekMark_ = function() { + this.seekMarkTimer_ = null; + this.seekMark_.classList.remove('visible'); +}; + +/** + * 'mouseout' event handler. + * @param {Event} e Event. + * @private + */ +MediaControls.PreciseSlider.prototype.onMouseMove_ = function(e) { + this.latestSeekRatio_ = this.getProportion(e.clientX); + + var self = this; + function showMark() { + if (!self.isDragging()) { + self.showSeekMark_(self.latestSeekRatio_, + MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY); + } + } + + if (this.seekMark_.classList.contains('visible')) { + showMark(); + } else if (!this.seekMarkTimer_) { + this.seekMarkTimer_ = + setTimeout(showMark, MediaControls.PreciseSlider.SHOW_DELAY); + } +}; + +/** + * 'mouseout' event handler. + * @param {Event} e Event. + * @private + */ +MediaControls.PreciseSlider.prototype.onMouseOut_ = function(e) { + for (var element = e.relatedTarget; element; element = element.parentNode) { + if (element == this.getContainer()) + return; + } + if (this.seekMarkTimer_) { + clearTimeout(this.seekMarkTimer_); + this.seekMarkTimer_ = null; + } + this.hideSeekMark_(); +}; + +/** + * 'change' event handler. + * @private + */ +MediaControls.PreciseSlider.prototype.onInputChange_ = function() { + MediaControls.Slider.prototype.onInputChange_.apply(this, arguments); + if (this.isDragging()) { + this.showSeekMark_( + this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE); + } +}; + +/** + * Mousedown/mouseup handler. + * @param {boolean} on True if the mouse is down. + * @private + */ +MediaControls.PreciseSlider.prototype.onInputDrag_ = function(on) { + MediaControls.Slider.prototype.onInputDrag_.apply(this, arguments); + + if (on) { + // Dragging started, align the seek mark with the thumb position. + this.showSeekMark_( + this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE); + } else { + // Just finished dragging. + // Show the label for the last time with a shorter timeout. + this.showSeekMark_( + this.getValue(), MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY); + this.latestMouseUpTime_ = Date.now(); + } +}; + +/** + * Create video controls. + * + * @param {HTMLElement} containerElement The container for the controls. + * @param {function} onMediaError Function to display an error message. + * @param {function(string):string} stringFunction Function providing localized + * strings. + * @param {function=} opt_fullScreenToggle Function to toggle fullscreen mode. + * @param {HTMLElement=} opt_stateIconParent The parent for the icon that + * gives visual feedback when the playback state changes. + * @constructor + */ +function VideoControls(containerElement, onMediaError, stringFunction, + opt_fullScreenToggle, opt_stateIconParent) { + MediaControls.call(this, containerElement, onMediaError); + this.stringFunction_ = stringFunction; + + this.container_.classList.add('video-controls'); + this.initPlayButton(); + this.initTimeControls(true /* show seek mark */); + this.initVolumeControls(); + + if (opt_fullScreenToggle) { + this.fullscreenButton_ = + this.createButton('fullscreen', opt_fullScreenToggle); + } + + if (opt_stateIconParent) { + this.stateIcon_ = this.createControl( + 'playback-state-icon', opt_stateIconParent); + this.textBanner_ = this.createControl('text-banner', opt_stateIconParent); + } + + var videoControls = this; + chrome.mediaPlayerPrivate.onTogglePlayState.addListener( + function() { videoControls.togglePlayStateWithFeedback(); }); +} + +/** + * No resume if we are within this margin from the start or the end. + */ +VideoControls.RESUME_MARGIN = 0.03; + +/** + * No resume for videos shorter than this. + */ +VideoControls.RESUME_THRESHOLD = 5 * 60; // 5 min. + +/** + * When resuming rewind back this much. + */ +VideoControls.RESUME_REWIND = 5; // seconds. + +VideoControls.prototype = { __proto__: MediaControls.prototype }; + +/** + * Shows icon feedback for the current state of the video player. + * @private + */ +VideoControls.prototype.showIconFeedback_ = function() { + this.stateIcon_.removeAttribute('state'); + setTimeout(function() { + this.stateIcon_.setAttribute('state', this.isPlaying() ? 'play' : 'pause'); + }.bind(this), 0); +}; + +/** + * Shows a text banner. + * + * @param {string} identifier String identifier. + * @private + */ +VideoControls.prototype.showTextBanner_ = function(identifier) { + this.textBanner_.removeAttribute('visible'); + this.textBanner_.textContent = this.stringFunction_(identifier); + setTimeout(function() { + this.textBanner_.setAttribute('visible', 'true'); + }.bind(this), 0); +}; + +/** + * Toggle play/pause state on a mouse click on the play/pause button. Can be + * called externally. + * + * @param {Event} event Mouse click event. + */ +VideoControls.prototype.onPlayButtonClicked = function(event) { + if (event.ctrlKey) { + this.toggleLoopedModeWithFeedback(true); + if (!this.isPlaying()) + this.togglePlayState(); + } else { + this.togglePlayState(); + } +}; + +/** + * Media completion handler. + */ +VideoControls.prototype.onMediaComplete = function() { + this.onMediaPlay_(false); // Just update the UI. + this.savePosition(); // This will effectively forget the position. +}; + +/** + * Toggles the looped mode with feedback. + * @param {boolean} on Whether enabled or not. + */ +VideoControls.prototype.toggleLoopedModeWithFeedback = function(on) { + if (!this.getMedia().duration) + return; + this.toggleLoopedMode(on); + if (on) { + // TODO(mtomasz): Simplify, crbug.com/254318. + this.showTextBanner_('GALLERY_VIDEO_LOOPED_MODE'); + } +}; + +/** + * Toggles the looped mode. + * @param {boolean} on Whether enabled or not. + */ +VideoControls.prototype.toggleLoopedMode = function(on) { + this.getMedia().loop = on; +}; + +/** + * Toggles play/pause state and flash an icon over the video. + */ +VideoControls.prototype.togglePlayStateWithFeedback = function() { + if (!this.getMedia().duration) + return; + + this.togglePlayState(); + this.showIconFeedback_(); +}; + +/** + * Toggles play/pause state. + */ +VideoControls.prototype.togglePlayState = function() { + if (this.isPlaying()) { + // User gave the Pause command. Save the state and reset the loop mode. + this.toggleLoopedMode(false); + this.savePosition(); + } + MediaControls.prototype.togglePlayState.apply(this, arguments); +}; + +/** + * Saves the playback position to the persistent storage. + * @param {boolean=} opt_sync True if the position must be saved synchronously + * (required when closing app windows). + */ +VideoControls.prototype.savePosition = function(opt_sync) { + if (!this.media_.duration || + this.media_.duration < VideoControls.RESUME_THRESHOLD) { + return; + } + + var ratio = this.media_.currentTime / this.media_.duration; + var position; + if (ratio < VideoControls.RESUME_MARGIN || + ratio > (1 - VideoControls.RESUME_MARGIN)) { + // We are too close to the beginning or the end. + // Remove the resume position so that next time we start from the beginning. + position = null; + } else { + position = Math.floor( + Math.max(0, this.media_.currentTime - VideoControls.RESUME_REWIND)); + } + + if (opt_sync) { + // Packaged apps cannot save synchronously. + // Pass the data to the background page. + if (!window.saveOnExit) + window.saveOnExit = []; + window.saveOnExit.push({ key: this.media_.src, value: position }); + } else { + util.AppCache.update(this.media_.src, position); + } +}; + +/** + * Resumes the playback position saved in the persistent storage. + */ +VideoControls.prototype.restorePlayState = function() { + if (this.media_.duration >= VideoControls.RESUME_THRESHOLD) { + util.AppCache.getValue(this.media_.src, function(position) { + if (position) + this.media_.currentTime = position; + }.bind(this)); + } +}; + +/** + * Updates style to best fit the size of the container. + */ +VideoControls.prototype.updateStyle = function() { + // We assume that the video controls element fills the parent container. + // This is easier than adding margins to this.container_.clientWidth. + var width = this.container_.parentNode.clientWidth; + + // Set the margin to 5px for width >= 400, 0px for width < 160, + // interpolate linearly in between. + this.container_.style.margin = + Math.ceil((Math.max(160, Math.min(width, 400)) - 160) / 48) + 'px'; + + var hideBelow = function(selector, limit) { + this.container_.querySelector(selector).style.display = + width < limit ? 'none' : '-webkit-box'; + }.bind(this); + + hideBelow('.time', 350); + hideBelow('.volume', 275); + hideBelow('.volume-controls', 210); + hideBelow('.fullscreen', 150); +}; + +/** + * Creates audio controls. + * + * @param {HTMLElement} container Parent container. + * @param {function(boolean)} advanceTrack Parameter: true=forward. + * @param {function} onError Error handler. + * @constructor + */ +function AudioControls(container, advanceTrack, onError) { + MediaControls.call(this, container, onError); + + this.container_.classList.add('audio-controls'); + + this.advanceTrack_ = advanceTrack; + + this.initPlayButton(); + this.initTimeControls(false /* no seek mark */); + /* No volume controls */ + this.createButton('previous', this.onAdvanceClick_.bind(this, false)); + this.createButton('next', this.onAdvanceClick_.bind(this, true)); + + var audioControls = this; + chrome.mediaPlayerPrivate.onNextTrack.addListener( + function() { audioControls.onAdvanceClick_(true); }); + chrome.mediaPlayerPrivate.onPrevTrack.addListener( + function() { audioControls.onAdvanceClick_(false); }); + chrome.mediaPlayerPrivate.onTogglePlayState.addListener( + function() { audioControls.togglePlayState(); }); +} + +AudioControls.prototype = { __proto__: MediaControls.prototype }; + +/** + * Media completion handler. Advances to the next track. + */ +AudioControls.prototype.onMediaComplete = function() { + this.advanceTrack_(true); +}; + +/** + * The track position after which "previous" button acts as "restart". + */ +AudioControls.TRACK_RESTART_THRESHOLD = 5; // seconds. + +/** + * @param {boolean} forward True if advancing forward. + * @private + */ +AudioControls.prototype.onAdvanceClick_ = function(forward) { + if (!forward && + (this.getMedia().currentTime > AudioControls.TRACK_RESTART_THRESHOLD)) { + // We are far enough from the beginning of the current track. + // Restart it instead of than skipping to the previous one. + this.getMedia().currentTime = 0; + } else { + this.advanceTrack_(forward); + } +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/media/media_util.js b/chromium/chrome/browser/resources/file_manager/foreground/js/media/media_util.js new file mode 100644 index 00000000000..a4c28348048 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/media/media_util.js @@ -0,0 +1,421 @@ +// 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'; + +/** + * Loads a thumbnail using provided url. In CANVAS mode, loaded images + * are attached as <canvas> element, while in IMAGE mode as <img>. + * <canvas> renders faster than <img>, however has bigger memory overhead. + * + * @param {string} url File URL. + * @param {ThumbnailLoader.LoaderType=} opt_loaderType Canvas or Image loader, + * default: IMAGE. + * @param {Object=} opt_metadata Metadata object. + * @param {string=} opt_mediaType Media type. + * @param {ThumbnailLoader.UseEmbedded=} opt_useEmbedded If to use embedded + * jpeg thumbnail if available. Default: USE_EMBEDDED. + * @param {number=} opt_priority Priority, the highest is 0. default: 2. + * @constructor + */ +function ThumbnailLoader(url, opt_loaderType, opt_metadata, opt_mediaType, + opt_useEmbedded, opt_priority) { + opt_useEmbedded = opt_useEmbedded || ThumbnailLoader.UseEmbedded.USE_EMBEDDED; + + this.mediaType_ = opt_mediaType || FileType.getMediaType(url); + this.loaderType_ = opt_loaderType || ThumbnailLoader.LoaderType.IMAGE; + this.metadata_ = opt_metadata; + this.priority_ = (opt_priority !== undefined) ? opt_priority : 2; + this.transform_ = null; + + if (!opt_metadata) { + this.thumbnailUrl_ = url; // Use the URL directly. + return; + } + + this.fallbackUrl_ = null; + this.thumbnailUrl_ = null; + if (opt_metadata.drive && opt_metadata.drive.customIconUrl) + this.fallbackUrl_ = opt_metadata.drive.customIconUrl; + + // Fetch the rotation from the Drive metadata (if available). + var driveTransform; + if (opt_metadata.drive && opt_metadata.drive.imageRotation !== undefined) { + driveTransform = { + scaleX: 1, + scaleY: 1, + rotate90: opt_metadata.drive.imageRotation / 90 + }; + } + + if (opt_metadata.thumbnail && opt_metadata.thumbnail.url && + opt_useEmbedded == ThumbnailLoader.UseEmbedded.USE_EMBEDDED) { + this.thumbnailUrl_ = opt_metadata.thumbnail.url; + this.transform_ = driveTransform !== undefined ? driveTransform : + opt_metadata.thumbnail.transform; + } else if (FileType.isImage(url)) { + this.thumbnailUrl_ = url; + this.transform_ = driveTransform !== undefined ? driveTransform : + opt_metadata.media && opt_metadata.media.imageTransform; + } else if (this.fallbackUrl_) { + // Use fallback as the primary thumbnail. + this.thumbnailUrl_ = this.fallbackUrl_; + this.fallbackUrl_ = null; + } // else the generic thumbnail based on the media type will be used. +} + +/** + * In percents (0.0 - 1.0), how much area can be cropped to fill an image + * in a container, when loading a thumbnail in FillMode.AUTO mode. + * The specified 30% value allows to fill 16:9, 3:2 pictures in 4:3 element. + * @type {number} + */ +ThumbnailLoader.AUTO_FILL_THRESHOLD = 0.3; + +/** + * Type of displaying a thumbnail within a box. + * @enum {number} + */ +ThumbnailLoader.FillMode = { + FILL: 0, // Fill whole box. Image may be cropped. + FIT: 1, // Keep aspect ratio, do not crop. + OVER_FILL: 2, // Fill whole box with possible stretching. + AUTO: 3 // Try to fill, but if incompatible aspect ratio, then fit. +}; + +/** + * Optimization mode for downloading thumbnails. + * @enum {number} + */ +ThumbnailLoader.OptimizationMode = { + NEVER_DISCARD: 0, // Never discards downloading. No optimization. + DISCARD_DETACHED: 1 // Canceled if the container is not attached anymore. +}; + +/** + * Type of element to store the image. + * @enum {number} + */ +ThumbnailLoader.LoaderType = { + IMAGE: 0, + CANVAS: 1 +}; + +/** + * Whether to use the embedded thumbnail, or not. The embedded thumbnail may + * be small. + * @enum {number} + */ +ThumbnailLoader.UseEmbedded = { + USE_EMBEDDED: 0, + NO_EMBEDDED: 1 +}; + +/** + * Maximum thumbnail's width when generating from the full resolution image. + * @const + * @type {number} + */ +ThumbnailLoader.THUMBNAIL_MAX_WIDTH = 500; + +/** + * Maximum thumbnail's height when generating from the full resolution image. + * @const + * @type {number} + */ +ThumbnailLoader.THUMBNAIL_MAX_HEIGHT = 500; + +/** + * Loads and attaches an image. + * + * @param {HTMLElement} box Container element. + * @param {ThumbnailLoader.FillMode} fillMode Fill mode. + * @param {ThumbnailLoader.OptimizationMode=} opt_optimizationMode Optimization + * for downloading thumbnails. By default optimizations are disabled. + * @param {function(Image, Object)} opt_onSuccess Success callback, + * accepts the image and the transform. + * @param {function} opt_onError Error callback. + * @param {function} opt_onGeneric Callback for generic image used. + */ +ThumbnailLoader.prototype.load = function(box, fillMode, opt_optimizationMode, + opt_onSuccess, opt_onError, opt_onGeneric) { + opt_optimizationMode = opt_optimizationMode || + ThumbnailLoader.OptimizationMode.NEVER_DISCARD; + + if (!this.thumbnailUrl_) { + // Relevant CSS rules are in file_types.css. + box.setAttribute('generic-thumbnail', this.mediaType_); + if (opt_onGeneric) opt_onGeneric(); + return; + } + + this.cancel(); + this.canvasUpToDate_ = false; + this.image_ = new Image(); + this.image_.onload = function() { + this.attachImage(box, fillMode); + if (opt_onSuccess) + opt_onSuccess(this.image_, this.transform_); + }.bind(this); + this.image_.onerror = function() { + if (opt_onError) + opt_onError(); + if (this.fallbackUrl_) { + new ThumbnailLoader(this.fallbackUrl_, + this.loaderType_, + null, // No metadata. + this.mediaType_, + undefined, // Default value for use-embedded. + this.priority_). + load(box, fillMode, opt_optimizationMode, opt_onSuccess); + } else { + box.setAttribute('generic-thumbnail', this.mediaType_); + } + }.bind(this); + + if (this.image_.src) { + console.warn('Thumbnail already loaded: ' + this.thumbnailUrl_); + return; + } + + // TODO(mtomasz): Smarter calculation of the requested size. + var wasAttached = box.ownerDocument.contains(box); + var modificationTime = this.metadata_ && + this.metadata_.filesystem && + this.metadata_.filesystem.modificationTime && + this.metadata_.filesystem.modificationTime.getTime(); + this.taskId_ = util.loadImage( + this.image_, + this.thumbnailUrl_, + { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH, + maxHeight: ThumbnailLoader.THUMBNAIL_MAX_HEIGHT, + cache: true, + priority: this.priority_, + timestamp: modificationTime }, + function() { + if (opt_optimizationMode == + ThumbnailLoader.OptimizationMode.DISCARD_DETACHED && + !box.ownerDocument.contains(box)) { + // If the container is not attached, then invalidate the download. + return false; + } + return true; + }); +}; + +/** + * Cancels loading the current image. + */ +ThumbnailLoader.prototype.cancel = function() { + if (this.taskId_) { + this.image_.onload = function() {}; + this.image_.onerror = function() {}; + util.cancelLoadImage(this.taskId_); + this.taskId_ = null; + } +}; + +/** + * @return {boolean} True if a valid image is loaded. + */ +ThumbnailLoader.prototype.hasValidImage = function() { + return !!(this.image_ && this.image_.width && this.image_.height); +}; + +/** + * @return {boolean} True if the image is rotated 90 degrees left or right. + * @private + */ +ThumbnailLoader.prototype.isRotated_ = function() { + return this.transform_ && (this.transform_.rotate90 % 2 == 1); +}; + +/** + * @return {number} Image width (corrected for rotation). + */ +ThumbnailLoader.prototype.getWidth = function() { + return this.isRotated_() ? this.image_.height : this.image_.width; +}; + +/** + * @return {number} Image height (corrected for rotation). + */ +ThumbnailLoader.prototype.getHeight = function() { + return this.isRotated_() ? this.image_.width : this.image_.height; +}; + +/** + * Load an image but do not attach it. + * + * @param {function(boolean)} callback Callback, parameter is true if the image + * has loaded successfully or a stock icon has been used. + */ +ThumbnailLoader.prototype.loadDetachedImage = function(callback) { + if (!this.thumbnailUrl_) { + callback(true); + return; + } + + this.cancel(); + this.canvasUpToDate_ = false; + this.image_ = new Image(); + this.image_.onload = callback.bind(null, true); + this.image_.onerror = callback.bind(null, false); + + // TODO(mtomasz): Smarter calculation of the requested size. + var modificationTime = this.metadata_ && + this.metadata_.filesystem && + this.metadata_.filesystem.modificationTime && + this.metadata_.filesystem.modificationTime.getTime(); + this.taskId_ = util.loadImage( + this.image_, + this.thumbnailUrl_, + { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH, + maxHeight: ThumbnailLoader.THUMBNAIL_MAX_HEIGHT, + cache: true, + priority: this.priority_, + timestamp: modificationTime }); +}; + +/** + * Renders the thumbnail into either canvas or an image element. + * @private + */ +ThumbnailLoader.prototype.renderMedia_ = function() { + if (this.loaderType_ != ThumbnailLoader.LoaderType.CANVAS) + return; + + if (!this.canvas_) + this.canvas_ = document.createElement('canvas'); + + // Copy the image to a canvas if the canvas is outdated. + if (!this.canvasUpToDate_) { + this.canvas_.width = this.image_.width; + this.canvas_.height = this.image_.height; + var context = this.canvas_.getContext('2d'); + context.drawImage(this.image_, 0, 0); + this.canvasUpToDate_ = true; + } +}; + +/** + * Attach the image to a given element. + * @param {Element} container Parent element. + * @param {ThumbnailLoader.FillMode} fillMode Fill mode. + */ +ThumbnailLoader.prototype.attachImage = function(container, fillMode) { + if (!this.hasValidImage()) { + container.setAttribute('generic-thumbnail', this.mediaType_); + return; + } + + this.renderMedia_(); + util.applyTransform(container, this.transform_); + var attachableMedia = this.loaderType_ == ThumbnailLoader.LoaderType.CANVAS ? + this.canvas_ : this.image_; + + ThumbnailLoader.centerImage_( + container, attachableMedia, fillMode, this.isRotated_()); + + if (attachableMedia.parentNode != container) { + container.textContent = ''; + container.appendChild(attachableMedia); + } + + if (!this.taskId_) + attachableMedia.classList.add('cached'); +}; + +/** + * Gets the loaded image. + * TODO(mtomasz): Apply transformations. + * + * @return {Image|HTMLCanvasElement} Either image or a canvas object. + */ +ThumbnailLoader.prototype.getImage = function() { + this.renderMedia_(); + return this.loaderType_ == ThumbnailLoader.LoaderType.CANVAS ? this.canvas_ : + this.image_; +}; + +/** + * Update the image style to fit/fill the container. + * + * Using webkit center packing does not align the image properly, so we need + * to wait until the image loads and its dimensions are known, then manually + * position it at the center. + * + * @param {HTMLElement} box Containing element. + * @param {Image|HTMLCanvasElement} img Element containing an image. + * @param {ThumbnailLoader.FillMode} fillMode Fill mode. + * @param {boolean} rotate True if the image should be rotated 90 degrees. + * @private + */ +ThumbnailLoader.centerImage_ = function(box, img, fillMode, rotate) { + var imageWidth = img.width; + var imageHeight = img.height; + + var fractionX; + var fractionY; + + var boxWidth = box.clientWidth; + var boxHeight = box.clientHeight; + + var fill; + switch (fillMode) { + case ThumbnailLoader.FillMode.FILL: + case ThumbnailLoader.FillMode.OVER_FILL: + fill = true; + break; + case ThumbnailLoader.FillMode.FIT: + fill = false; + break; + case ThumbnailLoader.FillMode.AUTO: + var imageRatio = imageWidth / imageHeight; + var boxRatio = 1.0; + if (boxWidth && boxHeight) + boxRatio = boxWidth / boxHeight; + // Cropped area in percents. + var ratioFactor = boxRatio / imageRatio; + fill = (ratioFactor >= 1.0 - ThumbnailLoader.AUTO_FILL_THRESHOLD) && + (ratioFactor <= 1.0 + ThumbnailLoader.AUTO_FILL_THRESHOLD); + break; + } + + if (boxWidth && boxHeight) { + // When we know the box size we can position the image correctly even + // in a non-square box. + var fitScaleX = (rotate ? boxHeight : boxWidth) / imageWidth; + var fitScaleY = (rotate ? boxWidth : boxHeight) / imageHeight; + + var scale = fill ? + Math.max(fitScaleX, fitScaleY) : + Math.min(fitScaleX, fitScaleY); + + if (fillMode != ThumbnailLoader.FillMode.OVER_FILL) + scale = Math.min(scale, 1); // Never overscale. + + fractionX = imageWidth * scale / boxWidth; + fractionY = imageHeight * scale / boxHeight; + } else { + // We do not know the box size so we assume it is square. + // Compute the image position based only on the image dimensions. + // First try vertical fit or horizontal fill. + fractionX = imageWidth / imageHeight; + fractionY = 1; + if ((fractionX < 1) == !!fill) { // Vertical fill or horizontal fit. + fractionY = 1 / fractionX; + fractionX = 1; + } + } + + function percent(fraction) { + return (fraction * 100).toFixed(2) + '%'; + } + + img.style.width = percent(fractionX); + img.style.height = percent(fractionY); + img.style.left = percent((1 - fractionX) / 2); + img.style.top = percent((1 - fractionY) / 2); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/media/mediaplayer_scripts.js b/chromium/chrome/browser/resources/file_manager/foreground/js/media/mediaplayer_scripts.js new file mode 100644 index 00000000000..496a8d09d98 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/media/mediaplayer_scripts.js @@ -0,0 +1,33 @@ +// 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. + +// The include directives are put into Javascript-style comments to prevent +// parsing errors in non-flattened mode. The flattener still sees them. +// Note that this makes the flattener to comment out the first line of the +// included file but that's all right since any javascript file should start +// with a copyright comment anyway. + + +//<include src="../../../../../../../ui/webui/resources/js/cr.js"/> +//<include src="../../../../../../../ui/webui/resources/js/cr/event_target.js"/> +//<include src="../../../../../../../ui/webui/resources/js/cr/ui/array_data_model.js"/> + +(function() { +// 'strict mode' is invoked for this scope. + +//<include src="../../../common/js/async_util.js"/> +//<include src="../../../common/js/util.js"/> +//<include src="../../../common/js/path_util.js"/> +//<include src="../file_type.js"/> +//<include src="../volume_manager_wrapper.js"> +//<include src="../metadata/metadata_cache.js"/> + +//<include src="media_controls.js"/> +//<include src="audio_player.js"/> +//<include src="player_testapi.js"/> + +window.reload = reload; +window.unload = unload; + +})(); diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/media/player_testapi.js b/chromium/chrome/browser/resources/file_manager/foreground/js/media/player_testapi.js new file mode 100644 index 00000000000..75f0b302502 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/media/player_testapi.js @@ -0,0 +1,193 @@ +// 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'; + +/** + * Test API for Chrome OS Video Player and Audio Player. + * + * To test the Video Player open a tab with the URL: + * chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj/video_player.html + * + * To test the Audio Player open a tab with the URL: + * chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj/mediaplayer.html + * + */ +var playerTestAPI = { + + /* Methods common for audio and video players */ + + /** + * Respond with the path to the current media source. + */ + getSrc: function() { + playerTestAPI.respond_(util.extractFilePath(playerTestAPI.getMedia_().src)); + }, + + /** + * Respond with a boolean value, true if the media is playing. + */ + isPlaying: function() { + playerTestAPI.respond_(playerTestAPI.getControls_().isPlaying()); + }, + + /** + * Play the media. + */ + play: function() { + playerTestAPI.getControls_().play(); + }, + + /** + * Pause the playback. + */ + pause: function() { + playerTestAPI.getControls_().pause(); + }, + + /** + * Respond with a number, duration of the media in seconds. + */ + getDuration: function() { + playerTestAPI.respond_(playerTestAPI.getMedia_().duration); + }, + + /** + * Respond with a number, current media position in seconds. + */ + getCurrentTime: function() { + playerTestAPI.respond_(playerTestAPI.getMedia_().currentTime); + }, + + /** + * Change media position. + * @param {number} time Media positions. + */ + seekTo: function(time) { + playerTestAPI.getMedia_().currentTime = time; + }, + + /* Video player-specific methods. + * + * To test the video player open a tab with the url: + * chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj/mediaplayer.html + * + */ + + /** + * Load the specified file in the video player, + * Starts playing immediately. + * @param {string} filePath File path. + */ + loadVideo: function(filePath) { + var url = util.makeFilesystemUrl(filePath); + location.href = location.origin + location.pathname + '?' + url; + reload(); + }, + + /** + * Respond with a number, current volume [0..100]. + */ + getVolume: function() { + playerTestAPI.respond_(playerTestAPI.getMedia_().volume * 100); + }, + + /** + * Change volume. + * @param {number} volume Volume [0..100]. + */ + setVolume: function(volume) { + playerTestAPI.respond_( + playerTestAPI.getControls_().onVolumeChange_(volume / 100)); + }, + + /** + * Respond with a boolean, true if the volume is muted. + */ + isMuted: function() { + playerTestAPI.respond_(playerTestAPI.getMedia_().volume == 0); + }, + + /** + * Mute the volume. No-op if already muted. + */ + mute: function() { + if (playerTestAPI.getMedia_().volume != 0) + playerTestAPI.getControls_().onSoundButtonClick_(); + }, + + /** + * Unmute the volume. No-op if not muted. + */ + unmute: function() { + if (playerTestAPI.getMedia_().volume == 0) + playerTestAPI.getControls_().onSoundButtonClick_(); + }, + + /* Audio player-specific methods. */ + + /** + * Load a group of tracks into the audio player. + * Starts playing one of the tracks immediately. + * @param {Array.<string>} filePaths Array of file paths. + * @param {number} firstTrack Number of the file to play first (0-based). + */ + loadAudio: function(filePaths, firstTrack) { + AudioPlayer.instance.load({ + items: filePaths.map(util.makeFilesystemUrl), + position: firstTrack + }); + }, + + /** + * Respond with a current track number, + */ + getTrackNumber: function() { + playerTestAPI.respond_(AudioPlayer.instance.currentTrack_); + }, + + /** + * Play the next track. + */ + forward: function() { + playerTestAPI.getControls_().onAdvanceClick_(true /* forward */); + }, + + /** + * Go back. Will restart the current track if the current position is > 5 sec + * or play the previous track otherwise. + */ + back: function() { + playerTestAPI.getControls_().onAdvanceClick_(false /* back */); + }, + + /* Utility methods */ + + /** + * @return {AudioControls|VideoControls} Media controls. + * @private + */ + getControls_: function() { + return window.controls || window.AudioPlayer.instance.audioControls_; + }, + + /** + * @return {HTMLVideoElement|HTMLAudioElement} Media element. + * @private + */ + getMedia_: function() { + return playerTestAPI.getControls_().getMedia(); + }, + + /** + * @param {string|boolean|number} value Value to send back. + * @private + */ + respond_: function(value) { + if (window.domAutomationController) + window.domAutomationController.send(value); + else + console.log('playerTestAPI response: ' + value); + } +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/media/util.js b/chromium/chrome/browser/resources/file_manager/foreground/js/media/util.js new file mode 100644 index 00000000000..4d3c953dd55 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/media/util.js @@ -0,0 +1,179 @@ +// 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'; + +/** + * A controller class detects mouse inactivity and hides "tool" elements. + * + * @param {Element} container The main DOM container. + * @param {number=} opt_timeout Hide timeout in ms. + * @param {function():boolean=} opt_toolsActive Function that returns |true| + * if the tools are active and should not be hidden. + * @constructor + */ +function MouseInactivityWatcher(container, opt_timeout, opt_toolsActive) { + this.container_ = container; + this.timeout_ = opt_timeout || MouseInactivityWatcher.DEFAULT_TIMEOUT; + this.toolsActive_ = opt_toolsActive || function() { return false }; + + this.onTimeoutBound_ = this.onTimeout_.bind(this); + this.timeoutID_ = null; + this.mouseOverTool_ = false; + + this.clientX_ = 0; + this.clientY_ = 0; + + /** + * Indicates if the inactivity watcher is enabled or disabled. Use getters + * and setters. + * @type {boolean} + * @private + **/ + this.disabled_ = false; + this.__defineSetter__('disabled', function(value) { + this.disabled_ = value; + if (value) + this.kick(); + else + this.check(); + }); + this.__defineGetter__('disabled', function() { + return this.disabled_; + }); + + this.container_.addEventListener('mousemove', this.onMouseMove_.bind(this)); + var tools = this.container_.querySelector('.tool'); + for (var i = 0; i < tools.length; i++) { + tools[i].addEventListener('mouseover', this.onToolMouseOver_.bind(this)); + tools[i].addEventListener('mouseout', this.onToolMouseOut_.bind(this)); + } + + // Show tools when the user touches the screen. + this.container_.addEventListener( + 'touchstart', this.activityStarted_.bind(this)); + var initiateFading = this.activityStopped_.bind(this, this.timeout_); + this.container_.addEventListener('touchend', initiateFading); + this.container_.addEventListener('touchcancel', initiateFading); +} + +/** + * Default inactivity timeout. + */ +MouseInactivityWatcher.DEFAULT_TIMEOUT = 3000; + +/** + * @param {boolean} on True if show, false if hide. + */ +MouseInactivityWatcher.prototype.showTools = function(on) { + if (on) + this.container_.setAttribute('tools', 'true'); + else + this.container_.removeAttribute('tools'); +}; + +/** + * To be called when the user started activity. Shows the tools + * and cancels the countdown. + * @private + */ +MouseInactivityWatcher.prototype.activityStarted_ = function() { + this.showTools(true); + + if (this.timeoutID_) { + clearTimeout(this.timeoutID_); + this.timeoutID_ = null; + } +}; + +/** + * Called when user activity has stopped. Re-starts the countdown. + * @param {number=} opt_timeout Timeout. + * @private + */ +MouseInactivityWatcher.prototype.activityStopped_ = function(opt_timeout) { + if (this.disabled_ || this.mouseOverTool_ || this.toolsActive_()) + return; + + if (this.timeoutID_) + clearTimeout(this.timeoutID_); + + this.timeoutID_ = setTimeout( + this.onTimeoutBound_, opt_timeout || this.timeout_); +}; + +/** + * Called when a user performed a short action (such as a click or a key press) + * that should show the tools if they are not visible. + * @param {number=} opt_timeout Timeout. + */ +MouseInactivityWatcher.prototype.kick = function(opt_timeout) { + this.activityStarted_(); + this.activityStopped_(opt_timeout); +}; + +/** + * Check if the tools are active and update the tools visibility accordingly. + */ +MouseInactivityWatcher.prototype.check = function() { + if (this.toolsActive_()) + this.activityStarted_(); + else + this.activityStopped_(); +}; + +/** + * Mouse move handler. + * + * @param {Event} e Event. + * @private + */ +MouseInactivityWatcher.prototype.onMouseMove_ = function(e) { + if (this.clientX_ == e.clientX && this.clientY_ == e.clientY) { + // The mouse has not moved, must be the cursor change triggered by + // some of the attributes on the root container. Ignore the event. + return; + } + this.clientX_ = e.clientX; + this.clientY_ = e.clientY; + + if (this.disabled_) + return; + + this.kick(); +}; + +/** + * Mouse over handler on a tool element. + * + * @param {Event} e Event. + * @private + */ +MouseInactivityWatcher.prototype.onToolMouseOver_ = function(e) { + this.mouseOverTool_ = true; + if (!this.disabled_) + this.kick(); +}; + +/** + * Mouse out handler on a tool element. + * + * @param {Event} e Event. + * @private + */ +MouseInactivityWatcher.prototype.onToolMouseOut_ = function(e) { + this.mouseOverTool_ = false; + if (!this.disabled_) + this.kick(); +}; + +/** + * Timeout handler. + * @private + */ +MouseInactivityWatcher.prototype.onTimeout_ = function() { + this.timeoutID_ = null; + if (!this.disabled_ && !this.toolsActive_()) + this.showTools(false); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/media/video_player.js b/chromium/chrome/browser/resources/file_manager/foreground/js/media/video_player.js new file mode 100644 index 00000000000..9b4b663fdc1 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/media/video_player.js @@ -0,0 +1,289 @@ +// 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'; + +/** + * Display error message. + * @param {string} message Message id. + */ +function showErrorMessage(message) { + var errorBanner = document.querySelector('#error'); + errorBanner.textContent = + loadTimeData.getString(message); + errorBanner.setAttribute('visible', 'true'); + + // The window is hidden if the video has not loaded yet. + chrome.app.window.current().show(); +} + +/** + * Handles playback (decoder) errors. + */ +function onPlaybackError() { + showErrorMessage('GALLERY_VIDEO_DECODING_ERROR'); + decodeErrorOccured = true; + + // Disable inactivity watcher, and disable the ui, by hiding tools manually. + controls.inactivityWatcher.disabled = true; + document.querySelector('#video-player').setAttribute('disabled', 'true'); + + // Detach the video element, since it may be unreliable and reset stored + // current playback time. + controls.cleanup(); + controls.clearState(); + + // Avoid reusing a video element. + video.parentNode.removeChild(video); + video = null; +} + +/** + * @param {Element} playerContainer Main container. + * @param {Element} videoContainer Container for the video element. + * @param {Element} controlsContainer Container for video controls. + * @constructor + */ +function FullWindowVideoControls( + playerContainer, videoContainer, controlsContainer) { + VideoControls.call(this, + controlsContainer, + onPlaybackError, + loadTimeData.getString.bind(loadTimeData), + this.toggleFullScreen_.bind(this), + videoContainer); + + this.playerContainer_ = playerContainer; + + this.updateStyle(); + window.addEventListener('resize', this.updateStyle.bind(this)); + + document.addEventListener('keydown', function(e) { + if (e.keyIdentifier == 'U+0020') { // Space + this.togglePlayStateWithFeedback(); + e.preventDefault(); + } + if (e.keyIdentifier == 'U+001B') { // Escape + util.toggleFullScreen( + chrome.app.window.current(), + false); // Leave the full screen mode. + e.preventDefault(); + } + }.bind(this)); + + // TODO(mtomasz): Simplify. crbug.com/254318. + videoContainer.addEventListener('click', function(e) { + if (e.ctrlKey) { + this.toggleLoopedModeWithFeedback(true); + if (!this.isPlaying()) + this.togglePlayStateWithFeedback(); + } else { + this.togglePlayStateWithFeedback(); + } + }.bind(this)); + + this.inactivityWatcher_ = new MouseInactivityWatcher(playerContainer); + this.__defineGetter__('inactivityWatcher', function() { + return this.inactivityWatcher_; + }); + + this.inactivityWatcher_.check(); +} + +FullWindowVideoControls.prototype = { __proto__: VideoControls.prototype }; + +/** + * Save the current state so that it survives page/app reload. + */ +FullWindowVideoControls.prototype.onPlayStateChanged = function() { + this.encodeState(); +}; + +/** + * Restore the state after the video is loaded. + */ +FullWindowVideoControls.prototype.restorePlayState = function() { + if (!this.decodeState()) { + VideoControls.prototype.restorePlayState.apply(this, arguments); + this.play(); + } +}; + +/** + * Toggles the full screen mode. + * @private + */ +FullWindowVideoControls.prototype.toggleFullScreen_ = function() { + var appWindow = chrome.app.window.current(); + util.toggleFullScreen(appWindow, !util.isFullScreen(appWindow)); +}; + +// TODO(mtomasz): Convert it to class members: crbug.com/171191. +var decodeErrorOccured; +var video; +var controls; +var metadataCache; +var volumeManager; +var selectedEntry; + +/** + * Initialize the video player window. + */ +function loadVideoPlayer() { + document.ondragstart = function(e) { e.preventDefault() }; + + chrome.fileBrowserPrivate.getStrings(function(strings) { + loadTimeData.data = strings; + + controls = new FullWindowVideoControls( + document.querySelector('#video-player'), + document.querySelector('#video-container'), + document.querySelector('#controls')); + + metadataCache = MetadataCache.createFull(); + volumeManager = new VolumeManagerWrapper( + VolumeManagerWrapper.DriveEnabledStatus.DRIVE_ENABLED); + volumeManager.addEventListener('externally-unmounted', + onExternallyUnmounted); + + // If the video player is starting before the first instance of the File + // Manager then it does not have access to filesystem URLs. + // Request it now. + volumeManager.ensureInitialized(reload); + var reloadVideo = function(e) { + if (decodeErrorOccured && + // Ignore shortcut keys + !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) { + reload(); + e.preventDefault(); + } + }; + + document.addEventListener('keydown', reloadVideo, true); + document.addEventListener('click', reloadVideo, true); + }); +} + +/** + * Closes video player when a volume containing the played item is unmounted. + * @param {Event} event The unmount event. + */ +function onExternallyUnmounted(event) { + if (!selectedEntry) + return; + + if (volumeManager.getVolumeInfo(selectedEntry) === event.volumeInfo) + window.close(); +} + +/** + * Unload the player. + */ +function unload() { + if (volumeManager) + volumeManager.dispose(); + + if (!controls.getMedia()) + return; + + controls.savePosition(true /* exiting */); + controls.cleanup(); +} + +/** + * Reload the player. + */ +function reload() { + // Re-enable ui and hide error message if already displayed. + document.querySelector('#video-player').removeAttribute('disabled'); + document.querySelector('#error').removeAttribute('visible'); + controls.inactivityWatcher.disabled = false; + decodeErrorOccured = false; + + var url; + if (window.appState) { + util.saveAppState(); + url = window.appState.url; + } else { + url = document.location.search.substr(1); + } + + document.title = decodeURIComponent(url.split('/').pop()); + var queue = new AsyncUtil.Queue(); + + queue.run(function(callback) { + webkitResolveLocalFileSystemURL(url, + function(entry) { + selectedEntry = entry; + callback(); + }, function() { + console.warn('Failed to resolve entry for: ' + url); + callback(); + }); + }); + + + queue.run(function(callback) { + if (!selectedEntry) { + showErrorMessage('GALLERY_VIDEO_ERROR'); + return; + } + metadataCache.get(selectedEntry, 'streaming', function(streaming) { + if (streaming && !navigator.onLine) { + showErrorMessage('GALLERY_VIDEO_OFFLINE'); + return; + } + + // Detach the previous video element, if exists. + if (video) + video.parentNode.removeChild(video); + + video = document.createElement('video'); + document.querySelector('#video-container').appendChild(video); + controls.attachMedia(video); + + video.src = selectedEntry.toURL(); + video.load(); + video.addEventListener('loadedmetadata', function() { + // TODO: chrome.app.window soon will be able to resize the content area. + // Until then use approximate title bar height. + var TITLE_HEIGHT = 28; + + var aspect = video.videoWidth / video.videoHeight; + var newWidth = video.videoWidth; + var newHeight = video.videoHeight + TITLE_HEIGHT; + + var shrinkX = newWidth / window.screen.availWidth; + var shrinkY = newHeight / window.screen.availHeight; + if (shrinkX > 1 || shrinkY > 1) { + if (shrinkY > shrinkX) { + newHeight = newHeight / shrinkY; + newWidth = (newHeight - TITLE_HEIGHT) * aspect; + } else { + newWidth = newWidth / shrinkX; + newHeight = newWidth / aspect + TITLE_HEIGHT; + } + } + + var oldLeft = window.screenX; + var oldTop = window.screenY; + var oldWidth = window.outerWidth; + var oldHeight = window.outerHeight; + + if (!oldWidth && !oldHeight) { + oldLeft = window.screen.availWidth / 2; + oldTop = window.screen.availHeight / 2; + } + + var appWindow = chrome.app.window.current(); + appWindow.resizeTo(newWidth, newHeight); + appWindow.moveTo(oldLeft - (newWidth - oldWidth) / 2, + oldTop - (newHeight - oldHeight) / 2); + appWindow.show(); + }); + }); + }); +} + +util.addPageLoadHandler(loadVideoPlayer); diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/media/video_player_scripts.js b/chromium/chrome/browser/resources/file_manager/foreground/js/media/video_player_scripts.js new file mode 100644 index 00000000000..54ea178f2fe --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/media/video_player_scripts.js @@ -0,0 +1,34 @@ +// 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. + +// The include directives are put into Javascript-style comments to prevent +// parsing errors in non-flattened mode. The flattener still sees them. +// Note that this makes the flattener to comment out the first line of the +// included file but that's all right since any javascript file should start +// with a copyright comment anyway. + +//<include src="../../../../../../../ui/webui/resources/js/cr.js"/> +//<include src="../../../../../../../ui/webui/resources/js/cr/event_target.js"/> +//<include src="../../../../../../../ui/webui/resources/js/cr/ui/array_data_model.js"/> +//<include src="../../../../../../../ui/webui/resources/js/load_time_data.js"/> + +(function() { +// 'strict mode' is invoked for this scope. + +//<include src="../../../common/js/async_util.js"/> +//<include src="../../../common/js/util.js"/> +//<include src="../../../common/js/path_util.js"/> +//<include src="../file_type.js"/> +//<include src="../volume_manager_wrapper.js"> +//<include src="../metadata/metadata_cache.js"/> + +//<include src="media_controls.js"/> +//<include src="util.js"/> +//<include src="video_player.js"/> +//<include src="player_testapi.js"/> + +window.reload = reload; +window.unload = unload; + +})(); diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/byte_reader.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/byte_reader.js new file mode 100644 index 00000000000..09cf306b53c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/byte_reader.js @@ -0,0 +1,535 @@ +// 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'; + +/** + * @constructor + * @param {ArrayBuffer} arrayBuffer // TODO(JSDOC). + * @param {number=} opt_offset // TODO(JSDOC). + * @param {number=} opt_length // TODO(JSDOC). + */ +function ByteReader(arrayBuffer, opt_offset, opt_length) { + opt_offset = opt_offset || 0; + opt_length = opt_length || (arrayBuffer.byteLength - opt_offset); + this.view_ = new DataView(arrayBuffer, opt_offset, opt_length); + this.pos_ = 0; + this.seekStack_ = []; + this.setByteOrder(ByteReader.BIG_ENDIAN); +} + +// Static constants and methods. + +/** + * Intel, 0x1234 is [0x34, 0x12] + * @const + * @type {number} + */ +ByteReader.LITTLE_ENDIAN = 0; +/** + * Motorola, 0x1234 is [0x12, 0x34] + * @const + * @type {number} + */ +ByteReader.BIG_ENDIAN = 1; + +/** + * Seek relative to the beginning of the buffer. + * @const + * @type {number} + */ +ByteReader.SEEK_BEG = 0; +/** + * Seek relative to the current position. + * @const + * @type {number} + */ +ByteReader.SEEK_CUR = 1; +/** + * Seek relative to the end of the buffer. + * @const + * @type {number} + */ +ByteReader.SEEK_END = 2; + +/** + * Throw an error if (0 > pos >= end) or if (pos + size > end). + * + * Static utility function. + * + * @param {number} pos // TODO(JSDOC). + * @param {number} size // TODO(JSDOC). + * @param {number} end // TODO(JSDOC). + */ +ByteReader.validateRead = function(pos, size, end) { + if (pos < 0 || pos >= end) + throw new Error('Invalid read position'); + + if (pos + size > end) + throw new Error('Read past end of buffer'); +}; + +/** + * Read as a sequence of characters, returning them as a single string. + * + * This is a static utility function. There is a member function with the + * same name which side-effects the current read position. + * + * @param {DataView} dataView // TODO(JSDOC). + * @param {number} pos // TODO(JSDOC). + * @param {number} size // TODO(JSDOC). + * @param {number=} opt_end // TODO(JSDOC). + * @return {string} // TODO(JSDOC). + */ +ByteReader.readString = function(dataView, pos, size, opt_end) { + ByteReader.validateRead(pos, size, opt_end || dataView.byteLength); + + var codes = []; + + for (var i = 0; i < size; ++i) + codes.push(dataView.getUint8(pos + i)); + + return String.fromCharCode.apply(null, codes); +}; + +/** + * Read as a sequence of characters, returning them as a single string. + * + * This is a static utility function. There is a member function with the + * same name which side-effects the current read position. + * + * @param {DataView} dataView // TODO(JSDOC). + * @param {number} pos // TODO(JSDOC). + * @param {number} size // TODO(JSDOC). + * @param {number=} opt_end // TODO(JSDOC). + * @return {string} // TODO(JSDOC). + */ +ByteReader.readNullTerminatedString = function(dataView, pos, size, opt_end) { + ByteReader.validateRead(pos, size, opt_end || dataView.byteLength); + + var codes = []; + + for (var i = 0; i < size; ++i) { + var code = dataView.getUint8(pos + i); + if (code == 0) break; + codes.push(code); + } + + return String.fromCharCode.apply(null, codes); +}; + +/** + * Read as a sequence of UTF16 characters, returning them as a single string. + * + * This is a static utility function. There is a member function with the + * same name which side-effects the current read position. + * + * @param {DataView} dataView // TODO(JSDOC). + * @param {number} pos // TODO(JSDOC). + * @param {boolean} bom // TODO(JSDOC). + * @param {number} size // TODO(JSDOC). + * @param {number=} opt_end // TODO(JSDOC). + * @return {string} // TODO(JSDOC). + */ +ByteReader.readNullTerminatedStringUTF16 = function( + dataView, pos, bom, size, opt_end) { + ByteReader.validateRead(pos, size, opt_end || dataView.byteLength); + + var littleEndian = false; + var start = 0; + + if (bom) { + littleEndian = (dataView.getUint8(pos) == 0xFF); + start = 2; + } + + var codes = []; + + for (var i = start; i < size; i += 2) { + var code = dataView.getUint16(pos + i, littleEndian); + if (code == 0) break; + codes.push(code); + } + + return String.fromCharCode.apply(null, codes); +}; + +/** + * @const + * @type {Array.<string>} + * @private + */ +ByteReader.base64Alphabet_ = + ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'). + split(''); + +/** + * Read as a sequence of bytes, returning them as a single base64 encoded + * string. + * + * This is a static utility function. There is a member function with the + * same name which side-effects the current read position. + * + * @param {DataView} dataView // TODO(JSDOC). + * @param {number} pos // TODO(JSDOC). + * @param {number} size // TODO(JSDOC). + * @param {number=} opt_end // TODO(JSDOC). + * @return {string} // TODO(JSDOC). + */ +ByteReader.readBase64 = function(dataView, pos, size, opt_end) { + ByteReader.validateRead(pos, size, opt_end || dataView.byteLength); + + var rv = []; + var chars = []; + var padding = 0; + + for (var i = 0; i < size; /* incremented inside */) { + var bits = dataView.getUint8(pos + (i++)) << 16; + + if (i < size) { + bits |= dataView.getUint8(pos + (i++)) << 8; + + if (i < size) { + bits |= dataView.getUint8(pos + (i++)); + } else { + padding = 1; + } + } else { + padding = 2; + } + + chars[3] = ByteReader.base64Alphabet_[bits & 63]; + chars[2] = ByteReader.base64Alphabet_[(bits >> 6) & 63]; + chars[1] = ByteReader.base64Alphabet_[(bits >> 12) & 63]; + chars[0] = ByteReader.base64Alphabet_[(bits >> 18) & 63]; + + rv.push.apply(rv, chars); + } + + if (padding > 0) + rv[rv.length - 1] = '='; + if (padding > 1) + rv[rv.length - 2] = '='; + + return rv.join(''); +}; + +/** + * Read as an image encoded in a data url. + * + * This is a static utility function. There is a member function with the + * same name which side-effects the current read position. + * + * @param {DataView} dataView // TODO(JSDOC). + * @param {number} pos // TODO(JSDOC). + * @param {number} size // TODO(JSDOC). + * @param {number=} opt_end // TODO(JSDOC). + * @return {string} // TODO(JSDOC). + */ +ByteReader.readImage = function(dataView, pos, size, opt_end) { + opt_end = opt_end || dataView.byteLength; + ByteReader.validateRead(pos, size, opt_end); + + // Two bytes is enough to identify the mime type. + var prefixToMime = { + '\x89P' : 'png', + '\xFF\xD8' : 'jpeg', + 'BM' : 'bmp', + 'GI' : 'gif' + }; + + var prefix = ByteReader.readString(dataView, pos, 2, opt_end); + var mime = prefixToMime[prefix] || + dataView.getUint16(pos, false).toString(16); // For debugging. + + var b64 = ByteReader.readBase64(dataView, pos, size, opt_end); + return 'data:image/' + mime + ';base64,' + b64; +}; + +// Instance methods. + +/** + * Return true if the requested number of bytes can be read from the buffer. + * + * @param {number} size // TODO(JSDOC). + * @return {boolean} // TODO(JSDOC). + */ +ByteReader.prototype.canRead = function(size) { + return this.pos_ + size <= this.view_.byteLength; +}; + +/** + * Return true if the current position is past the end of the buffer. + * @return {boolean} // TODO(JSDOC). + */ +ByteReader.prototype.eof = function() { + return this.pos_ >= this.view_.byteLength; +}; + +/** + * Return true if the current position is before the beginning of the buffer. + * @return {boolean} // TODO(JSDOC). + */ +ByteReader.prototype.bof = function() { + return this.pos_ < 0; +}; + +/** + * Return true if the current position is outside the buffer. + * @return {boolean} // TODO(JSDOC). + */ +ByteReader.prototype.beof = function() { + return this.pos_ >= this.view_.byteLength || this.pos_ < 0; +}; + +/** + * Set the expected byte ordering for future reads. + * @param {number} order // TODO(JSDOC). + */ +ByteReader.prototype.setByteOrder = function(order) { + this.littleEndian_ = order == ByteReader.LITTLE_ENDIAN; +}; + +/** + * Throw an error if the reader is at an invalid position, or if a read a read + * of |size| would put it in one. + * + * You may optionally pass opt_end to override what is considered to be the + * end of the buffer. + * + * @param {number} size // TODO(JSDOC). + * @param {number=} opt_end // TODO(JSDOC). + */ +ByteReader.prototype.validateRead = function(size, opt_end) { + if (typeof opt_end == 'undefined') + opt_end = this.view_.byteLength; + + ByteReader.validateRead(this.view_, this.pos_, size, opt_end); +}; + +/** + * @param {number} width // TODO(JSDOC). + * @param {boolean=} opt_signed // TODO(JSDOC). + * @param {number=} opt_end // TODO(JSDOC). + * @return {string} // TODO(JSDOC). + */ +ByteReader.prototype.readScalar = function(width, opt_signed, opt_end) { + var method = opt_signed ? 'getInt' : 'getUint'; + + switch (width) { + case 1: + method += '8'; + break; + + case 2: + method += '16'; + break; + + case 4: + method += '32'; + break; + + case 8: + method += '64'; + break; + + default: + throw new Error('Invalid width: ' + width); + break; + } + + this.validateRead(width, opt_end); + var rv = this.view_[method](this.pos_, this.littleEndian_); + this.pos_ += width; + return rv; +}; + +/** + * Read as a sequence of characters, returning them as a single string. + * + * Adjusts the current position on success. Throws an exception if the + * read would go past the end of the buffer. + * + * @param {number} size // TODO(JSDOC). + * @param {number=} opt_end // TODO(JSDOC). + * @return {string} // TODO(JSDOC). + */ +ByteReader.prototype.readString = function(size, opt_end) { + var rv = ByteReader.readString(this.view_, this.pos_, size, opt_end); + this.pos_ += size; + return rv; +}; + + +/** + * Read as a sequence of characters, returning them as a single string. + * + * Adjusts the current position on success. Throws an exception if the + * read would go past the end of the buffer. + * + * @param {number} size // TODO(JSDOC). + * @param {number=} opt_end // TODO(JSDOC). + * @return {string} // TODO(JSDOC). + */ +ByteReader.prototype.readNullTerminatedString = function(size, opt_end) { + var rv = ByteReader.readNullTerminatedString(this.view_, + this.pos_, + size, + opt_end); + this.pos_ += rv.length; + + if (rv.length < size) { + // If we've stopped reading because we found '0' but didn't hit size limit + // then we should skip additional '0' character + this.pos_++; + } + + return rv; +}; + + +/** + * Read as a sequence of UTF16 characters, returning them as a single string. + * + * Adjusts the current position on success. Throws an exception if the + * read would go past the end of the buffer. + * + * @param {boolean} bom // TODO(JSDOC). + * @param {number} size // TODO(JSDOC). + * @param {number=} opt_end // TODO(JSDOC). + * @return {string} // TODO(JSDOC). + */ +ByteReader.prototype.readNullTerminatedStringUTF16 = + function(bom, size, opt_end) { + var rv = ByteReader.readNullTerminatedStringUTF16( + this.view_, this.pos_, bom, size, opt_end); + + if (bom) { + // If the BOM word was present advance the position. + this.pos_ += 2; + } + + this.pos_ += rv.length; + + if (rv.length < size) { + // If we've stopped reading because we found '0' but didn't hit size limit + // then we should skip additional '0' character + this.pos_ += 2; + } + + return rv; +}; + + +/** + * Read as an array of numbers. + * + * Adjusts the current position on success. Throws an exception if the + * read would go past the end of the buffer. + * + * @param {number} size // TODO(JSDOC). + * @param {number=} opt_end // TODO(JSDOC). + * @param {function(new:Array.<*>)=} opt_arrayConstructor // TODO(JSDOC). + * @return {Array.<*>} // TODO(JSDOC). + */ +ByteReader.prototype.readSlice = function(size, opt_end, + opt_arrayConstructor) { + this.validateRead(size, opt_end); + + var arrayConstructor = opt_arrayConstructor || Uint8Array; + var slice = new arrayConstructor( + this.view_.buffer, this.view_.byteOffset + this.pos, size); + this.pos_ += size; + + return slice; +}; + +/** + * Read as a sequence of bytes, returning them as a single base64 encoded + * string. + * + * Adjusts the current position on success. Throws an exception if the + * read would go past the end of the buffer. + * + * @param {number} size // TODO(JSDOC). + * @param {number=} opt_end // TODO(JSDOC). + * @return {string} // TODO(JSDOC). + */ +ByteReader.prototype.readBase64 = function(size, opt_end) { + var rv = ByteReader.readBase64(this.view_, this.pos_, size, opt_end); + this.pos_ += size; + return rv; +}; + +/** + * Read an image returning it as a data url. + * + * Adjusts the current position on success. Throws an exception if the + * read would go past the end of the buffer. + * + * @param {number} size // TODO(JSDOC). + * @param {number=} opt_end // TODO(JSDOC). + * @return {string} // TODO(JSDOC). + */ +ByteReader.prototype.readImage = function(size, opt_end) { + var rv = ByteReader.readImage(this.view_, this.pos_, size, opt_end); + this.pos_ += size; + return rv; +}; + +/** + * Seek to a give position relative to opt_seekStart. + * + * @param {number} pos // TODO(JSDOC). + * @param {number=} opt_seekStart // TODO(JSDOC). + * @param {number=} opt_end // TODO(JSDOC). + */ +ByteReader.prototype.seek = function(pos, opt_seekStart, opt_end) { + opt_end = opt_end || this.view_.byteLength; + + var newPos; + if (opt_seekStart == ByteReader.SEEK_CUR) { + newPos = this.pos_ + pos; + } else if (opt_seekStart == ByteReader.SEEK_END) { + newPos = opt_end + pos; + } else { + newPos = pos; + } + + if (newPos < 0 || newPos > this.view_.byteLength) + throw new Error('Seek outside of buffer: ' + (newPos - opt_end)); + + this.pos_ = newPos; +}; + +/** + * Seek to a given position relative to opt_seekStart, saving the current + * position. + * + * Recover the current position with a call to seekPop. + * + * @param {number} pos // TODO(JSDOC). + * @param {number=} opt_seekStart // TODO(JSDOC). + */ +ByteReader.prototype.pushSeek = function(pos, opt_seekStart) { + var oldPos = this.pos_; + this.seek(pos, opt_seekStart); + // Alter the seekStack_ after the call to seek(), in case it throws. + this.seekStack_.push(oldPos); +}; + +/** + * Undo a previous seekPush. + */ +ByteReader.prototype.popSeek = function() { + this.seek(this.seekStack_.pop()); +}; + +/** + * Return the current read position. + * @return {number} // TODO(JSDOC). + */ +ByteReader.prototype.tell = function() { + return this.pos_; +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/exif_parser.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/exif_parser.js new file mode 100644 index 00000000000..f81e5cc0b9f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/exif_parser.js @@ -0,0 +1,439 @@ +// 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'; + +var EXIF_MARK_SOI = 0xffd8; // Start of image data. +var EXIF_MARK_SOS = 0xffda; // Start of "stream" (the actual image data). +var EXIF_MARK_SOF = 0xffc0; // Start of "frame" +var EXIF_MARK_EXIF = 0xffe1; // Start of exif block. + +var EXIF_ALIGN_LITTLE = 0x4949; // Indicates little endian exif data. +var EXIF_ALIGN_BIG = 0x4d4d; // Indicates big endian exif data. + +var EXIF_TAG_TIFF = 0x002a; // First directory containing TIFF data. +var EXIF_TAG_GPSDATA = 0x8825; // Pointer from TIFF to the GPS directory. +var EXIF_TAG_EXIFDATA = 0x8769; // Pointer from TIFF to the EXIF IFD. +var EXIF_TAG_SUBIFD = 0x014a; // Pointer from TIFF to "Extra" IFDs. + +var EXIF_TAG_JPG_THUMB_OFFSET = 0x0201; // Pointer from TIFF to thumbnail. +var EXIF_TAG_JPG_THUMB_LENGTH = 0x0202; // Length of thumbnail data. + +var EXIF_TAG_ORIENTATION = 0x0112; +var EXIF_TAG_X_DIMENSION = 0xA002; +var EXIF_TAG_Y_DIMENSION = 0xA003; + +function ExifParser(parent) { + ImageParser.call(this, parent, 'jpeg', /\.jpe?g$/i); +} + +ExifParser.prototype = {__proto__: ImageParser.prototype}; + +/** + * @param {File} file // TODO(JSDOC). + * @param {Object} metadata // TODO(JSDOC). + * @param {function} callback // TODO(JSDOC). + * @param {function} errorCallback // TODO(JSDOC). + */ +ExifParser.prototype.parse = function(file, metadata, callback, errorCallback) { + this.requestSlice(file, callback, errorCallback, metadata, 0); +}; + +/** + * @param {File} file // TODO(JSDOC). + * @param {function} callback // TODO(JSDOC). + * @param {function} errorCallback // TODO(JSDOC). + * @param {Object} metadata // TODO(JSDOC). + * @param {number} filePos // TODO(JSDOC). + * @param {number=} opt_length // TODO(JSDOC). + */ +ExifParser.prototype.requestSlice = function( + file, callback, errorCallback, metadata, filePos, opt_length) { + // Read at least 1Kb so that we do not issue too many read requests. + opt_length = Math.max(1024, opt_length || 0); + + var self = this; + var reader = new FileReader(); + reader.onerror = errorCallback; + reader.onload = function() { self.parseSlice( + file, callback, errorCallback, metadata, filePos, reader.result); + }; + reader.readAsArrayBuffer(file.slice(filePos, filePos + opt_length)); +}; + +/** + * @param {File} file // TODO(JSDOC). + * @param {function} callback // TODO(JSDOC). + * @param {function} errorCallback // TODO(JSDOC). + * @param {Object} metadata // TODO(JSDOC). + * @param {number} filePos // TODO(JSDOC). + * @param {ArrayBuffer} buf // TODO(JSDOC). + */ +ExifParser.prototype.parseSlice = function( + file, callback, errorCallback, metadata, filePos, buf) { + try { + var br = new ByteReader(buf); + + if (!br.canRead(4)) { + // We never ask for less than 4 bytes. This can only mean we reached EOF. + throw new Error('Unexpected EOF @' + (filePos + buf.byteLength)); + } + + if (filePos == 0) { + // First slice, check for the SOI mark. + var firstMark = this.readMark(br); + if (firstMark != EXIF_MARK_SOI) + throw new Error('Invalid file header: ' + firstMark.toString(16)); + } + + var self = this; + var reread = function(opt_offset, opt_bytes) { + self.requestSlice(file, callback, errorCallback, metadata, + filePos + br.tell() + (opt_offset || 0), opt_bytes); + }; + + while (true) { + if (!br.canRead(4)) { + // Cannot read the mark and the length, request a minimum-size slice. + reread(); + return; + } + + var mark = this.readMark(br); + if (mark == EXIF_MARK_SOS) + throw new Error('SOS marker found before SOF'); + + var markLength = this.readMarkLength(br); + + var nextSectionStart = br.tell() + markLength; + if (!br.canRead(markLength)) { + // Get the entire section. + if (filePos + br.tell() + markLength > file.size) { + throw new Error( + 'Invalid section length @' + (filePos + br.tell() - 2)); + } + reread(-4, markLength + 4); + return; + } + + if (mark == EXIF_MARK_EXIF) { + this.parseExifSection(metadata, buf, br); + } else if (ExifParser.isSOF_(mark)) { + // The most reliable size information is encoded in the SOF section. + br.seek(1, ByteReader.SEEK_CUR); // Skip the precision byte. + var height = br.readScalar(2); + var width = br.readScalar(2); + ExifParser.setImageSize(metadata, width, height); + callback(metadata); // We are done! + return; + } + + br.seek(nextSectionStart, ByteReader.SEEK_BEG); + } + } catch (e) { + errorCallback(e.toString()); + } +}; + +/** + * @private + * @param {number} mark // TODO(JSDOC). + * @return {boolean} // TODO(JSDOC). + */ +ExifParser.isSOF_ = function(mark) { + // There are 13 variants of SOF fragment format distinguished by the last + // hex digit of the mark, but the part we want is always the same. + if ((mark & ~0xF) != EXIF_MARK_SOF) return false; + + // If the last digit is 4, 8 or 12 it is not really a SOF. + var type = mark & 0xF; + return (type != 4 && type != 8 && type != 12); +}; + +/** + * @param {Object} metadata // TODO(JSDOC). + * @param {ArrayBuffer} buf // TODO(JSDOC). + * @param {ByteReader} br // TODO(JSDOC). + */ +ExifParser.prototype.parseExifSection = function(metadata, buf, br) { + var magic = br.readString(6); + if (magic != 'Exif\0\0') { + // Some JPEG files may have sections marked with EXIF_MARK_EXIF + // but containing something else (e.g. XML text). Ignore such sections. + this.vlog('Invalid EXIF magic: ' + magic + br.readString(100)); + return; + } + + // Offsets inside the EXIF block are based after the magic string. + // Create a new ByteReader based on the current position to make offset + // calculations simpler. + br = new ByteReader(buf, br.tell()); + + var order = br.readScalar(2); + if (order == EXIF_ALIGN_LITTLE) { + br.setByteOrder(ByteReader.LITTLE_ENDIAN); + } else if (order != EXIF_ALIGN_BIG) { + this.log('Invalid alignment value: ' + order.toString(16)); + return; + } + + var tag = br.readScalar(2); + if (tag != EXIF_TAG_TIFF) { + this.log('Invalid TIFF tag: ' + tag.toString(16)); + return; + } + + metadata.littleEndian = (order == EXIF_ALIGN_LITTLE); + metadata.ifd = { + image: {}, + thumbnail: {} + }; + var directoryOffset = br.readScalar(4); + + // Image directory. + this.vlog('Read image directory.'); + br.seek(directoryOffset); + directoryOffset = this.readDirectory(br, metadata.ifd.image); + metadata.imageTransform = this.parseOrientation(metadata.ifd.image); + + // Thumbnail Directory chained from the end of the image directory. + if (directoryOffset) { + this.vlog('Read thumbnail directory.'); + br.seek(directoryOffset); + this.readDirectory(br, metadata.ifd.thumbnail); + // If no thumbnail orientation is encoded, assume same orientation as + // the primary image. + metadata.thumbnailTransform = + this.parseOrientation(metadata.ifd.thumbnail) || + metadata.imageTransform; + } + + // EXIF Directory may be specified as a tag in the image directory. + if (EXIF_TAG_EXIFDATA in metadata.ifd.image) { + this.vlog('Read EXIF directory.'); + directoryOffset = metadata.ifd.image[EXIF_TAG_EXIFDATA].value; + br.seek(directoryOffset); + metadata.ifd.exif = {}; + this.readDirectory(br, metadata.ifd.exif); + } + + // GPS Directory may also be linked from the image directory. + if (EXIF_TAG_GPSDATA in metadata.ifd.image) { + this.vlog('Read GPS directory.'); + directoryOffset = metadata.ifd.image[EXIF_TAG_GPSDATA].value; + br.seek(directoryOffset); + metadata.ifd.gps = {}; + this.readDirectory(br, metadata.ifd.gps); + } + + // Thumbnail may be linked from the image directory. + if (EXIF_TAG_JPG_THUMB_OFFSET in metadata.ifd.thumbnail && + EXIF_TAG_JPG_THUMB_LENGTH in metadata.ifd.thumbnail) { + this.vlog('Read thumbnail image.'); + br.seek(metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_OFFSET].value); + metadata.thumbnailURL = br.readImage( + metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_LENGTH].value); + } else { + this.vlog('Image has EXIF data, but no JPG thumbnail.'); + } +}; + +/** + * @param {Object} metadata // TODO(JSDOC). + * @param {number} width // TODO(JSDOC). + * @param {number} height // TODO(JSDOC). + */ +ExifParser.setImageSize = function(metadata, width, height) { + if (metadata.imageTransform && metadata.imageTransform.rotate90) { + metadata.width = height; + metadata.height = width; + } else { + metadata.width = width; + metadata.height = height; + } +}; + +/** + * @param {ByteReader} br // TODO(JSDOC). + * @return {number} // TODO(JSDOC). + */ +ExifParser.prototype.readMark = function(br) { + return br.readScalar(2); +}; + +/** + * @param {ByteReader} br // TODO(JSDOC). + * @return {number} // TODO(JSDOC). + */ +ExifParser.prototype.readMarkLength = function(br) { + // Length includes the 2 bytes used to store the length. + return br.readScalar(2) - 2; +}; + +/** + * @param {ByteReader} br // TODO(JSDOC). + * @param {Array.<Object>} tags // TODO(JSDOC). + * @return {number} // TODO(JSDOC). + */ +ExifParser.prototype.readDirectory = function(br, tags) { + var entryCount = br.readScalar(2); + for (var i = 0; i < entryCount; i++) { + var tagId = br.readScalar(2); + var tag = tags[tagId] = {id: tagId}; + tag.format = br.readScalar(2); + tag.componentCount = br.readScalar(4); + this.readTagValue(br, tag); + } + + return br.readScalar(4); +}; + +/** + * @param {ByteReader} br // TODO(JSDOC). + * @param {Object} tag // TODO(JSDOC). + */ +ExifParser.prototype.readTagValue = function(br, tag) { + var self = this; + + function safeRead(size, readFunction, signed) { + try { + unsafeRead(size, readFunction, signed); + } catch (ex) { + self.log('error reading tag 0x' + tag.id.toString(16) + '/' + + tag.format + ', size ' + tag.componentCount + '*' + size + ' ' + + (ex.stack || '<no stack>') + ': ' + ex); + tag.value = null; + } + } + + function unsafeRead(size, readFunction, signed) { + if (!readFunction) + readFunction = function(size) { return br.readScalar(size, signed) }; + + var totalSize = tag.componentCount * size; + if (totalSize < 1) { + // This is probably invalid exif data, skip it. + tag.componentCount = 1; + tag.value = br.readScalar(4); + return; + } + + if (totalSize > 4) { + // If the total size is > 4, the next 4 bytes will be a pointer to the + // actual data. + br.pushSeek(br.readScalar(4)); + } + + if (tag.componentCount == 1) { + tag.value = readFunction(size); + } else { + // Read multiple components into an array. + tag.value = []; + for (var i = 0; i < tag.componentCount; i++) + tag.value[i] = readFunction(size); + } + + if (totalSize > 4) { + // Go back to the previous position if we had to jump to the data. + br.popSeek(); + } else if (totalSize < 4) { + // Otherwise, if the value wasn't exactly 4 bytes, skip over the + // unread data. + br.seek(4 - totalSize, ByteReader.SEEK_CUR); + } + } + + switch (tag.format) { + case 1: // Byte + case 7: // Undefined + safeRead(1); + break; + + case 2: // String + safeRead(1); + if (tag.componentCount == 0) { + tag.value = ''; + } else if (tag.componentCount == 1) { + tag.value = String.fromCharCode(tag.value); + } else { + tag.value = String.fromCharCode.apply(null, tag.value); + } + break; + + case 3: // Short + safeRead(2); + break; + + case 4: // Long + safeRead(4); + break; + + case 9: // Signed Long + safeRead(4, null, true); + break; + + case 5: // Rational + safeRead(8, function() { + return [br.readScalar(4), br.readScalar(4)]; + }); + break; + + case 10: // Signed Rational + safeRead(8, function() { + return [br.readScalar(4, true), br.readScalar(4, true)]; + }); + break; + + default: // ??? + this.vlog('Unknown tag format 0x' + Number(tag.id).toString(16) + + ': ' + tag.format); + safeRead(4); + break; + } + + this.vlog('Read tag: 0x' + tag.id.toString(16) + '/' + tag.format + ': ' + + tag.value); +}; + +/** + * TODO(JSDOC) + * @const + * @type {Array.<number>} + */ +ExifParser.SCALEX = [1, -1, -1, 1, 1, 1, -1, -1]; + +/** + * TODO(JSDOC) + * @const + * @type {Array.<number>} + */ +ExifParser.SCALEY = [1, 1, -1, -1, -1, 1, 1, -1]; + +/** + * TODO(JSDOC) + * @const + * @type {Array.<number>} + */ +ExifParser.ROTATE90 = [0, 0, 0, 0, 1, 1, 1, 1]; + +/** + * Transform exif-encoded orientation into a set of parameters compatible with + * CSS and canvas transforms (scaleX, scaleY, rotation). + * + * @param {Object} ifd exif property dictionary (image or thumbnail). + * @return {Object} // TODO(JSDOC). + */ +ExifParser.prototype.parseOrientation = function(ifd) { + if (ifd[EXIF_TAG_ORIENTATION]) { + var index = (ifd[EXIF_TAG_ORIENTATION].value || 1) - 1; + return { + scaleX: ExifParser.SCALEX[index], + scaleY: ExifParser.SCALEY[index], + rotate90: ExifParser.ROTATE90[index] + }; + } + return null; +}; + +MetadataDispatcher.registerParserClass(ExifParser); diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/function_parallel.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/function_parallel.js new file mode 100644 index 00000000000..1691a89a1fa --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/function_parallel.js @@ -0,0 +1,82 @@ +// 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'; + +/** + * @class FunctionSequence to invoke steps in sequence + * + * @param {string} name // TODO(JSDOC). + * @param {Array.<function>} steps Array of functions to invoke in parallel. + * @param {Object} logger // TODO(JSDOC). + * @param {function()} callback Callback to invoke on success. + * @param {function(string)} failureCallback Callback to invoke on failure. + * @constructor + */ +function FunctionParallel(name, steps, logger, callback, failureCallback) { + // Private variables hidden in closure + this.currentStepIdx_ = -1; + this.failed_ = false; + this.steps_ = steps; + this.callback_ = callback; + this.failureCallback_ = failureCallback; + this.logger = logger; + this.name = name; + + this.remaining = this.steps_.length; + + this.nextStep = this.nextStep_.bind(this); + this.onError = this.onError_.bind(this); + this.apply = this.start.bind(this); +} + + +/** + * Error handling function, which fires error callback. + * + * @param {string} err Error message. + * @private + */ +FunctionParallel.prototype.onError_ = function(err) { + if (!this.failed_) { + this.failed_ = true; + this.failureCallback_(err); + } +}; + +/** + * Advances to next step. This method should not be used externally. In external + * cases should be used nextStep function, which is defined in closure and thus + * has access to internal variables of functionsequence. + * + * @private + */ +FunctionParallel.prototype.nextStep_ = function() { + if (--this.remaining == 0 && !this.failed_) { + this.callback_(); + } +}; + +/** + * This function should be called only once on start, so start all the children + * at once + * @param {...} var_args // TODO(JSDOC). + */ +FunctionParallel.prototype.start = function(var_args) { + this.logger.vlog('Starting [' + this.steps_.length + '] parallel tasks ' + + 'with ' + arguments.length + ' argument(s)'); + if (this.logger.verbose) { + for (var j = 0; j < arguments.length; j++) { + this.logger.vlog(arguments[j]); + } + } + for (var i = 0; i < this.steps_.length; i++) { + this.logger.vlog('Attempting to start step [' + this.steps_[i].name + ']'); + try { + this.steps_[i].apply(this, arguments); + } catch (e) { + this.onError(e.toString()); + } + } +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/function_sequence.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/function_sequence.js new file mode 100644 index 00000000000..a2372f9f146 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/function_sequence.js @@ -0,0 +1,133 @@ +// 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'; + +/** + * @class FunctionSequence to invoke steps in sequence + * + * @param {string} name // TODO(JSDOC). + * @param {Array} steps array of functions to invoke in sequence. + * @param {Object} logger logger. + * @param {function} callback callback to invoke on success. + * @param {function} failureCallback callback to invoke on failure. + * @constructor + */ +function FunctionSequence(name, steps, logger, callback, failureCallback) { + // Private variables hidden in closure + this.currentStepIdx_ = -1; + this.failed_ = false; + this.steps_ = steps; + this.callback_ = callback; + this.failureCallback_ = failureCallback; + this.logger = logger; + this.name = name; + + this.onError = this.onError_.bind(this); + this.finish = this.finish_.bind(this); + this.nextStep = this.nextStep_.bind(this); + this.apply = this.apply_.bind(this); +} + +/** + * Sets new callback + * + * @param {function} callback new callback to call on succeed. + */ +FunctionSequence.prototype.setCallback = function(callback) { + this.callback_ = callback; +}; + +/** + * Sets new error callback + * + * @param {function} failureCallback new callback to call on failure. + */ +FunctionSequence.prototype.setFailureCallback = function(failureCallback) { + this.failureCallback_ = failureCallback; +}; + + +/** + * Error handling function, which traces current error step, stops sequence + * advancing and fires error callback. + * + * @param {string} err Error message. + * @private + */ +FunctionSequence.prototype.onError_ = function(err) { + this.logger.vlog('Failed step: ' + this.steps_[this.currentStepIdx_].name + + ': ' + err); + if (!this.failed_) { + this.failed_ = true; + this.failureCallback_(err); + } +}; + +/** + * Finishes sequence processing and jumps to the last step. + * This method should not be used externally. In external + * cases should be used finish function, which is defined in closure and thus + * has access to internal variables of functionsequence. + * @private + */ +FunctionSequence.prototype.finish_ = function() { + if (!this.failed_ && this.currentStepIdx_ < this.steps_.length) { + this.currentStepIdx_ = this.steps_.length; + this.callback_(); + } +}; + +/** + * Advances to next step. + * This method should not be used externally. In external + * cases should be used nextStep function, which is defined in closure and thus + * has access to internal variables of functionsequence. + * @private + * @param {...} var_args // TODO(JSDOC). + */ +FunctionSequence.prototype.nextStep_ = function(var_args) { + if (this.failed_) { + return; + } + + if (++this.currentStepIdx_ >= this.steps_.length) { + this.logger.vlog('Sequence ended'); + this.callback_.apply(this, arguments); + } else { + this.logger.vlog('Attempting to start step [' + + this.steps_[this.currentStepIdx_].name + + ']'); + try { + this.steps_[this.currentStepIdx_].apply(this, arguments); + } catch (e) { + this.onError(e.toString()); + } + } +}; + +/** + * This function should be called only once on start, so start sequence pipeline + * @param {...} var_args // TODO(JSDOC). + */ +FunctionSequence.prototype.start = function(var_args) { + if (this.started) { + throw new Error('"Start" method of FunctionSequence was called twice'); + } + + this.logger.log('Starting sequence with ' + arguments.length + ' arguments'); + + this.started = true; + this.nextStep.apply(this, arguments); +}; + +/** + * Add Function object mimics to FunctionSequence + * @private + * @param {*} obj // TODO(JSDOC). + * @param {Array.*} args // TODO(JSDOC). + */ +FunctionSequence.prototype.apply_ = function(obj, args) { + this.start.apply(this, args); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/id3_parser.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/id3_parser.js new file mode 100644 index 00000000000..4037d02e43e --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/id3_parser.js @@ -0,0 +1,708 @@ +// 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'; + +importScripts('function_sequence.js'); +importScripts('function_parallel.js'); + +function Id3Parser(parent) { + MetadataParser.call(this, parent, 'id3', /\.(mp3)$/i); +} + +Id3Parser.prototype = {__proto__: MetadataParser.prototype}; + +/** + * Reads synchsafe integer. + * 'SynchSafe' term is taken from id3 documentation. + * + * @param {ByteReader} reader - reader to use. + * @param {number} length - bytes to read. + * @return {number} // TODO(JSDOC). + * @private + */ +Id3Parser.readSynchSafe_ = function(reader, length) { + var rv = 0; + + switch (length) { + case 4: + rv = reader.readScalar(1, false) << 21; + case 3: + rv |= reader.readScalar(1, false) << 14; + case 2: + rv |= reader.readScalar(1, false) << 7; + case 1: + rv |= reader.readScalar(1, false); + } + + return rv; +}; + +/** + * Reads 3bytes integer. + * + * @param {ByteReader} reader - reader to use. + * @return {number} // TODO(JSDOC). + * @private + */ +Id3Parser.readUInt24_ = function(reader) { + return reader.readScalar(2, false) << 16 | reader.readScalar(1, false); +}; + +/** + * Reads string from reader with specified encoding + * + * @param {ByteReader} reader reader to use. + * @param {number} encoding string encoding. + * @param {number} size maximum string size. Actual result may be shorter. + * @return {string} // TODO(JSDOC). + * @private + */ +Id3Parser.prototype.readString_ = function(reader, encoding, size) { + switch (encoding) { + case Id3Parser.v2.ENCODING.ISO_8859_1: + return reader.readNullTerminatedString(size); + + case Id3Parser.v2.ENCODING.UTF_16: + return reader.readNullTerminatedStringUTF16(true, size); + + case Id3Parser.v2.ENCODING.UTF_16BE: + return reader.readNullTerminatedStringUTF16(false, size); + + case Id3Parser.v2.ENCODING.UTF_8: + // TODO: implement UTF_8. + this.log('UTF8 encoding not supported, used ISO_8859_1 instead'); + return reader.readNullTerminatedString(size); + + default: { + this.log('Unsupported encoding in ID3 tag: ' + encoding); + return ''; + } + } +}; + +/** + * Reads text frame from reader. + * + * @param {ByteReader} reader reader to use. + * @param {number} majorVersion major id3 version to use. + * @param {Object} frame frame so store data at. + * @param {number} end frame end position in reader. + * @private + */ +Id3Parser.prototype.readTextFrame_ = function(reader, + majorVersion, + frame, + end) { + frame.encoding = reader.readScalar(1, false, end); + frame.value = this.readString_(reader, frame.encoding, end - reader.tell()); +}; + +/** + * Reads user defined text frame from reader. + * + * @param {ByteReader} reader reader to use. + * @param {number} majorVersion major id3 version to use. + * @param {Object} frame frame so store data at. + * @param {number} end frame end position in reader. + * @private + */ +Id3Parser.prototype.readUserDefinedTextFrame_ = function(reader, + majorVersion, + frame, + end) { + frame.encoding = reader.readScalar(1, false, end); + + frame.description = this.readString_( + reader, + frame.encoding, + end - reader.tell()); + + frame.value = this.readString_( + reader, + frame.encoding, + end - reader.tell()); +}; + +/** + * @param {ByteReader} reader Reader to use. + * @param {number} majorVersion Major id3 version to use. + * @param {Object} frame Frame so store data at. + * @param {number} end Frame end position in reader. + * @private + */ +Id3Parser.prototype.readPIC_ = function(reader, majorVersion, frame, end) { + frame.encoding = reader.readScalar(1, false, end); + frame.format = reader.readNullTerminatedString(3, end - reader.tell()); + frame.pictureType = reader.readScalar(1, false, end); + frame.description = this.readString_(reader, + frame.encoding, + end - reader.tell()); + + + if (frame.format == '-->') { + frame.imageUrl = reader.readNullTerminatedString(end - reader.tell()); + } else { + frame.imageUrl = reader.readImage(end - reader.tell()); + } +}; + +/** + * @param {ByteReader} reader Reader to use. + * @param {number} majorVersion Major id3 version to use. + * @param {Object} frame Frame so store data at. + * @param {number} end Frame end position in reader. + * @private + */ +Id3Parser.prototype.readAPIC_ = function(reader, majorVersion, frame, end) { + this.vlog('Extracting picture'); + frame.encoding = reader.readScalar(1, false, end); + frame.mime = reader.readNullTerminatedString(end - reader.tell()); + frame.pictureType = reader.readScalar(1, false, end); + frame.description = this.readString_( + reader, + frame.encoding, + end - reader.tell()); + + if (frame.mime == '-->') { + frame.imageUrl = reader.readNullTerminatedString(end - reader.tell()); + } else { + frame.imageUrl = reader.readImage(end - reader.tell()); + } +}; + +/** + * Reads string from reader with specified encoding + * + * @param {ByteReader} reader reader to use. + * @param {number} majorVersion // TODO(JSDOC). + * @return {Object} frame read. + * @private + */ +Id3Parser.prototype.readFrame_ = function(reader, majorVersion) { + if (reader.eof()) + return null; + + var frame = {}; + + reader.pushSeek(reader.tell(), ByteReader.SEEK_BEG); + + var position = reader.tell(); + + frame.name = (majorVersion == 2) ? reader.readNullTerminatedString(3) : + reader.readNullTerminatedString(4); + + if (frame.name == '') + return null; + + this.vlog('Found frame ' + (frame.name) + ' at position ' + position); + + switch (majorVersion) { + case 2: + frame.size = Id3Parser.readUInt24_(reader); + frame.headerSize = 6; + break; + case 3: + frame.size = reader.readScalar(4, false); + frame.headerSize = 10; + frame.flags = reader.readScalar(2, false); + break; + case 4: + frame.size = Id3Parser.readSynchSafe_(reader, 4); + frame.headerSize = 10; + frame.flags = reader.readScalar(2, false); + break; + } + + this.vlog('Found frame [' + frame.name + '] with size [' + frame.size + ']'); + + if (Id3Parser.v2.HANDLERS[frame.name]) { + Id3Parser.v2.HANDLERS[frame.name].call( + this, + reader, + majorVersion, + frame, + reader.tell() + frame.size); + } else if (frame.name.charAt(0) == 'T' || frame.name.charAt(0) == 'W') { + this.readTextFrame_( + reader, + majorVersion, + frame, + reader.tell() + frame.size); + } + + reader.popSeek(); + + reader.seek(frame.size + frame.headerSize, ByteReader.SEEK_CUR); + + return frame; +}; + +/** + * @param {File} file // TODO(JSDOC). + * @param {Object} metadata // TODO(JSDOC). + * @param {function(Object)} callback // TODO(JSDOC). + * @param {function(etring)} onError // TODO(JSDOC). + */ +Id3Parser.prototype.parse = function(file, metadata, callback, onError) { + var self = this; + + this.log('Starting id3 parser for ' + file.name); + + var id3v1Parser = new FunctionSequence( + 'id3v1parser', + [ + /** + * Reads last 128 bytes of file in bytebuffer, + * which passes further. + * In last 128 bytes should be placed ID3v1 tag if available. + * @param {File} file File which bytes to read. + */ + function readTail(file) { + util.readFileBytes(file, file.size - 128, file.size, + this.nextStep, this.onError, this); + }, + + /** + * Attempts to extract ID3v1 tag from 128 bytes long ByteBuffer + * @param {File} file File which tags are being extracted. Could be used + * for logging purposes. + * @param {ByteReader} reader ByteReader of 128 bytes. + */ + function extractId3v1(file, reader) { + if (reader.readString(3) == 'TAG') { + this.logger.vlog('id3v1 found'); + var id3v1 = metadata.id3v1 = {}; + + var title = reader.readNullTerminatedString(30).trim(); + + if (title.length > 0) { + metadata.title = title; + } + + reader.seek(3 + 30, ByteReader.SEEK_BEG); + + var artist = reader.readNullTerminatedString(30).trim(); + if (artist.length > 0) { + metadata.artist = artist; + } + + reader.seek(3 + 30 + 30, ByteReader.SEEK_BEG); + + var album = reader.readNullTerminatedString(30).trim(); + if (album.length > 0) { + metadata.album = album; + } + } + this.nextStep(); + } + ], + this + ); + + var id3v2Parser = new FunctionSequence( + 'id3v2parser', + [ + function readHead(file) { + util.readFileBytes(file, 0, 10, this.nextStep, this.onError, + this); + }, + + /** + * Check if passed array of 10 bytes contains ID3 header. + * @param {File} file File to check and continue reading if ID3 + * metadata found. + * @param {ByteReader} reader Reader to fill with stream bytes. + */ + function checkId3v2(file, reader) { + if (reader.readString(3) == 'ID3') { + this.logger.vlog('id3v2 found'); + var id3v2 = metadata.id3v2 = {}; + id3v2.major = reader.readScalar(1, false); + id3v2.minor = reader.readScalar(1, false); + id3v2.flags = reader.readScalar(1, false); + id3v2.size = Id3Parser.readSynchSafe_(reader, 4); + + util.readFileBytes(file, 10, 10 + id3v2.size, this.nextStep, + this.onError, this); + } else { + this.finish(); + } + }, + + /** + * Extracts all ID3v2 frames from given bytebuffer. + * @param {File} file File being parsed. + * @param {ByteReader} reader Reader to use for metadata extraction. + */ + function extractFrames(file, reader) { + var id3v2 = metadata.id3v2; + + if ((id3v2.major > 2) && + (id3v2.flags & Id3Parser.v2.FLAG_EXTENDED_HEADER != 0)) { + // Skip extended header if found + if (id3v2.major == 3) { + reader.seek(reader.readScalar(4, false) - 4); + } else if (id3v2.major == 4) { + reader.seek(Id3Parser.readSynchSafe_(reader, 4) - 4); + } + } + + var frame; + + while (frame = self.readFrame_(reader, id3v2.major)) { + metadata.id3v2[frame.name] = frame; + } + + this.nextStep(); + }, + + /** + * Adds 'description' object to metadata. + * 'description' used to unify different parsers and make + * metadata parser-aware. + * Description is array if value-type pairs. Type should be used + * to properly format value before displaying to user. + */ + function prepareDescription() { + var id3v2 = metadata.id3v2; + + if (id3v2['APIC']) + metadata.thumbnailURL = id3v2['APIC'].imageUrl; + else if (id3v2['PIC']) + metadata.thumbnailURL = id3v2['PIC'].imageUrl; + + metadata.description = []; + + for (var key in id3v2) { + if (typeof(Id3Parser.v2.MAPPERS[key]) != 'undefined' && + id3v2[key].value.trim().length > 0) { + metadata.description.push({ + key: Id3Parser.v2.MAPPERS[key], + value: id3v2[key].value.trim() + }); + } + } + + function extract(propName, tags) { + for (var i = 1; i != arguments.length; i++) { + var tag = id3v2[arguments[i]]; + if (tag && tag.value) { + metadata[propName] = tag.value; + break; + } + } + } + + extract('album', 'TALB', 'TAL'); + extract('title', 'TIT2', 'TT2'); + extract('artist', 'TPE1', 'TP1'); + + metadata.description.sort(function(a, b) { + return Id3Parser.METADATA_ORDER.indexOf(a.key) - + Id3Parser.METADATA_ORDER.indexOf(b.key); + }); + this.nextStep(); + } + ], + this + ); + + var metadataParser = new FunctionParallel( + 'mp3metadataParser', + [id3v1Parser, id3v2Parser], + this, + function() { + callback.call(null, metadata); + }, + onError + ); + + id3v1Parser.setCallback(metadataParser.nextStep); + id3v2Parser.setCallback(metadataParser.nextStep); + + id3v1Parser.setFailureCallback(metadataParser.onError); + id3v2Parser.setFailureCallback(metadataParser.onError); + + this.vlog('Passed argument : ' + file); + + metadataParser.start(file); +}; + + +/** + * Metadata order to use for metadata generation + */ +Id3Parser.METADATA_ORDER = [ + 'ID3_TITLE', + 'ID3_LEAD_PERFORMER', + 'ID3_YEAR', + 'ID3_ALBUM', + 'ID3_TRACK_NUMBER', + 'ID3_BPM', + 'ID3_COMPOSER', + 'ID3_DATE', + 'ID3_PLAYLIST_DELAY', + 'ID3_LYRICIST', + 'ID3_FILE_TYPE', + 'ID3_TIME', + 'ID3_LENGTH', + 'ID3_FILE_OWNER', + 'ID3_BAND', + 'ID3_COPYRIGHT', + 'ID3_OFFICIAL_AUDIO_FILE_WEBPAGE', + 'ID3_OFFICIAL_ARTIST', + 'ID3_OFFICIAL_AUDIO_SOURCE_WEBPAGE', + 'ID3_PUBLISHERS_OFFICIAL_WEBPAGE' +]; + + +/** + * id3v1 constants + */ +Id3Parser.v1 = { + /** + * Genres list as described in id3 documentation. We aren't going to + * localize this list, because at least in Russian (and I think most + * other languages), translation exists at least for 10% and most time + * translation would degrade to transliteration. + */ + GENRES: [ + 'Blues', + 'Classic Rock', + 'Country', + 'Dance', + 'Disco', + 'Funk', + 'Grunge', + 'Hip-Hop', + 'Jazz', + 'Metal', + 'New Age', + 'Oldies', + 'Other', + 'Pop', + 'R&B', + 'Rap', + 'Reggae', + 'Rock', + 'Techno', + 'Industrial', + 'Alternative', + 'Ska', + 'Death Metal', + 'Pranks', + 'Soundtrack', + 'Euro-Techno', + 'Ambient', + 'Trip-Hop', + 'Vocal', + 'Jazz+Funk', + 'Fusion', + 'Trance', + 'Classical', + 'Instrumental', + 'Acid', + 'House', + 'Game', + 'Sound Clip', + 'Gospel', + 'Noise', + 'AlternRock', + 'Bass', + 'Soul', + 'Punk', + 'Space', + 'Meditative', + 'Instrumental Pop', + 'Instrumental Rock', + 'Ethnic', + 'Gothic', + 'Darkwave', + 'Techno-Industrial', + 'Electronic', + 'Pop-Folk', + 'Eurodance', + 'Dream', + 'Southern Rock', + 'Comedy', + 'Cult', + 'Gangsta', + 'Top 40', + 'Christian Rap', + 'Pop/Funk', + 'Jungle', + 'Native American', + 'Cabaret', + 'New Wave', + 'Psychadelic', + 'Rave', + 'Showtunes', + 'Trailer', + 'Lo-Fi', + 'Tribal', + 'Acid Punk', + 'Acid Jazz', + 'Polka', + 'Retro', + 'Musical', + 'Rock & Roll', + 'Hard Rock', + 'Folk', + 'Folk-Rock', + 'National Folk', + 'Swing', + 'Fast Fusion', + 'Bebob', + 'Latin', + 'Revival', + 'Celtic', + 'Bluegrass', + 'Avantgarde', + 'Gothic Rock', + 'Progressive Rock', + 'Psychedelic Rock', + 'Symphonic Rock', + 'Slow Rock', + 'Big Band', + 'Chorus', + 'Easy Listening', + 'Acoustic', + 'Humour', + 'Speech', + 'Chanson', + 'Opera', + 'Chamber Music', + 'Sonata', + 'Symphony', + 'Booty Bass', + 'Primus', + 'Porn Groove', + 'Satire', + 'Slow Jam', + 'Club', + 'Tango', + 'Samba', + 'Folklore', + 'Ballad', + 'Power Ballad', + 'Rhythmic Soul', + 'Freestyle', + 'Duet', + 'Punk Rock', + 'Drum Solo', + 'A capella', + 'Euro-House', + 'Dance Hall', + 'Goa', + 'Drum & Bass', + 'Club-House', + 'Hardcore', + 'Terror', + 'Indie', + 'BritPop', + 'Negerpunk', + 'Polsk Punk', + 'Beat', + 'Christian Gangsta Rap', + 'Heavy Metal', + 'Black Metal', + 'Crossover', + 'Contemporary Christian', + 'Christian Rock', + 'Merengue', + 'Salsa', + 'Thrash Metal', + 'Anime', + 'Jpop', + 'Synthpop' + ] +}; + +/** + * id3v2 constants + */ +Id3Parser.v2 = { + FLAG_EXTENDED_HEADER: 1 << 5, + + ENCODING: { + /** + * ISO-8859-1 [ISO-8859-1]. Terminated with $00. + * + * @const + * @type {number} + */ + ISO_8859_1: 0, + + + /** + * [UTF-16] encoded Unicode [UNICODE] with BOM. All + * strings in the same frame SHALL have the same byteorder. + * Terminated with $00 00. + * + * @const + * @type {number} + */ + UTF_16: 1, + + /** + * UTF-16BE [UTF-16] encoded Unicode [UNICODE] without BOM. + * Terminated with $00 00. + * + * @const + * @type {number} + */ + UTF_16BE: 2, + + /** + * UTF-8 [UTF-8] encoded Unicode [UNICODE]. Terminated with $00. + * + * @const + * @type {number} + */ + UTF_8: 3 + }, + HANDLERS: { + //User defined text information frame + TXX: Id3Parser.prototype.readUserDefinedTextFrame_, + //User defined URL link frame + WXX: Id3Parser.prototype.readUserDefinedTextFrame_, + + //User defined text information frame + TXXX: Id3Parser.prototype.readUserDefinedTextFrame_, + + //User defined URL link frame + WXXX: Id3Parser.prototype.readUserDefinedTextFrame_, + + //User attached image + PIC: Id3Parser.prototype.readPIC_, + + //User attached image + APIC: Id3Parser.prototype.readAPIC_ + }, + MAPPERS: { + TALB: 'ID3_ALBUM', + TBPM: 'ID3_BPM', + TCOM: 'ID3_COMPOSER', + TDAT: 'ID3_DATE', + TDLY: 'ID3_PLAYLIST_DELAY', + TEXT: 'ID3_LYRICIST', + TFLT: 'ID3_FILE_TYPE', + TIME: 'ID3_TIME', + TIT2: 'ID3_TITLE', + TLEN: 'ID3_LENGTH', + TOWN: 'ID3_FILE_OWNER', + TPE1: 'ID3_LEAD_PERFORMER', + TPE2: 'ID3_BAND', + TRCK: 'ID3_TRACK_NUMBER', + TYER: 'ID3_YEAR', + WCOP: 'ID3_COPYRIGHT', + WOAF: 'ID3_OFFICIAL_AUDIO_FILE_WEBPAGE', + WOAR: 'ID3_OFFICIAL_ARTIST', + WOAS: 'ID3_OFFICIAL_AUDIO_SOURCE_WEBPAGE', + WPUB: 'ID3_PUBLISHERS_OFFICIAL_WEBPAGE' + } +}; + +MetadataDispatcher.registerParserClass(Id3Parser); diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/image_parsers.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/image_parsers.js new file mode 100644 index 00000000000..52c4fc4e8a8 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/image_parsers.js @@ -0,0 +1,198 @@ +// 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'; + +/* Base class for image metadata parsers that only need to look at a short + fragment at the start of the file */ +function SimpleImageParser(parent, type, urlFilter, headerSize) { + ImageParser.call(this, parent, type, urlFilter); + this.headerSize = headerSize; +} + +SimpleImageParser.prototype = {__proto__: ImageParser.prototype}; + +/** + * @param {File} file // TODO(JSDOC). + * @param {Object} metadata // TODO(JSDOC). + * @param {function(Object)} callback // TODO(JSDOC). + * @param {function(string)} errorCallback // TODO(JSDOC). + */ +SimpleImageParser.prototype.parse = function( + file, metadata, callback, errorCallback) { + var self = this; + util.readFileBytes(file, 0, this.headerSize, + function(file, br) { + try { + self.parseHeader(metadata, br); + callback(metadata); + } catch (e) { + errorCallback(e.toString()); + } + }, + errorCallback); +}; + + +function PngParser(parent) { + SimpleImageParser.call(this, parent, 'png', /\.png$/i, 24); +} + +PngParser.prototype = {__proto__: SimpleImageParser.prototype}; + +/** + * @param {Object} metadata // TODO(JSDOC). + * @param {ByteReader} br // TODO(JSDOC). + */ +PngParser.prototype.parseHeader = function(metadata, br) { + br.setByteOrder(ByteReader.BIG_ENDIAN); + + var signature = br.readString(8); + if (signature != '\x89PNG\x0D\x0A\x1A\x0A') + throw new Error('Invalid PNG signature: ' + signature); + + br.seek(12); + var ihdr = br.readString(4); + if (ihdr != 'IHDR') + throw new Error('Missing IHDR chunk'); + + metadata.width = br.readScalar(4); + metadata.height = br.readScalar(4); +}; + +MetadataDispatcher.registerParserClass(PngParser); + + +function BmpParser(parent) { + SimpleImageParser.call(this, parent, 'bmp', /\.bmp$/i, 28); +} + +BmpParser.prototype = {__proto__: SimpleImageParser.prototype}; + +/** + * @param {Object} metadata // TODO(JSDOC). + * @param {ByteReader} br // TODO(JSDOC). + */ +BmpParser.prototype.parseHeader = function(metadata, br) { + br.setByteOrder(ByteReader.LITTLE_ENDIAN); + + var signature = br.readString(2); + if (signature != 'BM') + throw new Error('Invalid BMP signature: ' + signature); + + br.seek(18); + metadata.width = br.readScalar(4); + metadata.height = br.readScalar(4); +}; + +MetadataDispatcher.registerParserClass(BmpParser); + + +function GifParser(parent) { + SimpleImageParser.call(this, parent, 'gif', /\.Gif$/i, 10); +} + +GifParser.prototype = {__proto__: SimpleImageParser.prototype}; + +/** + * @param {Object} metadata // TODO(JSDOC). + * @param {ByteReader} br // TODO(JSDOC). + */ +GifParser.prototype.parseHeader = function(metadata, br) { + br.setByteOrder(ByteReader.LITTLE_ENDIAN); + + var signature = br.readString(6); + if (!signature.match(/GIF8(7|9)a/)) + throw new Error('Invalid GIF signature: ' + signature); + + metadata.width = br.readScalar(2); + metadata.height = br.readScalar(2); +}; + +MetadataDispatcher.registerParserClass(GifParser); + + +function WebpParser(parent) { + SimpleImageParser.call(this, parent, 'webp', /\.webp$/i, 30); +} + +WebpParser.prototype = {__proto__: SimpleImageParser.prototype}; + +/** + * @param {Object} metadata // TODO(JSDOC). + * @param {ByteReader} br // TODO(JSDOC). + */ +WebpParser.prototype.parseHeader = function(metadata, br) { + br.setByteOrder(ByteReader.LITTLE_ENDIAN); + + var riffSignature = br.readString(4); + if (riffSignature != 'RIFF') + throw new Error('Invalid RIFF signature: ' + riffSignature); + + br.seek(8); + var webpSignature = br.readString(4); + if (webpSignature != 'WEBP') + throw new Error('Invalid WEBP signature: ' + webpSignature); + + var chunkFormat = br.readString(4); + if (chunkFormat != 'VP8 ' && chunkFormat != 'VP8L') + throw new Error('Invalid chunk format: ' + chunkFormat); + + if (chunkFormat == 'VP8 ') { + // VP8 lossy bitstream format. + br.seek(23); + var lossySignature = br.readScalar(2) | (br.readScalar(1) << 16); + if (lossySignature != 0x2a019d) + throw new Error('Invalid VP8 lossy bitstream signature: ' + + lossySignature); + + var dimensionBits = br.readScalar(4); + metadata.width = dimensionBits & 0x3fff; + metadata.height = (dimensionBits >> 16) & 0x3fff; + } else { + // VP8 lossless bitstream format. + br.seek(20); + var losslessSignature = br.readScalar(1); + if (losslessSignature != 0x2f) + throw new Error('Invalid VP8 lossless bitstream signature: ' + + losslessSignature); + + var dimensionBits = br.readScalar(4); + metadata.width = (dimensionBits & 0x3fff) + 1; + metadata.height = ((dimensionBits >> 14) & 0x3fff) + 1; + } +}; + +MetadataDispatcher.registerParserClass(WebpParser); + +/** + * Parser for the header of .ico icon files. + * @param {MetadataDispatcher} parent Parent metadata dispatcher object. + * @constructor + * @extends SimpleImageParser + */ +function IcoParser(parent) { + SimpleImageParser.call(this, parent, 'ico', /\.ico$/i, 8); +} + +IcoParser.prototype = {__proto__: SimpleImageParser.prototype}; + +/** + * Parse the binary data as a ico header and stores to metadata. + * @param {Object} metadata Dictionary to store the parser metadata. + * @param {ByteReader} byteReader Reader for header binary data. + */ +IcoParser.prototype.parseHeader = function(metadata, byteReader) { + byteReader.setByteOrder(ByteReader.LITTLE_ENDIAN); + + var signature = byteReader.readString(4); + if (signature !== '\x00\x00\x00\x01') + throw new Error('Invalid ICO signature: ' + signature); + + byteReader.seek(2); + metadata.width = byteReader.readScalar(1); + metadata.height = byteReader.readScalar(1); +}; + +MetadataDispatcher.registerParserClass(IcoParser); diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_cache.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_cache.js new file mode 100644 index 00000000000..4bbbe182958 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_cache.js @@ -0,0 +1,1042 @@ +// 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'; + +/** + * MetadataCache is a map from Entry to an object containing properties. + * Properties are divided by types, and all properties of one type are accessed + * at once. + * Some of the properties: + * { + * filesystem: size, modificationTime + * internal: presence + * drive: pinned, present, hosted, availableOffline + * streaming: (no property) + * + * Following are not fetched for non-present drive files. + * media: artist, album, title, width, height, imageTransform, etc. + * thumbnail: url, transform + * + * Following are always fetched from content, and so force the downloading + * of remote drive files. One should use this for required content metadata, + * i.e. image orientation. + * fetchedMedia: width, height, etc. + * } + * + * Typical usages: + * { + * cache.get([entry1, entry2], 'drive|filesystem', function(metadata) { + * if (metadata[0].drive.pinned && metadata[1].filesystem.size == 0) + * alert("Pinned and empty!"); + * }); + * + * cache.set(entry, 'internal', {presence: 'deleted'}); + * + * cache.clear([fileEntry1, fileEntry2], 'filesystem'); + * + * // Getting fresh value. + * cache.clear(entry, 'thumbnail'); + * cache.get(entry, 'thumbnail', function(thumbnail) { + * img.src = thumbnail.url; + * }); + * + * var cached = cache.getCached(entry, 'filesystem'); + * var size = (cached && cached.size) || UNKNOWN_SIZE; + * } + * + * @constructor + */ +function MetadataCache() { + /** + * Map from Entry (using Entry.toURL) to metadata. Metadata contains + * |properties| - an hierarchical object of values, and an object for each + * metadata provider: <prodiver-id>: {time, callbacks} + * @private + */ + this.cache_ = {}; + + /** + * List of metadata providers. + * @private + */ + this.providers_ = []; + + /** + * List of observers added. Each one is an object with fields: + * re - regexp of urls; + * type - metadata type; + * callback - the callback. + * @private + */ + this.observers_ = []; + this.observerId_ = 0; + + this.batchCount_ = 0; + this.totalCount_ = 0; + + this.currentCacheSize_ = 0; + + /** + * Time of first get query of the current batch. Items updated later than this + * will not be evicted. + * @private + */ + this.lastBatchStart_ = new Date(); +} + +/** + * Observer type: it will be notified if the changed Entry is exactly the same + * as the observed Entry. + */ +MetadataCache.EXACT = 0; + +/** + * Observer type: it will be notified if the changed Entry is an immediate child + * of the observed Entry. + */ +MetadataCache.CHILDREN = 1; + +/** + * Observer type: it will be notified if the changed Entry is a descendant of + * of the observer Entry. + */ +MetadataCache.DESCENDANTS = 2; + +/** + * Margin of the cache size. This amount of caches may be kept in addition. + */ +MetadataCache.EVICTION_THRESHOLD_MARGIN = 500; + +/** + * @return {MetadataCache!} The cache with all providers. + */ +MetadataCache.createFull = function() { + var cache = new MetadataCache(); + cache.providers_.push(new FilesystemProvider()); + cache.providers_.push(new DriveProvider()); + cache.providers_.push(new ContentProvider()); + return cache; +}; + +/** + * Clones metadata entry. Metadata entries may contain scalars, arrays, + * hash arrays and Date object. Other objects are not supported. + * @param {Object} metadata Metadata object. + * @return {Object} Cloned entry. + */ +MetadataCache.cloneMetadata = function(metadata) { + if (metadata instanceof Array) { + var result = []; + for (var index = 0; index < metadata.length; index++) { + result[index] = MetadataCache.cloneMetadata(metadata[index]); + } + return result; + } else if (metadata instanceof Date) { + var result = new Date(); + result.setTime(metadata.getTime()); + return result; + } else if (metadata instanceof Object) { // Hash array only. + var result = {}; + for (var property in metadata) { + if (metadata.hasOwnProperty(property)) + result[property] = MetadataCache.cloneMetadata(metadata[property]); + } + return result; + } else { + return metadata; + } +}; + +/** + * @return {boolean} Whether all providers are ready. + */ +MetadataCache.prototype.isInitialized = function() { + for (var index = 0; index < this.providers_.length; index++) { + if (!this.providers_[index].isInitialized()) return false; + } + return true; +}; + +/** + * Sets the size of cache. The actual cache size may be larger than the given + * value. + * @param {number} size The cache size to be set. + */ +MetadataCache.prototype.setCacheSize = function(size) { + this.currentCacheSize_ = size; + + if (this.totalCount_ > this.currentEvictionThreshold_()) + this.evict_(); +}; + +/** + * Returns the current threshold to evict caches. When the number of caches + * exceeds this, the cache should be evicted. + * @return {number} Threshold to evict caches. + * @private + */ +MetadataCache.prototype.currentEvictionThreshold_ = function() { + return this.currentCacheSize_ * 2 + MetadataCache.EVICTION_THRESHOLD_MARGIN; +}; + +/** + * Fetches the metadata, puts it in the cache, and passes to callback. + * If required metadata is already in the cache, does not fetch it again. + * @param {Entry|Array.<Entry>} entries The list of entries. May be just a + * single item. + * @param {string} type The metadata type. + * @param {function(Object)} callback The metadata is passed to callback. + */ +MetadataCache.prototype.get = function(entries, type, callback) { + if (!(entries instanceof Array)) { + this.getOne(entries, type, callback); + return; + } + + if (entries.length == 0) { + if (callback) callback([]); + return; + } + + var result = []; + var remaining = entries.length; + this.startBatchUpdates(); + + var onOneItem = function(index, value) { + result[index] = value; + remaining--; + if (remaining == 0) { + this.endBatchUpdates(); + if (callback) setTimeout(callback, 0, result); + } + }; + + for (var index = 0; index < entries.length; index++) { + result.push(null); + this.getOne(entries[index], type, onOneItem.bind(this, index)); + } +}; + +/** + * Fetches the metadata for one Entry. See comments to |get|. + * @param {Entry} entry The entry. + * @param {string} type Metadata type. + * @param {function(Object)} callback The callback. + */ +MetadataCache.prototype.getOne = function(entry, type, callback) { + if (type.indexOf('|') != -1) { + var types = type.split('|'); + var result = {}; + var typesLeft = types.length; + + var onOneType = function(requestedType, metadata) { + result[requestedType] = metadata; + typesLeft--; + if (typesLeft == 0) callback(result); + }; + + for (var index = 0; index < types.length; index++) { + this.getOne(entry, types[index], onOneType.bind(null, types[index])); + } + return; + } + + callback = callback || function() {}; + + var entryURL = entry.toURL(); + if (!(entryURL in this.cache_)) { + this.cache_[entryURL] = this.createEmptyItem_(); + this.totalCount_++; + } + + var item = this.cache_[entryURL]; + + if (type in item.properties) { + callback(item.properties[type]); + return; + } + + this.startBatchUpdates(); + var providers = this.providers_.slice(); + var currentProvider; + var self = this; + + var onFetched = function() { + if (type in item.properties) { + self.endBatchUpdates(); + // Got properties from provider. + callback(item.properties[type]); + } else { + tryNextProvider(); + } + }; + + var onProviderProperties = function(properties) { + var id = currentProvider.getId(); + var fetchedCallbacks = item[id].callbacks; + delete item[id].callbacks; + item.time = new Date(); + self.mergeProperties_(entry, properties); + + for (var index = 0; index < fetchedCallbacks.length; index++) { + fetchedCallbacks[index](); + } + }; + + var queryProvider = function() { + var id = currentProvider.getId(); + if ('callbacks' in item[id]) { + // We are querying this provider now. + item[id].callbacks.push(onFetched); + } else { + item[id].callbacks = [onFetched]; + currentProvider.fetch(entry, type, onProviderProperties); + } + }; + + var tryNextProvider = function() { + if (providers.length == 0) { + self.endBatchUpdates(); + callback(item.properties[type] || null); + return; + } + + currentProvider = providers.shift(); + if (currentProvider.supportsEntry(entry) && + currentProvider.providesType(type)) { + queryProvider(); + } else { + tryNextProvider(); + } + }; + + tryNextProvider(); +}; + +/** + * Returns the cached metadata value, or |null| if not present. + * @param {Entry|Array.<Entry>} entries The list of entries. May be just a + * single entry. + * @param {string} type The metadata type. + * @return {Object} The metadata or null. + */ +MetadataCache.prototype.getCached = function(entries, type) { + var single = false; + if (!(entries instanceof Array)) { + single = true; + entries = [entries]; + } + + var result = []; + for (var index = 0; index < entries.length; index++) { + var entryURL = entries[index].toURL(); + result.push(entryURL in this.cache_ ? + (this.cache_[entryURL].properties[type] || null) : null); + } + + return single ? result[0] : result; +}; + +/** + * Puts the metadata into cache + * @param {Entry|Array.<Entry>} entries The list of entries. May be just a + * single entry. + * @param {string} type The metadata type. + * @param {Array.<Object>} values List of corresponding metadata values. + */ +MetadataCache.prototype.set = function(entries, type, values) { + if (!(entries instanceof Array)) { + entries = [entries]; + values = [values]; + } + + this.startBatchUpdates(); + for (var index = 0; index < entries.length; index++) { + var entryURL = entries[index].toURL(); + if (!(entryURL in this.cache_)) { + this.cache_[entryURL] = this.createEmptyItem_(); + this.totalCount_++; + } + this.cache_[entryURL].properties[type] = values[index]; + this.notifyObservers_(entries[index], type); + } + this.endBatchUpdates(); +}; + +/** + * Clears the cached metadata values. + * @param {Entry|Array.<Entry>} entries The list of entries. May be just a + * single entry. + * @param {string} type The metadata types or * for any type. + */ +MetadataCache.prototype.clear = function(entries, type) { + if (!(entries instanceof Array)) + entries = [entries]; + + var types = type.split('|'); + + for (var index = 0; index < entries.length; index++) { + var entry = entries[index]; + var entryURL = entry.toURL(); + if (entryURL in this.cache_) { + if (type === '*') { + this.cache_[entryURL].properties = {}; + } else { + for (var j = 0; j < types.length; j++) { + var type = types[j]; + delete this.cache_[entryURL].properties[type]; + } + } + } + } +}; + +/** + * Clears the cached metadata values recursively. + * @param {Entry} entry An entry to be cleared recursively from cache. + * @param {string} type The metadata types or * for any type. + */ +MetadataCache.prototype.clearRecursively = function(entry, type) { + var types = type.split('|'); + var keys = Object.keys(this.cache_); + var entryURL = entry.toURL(); + + for (var index = 0; index < keys.length; index++) { + var cachedEntryURL = keys[index]; + if (cachedEntryURL.substring(0, entryURL.length) === entryURL) { + if (type === '*') { + this.cache_[cachedEntryURL].properties = {}; + } else { + for (var j = 0; j < types.length; j++) { + var type = types[j]; + delete this.cache_[cachedEntryURL].properties[type]; + } + } + } + } +}; + +/** + * Adds an observer, which will be notified when metadata changes. + * @param {Entry} entry The root entry to look at. + * @param {number} relation This defines, which items will trigger the observer. + * See comments to |MetadataCache.EXACT| and others. + * @param {string} type The metadata type. + * @param {function(Array.<Entry>, Array.<Object>)} observer List of entries + * and corresponding metadata values are passed to this callback. + * @return {number} The observer id, which can be used to remove it. + */ +MetadataCache.prototype.addObserver = function( + entry, relation, type, observer) { + var entryURL = entry.toURL(); + var re; + if (relation == MetadataCache.CHILDREN) + re = entryURL + '(/[^/]*)?'; + else if (relation == MetadataCache.DESCENDANTS) + re = entryURL + '(/.*)?'; + else + re = entryURL; + + var id = ++this.observerId_; + this.observers_.push({ + re: new RegExp('^' + re + '$'), + type: type, + callback: observer, + id: id, + pending: {} + }); + + return id; +}; + +/** + * Removes the observer. + * @param {number} id Observer id. + * @return {boolean} Whether observer was removed or not. + */ +MetadataCache.prototype.removeObserver = function(id) { + for (var index = 0; index < this.observers_.length; index++) { + if (this.observers_[index].id == id) { + this.observers_.splice(index, 1); + return true; + } + } + return false; +}; + +/** + * Start batch updates. + */ +MetadataCache.prototype.startBatchUpdates = function() { + this.batchCount_++; + if (this.batchCount_ == 1) + this.lastBatchStart_ = new Date(); +}; + +/** + * End batch updates. Notifies observers if all nested updates are finished. + */ +MetadataCache.prototype.endBatchUpdates = function() { + this.batchCount_--; + if (this.batchCount_ != 0) return; + if (this.totalCount_ > this.currentEvictionThreshold_()) + this.evict_(); + for (var index = 0; index < this.observers_.length; index++) { + var observer = this.observers_[index]; + var entries = []; + var properties = []; + for (var entryURL in observer.pending) { + if (observer.pending.hasOwnProperty(entryURL) && + entryURL in this.cache_) { + var entry = observer.pending[entryURL]; + entries.push(entry); + properties.push( + this.cache_[entryURL].properties[observer.type] || null); + } + } + observer.pending = {}; + if (entries.length > 0) { + observer.callback(entries, properties); + } + } +}; + +/** + * Notifies observers or puts the data to pending list. + * @param {Entry} entry Changed entry. + * @param {string} type Metadata type. + * @private + */ +MetadataCache.prototype.notifyObservers_ = function(entry, type) { + var entryURL = entry.toURL(); + for (var index = 0; index < this.observers_.length; index++) { + var observer = this.observers_[index]; + if (observer.type == type && observer.re.test(entryURL)) { + if (this.batchCount_ == 0) { + // Observer expects array of urls and array of properties. + observer.callback( + [entry], [this.cache_[entryURL].properties[type] || null]); + } else { + observer.pending[entryURL] = entry; + } + } + } +}; + +/** + * Removes the oldest items from the cache. + * This method never removes the items from last batch. + * @private + */ +MetadataCache.prototype.evict_ = function() { + var toRemove = []; + + // We leave only a half of items, so we will not call evict_ soon again. + var desiredCount = this.currentEvictionThreshold_(); + var removeCount = this.totalCount_ - desiredCount; + for (var url in this.cache_) { + if (this.cache_.hasOwnProperty(url) && + this.cache_[url].time < this.lastBatchStart_) { + toRemove.push(url); + } + } + + toRemove.sort(function(a, b) { + var aTime = this.cache_[a].time; + var bTime = this.cache_[b].time; + return aTime < bTime ? -1 : aTime > bTime ? 1 : 0; + }.bind(this)); + + removeCount = Math.min(removeCount, toRemove.length); + this.totalCount_ -= removeCount; + for (var index = 0; index < removeCount; index++) { + delete this.cache_[toRemove[index]]; + } +}; + +/** + * @return {Object} Empty cache item. + * @private + */ +MetadataCache.prototype.createEmptyItem_ = function() { + var item = {properties: {}}; + for (var index = 0; index < this.providers_.length; index++) { + item[this.providers_[index].getId()] = {}; + } + return item; +}; + +/** + * Caches all the properties from data to cache entry for the entry. + * @param {Entry} entry The file entry. + * @param {Object} data The properties. + * @private + */ +MetadataCache.prototype.mergeProperties_ = function(entry, data) { + if (data == null) return; + var properties = this.cache_[entry.toURL()].properties; + for (var type in data) { + if (data.hasOwnProperty(type) && !properties.hasOwnProperty(type)) { + properties[type] = data[type]; + this.notifyObservers_(entry, type); + } + } +}; + +/** + * Base class for metadata providers. + * @constructor + */ +function MetadataProvider() { +} + +/** + * @param {Entry} entry The entry. + * @return {boolean} Whether this provider supports the entry. + */ +MetadataProvider.prototype.supportsEntry = function(entry) { return false; }; + +/** + * @param {string} type The metadata type. + * @return {boolean} Whether this provider provides this metadata. + */ +MetadataProvider.prototype.providesType = function(type) { return false; }; + +/** + * @return {string} Unique provider id. + */ +MetadataProvider.prototype.getId = function() { return ''; }; + +/** + * @return {boolean} Whether provider is ready. + */ +MetadataProvider.prototype.isInitialized = function() { return true; }; + +/** + * Fetches the metadata. It's suggested to return all the metadata this provider + * can fetch at once. + * @param {Entry} entry File entry. + * @param {string} type Requested metadata type. + * @param {function(Object)} callback Callback expects a map from metadata type + * to metadata value. + */ +MetadataProvider.prototype.fetch = function(entry, type, callback) { + throw new Error('Default metadata provider cannot fetch.'); +}; + + +/** + * Provider of filesystem metadata. + * This provider returns the following objects: + * filesystem: { size, modificationTime } + * @constructor + */ +function FilesystemProvider() { + MetadataProvider.call(this); +} + +FilesystemProvider.prototype = { + __proto__: MetadataProvider.prototype +}; + +/** + * @param {Entry} entry The entry. + * @return {boolean} Whether this provider supports the entry. + */ +FilesystemProvider.prototype.supportsEntry = function(entry) { + return true; +}; + +/** + * @param {string} type The metadata type. + * @return {boolean} Whether this provider provides this metadata. + */ +FilesystemProvider.prototype.providesType = function(type) { + return type == 'filesystem'; +}; + +/** + * @return {string} Unique provider id. + */ +FilesystemProvider.prototype.getId = function() { return 'filesystem'; }; + +/** + * Fetches the metadata. + * @param {Entry} entry File entry. + * @param {string} type Requested metadata type. + * @param {function(Object)} callback Callback expects a map from metadata type + * to metadata value. + */ +FilesystemProvider.prototype.fetch = function( + entry, type, callback) { + function onError(error) { + callback(null); + } + + function onMetadata(entry, metadata) { + callback({ + filesystem: { + size: entry.isFile ? (metadata.size || 0) : -1, + modificationTime: metadata.modificationTime + } + }); + } + + entry.getMetadata(onMetadata.bind(null, entry), onError); +}; + +/** + * Provider of drive metadata. + * This provider returns the following objects: + * drive: { pinned, hosted, present, customIconUrl, etc. } + * thumbnail: { url, transform } + * streaming: { } + * @constructor + */ +function DriveProvider() { + MetadataProvider.call(this); + + // We batch metadata fetches into single API call. + this.entries_ = []; + this.callbacks_ = []; + this.scheduled_ = false; + + this.callApiBound_ = this.callApi_.bind(this); +} + +DriveProvider.prototype = { + __proto__: MetadataProvider.prototype +}; + +/** + * @param {Entry} entry The entry. + * @return {boolean} Whether this provider supports the entry. + */ +DriveProvider.prototype.supportsEntry = function(entry) { + return FileType.isOnDrive(entry); +}; + +/** + * @param {string} type The metadata type. + * @return {boolean} Whether this provider provides this metadata. + */ +DriveProvider.prototype.providesType = function(type) { + return type == 'drive' || type == 'thumbnail' || + type == 'streaming' || type == 'media'; +}; + +/** + * @return {string} Unique provider id. + */ +DriveProvider.prototype.getId = function() { return 'drive'; }; + +/** + * Fetches the metadata. + * @param {Entry} entry File entry. + * @param {string} type Requested metadata type. + * @param {function(Object)} callback Callback expects a map from metadata type + * to metadata value. + */ +DriveProvider.prototype.fetch = function(entry, type, callback) { + this.entries_.push(entry); + this.callbacks_.push(callback); + if (!this.scheduled_) { + this.scheduled_ = true; + setTimeout(this.callApiBound_, 0); + } +}; + +/** + * Schedules the API call. + * @private + */ +DriveProvider.prototype.callApi_ = function() { + this.scheduled_ = false; + + var entries = this.entries_; + var callbacks = this.callbacks_; + this.entries_ = []; + this.callbacks_ = []; + var self = this; + + var task = function(entry, callback) { + // TODO(mtomasz): Make getDriveEntryProperties accept Entry instead of URL. + var entryURL = entry.toURL(); + chrome.fileBrowserPrivate.getDriveEntryProperties(entryURL, + function(properties) { + callback(self.convert_(properties, entry)); + }); + }; + + for (var i = 0; i < entries.length; i++) + task(entries[i], callbacks[i]); +}; + +/** + * @param {DriveEntryProperties} data Drive entry properties. + * @param {Entry} entry File entry. + * @return {boolean} True if the file is available offline. + */ +DriveProvider.isAvailableOffline = function(data, entry) { + if (data.isPresent) + return true; + + if (!data.isHosted) + return false; + + // What's available offline? See the 'Web' column at: + // http://support.google.com/drive/bin/answer.py?hl=en&answer=1628467 + var subtype = FileType.getType(entry).subtype; + return (subtype == 'doc' || + subtype == 'draw' || + subtype == 'sheet' || + subtype == 'slides'); +}; + +/** + * @param {DriveEntryProperties} data Drive entry properties. + * @return {boolean} True if opening the file does not require downloading it + * via a metered connection. + */ +DriveProvider.isAvailableWhenMetered = function(data) { + return data.isPresent || data.isHosted; +}; + +/** + * Converts API metadata to internal format. + * @param {Object} data Metadata from API call. + * @param {Entry} entry File entry. + * @return {Object} Metadata in internal format. + * @private + */ +DriveProvider.prototype.convert_ = function(data, entry) { + var result = {}; + result.drive = { + present: data.isPresent, + pinned: data.isPinned, + hosted: data.isHosted, + imageWidth: data.imageWidth, + imageHeight: data.imageHeight, + imageRotation: data.imageRotation, + availableOffline: DriveProvider.isAvailableOffline(data, entry), + availableWhenMetered: DriveProvider.isAvailableWhenMetered(data), + customIconUrl: data.customIconUrl || '', + contentMimeType: data.contentMimeType || '', + sharedWithMe: data.sharedWithMe, + shared: data.shared + }; + + if (!data.isPresent) { + // Block the local fetch for drive files, which require downloading. + result.thumbnail = {url: '', transform: null}; + result.media = {}; + } + + if ('thumbnailUrl' in data) { + result.thumbnail = { + url: data.thumbnailUrl, + transform: null + }; + } + if (!data.isPresent) { + // Indicate that the data is not available in local cache. + // It used to have a field 'url' for streaming play, but it is + // derprecated. See crbug.com/174560. + result.streaming = {}; + } + return result; +}; + + +/** + * Provider of content metadata. + * This provider returns the following objects: + * thumbnail: { url, transform } + * media: { artist, album, title, width, height, imageTransform, etc. } + * fetchedMedia: { same fields here } + * @constructor + */ +function ContentProvider() { + MetadataProvider.call(this); + + // Pass all URLs to the metadata reader until we have a correct filter. + this.urlFilter_ = /.*/; + + var path = document.location.pathname; + var workerPath = document.location.origin + + path.substring(0, path.lastIndexOf('/') + 1) + + 'foreground/js/metadata/metadata_dispatcher.js'; + + this.dispatcher_ = new SharedWorker(workerPath).port; + this.dispatcher_.start(); + + this.dispatcher_.onmessage = this.onMessage_.bind(this); + this.dispatcher_.postMessage({verb: 'init'}); + + // Initialization is not complete until the Worker sends back the + // 'initialized' message. See below. + this.initialized_ = false; + + // Map from Entry.toURL() to callback. + // Note that simultaneous requests for same url are handled in MetadataCache. + this.callbacks_ = {}; +} + +ContentProvider.prototype = { + __proto__: MetadataProvider.prototype +}; + +/** + * @param {Entry} entry The entry. + * @return {boolean} Whether this provider supports the entry. + */ +ContentProvider.prototype.supportsEntry = function(entry) { + return entry.toURL().match(this.urlFilter_); +}; + +/** + * @param {string} type The metadata type. + * @return {boolean} Whether this provider provides this metadata. + */ +ContentProvider.prototype.providesType = function(type) { + return type == 'thumbnail' || type == 'fetchedMedia' || type == 'media'; +}; + +/** + * @return {string} Unique provider id. + */ +ContentProvider.prototype.getId = function() { return 'content'; }; + +/** + * Fetches the metadata. + * @param {Entry} entry File entry. + * @param {string} type Requested metadata type. + * @param {function(Object)} callback Callback expects a map from metadata type + * to metadata value. + */ +ContentProvider.prototype.fetch = function(entry, type, callback) { + if (entry.isDirectory) { + callback({}); + return; + } + var entryURL = entry.toURL(); + this.callbacks_[entryURL] = callback; + this.dispatcher_.postMessage({verb: 'request', arguments: [entryURL]}); +}; + +/** + * Dispatch a message from a metadata reader to the appropriate on* method. + * @param {Object} event The event. + * @private + */ +ContentProvider.prototype.onMessage_ = function(event) { + var data = event.data; + + var methodName = + 'on' + data.verb.substr(0, 1).toUpperCase() + data.verb.substr(1) + '_'; + + if (!(methodName in this)) { + console.error('Unknown message from metadata reader: ' + data.verb, data); + return; + } + + this[methodName].apply(this, data.arguments); +}; + +/** + * @return {boolean} Whether provider is ready. + */ +ContentProvider.prototype.isInitialized = function() { + return this.initialized_; +}; + +/** + * Handles the 'initialized' message from the metadata reader Worker. + * @param {Object} regexp Regexp of supported urls. + * @private + */ +ContentProvider.prototype.onInitialized_ = function(regexp) { + this.urlFilter_ = regexp; + + // Tests can monitor for this state with + // ExtensionTestMessageListener listener("worker-initialized"); + // ASSERT_TRUE(listener.WaitUntilSatisfied()); + // Automated tests need to wait for this, otherwise we crash in + // browser_test cleanup because the worker process still has + // URL requests in-flight. + var test = chrome.test || window.top.chrome.test; + test.sendMessage('worker-initialized'); + this.initialized_ = true; +}; + +/** + * Converts content metadata from parsers to the internal format. + * @param {Object} metadata The content metadata. + * @param {Object=} opt_result The internal metadata object ot put result in. + * @return {Object!} Converted metadata. + */ +ContentProvider.ConvertContentMetadata = function(metadata, opt_result) { + var result = opt_result || {}; + + if ('thumbnailURL' in metadata) { + metadata.thumbnailTransform = metadata.thumbnailTransform || null; + result.thumbnail = { + url: metadata.thumbnailURL, + transform: metadata.thumbnailTransform + }; + } + + for (var key in metadata) { + if (metadata.hasOwnProperty(key)) { + if (!('media' in result)) result.media = {}; + result.media[key] = metadata[key]; + } + } + + if ('media' in result) { + result.fetchedMedia = result.media; + } + + return result; +}; + +/** + * Handles the 'result' message from the worker. + * @param {string} url File url. + * @param {Object} metadata The metadata. + * @private + */ +ContentProvider.prototype.onResult_ = function(url, metadata) { + var callback = this.callbacks_[url]; + delete this.callbacks_[url]; + callback(ContentProvider.ConvertContentMetadata(metadata)); +}; + +/** + * Handles the 'error' message from the worker. + * @param {string} url File entry. + * @param {string} step Step failed. + * @param {string} error Error description. + * @param {Object?} metadata The metadata, if available. + * @private + */ +ContentProvider.prototype.onError_ = function(url, step, error, metadata) { + if (MetadataCache.log) // Avoid log spam by default. + console.warn('metadata: ' + url + ': ' + step + ': ' + error); + metadata = metadata || {}; + // Prevent asking for thumbnail again. + metadata.thumbnailURL = ''; + this.onResult_(url, metadata); +}; + +/** + * Handles the 'log' message from the worker. + * @param {Array.<*>} arglist Log arguments. + * @private + */ +ContentProvider.prototype.onLog_ = function(arglist) { + if (MetadataCache.log) // Avoid log spam by default. + console.log.apply(console, ['metadata:'].concat(arglist)); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_dispatcher.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_dispatcher.js new file mode 100644 index 00000000000..b711d7559b7 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_dispatcher.js @@ -0,0 +1,226 @@ +// 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'; + +// All of these scripts could be imported with a single call to importScripts, +// but then load and compile time errors would all be reported from the same +// line. +importScripts('metadata_parser.js'); +importScripts('byte_reader.js'); +importScripts('../../../common/js/util.js'); + +/** + * Dispatches metadata requests to the correct parser. + * + * @param {Object} port Worker port. + * @constructor + */ +function MetadataDispatcher(port) { + this.port_ = port; + this.port_.onmessage = this.onMessage.bind(this); + + // Make sure to update component_extension_resources.grd + // when adding new parsers. + importScripts('exif_parser.js'); + importScripts('image_parsers.js'); + importScripts('mpeg_parser.js'); + importScripts('id3_parser.js'); + + var patterns = []; + + this.parserInstances_ = []; + for (var i = 0; i < MetadataDispatcher.parserClasses_.length; i++) { + var parserClass = MetadataDispatcher.parserClasses_[i]; + var parser = new parserClass(this); + this.parserInstances_.push(parser); + patterns.push(parser.urlFilter.source); + } + + this.parserRegexp_ = new RegExp('(' + patterns.join('|') + ')', 'i'); + + this.messageHandlers_ = { + init: this.init_.bind(this), + request: this.request_.bind(this) + }; +} + +/** + * List of registered parser classes. + * @private + */ +MetadataDispatcher.parserClasses_ = []; + +/** + * @param {function} parserClass Parser constructor function. + */ +MetadataDispatcher.registerParserClass = function(parserClass) { + MetadataDispatcher.parserClasses_.push(parserClass); +}; + +/** + * Verbose logging for the dispatcher. + * + * Individual parsers also take this as their default verbosity setting. + */ +MetadataDispatcher.prototype.verbose = false; + +/** + * |init| message handler. + * @private + */ +MetadataDispatcher.prototype.init_ = function() { + // Inform our owner that we're done initializing. + // If we need to pass more data back, we can add it to the param array. + this.postMessage('initialized', [this.parserRegexp_]); + this.log('initialized with URL filter ' + this.parserRegexp_); +}; + +/** + * |request| message handler. + * @param {string} fileURL File URL. + * @private + */ +MetadataDispatcher.prototype.request_ = function(fileURL) { + try { + this.processOneFile(fileURL, function callback(metadata) { + this.postMessage('result', [fileURL, metadata]); + }.bind(this)); + } catch (ex) { + this.error(fileURL, ex); + } +}; + +/** + * Indicate to the caller that an operation has failed. + * + * No other messages relating to the failed operation should be sent. + * @param {...Object} var_args Arguments. + */ +MetadataDispatcher.prototype.error = function(var_args) { + var ary = Array.apply(null, arguments); + this.postMessage('error', ary); +}; + +/** + * Send a log message to the caller. + * + * Callers must not parse log messages for control flow. + * @param {...Object} var_args Arguments. + */ +MetadataDispatcher.prototype.log = function(var_args) { + var ary = Array.apply(null, arguments); + this.postMessage('log', ary); +}; + +/** + * Send a log message to the caller only if this.verbose is true. + * @param {...Object} var_args Arguments. + */ +MetadataDispatcher.prototype.vlog = function(var_args) { + if (this.verbose) + this.log.apply(this, arguments); +}; + +/** + * Post a properly formatted message to the caller. + * @param {string} verb Message type descriptor. + * @param {Array.<Object>} args Arguments array. + */ +MetadataDispatcher.prototype.postMessage = function(verb, args) { + this.port_.postMessage({verb: verb, arguments: args}); +}; + +/** + * Message handler. + * @param {Event} event Event object. + */ +MetadataDispatcher.prototype.onMessage = function(event) { + var data = event.data; + + if (this.messageHandlers_.hasOwnProperty(data.verb)) { + this.messageHandlers_[data.verb].apply(this, data.arguments); + } else { + this.log('Unknown message from client: ' + data.verb, data); + } +}; + +/** + * @param {string} fileURL File URL. + * @param {function(Object)} callback Completion callback. + */ +MetadataDispatcher.prototype.processOneFile = function(fileURL, callback) { + var self = this; + var currentStep = -1; + + function nextStep(var_args) { + self.vlog('nextStep: ' + steps[currentStep + 1].name); + steps[++currentStep].apply(self, arguments); + } + + var metadata; + + function onError(err, stepName) { + self.error(fileURL, stepName || steps[currentStep].name, err.toString(), + metadata); + } + + var steps = + [ // Step one, find the parser matching the url. + function detectFormat() { + for (var i = 0; i != self.parserInstances_.length; i++) { + var parser = self.parserInstances_[i]; + if (fileURL.match(parser.urlFilter)) { + // Create the metadata object as early as possible so that we can + // pass it with the error message. + metadata = parser.createDefaultMetadata(); + nextStep(parser); + return; + } + } + onError('unsupported format'); + }, + + // Step two, turn the url into an entry. + function getEntry(parser) { + webkitResolveLocalFileSystemURL( + fileURL, + function(entry) { nextStep(entry, parser) }, + onError); + }, + + // Step three, turn the entry into a file. + function getFile(entry, parser) { + entry.file(function(file) { nextStep(file, parser) }, onError); + }, + + // Step four, parse the file content. + function parseContent(file, parser) { + metadata.fileSize = file.size; + try { + parser.parse(file, metadata, callback, onError); + } catch (e) { + onError(e.stack); + } + } + ]; + + nextStep(); +}; + +// Webworker spec says that the worker global object is called self. That's +// a terrible name since we use it all over the chrome codebase to capture +// the 'this' keyword in lambdas. +var global = self; + +if (global.constructor.name == 'SharedWorkerGlobalScope') { + global.addEventListener('connect', function(e) { + var port = e.ports[0]; + new MetadataDispatcher(port); + port.start(); + }); +} else { + // Non-shared worker. + new MetadataDispatcher(global); +} diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_parser.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_parser.js new file mode 100644 index 00000000000..087eb42a9aa --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_parser.js @@ -0,0 +1,62 @@ +// 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'; + +/** + * @param {MetadataDispatcher} parent Parent object. + * @param {string} type Parser type. + * @param {RegExp} urlFilter RegExp to match URLs. + * @constructor + */ +function MetadataParser(parent, type, urlFilter) { + this.parent_ = parent; + this.type = type; + this.urlFilter = urlFilter; + this.verbose = parent.verbose; + this.mimeType = 'unknown'; +} + +/** + * Output an error message. + * @param {...Object} var_args Arguments. + */ +MetadataParser.prototype.error = function(var_args) { + this.parent_.error.apply(this.parent_, arguments); +}; + +/** + * Output a log message. + * @param {...Object} var_args Arguments. + */ +MetadataParser.prototype.log = function(var_args) { + this.parent_.log.apply(this.parent_, arguments); +}; + +/** + * Output a log message if |verbose| flag is on. + * @param {...Object} var_args Arguments. + */ +MetadataParser.prototype.vlog = function(var_args) { + if (this.verbose) + this.parent_.log.apply(this.parent_, arguments); +}; + +/** + * @return {Object} Metadata object with the minimal set of properties. + */ +MetadataParser.prototype.createDefaultMetadata = function() { + return { + type: this.type, + mimeType: this.mimeType + }; +}; + +/* Base class for image metadata parsers */ +function ImageParser(parent, type, urlFilter) { + MetadataParser.apply(this, arguments); + this.mimeType = 'image/' + this.type; +} + +ImageParser.prototype = {__proto__: MetadataParser.prototype}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/mpeg_parser.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/mpeg_parser.js new file mode 100644 index 00000000000..03637cff6ad --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/mpeg_parser.js @@ -0,0 +1,317 @@ +// 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'; + +/** + * @param {MetadataDispatcher} parent Parent object. + * @constructor + */ +function MpegParser(parent) { + MetadataParser.call(this, parent, 'mpeg', /\.(mp4|m4v|m4a|mpe?g4?)$/i); + this.mimeType = 'video/mpeg'; +} + +MpegParser.prototype = {__proto__: MetadataParser.prototype}; + +/** + * Size of the atom header. + */ +MpegParser.HEADER_SIZE = 8; + +/** + * @param {ByteReader} br ByteReader instance. + * @param {number=} opt_end End of atom position. + * @return {number} Atom size. + */ +MpegParser.readAtomSize = function(br, opt_end) { + var pos = br.tell(); + + if (opt_end) { + // Assert that opt_end <= buffer end. + // When supplied, opt_end is the end of the enclosing atom and is used to + // check the correct nesting. + br.validateRead(opt_end - pos); + } + + var size = br.readScalar(4, false, opt_end); + + if (size < MpegParser.HEADER_SIZE) + throw 'atom too short (' + size + ') @' + pos; + + if (opt_end && pos + size > opt_end) + throw 'atom too long (' + size + '>' + (opt_end - pos) + ') @' + pos; + + return size; +}; + +/** + * @param {ByteReader} br ByteReader instance. + * @param {number=} opt_end End of atom position. + * @return {string} Atom name. + */ +MpegParser.readAtomName = function(br, opt_end) { + return br.readString(4, opt_end).toLowerCase(); +}; + +/** + * @param {Object} metadata Metadata object. + * @return {Object} Root of the parser tree. + */ +MpegParser.createRootParser = function(metadata) { + function findParentAtom(atom, name) { + for (;;) { + atom = atom.parent; + if (!atom) return null; + if (atom.name == name) return atom; + } + } + + function parseFtyp(br, atom) { + metadata.brand = br.readString(4, atom.end); + } + + function parseMvhd(br, atom) { + var version = br.readScalar(4, false, atom.end); + var offset = (version == 0) ? 8 : 16; + br.seek(offset, ByteReader.SEEK_CUR); + var timescale = br.readScalar(4, false, atom.end); + var duration = br.readScalar(4, false, atom.end); + metadata.duration = duration / timescale; + } + + function parseHdlr(br, atom) { + br.seek(8, ByteReader.SEEK_CUR); + findParentAtom(atom, 'trak').trackType = br.readString(4, atom.end); + } + + function parseStsd(br, atom) { + var track = findParentAtom(atom, 'trak'); + if (track && track.trackType == 'vide') { + br.seek(40, ByteReader.SEEK_CUR); + metadata.width = br.readScalar(2, false, atom.end); + metadata.height = br.readScalar(2, false, atom.end); + } + } + + function parseDataString(name, br, atom) { + br.seek(8, ByteReader.SEEK_CUR); + metadata[name] = br.readString(atom.end - br.tell(), atom.end); + } + + function parseCovr(br, atom) { + br.seek(8, ByteReader.SEEK_CUR); + metadata.thumbnailURL = br.readImage(atom.end - br.tell(), atom.end); + } + + // 'meta' atom can occur at one of the several places in the file structure. + var parseMeta = { + ilst: { + '©nam': { data: parseDataString.bind(null, 'title') }, + '©alb': { data: parseDataString.bind(null, 'album') }, + '©art': { data: parseDataString.bind(null, 'artist') }, + 'covr': { data: parseCovr } + }, + versioned: true + }; + + // main parser for the entire file structure. + return { + ftyp: parseFtyp, + moov: { + mvhd: parseMvhd, + trak: { + mdia: { + hdlr: parseHdlr, + minf: { + stbl: { + stsd: parseStsd + } + } + }, + meta: parseMeta + }, + udta: { + meta: parseMeta + }, + meta: parseMeta + }, + meta: parseMeta + }; +}; + +/** + * + * @param {File} file File. + * @param {Object} metadata Metadata. + * @param {function(Object)} callback Success callback. + * @param {function} onError Error callback. + */ +MpegParser.prototype.parse = function(file, metadata, callback, onError) { + this.rootParser_ = MpegParser.createRootParser(metadata); + + // Kick off the processing by reading the first atom's header. + this.requestRead(file, 0, MpegParser.HEADER_SIZE, null, + onError, callback.bind(null, metadata)); +}; + +/** + * @param {function(ByteReader, Object)|Object} parser Parser tree node. + * @param {ByteReader} br ByteReader instance. + * @param {Object} atom Atom descriptor. + * @param {number} filePos File position of the atom start. + */ +MpegParser.prototype.applyParser = function(parser, br, atom, filePos) { + if (this.verbose) { + var path = atom.name; + for (var p = atom.parent; p && p.name; p = p.parent) { + path = p.name + '.' + path; + } + + var action; + if (!parser) { + action = 'skipping '; + } else if (parser instanceof Function) { + action = 'parsing '; + } else { + action = 'recursing'; + } + + var start = atom.start - MpegParser.HEADER_SIZE; + this.vlog(path + ': ' + + '@' + (filePos + start) + ':' + (atom.end - start), + action); + } + + if (parser) { + if (parser instanceof Function) { + br.pushSeek(atom.start); + parser(br, atom); + br.popSeek(); + } else { + if (parser.versioned) { + atom.start += 4; + } + this.parseMpegAtomsInRange(parser, br, atom, filePos); + } + } +}; + +/** + * @param {function(ByteReader, Object)|Object} parser Parser tree node. + * @param {ByteReader} br ByteReader instance. + * @param {Object} parentAtom Parent atom descriptor. + * @param {number} filePos File position of the atom start. + */ +MpegParser.prototype.parseMpegAtomsInRange = function( + parser, br, parentAtom, filePos) { + var count = 0; + for (var offset = parentAtom.start; offset != parentAtom.end;) { + if (count++ > 100) // Most likely we are looping through a corrupt file. + throw 'too many child atoms in ' + parentAtom.name + ' @' + offset; + + br.seek(offset); + var size = MpegParser.readAtomSize(br, parentAtom.end); + var name = MpegParser.readAtomName(br, parentAtom.end); + + this.applyParser( + parser[name], + br, + { start: offset + MpegParser.HEADER_SIZE, + end: offset + size, + name: name, + parent: parentAtom + }, + filePos + ); + + offset += size; + } +}; + +/** + * @param {File} file File. + * @param {number} filePos Start position in the file. + * @param {number} size Atom size. + * @param {string} name Atom name. + * @param {function} onError Error callback. + * @param {function} onSuccess Success callback. + */ +MpegParser.prototype.requestRead = function( + file, filePos, size, name, onError, onSuccess) { + var self = this; + var reader = new FileReader(); + reader.onerror = onError; + reader.onload = function(event) { + self.processTopLevelAtom( + reader.result, file, filePos, size, name, onError, onSuccess); + }; + this.vlog('reading @' + filePos + ':' + size); + reader.readAsArrayBuffer(file.slice(filePos, filePos + size)); +}; + +/** + * @param {ArrayBuffer} buf Data buffer. + * @param {File} file File. + * @param {number} filePos Start position in the file. + * @param {number} size Atom size. + * @param {string} name Atom name. + * @param {function} onError Error callback. + * @param {function} onSuccess Success callback. + */ +MpegParser.prototype.processTopLevelAtom = function( + buf, file, filePos, size, name, onError, onSuccess) { + try { + var br = new ByteReader(buf); + + // the header has already been read. + var atomEnd = size - MpegParser.HEADER_SIZE; + + var bufLength = buf.byteLength; + + // Check the available data size. It should be either exactly + // what we requested or HEADER_SIZE bytes less (for the last atom). + if (bufLength != atomEnd && bufLength != size) { + throw 'Read failure @' + filePos + ', ' + + 'requested ' + size + ', read ' + bufLength; + } + + // Process the top level atom. + if (name) { // name is null only the first time. + this.applyParser( + this.rootParser_[name], + br, + {start: 0, end: atomEnd, name: name}, + filePos + ); + } + + filePos += bufLength; + if (bufLength == size) { + // The previous read returned everything we asked for, including + // the next atom header at the end of the buffer. + // Parse this header and schedule the next read. + br.seek(-MpegParser.HEADER_SIZE, ByteReader.SEEK_END); + var nextSize = MpegParser.readAtomSize(br); + var nextName = MpegParser.readAtomName(br); + + // If we do not have a parser for the next atom, skip the content and + // read only the header (the one after the next). + if (!this.rootParser_[nextName]) { + filePos += nextSize - MpegParser.HEADER_SIZE; + nextSize = MpegParser.HEADER_SIZE; + } + + this.requestRead(file, filePos, nextSize, nextName, onError, onSuccess); + } else { + // The previous read did not return the next atom header, EOF reached. + this.vlog('EOF @' + filePos); + onSuccess(); + } + } catch (e) { + onError(e.toString()); + } +}; + +MetadataDispatcher.registerParserClass(MpegParser); diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metrics.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metrics.js new file mode 100644 index 00000000000..3f34e51a299 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metrics.js @@ -0,0 +1,131 @@ +// 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. + +/** + * @fileoverview Utility methods for accessing chrome.metricsPrivate API. + * + * To be included as a first script in main.html + */ + +var metrics = {}; + +/** + * A map from interval name to interval start timestamp. + */ +metrics.intervals = {}; + +/** + * Start the named time interval. + * Should be followed by a call to recordInterval with the same name. + * + * @param {string} name Unique interval name. + */ +metrics.startInterval = function(name) { + metrics.intervals[name] = Date.now(); +}; + +metrics.startInterval('Load.Total'); +metrics.startInterval('Load.Script'); + +/** + * Convert a short metric name to the full format. + * + * @param {string} name Short metric name. + * @return {string} Full metric name. + * @private + */ +metrics.convertName_ = function(name) { + return 'FileBrowser.' + name; +}; + +/** + * Wrapper method for calling chrome.fileBrowserPrivate safely. + * @param {string} name Method name. + * @param {Array.<Object>} args Arguments. + * @private + */ +metrics.call_ = function(name, args) { + try { + chrome.metricsPrivate[name].apply(chrome.metricsPrivate, args); + } catch (e) { + console.error(e.stack); + } +}; + +/** + * Create a decorator function that calls a chrome.metricsPrivate function + * with the same name and correct parameters. + * + * @param {string} name Method name. + */ +metrics.decorate = function(name) { + metrics[name] = function() { + var args = Array.apply(null, arguments); + args[0] = metrics.convertName_(args[0]); + metrics.call_(name, args); + if (metrics.log) { + console.log('chrome.metricsPrivate.' + name, args); + } + }; +}; + +metrics.decorate('recordMediumCount'); +metrics.decorate('recordSmallCount'); +metrics.decorate('recordTime'); +metrics.decorate('recordUserAction'); + +/** + * Complete the time interval recording. + * + * Should be preceded by a call to startInterval with the same name. * + * + * @param {string} name Unique interval name. + */ +metrics.recordInterval = function(name) { + if (name in metrics.intervals) { + metrics.recordTime(name, Date.now() - metrics.intervals[name]); + } else { + console.error('Unknown interval: ' + name); + } +}; + +/** + * Record an enum value. + * + * @param {string} name Metric name. + * @param {Object} value Enum value. + * @param {Array.<Object>|number} validValues Array of valid values + * or a boundary number value. + */ +metrics.recordEnum = function(name, value, validValues) { + var boundaryValue; + var index; + if (validValues.constructor.name == 'Array') { + index = validValues.indexOf(value); + boundaryValue = validValues.length; + } else { + index = value; + boundaryValue = validValues; + } + // Collect invalid values in the overflow bucket at the end. + if (index < 0 || index > boundaryValue) + index = boundaryValue; + + // Setting min to 1 looks strange but this is exactly the recommended way + // of using histograms for enum-like types. Bucket #0 works as a regular + // bucket AND the underflow bucket. + // (Source: UMA_HISTOGRAM_ENUMERATION definition in base/metrics/histogram.h) + var metricDescr = { + 'metricName': metrics.convertName_(name), + 'type': 'histogram-linear', + 'min': 1, + 'max': boundaryValue, + 'buckets': boundaryValue + 1 + }; + metrics.call_('recordValue', [metricDescr, index]); + if (metrics.log) { + console.log('chrome.metricsPrivate.recordValue', + [metricDescr.metricName, index, value]); + } +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/navigation_list_model.js b/chromium/chrome/browser/resources/file_manager/foreground/js/navigation_list_model.js new file mode 100644 index 00000000000..f1d31e6966d --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/navigation_list_model.js @@ -0,0 +1,350 @@ +// 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'; + +/** + * Entry of NavigationListModel. This constructor should be called only from + * the helper methods (NavigationModelItem.create). + * + * @param {string} path Path. + * @param {DirectoryEntry} entry Entry. Can be null. + * @param {string} label Label. + * @constructor + */ +function NavigationModelItem(path, entry, label) { + this.path_ = path; + this.entry_ = entry; + this.label_ = label; + this.resolvingQueue_ = new AsyncUtil.Queue(); + + Object.seal(this); +} + +NavigationModelItem.prototype = { + get path() { return this.path_; }, + get label() { return this.label_; } +}; + +/** + * Returns the cached entry of the item. This may return NULL if the target is + * not available on the filesystem, is not resolved or is under resolving the + * entry. + * + * @return {Entry} Cached entry. + */ +NavigationModelItem.prototype.getCachedEntry = function() { + return this.entry_; +}; + +/** + * TODO(mtomasz): Use Entry instead of path. + * @param {VolumeManagerWrapper} volumeManager VolumeManagerWrapper instance. + * @param {string} path Path. + * @param {DirectoryEntry} entry Entry. Can be null. + * @param {string} label Label. + * @param {function(FileError)} errorCallback Called when the resolving is + * failed with the error. + * @return {NavigationModelItem} Created NavigationModelItem. + */ +NavigationModelItem.create = function( + volumeManager, path, entry, label, errorCallback) { + var item = new NavigationModelItem(path, entry, label); + + // If the given entry is null, try to resolve path to get an entry. + if (!entry) { + item.resolvingQueue_.run(function(continueCallback) { + volumeManager.resolveAbsolutePath( + path, + function(entry) { + if (entry.isDirectory) + item.entry_ = entry; + else + errorCallback(util.createFileError(FileError.TYPE_MISMATCH_ERR)); + continueCallback(); + }, + function(error) { + errorCallback(error); + continueCallback(); + }); + }); + } + return item; +}; + +/** + * Retrieves the entry. If the entry is being retrieved, waits until it + * finishes. + * @param {function(Entry)} callback Called with the resolved entry. The entry + * may be NULL if resolving is failed. + */ +NavigationModelItem.prototype.getEntryAsync = function(callback) { + // If resolving the entry is running, wait until it finishes. + this.resolvingQueue_.run(function(continueCallback) { + callback(this.entry_); + continueCallback(); + }.bind(this)); +}; + +/** + * Returns if this item is a shortcut or a volume root. + * @return {boolean} True if a shortcut, false if a volume root. + */ +NavigationModelItem.prototype.isShortcut = function() { + return !PathUtil.isRootPath(this.path_); +}; + +/** + * A navigation list model. This model combines the 2 lists. + * @param {VolumeManagerWrapper} volumeManager VolumeManagerWrapper instance. + * @param {cr.ui.ArrayDataModel} shortcutListModel The list of folder shortcut. + * @constructor + * @extends {cr.EventTarget} + */ +function NavigationListModel(volumeManager, shortcutListModel) { + cr.EventTarget.call(this); + + this.volumeManager_ = volumeManager; + this.shortcutListModel_ = shortcutListModel; + + var volumeInfoToModelItem = function(volumeInfo) { + if (volumeInfo.volumeType == util.VolumeType.DRIVE) { + // For drive volume, we assign the path to "My Drive". + return NavigationModelItem.create( + this.volumeManager_, + volumeInfo.mountPath + '/root', + null, + volumeInfo.getLabel(), + function() {}); + } else { + return NavigationModelItem.create( + this.volumeManager_, + volumeInfo.mountPath, + volumeInfo.root, + volumeInfo.getLabel(), + function() {}); + } + }.bind(this); + + var pathToModelItem = function(path) { + var item = NavigationModelItem.create( + this.volumeManager_, + path, + null, // Entry will be resolved. + PathUtil.getFolderLabel(path), + function(error) { + if (error.code == FileError.NOT_FOUND_ERR) + this.onItemNotFoundError(item); + }.bind(this)); + return item; + }.bind(this); + + /** + * Type of updated list. + * @enum {number} + * @const + */ + var ListType = { + VOLUME_LIST: 1, + SHORTCUT_LIST: 2 + }; + Object.freeze(ListType); + + // Generates this.volumeList_ and this.shortcutList_ from the models. + this.volumeList_ = + this.volumeManager_.volumeInfoList.slice().map(volumeInfoToModelItem); + + this.shortcutList_ = []; + for (var i = 0; i < this.shortcutListModel_.length; i++) { + var shortcutPath = this.shortcutListModel_.item(i); + var volumeInfo = this.volumeManager_.getVolumeInfo(shortcutPath); + var isMounted = volumeInfo && !volumeInfo.error; + if (isMounted) + this.shortcutList_.push(pathToModelItem(shortcutPath)); + } + + // Generates a combined 'permuted' event from an event of either list. + var permutedHandler = function(listType, event) { + var permutation; + + // Build the volumeList. + if (listType == ListType.VOLUME_LIST) { + // The volume is mounted or unmounted. + var newList = []; + + // Use the old instances if they just move. + for (var i = 0; i < event.permutation.length; i++) { + if (event.permutation[i] >= 0) + newList[event.permutation[i]] = this.volumeList_[i]; + } + + // Create missing instances. + for (var i = 0; i < event.newLength; i++) { + if (!newList[i]) { + newList[i] = volumeInfoToModelItem( + this.volumeManager_.volumeInfoList.item(i)); + } + } + this.volumeList_ = newList; + + permutation = event.permutation.slice(); + } else { + // volumeList part has not been changed, so the permutation should be + // idenetity mapping. + permutation = []; + for (var i = 0; i < this.volumeList_.length; i++) + permutation[i] = i; + } + + // Build the shortcutList. Even if the event is for the volumeInfoList + // update, the short cut path may be unmounted or newly mounted. So, here + // shortcutList will always be re-built. + // Currently this code may be redundant, as shortcut folder is supported + // only on Drive File System and we can assume single-profile, but + // multi-profile will be supported later. + // The shortcut list is sorted in case-insensitive lexicographical order. + // So we just can traverse the two list linearly. + var modelIndex = 0; + var oldListIndex = 0; + var newList = []; + while (modelIndex < this.shortcutListModel_.length && + oldListIndex < this.shortcutList_.length) { + var shortcutPath = this.shortcutListModel_.item(modelIndex); + var cmp = this.shortcutListModel_.compare( + shortcutPath, this.shortcutList_[oldListIndex].path); + if (cmp > 0) { + // The shortcut at shortcutList_[oldListIndex] is removed. + permutation.push(-1); + oldListIndex++; + continue; + } + + // Check if the volume where the shortcutPath is is mounted or not. + var volumeInfo = this.volumeManager_.getVolumeInfo(shortcutPath); + var isMounted = volumeInfo && !volumeInfo.error; + if (cmp == 0) { + // There exists an old NavigationModelItem instance. + if (isMounted) { + // Reuse the old instance. + permutation.push(newList.length + this.volumeList_.length); + newList.push(this.shortcutList_[oldListIndex]); + } else { + permutation.push(-1); + } + oldListIndex++; + } else { + // We needs to create a new instance for the shortcut path. + if (isMounted) + newList.push(pathToModelItem(shortcutPath)); + } + modelIndex++; + } + + // Add remaining (new) shortcuts if necessary. + for (; modelIndex < this.shortcutListModel_.length; modelIndex++) { + var shortcutPath = this.shortcutListModel_.item(modelIndex); + var volumeInfo = this.volumeManager_.getVolumeInfo(shortcutPath); + var isMounted = volumeInfo && !volumeInfo.error; + if (isMounted) + newList.push(pathToModelItem(shortcutPath)); + } + + // Fill remaining permutation if necessary. + for (; oldListIndex < this.shortcutList_.length; oldListIndex++) + permutation.push(-1); + + this.shortcutList_ = newList; + + // Dispatch permuted event. + var permutedEvent = new Event('permuted'); + permutedEvent.newLength = + this.volumeList_.length + this.shortcutList_.length; + permutedEvent.permutation = permutation; + this.dispatchEvent(permutedEvent); + }; + + this.volumeManager_.volumeInfoList.addEventListener( + 'permuted', permutedHandler.bind(this, ListType.VOLUME_LIST)); + this.shortcutListModel_.addEventListener( + 'permuted', permutedHandler.bind(this, ListType.SHORTCUT_LIST)); + + // 'change' event is just ignored, because it is not fired neither in + // the folder shortcut list nor in the volume info list. + // 'splice' and 'sorted' events are not implemented, since they are not used + // in list.js. +} + +/** + * NavigationList inherits cr.EventTarget. + */ +NavigationListModel.prototype = { + __proto__: cr.EventTarget.prototype, + get length() { return this.length_(); }, + get folderShortcutList() { return this.shortcutList_; } +}; + +/** + * Returns the item at the given index. + * @param {number} index The index of the entry to get. + * @return {?string} The path at the given index. + */ +NavigationListModel.prototype.item = function(index) { + var offset = this.volumeList_.length; + if (index < offset) + return this.volumeList_[index]; + return this.shortcutList_[index - offset]; +}; + +/** + * Returns the number of items in the model. + * @return {number} The length of the model. + * @private + */ +NavigationListModel.prototype.length_ = function() { + return this.volumeList_.length + this.shortcutList_.length; +}; + +/** + * Returns the first matching item. + * @param {NavigationModelItem} modelItem The entry to find. + * @param {number=} opt_fromIndex If provided, then the searching start at + * the {@code opt_fromIndex}. + * @return {number} The index of the first found element or -1 if not found. + */ +NavigationListModel.prototype.indexOf = function(modelItem, opt_fromIndex) { + for (var i = opt_fromIndex || 0; i < this.length; i++) { + if (modelItem === this.item(i)) + return i; + } + return -1; +}; + +/** + * Called when one od the items is not found on the filesystem. + * @param {NavigationModelItem} modelItem The entry which is not found. + */ +NavigationListModel.prototype.onItemNotFoundError = function(modelItem) { + var index = this.indexOf(modelItem); + if (index === -1) { + // Invalid modelItem. + } else if (index < this.volumeList_.length) { + // The item is in the volume list. + // Not implemented. + // TODO(yoshiki): Implement it when necessary. + } else { + // The item is in the folder shortcut list. + if (this.isDriveMounted()) + this.shortcutListModel_.remove(modelItem.path); + } +}; + +/** + * Returns if the drive is mounted or not. + * @return {boolean} True if the drive is mounted, false otherwise. + */ +NavigationListModel.prototype.isDriveMounted = function() { + var volumeInfo = + this.volumeManager_.getCurrentProfileVolumeInfo(RootType.DRIVE); + return !!volumeInfo && volumeInfo.root; +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery.js b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery.js new file mode 100644 index 00000000000..91171b6d3f1 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery.js @@ -0,0 +1,867 @@ +// 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'; + +/** + * Called from the main frame when unloading. + * @return {string?} User-visible message on null if it is OK to close. + */ +function beforeunload() { return Gallery.instance.onBeforeUnload() } + +/** + * Called from the main frame when unloading. + * @param {boolean=} opt_exiting True if the app is exiting. + */ +function unload(opt_exiting) { Gallery.instance.onUnload(opt_exiting) } + +/** + * Gallery for viewing and editing image files. + * + * @param {Object} context Object containing the following: + * {function(string)} onNameChange Called every time a selected + * item name changes (on rename and on selection change). + * {AppWindow} appWindow + * {function(string)} onBack + * {function()} onClose + * {function()} onMaximize + * {function(boolean)} onAppRegionChanged + * {MetadataCache} metadataCache + * {Array.<Object>} shareActions + * {string} readonlyDirName Directory name for readonly warning or null. + * {DirEntry} saveDirEntry Directory to save to. + * {function(string)} displayStringFunction. + * @param {VolumeManagerWrapper} volumeManager The VolumeManager instance of + * the system. + * @class + * @constructor + */ +function Gallery(context, volumeManager) { + this.container_ = document.querySelector('.gallery'); + this.document_ = document; + this.context_ = context; + this.metadataCache_ = context.metadataCache; + this.volumeManager_ = volumeManager; + this.selectedEntry_ = null; + + this.dataModel_ = new cr.ui.ArrayDataModel([]); + this.selectionModel_ = new cr.ui.ListSelectionModel(); + this.displayStringFunction_ = context.displayStringFunction; + + this.initDom_(); + this.initListeners_(); +} + +/** + * Gallery extends cr.EventTarget. + */ +Gallery.prototype.__proto__ = cr.EventTarget.prototype; + +/** + * Creates and initializes a Gallery object based on a context. + * + * @param {Object} context Gallery context. + * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system. + * @param {Array.<Entry>} entries Array of entries. + * @param {Array.<Entry>} selectedEntries Array of selected entries. + */ +Gallery.open = function(context, volumeManager, entries, selectedEntries) { + Gallery.instance = new Gallery(context, volumeManager); + Gallery.instance.load(entries, selectedEntries); +}; + +/** + * Tools fade-out timeout im milliseconds. + * @const + * @type {number} + */ +Gallery.FADE_TIMEOUT = 3000; + +/** + * First time tools fade-out timeout im milliseconds. + * @const + * @type {number} + */ +Gallery.FIRST_FADE_TIMEOUT = 1000; + +/** + * Time until mosaic is initialized in the background. Used to make gallery + * in the slide mode load faster. In miiliseconds. + * @const + * @type {number} + */ +Gallery.MOSAIC_BACKGROUND_INIT_DELAY = 1000; + +/** + * Types of metadata Gallery uses (to query the metadata cache). + * @const + * @type {string} + */ +Gallery.METADATA_TYPE = 'thumbnail|filesystem|media|streaming|drive'; + +/** + * Initializes listeners. + * @private + */ +Gallery.prototype.initListeners_ = function() { + this.document_.oncontextmenu = function(e) { e.preventDefault(); }; + this.keyDownBound_ = this.onKeyDown_.bind(this); + this.document_.body.addEventListener('keydown', this.keyDownBound_); + + this.inactivityWatcher_ = new MouseInactivityWatcher( + this.container_, Gallery.FADE_TIMEOUT, this.hasActiveTool.bind(this)); + + // Search results may contain files from different subdirectories so + // the observer is not going to work. + if (!this.context_.searchResults && this.context_.curDirEntry) { + this.thumbnailObserverId_ = this.metadataCache_.addObserver( + this.context_.curDirEntry, + MetadataCache.CHILDREN, + 'thumbnail', + this.updateThumbnails_.bind(this)); + } + + this.volumeManager_.addEventListener('externally-unmounted', + this.onExternallyUnmounted_.bind(this)); +}; + +/** + * Closes gallery when a volume containing the selected item is unmounted. + * @param {Event} event The unmount event. + * @private + */ +Gallery.prototype.onExternallyUnmounted_ = function(event) { + if (!this.selectedEntry_) + return; + + if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) === + event.volumeInfo) { + this.onBack_(); + } +}; + +/** + * Beforeunload handler. + * @return {string?} User-visible message on null if it is OK to close. + */ +Gallery.prototype.onBeforeUnload = function() { + return this.slideMode_.onBeforeUnload(); +}; + +/** + * Unloads the Gallery. + * @param {boolean} exiting True if the app is exiting. + */ +Gallery.prototype.onUnload = function(exiting) { + if (!this.context_.searchResults) { + this.metadataCache_.removeObserver(this.thumbnailObserverId_); + } + this.slideMode_.onUnload(exiting); +}; + +/** + * Initializes DOM UI + * @private + */ +Gallery.prototype.initDom_ = function() { + var content = util.createChild(this.container_, 'content'); + content.addEventListener('click', this.onContentClick_.bind(this)); + + this.header_ = util.createChild(this.container_, 'header tool dimmable'); + this.toolbar_ = util.createChild(this.container_, 'toolbar tool dimmable'); + + var backButton = util.createChild(this.container_, + 'back-button tool dimmable'); + util.createChild(backButton); + backButton.addEventListener('click', this.onBack_.bind(this)); + + var preventDefault = function(event) { event.preventDefault(); }; + + var maximizeButton = util.createChild(this.header_, + 'maximize-button tool dimmable', + 'button'); + maximizeButton.tabIndex = -1; + maximizeButton.addEventListener('click', this.onMaximize_.bind(this)); + maximizeButton.addEventListener('mousedown', preventDefault); + + var closeButton = util.createChild(this.header_, + 'close-button tool dimmable', + 'button'); + closeButton.tabIndex = -1; + closeButton.addEventListener('click', this.onClose_.bind(this)); + closeButton.addEventListener('mousedown', preventDefault); + + this.filenameSpacer_ = util.createChild(this.toolbar_, 'filename-spacer'); + this.filenameEdit_ = util.createChild(this.filenameSpacer_, + 'namebox', 'input'); + + this.filenameEdit_.setAttribute('type', 'text'); + this.filenameEdit_.addEventListener('blur', + this.onFilenameEditBlur_.bind(this)); + + this.filenameEdit_.addEventListener('focus', + this.onFilenameFocus_.bind(this)); + + this.filenameEdit_.addEventListener('keydown', + this.onFilenameEditKeydown_.bind(this)); + + util.createChild(this.toolbar_, 'button-spacer'); + + this.prompt_ = new ImageEditor.Prompt( + this.container_, this.displayStringFunction_); + + this.modeButton_ = util.createChild(this.toolbar_, 'button mode', 'button'); + this.modeButton_.addEventListener('click', + this.toggleMode_.bind(this, null)); + + this.mosaicMode_ = new MosaicMode(content, + this.dataModel_, + this.selectionModel_, + this.metadataCache_, + this.toggleMode_.bind(this, null)); + + this.slideMode_ = new SlideMode(this.container_, + content, + this.toolbar_, + this.prompt_, + this.dataModel_, + this.selectionModel_, + this.context_, + this.toggleMode_.bind(this), + this.displayStringFunction_); + + this.slideMode_.addEventListener('image-displayed', function() { + cr.dispatchSimpleEvent(this, 'image-displayed'); + }.bind(this)); + this.slideMode_.addEventListener('image-saved', function() { + cr.dispatchSimpleEvent(this, 'image-saved'); + }.bind(this)); + + var deleteButton = this.createToolbarButton_('delete', 'GALLERY_DELETE'); + deleteButton.addEventListener('click', this.delete_.bind(this)); + + this.shareButton_ = this.createToolbarButton_('share', 'GALLERY_SHARE'); + this.shareButton_.setAttribute('disabled', ''); + this.shareButton_.addEventListener('click', this.toggleShare_.bind(this)); + + this.shareMenu_ = util.createChild(this.container_, 'share-menu'); + this.shareMenu_.hidden = true; + util.createChild(this.shareMenu_, 'bubble-point'); + + this.dataModel_.addEventListener('splice', this.onSplice_.bind(this)); + this.dataModel_.addEventListener('content', this.onContentChange_.bind(this)); + + this.selectionModel_.addEventListener('change', this.onSelection_.bind(this)); + this.slideMode_.addEventListener('useraction', this.onUserAction_.bind(this)); +}; + +/** + * Creates toolbar button. + * + * @param {string} className Class to add. + * @param {string} title Button title. + * @return {HTMLElement} Newly created button. + * @private + */ +Gallery.prototype.createToolbarButton_ = function(className, title) { + var button = util.createChild(this.toolbar_, className, 'button'); + button.title = this.displayStringFunction_(title); + return button; +}; + +/** + * Loads the content. + * + * @param {Array.<Entry>} entries Array of entries. + * @param {Array.<Entry>} selectedEntries Array of selected entries. Must be a + * subset of {@code entries}. + */ +Gallery.prototype.load = function(entries, selectedEntries) { + var items = []; + for (var index = 0; index < entries.length; ++index) { + items.push(new Gallery.Item(entries[index])); + } + this.dataModel_.push.apply(this.dataModel_, items); + + this.selectionModel_.adjustLength(this.dataModel_.length); + + for (var i = 0; i !== selectedEntries.length; i++) { + var selectedIndex = entries.indexOf(selectedEntries[i]); + if (selectedIndex >= 0) + this.selectionModel_.setIndexSelected(selectedIndex, true); + else + console.error('Cannot select ' + selectedEntries[i]); + } + + if (this.selectionModel_.selectedIndexes.length === 0) + this.onSelection_(); + + var mosaic = this.mosaicMode_ && this.mosaicMode_.getMosaic(); + + // Mosaic view should show up if most of the selected files are images. + var imagesCount = 0; + for (var i = 0; i !== selectedEntries.length; i++) { + if (FileType.getMediaType(selectedEntries[i]) === 'image') + imagesCount++; + } + var mostlyImages = imagesCount > (selectedEntries.length / 2.0); + + var forcedMosaic = (this.context_.pageState && + this.context_.pageState.gallery === 'mosaic'); + + var showMosaic = (mostlyImages && selectedEntries.length > 1) || forcedMosaic; + if (mosaic && showMosaic) { + this.setCurrentMode_(this.mosaicMode_); + mosaic.init(); + mosaic.show(); + this.inactivityWatcher_.check(); // Show the toolbar. + cr.dispatchSimpleEvent(this, 'loaded'); + } else { + this.setCurrentMode_(this.slideMode_); + var maybeLoadMosaic = function() { + if (mosaic) + mosaic.init(); + cr.dispatchSimpleEvent(this, 'loaded'); + }.bind(this); + /* TODO: consider nice blow-up animation for the first image */ + this.slideMode_.enter(null, function() { + // Flash the toolbar briefly to show it is there. + this.inactivityWatcher_.kick(Gallery.FIRST_FADE_TIMEOUT); + }.bind(this), + maybeLoadMosaic); + } +}; + +/** + * Closes the Gallery and go to Files.app. + * @private + */ +Gallery.prototype.back_ = function() { + if (util.isFullScreen(this.context_.appWindow)) { + util.toggleFullScreen(this.context_.appWindow, + false); // Leave the full screen mode. + } + this.context_.onBack(this.getSelectedEntries()); +}; + +/** + * Handles user's 'Back' action (Escape or a click on the X icon). + * @private + */ +Gallery.prototype.onBack_ = function() { + this.executeWhenReady(this.back_.bind(this)); +}; + +/** + * Handles user's 'Close' action. + * @private + */ +Gallery.prototype.onClose_ = function() { + this.executeWhenReady(this.context_.onClose); +}; + +/** + * Handles user's 'Maximize' action (Escape or a click on the X icon). + * @private + */ +Gallery.prototype.onMaximize_ = function() { + this.executeWhenReady(this.context_.onMaximize); +}; + +/** + * Executes a function when the editor is done with the modifications. + * @param {function} callback Function to execute. + */ +Gallery.prototype.executeWhenReady = function(callback) { + this.currentMode_.executeWhenReady(callback); +}; + +/** + * @return {Object} File browser private API. + */ +Gallery.getFileBrowserPrivate = function() { + return chrome.fileBrowserPrivate || window.top.chrome.fileBrowserPrivate; +}; + +/** + * @return {boolean} True if some tool is currently active. + */ +Gallery.prototype.hasActiveTool = function() { + return this.currentMode_.hasActiveTool() || + this.isSharing_() || this.isRenaming_(); +}; + +/** +* External user action event handler. +* @private +*/ +Gallery.prototype.onUserAction_ = function() { + this.closeShareMenu_(); + // Show the toolbar and hide it after the default timeout. + this.inactivityWatcher_.kick(); +}; + +/** + * Sets the current mode, update the UI. + * @param {Object} mode Current mode. + * @private + */ +Gallery.prototype.setCurrentMode_ = function(mode) { + if (mode !== this.slideMode_ && mode !== this.mosaicMode_) + console.error('Invalid Gallery mode'); + + this.currentMode_ = mode; + this.container_.setAttribute('mode', this.currentMode_.getName()); + this.updateSelectionAndState_(); + this.updateButtons_(); +}; + +/** + * Mode toggle event handler. + * @param {function=} opt_callback Callback. + * @param {Event=} opt_event Event that caused this call. + * @private + */ +Gallery.prototype.toggleMode_ = function(opt_callback, opt_event) { + if (!this.modeButton_) + return; + + if (this.changingMode_) // Do not re-enter while changing the mode. + return; + + if (opt_event) + this.onUserAction_(); + + this.changingMode_ = true; + + var onModeChanged = function() { + this.changingMode_ = false; + if (opt_callback) opt_callback(); + }.bind(this); + + var tileIndex = Math.max(0, this.selectionModel_.selectedIndex); + + var mosaic = this.mosaicMode_.getMosaic(); + var tileRect = mosaic.getTileRect(tileIndex); + + if (this.currentMode_ === this.slideMode_) { + this.setCurrentMode_(this.mosaicMode_); + mosaic.transform( + tileRect, this.slideMode_.getSelectedImageRect(), true /* instant */); + this.slideMode_.leave(tileRect, + function() { + // Animate back to normal position. + mosaic.transform(); + mosaic.show(); + onModeChanged(); + }.bind(this)); + } else { + this.setCurrentMode_(this.slideMode_); + this.slideMode_.enter(tileRect, + function() { + // Animate to zoomed position. + mosaic.transform(tileRect, this.slideMode_.getSelectedImageRect()); + mosaic.hide(); + }.bind(this), + onModeChanged); + } +}; + +/** + * Deletes the selected items. + * @private + */ +Gallery.prototype.delete_ = function() { + this.onUserAction_(); + + // Clone the sorted selected indexes array. + var indexesToRemove = this.selectionModel_.selectedIndexes.slice(); + if (!indexesToRemove.length) + return; + + /* TODO(dgozman): Implement Undo delete, Remove the confirmation dialog. */ + + var itemsToRemove = this.getSelectedItems(); + var plural = itemsToRemove.length > 1; + var param = plural ? itemsToRemove.length : itemsToRemove[0].getFileName(); + + function deleteNext() { + if (!itemsToRemove.length) + return; // All deleted. + + // TODO(hirono): Use fileOperationManager. + var entry = itemsToRemove.pop().getEntry(); + entry.remove(deleteNext, function() { + util.flog('Error deleting: ' + entry.fullPath, deleteNext); + }); + } + + // Prevent the Gallery from handling Esc and Enter. + this.document_.body.removeEventListener('keydown', this.keyDownBound_); + var restoreListener = function() { + this.document_.body.addEventListener('keydown', this.keyDownBound_); + }.bind(this); + + cr.ui.dialogs.BaseDialog.OK_LABEL = this.displayStringFunction_( + 'GALLERY_OK_LABEL'); + cr.ui.dialogs.BaseDialog.CANCEL_LABEL = + this.displayStringFunction_('GALLERY_CANCEL_LABEL'); + var confirm = new cr.ui.dialogs.ConfirmDialog(this.container_); + confirm.show( + this.displayStringFunction_(plural ? 'GALLERY_CONFIRM_DELETE_SOME' : + 'GALLERY_CONFIRM_DELETE_ONE', param), + function() { + restoreListener(); + this.selectionModel_.unselectAll(); + this.selectionModel_.leadIndex = -1; + // Remove items from the data model, starting from the highest index. + while (indexesToRemove.length) + this.dataModel_.splice(indexesToRemove.pop(), 1); + // Delete actual files. + deleteNext(); + }.bind(this), + function() { + // Restore the listener after a timeout so that ESC is processed. + setTimeout(restoreListener, 0); + }); +}; + +/** + * @return {Array.<Gallery.Item>} Current selection. + */ +Gallery.prototype.getSelectedItems = function() { + return this.selectionModel_.selectedIndexes.map( + this.dataModel_.item.bind(this.dataModel_)); +}; + +/** + * @return {Array.<Entry>} Array of currently selected entries. + */ +Gallery.prototype.getSelectedEntries = function() { + return this.selectionModel_.selectedIndexes.map(function(index) { + return this.dataModel_.item(index).getEntry(); + }.bind(this)); +}; + +/** + * @return {Gallery.Item} Current single selection. + */ +Gallery.prototype.getSingleSelectedItem = function() { + var items = this.getSelectedItems(); + if (items.length > 1) + throw new Error('Unexpected multiple selection'); + return items[0]; +}; + +/** + * Selection change event handler. + * @private + */ +Gallery.prototype.onSelection_ = function() { + this.updateSelectionAndState_(); + this.updateShareMenu_(); +}; + +/** + * Data model splice event handler. + * @private + */ +Gallery.prototype.onSplice_ = function() { + this.selectionModel_.adjustLength(this.dataModel_.length); +}; + +/** + * Content change event handler. + * @param {Event} event Event. + * @private +*/ +Gallery.prototype.onContentChange_ = function(event) { + var index = this.dataModel_.indexOf(event.item); + if (index !== this.selectionModel_.selectedIndex) + console.error('Content changed for unselected item'); + this.updateSelectionAndState_(); +}; + +/** + * Keydown handler. + * + * @param {Event} event Event. + * @private + */ +Gallery.prototype.onKeyDown_ = function(event) { + var wasSharing = this.isSharing_(); + this.closeShareMenu_(); + + if (this.currentMode_.onKeyDown(event)) + return; + + switch (util.getKeyModifiers(event) + event.keyIdentifier) { + case 'U+0008': // Backspace. + // The default handler would call history.back and close the Gallery. + event.preventDefault(); + break; + + case 'U+001B': // Escape + // Swallow Esc if it closed the Share menu, otherwise close the Gallery. + if (!wasSharing) + this.onBack_(); + break; + + case 'U+004D': // 'm' switches between Slide and Mosaic mode. + this.toggleMode_(null, event); + break; + + case 'U+0056': // 'v' + this.slideMode_.startSlideshow(SlideMode.SLIDESHOW_INTERVAL_FIRST, event); + break; + + case 'U+007F': // Delete + case 'Shift-U+0033': // Shift+'3' (Delete key might be missing). + this.delete_(); + break; + } +}; + +// Name box and rename support. + +/** + * Updates the UI related to the selected item and the persistent state. + * + * @private + */ +Gallery.prototype.updateSelectionAndState_ = function() { + var path; + var displayName = ''; + + var selectedItems = this.getSelectedItems(); + if (selectedItems.length === 1) { + var item = selectedItems[0]; + var entry = item.getEntry(); + window.top.document.title = entry.name; + displayName = ImageUtil.getDisplayNameFromName(entry.name); + } else if (selectedItems.length > 1 && this.context_.curDirEntry) { + // If the Gallery was opened on search results the search query will not be + // recorded in the app state and the relaunch will just open the gallery + // in the curDirEntry directory. + path = this.context_.curDirEntry.fullPath; + window.top.document.title = this.context_.curDirEntry.name; + displayName = + this.displayStringFunction_('GALLERY_ITEMS_SELECTED', + selectedItems.length); + } + + window.top.util.updateAppState(path, + {gallery: (this.currentMode_ === this.mosaicMode_ ? 'mosaic' : 'slide')}); + + // We can't rename files in readonly directory. + // We can only rename a single file. + this.filenameEdit_.disabled = selectedItems.length !== 1 || + this.context_.readonlyDirName; + + this.filenameEdit_.value = displayName; + + // Resolve real filesystem path of the current file. + if (this.selectionModel_.selectedIndexes.length) { + var selectedIndex = this.selectionModel_.selectedIndex; + var selectedItem = + this.dataModel_.item(this.selectionModel_.selectedIndex); + this.selectedEntry_ = selectedItem.getEntry(); + } +}; + +/** + * Click event handler on filename edit box + * @private + */ +Gallery.prototype.onFilenameFocus_ = function() { + ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', true); + this.filenameEdit_.originalValue = this.filenameEdit_.value; + setTimeout(this.filenameEdit_.select.bind(this.filenameEdit_), 0); + this.onUserAction_(); +}; + +/** + * Blur event handler on filename edit box. + * + * @param {Event} event Blur event. + * @return {boolean} if default action should be prevented. + * @private + */ +Gallery.prototype.onFilenameEditBlur_ = function(event) { + if (this.filenameEdit_.value && this.filenameEdit_.value[0] === '.') { + this.prompt_.show('GALLERY_FILE_HIDDEN_NAME', 5000); + this.filenameEdit_.focus(); + event.stopPropagation(); + event.preventDefault(); + return false; + } + + var item = this.getSingleSelectedItem(); + var oldEntry = item.getEntry(); + + var onFileExists = function() { + this.prompt_.show('GALLERY_FILE_EXISTS', 3000); + this.filenameEdit_.value = name; + this.filenameEdit_.focus(); + }.bind(this); + + var onSuccess = function() { + var event = new Event('content'); + event.item = item; + event.oldEntry = oldEntry; + event.metadata = null; // Metadata unchanged. + this.dataModel_.dispatchEvent(event); + }.bind(this); + + if (this.filenameEdit_.value) { + this.getSingleSelectedItem().rename( + this.filenameEdit_.value, onSuccess, onFileExists); + } + + ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', false); + this.onUserAction_(); +}; + +/** + * Keydown event handler on filename edit box + * @private + */ +Gallery.prototype.onFilenameEditKeydown_ = function() { + switch (event.keyCode) { + case 27: // Escape + this.filenameEdit_.value = this.filenameEdit_.originalValue; + this.filenameEdit_.blur(); + break; + + case 13: // Enter + this.filenameEdit_.blur(); + break; + } + event.stopPropagation(); +}; + +/** + * @return {boolean} True if file renaming is currently in progress. + * @private + */ +Gallery.prototype.isRenaming_ = function() { + return this.filenameSpacer_.hasAttribute('renaming'); +}; + +/** + * Content area click handler. + * @private + */ +Gallery.prototype.onContentClick_ = function() { + this.closeShareMenu_(); + this.filenameEdit_.blur(); +}; + +// Share button support. + +/** + * @return {boolean} True if the Share menu is active. + * @private + */ +Gallery.prototype.isSharing_ = function() { + return !this.shareMenu_.hidden; +}; + +/** + * Close Share menu if it is open. + * @private + */ +Gallery.prototype.closeShareMenu_ = function() { + if (this.isSharing_()) + this.toggleShare_(); +}; + +/** + * Share button handler. + * @private + */ +Gallery.prototype.toggleShare_ = function() { + if (!this.shareButton_.hasAttribute('disabled')) + this.shareMenu_.hidden = !this.shareMenu_.hidden; + this.inactivityWatcher_.check(); +}; + +/** + * Updates available actions list based on the currently selected urls. + * @private. + */ +Gallery.prototype.updateShareMenu_ = function() { + var entries = this.getSelectedEntries(); + + function isShareAction(task) { + var taskParts = task.taskId.split('|'); + return taskParts[0] !== chrome.runtime.id; + } + + var api = Gallery.getFileBrowserPrivate(); + var mimeTypes = []; // TODO(kaznacheev) Collect mime types properly. + + var createShareMenu = function(tasks) { + var wasHidden = this.shareMenu_.hidden; + this.shareMenu_.hidden = true; + var items = this.shareMenu_.querySelectorAll('.item'); + for (var i = 0; i !== items.length; i++) { + items[i].parentNode.removeChild(items[i]); + } + + for (var t = 0; t !== tasks.length; t++) { + var task = tasks[t]; + if (!isShareAction(task)) continue; + + var item = util.createChild(this.shareMenu_, 'item'); + item.textContent = task.title; + item.style.backgroundImage = 'url(' + task.iconUrl + ')'; + item.addEventListener('click', function(taskId) { + this.toggleShare_(); // Hide the menu. + this.executeWhenReady(api.executeTask.bind(api, taskId, entries)); + }.bind(this, task.taskId)); + } + + var empty = this.shareMenu_.querySelector('.item') === null; + ImageUtil.setAttribute(this.shareButton_, 'disabled', empty); + this.shareMenu_.hidden = wasHidden || empty; + }.bind(this); + + // Create or update the share menu with a list of sharing tasks and show + // or hide the share button. + // TODO(mtomasz): Pass Entries directly, instead of URLs. + if (!entries.length) + createShareMenu([]); // Empty list of tasks, since there is no selection. + else + api.getFileTasks(util.entriesToURLs(entries), mimeTypes, createShareMenu); +}; + +/** + * Updates thumbnails. + * @private + */ +Gallery.prototype.updateThumbnails_ = function() { + if (this.currentMode_ === this.slideMode_) + this.slideMode_.updateThumbnails(); + + if (this.mosaicMode_) { + var mosaic = this.mosaicMode_.getMosaic(); + if (mosaic.isInitialized()) + mosaic.reload(); + } +}; + +/** + * Updates buttons. + * @private + */ +Gallery.prototype.updateButtons_ = function() { + if (this.modeButton_) { + var oppositeMode = + this.currentMode_ === this.slideMode_ ? this.mosaicMode_ : + this.slideMode_; + this.modeButton_.title = + this.displayStringFunction_(oppositeMode.getTitle()); + } +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery_item.js b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery_item.js new file mode 100644 index 00000000000..2ae80c92dd7 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery_item.js @@ -0,0 +1,227 @@ +// 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'; + +/** + * Object representing an image item (a photo or a video). + * + * @param {FileEntry} entry Image entry. + * @constructor + */ +Gallery.Item = function(entry) { + this.entry_ = entry; + this.original_ = true; +}; + +/** + * @return {FileEntry} Image entry. + */ +Gallery.Item.prototype.getEntry = function() { return this.entry_ }; + +/** + * @return {string} File name. + */ +Gallery.Item.prototype.getFileName = function() { + return this.entry_.name; +}; + +/** + * @return {boolean} True if this image has not been created in this session. + */ +Gallery.Item.prototype.isOriginal = function() { return this.original_ }; + +// TODO: Localize? +/** + * @type {string} Suffix for a edited copy file name. + */ +Gallery.Item.COPY_SIGNATURE = ' - Edited'; + +/** + * Regular expression to match '... - Edited'. + * @type {RegExp} + */ +Gallery.Item.REGEXP_COPY_0 = + new RegExp('^(.+)' + Gallery.Item.COPY_SIGNATURE + '$'); + +/** + * Regular expression to match '... - Edited (N)'. + * @type {RegExp} + */ +Gallery.Item.REGEXP_COPY_N = + new RegExp('^(.+)' + Gallery.Item.COPY_SIGNATURE + ' \\((\\d+)\\)$'); + +/** + * Creates a name for an edited copy of the file. + * + * @param {Entry} dirEntry Entry. + * @param {function} callback Callback. + * @private + */ +Gallery.Item.prototype.createCopyName_ = function(dirEntry, callback) { + var name = this.getFileName(); + + // If the item represents a file created during the current Gallery session + // we reuse it for subsequent saves instead of creating multiple copies. + if (!this.original_) { + callback(name); + return; + } + + var ext = ''; + var index = name.lastIndexOf('.'); + if (index != -1) { + ext = name.substr(index); + name = name.substr(0, index); + } + + if (!ext.match(/jpe?g/i)) { + // Chrome can natively encode only two formats: JPEG and PNG. + // All non-JPEG images are saved in PNG, hence forcing the file extension. + ext = '.png'; + } + + function tryNext(tries) { + // All the names are used. Let's overwrite the last one. + if (tries == 0) { + setTimeout(callback, 0, name + ext); + return; + } + + // If the file name contains the copy signature add/advance the sequential + // number. + var matchN = Gallery.Item.REGEXP_COPY_N.exec(name); + var match0 = Gallery.Item.REGEXP_COPY_0.exec(name); + if (matchN && matchN[1] && matchN[2]) { + var copyNumber = parseInt(matchN[2], 10) + 1; + name = matchN[1] + Gallery.Item.COPY_SIGNATURE + ' (' + copyNumber + ')'; + } else if (match0 && match0[1]) { + name = match0[1] + Gallery.Item.COPY_SIGNATURE + ' (1)'; + } else { + name += Gallery.Item.COPY_SIGNATURE; + } + + dirEntry.getFile(name + ext, {create: false, exclusive: false}, + tryNext.bind(null, tries - 1), + callback.bind(null, name + ext)); + } + + tryNext(10); +}; + +/** + * Writes the new item content to the file. + * + * @param {Entry} overrideDir Directory to save to. If null, save to the same + * directory as the original. + * @param {boolean} overwrite True if overwrite, false if copy. + * @param {HTMLCanvasElement} canvas Source canvas. + * @param {ImageEncoder.MetadataEncoder} metadataEncoder MetadataEncoder. + * @param {function(boolean)=} opt_callback Callback accepting true for success. + */ +Gallery.Item.prototype.saveToFile = function( + overrideDir, overwrite, canvas, metadataEncoder, opt_callback) { + ImageUtil.metrics.startInterval(ImageUtil.getMetricName('SaveTime')); + + var name = this.getFileName(); + + var onSuccess = function(entry) { + ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 1, 2); + ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('SaveTime')); + this.entry_ = entry; + if (opt_callback) opt_callback(true); + }.bind(this); + + function onError(error) { + console.error('Error saving from gallery', name, error); + ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 0, 2); + if (opt_callback) opt_callback(false); + } + + function doSave(newFile, fileEntry) { + fileEntry.createWriter(function(fileWriter) { + function writeContent() { + fileWriter.onwriteend = onSuccess.bind(null, fileEntry); + fileWriter.write(ImageEncoder.getBlob(canvas, metadataEncoder)); + } + fileWriter.onerror = function(error) { + onError(error); + // Disable all callbacks on the first error. + fileWriter.onerror = null; + fileWriter.onwriteend = null; + }; + if (newFile) { + writeContent(); + } else { + fileWriter.onwriteend = writeContent; + fileWriter.truncate(0); + } + }, onError); + } + + function getFile(dir, newFile) { + dir.getFile(name, {create: newFile, exclusive: newFile}, + doSave.bind(null, newFile), onError); + } + + function checkExistence(dir) { + dir.getFile(name, {create: false, exclusive: false}, + getFile.bind(null, dir, false /* existing file */), + getFile.bind(null, dir, true /* create new file */)); + } + + var saveToDir = function(dir) { + if (overwrite) { + checkExistence(dir); + } else { + this.createCopyName_(dir, function(copyName) { + this.original_ = false; + name = copyName; + checkExistence(dir); + }.bind(this)); + } + }.bind(this); + + if (overrideDir) { + saveToDir(overrideDir); + } else { + this.entry_.getParent(saveToDir, onError); + } +}; + +/** + * Renames the file. + * + * @param {string} displayName New display name (without the extension). + * @param {function()} onSuccess Success callback. + * @param {function()} onExists Called if the file with the new name exists. + */ +Gallery.Item.prototype.rename = function(displayName, onSuccess, onExists) { + var fileName = this.entry_.name.replace( + ImageUtil.getDisplayNameFromName(this.entry_.name), displayName); + + if (name === this.entry_.name) + return; + + var onRenamed = function(entry) { + this.entry_ = entry; + onSuccess(); + }.bind(this); + + var onError = function() { + console.error('Rename error: "' + oldName + '" to "' + newName + '"'); + }; + + var moveIfDoesNotExist = function(parentDir) { + parentDir.getFile( + fileName, + {create: false, exclusive: false}, + onExists, + function() { + this.entry_.moveTo(parentDir, fileName, onRenamed, onError); + }.bind(this)); + }.bind(this); + + this.entry_.getParent(moveIfDoesNotExist, onError); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery_scripts.js b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery_scripts.js new file mode 100644 index 00000000000..336fa207938 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery_scripts.js @@ -0,0 +1,70 @@ +// 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. + +// The include directives are put into Javascript-style comments to prevent +// parsing errors in non-flattened mode. The flattener still sees them. +// Note that this makes the flattener to comment out the first line of the +// included file but that's all right since any javascript file should start +// with a copyright comment anyway. + +//<include src="../metrics.js"> + +//<include src="../../../../image_loader/image_loader_client.js"/> + +//<include src="../../../../../../../ui/webui/resources/js/cr.js"> +//<include src="../../../../../../../ui/webui/resources/js/event_tracker.js"> +//<include src="../../../../../../../ui/webui/resources/js/load_time_data.js"> + +//<include src="../../../../../../../ui/webui/resources/js/cr/ui.js"> +//<include src="../../../../../../../ui/webui/resources/js/cr/event_target.js"> +//<include src="../../../../../../../ui/webui/resources/js/cr/ui/touch_handler.js"> +//<include src="../../../../../../../ui/webui/resources/js/cr/ui/array_data_model.js"> +//<include src="../../../../../../../ui/webui/resources/js/cr/ui/dialogs.js"> +//<include src="../../../../../../../ui/webui/resources/js/cr/ui/list_item.js"> +//<include src="../../../../../../../ui/webui/resources/js/cr/ui/list_selection_model.js"> +//<include src="../../../../../../../ui/webui/resources/js/cr/ui/list_single_selection_model.js"> +//<include src="../../../../../../../ui/webui/resources/js/cr/ui/list_selection_controller.js"> +//<include src="../../../../../../../ui/webui/resources/js/cr/ui/list.js"> +//<include src="../../../../../../../ui/webui/resources/js/cr/ui/grid.js"> + +(function() { +// 'strict mode' is invoked for this scope. + +//<include src="../../../common/js/async_util.js"> +//<include src="../../../common/js/util.js"> +//<include src="../../../common/js/path_util.js"> +//<include src="../file_type.js"> +//<include src="../volume_manager_wrapper.js"> + +//<include src="../image_editor/image_util.js"/> +//<include src="../image_editor/viewport.js"/> +//<include src="../image_editor/image_buffer.js"/> +//<include src="../image_editor/image_view.js"/> +//<include src="../image_editor/commands.js"/> +//<include src="../image_editor/image_editor.js"/> +//<include src="../image_editor/image_transform.js"/> +//<include src="../image_editor/image_adjust.js"/> +//<include src="../image_editor/filter.js"/> +//<include src="../image_editor/image_encoder.js"/> +//<include src="../image_editor/exif_encoder.js"/> + +//<include src="../media/media_controls.js"/> +//<include src="../media/media_util.js"/> +//<include src="../media/util.js"/> + +//<include src="../metadata/metadata_cache.js"/> + +//<include src="gallery.js"> +//<include src="gallery_item.js"> +//<include src="mosaic_mode.js"> +//<include src="slide_mode.js"> +//<include src="ribbon.js"> + +// Exports +window.ImageUtil = ImageUtil; +window.Gallery = Gallery; +window.beforeunload = beforeunload; +window.unload = unload; + +})(); diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/photo/mosaic_mode.js b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/mosaic_mode.js new file mode 100644 index 00000000000..6231864bbd8 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/mosaic_mode.js @@ -0,0 +1,2012 @@ +// 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'; + +/** + * @param {Element} container Content container. + * @param {cr.ui.ArrayDataModel} dataModel Data model. + * @param {cr.ui.ListSelectionModel} selectionModel Selection model. + * @param {MetadataCache} metadataCache Metadata cache. + * @param {function} toggleMode Function to switch to the Slide mode. + * @constructor + */ +function MosaicMode( + container, dataModel, selectionModel, metadataCache, toggleMode) { + this.mosaic_ = new Mosaic( + container.ownerDocument, dataModel, selectionModel, metadataCache); + container.appendChild(this.mosaic_); + + this.toggleMode_ = toggleMode; + this.mosaic_.addEventListener('dblclick', this.toggleMode_); + this.showingTimeoutID_ = null; +} + +/** + * @return {Mosaic} The mosaic control. + */ +MosaicMode.prototype.getMosaic = function() { return this.mosaic_ }; + +/** + * @return {string} Mode name. + */ +MosaicMode.prototype.getName = function() { return 'mosaic' }; + +/** + * @return {string} Mode title. + */ +MosaicMode.prototype.getTitle = function() { return 'GALLERY_MOSAIC' }; + +/** + * Execute an action (this mode has no busy state). + * @param {function} action Action to execute. + */ +MosaicMode.prototype.executeWhenReady = function(action) { action() }; + +/** + * @return {boolean} Always true (no toolbar fading in this mode). + */ +MosaicMode.prototype.hasActiveTool = function() { return true }; + +/** + * Keydown handler. + * + * @param {Event} event Event. + * @return {boolean} True if processed. + */ +MosaicMode.prototype.onKeyDown = function(event) { + switch (util.getKeyModifiers(event) + event.keyIdentifier) { + case 'Enter': + this.toggleMode_(); + return true; + } + return this.mosaic_.onKeyDown(event); +}; + +//////////////////////////////////////////////////////////////////////////////// + +/** + * Mosaic control. + * + * @param {Document} document Document. + * @param {cr.ui.ArrayDataModel} dataModel Data model. + * @param {cr.ui.ListSelectionModel} selectionModel Selection model. + * @param {MetadataCache} metadataCache Metadata cache. + * @return {Element} Mosaic element. + * @constructor + */ +function Mosaic(document, dataModel, selectionModel, metadataCache) { + var self = document.createElement('div'); + Mosaic.decorate(self, dataModel, selectionModel, metadataCache); + return self; +} + +/** + * Inherit from HTMLDivElement. + */ +Mosaic.prototype.__proto__ = HTMLDivElement.prototype; + +/** + * Default layout delay in ms. + * @const + * @type {number} + */ +Mosaic.LAYOUT_DELAY = 200; + +/** + * Smooth scroll animation duration when scrolling using keyboard or + * clicking on a partly visible tile. In ms. + * @const + * @type {number} + */ +Mosaic.ANIMATED_SCROLL_DURATION = 500; + +/** + * Decorate a Mosaic instance. + * + * @param {Mosaic} self Self pointer. + * @param {cr.ui.ArrayDataModel} dataModel Data model. + * @param {cr.ui.ListSelectionModel} selectionModel Selection model. + * @param {MetadataCache} metadataCache Metadata cache. + */ +Mosaic.decorate = function(self, dataModel, selectionModel, metadataCache) { + self.__proto__ = Mosaic.prototype; + self.className = 'mosaic'; + + self.dataModel_ = dataModel; + self.selectionModel_ = selectionModel; + self.metadataCache_ = metadataCache; + + // Initialization is completed lazily on the first call to |init|. +}; + +/** + * Initialize the mosaic element. + */ +Mosaic.prototype.init = function() { + if (this.tiles_) + return; // Already initialized, nothing to do. + + this.layoutModel_ = new Mosaic.Layout(); + this.onResize_(); + + this.selectionController_ = + new Mosaic.SelectionController(this.selectionModel_, this.layoutModel_); + + this.tiles_ = []; + for (var i = 0; i != this.dataModel_.length; i++) + this.tiles_.push(new Mosaic.Tile(this, this.dataModel_.item(i))); + + this.selectionModel_.selectedIndexes.forEach(function(index) { + this.tiles_[index].select(true); + }.bind(this)); + + this.initTiles_(this.tiles_); + + // The listeners might be called while some tiles are still loading. + this.initListeners_(); +}; + +/** + * @return {boolean} Whether mosaic is initialized. + */ +Mosaic.prototype.isInitialized = function() { + return !!this.tiles_; +}; + +/** + * Start listening to events. + * + * We keep listening to events even when the mosaic is hidden in order to + * keep the layout up to date. + * + * @private + */ +Mosaic.prototype.initListeners_ = function() { + this.ownerDocument.defaultView.addEventListener( + 'resize', this.onResize_.bind(this)); + + var mouseEventBound = this.onMouseEvent_.bind(this); + this.addEventListener('mousemove', mouseEventBound); + this.addEventListener('mousedown', mouseEventBound); + this.addEventListener('mouseup', mouseEventBound); + this.addEventListener('scroll', this.onScroll_.bind(this)); + + this.selectionModel_.addEventListener('change', this.onSelection_.bind(this)); + this.selectionModel_.addEventListener('leadIndexChange', + this.onLeadChange_.bind(this)); + + this.dataModel_.addEventListener('splice', this.onSplice_.bind(this)); + this.dataModel_.addEventListener('content', this.onContentChange_.bind(this)); +}; + +/** + * Smoothly scrolls the container to the specified position using + * f(x) = sqrt(x) speed function normalized to animation duration. + * @param {number} targetPosition Horizontal scroll position in pixels. + */ +Mosaic.prototype.animatedScrollTo = function(targetPosition) { + if (this.scrollAnimation_) { + webkitCancelAnimationFrame(this.scrollAnimation_); + this.scrollAnimation_ = null; + } + + // Mouse move events are fired without touching the mouse because of scrolling + // the container. Therefore, these events have to be suppressed. + this.suppressHovering_ = true; + + // Calculates integral area from t1 to t2 of f(x) = sqrt(x) dx. + var integral = function(t1, t2) { + return 2.0 / 3.0 * Math.pow(t2, 3.0 / 2.0) - + 2.0 / 3.0 * Math.pow(t1, 3.0 / 2.0); + }; + + var delta = targetPosition - this.scrollLeft; + var factor = delta / integral(0, Mosaic.ANIMATED_SCROLL_DURATION); + var startTime = Date.now(); + var lastPosition = 0; + var scrollOffset = this.scrollLeft; + + var animationFrame = function() { + var position = Date.now() - startTime; + var step = factor * + integral(Math.max(0, Mosaic.ANIMATED_SCROLL_DURATION - position), + Math.max(0, Mosaic.ANIMATED_SCROLL_DURATION - lastPosition)); + scrollOffset += step; + + var oldScrollLeft = this.scrollLeft; + var newScrollLeft = Math.round(scrollOffset); + + if (oldScrollLeft != newScrollLeft) + this.scrollLeft = newScrollLeft; + + if (step == 0 || this.scrollLeft != newScrollLeft) { + this.scrollAnimation_ = null; + // Release the hovering lock after a safe delay to avoid hovering + // a tile because of altering |this.scrollLeft|. + setTimeout(function() { + if (!this.scrollAnimation_) + this.suppressHovering_ = false; + }.bind(this), 100); + } else { + // Continue the animation. + this.scrollAnimation_ = requestAnimationFrame(animationFrame); + } + + lastPosition = position; + }.bind(this); + + // Start the animation. + this.scrollAnimation_ = requestAnimationFrame(animationFrame); +}; + +/** + * @return {Mosaic.Tile} Selected tile or undefined if no selection. + */ +Mosaic.prototype.getSelectedTile = function() { + return this.tiles_ && this.tiles_[this.selectionModel_.selectedIndex]; +}; + +/** + * @param {number} index Tile index. + * @return {Rect} Tile's image rectangle. + */ +Mosaic.prototype.getTileRect = function(index) { + var tile = this.tiles_[index]; + return tile && tile.getImageRect(); +}; + +/** + * @param {number} index Tile index. + * Scroll the given tile into the viewport. + */ +Mosaic.prototype.scrollIntoView = function(index) { + var tile = this.tiles_[index]; + if (tile) tile.scrollIntoView(); +}; + +/** + * Initializes multiple tiles. + * + * @param {Array.<Mosaic.Tile>} tiles Array of tiles. + * @param {function()=} opt_callback Completion callback. + * @private + */ +Mosaic.prototype.initTiles_ = function(tiles, opt_callback) { + // We do not want to use tile indices in asynchronous operations because they + // do not survive data model splices. Copy tile references instead. + tiles = tiles.slice(); + + // Throttle the metadata access so that we do not overwhelm the file system. + var MAX_CHUNK_SIZE = 10; + + var loadChunk = function() { + if (!tiles.length) { + if (opt_callback) opt_callback(); + return; + } + var chunkSize = Math.min(tiles.length, MAX_CHUNK_SIZE); + var loaded = 0; + for (var i = 0; i != chunkSize; i++) { + this.initTile_(tiles.shift(), function() { + if (++loaded == chunkSize) { + this.layout(); + loadChunk(); + } + }.bind(this)); + } + }.bind(this); + + loadChunk(); +}; + +/** + * Initializes a single tile. + * + * @param {Mosaic.Tile} tile Tile. + * @param {function()} callback Completion callback. + * @private + */ +Mosaic.prototype.initTile_ = function(tile, callback) { + var onImageMeasured = callback; + this.metadataCache_.get(tile.getItem().getEntry(), Gallery.METADATA_TYPE, + function(metadata) { + tile.init(metadata, onImageMeasured); + }); +}; + +/** + * Reload all tiles. + */ +Mosaic.prototype.reload = function() { + this.layoutModel_.reset_(); + this.tiles_.forEach(function(t) { t.markUnloaded() }); + this.initTiles_(this.tiles_); +}; + +/** + * Layout the tiles in the order of their indices. + * + * Starts where it last stopped (at #0 the first time). + * Stops when all tiles are processed or when the next tile is still loading. + */ +Mosaic.prototype.layout = function() { + if (this.layoutTimer_) { + clearTimeout(this.layoutTimer_); + this.layoutTimer_ = null; + } + while (true) { + var index = this.layoutModel_.getTileCount(); + if (index == this.tiles_.length) + break; // All tiles done. + var tile = this.tiles_[index]; + if (!tile.isInitialized()) + break; // Next layout will try to restart from here. + this.layoutModel_.add(tile, index + 1 == this.tiles_.length); + } + this.loadVisibleTiles_(); +}; + +/** + * Schedule the layout. + * + * @param {number=} opt_delay Delay in ms. + */ +Mosaic.prototype.scheduleLayout = function(opt_delay) { + if (!this.layoutTimer_) { + this.layoutTimer_ = setTimeout(function() { + this.layoutTimer_ = null; + this.layout(); + }.bind(this), opt_delay || 0); + } +}; + +/** + * Resize handler. + * + * @private + */ +Mosaic.prototype.onResize_ = function() { + this.layoutModel_.setViewportSize(this.clientWidth, this.clientHeight - + (Mosaic.Layout.PADDING_TOP + Mosaic.Layout.PADDING_BOTTOM)); + this.scheduleLayout(); +}; + +/** + * Mouse event handler. + * + * @param {Event} event Event. + * @private + */ +Mosaic.prototype.onMouseEvent_ = function(event) { + // Navigating with mouse, enable hover state. + if (!this.suppressHovering_) + this.classList.add('hover-visible'); + + if (event.type == 'mousemove') + return; + + var index = -1; + for (var target = event.target; + target && (target != this); + target = target.parentNode) { + if (target.classList.contains('mosaic-tile')) { + index = this.dataModel_.indexOf(target.getItem()); + break; + } + } + this.selectionController_.handlePointerDownUp(event, index); +}; + +/** + * Scroll handler. + * @private + */ +Mosaic.prototype.onScroll_ = function() { + requestAnimationFrame(function() { + this.loadVisibleTiles_(); + }.bind(this)); +}; + +/** + * Selection change handler. + * + * @param {Event} event Event. + * @private + */ +Mosaic.prototype.onSelection_ = function(event) { + for (var i = 0; i != event.changes.length; i++) { + var change = event.changes[i]; + var tile = this.tiles_[change.index]; + if (tile) tile.select(change.selected); + } +}; + +/** + * Lead item change handler. + * + * @param {Event} event Event. + * @private + */ +Mosaic.prototype.onLeadChange_ = function(event) { + var index = event.newValue; + if (index >= 0) { + var tile = this.tiles_[index]; + if (tile) tile.scrollIntoView(); + } +}; + +/** + * Splice event handler. + * + * @param {Event} event Event. + * @private + */ +Mosaic.prototype.onSplice_ = function(event) { + var index = event.index; + this.layoutModel_.invalidateFromTile_(index); + + if (event.removed.length) { + for (var t = 0; t != event.removed.length; t++) + this.removeChild(this.tiles_[index + t]); + + this.tiles_.splice(index, event.removed.length); + this.scheduleLayout(Mosaic.LAYOUT_DELAY); + } + + if (event.added.length) { + var newTiles = []; + for (var t = 0; t != event.added.length; t++) + newTiles.push(new Mosaic.Tile(this, this.dataModel_.item(index + t))); + + this.tiles_.splice.apply(this.tiles_, [index, 0].concat(newTiles)); + this.initTiles_(newTiles); + } + + if (this.tiles_.length != this.dataModel_.length) + console.error('Mosaic is out of sync'); +}; + +/** + * Content change handler. + * + * @param {Event} event Event. + * @private + */ +Mosaic.prototype.onContentChange_ = function(event) { + if (!this.tiles_) + return; + + if (!event.metadata) + return; // Thumbnail unchanged, nothing to do. + + var index = this.dataModel_.indexOf(event.item); + if (index != this.selectionModel_.selectedIndex) + console.error('Content changed for unselected item'); + + this.layoutModel_.invalidateFromTile_(index); + this.tiles_[index].init(event.metadata, function() { + this.tiles_[index].unload(); + this.tiles_[index].load( + Mosaic.Tile.LoadMode.HIGH_DPI, + this.scheduleLayout.bind(this, Mosaic.LAYOUT_DELAY)); + }.bind(this)); +}; + +/** + * Keydown event handler. + * + * @param {Event} event Event. + * @return {boolean} True if the event has been consumed. + */ +Mosaic.prototype.onKeyDown = function(event) { + this.selectionController_.handleKeyDown(event); + if (event.defaultPrevented) // Navigating with keyboard, hide hover state. + this.classList.remove('hover-visible'); + return event.defaultPrevented; +}; + +/** + * @return {boolean} True if the mosaic zoom effect can be applied. It is + * too slow if there are to many images. + * TODO(kaznacheev): Consider unloading the images that are out of the viewport. + */ +Mosaic.prototype.canZoom = function() { + return this.tiles_.length < 100; +}; + +/** + * Show the mosaic. + */ +Mosaic.prototype.show = function() { + var duration = ImageView.MODE_TRANSITION_DURATION; + if (this.canZoom()) { + // Fade in in parallel with the zoom effect. + this.setAttribute('visible', 'zooming'); + } else { + // Mosaic is not animating but the large image is. Fade in the mosaic + // shortly before the large image animation is done. + duration -= 100; + } + this.showingTimeoutID_ = setTimeout(function() { + this.showingTimeoutID_ = null; + // Make the selection visible. + // If the mosaic is not animated it will start fading in now. + this.setAttribute('visible', 'normal'); + this.loadVisibleTiles_(); + }.bind(this), duration); +}; + +/** + * Hide the mosaic. + */ +Mosaic.prototype.hide = function() { + if (this.showingTimeoutID_ != null) { + clearTimeout(this.showingTimeoutID_); + this.showingTimeoutID_ = null; + } + this.removeAttribute('visible'); +}; + +/** + * Checks if the mosaic view is visible. + * @return {boolean} True if visible, false otherwise. + * @private + */ +Mosaic.prototype.isVisible_ = function() { + return this.hasAttribute('visible'); +}; + +/** + * Loads visible tiles. Ignores consecutive calls. Does not reload already + * loaded images. + * @private + */ +Mosaic.prototype.loadVisibleTiles_ = function() { + if (this.loadVisibleTilesSuppressed_) { + this.loadVisibleTilesScheduled_ = true; + return; + } + + this.loadVisibleTilesSuppressed_ = true; + this.loadVisibleTilesScheduled_ = false; + setTimeout(function() { + this.loadVisibleTilesSuppressed_ = false; + if (this.loadVisibleTilesScheduled_) + this.loadVisibleTiles_(); + }.bind(this), 100); + + // Tiles only in the viewport (visible). + var visibleRect = new Rect(0, + 0, + this.clientWidth, + this.clientHeight); + + // Tiles in the viewport and also some distance on the left and right. + var renderableRect = new Rect(-this.clientWidth, + 0, + 3 * this.clientWidth, + this.clientHeight); + + // Unload tiles out of scope. + for (var index = 0; index < this.tiles_.length; index++) { + var tile = this.tiles_[index]; + var imageRect = tile.getImageRect(); + // Unload a thumbnail. + if (imageRect && !imageRect.intersects(renderableRect)) + tile.unload(); + } + + // Load the visible tiles first. + var allVisibleLoaded = true; + // Show high-dpi only when the mosaic view is visible. + var loadMode = this.isVisible_() ? Mosaic.Tile.LoadMode.HIGH_DPI : + Mosaic.Tile.LoadMode.LOW_DPI; + for (var index = 0; index < this.tiles_.length; index++) { + var tile = this.tiles_[index]; + var imageRect = tile.getImageRect(); + // Load a thumbnail. + if (!tile.isLoading(loadMode) && !tile.isLoaded(loadMode) && imageRect && + imageRect.intersects(visibleRect)) { + tile.load(loadMode, function() {}); + allVisibleLoaded = false; + } + } + + // Load also another, nearby, if the visible has been already loaded. + if (allVisibleLoaded) { + for (var index = 0; index < this.tiles_.length; index++) { + var tile = this.tiles_[index]; + var imageRect = tile.getImageRect(); + // Load a thumbnail. + if (!tile.isLoading() && !tile.isLoaded() && imageRect && + imageRect.intersects(renderableRect)) { + tile.load(Mosaic.Tile.LoadMode.LOW_DPI, function() {}); + } + } + } +}; + +/** + * Apply or reset the zoom transform. + * + * @param {Rect} tileRect Tile rectangle. Reset the transform if null. + * @param {Rect} imageRect Large image rectangle. Reset the transform if null. + * @param {boolean=} opt_instant True of the transition should be instant. + */ +Mosaic.prototype.transform = function(tileRect, imageRect, opt_instant) { + if (opt_instant) { + this.style.webkitTransitionDuration = '0'; + } else { + this.style.webkitTransitionDuration = + ImageView.MODE_TRANSITION_DURATION + 'ms'; + } + + if (this.canZoom() && tileRect && imageRect) { + var scaleX = imageRect.width / tileRect.width; + var scaleY = imageRect.height / tileRect.height; + var shiftX = (imageRect.left + imageRect.width / 2) - + (tileRect.left + tileRect.width / 2); + var shiftY = (imageRect.top + imageRect.height / 2) - + (tileRect.top + tileRect.height / 2); + this.style.webkitTransform = + 'translate(' + shiftX * scaleX + 'px, ' + shiftY * scaleY + 'px)' + + 'scaleX(' + scaleX + ') scaleY(' + scaleY + ')'; + } else { + this.style.webkitTransform = ''; + } +}; + +//////////////////////////////////////////////////////////////////////////////// + +/** + * Creates a selection controller that is to be used with grid. + * @param {cr.ui.ListSelectionModel} selectionModel The selection model to + * interact with. + * @param {Mosaic.Layout} layoutModel The layout model to use. + * @constructor + * @extends {!cr.ui.ListSelectionController} + */ +Mosaic.SelectionController = function(selectionModel, layoutModel) { + cr.ui.ListSelectionController.call(this, selectionModel); + this.layoutModel_ = layoutModel; +}; + +/** + * Extends cr.ui.ListSelectionController. + */ +Mosaic.SelectionController.prototype.__proto__ = + cr.ui.ListSelectionController.prototype; + +/** @override */ +Mosaic.SelectionController.prototype.getLastIndex = function() { + return this.layoutModel_.getLaidOutTileCount() - 1; +}; + +/** @override */ +Mosaic.SelectionController.prototype.getIndexBefore = function(index) { + return this.layoutModel_.getHorizontalAdjacentIndex(index, -1); +}; + +/** @override */ +Mosaic.SelectionController.prototype.getIndexAfter = function(index) { + return this.layoutModel_.getHorizontalAdjacentIndex(index, 1); +}; + +/** @override */ +Mosaic.SelectionController.prototype.getIndexAbove = function(index) { + return this.layoutModel_.getVerticalAdjacentIndex(index, -1); +}; + +/** @override */ +Mosaic.SelectionController.prototype.getIndexBelow = function(index) { + return this.layoutModel_.getVerticalAdjacentIndex(index, 1); +}; + +//////////////////////////////////////////////////////////////////////////////// + +/** + * Mosaic layout. + * + * @param {string=} opt_mode Layout mode. + * @param {Mosaic.Density=} opt_maxDensity Layout density. + * @constructor + */ +Mosaic.Layout = function(opt_mode, opt_maxDensity) { + this.mode_ = opt_mode || Mosaic.Layout.MODE_TENTATIVE; + this.maxDensity_ = opt_maxDensity || Mosaic.Density.createHighest(); + this.reset_(); +}; + +/** + * Blank space at the top of the mosaic element. We do not do that in CSS + * to make transition effects easier. + */ +Mosaic.Layout.PADDING_TOP = 50; + +/** + * Blank space at the bottom of the mosaic element. + */ +Mosaic.Layout.PADDING_BOTTOM = 50; + +/** + * Horizontal and vertical spacing between images. Should be kept in sync + * with the style of .mosaic-item in gallery.css (= 2 * ( 4 + 1)) + */ +Mosaic.Layout.SPACING = 10; + +/** + * Margin for scrolling using keyboard. Distance between a selected tile + * and window border. + */ +Mosaic.Layout.SCROLL_MARGIN = 30; + +/** + * Layout mode: commit to DOM immediately. + */ +Mosaic.Layout.MODE_FINAL = 'final'; + +/** + * Layout mode: do not commit layout to DOM until it is complete or the viewport + * overflows. + */ +Mosaic.Layout.MODE_TENTATIVE = 'tentative'; + +/** + * Layout mode: never commit layout to DOM. + */ +Mosaic.Layout.MODE_DRY_RUN = 'dry_run'; + +/** + * Reset the layout. + * + * @private + */ +Mosaic.Layout.prototype.reset_ = function() { + this.columns_ = []; + this.newColumn_ = null; + this.density_ = Mosaic.Density.createLowest(); + if (this.mode_ != Mosaic.Layout.MODE_DRY_RUN) // DRY_RUN is sticky. + this.mode_ = Mosaic.Layout.MODE_TENTATIVE; +}; + +/** + * @param {number} width Viewport width. + * @param {number} height Viewport height. + */ +Mosaic.Layout.prototype.setViewportSize = function(width, height) { + this.viewportWidth_ = width; + this.viewportHeight_ = height; + this.reset_(); +}; + +/** + * @return {number} Total width of the layout. + */ +Mosaic.Layout.prototype.getWidth = function() { + var lastColumn = this.getLastColumn_(); + return lastColumn ? lastColumn.getRight() : 0; +}; + +/** + * @return {number} Total height of the layout. + */ +Mosaic.Layout.prototype.getHeight = function() { + var firstColumn = this.columns_[0]; + return firstColumn ? firstColumn.getHeight() : 0; +}; + +/** + * @return {Array.<Mosaic.Tile>} All tiles in the layout. + */ +Mosaic.Layout.prototype.getTiles = function() { + return Array.prototype.concat.apply([], + this.columns_.map(function(c) { return c.getTiles() })); +}; + +/** + * @return {number} Total number of tiles added to the layout. + */ +Mosaic.Layout.prototype.getTileCount = function() { + return this.getLaidOutTileCount() + + (this.newColumn_ ? this.newColumn_.getTileCount() : 0); +}; + +/** + * @return {Mosaic.Column} The last column or null for empty layout. + * @private + */ +Mosaic.Layout.prototype.getLastColumn_ = function() { + return this.columns_.length ? this.columns_[this.columns_.length - 1] : null; +}; + +/** + * @return {number} Total number of tiles in completed columns. + */ +Mosaic.Layout.prototype.getLaidOutTileCount = function() { + var lastColumn = this.getLastColumn_(); + return lastColumn ? lastColumn.getNextTileIndex() : 0; +}; + +/** + * Add a tile to the layout. + * + * @param {Mosaic.Tile} tile The tile to be added. + * @param {boolean} isLast True if this tile is the last. + */ +Mosaic.Layout.prototype.add = function(tile, isLast) { + var layoutQueue = [tile]; + + // There are two levels of backtracking in the layout algorithm. + // |Mosaic.Layout.density_| tracks the state of the 'global' backtracking + // which aims to use as much of the viewport space as possible. + // It starts with the lowest density and increases it until the layout + // fits into the viewport. If it does not fit even at the highest density, + // the layout continues with the highest density. + // + // |Mosaic.Column.density_| tracks the state of the 'local' backtracking + // which aims to avoid producing unnaturally looking columns. + // It starts with the current global density and decreases it until the column + // looks nice. + + while (layoutQueue.length) { + if (!this.newColumn_) { + var lastColumn = this.getLastColumn_(); + this.newColumn_ = new Mosaic.Column( + this.columns_.length, + lastColumn ? lastColumn.getNextRowIndex() : 0, + lastColumn ? lastColumn.getNextTileIndex() : 0, + lastColumn ? lastColumn.getRight() : 0, + this.viewportHeight_, + this.density_.clone()); + } + + this.newColumn_.add(layoutQueue.shift()); + + var isFinalColumn = isLast && !layoutQueue.length; + + if (!this.newColumn_.prepareLayout(isFinalColumn)) + continue; // Column is incomplete. + + if (this.newColumn_.isSuboptimal()) { + layoutQueue = this.newColumn_.getTiles().concat(layoutQueue); + this.newColumn_.retryWithLowerDensity(); + continue; + } + + this.columns_.push(this.newColumn_); + this.newColumn_ = null; + + if (this.mode_ == Mosaic.Layout.MODE_FINAL) { + this.getLastColumn_().layout(); + continue; + } + + if (this.getWidth() > this.viewportWidth_) { + // Viewport completely filled. + if (this.density_.equals(this.maxDensity_)) { + // Max density reached, commit if tentative, just continue if dry run. + if (this.mode_ == Mosaic.Layout.MODE_TENTATIVE) + this.commit_(); + continue; + } + + // Rollback the entire layout, retry with higher density. + layoutQueue = this.getTiles().concat(layoutQueue); + this.columns_ = []; + this.density_.increase(); + continue; + } + + if (isFinalColumn && this.mode_ == Mosaic.Layout.MODE_TENTATIVE) { + // The complete tentative layout fits into the viewport. + var stretched = this.findHorizontalLayout_(); + if (stretched) + this.columns_ = stretched.columns_; + // Center the layout in the viewport and commit. + this.commit_((this.viewportWidth_ - this.getWidth()) / 2, + (this.viewportHeight_ - this.getHeight()) / 2); + } + } +}; + +/** + * Commit the tentative layout. + * + * @param {number=} opt_offsetX Horizontal offset. + * @param {number=} opt_offsetY Vertical offset. + * @private + */ +Mosaic.Layout.prototype.commit_ = function(opt_offsetX, opt_offsetY) { + console.assert(this.mode_ != Mosaic.Layout.MODE_FINAL, + 'Did not expect final layout'); + for (var i = 0; i != this.columns_.length; i++) { + this.columns_[i].layout(opt_offsetX, opt_offsetY); + } + this.mode_ = Mosaic.Layout.MODE_FINAL; +}; + +/** + * Find the most horizontally stretched layout built from the same tiles. + * + * The main layout algorithm fills the entire available viewport height. + * If there is too few tiles this results in a layout that is unnaturally + * stretched in the vertical direction. + * + * This method tries a number of smaller heights and returns the most + * horizontally stretched layout that still fits into the viewport. + * + * @return {Mosaic.Layout} A horizontally stretched layout. + * @private + */ +Mosaic.Layout.prototype.findHorizontalLayout_ = function() { + // If the layout aspect ratio is not dramatically different from + // the viewport aspect ratio then there is no need to optimize. + if (this.getWidth() / this.getHeight() > + this.viewportWidth_ / this.viewportHeight_ * 0.9) + return null; + + var tiles = this.getTiles(); + if (tiles.length == 1) + return null; // Single tile layout is always the same. + + var tileHeights = tiles.map(function(t) { return t.getMaxContentHeight() }); + var minTileHeight = Math.min.apply(null, tileHeights); + + for (var h = minTileHeight; h < this.viewportHeight_; h += minTileHeight) { + var layout = new Mosaic.Layout( + Mosaic.Layout.MODE_DRY_RUN, this.density_.clone()); + layout.setViewportSize(this.viewportWidth_, h); + for (var t = 0; t != tiles.length; t++) + layout.add(tiles[t], t + 1 == tiles.length); + + if (layout.getWidth() <= this.viewportWidth_) + return layout; + } + + return null; +}; + +/** + * Invalidate the layout after the given tile was modified (added, deleted or + * changed dimensions). + * + * @param {number} index Tile index. + * @private + */ +Mosaic.Layout.prototype.invalidateFromTile_ = function(index) { + var columnIndex = this.getColumnIndexByTile_(index); + if (columnIndex < 0) + return; // Index not in the layout, probably already invalidated. + + if (this.columns_[columnIndex].getLeft() >= this.viewportWidth_) { + // The columns to the right cover the entire viewport width, so there is no + // chance that the modified layout would fit into the viewport. + // No point in restarting the entire layout, keep the columns to the right. + console.assert(this.mode_ == Mosaic.Layout.MODE_FINAL, + 'Expected FINAL layout mode'); + this.columns_ = this.columns_.slice(0, columnIndex); + this.newColumn_ = null; + } else { + // There is a chance that the modified layout would fit into the viewport. + this.reset_(); + this.mode_ = Mosaic.Layout.MODE_TENTATIVE; + } +}; + +/** + * Get the index of the tile to the left or to the right from the given tile. + * + * @param {number} index Tile index. + * @param {number} direction -1 for left, 1 for right. + * @return {number} Adjacent tile index. + */ +Mosaic.Layout.prototype.getHorizontalAdjacentIndex = function( + index, direction) { + var column = this.getColumnIndexByTile_(index); + if (column < 0) { + console.error('Cannot find column for tile #' + index); + return -1; + } + + var row = this.columns_[column].getRowByTileIndex(index); + if (!row) { + console.error('Cannot find row for tile #' + index); + return -1; + } + + var sameRowNeighbourIndex = index + direction; + if (row.hasTile(sameRowNeighbourIndex)) + return sameRowNeighbourIndex; + + var adjacentColumn = column + direction; + if (adjacentColumn < 0 || adjacentColumn == this.columns_.length) + return -1; + + return this.columns_[adjacentColumn]. + getEdgeTileIndex_(row.getCenterY(), -direction); +}; + +/** + * Get the index of the tile to the top or to the bottom from the given tile. + * + * @param {number} index Tile index. + * @param {number} direction -1 for above, 1 for below. + * @return {number} Adjacent tile index. + */ +Mosaic.Layout.prototype.getVerticalAdjacentIndex = function( + index, direction) { + var column = this.getColumnIndexByTile_(index); + if (column < 0) { + console.error('Cannot find column for tile #' + index); + return -1; + } + + var row = this.columns_[column].getRowByTileIndex(index); + if (!row) { + console.error('Cannot find row for tile #' + index); + return -1; + } + + // Find the first item in the next row, or the last item in the previous row. + var adjacentRowNeighbourIndex = + row.getEdgeTileIndex_(direction) + direction; + + if (adjacentRowNeighbourIndex < 0 || + adjacentRowNeighbourIndex > this.getTileCount() - 1) + return -1; + + if (!this.columns_[column].hasTile(adjacentRowNeighbourIndex)) { + // It is not in the current column, so return it. + return adjacentRowNeighbourIndex; + } else { + // It is in the current column, so we have to find optically the closest + // tile in the adjacent row. + var adjacentRow = this.columns_[column].getRowByTileIndex( + adjacentRowNeighbourIndex); + var previousTileCenterX = row.getTileByIndex(index).getCenterX(); + + // Find the closest one. + var closestIndex = -1; + var closestDistance; + var adjacentRowTiles = adjacentRow.getTiles(); + for (var t = 0; t != adjacentRowTiles.length; t++) { + var distance = + Math.abs(adjacentRowTiles[t].getCenterX() - previousTileCenterX); + if (closestIndex == -1 || distance < closestDistance) { + closestIndex = adjacentRow.getEdgeTileIndex_(-1) + t; + closestDistance = distance; + } + } + return closestIndex; + } +}; + +/** + * @param {number} index Tile index. + * @return {number} Index of the column containing the given tile. + * @private + */ +Mosaic.Layout.prototype.getColumnIndexByTile_ = function(index) { + for (var c = 0; c != this.columns_.length; c++) { + if (this.columns_[c].hasTile(index)) + return c; + } + return -1; +}; + +/** + * Scale the given array of size values to satisfy 3 conditions: + * 1. The new sizes must be integer. + * 2. The new sizes must sum up to the given |total| value. + * 3. The relative proportions of the sizes should be as close to the original + * as possible. + * + * @param {Array.<number>} sizes Array of sizes. + * @param {number} newTotal New total size. + */ +Mosaic.Layout.rescaleSizesToNewTotal = function(sizes, newTotal) { + var total = 0; + + var partialTotals = [0]; + for (var i = 0; i != sizes.length; i++) { + total += sizes[i]; + partialTotals.push(total); + } + + var scale = newTotal / total; + + for (i = 0; i != sizes.length; i++) { + sizes[i] = Math.round(partialTotals[i + 1] * scale) - + Math.round(partialTotals[i] * scale); + } +}; + +//////////////////////////////////////////////////////////////////////////////// + +/** + * Representation of the layout density. + * + * @param {number} horizontal Horizontal density, number tiles per row. + * @param {number} vertical Vertical density, frequency of rows forced to + * contain a single tile. + * @constructor + */ +Mosaic.Density = function(horizontal, vertical) { + this.horizontal = horizontal; + this.vertical = vertical; +}; + +/** + * Minimal horizontal density (tiles per row). + */ +Mosaic.Density.MIN_HORIZONTAL = 1; + +/** + * Minimal horizontal density (tiles per row). + */ +Mosaic.Density.MAX_HORIZONTAL = 3; + +/** + * Minimal vertical density: force 1 out of 2 rows to containt a single tile. + */ +Mosaic.Density.MIN_VERTICAL = 2; + +/** + * Maximal vertical density: force 1 out of 3 rows to containt a single tile. + */ +Mosaic.Density.MAX_VERTICAL = 3; + +/** + * @return {Mosaic.Density} Lowest density. + */ +Mosaic.Density.createLowest = function() { + return new Mosaic.Density( + Mosaic.Density.MIN_HORIZONTAL, + Mosaic.Density.MIN_VERTICAL /* ignored when horizontal is at min */); +}; + +/** + * @return {Mosaic.Density} Highest density. + */ +Mosaic.Density.createHighest = function() { + return new Mosaic.Density( + Mosaic.Density.MAX_HORIZONTAL, + Mosaic.Density.MAX_VERTICAL); +}; + +/** + * @return {Mosaic.Density} A clone of this density object. + */ +Mosaic.Density.prototype.clone = function() { + return new Mosaic.Density(this.horizontal, this.vertical); +}; + +/** + * @param {Mosaic.Density} that The other object. + * @return {boolean} True if equal. + */ +Mosaic.Density.prototype.equals = function(that) { + return this.horizontal == that.horizontal && + this.vertical == that.vertical; +}; + +/** + * Increase the density to the next level. + */ +Mosaic.Density.prototype.increase = function() { + if (this.horizontal == Mosaic.Density.MIN_HORIZONTAL || + this.vertical == Mosaic.Density.MAX_VERTICAL) { + console.assert(this.horizontal < Mosaic.Density.MAX_HORIZONTAL); + this.horizontal++; + this.vertical = Mosaic.Density.MIN_VERTICAL; + } else { + this.vertical++; + } +}; + +/** + * Decrease horizontal density. + */ +Mosaic.Density.prototype.decreaseHorizontal = function() { + console.assert(this.horizontal > Mosaic.Density.MIN_HORIZONTAL); + this.horizontal--; +}; + +/** + * @param {number} tileCount Number of tiles in the row. + * @param {number} rowIndex Global row index. + * @return {boolean} True if the row is complete. + */ +Mosaic.Density.prototype.isRowComplete = function(tileCount, rowIndex) { + return (tileCount == this.horizontal) || (rowIndex % this.vertical) == 0; +}; + +//////////////////////////////////////////////////////////////////////////////// + +/** + * A column in a mosaic layout. Contains rows. + * + * @param {number} index Column index. + * @param {number} firstRowIndex Global row index. + * @param {number} firstTileIndex Index of the first tile in the column. + * @param {number} left Left edge coordinate. + * @param {number} maxHeight Maximum height. + * @param {Mosaic.Density} density Layout density. + * @constructor + */ +Mosaic.Column = function(index, firstRowIndex, firstTileIndex, left, maxHeight, + density) { + this.index_ = index; + this.firstRowIndex_ = firstRowIndex; + this.firstTileIndex_ = firstTileIndex; + this.left_ = left; + this.maxHeight_ = maxHeight; + this.density_ = density; + + this.reset_(); +}; + +/** + * Reset the layout. + * @private + */ +Mosaic.Column.prototype.reset_ = function() { + this.tiles_ = []; + this.rows_ = []; + this.newRow_ = null; +}; + +/** + * @return {number} Number of tiles in the column. + */ +Mosaic.Column.prototype.getTileCount = function() { return this.tiles_.length }; + +/** + * @return {number} Index of the last tile + 1. + */ +Mosaic.Column.prototype.getNextTileIndex = function() { + return this.firstTileIndex_ + this.getTileCount(); +}; + +/** + * @return {number} Global index of the last row + 1. + */ +Mosaic.Column.prototype.getNextRowIndex = function() { + return this.firstRowIndex_ + this.rows_.length; +}; + +/** + * @return {Array.<Mosaic.Tile>} Array of tiles in the column. + */ +Mosaic.Column.prototype.getTiles = function() { return this.tiles_ }; + +/** + * @param {number} index Tile index. + * @return {boolean} True if this column contains the tile with the given index. + */ +Mosaic.Column.prototype.hasTile = function(index) { + return this.firstTileIndex_ <= index && + index < (this.firstTileIndex_ + this.getTileCount()); +}; + +/** + * @param {number} y Y coordinate. + * @param {number} direction -1 for left, 1 for right. + * @return {number} Index of the tile lying on the edge of the column at the + * given y coordinate. + * @private + */ +Mosaic.Column.prototype.getEdgeTileIndex_ = function(y, direction) { + for (var r = 0; r < this.rows_.length; r++) { + if (this.rows_[r].coversY(y)) + return this.rows_[r].getEdgeTileIndex_(direction); + } + return -1; +}; + +/** + * @param {number} index Tile index. + * @return {Mosaic.Row} The row containing the tile with a given index. + */ +Mosaic.Column.prototype.getRowByTileIndex = function(index) { + for (var r = 0; r != this.rows_.length; r++) + if (this.rows_[r].hasTile(index)) + return this.rows_[r]; + + return null; +}; + +/** + * Add a tile to the column. + * + * @param {Mosaic.Tile} tile The tile to add. + */ +Mosaic.Column.prototype.add = function(tile) { + var rowIndex = this.getNextRowIndex(); + + if (!this.newRow_) + this.newRow_ = new Mosaic.Row(this.getNextTileIndex()); + + this.tiles_.push(tile); + this.newRow_.add(tile); + + if (this.density_.isRowComplete(this.newRow_.getTileCount(), rowIndex)) { + this.rows_.push(this.newRow_); + this.newRow_ = null; + } +}; + +/** + * Prepare the column layout. + * + * @param {boolean=} opt_force True if the layout must be performed even for an + * incomplete column. + * @return {boolean} True if the layout was performed. + */ +Mosaic.Column.prototype.prepareLayout = function(opt_force) { + if (opt_force && this.newRow_) { + this.rows_.push(this.newRow_); + this.newRow_ = null; + } + + if (this.rows_.length == 0) + return false; + + this.width_ = Math.min.apply( + null, this.rows_.map(function(row) { return row.getMaxWidth() })); + + this.height_ = 0; + + this.rowHeights_ = []; + for (var r = 0; r != this.rows_.length; r++) { + var rowHeight = this.rows_[r].getHeightForWidth(this.width_); + this.height_ += rowHeight; + this.rowHeights_.push(rowHeight); + } + + var overflow = this.height_ / this.maxHeight_; + if (!opt_force && (overflow < 1)) + return false; + + if (overflow > 1) { + // Scale down the column width and height. + this.width_ = Math.round(this.width_ / overflow); + this.height_ = this.maxHeight_; + Mosaic.Layout.rescaleSizesToNewTotal(this.rowHeights_, this.maxHeight_); + } + + return true; +}; + +/** + * Retry the column layout with less tiles per row. + */ +Mosaic.Column.prototype.retryWithLowerDensity = function() { + this.density_.decreaseHorizontal(); + this.reset_(); +}; + +/** + * @return {number} Column left edge coordinate. + */ +Mosaic.Column.prototype.getLeft = function() { return this.left_ }; + +/** + * @return {number} Column right edge coordinate after the layout. + */ +Mosaic.Column.prototype.getRight = function() { + return this.left_ + this.width_; +}; + +/** + * @return {number} Column height after the layout. + */ +Mosaic.Column.prototype.getHeight = function() { return this.height_ }; + +/** + * Perform the column layout. + * @param {number=} opt_offsetX Horizontal offset. + * @param {number=} opt_offsetY Vertical offset. + */ +Mosaic.Column.prototype.layout = function(opt_offsetX, opt_offsetY) { + opt_offsetX = opt_offsetX || 0; + opt_offsetY = opt_offsetY || 0; + var rowTop = Mosaic.Layout.PADDING_TOP; + for (var r = 0; r != this.rows_.length; r++) { + this.rows_[r].layout( + opt_offsetX + this.left_, + opt_offsetY + rowTop, + this.width_, + this.rowHeights_[r]); + rowTop += this.rowHeights_[r]; + } +}; + +/** + * Check if the column layout is too ugly to be displayed. + * + * @return {boolean} True if the layout is suboptimal. + */ +Mosaic.Column.prototype.isSuboptimal = function() { + var tileCounts = + this.rows_.map(function(row) { return row.getTileCount() }); + + var maxTileCount = Math.max.apply(null, tileCounts); + if (maxTileCount == 1) + return false; // Every row has exactly 1 tile, as optimal as it gets. + + var sizes = + this.tiles_.map(function(tile) { return tile.getMaxContentHeight() }); + + // Ugly layout #1: all images are small and some are one the same row. + var allSmall = Math.max.apply(null, sizes) <= Mosaic.Tile.SMALL_IMAGE_SIZE; + if (allSmall) + return true; + + // Ugly layout #2: all images are large and none occupies an entire row. + var allLarge = Math.min.apply(null, sizes) > Mosaic.Tile.SMALL_IMAGE_SIZE; + var allCombined = Math.min.apply(null, tileCounts) != 1; + if (allLarge && allCombined) + return true; + + // Ugly layout #3: some rows have too many tiles for the resulting width. + if (this.width_ / maxTileCount < 100) + return true; + + return false; +}; + +//////////////////////////////////////////////////////////////////////////////// + +/** + * A row in a mosaic layout. Contains tiles. + * + * @param {number} firstTileIndex Index of the first tile in the row. + * @constructor + */ +Mosaic.Row = function(firstTileIndex) { + this.firstTileIndex_ = firstTileIndex; + this.tiles_ = []; +}; + +/** + * @param {Mosaic.Tile} tile The tile to add. + */ +Mosaic.Row.prototype.add = function(tile) { + console.assert(this.getTileCount() < Mosaic.Density.MAX_HORIZONTAL); + this.tiles_.push(tile); +}; + +/** + * @return {Array.<Mosaic.Tile>} Array of tiles in the row. + */ +Mosaic.Row.prototype.getTiles = function() { return this.tiles_ }; + +/** + * Get a tile by index. + * @param {number} index Tile index. + * @return {Mosaic.Tile} Requested tile or null if not found. + */ +Mosaic.Row.prototype.getTileByIndex = function(index) { + if (!this.hasTile(index)) + return null; + return this.tiles_[index - this.firstTileIndex_]; +}; + +/** + * + * @return {number} Number of tiles in the row. + */ +Mosaic.Row.prototype.getTileCount = function() { return this.tiles_.length }; + +/** + * @param {number} index Tile index. + * @return {boolean} True if this row contains the tile with the given index. + */ +Mosaic.Row.prototype.hasTile = function(index) { + return this.firstTileIndex_ <= index && + index < (this.firstTileIndex_ + this.tiles_.length); +}; + +/** + * @param {number} y Y coordinate. + * @return {boolean} True if this row covers the given Y coordinate. + */ +Mosaic.Row.prototype.coversY = function(y) { + return this.top_ <= y && y < (this.top_ + this.height_); +}; + +/** + * @return {number} Y coordinate of the tile center. + */ +Mosaic.Row.prototype.getCenterY = function() { + return this.top_ + Math.round(this.height_ / 2); +}; + +/** + * Get the first or the last tile. + * + * @param {number} direction -1 for the first tile, 1 for the last tile. + * @return {number} Tile index. + * @private + */ +Mosaic.Row.prototype.getEdgeTileIndex_ = function(direction) { + if (direction < 0) + return this.firstTileIndex_; + else + return this.firstTileIndex_ + this.getTileCount() - 1; +}; + +/** + * @return {number} Aspect ration of the combined content box of this row. + * @private + */ +Mosaic.Row.prototype.getTotalContentAspectRatio_ = function() { + var sum = 0; + for (var t = 0; t != this.tiles_.length; t++) + sum += this.tiles_[t].getAspectRatio(); + return sum; +}; + +/** + * @return {number} Total horizontal spacing in this row. This includes + * the spacing between the tiles and both left and right margins. + * + * @private + */ +Mosaic.Row.prototype.getTotalHorizontalSpacing_ = function() { + return Mosaic.Layout.SPACING * this.getTileCount(); +}; + +/** + * @return {number} Maximum width that this row may have without overscaling + * any of the tiles. + */ +Mosaic.Row.prototype.getMaxWidth = function() { + var contentHeight = Math.min.apply(null, + this.tiles_.map(function(tile) { return tile.getMaxContentHeight() })); + + var contentWidth = + Math.round(contentHeight * this.getTotalContentAspectRatio_()); + return contentWidth + this.getTotalHorizontalSpacing_(); +}; + +/** + * Compute the height that best fits the supplied row width given + * aspect ratios of the tiles in this row. + * + * @param {number} width Row width. + * @return {number} Height. + */ +Mosaic.Row.prototype.getHeightForWidth = function(width) { + var contentWidth = width - this.getTotalHorizontalSpacing_(); + var contentHeight = + Math.round(contentWidth / this.getTotalContentAspectRatio_()); + return contentHeight + Mosaic.Layout.SPACING; +}; + +/** + * Position the row in the mosaic. + * + * @param {number} left Left position. + * @param {number} top Top position. + * @param {number} width Width. + * @param {number} height Height. + */ +Mosaic.Row.prototype.layout = function(left, top, width, height) { + this.top_ = top; + this.height_ = height; + + var contentWidth = width - this.getTotalHorizontalSpacing_(); + var contentHeight = height - Mosaic.Layout.SPACING; + + var tileContentWidth = this.tiles_.map( + function(tile) { return tile.getAspectRatio() }); + + Mosaic.Layout.rescaleSizesToNewTotal(tileContentWidth, contentWidth); + + var tileLeft = left; + for (var t = 0; t != this.tiles_.length; t++) { + var tileWidth = tileContentWidth[t] + Mosaic.Layout.SPACING; + this.tiles_[t].layout(tileLeft, top, tileWidth, height); + tileLeft += tileWidth; + } +}; + +//////////////////////////////////////////////////////////////////////////////// + +/** + * A single tile of the image mosaic. + * + * @param {Element} container Container element. + * @param {Gallery.Item} item Gallery item associated with this tile. + * @return {Element} The new tile element. + * @constructor + */ +Mosaic.Tile = function(container, item) { + var self = container.ownerDocument.createElement('div'); + Mosaic.Tile.decorate(self, container, item); + return self; +}; + +/** + * @param {Element} self Self pointer. + * @param {Element} container Container element. + * @param {Gallery.Item} item Gallery item associated with this tile. + */ +Mosaic.Tile.decorate = function(self, container, item) { + self.__proto__ = Mosaic.Tile.prototype; + self.className = 'mosaic-tile'; + + self.container_ = container; + self.item_ = item; + self.left_ = null; // Mark as not laid out. +}; + +/** + * Load mode for the tile's image. + * @enum {number} + */ +Mosaic.Tile.LoadMode = { + LOW_DPI: 0, + HIGH_DPI: 1 +}; + +/** +* Inherit from HTMLDivElement. +*/ +Mosaic.Tile.prototype.__proto__ = HTMLDivElement.prototype; + +/** + * Minimum tile content size. + */ +Mosaic.Tile.MIN_CONTENT_SIZE = 64; + +/** + * Maximum tile content size. + */ +Mosaic.Tile.MAX_CONTENT_SIZE = 512; + +/** + * Default size for a tile with no thumbnail image. + */ +Mosaic.Tile.GENERIC_ICON_SIZE = 128; + +/** + * Max size of an image considered to be 'small'. + * Small images are laid out slightly differently. + */ +Mosaic.Tile.SMALL_IMAGE_SIZE = 160; + +/** + * @return {Gallery.Item} The Gallery item. + */ +Mosaic.Tile.prototype.getItem = function() { return this.item_ }; + +/** + * @return {number} Maximum content height that this tile can have. + */ +Mosaic.Tile.prototype.getMaxContentHeight = function() { + return this.maxContentHeight_; +}; + +/** + * @return {number} The aspect ratio of the tile image. + */ +Mosaic.Tile.prototype.getAspectRatio = function() { return this.aspectRatio_ }; + +/** + * @return {boolean} True if the tile is initialized. + */ +Mosaic.Tile.prototype.isInitialized = function() { + return !!this.maxContentHeight_; +}; + +/** + * Checks whether the image of specified (or better resolution) has been loaded. + * + * @param {Mosaic.Tile.LoadMode=} opt_loadMode Loading mode, default: LOW_DPI. + * @return {boolean} True if the tile is loaded with the specified dpi or + * better. + */ +Mosaic.Tile.prototype.isLoaded = function(opt_loadMode) { + var loadMode = opt_loadMode || Mosaic.Tile.LoadMode.LOW_DPI; + switch (loadMode) { + case Mosaic.Tile.LoadMode.LOW_DPI: + if (this.imagePreloaded_ || this.imageLoaded_) + return true; + break; + case Mosaic.Tile.LoadMode.HIGH_DPI: + if (this.imageLoaded_) + return true; + break; + } + return false; +}; + +/** + * Checks whether the image of specified (or better resolution) is being loaded. + * + * @param {Mosaic.Tile.LoadMode=} opt_loadMode Loading mode, default: LOW_DPI. + * @return {boolean} True if the tile is being loaded with the specified dpi or + * better. + */ +Mosaic.Tile.prototype.isLoading = function(opt_loadMode) { + var loadMode = opt_loadMode || Mosaic.Tile.LoadMode.LOW_DPI; + switch (loadMode) { + case Mosaic.Tile.LoadMode.LOW_DPI: + if (this.imagePreloading_ || this.imageLoading_) + return true; + break; + case Mosaic.Tile.LoadMode.HIGH_DPI: + if (this.imageLoading_) + return true; + break; + } + return false; +}; + +/** + * Mark the tile as not loaded to prevent it from participating in the layout. + */ +Mosaic.Tile.prototype.markUnloaded = function() { + this.maxContentHeight_ = 0; + if (this.thumbnailLoader_) { + this.thumbnailLoader_.cancel(); + this.imagePreloaded_ = false; + this.imagePreloading_ = false; + this.imageLoaded_ = false; + this.imageLoading_ = false; + } +}; + +/** + * Initializes the thumbnail in the tile. Does not load an image, but sets + * target dimensions using metadata. + * + * @param {Object} metadata Metadata object. + * @param {function()} onImageMeasured Image measured callback. + */ +Mosaic.Tile.prototype.init = function(metadata, onImageMeasured) { + this.markUnloaded(); + this.left_ = null; // Mark as not laid out. + + // Set higher priority for the selected elements to load them first. + var priority = this.getAttribute('selected') ? 2 : 3; + + // Use embedded thumbnails on Drive, since they have higher resolution. + var hidpiEmbedded = FileType.isOnDrive(this.getItem().getEntry()); + this.thumbnailLoader_ = new ThumbnailLoader( + this.getItem().getEntry().toURL(), + ThumbnailLoader.LoaderType.CANVAS, + metadata, + undefined, // Media type. + hidpiEmbedded ? ThumbnailLoader.UseEmbedded.USE_EMBEDDED : + ThumbnailLoader.UseEmbedded.NO_EMBEDDED, + priority); + + // If no hidpi embedded thumbnail available, then use the low resolution + // for preloading. + if (!hidpiEmbedded) { + this.thumbnailPreloader_ = new ThumbnailLoader( + this.getItem().getEntry().toURL(), + ThumbnailLoader.LoaderType.CANVAS, + metadata, + undefined, // Media type. + ThumbnailLoader.UseEmbedded.USE_EMBEDDED, + 2); // Preloaders have always higher priotity, so the preload images + // are loaded as soon as possible. + } + + var setDimensions = function(width, height) { + if (width > height) { + if (width > Mosaic.Tile.MAX_CONTENT_SIZE) { + height = Math.round(height * Mosaic.Tile.MAX_CONTENT_SIZE / width); + width = Mosaic.Tile.MAX_CONTENT_SIZE; + } + } else { + if (height > Mosaic.Tile.MAX_CONTENT_SIZE) { + width = Math.round(width * Mosaic.Tile.MAX_CONTENT_SIZE / height); + height = Mosaic.Tile.MAX_CONTENT_SIZE; + } + } + this.maxContentHeight_ = Math.max(Mosaic.Tile.MIN_CONTENT_SIZE, height); + this.aspectRatio_ = width / height; + onImageMeasured(); + }.bind(this); + + // Dimensions are always acquired from the metadata. For local files, it is + // extracted from headers. For Drive files, it is received via the Drive API. + // If the dimensions are not available, then the fallback dimensions will be + // used (same as for the generic icon). + if (metadata.media && metadata.media.width) { + setDimensions(metadata.media.width, metadata.media.height); + } else if (metadata.drive && metadata.drive.imageWidth && + metadata.drive.imageHeight) { + setDimensions(metadata.drive.imageWidth, metadata.drive.imageHeight); + } else { + // No dimensions in metadata, then use the generic dimensions. + setDimensions(Mosaic.Tile.GENERIC_ICON_SIZE, + Mosaic.Tile.GENERIC_ICON_SIZE); + } +}; + +/** + * Loads an image into the tile. + * + * The mode argument is a hint. Use low-dpi for faster response, and high-dpi + * for better output, but possibly affecting performance. + * + * If the mode is high-dpi, then a the high-dpi image is loaded, but also + * low-dpi image is loaded for preloading (if available). + * For the low-dpi mode, only low-dpi image is loaded. If not available, then + * the high-dpi image is loaded as a fallback. + * + * @param {Mosaic.Tile.LoadMode} loadMode Loading mode. + * @param {function(boolean)} onImageLoaded Callback when image is loaded. + * The argument is true for success, false for failure. + */ +Mosaic.Tile.prototype.load = function(loadMode, onImageLoaded) { + // Attaches the image to the tile and finalizes loading process for the + // specified loader. + var finalizeLoader = function(mode, success, loader) { + if (success && this.wrapper_) { + // Show the fade-in animation only when previously there was no image + // attached in this tile. + if (!this.imageLoaded_ && !this.imagePreloaded_) + this.wrapper_.classList.add('animated'); + else + this.wrapper_.classList.remove('animated'); + } + loader.attachImage(this.wrapper_, ThumbnailLoader.FillMode.OVER_FILL); + onImageLoaded(success); + switch (mode) { + case Mosaic.Tile.LoadMode.LOW_DPI: + this.imagePreloading_ = false; + this.imagePreloaded_ = true; + break; + case Mosaic.Tile.LoadMode.HIGH_DPI: + this.imageLoading_ = false; + this.imageLoaded_ = true; + break; + } + }.bind(this); + + // Always load the low-dpi image first if it is available for the fastest + // feedback. + if (!this.imagePreloading_ && this.thumbnailPreloader_) { + this.imagePreloading_ = true; + this.thumbnailPreloader_.loadDetachedImage(function(success) { + // Hi-dpi loaded first, ignore this call then. + if (this.imageLoaded_) + return; + finalizeLoader(Mosaic.Tile.LoadMode.LOW_DPI, + success, + this.thumbnailPreloader_); + }.bind(this)); + } + + // Load the high-dpi image only when it is requested, or the low-dpi is not + // available. + if (!this.imageLoading_ && + (loadMode == Mosaic.Tile.LoadMode.HIGH_DPI || !this.imagePreloading_)) { + this.imageLoading_ = true; + this.thumbnailLoader_.loadDetachedImage(function(success) { + // Cancel preloading, since the hi-dpi image is ready. + if (this.thumbnailPreloader_) + this.thumbnailPreloader_.cancel(); + finalizeLoader(Mosaic.Tile.LoadMode.HIGH_DPI, + success, + this.thumbnailLoader_); + }.bind(this)); + } +}; + +/** + * Unloads an image from the tile. + */ +Mosaic.Tile.prototype.unload = function() { + this.thumbnailLoader_.cancel(); + if (this.thumbnailPreloader_) + this.thumbnailPreloader_.cancel(); + this.imagePreloaded_ = false; + this.imageLoaded_ = false; + this.imagePreloading_ = false; + this.imageLoading_ = false; + this.wrapper_.innerText = ''; +}; + +/** + * Select/unselect the tile. + * + * @param {boolean} on True if selected. + */ +Mosaic.Tile.prototype.select = function(on) { + if (on) + this.setAttribute('selected', true); + else + this.removeAttribute('selected'); +}; + +/** + * Position the tile in the mosaic. + * + * @param {number} left Left position. + * @param {number} top Top position. + * @param {number} width Width. + * @param {number} height Height. + */ +Mosaic.Tile.prototype.layout = function(left, top, width, height) { + this.left_ = left; + this.top_ = top; + this.width_ = width; + this.height_ = height; + + this.style.left = left + 'px'; + this.style.top = top + 'px'; + this.style.width = width + 'px'; + this.style.height = height + 'px'; + + if (!this.wrapper_) { // First time, create DOM. + this.container_.appendChild(this); + var border = util.createChild(this, 'img-border'); + this.wrapper_ = util.createChild(border, 'img-wrapper'); + } + if (this.hasAttribute('selected')) + this.scrollIntoView(false); + + if (this.imageLoaded_) { + this.thumbnailLoader_.attachImage(this.wrapper_, + ThumbnailLoader.FillMode.FILL); + } +}; + +/** + * If the tile is not fully visible scroll the parent to make it fully visible. + * @param {boolean=} opt_animated True, if scroll should be animated, + * default: true. + */ +Mosaic.Tile.prototype.scrollIntoView = function(opt_animated) { + if (this.left_ == null) // Not laid out. + return; + + var targetPosition; + var tileLeft = this.left_ - Mosaic.Layout.SCROLL_MARGIN; + if (tileLeft < this.container_.scrollLeft) { + targetPosition = tileLeft; + } else { + var tileRight = this.left_ + this.width_ + Mosaic.Layout.SCROLL_MARGIN; + var scrollRight = this.container_.scrollLeft + this.container_.clientWidth; + if (tileRight > scrollRight) + targetPosition = tileRight - this.container_.clientWidth; + } + + if (targetPosition) { + if (opt_animated === false) + this.container_.scrollLeft = targetPosition; + else + this.container_.animatedScrollTo(targetPosition); + } +}; + +/** + * @return {Rect} Rectangle occupied by the tile's image, + * relative to the viewport. + */ +Mosaic.Tile.prototype.getImageRect = function() { + if (this.left_ == null) // Not laid out. + return null; + + var margin = Mosaic.Layout.SPACING / 2; + return new Rect(this.left_ - this.container_.scrollLeft, this.top_, + this.width_, this.height_).inflate(-margin, -margin); +}; + +/** + * @return {number} X coordinate of the tile center. + */ +Mosaic.Tile.prototype.getCenterX = function() { + return this.left_ + Math.round(this.width_ / 2); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/photo/ribbon.js b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/ribbon.js new file mode 100644 index 00000000000..4d1c81c6c3c --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/ribbon.js @@ -0,0 +1,366 @@ +// 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'; + +/** + * Scrollable thumbnail ribbon at the bottom of the Gallery in the Slide mode. + * + * @param {Document} document Document. + * @param {MetadataCache} metadataCache MetadataCache instance. + * @param {cr.ui.ArrayDataModel} dataModel Data model. + * @param {cr.ui.ListSelectionModel} selectionModel Selection model. + * @return {Element} Ribbon element. + * @constructor + */ +function Ribbon(document, metadataCache, dataModel, selectionModel) { + var self = document.createElement('div'); + Ribbon.decorate(self, metadataCache, dataModel, selectionModel); + return self; +} + +/** + * Inherit from HTMLDivElement. + */ +Ribbon.prototype.__proto__ = HTMLDivElement.prototype; + +/** + * Decorate a Ribbon instance. + * + * @param {Ribbon} self Self pointer. + * @param {MetadataCache} metadataCache MetadataCache instance. + * @param {cr.ui.ArrayDataModel} dataModel Data model. + * @param {cr.ui.ListSelectionModel} selectionModel Selection model. + */ +Ribbon.decorate = function(self, metadataCache, dataModel, selectionModel) { + self.__proto__ = Ribbon.prototype; + self.metadataCache_ = metadataCache; + self.dataModel_ = dataModel; + self.selectionModel_ = selectionModel; + + self.className = 'ribbon'; +}; + +/** + * Max number of thumbnails in the ribbon. + * @type {number} + */ +Ribbon.ITEMS_COUNT = 5; + +/** + * Force redraw the ribbon. + */ +Ribbon.prototype.redraw = function() { + this.onSelection_(); +}; + +/** + * Clear all cached data to force full redraw on the next selection change. + */ +Ribbon.prototype.reset = function() { + this.renderCache_ = {}; + this.firstVisibleIndex_ = 0; + this.lastVisibleIndex_ = -1; // Zero thumbnails +}; + +/** + * Enable the ribbon. + */ +Ribbon.prototype.enable = function() { + this.onContentBound_ = this.onContentChange_.bind(this); + this.dataModel_.addEventListener('content', this.onContentBound_); + + this.onSpliceBound_ = this.onSplice_.bind(this); + this.dataModel_.addEventListener('splice', this.onSpliceBound_); + + this.onSelectionBound_ = this.onSelection_.bind(this); + this.selectionModel_.addEventListener('change', this.onSelectionBound_); + + this.reset(); + this.redraw(); +}; + +/** + * Disable ribbon. + */ +Ribbon.prototype.disable = function() { + this.dataModel_.removeEventListener('content', this.onContentBound_); + this.dataModel_.removeEventListener('splice', this.onSpliceBound_); + this.selectionModel_.removeEventListener('change', this.onSelectionBound_); + + this.removeVanishing_(); + this.textContent = ''; +}; + +/** + * Data model splice handler. + * @param {Event} event Event. + * @private + */ +Ribbon.prototype.onSplice_ = function(event) { + if (event.removed.length == 0) + return; + + if (event.removed.length > 1) { + console.error('Cannot remove multiple items'); + return; + } + + var removed = this.renderCache_[event.removed[0].getEntry().toURL()]; + if (!removed || !removed.parentNode || !removed.hasAttribute('selected')) { + console.error('Can only remove the selected item'); + return; + } + + var persistentNodes = this.querySelectorAll('.ribbon-image:not([vanishing])'); + if (this.lastVisibleIndex_ < this.dataModel_.length) { // Not at the end. + var lastNode = persistentNodes[persistentNodes.length - 1]; + if (lastNode.nextSibling) { + // Pull back a vanishing node from the right. + lastNode.nextSibling.removeAttribute('vanishing'); + } else { + // Push a new item at the right end. + this.appendChild(this.renderThumbnail_(this.lastVisibleIndex_)); + } + } else { + // No items to the right, move the window to the left. + this.lastVisibleIndex_--; + if (this.firstVisibleIndex_) { + this.firstVisibleIndex_--; + var firstNode = persistentNodes[0]; + if (firstNode.previousSibling) { + // Pull back a vanishing node from the left. + firstNode.previousSibling.removeAttribute('vanishing'); + } else { + // Push a new item at the left end. + var newThumbnail = this.renderThumbnail_(this.firstVisibleIndex_); + newThumbnail.style.marginLeft = -(this.clientHeight - 2) + 'px'; + this.insertBefore(newThumbnail, this.firstChild); + setTimeout(function() { + newThumbnail.style.marginLeft = '0'; + }, 0); + } + } + } + + removed.removeAttribute('selected'); + removed.setAttribute('vanishing', 'smooth'); + this.scheduleRemove_(); +}; + +/** + * Selection change handler. + * @private + */ +Ribbon.prototype.onSelection_ = function() { + var indexes = this.selectionModel_.selectedIndexes; + if (indexes.length == 0) + return; // Ignore temporary empty selection. + var selectedIndex = indexes[0]; + + var length = this.dataModel_.length; + + // TODO(dgozman): use margin instead of 2 here. + var itemWidth = this.clientHeight - 2; + var fullItems = Ribbon.ITEMS_COUNT; + fullItems = Math.min(fullItems, length); + var right = Math.floor((fullItems - 1) / 2); + + var fullWidth = fullItems * itemWidth; + this.style.width = fullWidth + 'px'; + + var lastIndex = selectedIndex + right; + lastIndex = Math.max(lastIndex, fullItems - 1); + lastIndex = Math.min(lastIndex, length - 1); + var firstIndex = lastIndex - fullItems + 1; + + if (this.firstVisibleIndex_ != firstIndex || + this.lastVisibleIndex_ != lastIndex) { + + if (this.lastVisibleIndex_ == -1) { + this.firstVisibleIndex_ = firstIndex; + this.lastVisibleIndex_ = lastIndex; + } + + this.removeVanishing_(); + + this.textContent = ''; + var startIndex = Math.min(firstIndex, this.firstVisibleIndex_); + // All the items except the first one treated equally. + for (var index = startIndex + 1; + index <= Math.max(lastIndex, this.lastVisibleIndex_); + ++index) { + // Only add items that are in either old or the new viewport. + if (this.lastVisibleIndex_ < index && index < firstIndex || + lastIndex < index && index < this.firstVisibleIndex_) + continue; + var box = this.renderThumbnail_(index); + box.style.marginLeft = '0'; + this.appendChild(box); + if (index < firstIndex || index > lastIndex) { + // If the node is not in the new viewport we only need it while + // the animation is playing out. + box.setAttribute('vanishing', 'slide'); + } + } + + var slideCount = this.childNodes.length + 1 - Ribbon.ITEMS_COUNT; + var margin = itemWidth * slideCount; + var startBox = this.renderThumbnail_(startIndex); + if (startIndex == firstIndex) { + // Sliding to the right. + startBox.style.marginLeft = -margin + 'px'; + if (this.firstChild) + this.insertBefore(startBox, this.firstChild); + else + this.appendChild(startBox); + setTimeout(function() { + startBox.style.marginLeft = '0'; + }, 0); + } else { + // Sliding to the left. Start item will become invisible and should be + // removed afterwards. + startBox.setAttribute('vanishing', 'slide'); + startBox.style.marginLeft = '0'; + if (this.firstChild) + this.insertBefore(startBox, this.firstChild); + else + this.appendChild(startBox); + setTimeout(function() { + startBox.style.marginLeft = -margin + 'px'; + }, 0); + } + + ImageUtil.setClass(this, 'fade-left', + firstIndex > 0 && selectedIndex != firstIndex); + + ImageUtil.setClass(this, 'fade-right', + lastIndex < length - 1 && selectedIndex != lastIndex); + + this.firstVisibleIndex_ = firstIndex; + this.lastVisibleIndex_ = lastIndex; + + this.scheduleRemove_(); + } + + var oldSelected = this.querySelector('[selected]'); + if (oldSelected) oldSelected.removeAttribute('selected'); + + var newSelected = + this.renderCache_[this.dataModel_.item(selectedIndex).getEntry().toURL()]; + if (newSelected) newSelected.setAttribute('selected', true); +}; + +/** + * Schedule the removal of thumbnails marked as vanishing. + * @private + */ +Ribbon.prototype.scheduleRemove_ = function() { + if (this.removeTimeout_) + clearTimeout(this.removeTimeout_); + + this.removeTimeout_ = setTimeout(function() { + this.removeTimeout_ = null; + this.removeVanishing_(); + }.bind(this), 200); +}; + +/** + * Remove all thumbnails marked as vanishing. + * @private + */ +Ribbon.prototype.removeVanishing_ = function() { + if (this.removeTimeout_) { + clearTimeout(this.removeTimeout_); + this.removeTimeout_ = 0; + } + var vanishingNodes = this.querySelectorAll('[vanishing]'); + for (var i = 0; i != vanishingNodes.length; i++) { + vanishingNodes[i].removeAttribute('vanishing'); + this.removeChild(vanishingNodes[i]); + } +}; + +/** + * Create a DOM element for a thumbnail. + * + * @param {number} index Item index. + * @return {Element} Newly created element. + * @private + */ +Ribbon.prototype.renderThumbnail_ = function(index) { + var item = this.dataModel_.item(index); + var url = item.getEntry().toURL(); + + var cached = this.renderCache_[url]; + if (cached) { + var img = cached.querySelector('img'); + if (img) + img.classList.add('cached'); + return cached; + } + + var thumbnail = this.ownerDocument.createElement('div'); + thumbnail.className = 'ribbon-image'; + thumbnail.addEventListener('click', function() { + var index = this.dataModel_.indexOf(item); + this.selectionModel_.unselectAll(); + this.selectionModel_.setIndexSelected(index, true); + }.bind(this)); + + util.createChild(thumbnail, 'image-wrapper'); + + this.metadataCache_.get(item.getEntry(), Gallery.METADATA_TYPE, + this.setThumbnailImage_.bind(this, thumbnail, url)); + + // TODO: Implement LRU eviction. + // Never evict the thumbnails that are currently in the DOM because we rely + // on this cache to find them by URL. + this.renderCache_[url] = thumbnail; + return thumbnail; +}; + +/** + * Set the thumbnail image. + * + * @param {Element} thumbnail Thumbnail element. + * @param {string} url Image url. + * @param {Object} metadata Metadata. + * @private + */ +Ribbon.prototype.setThumbnailImage_ = function(thumbnail, url, metadata) { + new ThumbnailLoader(url, ThumbnailLoader.LoaderType.IMAGE, metadata).load( + thumbnail.querySelector('.image-wrapper'), + ThumbnailLoader.FillMode.FILL /* fill */, + ThumbnailLoader.OptimizationMode.NEVER_DISCARD); +}; + +/** + * Content change handler. + * + * @param {Event} event Event. + * @private + */ +Ribbon.prototype.onContentChange_ = function(event) { + var url = event.item.getEntry().toURL(); + this.remapCache_(event.oldUrl, url); + + var thumbnail = this.renderCache_[url]; + if (thumbnail && event.metadata) + this.setThumbnailImage_(thumbnail, url, event.metadata); +}; + +/** + * Update the thumbnail element cache. + * + * @param {string} oldUrl Old url. + * @param {string} newUrl New url. + * @private + */ +Ribbon.prototype.remapCache_ = function(oldUrl, newUrl) { + if (oldUrl != newUrl && (oldUrl in this.renderCache_)) { + this.renderCache_[newUrl] = this.renderCache_[oldUrl]; + delete this.renderCache_[oldUrl]; + } +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/photo/slide_mode.js b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/slide_mode.js new file mode 100644 index 00000000000..d9b47395bb9 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/slide_mode.js @@ -0,0 +1,1354 @@ +// 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'; + +/** + * Slide mode displays a single image and has a set of controls to navigate + * between the images and to edit an image. + * + * TODO(kaznacheev): Introduce a parameter object. + * + * @param {Element} container Main container element. + * @param {Element} content Content container element. + * @param {Element} toolbar Toolbar element. + * @param {ImageEditor.Prompt} prompt Prompt. + * @param {cr.ui.ArrayDataModel} dataModel Data model. + * @param {cr.ui.ListSelectionModel} selectionModel Selection model. + * @param {Object} context Context. + * @param {function(function())} toggleMode Function to toggle the Gallery mode. + * @param {function(string):string} displayStringFunction String formatting + * function. + * @constructor + */ +function SlideMode(container, content, toolbar, prompt, + dataModel, selectionModel, context, + toggleMode, displayStringFunction) { + this.container_ = container; + this.document_ = container.ownerDocument; + this.content = content; + this.toolbar_ = toolbar; + this.prompt_ = prompt; + this.dataModel_ = dataModel; + this.selectionModel_ = selectionModel; + this.context_ = context; + this.metadataCache_ = context.metadataCache; + this.toggleMode_ = toggleMode; + this.displayStringFunction_ = displayStringFunction; + + this.onSelectionBound_ = this.onSelection_.bind(this); + this.onSpliceBound_ = this.onSplice_.bind(this); + this.onContentBound_ = this.onContentChange_.bind(this); + + // Unique numeric key, incremented per each load attempt used to discard + // old attempts. This can happen especially when changing selection fast or + // Internet connection is slow. + this.currentUniqueKey_ = 0; + + this.initListeners_(); + this.initDom_(); +} + +/** + * SlideMode extends cr.EventTarget. + */ +SlideMode.prototype.__proto__ = cr.EventTarget.prototype; + +/** + * List of available editor modes. + * @type {Array.<ImageEditor.Mode>} + */ +SlideMode.editorModes = [ + new ImageEditor.Mode.InstantAutofix(), + new ImageEditor.Mode.Crop(), + new ImageEditor.Mode.Exposure(), + new ImageEditor.Mode.OneClick( + 'rotate_left', 'GALLERY_ROTATE_LEFT', new Command.Rotate(-1)), + new ImageEditor.Mode.OneClick( + 'rotate_right', 'GALLERY_ROTATE_RIGHT', new Command.Rotate(1)) +]; + +/** + * @return {string} Mode name. + */ +SlideMode.prototype.getName = function() { return 'slide' }; + +/** + * @return {string} Mode title. + */ +SlideMode.prototype.getTitle = function() { return 'GALLERY_SLIDE' }; + +/** + * Initialize the listeners. + * @private + */ +SlideMode.prototype.initListeners_ = function() { + window.addEventListener('resize', this.onResize_.bind(this), false); +}; + +/** + * Initialize the UI. + * @private + */ +SlideMode.prototype.initDom_ = function() { + // Container for displayed image or video. + this.imageContainer_ = util.createChild( + this.document_.querySelector('.content'), 'image-container'); + this.imageContainer_.addEventListener('click', this.onClick_.bind(this)); + + this.document_.addEventListener('click', this.onDocumentClick_.bind(this)); + + // Overwrite options and info bubble. + this.options_ = util.createChild( + this.toolbar_.querySelector('.filename-spacer'), 'options'); + + this.savedLabel_ = util.createChild(this.options_, 'saved'); + this.savedLabel_.textContent = this.displayStringFunction_('GALLERY_SAVED'); + + var overwriteOriginalBox = + util.createChild(this.options_, 'overwrite-original'); + + this.overwriteOriginal_ = util.createChild( + overwriteOriginalBox, 'common white', 'input'); + this.overwriteOriginal_.type = 'checkbox'; + this.overwriteOriginal_.id = 'overwrite-checkbox'; + util.platform.getPreference(SlideMode.OVERWRITE_KEY, function(value) { + // Out-of-the box default is 'true' + this.overwriteOriginal_.checked = + (typeof value !== 'string' || value === 'true'); + }.bind(this)); + this.overwriteOriginal_.addEventListener('click', + this.onOverwriteOriginalClick_.bind(this)); + + var overwriteLabel = util.createChild(overwriteOriginalBox, '', 'label'); + overwriteLabel.textContent = + this.displayStringFunction_('GALLERY_OVERWRITE_ORIGINAL'); + overwriteLabel.setAttribute('for', 'overwrite-checkbox'); + + this.bubble_ = util.createChild(this.toolbar_, 'bubble'); + this.bubble_.hidden = true; + + var bubbleContent = util.createChild(this.bubble_); + bubbleContent.innerHTML = this.displayStringFunction_( + 'GALLERY_OVERWRITE_BUBBLE'); + + util.createChild(this.bubble_, 'pointer bottom', 'span'); + + var bubbleClose = util.createChild(this.bubble_, 'close-x'); + bubbleClose.addEventListener('click', this.onCloseBubble_.bind(this)); + + // Video player controls. + this.mediaSpacer_ = + util.createChild(this.container_, 'video-controls-spacer'); + this.mediaToolbar_ = util.createChild(this.mediaSpacer_, 'tool'); + this.mediaControls_ = new VideoControls( + this.mediaToolbar_, + this.showErrorBanner_.bind(this, 'GALLERY_VIDEO_ERROR'), + this.displayStringFunction_.bind(this), + this.toggleFullScreen_.bind(this), + this.container_); + + // Ribbon and related controls. + this.arrowBox_ = util.createChild(this.container_, 'arrow-box'); + + this.arrowLeft_ = + util.createChild(this.arrowBox_, 'arrow left tool dimmable'); + this.arrowLeft_.addEventListener('click', + this.advanceManually.bind(this, -1)); + util.createChild(this.arrowLeft_); + + util.createChild(this.arrowBox_, 'arrow-spacer'); + + this.arrowRight_ = + util.createChild(this.arrowBox_, 'arrow right tool dimmable'); + this.arrowRight_.addEventListener('click', + this.advanceManually.bind(this, 1)); + util.createChild(this.arrowRight_); + + this.ribbonSpacer_ = util.createChild(this.toolbar_, 'ribbon-spacer'); + this.ribbon_ = new Ribbon(this.document_, + this.metadataCache_, this.dataModel_, this.selectionModel_); + this.ribbonSpacer_.appendChild(this.ribbon_); + + // Error indicator. + var errorWrapper = util.createChild(this.container_, 'prompt-wrapper'); + errorWrapper.setAttribute('pos', 'center'); + + this.errorBanner_ = util.createChild(errorWrapper, 'error-banner'); + + util.createChild(this.container_, 'spinner'); + + var slideShowButton = util.createChild(this.toolbar_, + 'button slideshow', 'button'); + slideShowButton.title = this.displayStringFunction_('GALLERY_SLIDESHOW'); + slideShowButton.addEventListener('click', + this.startSlideshow.bind(this, SlideMode.SLIDESHOW_INTERVAL_FIRST)); + + var slideShowToolbar = + util.createChild(this.container_, 'tool slideshow-toolbar'); + util.createChild(slideShowToolbar, 'slideshow-play'). + addEventListener('click', this.toggleSlideshowPause_.bind(this)); + util.createChild(slideShowToolbar, 'slideshow-end'). + addEventListener('click', this.stopSlideshow_.bind(this)); + + // Editor. + + this.editButton_ = util.createChild(this.toolbar_, 'button edit', 'button'); + this.editButton_.title = this.displayStringFunction_('GALLERY_EDIT'); + this.editButton_.setAttribute('disabled', ''); // Disabled by default. + this.editButton_.addEventListener('click', this.toggleEditor.bind(this)); + + this.printButton_ = util.createChild(this.toolbar_, 'button print', 'button'); + this.printButton_.title = this.displayStringFunction_('GALLERY_PRINT'); + this.printButton_.setAttribute('disabled', ''); // Disabled by default. + this.printButton_.addEventListener('click', this.print_.bind(this)); + + this.editBarSpacer_ = util.createChild(this.toolbar_, 'edit-bar-spacer'); + this.editBarMain_ = util.createChild(this.editBarSpacer_, 'edit-main'); + + this.editBarMode_ = util.createChild(this.container_, 'edit-modal'); + this.editBarModeWrapper_ = util.createChild( + this.editBarMode_, 'edit-modal-wrapper'); + this.editBarModeWrapper_.hidden = true; + + // Objects supporting image display and editing. + this.viewport_ = new Viewport(); + + this.imageView_ = new ImageView( + this.imageContainer_, + this.viewport_, + this.metadataCache_); + + this.editor_ = new ImageEditor( + this.viewport_, + this.imageView_, + this.prompt_, + { + root: this.container_, + image: this.imageContainer_, + toolbar: this.editBarMain_, + mode: this.editBarModeWrapper_ + }, + SlideMode.editorModes, + this.displayStringFunction_, + this.onToolsVisibilityChanged_.bind(this)); + + this.editor_.getBuffer().addOverlay( + new SwipeOverlay(this.advanceManually.bind(this))); +}; + +/** + * Load items, display the selected item. + * @param {Rect} zoomFromRect Rectangle for zoom effect. + * @param {function} displayCallback Called when the image is displayed. + * @param {function} loadCallback Called when the image is displayed. + */ +SlideMode.prototype.enter = function( + zoomFromRect, displayCallback, loadCallback) { + this.sequenceDirection_ = 0; + this.sequenceLength_ = 0; + + var loadDone = function(loadType, delay) { + this.active_ = true; + + this.selectionModel_.addEventListener('change', this.onSelectionBound_); + this.dataModel_.addEventListener('splice', this.onSpliceBound_); + this.dataModel_.addEventListener('content', this.onContentBound_); + + ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1); + this.ribbon_.enable(); + + // Wait 1000ms after the animation is done, then prefetch the next image. + this.requestPrefetch(1, delay + 1000); + + if (loadCallback) loadCallback(); + }.bind(this); + + // The latest |leave| call might have left the image animating. Remove it. + this.unloadImage_(); + + if (this.getItemCount_() === 0) { + this.displayedIndex_ = -1; + //TODO(kaznacheev) Show this message in the grid mode too. + this.showErrorBanner_('GALLERY_NO_IMAGES'); + loadDone(); + } else { + // Remember the selection if it is empty or multiple. It will be restored + // in |leave| if the user did not changing the selection manually. + var currentSelection = this.selectionModel_.selectedIndexes; + if (currentSelection.length === 1) + this.savedSelection_ = null; + else + this.savedSelection_ = currentSelection; + + // Ensure valid single selection. + // Note that the SlideMode object is not listening to selection change yet. + this.select(Math.max(0, this.getSelectedIndex())); + this.displayedIndex_ = this.getSelectedIndex(); + + var selectedItem = this.getSelectedItem(); + // Show the selected item ASAP, then complete the initialization + // (loading the ribbon thumbnails can take some time). + this.metadataCache_.get(selectedItem.getEntry(), Gallery.METADATA_TYPE, + function(metadata) { + this.loadItem_(selectedItem.getEntry(), metadata, + zoomFromRect && this.imageView_.createZoomEffect(zoomFromRect), + displayCallback, loadDone); + }.bind(this)); + + } +}; + +/** + * Leave the mode. + * @param {Rect} zoomToRect Rectangle for zoom effect. + * @param {function} callback Called when the image is committed and + * the zoom-out animation has started. + */ +SlideMode.prototype.leave = function(zoomToRect, callback) { + var commitDone = function() { + this.stopEditing_(); + this.stopSlideshow_(); + ImageUtil.setAttribute(this.arrowBox_, 'active', false); + this.selectionModel_.removeEventListener( + 'change', this.onSelectionBound_); + this.dataModel_.removeEventListener('splice', this.onSpliceBound_); + this.dataModel_.removeEventListener('content', this.onContentBound_); + this.ribbon_.disable(); + this.active_ = false; + if (this.savedSelection_) + this.selectionModel_.selectedIndexes = this.savedSelection_; + this.unloadImage_(zoomToRect); + callback(); + }.bind(this); + + if (this.getItemCount_() === 0) { + this.showErrorBanner_(false); + commitDone(); + } else { + this.commitItem_(commitDone); + } + + // Disable the slide-mode only buttons when leaving. + this.editButton_.setAttribute('disabled', ''); + this.printButton_.setAttribute('disabled', ''); +}; + + +/** + * Execute an action when the editor is not busy. + * + * @param {function} action Function to execute. + */ +SlideMode.prototype.executeWhenReady = function(action) { + this.editor_.executeWhenReady(action); +}; + +/** + * @return {boolean} True if the mode has active tools (that should not fade). + */ +SlideMode.prototype.hasActiveTool = function() { + return this.isEditing(); +}; + +/** + * @return {number} Item count. + * @private + */ +SlideMode.prototype.getItemCount_ = function() { + return this.dataModel_.length; +}; + +/** + * @param {number} index Index. + * @return {Gallery.Item} Item. + */ +SlideMode.prototype.getItem = function(index) { + return this.dataModel_.item(index); +}; + +/** + * @return {Gallery.Item} Selected index. + */ +SlideMode.prototype.getSelectedIndex = function() { + return this.selectionModel_.selectedIndex; +}; + +/** + * @return {Rect} Screen rectangle of the selected image. + */ +SlideMode.prototype.getSelectedImageRect = function() { + if (this.getSelectedIndex() < 0) + return null; + else + return this.viewport_.getScreenClipped(); +}; + +/** + * @return {Gallery.Item} Selected item. + */ +SlideMode.prototype.getSelectedItem = function() { + return this.getItem(this.getSelectedIndex()); +}; + +/** + * Toggles the full screen mode. + * @private + */ +SlideMode.prototype.toggleFullScreen_ = function() { + util.toggleFullScreen(this.context_.appWindow, + !util.isFullScreen(this.context_.appWindow)); +}; + +/** + * Selection change handler. + * + * Commits the current image and displays the newly selected image. + * @private + */ +SlideMode.prototype.onSelection_ = function() { + if (this.selectionModel_.selectedIndexes.length === 0) + return; // Temporary empty selection. + + // Forget the saved selection if the user changed the selection manually. + if (!this.isSlideshowOn_()) + this.savedSelection_ = null; + + if (this.getSelectedIndex() === this.displayedIndex_) + return; // Do not reselect. + + this.commitItem_(this.loadSelectedItem_.bind(this)); +}; + +/** + * Handles changes in tools visibility, and if the header is dimmed, then + * requests disabling the draggable app region. + * + * @private + */ +SlideMode.prototype.onToolsVisibilityChanged_ = function() { + var headerDimmed = + this.document_.querySelector('.header').hasAttribute('dimmed'); + this.context_.onAppRegionChanged(!headerDimmed); +}; + +/** + * Change the selection. + * + * @param {number} index New selected index. + * @param {number=} opt_slideHint Slide animation direction (-1|1). + */ +SlideMode.prototype.select = function(index, opt_slideHint) { + this.slideHint_ = opt_slideHint; + this.selectionModel_.selectedIndex = index; + this.selectionModel_.leadIndex = index; +}; + +/** + * Load the selected item. + * + * @private + */ +SlideMode.prototype.loadSelectedItem_ = function() { + var slideHint = this.slideHint_; + this.slideHint_ = undefined; + + var index = this.getSelectedIndex(); + if (index === this.displayedIndex_) + return; // Do not reselect. + + var step = slideHint || (index - this.displayedIndex_); + + if (Math.abs(step) != 1) { + // Long leap, the sequence is broken, we have no good prefetch candidate. + this.sequenceDirection_ = 0; + this.sequenceLength_ = 0; + } else if (this.sequenceDirection_ === step) { + // Keeping going in sequence. + this.sequenceLength_++; + } else { + // Reversed the direction. Reset the counter. + this.sequenceDirection_ = step; + this.sequenceLength_ = 1; + } + + if (this.sequenceLength_ <= 1) { + // We have just broke the sequence. Touch the current image so that it stays + // in the cache longer. + this.imageView_.prefetch(this.imageView_.contentEntry_); + } + + this.displayedIndex_ = index; + + function shouldPrefetch(loadType, step, sequenceLength) { + // Never prefetch when selecting out of sequence. + if (Math.abs(step) != 1) + return false; + + // Never prefetch after a video load (decoding the next image can freeze + // the UI for a second or two). + if (loadType === ImageView.LOAD_TYPE_VIDEO_FILE) + return false; + + // Always prefetch if the previous load was from cache. + if (loadType === ImageView.LOAD_TYPE_CACHED_FULL) + return true; + + // Prefetch if we have been going in the same direction for long enough. + return sequenceLength >= 3; + } + + var selectedItem = this.getSelectedItem(); + this.currentUniqueKey_++; + var selectedUniqueKey = this.currentUniqueKey_; + var onMetadata = function(metadata) { + // Discard, since another load has been invoked after this one. + if (selectedUniqueKey != this.currentUniqueKey_) return; + this.loadItem_(selectedItem.getEntry(), metadata, + new ImageView.Effect.Slide(step, this.isSlideshowPlaying_()), + function() {} /* no displayCallback */, + function(loadType, delay) { + // Discard, since another load has been invoked after this one. + if (selectedUniqueKey != this.currentUniqueKey_) return; + if (shouldPrefetch(loadType, step, this.sequenceLength_)) { + this.requestPrefetch(step, delay); + } + if (this.isSlideshowPlaying_()) + this.scheduleNextSlide_(); + }.bind(this)); + }.bind(this); + this.metadataCache_.get( + selectedItem.getEntry(), Gallery.METADATA_TYPE, onMetadata); +}; + +/** + * Unload the current image. + * + * @param {Rect} zoomToRect Rectangle for zoom effect. + * @private + */ +SlideMode.prototype.unloadImage_ = function(zoomToRect) { + this.imageView_.unload(zoomToRect); + this.container_.removeAttribute('video'); +}; + +/** + * Data model 'splice' event handler. + * @param {Event} event Event. + * @private + */ +SlideMode.prototype.onSplice_ = function(event) { + ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1); + + // Splice invalidates saved indices, drop the saved selection. + this.savedSelection_ = null; + + if (event.removed.length != 1) + return; + + // Delay the selection to let the ribbon splice handler work first. + setTimeout(function() { + if (event.index < this.dataModel_.length) { + // There is the next item, select it. + // The next item is now at the same index as the removed one, so we need + // to correct displayIndex_ so that loadSelectedItem_ does not think + // we are re-selecting the same item (and does right-to-left slide-in + // animation). + this.displayedIndex_ = event.index - 1; + this.select(event.index); + } else if (this.dataModel_.length) { + // Removed item is the rightmost, but there are more items. + this.select(event.index - 1); // Select the new last index. + } else { + // No items left. Unload the image and show the banner. + this.commitItem_(function() { + this.unloadImage_(); + this.showErrorBanner_('GALLERY_NO_IMAGES'); + }.bind(this)); + } + }.bind(this), 0); +}; + +/** + * @param {number} direction -1 for left, 1 for right. + * @return {number} Next index in the given direction, with wrapping. + * @private + */ +SlideMode.prototype.getNextSelectedIndex_ = function(direction) { + function advance(index, limit) { + index += (direction > 0 ? 1 : -1); + if (index < 0) + return limit - 1; + if (index === limit) + return 0; + return index; + } + + // If the saved selection is multiple the Slideshow should cycle through + // the saved selection. + if (this.isSlideshowOn_() && + this.savedSelection_ && this.savedSelection_.length > 1) { + var pos = advance(this.savedSelection_.indexOf(this.getSelectedIndex()), + this.savedSelection_.length); + return this.savedSelection_[pos]; + } else { + return advance(this.getSelectedIndex(), this.getItemCount_()); + } +}; + +/** + * Advance the selection based on the pressed key ID. + * @param {string} keyID Key identifier. + */ +SlideMode.prototype.advanceWithKeyboard = function(keyID) { + this.advanceManually(keyID === 'Up' || keyID === 'Left' ? -1 : 1); +}; + +/** + * Advance the selection as a result of a user action (as opposed to an + * automatic change in the slideshow mode). + * @param {number} direction -1 for left, 1 for right. + */ +SlideMode.prototype.advanceManually = function(direction) { + if (this.isSlideshowPlaying_()) { + this.pauseSlideshow_(); + cr.dispatchSimpleEvent(this, 'useraction'); + } + this.selectNext(direction); +}; + +/** + * Select the next item. + * @param {number} direction -1 for left, 1 for right. + */ +SlideMode.prototype.selectNext = function(direction) { + this.select(this.getNextSelectedIndex_(direction), direction); +}; + +/** + * Select the first item. + */ +SlideMode.prototype.selectFirst = function() { + this.select(0); +}; + +/** + * Select the last item. + */ +SlideMode.prototype.selectLast = function() { + this.select(this.getItemCount_() - 1); +}; + +// Loading/unloading + +/** + * Load and display an item. + * + * @param {FileEntry} entry Item entry to be loaded. + * @param {Object} metadata Item metadata. + * @param {Object} effect Transition effect object. + * @param {function} displayCallback Called when the image is displayed + * (which can happen before the image load due to caching). + * @param {function} loadCallback Called when the image is fully loaded. + * @private + */ +SlideMode.prototype.loadItem_ = function( + entry, metadata, effect, displayCallback, loadCallback) { + this.selectedImageMetadata_ = MetadataCache.cloneMetadata(metadata); + + this.showSpinner_(true); + + var loadDone = function(loadType, delay, error) { + var video = this.isShowingVideo_(); + ImageUtil.setAttribute(this.container_, 'video', video); + + this.showSpinner_(false); + if (loadType === ImageView.LOAD_TYPE_ERROR) { + // if we have a specific error, then display it + if (error) { + this.showErrorBanner_(error); + } else { + // otherwise try to infer general error + this.showErrorBanner_( + video ? 'GALLERY_VIDEO_ERROR' : 'GALLERY_IMAGE_ERROR'); + } + } else if (loadType === ImageView.LOAD_TYPE_OFFLINE) { + this.showErrorBanner_( + video ? 'GALLERY_VIDEO_OFFLINE' : 'GALLERY_IMAGE_OFFLINE'); + } + + if (video) { + // The editor toolbar does not make sense for video, hide it. + this.stopEditing_(); + this.mediaControls_.attachMedia(this.imageView_.getVideo()); + + // TODO(kaznacheev): Add metrics for video playback. + } else { + ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('View')); + + var toMillions = function(number) { + return Math.round(number / (1000 * 1000)); + }; + + ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MB'), + toMillions(metadata.filesystem.size)); + + var canvas = this.imageView_.getCanvas(); + ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MPix'), + toMillions(canvas.width * canvas.height)); + + var extIndex = entry.name.lastIndexOf('.'); + var ext = extIndex < 0 ? '' : + entry.name.substr(extIndex + 1).toLowerCase(); + if (ext === 'jpeg') ext = 'jpg'; + ImageUtil.metrics.recordEnum( + ImageUtil.getMetricName('FileType'), ext, ImageUtil.FILE_TYPES); + } + + // Enable or disable buttons for editing and printing. + if (video || error) { + this.editButton_.setAttribute('disabled', ''); + this.printButton_.setAttribute('disabled', ''); + } else { + this.editButton_.removeAttribute('disabled'); + this.printButton_.removeAttribute('disabled'); + } + + // For once edited image, disallow the 'overwrite' setting change. + ImageUtil.setAttribute(this.options_, 'saved', + !this.getSelectedItem().isOriginal()); + + util.platform.getPreference(SlideMode.OVERWRITE_BUBBLE_KEY, + function(value) { + var times = typeof value === 'string' ? parseInt(value, 10) : 0; + if (times < SlideMode.OVERWRITE_BUBBLE_MAX_TIMES) { + this.bubble_.hidden = false; + if (this.isEditing()) { + util.platform.setPreference( + SlideMode.OVERWRITE_BUBBLE_KEY, times + 1); + } + } + }.bind(this)); + + loadCallback(loadType, delay); + }.bind(this); + + var displayDone = function() { + cr.dispatchSimpleEvent(this, 'image-displayed'); + displayCallback(); + }.bind(this); + + this.editor_.openSession(entry, metadata, effect, + this.saveCurrentImage_.bind(this), displayDone, loadDone); +}; + +/** + * Commit changes to the current item and reset all messages/indicators. + * + * @param {function} callback Callback. + * @private + */ +SlideMode.prototype.commitItem_ = function(callback) { + this.showSpinner_(false); + this.showErrorBanner_(false); + this.editor_.getPrompt().hide(); + + // Detach any media attached to the controls. + if (this.mediaControls_.getMedia()) + this.mediaControls_.detachMedia(); + + // If showing the video, then pause it. Note, that it may not be attached + // to the media controls yet. + if (this.isShowingVideo_()) { + this.imageView_.getVideo().pause(); + // Force stop downloading, if uncached on Drive. + this.imageView_.getVideo().src = ''; + this.imageView_.getVideo().load(); + } + + this.editor_.closeSession(callback); +}; + +/** + * Request a prefetch for the next image. + * + * @param {number} direction -1 or 1. + * @param {number} delay Delay in ms. Used to prevent the CPU-heavy image + * loading from disrupting the animation that might be still in progress. + */ +SlideMode.prototype.requestPrefetch = function(direction, delay) { + if (this.getItemCount_() <= 1) return; + + var index = this.getNextSelectedIndex_(direction); + var nextItemEntry = this.getItem(index).getEntry(); + this.imageView_.prefetch(nextItemEntry, delay); +}; + +// Event handlers. + +/** + * Unload handler, to be called from the top frame. + * @param {boolean} exiting True if the app is exiting. + */ +SlideMode.prototype.onUnload = function(exiting) { + if (this.isShowingVideo_() && this.mediaControls_.isPlaying()) { + this.mediaControls_.savePosition(exiting); + } +}; + +/** + * beforeunload handler, to be called from the top frame. + * @return {string} Message to show if there are unsaved changes. + */ +SlideMode.prototype.onBeforeUnload = function() { + if (this.editor_.isBusy()) + return this.displayStringFunction_('GALLERY_UNSAVED_CHANGES'); + return null; +}; + +/** + * Click handler for the image container. + * + * @param {Event} event Mouse click event. + * @private + */ +SlideMode.prototype.onClick_ = function(event) { + if (!this.isShowingVideo_() || !this.mediaControls_.getMedia()) + return; + if (event.ctrlKey) { + this.mediaControls_.toggleLoopedModeWithFeedback(true); + if (!this.mediaControls_.isPlaying()) + this.mediaControls_.togglePlayStateWithFeedback(); + } else { + this.mediaControls_.togglePlayStateWithFeedback(); + } +}; + +/** + * Click handler for the entire document. + * @param {Event} e Mouse click event. + * @private + */ +SlideMode.prototype.onDocumentClick_ = function(e) { + // Close the bubble if clicked outside of it and if it is visible. + if (!this.bubble_.contains(e.target) && + !this.editButton_.contains(e.target) && + !this.arrowLeft_.contains(e.target) && + !this.arrowRight_.contains(e.target) && + !this.bubble_.hidden) { + this.bubble_.hidden = true; + } +}; + +/** + * Keydown handler. + * + * @param {Event} event Event. + * @return {boolean} True if handled. + */ +SlideMode.prototype.onKeyDown = function(event) { + var keyID = util.getKeyModifiers(event) + event.keyIdentifier; + + if (this.isSlideshowOn_()) { + switch (keyID) { + case 'U+001B': // Escape exits the slideshow. + this.stopSlideshow_(event); + break; + + case 'U+0020': // Space pauses/resumes the slideshow. + this.toggleSlideshowPause_(); + break; + + case 'Up': + case 'Down': + case 'Left': + case 'Right': + this.advanceWithKeyboard(keyID); + break; + } + return true; // Consume all keystrokes in the slideshow mode. + } + + if (this.isEditing() && this.editor_.onKeyDown(event)) + return true; + + switch (keyID) { + case 'U+0020': // Space toggles the video playback. + if (this.isShowingVideo_() && this.mediaControls_.getMedia()) + this.mediaControls_.togglePlayStateWithFeedback(); + break; + + case 'Ctrl-U+0050': // Ctrl+'p' prints the current image. + if (!this.printButton_.hasAttribute('disabled')) + this.print_(); + break; + + case 'U+0045': // 'e' toggles the editor. + if (!this.editButton_.hasAttribute('disabled')) + this.toggleEditor(event); + break; + + case 'U+001B': // Escape + if (!this.isEditing()) + return false; // Not handled. + this.toggleEditor(event); + break; + + case 'Home': + this.selectFirst(); + break; + case 'End': + this.selectLast(); + break; + case 'Up': + case 'Down': + case 'Left': + case 'Right': + this.advanceWithKeyboard(keyID); + break; + + default: return false; + } + + return true; +}; + +/** + * Resize handler. + * @private + */ +SlideMode.prototype.onResize_ = function() { + this.viewport_.sizeByFrameAndFit(this.container_); + this.viewport_.repaint(); +}; + +/** + * Update thumbnails. + */ +SlideMode.prototype.updateThumbnails = function() { + this.ribbon_.reset(); + if (this.active_) + this.ribbon_.redraw(); +}; + +// Saving + +/** + * Save the current image to a file. + * + * @param {function} callback Callback. + * @private + */ +SlideMode.prototype.saveCurrentImage_ = function(callback) { + var item = this.getSelectedItem(); + var oldEntry = item.getEntry(); + var canvas = this.imageView_.getCanvas(); + + this.showSpinner_(true); + var metadataEncoder = ImageEncoder.encodeMetadata( + this.selectedImageMetadata_.media, canvas, 1 /* quality */); + + this.selectedImageMetadata_ = ContentProvider.ConvertContentMetadata( + metadataEncoder.getMetadata(), this.selectedImageMetadata_); + + item.saveToFile( + this.context_.saveDirEntry, + this.shouldOverwriteOriginal_(), + canvas, + metadataEncoder, + function(success) { + // TODO(kaznacheev): Implement write error handling. + // Until then pretend that the save succeeded. + this.showSpinner_(false); + this.flashSavedLabel_(); + + var event = new Event('content'); + event.item = item; + event.oldEntry = oldEntry; + event.metadata = this.selectedImageMetadata_; + this.dataModel_.dispatchEvent(event); + + // Allow changing the 'Overwrite original' setting only if the user + // used Undo to restore the original image AND it is not a copy. + // Otherwise lock the setting in its current state. + var mayChangeOverwrite = !this.editor_.canUndo() && item.isOriginal(); + ImageUtil.setAttribute(this.options_, 'saved', !mayChangeOverwrite); + + if (this.imageView_.getContentRevision() === 1) { // First edit. + ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('Edit')); + } + + if (!util.isSameEntry(oldEntry, item.getEntry())) { + this.dataModel_.splice( + this.getSelectedIndex(), 0, new Gallery.Item(oldEntry)); + // The ribbon will ignore the splice above and redraw after the + // select call below (while being obscured by the Editor toolbar, + // so there is no need for nice animation here). + // SlideMode will ignore the selection change as the displayed item + // index has not changed. + this.select(++this.displayedIndex_); + } + callback(); + cr.dispatchSimpleEvent(this, 'image-saved'); + }.bind(this)); +}; + +/** + * Update caches when the selected item has been renamed. + * @param {Event} event Event. + * @private + */ +SlideMode.prototype.onContentChange_ = function(event) { + var newEntry = event.item.getEntry(); + if (util.isSameEntry(newEntry, event.oldEntry)) + this.imageView_.changeEntry(newEntry); + this.metadataCache_.clear(event.oldEntry, Gallery.METADATA_TYPE); +}; + +/** + * Flash 'Saved' label briefly to indicate that the image has been saved. + * @private + */ +SlideMode.prototype.flashSavedLabel_ = function() { + var setLabelHighlighted = + ImageUtil.setAttribute.bind(null, this.savedLabel_, 'highlighted'); + setTimeout(setLabelHighlighted.bind(null, true), 0); + setTimeout(setLabelHighlighted.bind(null, false), 300); +}; + +/** + * Local storage key for the 'Overwrite original' setting. + * @type {string} + */ +SlideMode.OVERWRITE_KEY = 'gallery-overwrite-original'; + +/** + * Local storage key for the number of times that + * the overwrite info bubble has been displayed. + * @type {string} + */ +SlideMode.OVERWRITE_BUBBLE_KEY = 'gallery-overwrite-bubble'; + +/** + * Max number that the overwrite info bubble is shown. + * @type {number} + */ +SlideMode.OVERWRITE_BUBBLE_MAX_TIMES = 5; + +/** + * @return {boolean} True if 'Overwrite original' is set. + * @private + */ +SlideMode.prototype.shouldOverwriteOriginal_ = function() { + return this.overwriteOriginal_.checked; +}; + +/** + * 'Overwrite original' checkbox handler. + * @param {Event} event Event. + * @private + */ +SlideMode.prototype.onOverwriteOriginalClick_ = function(event) { + util.platform.setPreference(SlideMode.OVERWRITE_KEY, event.target.checked); +}; + +/** + * Overwrite info bubble close handler. + * @private + */ +SlideMode.prototype.onCloseBubble_ = function() { + this.bubble_.hidden = true; + util.platform.setPreference(SlideMode.OVERWRITE_BUBBLE_KEY, + SlideMode.OVERWRITE_BUBBLE_MAX_TIMES); +}; + +// Slideshow + +/** + * Slideshow interval in ms. + */ +SlideMode.SLIDESHOW_INTERVAL = 5000; + +/** + * First slideshow interval in ms. It should be shorter so that the user + * is not guessing whether the button worked. + */ +SlideMode.SLIDESHOW_INTERVAL_FIRST = 1000; + +/** + * Empirically determined duration of the fullscreen toggle animation. + */ +SlideMode.FULLSCREEN_TOGGLE_DELAY = 500; + +/** + * @return {boolean} True if the slideshow is on. + * @private + */ +SlideMode.prototype.isSlideshowOn_ = function() { + return this.container_.hasAttribute('slideshow'); +}; + +/** + * Start the slideshow. + * @param {number=} opt_interval First interval in ms. + * @param {Event=} opt_event Event. + */ +SlideMode.prototype.startSlideshow = function(opt_interval, opt_event) { + // Set the attribute early to prevent the toolbar from flashing when + // the slideshow is being started from the mosaic view. + this.container_.setAttribute('slideshow', 'playing'); + + if (this.active_) { + this.stopEditing_(); + } else { + // We are in the Mosaic mode. Toggle the mode but remember to return. + this.leaveAfterSlideshow_ = true; + this.toggleMode_(this.startSlideshow.bind( + this, SlideMode.SLIDESHOW_INTERVAL, opt_event)); + return; + } + + if (opt_event) // Caused by user action, notify the Gallery. + cr.dispatchSimpleEvent(this, 'useraction'); + + this.fullscreenBeforeSlideshow_ = util.isFullScreen(this.context_.appWindow); + if (!this.fullscreenBeforeSlideshow_) { + // Wait until the zoom animation from the mosaic mode is done. + setTimeout(this.toggleFullScreen_.bind(this), + ImageView.ZOOM_ANIMATION_DURATION); + opt_interval = (opt_interval || SlideMode.SLIDESHOW_INTERVAL) + + SlideMode.FULLSCREEN_TOGGLE_DELAY; + } + + this.resumeSlideshow_(opt_interval); +}; + +/** + * Stop the slideshow. + * @param {Event=} opt_event Event. + * @private + */ +SlideMode.prototype.stopSlideshow_ = function(opt_event) { + if (!this.isSlideshowOn_()) + return; + + if (opt_event) // Caused by user action, notify the Gallery. + cr.dispatchSimpleEvent(this, 'useraction'); + + this.pauseSlideshow_(); + this.container_.removeAttribute('slideshow'); + + // Do not restore fullscreen if we exited fullscreen while in slideshow. + var fullscreen = util.isFullScreen(this.context_.appWindow); + var toggleModeDelay = 0; + if (!this.fullscreenBeforeSlideshow_ && fullscreen) { + this.toggleFullScreen_(); + toggleModeDelay = SlideMode.FULLSCREEN_TOGGLE_DELAY; + } + if (this.leaveAfterSlideshow_) { + this.leaveAfterSlideshow_ = false; + setTimeout(this.toggleMode_.bind(this), toggleModeDelay); + } +}; + +/** + * @return {boolean} True if the slideshow is playing (not paused). + * @private + */ +SlideMode.prototype.isSlideshowPlaying_ = function() { + return this.container_.getAttribute('slideshow') === 'playing'; +}; + +/** + * Pause/resume the slideshow. + * @private + */ +SlideMode.prototype.toggleSlideshowPause_ = function() { + cr.dispatchSimpleEvent(this, 'useraction'); // Show the tools. + if (this.isSlideshowPlaying_()) { + this.pauseSlideshow_(); + } else { + this.resumeSlideshow_(SlideMode.SLIDESHOW_INTERVAL_FIRST); + } +}; + +/** + * @param {number=} opt_interval Slideshow interval in ms. + * @private + */ +SlideMode.prototype.scheduleNextSlide_ = function(opt_interval) { + console.assert(this.isSlideshowPlaying_(), 'Inconsistent slideshow state'); + + if (this.slideShowTimeout_) + clearTimeout(this.slideShowTimeout_); + + this.slideShowTimeout_ = setTimeout(function() { + this.slideShowTimeout_ = null; + this.selectNext(1); + }.bind(this), + opt_interval || SlideMode.SLIDESHOW_INTERVAL); +}; + +/** + * Resume the slideshow. + * @param {number=} opt_interval Slideshow interval in ms. + * @private + */ +SlideMode.prototype.resumeSlideshow_ = function(opt_interval) { + this.container_.setAttribute('slideshow', 'playing'); + this.scheduleNextSlide_(opt_interval); +}; + +/** + * Pause the slideshow. + * @private + */ +SlideMode.prototype.pauseSlideshow_ = function() { + this.container_.setAttribute('slideshow', 'paused'); + if (this.slideShowTimeout_) { + clearTimeout(this.slideShowTimeout_); + this.slideShowTimeout_ = null; + } +}; + +/** + * @return {boolean} True if the editor is active. + */ +SlideMode.prototype.isEditing = function() { + return this.container_.hasAttribute('editing'); +}; + +/** + * Stop editing. + * @private + */ +SlideMode.prototype.stopEditing_ = function() { + if (this.isEditing()) + this.toggleEditor(); +}; + +/** + * Activate/deactivate editor. + * @param {Event=} opt_event Event. + */ +SlideMode.prototype.toggleEditor = function(opt_event) { + if (opt_event) // Caused by user action, notify the Gallery. + cr.dispatchSimpleEvent(this, 'useraction'); + + if (!this.active_) { + this.toggleMode_(this.toggleEditor.bind(this)); + return; + } + + this.stopSlideshow_(); + if (!this.isEditing() && this.isShowingVideo_()) + return; // No editing for videos. + + ImageUtil.setAttribute(this.container_, 'editing', !this.isEditing()); + + if (this.isEditing()) { // isEditing has just been flipped to a new value. + if (this.context_.readonlyDirName) { + this.editor_.getPrompt().showAt( + 'top', 'GALLERY_READONLY_WARNING', 0, this.context_.readonlyDirName); + } + } else { + this.editor_.getPrompt().hide(); + this.editor_.leaveModeGently(); + } +}; + +/** + * Prints the current item. + * @private + */ +SlideMode.prototype.print_ = function() { + cr.dispatchSimpleEvent(this, 'useraction'); + window.print(); +}; + +/** + * Display the error banner. + * @param {string} message Message. + * @private + */ +SlideMode.prototype.showErrorBanner_ = function(message) { + if (message) { + this.errorBanner_.textContent = this.displayStringFunction_(message); + } + ImageUtil.setAttribute(this.container_, 'error', !!message); +}; + +/** + * Show/hide the busy spinner. + * + * @param {boolean} on True if show, false if hide. + * @private + */ +SlideMode.prototype.showSpinner_ = function(on) { + if (this.spinnerTimer_) { + clearTimeout(this.spinnerTimer_); + this.spinnerTimer_ = null; + } + + if (on) { + this.spinnerTimer_ = setTimeout(function() { + this.spinnerTimer_ = null; + ImageUtil.setAttribute(this.container_, 'spinner', true); + }.bind(this), 1000); + } else { + ImageUtil.setAttribute(this.container_, 'spinner', false); + } +}; + +/** + * @return {boolean} True if the current item is a video. + * @private + */ +SlideMode.prototype.isShowingVideo_ = function() { + return !!this.imageView_.getVideo(); +}; + +/** + * Overlay that handles swipe gestures. Changes to the next or previous file. + * @param {function(number)} callback A callback accepting the swipe direction + * (1 means left, -1 right). + * @constructor + * @implements {ImageBuffer.Overlay} + */ +function SwipeOverlay(callback) { + this.callback_ = callback; +} + +/** + * Inherit ImageBuffer.Overlay. + */ +SwipeOverlay.prototype.__proto__ = ImageBuffer.Overlay.prototype; + +/** + * @param {number} x X pointer position. + * @param {number} y Y pointer position. + * @param {boolean} touch True if dragging caused by touch. + * @return {function} The closure to call on drag. + */ +SwipeOverlay.prototype.getDragHandler = function(x, y, touch) { + if (!touch) + return null; + var origin = x; + var done = false; + return function(x, y) { + if (!done && origin - x > SwipeOverlay.SWIPE_THRESHOLD) { + this.callback_(1); + done = true; + } else if (!done && x - origin > SwipeOverlay.SWIPE_THRESHOLD) { + this.callback_(-1); + done = true; + } + }.bind(this); +}; + +/** + * If the user touched the image and moved the finger more than SWIPE_THRESHOLD + * horizontally it's considered as a swipe gesture (change the current image). + */ +SwipeOverlay.SWIPE_THRESHOLD = 100; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/scrollbar.js b/chromium/chrome/browser/resources/file_manager/foreground/js/scrollbar.js new file mode 100644 index 00000000000..a81fe8db12f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/scrollbar.js @@ -0,0 +1,294 @@ +// Copyright (c) 2013 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'; + +/** + * Creates a new scroll bar element. + * @extends {HTMLDivElement} + * @constructor + */ +var ScrollBar = cr.ui.define('div'); + +/** + * Mode of the scrollbar. As for now, only vertical scrollbars are supported. + * @type {number} + */ +ScrollBar.Mode = { + VERTICAL: 0, + HORIZONTAL: 1 +}; + +ScrollBar.prototype = { + set mode(value) { + this.mode_ = value; + if (this.mode_ == ScrollBar.Mode.VERTICAL) { + this.classList.remove('scrollbar-horizontal'); + this.classList.add('scrollbar-vertical'); + } else { + this.classList.remove('scrollbar-vertical'); + this.classList.add('scrollbar-horizontal'); + } + this.redraw_(); + }, + get mode() { + return this.mode_; + } +}; + +/** + * Inherits after HTMLDivElement. + */ +ScrollBar.prototype.__proto__ = HTMLDivElement.prototype; + +/** + * Initializes the DOM structure of the scrollbar. + */ +ScrollBar.prototype.decorate = function() { + this.classList.add('scrollbar'); + this.button_ = util.createChild(this, 'scrollbar-button', 'div'); + this.mode = ScrollBar.Mode.VERTICAL; + + this.button_.addEventListener('mousedown', + this.onButtonPressed_.bind(this)); + window.addEventListener('mouseup', this.onMouseUp_.bind(this)); + window.addEventListener('mousemove', this.onMouseMove_.bind(this)); +}; + +/** + * Initialize a scrollbar. + * + * @param {Element} parent Parent element, must have a relative or absolute + * positioning. + * @param {Element=} opt_scrollableArea Element with scrollable contents. + * If not passed, then call attachToView manually when the scrollable + * element becomes available. + */ +ScrollBar.prototype.initialize = function(parent, opt_scrollableArea) { + parent.appendChild(this); + if (opt_scrollableArea) + this.attachToView(opt_scrollableArea); +}; + +/** + * Attaches the scrollbar to a scrollable element and attaches handlers. + * @param {Element} view Scrollable element. + */ +ScrollBar.prototype.attachToView = function(view) { + this.view_ = view; + this.view_.addEventListener('scroll', this.onScroll_.bind(this)); + this.view_.addEventListener('relayout', this.onRelayout_.bind(this)); + this.domObserver_ = new MutationObserver(this.onDomChanged_.bind(this)); + this.domObserver_.observe(this.view_, {subtree: true, attributes: true}); + this.onRelayout_(); +}; + +/** + * Scroll handler. + * @private + */ +ScrollBar.prototype.onScroll_ = function() { + this.scrollTop_ = this.view_.scrollTop; + this.redraw_(); +}; + +/** + * Relayout handler. + * @private + */ +ScrollBar.prototype.onRelayout_ = function() { + this.scrollHeight_ = this.view_.scrollHeight; + this.clientHeight_ = this.view_.clientHeight; + this.offsetTop_ = this.view_.offsetTop; + this.scrollTop_ = this.view_.scrollTop; + this.redraw_(); +}; + +/** + * Pressing on the scrollbar's button handler. + * + * @param {Event} event Pressing event. + * @private + */ +ScrollBar.prototype.onButtonPressed_ = function(event) { + this.buttonPressed_ = true; + this.buttonPressedEvent_ = event; + this.buttonPressedPosition_ = this.button_.offsetTop - this.view_.offsetTop; + this.button_.classList.add('pressed'); + + event.preventDefault(); +}; + +/** + * Releasing the button handler. Note, that it may not be called when releasing + * outside of the window. Therefore this is also called from onMouseMove_. + * + * @param {Event} event Mouse event. + * @private + */ +ScrollBar.prototype.onMouseUp_ = function(event) { + this.buttonPressed_ = false; + this.button_.classList.remove('pressed'); +}; + +/** + * Mouse move handler. Updates the scroll position. + * + * @param {Event} event Mouse event. + * @private + */ +ScrollBar.prototype.onMouseMove_ = function(event) { + if (!this.buttonPressed_) + return; + if (!event.which) { + this.onMouseUp_(event); + return; + } + var clientSize = this.getClientHeight(); + var totalSize = this.getTotalHeight(); + // TODO(hirono): Fix the geometric calculation. crbug.com/253779 + var buttonSize = Math.max(50, clientSize / totalSize * clientSize); + var buttonPosition = this.buttonPressedPosition_ + + (event.screenY - this.buttonPressedEvent_.screenY); + // Ensures the scrollbar is in the view. + buttonPosition = + Math.max(0, Math.min(buttonPosition, clientSize - buttonSize)); + var scrollPosition; + if (clientSize > buttonSize) { + scrollPosition = Math.max(totalSize - clientSize, 0) * + buttonPosition / (clientSize - buttonSize); + } else { + scrollPosition = 0; + } + + this.scrollTop_ = scrollPosition; + this.view_.scrollTop = scrollPosition; + this.redraw_(); +}; + +/** + * Handles changed in Dom by redrawing the scrollbar. Ignores consecutive calls. + * @private + */ +ScrollBar.prototype.onDomChanged_ = function() { + if (this.domChangedTimer_) { + clearTimeout(this.domChangedTimer_); + this.domChangedTimer_ = null; + } + this.domChangedTimer_ = setTimeout(function() { + this.onRelayout_(); + this.domChangedTimer_ = null; + }.bind(this), 50); +}; + +/** + * Redraws the scrollbar. + * @private + */ +ScrollBar.prototype.redraw_ = function() { + if (!this.view_) + return; + + var clientSize = this.getClientHeight(); + var clientTop = this.offsetTop_; + var scrollPosition = this.scrollTop_; + var totalSize = this.getTotalHeight(); + var hidden = totalSize <= clientSize; + + var buttonSize = Math.max(50, clientSize / totalSize * clientSize); + var buttonPosition; + if (clientSize - buttonSize > 0) { + buttonPosition = scrollPosition / (totalSize - clientSize) * + (clientSize - buttonSize); + } else { + buttonPosition = 0; + } + var buttonTop = buttonPosition + clientTop; + + var time = Date.now(); + if (this.hidden != hidden || + this.lastButtonTop_ != buttonTop || + this.lastButtonSize_ != buttonSize) { + requestAnimationFrame(function() { + this.hidden = hidden; + this.button_.style.top = buttonTop + 'px'; + this.button_.style.height = buttonSize + 'px'; + }.bind(this)); + } + + this.lastButtonTop_ = buttonTop; + this.lastButtonSize_ = buttonSize; +}; + +/** + * Returns the viewport height of the view. + * @return {number} The viewport height of the view in px. + * @protected + */ +ScrollBar.prototype.getClientHeight = function() { + return this.clientHeight_; +}; + +/** + * Returns the total height of the view. + * @return {number} The total height of the view in px. + * @protected + */ +ScrollBar.prototype.getTotalHeight = function() { + return this.scrollHeight_; +}; + +/** + * Creates a new scroll bar for elements in the main panel. + * @extends {ScrollBar} + * @constructor + */ +var MainPanelScrollBar = cr.ui.define('div'); + +/** + * Inherits after ScrollBar. + */ +MainPanelScrollBar.prototype.__proto__ = ScrollBar.prototype; + +/** @override */ +MainPanelScrollBar.prototype.decorate = function() { + ScrollBar.prototype.decorate.call(this); + + /** + * Margin for the transparent preview panel at the bottom. + * @type {number} + * @private + */ + this.bottomMarginForPanel_ = 0; +}; + +/** + * GReturns the viewport height of the view, considering the preview panel. + * + * @return {number} The viewport height of the view in px. + * @override + * @protected + */ +MainPanelScrollBar.prototype.getClientHeight = function() { + return this.clientHeight_ - this.bottomMarginForPanel_; +}; + +/** + * Returns the total height of the view, considering the preview panel. + * + * @return {number} The total height of the view in px. + * @override + * @protected + */ +MainPanelScrollBar.prototype.getTotalHeight = function() { + return this.scrollHeight_ - this.bottomMarginForPanel_; +}; + +/** + * Sets the bottom margin height of the view for the transparent preview panel. + * @param {number} margin Margin to be set in px. + */ +MainPanelScrollBar.prototype.setBottomMarginForPanel = function(margin) { + this.bottomMarginForPanel_ = margin; +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/share_client.js b/chromium/chrome/browser/resources/file_manager/foreground/js/share_client.js new file mode 100644 index 00000000000..3044b21a17e --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/share_client.js @@ -0,0 +1,188 @@ +// Copyright 2013 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'; + +/** + * @param {WebView} webView Web View tag. + * @param {string} url Share Url for an entry. + * @param {ShareClient.Observer} observer Observer instance. + * @constructor + */ +function ShareClient(webView, url, observer) { + this.webView_ = webView; + this.url_ = url; + this.observer_ = observer; + this.loaded_ = false; + this.loading_ = false; + this.onMessageBound_ = this.onMessage_.bind(this); + this.onLoadStopBound_ = this.onLoadStop_.bind(this); + this.onLoadAbortBound_ = this.onLoadAbort_.bind(this); +} + +/** + * Source origin of the client. + * @type {string} + * @const + */ +ShareClient.SHARE_ORIGIN = + 'chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj'; + +/** + * Target origin of the embedded dialog. + * @type {string} + * @const + */ +ShareClient.SHARE_TARGET = 'https://drive.google.com'; + +/** + * Observes for state changes of the embedded dialog. + * @interface + */ +ShareClient.Observer = function() { +}; + +/** + * Notifies about the embedded dialog being loaded. + */ +ShareClient.Observer.prototype.onLoaded = function() { +}; + +/** + * Notifies when the the embedded dialog failed to load. + */ +ShareClient.Observer.prototype.onLoadingFailed = function() { +}; + +/** + * Notifies about changed dimensions of the embedded dialog. + * @param {number} width Width in pixels. + * @param {number} height Height in pixels. + * @param {function()} callback Completion callback. Call when finished + * handling the resize. + */ +ShareClient.Observer.prototype.onResized = function(width, height, callback) { +}; + +/** + * Notifies about the embedded dialog being closed. + */ +ShareClient.Observer.prototype.onClosed = function() { +}; + +/** + * Handles messages from the embedded dialog. + * @param {Event} e Message event. + * @private + */ +ShareClient.prototype.onMessage_ = function(e) { + if (e.origin != ShareClient.SHARE_TARGET && !window.IN_TEST) { + // Logs added temporarily to track crbug.com/288783. + console.debug('Received a message from an illegal origin: ' + e.origin); + return; + } + + var data = JSON.parse(e.data); + // Logs added temporarily to track crbug.com/288783. + console.debug('Received message: ' + data.type); + + switch (data.type) { + case 'resize': + this.observer_.onResized(data.args.width, + data.args.height, + this.postMessage_.bind(this, 'resizeComplete')); + break; + case 'prepareForVisible': + this.postMessage_('prepareComplete'); + if (!this.loaded_) { + this.loading_ = false; + this.loaded_ = true; + this.observer_.onLoaded(); + } + break; + case 'setVisible': + if (!data.args.visible) + this.observer_.onClosed(); + break; + } +}; + +/** + * Handles completion of the web view request. + * @param {Event} e Message event. + * @private + */ +ShareClient.prototype.onLoadStop_ = function(e) { + // Logs added temporarily to track crbug.com/288783. + console.debug('Web View loaded.'); + + this.postMessage_('makeBodyVisible'); +}; + +/** + * Handles termination of the web view request. + * @param {Event} e Message event. + * @private + */ +ShareClient.prototype.onLoadAbort_ = function(e) { + // Logs added temporarily to track crbug.com/288783. + console.debug('Web View failed to load with error: ' + e.reason + ', url: ' + + e.url + ' while requested: ' + this.url_); + + this.observer_.onLoadFailed(); +}; + +/** + * Sends a message to the embedded dialog. + * @param {string} type Message type. + * @param {Object=} opt_args Optional arguments. + * @private + */ +ShareClient.prototype.postMessage_ = function(type, opt_args) { + // Logs added temporarily to track crbug.com/288783. + console.debug('Sending message: ' + type); + + var message = { + type: type, + args: opt_args + }; + this.webView_.contentWindow.postMessage( + JSON.stringify(message), + !window.IN_TEST ? ShareClient.SHARE_TARGET : '*'); +}; + +/** + * Loads the embedded dialog. Can be called only one. + */ +ShareClient.prototype.load = function() { + if (this.loading_ || this.loaded_) + throw new Error('Already loaded.'); + this.loading_ = true; + + // Logs added temporarily to track crbug.com/288783. + console.debug('Loading.'); + + window.addEventListener('message', this.onMessageBound_); + this.webView_.addEventListener('loadstop', this.onLoadStopBound_); + this.webView_.addEventListener('loadabort', this.onLoadAbortBound_); + this.webView_.setAttribute('src', this.url_); +}; + +/** + * Aborts loading of the embedded dialog and performs cleanup. + */ +ShareClient.prototype.abort = function() { + window.removeEventListener('message', this.onMessageBound_); + this.webView_.removeEventListener('loadstop', this.onLoadStopBound_); + this.webView_.removeEventListener( + 'loadabort', this.onLoadAbortBound_); + this.webView_.stop(); +}; + +/** + * Cleans the dialog by removing all handlers. + */ +ShareClient.prototype.dispose = function() { + this.abort(); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/share_dialog.js b/chromium/chrome/browser/resources/file_manager/foreground/js/share_dialog.js new file mode 100644 index 00000000000..ff7fbf6b665 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/share_dialog.js @@ -0,0 +1,314 @@ +// Copyright 2013 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'; + +/** + * @param {HTMLElement} parentNode Node to be parent for this dialog. + * @constructor + * @extends {FileManagerDialogBase} + * @implements {ShareClient.Observer} + */ +function ShareDialog(parentNode) { + this.queue_ = new AsyncUtil.Queue(); + this.onQueueTaskFinished_ = null; + this.shareClient_ = null; + this.spinner_ = null; + this.spinnerLayer_ = null; + this.webViewWrapper_ = null; + this.webView_ = null; + this.failureTimeout_ = null; + this.callback_ = null; + + FileManagerDialogBase.call(this, parentNode); +} + +/** + * Timeout for loading the share dialog before giving up. + * @type {number} + * @const + */ +ShareDialog.FAILURE_TIMEOUT = 10000; + +/** + * The result of opening the dialog. + * @enum {string} + * @const + */ +ShareDialog.Result = Object.freeze({ + // The dialog is closed normally. This includes user cancel. + SUCCESS: 'success', + // The dialog is closed by network error. + NETWORK_ERROR: 'networkError', + // The dialog is not opened because it is already showing. + ALREADY_SHOWING: 'alreadyShowing' +}); + +/** + * Wraps a Web View element and adds authorization headers to it. + * @param {string} urlPattern Pattern of urls to be authorized. + * @param {WebView} webView Web View element to be wrapped. + * @constructor + */ +ShareDialog.WebViewAuthorizer = function(urlPattern, webView) { + this.urlPattern_ = urlPattern; + this.webView_ = webView; + this.initialized_ = false; + this.accessToken_ = null; +}; + +/** + * Initializes the web view by installing hooks injecting the authorization + * headers. + * @param {function()} callback Completion callback. + */ +ShareDialog.WebViewAuthorizer.prototype.initialize = function(callback) { + if (this.initialized_) { + callback(); + return; + } + + var registerInjectionHooks = function() { + this.webView_.removeEventListener('loadstop', registerInjectionHooks); + this.webView_.request.onBeforeSendHeaders.addListener( + this.authorizeRequest_.bind(this), + {urls: [this.urlPattern_]}, + ['blocking', 'requestHeaders']); + this.initialized_ = true; + callback(); + }.bind(this); + + this.webView_.addEventListener('loadstop', registerInjectionHooks); + this.webView_.setAttribute('src', 'data:text/html,'); +}; + +/** + * Authorizes the web view by fetching the freshest access tokens. + * @param {function()} callback Completion callback. + */ +ShareDialog.WebViewAuthorizer.prototype.authorize = function(callback) { + // Fetch or update the access token. + chrome.fileBrowserPrivate.requestAccessToken(false, // force_refresh + function(inAccessToken) { + this.accessToken_ = inAccessToken; + callback(); + }.bind(this)); +}; + +/** + * Injects headers into the passed request. + * @param {Event} e Request event. + * @return {{requestHeaders: HttpHeaders}} Modified headers. + * @private + */ +ShareDialog.WebViewAuthorizer.prototype.authorizeRequest_ = function(e) { + e.requestHeaders.push({ + name: 'Authorization', + value: 'Bearer ' + this.accessToken_ + }); + return {requestHeaders: e.requestHeaders}; +}; + +ShareDialog.prototype = { + __proto__: FileManagerDialogBase.prototype +}; + +/** + * One-time initialization of DOM. + * @private + */ +ShareDialog.prototype.initDom_ = function() { + FileManagerDialogBase.prototype.initDom_.call(this); + this.frame_.classList.add('share-dialog-frame'); + + this.spinnerLayer_ = this.document_.createElement('div'); + this.spinnerLayer_.className = 'spinner-layer'; + this.frame_.appendChild(this.spinnerLayer_); + + this.webViewWrapper_ = this.document_.createElement('div'); + this.webViewWrapper_.className = 'share-dialog-webview-wrapper'; + this.cancelButton_.hidden = true; + this.okButton_.hidden = true; + this.frame_.insertBefore(this.webViewWrapper_, + this.frame_.querySelector('.cr-dialog-buttons')); +}; + +/** + * @override + */ +ShareDialog.prototype.onResized = function(width, height, callback) { + if (width && height) { + this.webViewWrapper_.style.width = width + 'px'; + this.webViewWrapper_.style.height = height + 'px'; + this.webView_.style.width = width + 'px'; + this.webView_.style.height = height + 'px'; + } + setTimeout(callback, 0); +}; + +/** + * @override + */ +ShareDialog.prototype.onClosed = function() { + this.hide(); +}; + +/** + * @override + */ +ShareDialog.prototype.onLoaded = function() { + if (this.failureTimeout_) { + clearTimeout(this.failureTimeout_); + this.failureTimeout_ = null; + } + + // Logs added temporarily to track crbug.com/288783. + console.debug('Loaded.'); + + this.okButton_.hidden = false; + this.spinnerLayer_.hidden = true; + this.webViewWrapper_.classList.add('loaded'); + this.webView_.focus(); +}; + +/** + * @override + */ +ShareDialog.prototype.onLoadFailed = function() { + this.hideWithResult(ShareDialog.Result.NETWORK_ERROR); +}; + +/** + * @override + */ +ShareDialog.prototype.hide = function(opt_onHide) { + this.hideWithResult(ShareDialog.Result.SUCCESS, opt_onHide); +}; + +/** + * Hide the dialog with the result and the callback. + * @param {ShareDialog.Result} result Result passed to the closing callback. + * @param {function()=} opt_onHide Callback called at the end of hiding. + */ +ShareDialog.prototype.hideWithResult = function(result, opt_onHide) { + if (!this.isShowing()) + return; + + if (this.shareClient_) { + this.shareClient_.dispose(); + this.shareClient_ = null; + } + + this.webViewWrapper_.textContent = ''; + if (this.failureTimeout_) { + clearTimeout(this.failureTimeout_); + this.failureTimeout_ = null; + } + + FileManagerDialogBase.prototype.hide.call( + this, + function() { + if (opt_onHide) + opt_onHide(); + this.callback_(result); + this.callback_ = null; + }.bind(this)); +}; + +/** + * Shows the dialog. + * @param {FileEntry} entry Entry to share. + * @param {function(boolean)} callback Callback to be called when the showing + * task is completed. The argument is whether to succeed or not. Note that + * cancel is regarded as success. + */ +ShareDialog.prototype.show = function(entry, callback) { + // If the dialog is already showing, return the error. + if (this.isShowing()) { + callback(ShareDialog.Result.ALREADY_SHOWING); + return; + } + + // Initialize the variables. + this.callback_ = callback; + this.spinnerLayer_.hidden = false; + this.webViewWrapper_.style.width = ''; + this.webViewWrapper_.style.height = ''; + + // If the embedded share dialog is not started within some time, then + // give up and show an error message. + this.failureTimeout_ = setTimeout(function() { + this.hideWithResult(ShareDialog.Result.NETWORK_ERROR); + + // Logs added temporarily to track crbug.com/288783. + console.debug('Timeout. Web View points at: ' + this.webView_.src); + }.bind(this), ShareDialog.FAILURE_TIMEOUT); + + // TODO(mtomasz): Move to initDom_() once and reuse <webview> once it gets + // fixed. See: crbug.com/260622. + this.webView_ = util.createChild( + this.webViewWrapper_, 'share-dialog-webview', 'webview'); + this.webView_.setAttribute('tabIndex', '-1'); + this.webViewAuthorizer_ = new ShareDialog.WebViewAuthorizer( + !window.IN_TEST ? (ShareClient.SHARE_TARGET + '/*') : '<all_urls>', + this.webView_); + this.webView_.addEventListener('newwindow', function(e) { + // Discard the window object and reopen in an external window. + e.window.discard(); + util.visitURL(e.targetUrl); + e.preventDefault(); + }); + var show = FileManagerDialogBase.prototype.showBlankDialog.call(this); + if (!show) { + // The code shoundn't get here, since already-showing was handled before. + console.error('ShareDialog can\'t be shown.'); + return; + } + + // Initialize and authorize the Web View tag asynchronously. + var group = new AsyncUtil.Group(); + + // Fetches an url to the sharing dialog. + var shareUrl; + group.add(function(inCallback) { + chrome.fileBrowserPrivate.getShareUrl( + entry.toURL(), + function(inShareUrl) { + if (!chrome.runtime.lastError) + shareUrl = inShareUrl; + inCallback(); + }); + }); + group.add(this.webViewAuthorizer_.initialize.bind(this.webViewAuthorizer_)); + group.add(this.webViewAuthorizer_.authorize.bind(this.webViewAuthorizer_)); + + // Loads the share widget once all the previous async calls are finished. + group.run(function() { + // If the url is not obtained, return the network error. + if (!shareUrl) { + // Logs added temporarily to track crbug.com/288783. + console.debug('URL not available.'); + + this.hideWithResult(ShareDialog.Result.NETWORK_ERROR); + return; + } + // Already inactive, therefore ignore. + if (!this.isShowing()) + return; + this.shareClient_ = new ShareClient(this.webView_, + shareUrl, + this); + this.shareClient_.load(); + }.bind(this)); +}; + +/** + * Tells whether the share dialog is showing or not. + * + * @return {boolean} True since the show method is called and until the closing + * callback is invoked. + */ +ShareDialog.prototype.isShowing = function() { + return !!this.callback_; +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/suggest_apps_dialog.js b/chromium/chrome/browser/resources/file_manager/foreground/js/suggest_apps_dialog.js new file mode 100644 index 00000000000..27d12dfe19b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/suggest_apps_dialog.js @@ -0,0 +1,554 @@ +// Copyright (c) 2013 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'; + +/** + * SuggestAppsDialog contains a list box to select an app to be opened the file + * with. This dialog should be used as action picker for file operations. + */ + +/** + * The width of the widget (in pixel). + * @type {number} + * @const + */ +var WEBVIEW_WIDTH = 735; +/** + * The height of the widget (in pixel). + * @type {number} + * @const + */ +var WEBVIEW_HEIGHT = 480; + +/** + * The URL of the widget. + * @type {string} + * @const + */ +var CWS_WIDGET_URL = + 'https://clients5.google.com/webstore/wall/cros-widget-container'; +/** + * The origin of the widget. + * @type {string} + * @const + */ +var CWS_WIDGET_ORIGIN = 'https://clients5.google.com'; + +/** + * Creates dialog in DOM tree. + * + * @param {HTMLElement} parentNode Node to be parent for this dialog. + * @param {Object} state Static state of suggest app dialog. + * @constructor + * @extends {FileManagerDialogBase} + */ +function SuggestAppsDialog(parentNode, state) { + FileManagerDialogBase.call(this, parentNode); + + this.frame_.id = 'suggest-app-dialog'; + + this.webviewContainer_ = this.document_.createElement('div'); + this.webviewContainer_.id = 'webview-container'; + this.webviewContainer_.style.width = WEBVIEW_WIDTH + 'px'; + this.webviewContainer_.style.height = WEBVIEW_HEIGHT + 'px'; + this.frame_.insertBefore(this.webviewContainer_, this.text_.nextSibling); + + var spinnerLayer = this.document_.createElement('div'); + spinnerLayer.className = 'spinner-layer'; + this.webviewContainer_.appendChild(spinnerLayer); + + this.buttons_ = this.document_.createElement('div'); + this.buttons_.id = 'buttons'; + this.frame_.appendChild(this.buttons_); + + this.webstoreButton_ = this.document_.createElement('div'); + this.webstoreButton_.id = 'webstore-button'; + this.webstoreButton_.innerHTML = str('SUGGEST_DIALOG_LINK_TO_WEBSTORE'); + this.webstoreButton_.addEventListener( + 'click', this.onWebstoreLinkClicked_.bind(this)); + this.buttons_.appendChild(this.webstoreButton_); + + this.initialFocusElement_ = this.webviewContainer_; + + this.webview_ = null; + this.accessToken_ = null; + this.widgetUrl_ = + state.overrideCwsContainerUrlForTest || CWS_WIDGET_URL; + this.widgetOrigin_ = + state.overrideCwsContainerOriginForTest || CWS_WIDGET_ORIGIN; + + this.extension_ = null; + this.mime_ = null; + this.installingItemId_ = null; + this.state_ = SuggestAppsDialog.State.UNINITIALIZED; + + this.initializationTask_ = new AsyncUtil.Group(); + this.initializationTask_.add(this.retrieveAuthorizeToken_.bind(this)); + this.initializationTask_.run(); +} + +SuggestAppsDialog.prototype = { + __proto__: FileManagerDialogBase.prototype +}; + +/** + * @enum {string} + * @const + */ +SuggestAppsDialog.State = { + UNINITIALIZED: 'SuggestAppsDialog.State.UNINITIALIZED', + INITIALIZING: 'SuggestAppsDialog.State.INITIALIZING', + INITIALIZE_FAILED_CLOSING: + 'SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING', + INITIALIZED: 'SuggestAppsDialog.State.INITIALIZED', + INSTALLING: 'SuggestAppsDialog.State.INSTALLING', + INSTALLED_CLOSING: 'SuggestAppsDialog.State.INSTALLED_CLOSING', + OPENING_WEBSTORE_CLOSING: 'SuggestAppsDialog.State.OPENING_WEBSTORE_CLOSING', + CANCELED_CLOSING: 'SuggestAppsDialog.State.CANCELED_CLOSING' +}; +Object.freeze(SuggestAppsDialog.State); + +/** + * @enum {string} + * @const + */ +SuggestAppsDialog.Result = { + // Install is done. The install app should be opened. + INSTALL_SUCCESSFUL: 'SuggestAppsDialog.Result.INSTALL_SUCCESSFUL', + // User cancelled the suggest app dialog. No message should be shown. + USER_CANCELL: 'SuggestAppsDialog.Result.USER_CANCELL', + // User clicked the link to web store so the dialog is closed. + WEBSTORE_LINK_OPENED: 'SuggestAppsDialog.Result.WEBSTORE_LINK_OPENED', + // Failed to load the widget. Error message should be shown. + FAILED: 'SuggestAppsDialog.Result.FAILED' +}; +Object.freeze(SuggestAppsDialog.Result); + +/** + * @override + */ +SuggestAppsDialog.prototype.onInputFocus = function() { + this.webviewContainer_.select(); +}; + +/** + * Injects headers into the passed request. + * + * @param {Event} e Request event. + * @return {{requestHeaders: HttpHeaders}} Modified headers. + * @private + */ +SuggestAppsDialog.prototype.authorizeRequest_ = function(e) { + e.requestHeaders.push({ + name: 'Authorization', + value: 'Bearer ' + this.accessToken_ + }); + return {requestHeaders: e.requestHeaders}; +}; + +/** + * Retrieves the authorize token. This method should be called in + * initialization of the dialog. + * + * @param {function()} callback Called when the token is retrieved. + * @private + */ +SuggestAppsDialog.prototype.retrieveAuthorizeToken_ = function(callback) { + if (window.IN_TEST) { + // In test, use a dummy string as token. This must be a non-empty string. + this.accessToken_ = 'DUMMY_ACCESS_TOKEN_FOR_TEST'; + } + + if (this.accessToken_) { + callback(); + return; + } + + // Fetch or update the access token. + chrome.fileBrowserPrivate.requestWebStoreAccessToken( + function(accessToken) { + // In case of error, this.accessToken_ will be set to null. + this.accessToken_ = accessToken; + callback(); + }.bind(this)); +}; + +/** + * Dummy function for SuggestAppsDialog.show() not to be called unintentionally. + */ +SuggestAppsDialog.prototype.show = function() { + console.error('SuggestAppsDialog.show() shouldn\'t be called directly.'); +}; + +/** + * Shows suggest-apps dialog by file extension and mime. + * + * @param {string} extension Extension of the file. + * @param {string} mime Mime of the file. + * @param {function(boolean)} onDialogClosed Called when the dialog is closed. + * The argument is the result of installation: true if an app is installed, + * false otherwise. + */ +SuggestAppsDialog.prototype.showByExtensionAndMime = + function(extension, mime, onDialogClosed) { + this.text_.hidden = true; + this.dialogText_ = ''; + this.showInternal_(null, extension, mime, onDialogClosed); +}; + +/** + * Shows suggest-apps dialog by the filename. + * + * @param {string} filename Filename (without extension) of the file. + * @param {function(boolean)} onDialogClosed Called when the dialog is closed. + * The argument is the result of installation: true if an app is installed, + * false otherwise. + */ +SuggestAppsDialog.prototype.showByFilename = + function(filename, onDialogClosed) { + this.text_.hidden = false; + this.dialogText_ = str('SUGGEST_DIALOG_MESSAGE_FOR_EXECUTABLE'); + this.showInternal_(filename, null, null, onDialogClosed); +}; + +/** + * Internal methdo to shows a dialog. This should be called only from 'Suggest. + * appDialog.showXxxx()' functions. + * + * @param {string} filename Filename (without extension) of the file. + * @param {string} extension Extension of the file. + * @param {string} mime Mime of the file. + * @param {function(boolean)} onDialogClosed Called when the dialog is closed. + * The argument is the result of installation: true if an app is installed, + * false otherwise. + * @private + */ +SuggestAppsDialog.prototype.showInternal_ = + function(filename, extension, mime, onDialogClosed) { + if (this.state_ != SuggestAppsDialog.State.UNINITIALIZED) { + console.error('Invalid state.'); + return; + } + + this.extension_ = extension; + this.mimeType_ = mime; + this.onDialogClosed_ = onDialogClosed; + this.state_ = SuggestAppsDialog.State.INITIALIZING; + + SuggestAppsDialog.Metrics.recordShowDialog(); + SuggestAppsDialog.Metrics.startLoad(); + + // Makes it sure that the initialization is completed. + this.initializationTask_.run(function() { + if (!this.accessToken_) { + this.state_ = SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING; + this.onHide_(); + return; + } + + var title = str('SUGGEST_DIALOG_TITLE'); + var show = this.dialogText_ ? + FileManagerDialogBase.prototype.showTitleAndTextDialog.call( + this, title, this.dialogText_) : + FileManagerDialogBase.prototype.showTitleOnlyDialog.call( + this, title); + if (!show) { + console.error('SuggestAppsDialog can\'t be shown'); + this.state_ = SuggestAppsDialog.State.UNINITIALIZED; + this.onHide(); + return; + } + + this.webview_ = this.document_.createElement('webview'); + this.webview_.id = 'cws-widget'; + this.webview_.partition = 'persist:cwswidgets'; + this.webview_.style.width = WEBVIEW_WIDTH + 'px'; + this.webview_.style.height = WEBVIEW_HEIGHT + 'px'; + this.webview_.request.onBeforeSendHeaders.addListener( + this.authorizeRequest_.bind(this), + {urls: [this.widgetOrigin_ + '/*']}, + ['blocking', 'requestHeaders']); + this.webview_.addEventListener('newwindow', function(event) { + // Discard the window object and reopen in an external window. + event.window.discard(); + util.visitURL(event.targetUrl); + event.preventDefault(); + }); + this.webviewContainer_.appendChild(this.webview_); + + this.frame_.classList.add('show-spinner'); + + this.webviewClient_ = new CWSContainerClient( + this.webview_, + extension, mime, filename, + WEBVIEW_WIDTH, WEBVIEW_HEIGHT, + this.widgetUrl_, this.widgetOrigin_); + this.webviewClient_.addEventListener(CWSContainerClient.Events.LOADED, + this.onWidgetLoaded_.bind(this)); + this.webviewClient_.addEventListener(CWSContainerClient.Events.LOAD_FAILED, + this.onWidgetLoadFailed_.bind(this)); + this.webviewClient_.addEventListener( + CWSContainerClient.Events.REQUEST_INSTALL, + this.onInstallRequest_.bind(this)); + this.webviewClient_.load(); + }.bind(this)); +}; + +/** + * Called when the 'See more...' link is clicked to be navigated to Webstore. + * @param {Event} e Event. + * @private + */ +SuggestAppsDialog.prototype.onWebstoreLinkClicked_ = function(e) { + var webStoreUrl = + FileTasks.createWebStoreLink(this.extension_, this.mimeType_); + chrome.windows.create({url: webStoreUrl}); + this.state_ = SuggestAppsDialog.State.OPENING_WEBSTORE_CLOSING; + this.hide(); +}; + +/** + * Called when the widget is loaded successfully. + * @param {Event} event Event. + * @private + */ +SuggestAppsDialog.prototype.onWidgetLoaded_ = function(event) { + SuggestAppsDialog.Metrics.finishLoad(); + SuggestAppsDialog.Metrics.recordLoad( + SuggestAppsDialog.Metrics.LOAD.SUCCEEDED); + + this.frame_.classList.remove('show-spinner'); + this.state_ = SuggestAppsDialog.State.INITIALIZED; + + this.webview_.focus(); +}; + +/** + * Called when the widget is failed to load. + * @param {Event} event Event. + * @private + */ +SuggestAppsDialog.prototype.onWidgetLoadFailed_ = function(event) { + SuggestAppsDialog.Metrics.recordLoad(SuggestAppsDialog.Metrics.LOAD.FAILURE); + + this.frame_.classList.remove('show-spinner'); + this.state_ = SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING; + + this.hide(); +}; + +/** + * Called when the connection status is changed. + * @param {util.DriveConnectionType} connectionType Current connection type. + */ +SuggestAppsDialog.prototype.onDriveConnectionChanged = + function(connectionType) { + if (this.state_ !== SuggestAppsDialog.State.UNINITIALIZED && + connectionType === util.DriveConnectionType.OFFLINE) { + this.state_ = SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING; + this.hide(); + } +}; + +/** + * Called when receiving the install request from the webview client. + * @param {Event} e Event. + * @private + */ +SuggestAppsDialog.prototype.onInstallRequest_ = function(e) { + var itemId = e.itemId; + this.installingItemId_ = itemId; + + this.appInstaller_ = new AppInstaller(itemId); + this.appInstaller_.install(this.onInstallCompleted_.bind(this)); + + this.frame_.classList.add('show-spinner'); + this.state_ = SuggestAppsDialog.State.INSTALLING; +}; + +/** + * Called when the installation is completed from the app installer. + * @param {AppInstaller.Result} result Result of the installation. + * @param {string} error Detail of the error. + * @private + */ +SuggestAppsDialog.prototype.onInstallCompleted_ = function(result, error) { + var success = (result === AppInstaller.Result.SUCCESS); + + this.frame_.classList.remove('show-spinner'); + this.state_ = success ? + SuggestAppsDialog.State.INSTALLED_CLOSING : + SuggestAppsDialog.State.INITIALIZED; // Back to normal state. + this.webviewClient_.onInstallCompleted(success, this.installingItemId_); + this.installingItemId_ = null; + + switch (result) { + case AppInstaller.Result.SUCCESS: + SuggestAppsDialog.Metrics.recordInstall( + SuggestAppsDialog.Metrics.INSTALL.SUCCESS); + this.hide(); + break; + case AppInstaller.Result.CANCELLED: + SuggestAppsDialog.Metrics.recordInstall( + SuggestAppsDialog.Metrics.INSTALL.CANCELLED); + // User cancelled the installation. Do nothing. + break; + case AppInstaller.Result.ERROR: + SuggestAppsDialog.Metrics.recordInstall( + SuggestAppsDialog.Metrics.INSTALL.FAILED); + fileManager.error.show(str('SUGGEST_DIALOG_INSTALLATION_FAILED')); + break; + } +}; + +/** + * @override + */ +SuggestAppsDialog.prototype.hide = function(opt_originalOnHide) { + switch (this.state_) { + case SuggestAppsDialog.State.INSTALLING: + // Install is being aborted. Send the failure result. + // Cancels the install. + if (this.webviewClient_) + this.webviewClient_.onInstallCompleted(false, this.installingItemId_); + this.installingItemId_ = null; + + // Assumes closing the dialog as canceling the install. + this.state_ = SuggestAppsDialog.State.CANCELED_CLOSING; + break; + case SuggestAppsDialog.State.INITIALIZING: + SuggestAppsDialog.Metrics.recordLoad( + SuggestAppsDialog.Metrics.LOAD.CANCELLED); + this.state_ = SuggestAppsDialog.State.CANCELED_CLOSING; + break; + case SuggestAppsDialog.State.INSTALLED_CLOSING: + case SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING: + case SuggestAppsDialog.State.OPENING_WEBSTORE_CLOSING: + // Do nothing. + break; + case SuggestAppsDialog.State.INITIALIZED: + this.state_ = SuggestAppsDialog.State.CANCELED_CLOSING; + break; + default: + this.state_ = SuggestAppsDialog.State.CANCELED_CLOSING; + console.error('Invalid state.'); + } + + if (this.webviewClient_) { + this.webviewClient_.dispose(); + this.webviewClient_ = null; + } + + this.webviewContainer_.removeChild(this.webview_); + this.webview_ = null; + this.extension_ = null; + this.mime_ = null; + + FileManagerDialogBase.prototype.hide.call( + this, + this.onHide_.bind(this, opt_originalOnHide)); +}; + +/** + * @param {function()=} opt_originalOnHide Original onHide function passed to + * SuggestAppsDialog.hide(). + * @private + */ +SuggestAppsDialog.prototype.onHide_ = function(opt_originalOnHide) { + // Calls the callback after the dialog hides. + if (opt_originalOnHide) + opt_originalOnHide(); + + var result; + switch (this.state_) { + case SuggestAppsDialog.State.INSTALLED_CLOSING: + result = SuggestAppsDialog.Result.INSTALL_SUCCESSFUL; + SuggestAppsDialog.Metrics.recordCloseDialog( + SuggestAppsDialog.Metrics.CLOSE_DIALOG.ITEM_INSTALLED); + break; + case SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING: + result = SuggestAppsDialog.Result.FAILED; + break; + case SuggestAppsDialog.State.CANCELED_CLOSING: + result = SuggestAppsDialog.Result.USER_CANCELL; + SuggestAppsDialog.Metrics.recordCloseDialog( + SuggestAppsDialog.Metrics.CLOSE_DIALOG.USER_CANCELL); + break; + case SuggestAppsDialog.State.OPENING_WEBSTORE_CLOSING: + result = SuggestAppsDialog.Result.WEBSTORE_LINK_OPENED; + SuggestAppsDialog.Metrics.recordCloseDialog( + SuggestAppsDialog.Metrics.CLOSE_DIALOG.WEB_STORE_LINK); + break; + default: + result = SuggestAppsDialog.Result.USER_CANCELL; + SuggestAppsDialog.Metrics.recordCloseDialog( + SuggestAppsDialog.Metrics.CLOSE_DIALOG.UNKNOWN_ERROR); + console.error('Invalid state.'); + } + this.state_ = SuggestAppsDialog.State.UNINITIALIZED; + + this.onDialogClosed_(result); +}; + +/** + * Utility methods and constants to record histograms. + */ +SuggestAppsDialog.Metrics = Object.freeze({ + LOAD: Object.freeze({ + SUCCEEDED: 0, + CANCELLED: 1, + FAILED: 2, + }), + + /** + * @param {SuggestAppsDialog.Metrics.LOAD} result Result of load. + */ + recordLoad: function(result) { + if (0 <= result && result < 3) + metrics.recordEnum('SuggestApps.Load', result, 3); + }, + + CLOSE_DIALOG: Object.freeze({ + UNKOWN_ERROR: 0, + ITEM_INSTALLED: 1, + USER_CANCELLED: 2, + WEBSTORE_LINK_OPENED: 3, + }), + + /** + * @param {SuggestAppsDialog.Metrics.CLOSE_DIALOG} reason Reason of closing + * dialog. + */ + recordCloseDialog: function(reason) { + if (0 <= reason && reason < 4) + metrics.recordEnum('SuggestApps.CloseDialog', reason, 4); + }, + + INSTALL: Object.freeze({ + SUCCEEDED: 0, + CANCELLED: 1, + FAILED: 2, + }), + + /** + * @param {SuggestAppsDialog.Metrics.INSTALL} result Result of installation. + */ + recordInstall: function(result) { + if (0 <= result && result < 3) + metrics.recordEnum('SuggestApps.Install', result, 3); + }, + + recordShowDialog: function() { + metrics.recordUserAction('SuggestApps.ShowDialog'); + }, + + startLoad: function() { + metrics.startInterval('SuggestApps.LoadTime'); + }, + + finishLoad: function() { + metrics.recordInterval('SuggestApps.LoadTime'); + }, +}); diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/text_measure.js b/chromium/chrome/browser/resources/file_manager/foreground/js/text_measure.js new file mode 100644 index 00000000000..a8db83ae582 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/text_measure.js @@ -0,0 +1,50 @@ +// Copyright 2013 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'; + +/** + * TextMeasure constructor. + * + * TextMeasure is a measure for text that returns the width of text. This + * class has a dummy span element. When measuring the width of text, it sets + * the text to the element and obtains the element's size by + * getBoundingClientRect. + * + * @constructor + * @param {HTMLElement} element Element that has styles of measured text. The + * width of text is measures like as it is rendered in this element. + */ +var TextMeasure = function(element) { + var doc = element.ownerDocument; + this.dummySpan_ = doc.createElement('span'); + this.dummySpan_ = doc.getElementsByTagName('body')[0]. + appendChild(this.dummySpan_); + this.dummySpan_.style.position = 'absolute'; + this.dummySpan_.style.visibility = 'hidden'; + var styles = window.getComputedStyle(element, ''); + var stylesToBeCopied = [ + 'fontSize', + 'fontStyle', + 'fontWeight', + 'fontFamily', + 'letterSpacing' + ]; + for (var i = 0; i < stylesToBeCopied.length; i++) { + this.dummySpan_.style[stylesToBeCopied[i]] = styles[stylesToBeCopied[i]]; + } + Object.seal(this); +}; + +/** + * Measures the width of text. + * + * @param {string} text Text that is measured the width. + * @return {number} Width of the specified text. + */ +TextMeasure.prototype.getWidth = function(text) { + this.dummySpan_.innerText = text; + var rect = this.dummySpan_.getBoundingClientRect(); + return rect ? rect.width : 0; +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/tree.css.js b/chromium/chrome/browser/resources/file_manager/foreground/js/tree.css.js new file mode 100644 index 00000000000..6b89acbdb86 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/tree.css.js @@ -0,0 +1,59 @@ +// Copyright 2013 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'; + +/** + * Custom version of chrome://resources/css/tree.css.js, adding support for + * inverted arrow icons. + */ +(function() { + /** + * @type {number} + * @const + */ + var WIDTH = 14; + + /** + * @type {number} + * @const + */ + var HEIGHT = WIDTH / 2 + 2; + + /** + * @type {number} + * @const + */ + var MARGIN = 1; + + /** + * @param {string} name CSS canvas identifier. + * @param {string} backgroundColor Background color. + * @param {string} strokeColor Outline color. + */ + function prepareTriangle(name, backgroundColor, strokeColor) { + var ctx = document.getCSSCanvasContext('2d', + name, + WIDTH + MARGIN * 2, + HEIGHT + MARGIN * 2); + + ctx.fillStyle = backgroundColor; + ctx.strokeStyle = strokeColor; + ctx.translate(MARGIN, MARGIN); + + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, 2); + ctx.lineTo(WIDTH / 2, HEIGHT); + ctx.lineTo(WIDTH, 2); + ctx.lineTo(WIDTH, 0); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + } + + prepareTriangle( + 'tree-triangle', 'rgba(122, 122, 122, 0.6)', 'rgba(0, 0, 0, 0)'); + prepareTriangle('tree-triangle-inverted', '#ffffff', '#ffffff'); +})(); diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/ui/breadcrumbs_controller.js b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/breadcrumbs_controller.js new file mode 100644 index 00000000000..4189ef9ea0b --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/breadcrumbs_controller.js @@ -0,0 +1,262 @@ +// Copyright 2013 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'; + +/** + * @extends cr.EventTarget + * @param {HTMLDivElement} div Div container for breadcrumbs. + * @param {MetadataCache} metadataCache To retrieve metadata. + * @param {VolumeManagerWrapper} volumeManager Volume manager. + * @constructor + */ +function BreadcrumbsController(div, metadataCache, volumeManager) { + this.bc_ = div; + this.metadataCache_ = metadataCache; + this.volumeManager_ = volumeManager; + this.entry_ = null; + + /** + * Sequence value to skip requests that are out of date. + * @type {number} + * @private + */ + this.showSequence_ = 0; + + // Register events and seql the object. + div.addEventListener('click', this.onClick_.bind(this)); +} + +/** + * Extends cr.EventTarget. + */ +BreadcrumbsController.prototype.__proto__ = cr.EventTarget.prototype; + +/** + * Shows breadcrumbs. + * + * @param {Entry} entry Target entry. + */ +BreadcrumbsController.prototype.show = function(entry) { + if (entry === this.entry_) + return; + + this.entry_ = entry; + this.bc_.hidden = false; + this.bc_.textContent = ''; + this.showSequence_++; + + var queue = new AsyncUtil.Queue(); + var entries = []; + var error = false; + + // Obtain entries from the target entry to the root. + var loop; + var resolveParent = function(inEntry, callback) { + entries.unshift(inEntry); + if (!this.volumeManager_.getLocationInfo(inEntry).isRootEntry) { + inEntry.getParent(function(parent) { + resolveParent(parent, callback); + }, function() { + error = true; + callback(); + }); + } else { + callback(); + } + }.bind(this); + queue.run(resolveParent.bind(null, entry)); + + // Override DRIVE_OTHER root to DRIVE_SHARED_WITH_ME root. + queue.run(function(callback) { + // If an error was occured, just skip. + if (error) { + callback(); + return; + } + + // If the path is not under the drive other root, it is not needed to + // override root type. + var locationInfo = this.volumeManager_.getLocationInfo(entry); + if (!locationInfo) { + error = true; + callback(); + return; + } + if (locationInfo.rootType !== RootType.DRIVE_OTHER) { + callback(); + return; + } + + // Otherwise check the metadata of the directory localted at just under + // drive other. + if (!entries[1]) { + error = true; + callback(); + return; + } + this.metadataCache_.getOne(entries[1], 'drive', function(result) { + if (result && result.sharedWithMe) + entries[0] = RootType.DRIVE_SHARED_WITH_ME; + else + entries.shift(); + callback(); + }); + }.bind(this)); + + // Update DOM element. + queue.run(function(sequence, callback) { + // Check the sequence number to skip requests that are out of date. + if (this.showSequence_ === sequence && !error) + this.updateInternal_(entries); + callback(); + }.bind(this, this.showSequence_)); +}; + +/** + * Updates the breadcrumb display. + * @param {Array.<Entry|RootType>} entries Location information of target path. + * @private + */ +BreadcrumbsController.prototype.updateInternal_ = function(entries) { + // Make elements. + var doc = this.bc_.ownerDocument; + for (var i = 0; i < entries.length; i++) { + // Add a component. + var entry = entries[i]; + var div = doc.createElement('div'); + div.className = 'breadcrumb-path'; + if (entry === RootType.DRIVE_SHARED_WITH_ME) { + div.textContent = PathUtil.getRootLabel(RootType.DRIVE_SHARED_WITH_ME); + } else { + var location = this.volumeManager_.getLocationInfo(entry); + div.textContent = (location && location.isRootEntry) ? + PathUtil.getRootLabel(entry.fullPath) : entry.name; + } + div.entry = entry; + this.bc_.appendChild(div); + + // If this is the last component, break here. + if (i === entries.length - 1) { + div.classList.add('breadcrumb-last'); + break; + } + + // Add a separator. + var separator = doc.createElement('div'); + separator.className = 'separator'; + this.bc_.appendChild(separator); + } + + this.truncate(); +}; + +/** + * Updates breadcrumbs widths in order to truncate it properly. + */ +BreadcrumbsController.prototype.truncate = function() { + if (!this.bc_.firstChild) + return; + + // Assume style.width == clientWidth (items have no margins or paddings). + + for (var item = this.bc_.firstChild; item; item = item.nextSibling) { + item.removeAttribute('style'); + item.removeAttribute('collapsed'); + } + + var containerWidth = this.bc_.clientWidth; + + var pathWidth = 0; + var currentWidth = 0; + var lastSeparator; + for (var item = this.bc_.firstChild; item; item = item.nextSibling) { + if (item.className == 'separator') { + pathWidth += currentWidth; + currentWidth = item.clientWidth; + lastSeparator = item; + } else { + currentWidth += item.clientWidth; + } + } + if (pathWidth + currentWidth <= containerWidth) + return; + if (!lastSeparator) { + this.bc_.lastChild.style.width = Math.min(currentWidth, containerWidth) + + 'px'; + return; + } + var lastCrumbSeparatorWidth = lastSeparator.clientWidth; + // Current directory name may occupy up to 70% of space or even more if the + // path is short. + var maxPathWidth = Math.max(Math.round(containerWidth * 0.3), + containerWidth - currentWidth); + maxPathWidth = Math.min(pathWidth, maxPathWidth); + + var parentCrumb = lastSeparator.previousSibling; + var collapsedWidth = 0; + if (parentCrumb && pathWidth - maxPathWidth > parentCrumb.clientWidth) { + // At least one crumb is hidden completely (or almost completely). + // Show sign of hidden crumbs like this: + // root > some di... > ... > current directory. + parentCrumb.setAttribute('collapsed', ''); + collapsedWidth = Math.min(maxPathWidth, parentCrumb.clientWidth); + maxPathWidth -= collapsedWidth; + if (parentCrumb.clientWidth != collapsedWidth) + parentCrumb.style.width = collapsedWidth + 'px'; + + lastSeparator = parentCrumb.previousSibling; + if (!lastSeparator) + return; + collapsedWidth += lastSeparator.clientWidth; + maxPathWidth = Math.max(0, maxPathWidth - lastSeparator.clientWidth); + } + + pathWidth = 0; + for (var item = this.bc_.firstChild; item != lastSeparator; + item = item.nextSibling) { + // TODO(serya): Mixing access item.clientWidth and modifying style and + // attributes could cause multiple layout reflows. + if (pathWidth + item.clientWidth <= maxPathWidth) { + pathWidth += item.clientWidth; + } else if (pathWidth == maxPathWidth) { + item.style.width = '0'; + } else if (item.classList.contains('separator')) { + // Do not truncate separator. Instead let the last crumb be longer. + item.style.width = '0'; + maxPathWidth = pathWidth; + } else { + // Truncate the last visible crumb. + item.style.width = (maxPathWidth - pathWidth) + 'px'; + pathWidth = maxPathWidth; + } + } + + currentWidth = Math.min(currentWidth, + containerWidth - pathWidth - collapsedWidth); + this.bc_.lastChild.style.width = + (currentWidth - lastCrumbSeparatorWidth) + 'px'; +}; + +/** + * Hide breadcrumbs div. + */ +BreadcrumbsController.prototype.hide = function() { + this.bc_.hidden = true; +}; + +/** + * Handle a click event on a breadcrumb element. + * @param {Event} event The click event. + * @private + */ +BreadcrumbsController.prototype.onClick_ = function(event) { + if (!event.target.classList.contains('breadcrumb-path') || + event.target.classList.contains('breadcrumb-last')) + return; + + var newEvent = new Event('pathclick'); + newEvent.entry = event.target.entry; + this.dispatchEvent(newEvent); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/ui/conflict_dialog.js b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/conflict_dialog.js new file mode 100644 index 00000000000..8aecd737228 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/conflict_dialog.js @@ -0,0 +1,132 @@ +// Copyright 2013 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'; + +/** + * Dialog to confirm the operation for conflicted file operations. + * + * @param {HTMLElement} parentNode Node to be parent for this dialog. + * @constructor + * @extends {FileManagerDialogBase} + */ +function ConflictDialog(parentNode) { + FileManagerDialogBase.call(this, parentNode); + + /** + * Callback to be called when the showing task is completed. The first + * argument is which button is pressed. The second argument is whether to + * apply all or not. + * + * @type {function(ConflictDialog.Result, boolean)} + * @private + */ + this.callback_ = null; + + /** + * Checkbox to specify whether to apply the selection to all entries or not. + * @type {HTMLElement} + * @private + */ + this.applyAllCheckbox_ = parentNode.ownerDocument.createElement('input'); + this.applyAllCheckbox_.id = 'conflict-confirm-dialog-apply-all-checkbox'; + this.applyAllCheckbox_.type = 'checkbox'; + + // Apply all line. + var applyAllLabel = parentNode.ownerDocument.createElement('label'); + applyAllLabel.textContent = str('CONFLICT_DIALOG_APPLY_TO_ALL'); + applyAllLabel.setAttribute('for', this.applyAllCheckbox_.id); + + /** + * Element of the keep both button. + * @type {HTMLElement} + * @private + */ + this.keepBothButton_ = parentNode.ownerDocument.createElement('button'); + this.keepBothButton_.textContent = str('CONFLICT_DIALOG_KEEP_BOTH'); + this.keepBothButton_.addEventListener( + 'click', + this.hideWithResult_.bind(this, ConflictDialog.Result.KEEP_BOTH)); + + /** + * Element of the replace button. + * @type {HTMLElement} + * @private + */ + this.replaceButton_ = parentNode.ownerDocument.createElement('button'); + this.replaceButton_.textContent = str('CONFLICT_DIALOG_REPLACE'); + this.replaceButton_.addEventListener( + 'click', + this.hideWithResult_.bind(this, ConflictDialog.Result.REPLACE)); + + // Buttons line. + var buttons = this.okButton_.parentNode; + buttons.insertBefore(this.applyAllCheckbox_, this.okButton_); + buttons.insertBefore(applyAllLabel, this.okButton_); + buttons.replaceChild(this.keepBothButton_, this.okButton_); + buttons.appendChild(this.replaceButton_); + + // Frame + this.frame_.id = 'conflict-confirm-dialog'; +} + +/** + * Result of conflict confirm dialogs. + * @enum {string} + * @const + */ +ConflictDialog.Result = Object.freeze({ + KEEP_BOTH: 'keepBoth', + CANCEL: 'cancel', + REPLACE: 'replace' +}); + +ConflictDialog.prototype = { + __proto__: FileManagerDialogBase.prototype +}; + +/** + * Shows the conflict confirm dialog. + * + * @param {string} fileName Filename that is conflicted. + * @param {function(ConflictDialog.Result, boolean)} callback Complete + * callbak. See also ConflictDialog#callback_. + * @return {boolean} True if the dialog can show successfully. False if the + * dialog failed to show due to an existing dialog. + */ +ConflictDialog.prototype.show = function(fileName, callback) { + if (this.callback_) + return false; + + this.callback_ = callback; + FileManagerDialogBase.prototype.showOkCancelDialog.call( + this, + '', // We dont't show the title for the dialog. + strf('CONFLICT_DIALOG_MESSAGE', fileName)); + return true; +}; + +/** + * Handles cancellation. + * @param {Event} event Click event. + * @private + */ +ConflictDialog.prototype.onCancelClick_ = function(event) { + this.hideWithResult_(ConflictDialog.Result.CANCEL); +}; + +/** + * Hides the dialog box with the result. + * @param {ConflictDialog.Result} result Result. + * @private + */ +ConflictDialog.prototype.hideWithResult_ = function(result) { + this.hide(function() { + if (!this.callback_) + return; + this.callback_(result, this.applyAllCheckbox_.checked); + this.callback_ = null; + this.applyAllCheckbox_.checked = false; + }.bind(this)); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/ui/file_manager_dialog_base.js b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/file_manager_dialog_base.js new file mode 100644 index 00000000000..63b856f087f --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/file_manager_dialog_base.js @@ -0,0 +1,122 @@ +// Copyright 2013 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'; + +/** + * This class is an extended class, to manage the status of the dialogs. + * + * @param {HTMLElement} parentNode Parent node of the dialog. + * @extends {cr.ui.dialogs.FileManagerDialogBase} + * @constructor + */ +var FileManagerDialogBase = function(parentNode) { + cr.ui.dialogs.BaseDialog.call(this, parentNode); +}; + +FileManagerDialogBase.prototype = { + __proto__: cr.ui.dialogs.BaseDialog.prototype +}; + +/** + * The FileManager object. This is used to notify events of showing or hiding + * dialog to file manager. + * + * @type {FileManager} + * @private + */ +FileManagerDialogBase.fileManager_ = null; + +/** + * Setter of FileManagerDialogBase.fileManager_. + * @param {FileManager} fileManager The fileManager object. + */ +FileManagerDialogBase.setFileManager = function(fileManager) { + FileManagerDialogBase.fileManager_ = fileManager; +}; + +/** + * The flag if any dialog is shown. True if a dialog is visible, false + * otherwise. + * @type {boolean} + */ +FileManagerDialogBase.shown = false; + +/** + * @param {string} title Title. + * @param {string} message Message. + * @param {function()} onOk Called when the OK button is pressed. + * @param {function()} onCancel Called when the cancel button is pressed. + * @return {boolean} True if the dialog can show successfully. False if the + * dialog failed to show due to an existing dialog. + */ +FileManagerDialogBase.prototype.showOkCancelDialog = function( + title, message, onOk, onCancel) { + return this.showImpl_(title, message, onOk, onCancel); +}; + +/** + * @param {string} title Title. + * @param {string} message Message. + * @param {function()} onOk Called when the OK button is pressed. + * @param {function()} onCancel Called when the cancel button is pressed. + * @return {boolean} True if the dialog can show successfully. False if the + * dialog failed to show due to an existing dialog. + * @private + */ +FileManagerDialogBase.prototype.showImpl_ = function( + title, message, onOk, onCancel) { + if (FileManagerDialogBase.shown) + return false; + + FileManagerDialogBase.shown = true; + if (FileManagerDialogBase.fileManager_) + FileManagerDialogBase.fileManager_.onDialogShownOrHidden(true); + cr.ui.dialogs.BaseDialog.prototype.showWithTitle.call( + this, title, message, onOk, onCancel, null); + + return true; +}; + +/** + * @return {boolean} True if the dialog can show successfully. False if the + * dialog failed to show due to an existing dialog. + */ +FileManagerDialogBase.prototype.showBlankDialog = function() { + return this.showImpl_('', '', null, null, null); +}; + +/** + * @param {string} title Title. + * @return {boolean} True if the dialog can show successfully. False if the + * dialog failed to show due to an existing dialog. + */ +FileManagerDialogBase.prototype.showTitleOnlyDialog = function(title) { + return this.showImpl_(title, '', null, null, null); +}; + +/** + * @param {string} title Title. + * @param {string} text Text to be shown in the dialog. + * @return {boolean} True if the dialog can show successfully. False if the + * dialog failed to show due to an existing dialog. + */ +FileManagerDialogBase.prototype.showTitleAndTextDialog = function(title, text) { + return this.showImpl_(title, text, null, null, null); +}; + +/** + * @param {function()=} opt_onHide Called when the dialog is hidden. + */ +FileManagerDialogBase.prototype.hide = function(opt_onHide) { + cr.ui.dialogs.BaseDialog.prototype.hide.call( + this, + function() { + if (opt_onHide) + opt_onHide(); + if (FileManagerDialogBase.fileManager_) + FileManagerDialogBase.fileManager_.onDialogShownOrHidden(false); + FileManagerDialogBase.shown = false; + }); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/ui/file_manager_ui.js b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/file_manager_ui.js new file mode 100644 index 00000000000..1d460f54cdb --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/file_manager_ui.js @@ -0,0 +1,190 @@ +// Copyright 2013 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 root of the file manager's view managing the DOM of Files.app. + * + * @param {HTMLElement} element Top level element of Files.app. + * @param {DialogType} dialogType Dialog type. + * @constructor. + */ +var FileManagerUI = function(element, dialogType) { + /** + * Top level element of Files.app. + * @type {HTMLElement} + * @private + */ + this.element_ = element; + + /** + * Dialog type. + * @type {DialogType} + * @private + */ + this.dialogType_ = dialogType; + + /** + * Error dialog. + * @type {ErrorDialog} + */ + this.errorDialog = null; + + /** + * Alert dialog. + * @type {cr.ui.dialogs.AlertDialog} + */ + this.alertDialog = null; + + /** + * Confirm dialog. + * @type {cr.ui.dialogs.ConfirmDialog} + */ + this.confirmDialog = null; + + /** + * Prompt dialog. + * @type {cr.ui.dialogs.PromptDialog} + */ + this.promptDialog = null; + + /** + * Share dialog. + * @type {ShareDialog} + */ + this.shareDialog = null; + + /** + * Default task picker. + * @type {DefaultActionDialog} + */ + this.defaultTaskPicker = null; + + /** + * Suggest apps dialog. + * @type {SuggestAppsDialog} + */ + this.suggestAppsDialog = null; + + /** + * Conflict dialog. + * @type {ConflictDialog} + */ + this.conflictDialog = null; + + /** + * Search box. + * @type {SearchBox} + */ + this.searchBox = null; + + /** + * File type selector in the footer. + * @type {HTMLElement} + */ + this.fileTypeSelector = null; + + /** + * OK button in the footer. + * @type {HTMLElement} + */ + this.okButton = null; + + /** + * Cancel button in the footer. + * @type {HTMLElement} + */ + this.cancelButton = null; + + Object.seal(this); + + // Initialize the header. + this.element_.querySelector('#app-name').innerText = + chrome.runtime.getManifest().name; + + // Initialize dialog type. + this.initDialogType_(); + + // Pre-populate the static localized strings. + i18nTemplate.process(this.element_.ownerDocument, loadTimeData); +}; + +/** + * Tweak the UI to become a particular kind of dialog, as determined by the + * dialog type parameter passed to the constructor. + * + * @private + */ +FileManagerUI.prototype.initDialogType_ = function() { + // Obtain elements. + var hasFooterPanel = + this.dialogType_ == DialogType.SELECT_SAVEAS_FILE || + DialogType.isFolderDialog(this.dialogType_); + + // If the footer panel exists, the buttons are placed there. Otherwise, + // the buttons are on the preview panel. + var parentPanelOfButtons = this.element_.ownerDocument.querySelector( + !hasFooterPanel ? '.preview-panel' : '.dialog-footer'); + parentPanelOfButtons.classList.add('button-panel'); + this.fileTypeSelector = parentPanelOfButtons.querySelector('.file-type'); + this.okButton = parentPanelOfButtons.querySelector('.ok'); + this.cancelButton = parentPanelOfButtons.querySelector('.cancel'); + + // Set attributes. + var okLabel = str('OPEN_LABEL'); + + switch (this.dialogType_) { + case DialogType.SELECT_UPLOAD_FOLDER: + okLabel = str('UPLOAD_LABEL'); + break; + + case DialogType.SELECT_SAVEAS_FILE: + okLabel = str('SAVE_LABEL'); + break; + + case DialogType.SELECT_FOLDER: + case DialogType.SELECT_OPEN_FILE: + case DialogType.SELECT_OPEN_MULTI_FILE: + case DialogType.FULL_PAGE: + break; + + default: + throw new Error('Unknown dialog type: ' + this.dialogType); + } + + this.okButton.textContent = okLabel; + this.element_.setAttribute('type', this.dialogType_); +}; + +/** + * Initialize the dialogs. + */ +FileManagerUI.prototype.initDialogs = function() { + // Initialize the dialog label. + var dialogs = cr.ui.dialogs; + dialogs.BaseDialog.OK_LABEL = str('OK_LABEL'); + dialogs.BaseDialog.CANCEL_LABEL = str('CANCEL_LABEL'); + var appState = window.appState || {}; + + // Create the dialog instances. + this.errorDialog = new ErrorDialog(this.element_); + this.alertDialog = new dialogs.AlertDialog(this.element_); + this.confirmDialog = new dialogs.ConfirmDialog(this.element_); + this.promptDialog = new dialogs.PromptDialog(this.element_); + this.shareDialog = new ShareDialog(this.element_); + this.defaultTaskPicker = + new cr.filebrowser.DefaultActionDialog(this.element_); + this.suggestAppsDialog = new SuggestAppsDialog( + this.element_, appState.suggestAppsDialogState || {}); + this.conflictDialog = new ConflictDialog(this.element_); +}; + +/** + * Initialize here elements, which are expensive + * or hidden in the beginning. + */ +FileManagerUI.prototype.initAdditionalUI = function() { + this.searchBox = new SearchBox(this.element_.querySelector('#search-box')); +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/ui/navigation_list.js b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/navigation_list.js new file mode 100644 index 00000000000..f471aa6c0de --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/navigation_list.js @@ -0,0 +1,386 @@ +// 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'; + +/** + * A navigation list item. + * @constructor + * @extends {HTMLLIElement} + */ +var NavigationListItem = cr.ui.define('li'); + +NavigationListItem.prototype = { + __proto__: HTMLLIElement.prototype, + get modelItem() { return this.modelItem_; } +}; + +/** + * Decorate the item. + */ +NavigationListItem.prototype.decorate = function() { + // decorate() may be called twice: from the constructor and from + // List.createItem(). This check prevents double-decorating. + if (this.className) + return; + + this.className = 'root-item'; + this.setAttribute('role', 'option'); + + this.iconDiv_ = cr.doc.createElement('div'); + this.iconDiv_.className = 'volume-icon'; + this.appendChild(this.iconDiv_); + + this.label_ = cr.doc.createElement('div'); + this.label_.className = 'root-label'; + this.appendChild(this.label_); + + cr.defineProperty(this, 'lead', cr.PropertyKind.BOOL_ATTR); + cr.defineProperty(this, 'selected', cr.PropertyKind.BOOL_ATTR); +}; + +/** + * Associate a path with this item. + * @param {NavigationModelItem} modelItem NavigationModelItem of this item. + * @param {string=} opt_deviceType The type of the device. Available iff the + * path represents removable storage. + */ +NavigationListItem.prototype.setModelItem = + function(modelItem, opt_deviceType) { + if (this.modelItem_) + console.warn('NavigationListItem.setModelItem should be called only once.'); + + this.modelItem_ = modelItem; + + var rootType = PathUtil.getRootType(modelItem.path); + this.iconDiv_.setAttribute('volume-type-icon', rootType); + if (opt_deviceType) { + this.iconDiv_.setAttribute('volume-subtype', opt_deviceType); + } + + this.label_.textContent = modelItem.label; + + if (rootType === RootType.ARCHIVE || rootType === RootType.REMOVABLE) { + this.eject_ = cr.doc.createElement('div'); + // Block other mouse handlers. + this.eject_.addEventListener( + 'mouseup', function(event) { event.stopPropagation() }); + this.eject_.addEventListener( + 'mousedown', function(event) { event.stopPropagation() }); + + this.eject_.className = 'root-eject'; + this.eject_.addEventListener('click', function(event) { + event.stopPropagation(); + cr.dispatchSimpleEvent(this, 'eject'); + }.bind(this)); + + this.appendChild(this.eject_); + } +}; + +/** + * Associate a context menu with this item. + * @param {cr.ui.Menu} menu Menu this item. + */ +NavigationListItem.prototype.maybeSetContextMenu = function(menu) { + if (!this.modelItem_.path) { + console.error('NavigationListItem.maybeSetContextMenu must be called ' + + 'after setModelItem().'); + return; + } + + var isRoot = PathUtil.isRootPath(this.modelItem_.path); + var rootType = PathUtil.getRootType(this.modelItem_.path); + // The context menu is shown on the following items: + // - Removable and Archive volumes + // - Folder shortcuts + if (!isRoot || + (rootType != RootType.DRIVE && rootType != RootType.DOWNLOADS)) + cr.ui.contextMenuHandler.setContextMenu(this, menu); +}; + +/** + * A navigation list. + * @constructor + * @extends {cr.ui.List} + */ +function NavigationList() { +} + +/** + * NavigationList inherits cr.ui.List. + */ +NavigationList.prototype = { + __proto__: cr.ui.List.prototype, + + set dataModel(dataModel) { + if (!this.onListContentChangedBound_) + this.onListContentChangedBound_ = this.onListContentChanged_.bind(this); + + if (this.dataModel_) { + this.dataModel_.removeEventListener( + 'change', this.onListContentChangedBound_); + this.dataModel_.removeEventListener( + 'permuted', this.onListContentChangedBound_); + } + + var parentSetter = cr.ui.List.prototype.__lookupSetter__('dataModel'); + parentSetter.call(this, dataModel); + + // This must be placed after the parent method is called, in order to make + // it sure that the list was changed. + dataModel.addEventListener('change', this.onListContentChangedBound_); + dataModel.addEventListener('permuted', this.onListContentChangedBound_); + }, + + get dataModel() { + return this.dataModel_; + }, + + // TODO(yoshiki): Add a setter of 'directoryModel'. +}; + +/** + * @param {HTMLElement} el Element to be DirectoryItem. + * @param {VolumeManagerWrapper} volumeManager The VolumeManager of the system. + * @param {DirectoryModel} directoryModel Current DirectoryModel. + * folders. + */ +NavigationList.decorate = function(el, volumeManager, directoryModel) { + el.__proto__ = NavigationList.prototype; + el.decorate(volumeManager, directoryModel); +}; + +/** + * @param {VolumeManagerWrapper} volumeManager The VolumeManager of the system. + * @param {DirectoryModel} directoryModel Current DirectoryModel. + */ +NavigationList.prototype.decorate = function(volumeManager, directoryModel) { + cr.ui.List.decorate(this); + this.__proto__ = NavigationList.prototype; + + this.directoryModel_ = directoryModel; + this.volumeManager_ = volumeManager; + this.selectionModel = new cr.ui.ListSingleSelectionModel(); + + this.directoryModel_.addEventListener('directory-changed', + this.onCurrentDirectoryChanged_.bind(this)); + this.selectionModel.addEventListener( + 'change', this.onSelectionChange_.bind(this)); + this.selectionModel.addEventListener( + 'beforeChange', this.onBeforeSelectionChange_.bind(this)); + + this.scrollBar_ = new ScrollBar(); + this.scrollBar_.initialize(this.parentNode, this); + + // Overriding default role 'list' set by cr.ui.List.decorate() to 'listbox' + // role for better accessibility on ChromeOS. + this.setAttribute('role', 'listbox'); + + var self = this; + this.itemConstructor = function(modelItem) { + return self.renderRoot_(modelItem); + }; +}; + +/** + * This overrides cr.ui.List.measureItem(). + * In the method, a temporary element is added/removed from the list, and we + * need to omit animations for such temporary items. + * + * @param {ListItem=} opt_item The list item to be measured. + * @return {{height: number, marginTop: number, marginBottom:number, + * width: number, marginLeft: number, marginRight:number}} Size. + * @override + */ +NavigationList.prototype.measureItem = function(opt_item) { + this.measuringTemporaryItemNow_ = true; + var result = cr.ui.List.prototype.measureItem.call(this, opt_item); + this.measuringTemporaryItemNow_ = false; + return result; +}; + +/** + * Creates an element of a navigation list. This method is called from + * cr.ui.List internally. + * + * @param {NavigationModelItem} modelItem NavigationModelItem to be rendered. + * @return {NavigationListItem} Rendered element. + * @private + */ +NavigationList.prototype.renderRoot_ = function(modelItem) { + var item = new NavigationListItem(); + var volumeInfo = + PathUtil.isRootPath(modelItem.path) && + this.volumeManager_.getVolumeInfo(modelItem.path); + item.setModelItem(modelItem, volumeInfo && volumeInfo.deviceType); + + var handleClick = function() { + if (item.selected && + modelItem.path !== this.directoryModel_.getCurrentDirPath()) { + metrics.recordUserAction('FolderShortcut.Navigate'); + this.changeDirectory_(modelItem); + } + }.bind(this); + item.addEventListener('click', handleClick); + + var handleEject = function() { + var unmountCommand = cr.doc.querySelector('command#unmount'); + // Let's make sure 'canExecute' state of the command is properly set for + // the root before executing it. + unmountCommand.canExecuteChange(item); + unmountCommand.execute(item); + }; + item.addEventListener('eject', handleEject); + + if (this.contextMenu_) + item.maybeSetContextMenu(this.contextMenu_); + + return item; +}; + +/** + * Changes the current directory to the given path. + * If the given path is not found, a 'shortcut-target-not-found' event is + * fired. + * + * @param {NavigationModelItem} modelItem Directory to be chagned to. + * @private + */ +NavigationList.prototype.changeDirectory_ = function(modelItem) { + var onErrorCallback = function(error) { + if (error.code === FileError.NOT_FOUND_ERR) + this.dataModel.onItemNotFoundError(modelItem); + }.bind(this); + + this.directoryModel_.changeDirectory(modelItem.path, onErrorCallback); +}; + +/** + * Sets a context menu. Context menu is enabled only on archive and removable + * volumes as of now. + * + * @param {cr.ui.Menu} menu Context menu. + */ +NavigationList.prototype.setContextMenu = function(menu) { + this.contextMenu_ = menu; + + for (var i = 0; i < this.dataModel.length; i++) { + this.getListItemByIndex(i).maybeSetContextMenu(this.contextMenu_); + } +}; + +/** + * Selects the n-th item from the list. + * + * @param {number} index Item index. + * @return {boolean} True for success, otherwise false. + */ +NavigationList.prototype.selectByIndex = function(index) { + if (index < 0 || index > this.dataModel.length - 1) + return false; + + var newModelItem = this.dataModel.item(index); + var newPath = newModelItem.path; + if (!newPath) + return false; + + // Prevents double-moving to the current directory. + // eg. When user clicks the item, changing directory has already been done in + // click handler. + var entry = this.directoryModel_.getCurrentDirEntry(); + if (entry && entry.fullPath == newPath) + return false; + + metrics.recordUserAction('FolderShortcut.Navigate'); + this.changeDirectory_(newModelItem); + return true; +}; + +/** + * Handler before root item change. + * @param {Event} event The event. + * @private + */ +NavigationList.prototype.onBeforeSelectionChange_ = function(event) { + if (event.changes.length == 1 && !event.changes[0].selected) + event.preventDefault(); +}; + +/** + * Handler for root item being clicked. + * @param {Event} event The event. + * @private + */ +NavigationList.prototype.onSelectionChange_ = function(event) { + // This handler is invoked even when the navigation list itself changes the + // selection. In such case, we shouldn't handle the event. + if (this.dontHandleSelectionEvent_) + return; + + this.selectByIndex(this.selectionModel.selectedIndex); +}; + +/** + * Invoked when the current directory is changed. + * @param {Event} event The event. + * @private + */ +NavigationList.prototype.onCurrentDirectoryChanged_ = function(event) { + this.selectBestMatchItem_(); +}; + +/** + * Invoked when the content in the data model is changed. + * @param {Event} event The event. + * @private + */ +NavigationList.prototype.onListContentChanged_ = function(event) { + this.selectBestMatchItem_(); +}; + +/** + * Synchronizes the volume list selection with the current directory, after + * it is changed outside of the volume list. + * @private + */ +NavigationList.prototype.selectBestMatchItem_ = function() { + var entry = this.directoryModel_.getCurrentDirEntry(); + var path = entry && entry.fullPath; + if (!path) + return; + + // (1) Select the nearest parent directory (including the shortcut folders). + var bestMatchIndex = -1; + var bestMatchSubStringLen = 0; + for (var i = 0; i < this.dataModel.length; i++) { + var itemPath = this.dataModel.item(i).path; + if (path.indexOf(itemPath) == 0) { + if (bestMatchSubStringLen < itemPath.length) { + bestMatchIndex = i; + bestMatchSubStringLen = itemPath.length; + } + } + } + if (bestMatchIndex != -1) { + // Not to invoke the handler of this instance, sets the guard. + this.dontHandleSelectionEvent_ = true; + this.selectionModel.selectedIndex = bestMatchIndex; + this.dontHandleSelectionEvent_ = false; + return; + } + + // (2) Selects the volume of the current directory. + var newRootPath = PathUtil.getRootPath(path); + for (var i = 0; i < this.dataModel.length; i++) { + var itemPath = this.dataModel.item(i).path; + if (PathUtil.getRootPath(itemPath) == newRootPath) { + // Not to invoke the handler of this instance, sets the guard. + this.dontHandleSelectionEvent_ = true; + this.selectionModel.selectedIndex = i; + this.dontHandleSelectionEvent_ = false; + return; + } + } +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/ui/preview_panel.js b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/preview_panel.js new file mode 100644 index 00000000000..a8d0db482df --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/preview_panel.js @@ -0,0 +1,518 @@ +// Copyright (c) 2013 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'; + +/** + * PreviewPanel UI class. + * @param {HTMLElement} element DOM Element of preview panel. + * @param {PreviewPanel.VisibilityType} visibilityType Initial value of the + * visibility type. + * @param {MetadataCache} metadataCache Metadata cache. + * @param {VolumeManagerWrapper} volumeManager Volume manager. + * @constructor + * @extends {cr.EventTarget} + */ +var PreviewPanel = function(element, + visibilityType, + metadataCache, + volumeManager) { + /** + * The cached height of preview panel. + * @type {number} + * @private + */ + this.height_ = 0; + + /** + * Visibility type of the preview panel. + * @type {PreviewPanel.VisiblityType} + * @private + */ + this.visibilityType_ = visibilityType; + + /** + * Current entry to be displayed. + * @type {Entry} + * @private + */ + this.currentEntry_ = null; + + /** + * Dom element of the preview panel. + * @type {HTMLElement} + * @private + */ + this.element_ = element; + + /** + * @type {BreadcrumbsController} + */ + this.breadcrumbs = new BreadcrumbsController( + element.querySelector('#search-breadcrumbs'), + metadataCache, + volumeManager); + + /** + * @type {PreviewPanel.Thumbnails} + */ + this.thumbnails = new PreviewPanel.Thumbnails( + element.querySelector('.preview-thumbnails'), metadataCache); + + /** + * @type {HTMLElement} + * @private + */ + this.summaryElement_ = element.querySelector('.preview-summary'); + + /** + * @type {PreviewPanel.CalculatingSizeLabel} + * @private + */ + this.calculatingSizeLabel_ = new PreviewPanel.CalculatingSizeLabel( + this.summaryElement_.querySelector('.calculating-size')); + + /** + * @type {HTMLElement} + * @private + */ + this.previewText_ = element.querySelector('.preview-text'); + + /** + * FileSelection to be displayed. + * @type {FileSelection} + * @private + */ + this.selection_ = {entries: [], computeBytes: function() {}}; + + /** + * Sequence value that is incremented by every selection update and is used to + * check if the callback is up to date or not. + * @type {number} + * @private + */ + this.sequence_ = 0; + + /** + * @type {VolumeManager} + * @private + */ + this.volumeManager_ = volumeManager; + + cr.EventTarget.call(this); +}; + +/** + * Name of PreviewPanels's event. + * @enum {string} + * @const + */ +PreviewPanel.Event = Object.freeze({ + // Event to be triggered at the end of visibility change. + VISIBILITY_CHANGE: 'visibilityChange' +}); + +/** + * Visibility type of the preview panel. + */ +PreviewPanel.VisibilityType = Object.freeze({ + // Preview panel always shows. + ALWAYS_VISIBLE: 'alwaysVisible', + // Preview panel shows when the selection property are set. + AUTO: 'auto', + // Preview panel does not show. + ALWAYS_HIDDEN: 'alwaysHidden' +}); + +/** + * @private + */ +PreviewPanel.Visibility_ = Object.freeze({ + VISIBLE: 'visible', + HIDING: 'hiding', + HIDDEN: 'hidden' +}); + +PreviewPanel.prototype = { + __proto__: cr.EventTarget.prototype, + + /** + * Setter for the current entry. + * @param {Entry} entry New entry. + */ + set currentEntry(entry) { + if (util.isSameEntry(this.currentEntry_, entry)) + return; + this.currentEntry_ = entry; + this.updateVisibility_(); + this.updatePreviewArea_(); + }, + + /** + * Setter for the visibility type. + * @param {PreviewPanel.VisibilityType} visibilityType New value of visibility + * type. + */ + set visibilityType(visibilityType) { + this.visibilityType_ = visibilityType; + this.updateVisibility_(); + }, + + get visible() { + return this.element_.getAttribute('visibility') == + PreviewPanel.Visibility_.VISIBLE; + }, + + /** + * Obtains the height of preview panel. + * @return {number} Height of preview panel. + */ + get height() { + this.height_ = this.height_ || this.element_.clientHeight; + return this.height_; + } +}; + +/** + * Initializes the element. + */ +PreviewPanel.prototype.initialize = function() { + this.element_.addEventListener('webkitTransitionEnd', + this.onTransitionEnd_.bind(this)); + this.updatePreviewArea_(); + this.updateVisibility_(); +}; + +/** + * Apply the selection and update the view of the preview panel. + * @param {FileSelection} selection Selection to be applied. + */ +PreviewPanel.prototype.setSelection = function(selection) { + this.sequence_++; + this.selection_ = selection; + this.updateVisibility_(); + // If the previw panel is hiding, does not update the current view. + if (this.visible) + this.updatePreviewArea_(); +}; + +/** + * Update the visibility of the preview panel. + * @private + */ +PreviewPanel.prototype.updateVisibility_ = function() { + // Get the new visibility value. + var visibility = this.element_.getAttribute('visibility'); + var newVisible = null; + switch (this.visibilityType_) { + case PreviewPanel.VisibilityType.ALWAYS_VISIBLE: + newVisible = true; + break; + case PreviewPanel.VisibilityType.AUTO: + newVisible = + this.selection_.entries.length !== 0 || + (this.currentEntry_ && + !this.volumeManager_.getLocationInfo( + this.currentEntry_).isRootEntry); + break; + case PreviewPanel.VisibilityType.ALWAYS_HIDDEN: + newVisible = false; + break; + default: + console.error('Invalid visibilityType.'); + return; + } + + // If the visibility has been already the new value, just return. + if ((visibility == PreviewPanel.Visibility_.VISIBLE && newVisible) || + (visibility == PreviewPanel.Visibility_.HIDDEN && !newVisible)) + return; + + // Set the new visibility value. + if (newVisible) { + this.element_.setAttribute('visibility', PreviewPanel.Visibility_.VISIBLE); + cr.dispatchSimpleEvent(this, PreviewPanel.Event.VISIBILITY_CHANGE); + } else { + this.element_.setAttribute('visibility', PreviewPanel.Visibility_.HIDING); + } +}; + +/** + * Update the text in the preview panel. + * + * @param {boolean} breadCrumbsVisible Whether the bread crumbs is visible or + * not. + * @private + */ +PreviewPanel.prototype.updatePreviewArea_ = function(breadCrumbsVisible) { + var selection = this.selection_; + + // Update thumbnails. + this.thumbnails.selection = selection.totalCount !== 0 ? + selection : {entries: [this.currentEntry_]}; + + // Check if the breadcrumb list should show instead on the preview text. + var entry; + if (this.selection_.totalCount == 1) + entry = this.selection_.entries[0]; + else if (this.selection_.totalCount == 0) + entry = this.currentEntry_; + + if (entry) { + this.breadcrumbs.show(entry); + this.calculatingSizeLabel_.hidden = true; + this.previewText_.textContent = ''; + return; + } + this.breadcrumbs.hide(); + + // Obtains the preview text. + var text; + 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); + + // Obtains the size of files. + this.calculatingSizeLabel_.hidden = selection.bytesKnown; + if (selection.bytesKnown && selection.showBytes) + text += ', ' + util.bytesToString(selection.bytes); + + // Set the preview text to the element. + this.previewText_.textContent = text; + + // Request the byte calculation if needed. + if (!selection.bytesKnown) { + this.selection_.computeBytes(function(sequence) { + // Selection has been already updated. + if (this.sequence_ != sequence) + return; + this.updatePreviewArea_(); + }.bind(this, this.sequence_)); + } +}; + +/** + * Event handler to be called at the end of hiding transition. + * @param {Event} event The webkitTransitionEnd event. + * @private + */ +PreviewPanel.prototype.onTransitionEnd_ = function(event) { + if (event.target != this.element_ || event.propertyName != 'opacity') + return; + var visibility = this.element_.getAttribute('visibility'); + if (visibility != PreviewPanel.Visibility_.HIDING) + return; + this.element_.setAttribute('visibility', PreviewPanel.Visibility_.HIDDEN); + cr.dispatchSimpleEvent(this, PreviewPanel.Event.VISIBILITY_CHANGE); +}; + +/** + * Animating label that is shown during the bytes of selection entries is being + * calculated. + * + * This label shows dots and varying the number of dots every + * CalculatingSizeLabel.PERIOD milliseconds. + * @param {HTMLElement} element DOM element of the label. + * @constructor + */ +PreviewPanel.CalculatingSizeLabel = function(element) { + this.element_ = element; + this.count_ = 0; + this.intervalID_ = null; + Object.seal(this); +}; + +/** + * Time period in milliseconds. + * @const {number} + */ +PreviewPanel.CalculatingSizeLabel.PERIOD = 500; + +PreviewPanel.CalculatingSizeLabel.prototype = { + /** + * Set visibility of the label. + * When it is displayed, the text is animated. + * @param {boolean} hidden Whether to hide the label or not. + */ + set hidden(hidden) { + this.element_.hidden = hidden; + if (!hidden) { + if (this.intervalID_ != null) + return; + this.count_ = 2; + this.intervalID_ = + setInterval(this.onStep_.bind(this), + PreviewPanel.CalculatingSizeLabel.PERIOD); + this.onStep_(); + } else { + if (this.intervalID_ == null) + return; + clearInterval(this.intervalID_); + this.intervalID_ = null; + } + } +}; + +/** + * Increments the counter and updates the number of dots. + * @private + */ +PreviewPanel.CalculatingSizeLabel.prototype.onStep_ = function() { + var text = str('CALCULATING_SIZE'); + for (var i = 0; i < ~~(this.count_ / 2) % 4; i++) { + text += '.'; + } + this.element_.textContent = text; + this.count_++; +}; + +/** + * Thumbnails on the preview panel. + * + * @param {HTMLElement} element DOM Element of thumbnail container. + * @param {MetadataCache} metadataCache MetadataCache. + * @constructor + */ +PreviewPanel.Thumbnails = function(element, metadataCache) { + this.element_ = element; + this.metadataCache_ = metadataCache; + this.sequence_ = 0; + Object.seal(this); +}; + +/** + * Maximum number of thumbnails. + * @const {number} + */ +PreviewPanel.Thumbnails.MAX_THUMBNAIL_COUNT = 4; + +/** + * Edge length of the thumbnail square. + * @const {number} + */ +PreviewPanel.Thumbnails.THUMBNAIL_SIZE = 35; + +/** + * Longer edge length of zoomed thumbnail rectangle. + * @const {number} + */ +PreviewPanel.Thumbnails.ZOOMED_THUMBNAIL_SIZE = 200; + +PreviewPanel.Thumbnails.prototype = { + /** + * Sets entries to be displayed in the view. + * @param {Array.<Entry>} value Entries. + */ + set selection(value) { + this.sequence_++; + this.loadThumbnails_(value); + } +}; + +/** + * Loads thumbnail images. + * @param {FileSelection} selection Selection containing entries that are + * sources of images. + * @private + */ +PreviewPanel.Thumbnails.prototype.loadThumbnails_ = function(selection) { + var entries = selection.entries; + this.element_.classList.remove('has-zoom'); + this.element_.innerText = ''; + var clickHandler = selection.tasks && + selection.tasks.executeDefault.bind(selection.tasks); + var length = Math.min(entries.length, + PreviewPanel.Thumbnails.MAX_THUMBNAIL_COUNT); + for (var i = 0; i < length; i++) { + // Create a box. + var box = this.element_.ownerDocument.createElement('div'); + box.style.zIndex = PreviewPanel.Thumbnails.MAX_THUMBNAIL_COUNT + 1 - i; + + // Load the image. + if (entries[i]) { + FileGrid.decorateThumbnailBox(box, + entries[i], + this.metadataCache_, + ThumbnailLoader.FillMode.FILL, + FileGrid.ThumbnailQuality.LOW, + i == 0 && length == 1 && + this.setZoomedImage_.bind(this)); + } + + // Register the click handler. + if (clickHandler) + box.addEventListener('click', clickHandler); + + // Append + this.element_.appendChild(box); + } +}; + +/** + * Create the zoomed version of image and set it to the DOM element to show the + * zoomed image. + * + * @param {Image} image Image to be source of the zoomed image. + * @param {transform} transform Transformation to be applied to the image. + * @private + */ +PreviewPanel.Thumbnails.prototype.setZoomedImage_ = function(image, transform) { + if (!image) + return; + var width = image.width || 0; + var height = image.height || 0; + if (width == 0 || + height == 0 || + (width < PreviewPanel.Thumbnails.THUMBNAIL_SIZE * 2 && + height < PreviewPanel.Thumbnails.THUMBNAIL_SIZE * 2)) + return; + + var scale = Math.min(1, + PreviewPanel.Thumbnails.ZOOMED_THUMBNAIL_SIZE / + Math.max(width, height)); + var imageWidth = ~~(width * scale); + var imageHeight = ~~(height * scale); + var zoomedImage = this.element_.ownerDocument.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 IMAGE + // scale it smoothly. + var INTERMEDIATE_SCALE = 3; + var canvas = this.element_.ownerDocument.createElement('canvas'); + canvas.width = imageWidth * INTERMEDIATE_SCALE; + canvas.height = imageHeight * INTERMEDIATE_SCALE; + var ctx = canvas.getContext('2d'); + ctx.drawImage(image, 0, 0, canvas.width, canvas.height); + // Using bigger than default compression reduces image size by + // several times. Quality degradation compensated by greater resolution. + zoomedImage.src = canvas.toDataURL('image/jpeg', 0.6); + } else { + zoomedImage.src = image.src; + } + + var boxWidth = Math.max(PreviewPanel.Thumbnails.THUMBNAIL_SIZE, imageWidth); + var boxHeight = Math.max(PreviewPanel.Thumbnails.THUMBNAIL_SIZE, imageHeight); + if (transform && transform.rotate90 % 2 == 1) { + var t = boxWidth; + boxWidth = boxHeight; + boxHeight = t; + } + + util.applyTransform(zoomedImage, transform); + + var zoomedBox = this.element_.ownerDocument.createElement('div'); + zoomedBox.className = 'popup'; + zoomedBox.style.width = boxWidth + 'px'; + zoomedBox.style.height = boxHeight + 'px'; + zoomedBox.appendChild(zoomedImage); + + this.element_.appendChild(zoomedBox); + this.element_.classList.add('has-zoom'); + return; +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/ui/progress_center_panel.js b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/progress_center_panel.js new file mode 100644 index 00000000000..bcfe6d8e747 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/progress_center_panel.js @@ -0,0 +1,329 @@ +// Copyright 2013 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'; + +/** + * Progress center panel. + * + * @param {HTMLElement} element DOM Element of the process center panel. + * @constructor + */ +var ProgressCenterPanel = function(element) { + /** + * Root element of the progress center. + * @type {!HTMLElement} + * @private + */ + this.element_ = element; + + /** + * Open view containing multiple progress items. + * @type {!HTMLElement} + * @private + */ + this.openView_ = this.element_.querySelector('#progress-center-open-view'); + + /** + * Close view that is a summarized progress item. + * @type {!HTMLElement} + * @private + */ + this.closeView_ = this.element_.querySelector('#progress-center-close-view'); + + /** + * Toggle animation rule of the progress center. + * @type {!CSSKeyFrameRule} + * @private + */ + this.toggleAnimation_ = ProgressCenterPanel.getToggleAnimation_( + element.ownerDocument); + + /** + * Reset is requested but it is pending until the transition of progress bar + * is complete. + * @type {boolean} + * @private + */ + this.resetRequested_ = false; + + /** + * Callback to becalled with the ID of the progress item when the cancel + * button is clicked. + */ + this.cancelCallback = null; + + Object.seal(this); + + // Register event handlers. + element.addEventListener('click', this.onClick_.bind(this)); + element.addEventListener( + 'webkitAnimationEnd', this.onToggleAnimationEnd_.bind(this)); + element.addEventListener( + 'webkitTransitionEnd', this.onItemTransitionEnd_.bind(this)); +}; + +/** + * Updates attributes of the item element. + * @param {!HTMLElement} element Element to be updated. + * @param {!ProgressCenterItem} item Progress center item. + * @private + */ +ProgressCenterPanel.updateItemElement_ = function(element, item) { + // Sets element attributes. + element.setAttribute('data-progress-id', item.id); + element.classList.toggle('error', item.state === ProgressItemState.ERROR); + element.classList.toggle('cancelable', item.cancelable); + + // Only when the previousWidthRate is not NaN (when style width is already + // set) and the progress rate increases, we use transition animation. + var previousWidthRate = + parseInt(element.querySelector('.progress-track').style.width); + var targetWidthRate = item.progressRateInPercent; + var animation = !isNaN(previousWidthRate) && + previousWidthRate < targetWidthRate; + if (item.state === ProgressItemState.COMPLETED && animation) { + // The attribute pre-complete means that the actual operation is already + // done but the UI transition of progress bar is not complete. + element.setAttribute('pre-complete', ''); + } else { + element.querySelector('label').textContent = item.message; + } + + // To commit the property change and to trigger the transition even if the + // change is done synchronously, assign the width value asynchronously. + var updateTrackWidth = function() { + var track = element.querySelector('.progress-track'); + track.classList.toggle('animated', animation); + track.style.width = targetWidthRate + '%'; + track.hidden = false; + }; + if (animation) + setTimeout(updateTrackWidth); + else + updateTrackWidth(); +}; + +/** + * Obtains the toggle animation keyframes rule from the document. + * @param {HTMLDocument} document Document containing the rule. + * @return {CSSKeyFrameRules} Animation rule. + * @private + */ +ProgressCenterPanel.getToggleAnimation_ = function(document) { + for (var i = 0; i < document.styleSheets.length; i++) { + var styleSheet = document.styleSheets[i]; + for (var j = 0; j < styleSheet.cssRules.length; j++) { + var rule = styleSheet.cssRules[j]; + if (rule.type === CSSRule.WEBKIT_KEYFRAMES_RULE && + rule.name === 'progress-center-toggle') { + return rule; + } + } + } + throw new Error('The progress-center-toggle rules is not found.'); +}; + +/** + * Updates an item to the progress center panel. + * @param {!ProgressCenterItem} item Item including new contents. + */ +ProgressCenterPanel.prototype.updateItem = function(item) { + // If reset is requested, force to reset. + if (this.resetRequested_) + this.reset(true); + + var itemElement = this.getItemElement_(item.id); + + // Check whether the item should be displayed or not by referring its state. + switch (item.state) { + // Should show the item. + case ProgressItemState.PROGRESSING: + case ProgressItemState.ERROR: + // If the item has not been added yet, create a new element and add it. + if (!itemElement) { + itemElement = this.createNewItemElement_(); + this.openView_.insertBefore(itemElement, this.openView_.firstNode); + } + + // Update the element by referring the item model. + ProgressCenterPanel.updateItemElement_(itemElement, item); + this.element_.hidden = false; + break; + + // Should not show the item. + case ProgressItemState.COMPLETED: + case ProgressItemState.CANCELED: + // If itemElement is not shown, just break. + if (!itemElement) + break; + + // If the item is complete state, once update it because it may turn to + // have the pre-complete attribute. + if (item.state === ProgressItemState.COMPLETED) + ProgressCenterPanel.updateItemElement_(itemElement, item); + + // If the item has the pre-complete attribute, keep showing it. Otherwise, + // just remove it. + if (item.state !== ProgressItemState.COMPLETED || + !itemElement.hasAttribute('pre-complete')) { + this.openView_.removeChild(itemElement); + } + break; + } +}; + +/** + * Updates close showing summarized item. + * @param {!ProgressCenterItem} summarizedItem Item to be displayed in the close + * view. + */ +ProgressCenterPanel.prototype.updateCloseView = function(summarizedItem) { + this.closeView_.classList.toggle('single', !summarizedItem.summarized); + ProgressCenterPanel.updateItemElement_(this.closeView_, summarizedItem); +}; + +/** + * Remove all the items. + * @param {boolean=} opt_force True if we force to reset and do not wait the + * transition of progress bar. False otherwise. False is default. + */ +ProgressCenterPanel.prototype.reset = function(opt_force) { + if (!opt_force && this.element_.querySelector('[pre-complete]')) { + this.resetRequested_ = true; + return; + } + + // Clear the flag. + this.resetRequested_ = false; + + // Clear the all compete item. + this.openView_.innerHTML = ''; + + // Clear track width of close view. + this.closeView_.querySelector('.progress-track').style.width = ''; + + // Hide the progress center. + this.element_.hidden = true; + this.closeView_.querySelector('.progress-track').hidden = true; + this.element_.classList.remove('opened'); +}; + +/** + * Gets an item element having the specified ID. + * @param {string} id progress item ID. + * @return {HTMLElement} Item element having the ID. + * @private + */ +ProgressCenterPanel.prototype.getItemElement_ = function(id) { + var query = 'li[data-progress-id="' + id + '"]'; + return this.openView_.querySelector(query); +}; + +/** + * Creates an item element. + * @return {HTMLElement} Created item element. + * @private + */ +ProgressCenterPanel.prototype.createNewItemElement_ = function() { + var label = this.element_.ownerDocument.createElement('label'); + label.className = 'label'; + + var progressBarIndicator = this.element_.ownerDocument.createElement('div'); + progressBarIndicator.className = 'progress-track'; + + var progressBar = this.element_.ownerDocument.createElement('div'); + progressBar.className = 'progress-bar'; + progressBar.appendChild(progressBarIndicator); + + var cancelButton = this.element_.ownerDocument.createElement('button'); + cancelButton.className = 'cancel'; + cancelButton.setAttribute('tabindex', '-1'); + + var progressFrame = this.element_.ownerDocument.createElement('div'); + progressFrame.className = 'progress-frame'; + progressFrame.appendChild(progressBar); + progressFrame.appendChild(cancelButton); + + var itemElement = this.element_.ownerDocument.createElement('li'); + itemElement.appendChild(label); + itemElement.appendChild(progressFrame); + + return itemElement; +}; + +/** + * Handles the animation end event of the progress center. + * @param {Event} event Animation end event. + * @private + */ +ProgressCenterPanel.prototype.onToggleAnimationEnd_ = function(event) { + // Transition end of the root element's height. + if (event.target === this.element_ && + event.animationName === 'progress-center-toggle') { + this.element_.classList.remove('animated'); + return; + } +}; + +/** + * Handles the transition end event of items. + * @param {Event} event Transition end event. + * @private + */ +ProgressCenterPanel.prototype.onItemTransitionEnd_ = function(event) { + var itemElement = event.target.parentNode.parentNode.parentNode; + if (!itemElement.hasAttribute('pre-complete') || + event.propertyName !== 'width') + return; + if (itemElement !== this.closeView_) + this.openView_.removeChild(itemElement); + itemElement.removeAttribute('pre-complete'); + + if (this.resetRequested_) + this.reset(); +}; + +/** + * Handles the click event. + * @param {Event} event Click event. + * @private + */ +ProgressCenterPanel.prototype.onClick_ = function(event) { + // Toggle button. + if (event.target.classList.contains('toggle') && + (!this.closeView_.classList.contains('single') || + this.element_.classList.contains('opened'))) { + + // If the progress center has already animated, just return. + if (this.element_.classList.contains('animated')) + return; + + // Obtains current and target height. + var currentHeight; + var targetHeight; + if (this.element_.classList.contains('opened')) { + currentHeight = this.openView_.getBoundingClientRect().height; + targetHeight = this.closeView_.getBoundingClientRect().height; + } else { + currentHeight = this.closeView_.getBoundingClientRect().height; + targetHeight = this.openView_.getBoundingClientRect().height; + } + + // Set styles for animation. + this.toggleAnimation_.cssRules[0].style.height = currentHeight + 'px'; + this.toggleAnimation_.cssRules[1].style.height = targetHeight + 'px'; + this.element_.classList.add('animated'); + this.element_.classList.toggle('opened'); + return; + } + + // Cancel button. + if (this.cancelCallback) { + var id = event.target.classList.contains('toggle') ? + this.closeView_.getAttribute('data-progress-id') : + event.target.parentNode.parentNode.getAttribute('data-progress-id'); + this.cancelCallback(id); + } +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/ui/search_box.js b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/search_box.js new file mode 100644 index 00000000000..30a7ea73faf --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/search_box.js @@ -0,0 +1,202 @@ +// Copyright 2013 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'; + +/** + * Search box. + * + * @param {element} element Root element of the search box. + * @constructor + */ +function SearchBox(element) { + /** + * Autocomplete List. + * @type {AutocompleteList} + */ + this.autocompleteList = new SearchBox.AutocompleteList(element.ownerDocument); + + /** + * Root element of the search box. + * @type {HTMLElement} + */ + this.element = element; + + /** + * Text input of the search box. + * @type {HTMLElement} + */ + this.inputElement = element.querySelector('input'); + + /** + * Clear button of the search box. + * @type {HTMLElement} + */ + this.clearButton = element.querySelector('.clear'); + + /** + * Text measure. + * @type {TextMeasure} + * @private + */ + this.textMeasure_ = new TextMeasure(this.inputElement); + + Object.freeze(this); + + // Register events. + this.inputElement.addEventListener('input', this.updateStyles_.bind(this)); + this.inputElement.addEventListener('keydown', this.onKeyDown_.bind(this)); + this.inputElement.addEventListener('focus', this.onFocus_.bind(this)); + this.inputElement.addEventListener('blur', this.onBlur_.bind(this)); + element.querySelector('.icon').addEventListener( + 'click', this.onIconClick_.bind(this)); + element.parentNode.appendChild(this.autocompleteList); +} + +/** + * Autocomplete list for search box. + * @param {HTMLDocument} document Document. + * @constructor + */ +SearchBox.AutocompleteList = function(document) { + var self = cr.ui.AutocompleteList.call(this); + self.__proto__ = SearchBox.AutocompleteList.prototype; + self.id = 'autocomplete-list'; + self.autoExpands = true; + self.itemConstructor = SearchBox.AutocompleteListItem_.bind(null, document); + self.addEventListener('mouseover', self.onMouseOver_.bind(self)); + return self; +}; + +SearchBox.AutocompleteList.prototype = { + __proto__: cr.ui.AutocompleteList.prototype +}; + +/** + * Do nothing when a suggestion is selected. + * @override + */ +SearchBox.AutocompleteList.prototype.handleSelectedSuggestion = function() {}; + +/** + * Change the selection by a mouse over instead of just changing the + * color of moused over element with :hover in CSS. Here's why: + * + * 1) The user selects an item A with up/down keys (item A is highlighted) + * 2) Then the user moves the cursor to another item B + * + * If we just change the color of moused over element (item B), both + * the item A and B are highlighted. This is bad. We should change the + * selection so only the item B is highlighted. + * + * @param {Event} event Event. + * @private + */ +SearchBox.AutocompleteList.prototype.onMouseOver_ = function(event) { + if (event.target.itemInfo) + this.selectedItem = event.target.itemInfo; +}; + +/** + * ListItem element for autocomple. + * + * @param {HTMLDocument} document Document. + * @param {Object} item An object representing a suggestion. + * @constructor + * @private + */ +SearchBox.AutocompleteListItem_ = function(document, item) { + var li = new cr.ui.ListItem(); + li.itemInfo = item; + + var icon = document.createElement('div'); + icon.className = 'detail-icon'; + + var text = document.createElement('div'); + text.className = 'detail-text'; + + if (item.isHeaderItem) { + icon.setAttribute('search-icon', ''); + text.innerHTML = + strf('SEARCH_DRIVE_HTML', util.htmlEscape(item.searchQuery)); + } else { + var iconType = FileType.getIcon(item.entry); + icon.setAttribute('file-type-icon', iconType); + // highlightedBaseName is a piece of HTML with meta characters properly + // escaped. See the comment at fileBrowserPrivate.searchDriveMetadata(). + text.innerHTML = item.highlightedBaseName; + } + li.appendChild(icon); + li.appendChild(text); + return li; +}; + +/** + * Updates the size related style. + */ +SearchBox.prototype.updateSizeRelatedStyle = function() { + // Hide the search box if there is not enough space. + this.element.classList.toggle( + 'too-short', + this.element.clientWidth < 100); +}; + +/** + * Clears the search query. + */ +SearchBox.prototype.clear = function() { + this.inputElement.value = ''; + this.updateStyles_(); +}; + +/** + * Handles a focus event of the search box. + * @private + */ +SearchBox.prototype.onFocus_ = function() { + this.element.classList.toggle('has-cursor', true); + this.inputElement.tabIndex = '99'; // See: go/filesapp-tabindex. + this.autocompleteList.attachToInput(this.inputElement); +}; + +/** + * Handles a blur event of the search box. + * @private + */ +SearchBox.prototype.onBlur_ = function() { + this.element.classList.toggle('has-cursor', false); + this.inputElement.tabIndex = '-1'; + this.autocompleteList.detach(); +}; + +/** + * Handles a keydown event of the search box. + * @private + */ +SearchBox.prototype.onKeyDown_ = function() { + // Handle only Esc key now. + if (event.keyCode != 27 || this.inputElement.value) + return; + this.inputElement.blur(); +}; + +/** + * Handles a click event of the search icon. + * @private + */ +SearchBox.prototype.onIconClick_ = function() { + this.inputElement.focus(); +}; + +/** + * Updates styles of the search box. + * @private + */ +SearchBox.prototype.updateStyles_ = function() { + this.element.classList.toggle('has-text', + !!this.inputElement.value); + var width = this.textMeasure_.getWidth(this.inputElement.value) + + 16 /* Extra space to allow leeway. */; + this.inputElement.style.width = width + 'px'; +}; diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/volume_manager_wrapper.js b/chromium/chrome/browser/resources/file_manager/foreground/js/volume_manager_wrapper.js new file mode 100644 index 00000000000..a9631dc92d9 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/foreground/js/volume_manager_wrapper.js @@ -0,0 +1,334 @@ +// Copyright 2013 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. + +/** + * Thin wrapper for VolumeManager. This should be an interface proxy to talk + * to VolumeManager. This class also filters Drive related data/events if + * driveEnabled is set to false. + * + * @param {VolumeManagerWrapper.DriveEnabledStatus} driveEnabled DRIVE_ENABLED + * if drive should be available. DRIVE_DISABLED if drive related + * data/events should be hidden. + * @param {DOMWindow} opt_backgroundPage Window object of the background + * page. If this is specified, the class skips to get background page. + * TOOD(hirono): Let all clients of the class pass the background page and + * make the argument not optional. + * @constructor + * @extends {cr.EventTarget} + */ +function VolumeManagerWrapper(driveEnabled, opt_backgroundPage) { + cr.EventTarget.call(this); + + this.driveEnabled_ = driveEnabled; + this.volumeInfoList = new cr.ui.ArrayDataModel([]); + + this.volumeManager_ = null; + this.pendingTasks_ = []; + this.onEventBound_ = this.onEvent_.bind(this); + this.onVolumeInfoListUpdatedBound_ = + this.onVolumeInfoListUpdated_.bind(this); + + this.disposed_ = false; + + // Start initialize the VolumeManager. + var queue = new AsyncUtil.Queue(); + + if (opt_backgroundPage) { + this.backgroundPage_ = opt_backgroundPage; + } else { + queue.run(function(callNextStep) { + chrome.runtime.getBackgroundPage(function(backgroundPage) { + this.backgroundPage_ = backgroundPage; + callNextStep(); + }.bind(this)); + }.bind(this)); + } + + queue.run(function(callNextStep) { + this.backgroundPage_.VolumeManager.getInstance(function(volumeManager) { + this.onReady_(volumeManager); + callNextStep(); + }.bind(this)); + }.bind(this)); +} + +/** + * If the drive is enabled on the wrapper. + * @enum {boolean} + */ +VolumeManagerWrapper.DriveEnabledStatus = { + DRIVE_ENABLED: true, + DRIVE_DISABLED: false +}; + +/** + * Extends cr.EventTarget. + */ +VolumeManagerWrapper.prototype.__proto__ = cr.EventTarget.prototype; + +/** + * Called when the VolumeManager gets ready for post initialization. + * @param {VolumeManager} volumeManager The initialized VolumeManager instance. + * @private + */ +VolumeManagerWrapper.prototype.onReady_ = function(volumeManager) { + if (this.disposed_) + return; + + this.volumeManager_ = volumeManager; + + // Subscribe to VolumeManager. + this.volumeManager_.addEventListener( + 'drive-connection-changed', this.onEventBound_); + this.volumeManager_.addEventListener( + 'externally-unmounted', this.onEventBound_); + + // Cache volumeInfoList. + var volumeInfoList = []; + for (var i = 0; i < this.volumeManager_.volumeInfoList.length; i++) { + var volumeInfo = this.volumeManager_.volumeInfoList.item(i); + // TODO(hidehiko): Filter mounted volumes located on Drive File System. + if (!this.driveEnabled_ && volumeInfo.volumeType === util.VolumeType.DRIVE) + continue; + volumeInfoList.push(volumeInfo); + } + this.volumeInfoList.splice.apply( + this.volumeInfoList, + [0, this.volumeInfoList.length].concat(volumeInfoList)); + + // Subscribe to VolumeInfoList. + // In VolumeInfoList, we only use 'splice' event. + this.volumeManager_.volumeInfoList.addEventListener( + 'splice', this.onVolumeInfoListUpdatedBound_); + + // Run pending tasks. + var pendingTasks = this.pendingTasks_; + this.pendingTasks_ = null; + for (var i = 0; i < pendingTasks.length; i++) + pendingTasks[i](); +}; + +/** + * Disposes the instance. After the invocation of this method, any other + * method should not be called. + */ +VolumeManagerWrapper.prototype.dispose = function() { + this.disposed_ = true; + + if (!this.volumeManager_) + return; + this.volumeManager_.removeEventListener( + 'drive-connection-changed', this.onEventBound_); + this.volumeManager_.removeEventListener( + 'externally-unmounted', this.onEventBound_); + this.volumeManager_.volumeInfoList.removeEventListener( + 'splice', this.onVolumeInfoListUpdatedBound_); +}; + +/** + * Called on events sent from VolumeManager. This has responsibility to + * re-dispatch the event to the listeners. + * @param {Event} event Event object sent from VolumeManager. + * @private + */ +VolumeManagerWrapper.prototype.onEvent_ = function(event) { + if (!this.driveEnabled_) { + // If the drive is disabled, ignore all drive related events. + if (event.type === 'drive-connection-changed' || + (event.type === 'externally-unmounted' && + event.volumeInfo.volumeType === util.VolumeType.DRIVE)) + return; + } + + this.dispatchEvent(event); +}; + +/** + * Called on events of modifying VolumeInfoList. + * @param {Event} event Event object sent from VolumeInfoList. + * @private + */ +VolumeManagerWrapper.prototype.onVolumeInfoListUpdated_ = function(event) { + if (this.driveEnabled_) { + // Apply the splice as is. + this.volumeInfoList.splice.apply( + this.volumeInfoList, + [event.index, event.removed.length].concat(event.added)); + } else { + // Filters drive related volumes. + var index = event.index; + for (var i = 0; i < event.index; i++) { + if (this.volumeManager_.volumeInfoList.item(i).volumeType === + util.VolumeType.DRIVE) + index--; + } + + var numRemovedVolumes = 0; + for (var i = 0; i < event.removed.length; i++) { + if (event.removed[i].volumeType !== util.VolumeType.DRIVE) + numRemovedVolumes++; + } + + var addedVolumes = []; + for (var i = 0; i < event.added.length; i++) { + var volumeInfo = event.added[i]; + if (volumeInfo.volumeType !== util.VolumeType.DRIVE) + addedVolumes.push(volumeInfo); + } + + this.volumeInfoList.splice.apply( + this.volumeInfoList, + [index, numRemovedVolumes].concat(addedVolumes)); + } +}; + +/** + * Ensures the VolumeManager is initialized, and then invokes callback. + * If the VolumeManager is already initialized, callback will be called + * immediately. + * @param {function()} callback Called on initialization completion. + */ +VolumeManagerWrapper.prototype.ensureInitialized = function(callback) { + if (this.pendingTasks_) { + this.pendingTasks_.push(this.ensureInitialized.bind(this, callback)); + return; + } + + callback(); +}; + +/** + * @return {util.DriveConnectionType} Current drive connection state. + */ +VolumeManagerWrapper.prototype.getDriveConnectionState = function() { + if (!this.driveEnabled_ || !this.volumeManager_) { + return { + type: util.DriveConnectionType.OFFLINE, + reason: util.DriveConnectionReason.NO_SERVICE + }; + } + + return this.volumeManager_.getDriveConnectionState(); +}; + +/** + * @param {string} mountPath The path to mount location of the volume. + * @return {VolumeInfo} The VolumeInfo instance for the volume mounted at + * mountPath, or null if no volume is found + */ +VolumeManagerWrapper.prototype.getVolumeInfo = function(mountPath) { + return this.filterDisabledDriveVolume_( + this.volumeManager_ && this.volumeManager_.getVolumeInfo(mountPath)); +}; + +/** + * Obtains a volume information from a file entry URL. + * TODO(hirono): Check a file system to find a volume. + * + * @param {string} url URL of entry. + * @return {VolumeInfo} Volume info. + */ +VolumeManagerWrapper.prototype.getVolumeInfoByURL = function(url) { + return this.filterDisabledDriveVolume_( + this.volumeManager_ && this.volumeManager_.getVolumeInfoByURL(url)); +}; + +/** + * Obtains a volume infomration of the current profile. + * + * @param {util.VolumeType} volumeType Volume type. + * @return {VolumeInfo} Found volume info. + */ +VolumeManagerWrapper.prototype.getCurrentProfileVolumeInfo = + function(volumeType) { + return this.filterDisabledDriveVolume_( + this.volumeManager_ && + this.volumeManager_.getCurrentProfileVolumeInfo(volumeType)); +}; + +/** + * Obtains location information from an entry. + * + * @param {Entry} entry File or directory entry. + * @return {EntryLocation} Location information. + */ +VolumeManagerWrapper.prototype.getLocationInfo = function(entry) { + return this.volumeManager_ && this.volumeManager_.getLocationInfo(entry); +}; + +/** + * Requests to mount the archive file. + * @param {string} fileUrl The path to the archive file to be mounted. + * @param {function(string)} successCallback Called with mount path on success. + * @param {function(util.VolumeError)} errorCallback Called when an error + * occurs. + */ +VolumeManagerWrapper.prototype.mountArchive = function( + fileUrl, successCallback, errorCallback) { + if (this.pendingTasks_) { + this.pendingTasks_.push( + this.mountArchive.bind(this, fileUrl, successCallback, errorCallback)); + return; + } + + this.volumeManager_.mountArchive(fileUrl, successCallback, errorCallback); +}; + +/** + * Requests unmount the volume at mountPath. + * @param {string} mountPath The path to the mount location of the volume. + * @param {function(string)} successCallback Called with the mount path + * on success. + * @param {function(util.VolumeError)} errorCallback Called when an error + * occurs. + */ +VolumeManagerWrapper.prototype.unmount = function( + mountPath, successCallback, errorCallback) { + if (this.pendingTasks_) { + this.pendingTasks_.push( + this.unmount.bind(this, mountPath, successCallback, errorCallback)); + return; + } + + this.volumeManager_.unmount(mountPath, successCallback, errorCallback); +}; + +/** + * Resolves the absolute path to an entry instance. + * @param {string} path The path to be resolved. + * @param {function(Entry)} successCallback Called with the resolved entry + * on success. + * @param {function(FileError)} errorCallback Called with the error on error. + */ +VolumeManagerWrapper.prototype.resolveAbsolutePath = function( + path, successCallback, errorCallback) { + if (this.pendingTasks_) { + this.pendingTasks_.push(this.resolveAbsolutePath.bind( + this, path, successCallback, errorCallback)); + return; + } + + // If the drive is disabled, any resolving the path under drive should be + // failed. + if (!this.driveEnabled_ && PathUtil.isDriveBasedPath(path)) { + errorCallback(util.createFileError(FileError.NOT_FOUND_ERR)); + return; + } + + this.volumeManager_.resolveAbsolutePath(path, successCallback, errorCallback); +}; + +/** + * Filters volume info by referring driveEnabled. + * + * @param {VolumeInfo} volumeInfo Volume info. + * @return {VolumeInfo} Null if the drive is disabled and the given volume is + * drive. Otherwise just returns the volume. + * @private + */ +VolumeManagerWrapper.prototype.filterDisabledDriveVolume_ = + function(volumeInfo) { + var isDrive = volumeInfo && volumeInfo.volumeType === util.VolumeType.DRIVE; + return this.driveEnabled_ || !isDrive ? volumeInfo : null; +}; diff --git a/chromium/chrome/browser/resources/file_manager/gallery.html b/chromium/chrome/browser/resources/file_manager/gallery.html new file mode 100644 index 00000000000..02e3e4fc25e --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/gallery.html @@ -0,0 +1,79 @@ +<!-- + -- 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. + --> +<html> +<head> + <link rel="stylesheet" href="foreground/css/list.css"> + <link rel="stylesheet" href="foreground/css/common.css"> + <link rel="stylesheet" href="foreground/css/file_types.css"> + <link rel="stylesheet" href="foreground/css/gallery.css"> + <link rel="stylesheet" href="foreground/css/media_controls.css"> + + <!-- Don't load gallery_scripts.js when flattening is disabled --> + <if expr="0"><!-- </if> + <script src="foreground/js/photo/gallery_scripts.js"></script> + <if expr="0"> --></if> + + <if expr="0"> + <!-- This section is used when the file manager is loaded with + 'filemgr-ext-path' command-line flag. --> + <!-- Keep the list in sync with gallery_scripts.js. --> + <script src="foreground/js/metrics.js"></script> + + <!-- Loads the client of the image loader extension --> + <script src="chrome-extension://pmfjbimdmchhbnneeidfognadeopoehp/image_loader_client.js"></script> + + <script src="chrome://resources/js/cr.js"></script> + <script src="chrome://resources/js/event_tracker.js"></script> + <script src="chrome://resources/js/load_time_data.js"></script> + + <script src="chrome://resources/js/cr/ui.js"></script> + <script src="chrome://resources/js/cr/event_target.js"></script> + <script src="chrome://resources/js/cr/ui/touch_handler.js"></script> + <script src="chrome://resources/js/cr/ui/array_data_model.js"></script> + <script src="chrome://resources/js/cr/ui/dialogs.js"></script> + <script src="chrome://resources/js/cr/ui/list_item.js"></script> + <script src="chrome://resources/js/cr/ui/list_selection_model.js"></script> + <script src="chrome://resources/js/cr/ui/list_single_selection_model.js"></script> + <script src="chrome://resources/js/cr/ui/list_selection_controller.js"></script> + <script src="chrome://resources/js/cr/ui/list.js"></script> + <script src="chrome://resources/js/cr/ui/grid.js"></script> + + <script src="common/js/async_util.js"></script> + <script src="common/js/util.js"></script> + <script src="common/js/path_util.js"></script> + + <script src="foreground/js/file_type.js"></script> + <script src="foreground/js/volume_manager_wrapper.js"></script> + + <script src="foreground/js/image_editor/image_util.js"></script> + <script src="foreground/js/image_editor/viewport.js"></script> + <script src="foreground/js/image_editor/image_buffer.js"></script> + <script src="foreground/js/image_editor/image_view.js"></script> + <script src="foreground/js/image_editor/commands.js"></script> + <script src="foreground/js/image_editor/image_editor.js"></script> + <script src="foreground/js/image_editor/image_transform.js"></script> + <script src="foreground/js/image_editor/image_adjust.js"></script> + <script src="foreground/js/image_editor/filter.js"></script> + <script src="foreground/js/image_editor/image_encoder.js"></script> + <script src="foreground/js/image_editor/exif_encoder.js"></script> + + <script src="foreground/js/media/media_controls.js"></script> + <script src="foreground/js/media/media_util.js"></script> + <script src="foreground/js/media/util.js"></script> + + <script src="foreground/js/metadata/metadata_cache.js"></script> + + <script src="foreground/js/photo/gallery.js"></script> + <script src="foreground/js/photo/gallery_item.js"></script> + <script src="foreground/js/photo/mosaic_mode.js"></script> + <script src="foreground/js/photo/slide_mode.js"></script> + <script src="foreground/js/photo/ribbon.js"></script> + </if> +</head> +<body> + <div class="gallery"></div> +</body> +</html> diff --git a/chromium/chrome/browser/resources/file_manager/main.html b/chromium/chrome/browser/resources/file_manager/main.html new file mode 100644 index 00000000000..7ae3c4ff98e --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/main.html @@ -0,0 +1,413 @@ +<!DOCTYPE HTML> +<!-- + -- 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. + --> +<html i18n-values="dir:textdirection;"> + <head> + <!-- We have to set some default title, or chrome will use the page name. + -- As soon as we init and change to a directory, we'll show the directory + -- path. Until then, use an invisible non-whitespace character. + --> + <title></title> + + <meta name="google" value="notranslate"> + + <link rel="stylesheet" href="chrome://resources/css/apps/topbutton_bar.css"></link> + + <link rel="stylesheet" href="foreground/css/list.css"></link> + <link rel="stylesheet" href="foreground/css/table.css"></link> + <link rel="stylesheet" href="foreground/css/tree.css"></link> + <link rel="stylesheet" href="foreground/css/menu.css"></link> + <link rel="stylesheet" href="foreground/css/combobutton.css"></link> + <link rel="stylesheet" href="foreground/css/file_manager.css"></link> + <link rel="stylesheet" href="foreground/css/file_types.css"></link> + <link rel="stylesheet" href="foreground/css/common.css"></link> + + <!-- Don't load main_scripts.js when flattening is disabled. --> + <if expr="0"><!-- </if> + <script src="foreground/js/main_scripts.js"></script> + <if expr="0"> --></if> + + <if expr="0"> + <!-- This section is used when the file manager is loaded with + 'filemgr-ext-path' command-line flag. --> + <!-- Keep the list in sync with js/main_scripts.js. --> + + <!-- metrics.js initiates load performance tracking + so we want to parse it as early as possible --> + <script src="foreground/js/metrics.js"></script> + + <!-- Loads the client of the image loader extension --> + <script src="chrome-extension://pmfjbimdmchhbnneeidfognadeopoehp/image_loader_client.js"></script> + + <script src="chrome://resources/js/load_time_data.js"></script> + <script src="chrome://resources/js/cr.js"></script> + <script src="chrome://resources/js/util.js"></script> + <script src="chrome://resources/js/i18n_template_no_process.js"></script> + + <script src="chrome://resources/js/event_tracker.js"></script> + <script src="chrome://resources/js/cr/ui.js"></script> + <script src="chrome://resources/js/cr/event_target.js"></script> + <script src="chrome://resources/js/cr/ui/touch_handler.js"></script> + <script src="chrome://resources/js/cr/ui/array_data_model.js"></script> + <script src="chrome://resources/js/cr/ui/dialogs.js"></script> + <script src="chrome://resources/js/cr/ui/list_item.js"></script> + <script src="chrome://resources/js/cr/ui/list_selection_model.js"></script> + <script src="chrome://resources/js/cr/ui/list_single_selection_model.js"></script> + <script src="chrome://resources/js/cr/ui/list_selection_controller.js"></script> + <script src="chrome://resources/js/cr/ui/list.js"></script> + <script src="chrome://resources/js/cr/ui/tree.js"></script> + <script src="chrome://resources/js/cr/ui/autocomplete_list.js"></script> + + <script src="chrome://resources/js/cr/ui/splitter.js"></script> + <script src="chrome://resources/js/cr/ui/table/table_splitter.js"></script> + + <script src="chrome://resources/js/cr/ui/table/table_column.js"></script> + <script src="chrome://resources/js/cr/ui/table/table_column_model.js"></script> + <script src="chrome://resources/js/cr/ui/table/table_header.js"></script> + <script src="chrome://resources/js/cr/ui/table/table_list.js"></script> + <script src="chrome://resources/js/cr/ui/table.js"></script> + + <script src="chrome://resources/js/cr/ui/grid.js"></script> + + <script src="chrome://resources/js/cr/ui/command.js"></script> + <script src="chrome://resources/js/cr/ui/position_util.js"></script> + <script src="chrome://resources/js/cr/ui/menu_item.js"></script> + <script src="chrome://resources/js/cr/ui/menu.js"></script> + <script src="chrome://resources/js/cr/ui/menu_button.js"></script> + <script src="chrome://resources/js/cr/ui/context_menu_handler.js"></script> + + <!-- This script must be loaded before all other Files.app's scripts. --> + <script src="foreground/js/error_counter.js"></script> + + <script src="common/js/async_util.js"></script> + <script src="common/js/path_util.js"></script> + <script src="common/js/util.js"></script> + <script src="common/js/progress_center_common.js"></script> + + <script src="foreground/js/combobutton.js"></script> + <script src="foreground/js/commandbutton.js"></script> + <script src="foreground/js/ui/file_manager_dialog_base.js"></script> + + <script src="foreground/js/app_installer.js"></script> + <script src="foreground/js/cws_container_client.js"></script> + <script src="foreground/js/directory_contents.js"></script> + <script src="foreground/js/directory_model.js"></script> + <script src="foreground/js/directory_tree.js"></script> + <script src="foreground/js/drag_selector.js"></script> + <script src="foreground/js/drive_banners.js"></script> + <script src="foreground/js/error_dialog.js"></script> + <script src="foreground/js/file_operation_manager_wrapper.js"></script> + <script src="foreground/js/file_grid.js"></script> + <script src="foreground/js/file_manager.js"></script> + <script src="foreground/js/file_selection.js"></script> + <script src="foreground/js/file_table.js"></script> + <script src="foreground/js/file_tasks.js"></script> + <script src="foreground/js/file_transfer_controller.js"></script> + <script src="foreground/js/file_type.js"></script> + <script src="foreground/js/file_watcher.js"></script> + <script src="foreground/js/folder_shortcuts_data_model.js"></script> + <script src="foreground/js/navigation_list_model.js"></script> + <script src="foreground/js/scrollbar.js"></script> + <script src="foreground/js/share_client.js"></script> + <script src="foreground/js/share_dialog.js"></script> + <script src="foreground/js/suggest_apps_dialog.js"></script> + <script src="foreground/js/text_measure.js"></script> + <script src="foreground/js/tree.css.js"></script> + <script src="foreground/js/ui/breadcrumbs_controller.js"></script> + <script src="foreground/js/ui/conflict_dialog.js"></script> + <script src="foreground/js/ui/file_manager_ui.js"></script> + <script src="foreground/js/ui/navigation_list.js"></script> + <script src="foreground/js/ui/preview_panel.js"></script> + <script src="foreground/js/ui/progress_center_panel.js"></script> + <script src="foreground/js/ui/search_box.js"></script> + <script src="foreground/js/volume_manager_wrapper.js"></script> + <script src="foreground/js/media/media_util.js"></script> + <script src="foreground/js/metadata/metadata_cache.js"></script> + <script src="foreground/js/default_action_dialog.js"></script> + <script src="foreground/js/file_manager_commands.js"></script> + + <!-- For accurate load performance tracking main.js should be + the last script to include. --> + <script src="foreground/js/main.js"></script> + </if> + + </head> + <body tabindex="-1" i18n-values=".style.fontFamily:WEB_FONT_FAMILY; + .style.fontSize:WEB_FONT_SIZE"> + <commands> + <!-- We have to use U+XXXX notation here according to event.keyIdentifier + property --> + <command id="cut" i18n-values="label:CUT_BUTTON_LABEL" + shortcut="U+0058-Ctrl"> + <command id="copy" i18n-values="label:COPY_BUTTON_LABEL" + shortcut="U+0043-Ctrl"> + <command id="paste" i18n-values="label:PASTE_BUTTON_LABEL" + shortcut="U+0056-Ctrl"> + <command id="rename" i18n-values="label:RENAME_BUTTON_LABEL" + shortcut="Enter-Ctrl"> + <command id="delete" shortcut="U+007F"> + <command id="create-folder-shortcut" + i18n-values="label:CREATE_FOLDER_SHORTCUT_BUTTON_LABEL"> + <command id="remove-folder-shortcut" + i18n-values="label:REMOVE_FOLDER_SHORTCUT_BUTTON_LABEL"> + <command id="new-folder" i18n-values="label:NEW_FOLDER_BUTTON_LABEL" + shortcut="U+0045-Ctrl"> + <command id="new-window" i18n-values="label:NEW_WINDOW_BUTTON_LABEL" + shortcut="U+004E-Ctrl"> + + <command id="search" shortcut="U+0046-Ctrl U+00BF"> + + <!-- Shortcuts for toggling between volumes (CTRL-1..CTRL-9). --> + <command id="volume-switch-1" shortcut="U+0031-Ctrl"> + <command id="volume-switch-2" shortcut="U+0032-Ctrl"> + <command id="volume-switch-3" shortcut="U+0033-Ctrl"> + <command id="volume-switch-4" shortcut="U+0034-Ctrl"> + <command id="volume-switch-5" shortcut="U+0035-Ctrl"> + <command id="volume-switch-6" shortcut="U+0036-Ctrl"> + <command id="volume-switch-7" shortcut="U+0037-Ctrl"> + <command id="volume-switch-8" shortcut="U+0038-Ctrl"> + <command id="volume-switch-9" shortcut="U+0039-Ctrl"> + + <command id="unmount" i18n-values="label:UNMOUNT_DEVICE_BUTTON_LABEL"> + <command id="format" i18n-values="label:FORMAT_DEVICE_BUTTON_LABEL"> + + <command id="volume-help" i18n-values="label:DRIVE_MENU_HELP"> + <command id="drive-buy-more-space" + i18n-values="label:DRIVE_BUY_MORE_SPACE"> + <command id="drive-go-to-drive" + i18n-values="label:DRIVE_VISIT_DRIVE_GOOGLE_COM"> + + <command id="toggle-pinned" i18n-values="label:OFFLINE_COLUMN_LABEL"> + + <command id="open-with" i18n-values="label:OPEN_WITH_BUTTON_LABEL"> + <command id="zip-selection" + i18n-values="label:ZIP_SELECTION_BUTTON_LABEL"> + <command id="share" i18n-values="label:SHARE_BUTTON_LABEL" + shortcut="U+00BE" hide-shortcut-text><!-- Shortcut: '.' --> + + <command id="zoom-in" shortcut="U+00BB-Ctrl"> + <command id="zoom-out" shortcut="U+00BD-Ctrl"> + <command id="zoom-reset" shortcut="U+0030-Ctrl"> + </commands> + + <menu id="file-context-menu" class="chrome-menu" showShortcuts> + <menuitem id="default-action" + visibleif="full-page" hidden></menuitem> + <menuitem command="#open-with" + visibleif="full-page" hidden></menuitem> + <hr id="default-action-separator" visibleif="full-page" hidden> + <menuitem command="#toggle-pinned" checkable></menuitem> + <menuitem command="#share"></menuitem> + <menuitem command="#create-folder-shortcut"></menuitem> + <hr command="#share"> + <menuitem command="#cut" visibleif="full-page"></menuitem> + <menuitem command="#copy" visibleif="full-page"></menuitem> + <menuitem command="#paste" visibleif="full-page"></menuitem> + <hr visibleif="full-page"> + <menuitem command="#rename"></menuitem> + <menuitem command="#delete" i18n-content="DELETE_BUTTON_LABEL"></menuitem> + <menuitem command="#zip-selection"></menuitem> + <hr visibleif="saveas-file full-page"> + <menuitem command="#new-folder" + visibleif="saveas-file full-page"></menuitem> + </menu> + + <menu id="roots-context-menu" class="chrome-menu" > + <menuitem command="#unmount"></menuitem> + <menuitem command="#format"></menuitem> + <menuitem command="#remove-folder-shortcut"></menuitem> + </menu> + + <menu id="directory-tree-context-menu" class="chrome-menu" > + <menuitem command="#create-folder-shortcut"></menuitem> + </menu> + + <menu id="gear-menu" class="chrome-menu" showShortcuts> + <menuitem id="gear-menu-newwindow" command="#new-window"></menuitem> + <menuitem id="gear-menu-newfolder" command="#new-folder"></menuitem> + <hr id="drive-separator"> + <menuitem id="drive-sync-settings" + i18n-content="DRIVE_MOBILE_CONNECTION_OPTION"></menuitem> + <menuitem id="drive-hosted-settings" + i18n-content="DRIVE_SHOW_HOSTED_FILES_OPTION"></menuitem> + <hr> + <div i18n-content="VIEW_TYPE_LABEL"></div> + <menuitem class="menuitem-button left" id="detail-view" + i18n-values="aria-label:DETAIL_VIEW_TOOLTIP"> + </menuitem> + <menuitem class="menuitem-button right" id="thumbnail-view" + i18n-values="aria-label:THUMBNAIL_VIEW_TOOLTIP"> + </menuitem> + <hr> + <menuitem id="gear-menu-drive-buy-more-space" + command="#drive-buy-more-space"></menuitem> + <menuitem id="gear-menu-drive-go-to-drive" + command="#drive-go-to-drive"></menuitem> + <menuitem id="gear-menu-volume-help" + command="#volume-help"></menuitem> + <hr> + <div id="volume-space-info"> + <div id="volume-space-info-contents"> + <span id="volume-space-info-label"></span> + <div class="progress-bar"> + <div class="progress-track" id="volume-space-info-bar"></div> + </div> + </div> + </div> + </menu> + + <menu id="tasks-menu" class="chrome-menu"> + </menu> + + <menu id="text-context-menu" class="chrome-menu" showShortcuts> + <menuitem command="#cut"></menuitem> + <menuitem command="#copy"></menuitem> + <menuitem command="#paste"></menuitem> + <menuitem command="#delete" i18n-content="DELETE_BUTTON_LABEL"></menuitem> + </menu> + + <div class="dialog-container"> + <div class="dialog-navigation-list"> + <div class="dialog-navigation-list-header"> + <span id="app-name"></span> + </div> + <div class="dialog-navigation-list-contents"> + <list id="navigation-list" tabindex="14"></list> + </div> + <div class="dialog-navigation-list-footer"> + <div id="progress-center" hidden> + <li id="progress-center-close-view"> + <label></label> + <div class="progress-frame"> + <div class="progress-bar"> + <div class="progress-track" hidden></div> + </div> + </div> + </li> + <div id="progress-center-open-view"></div> + <button class="toggle" tabindex="-1"></button> + </div> + </div> + </div> + <div class="splitter" id="navigation-list-splitter"></div> + <div class="dialog-main"> + <div class="dialog-header"> + <div id="search-box"> + <div class="icon"></div> + <div class="full-size"> + <input type="search" tabindex="-1" + i18n-values="aria-label:SEARCH_TEXT_LABEL"> + </div> + <button class="clear"></button> + </div> + <div class="topbutton-bar"> + <button class="menubutton gear-button" id="gear-button" tabindex="3" + menu="#gear-menu" + i18n-values="aria-label:GEAR_BUTTON_TOOLTIP" + aria-activedescendant="gear-menu"> + </button> + <button class="maximize-button" id="maximize-button" + visibleif="full-page" tabindex="-1"> + </button> + <button class="close-button" id="close-button" + visibleif="full-page" tabindex="-1"> + </button> + </div> + </div> + <div class="dialog-body"> + <div class="main-panel"> + <!-- The middle bar and spilitter are hidden by default, and will be shown by script if necessary. --> + <div class="dialog-middlebar-contents" hidden> + <div id="middlebar-header" + i18n-content="DRIVE_DIRECTORY_LABEL"></div> + <tree id="directory-tree" tabindex="15"></tree> + </div> + <div class="splitter" id="middlebar-splitter" hidden></div> + <div class="filelist-panel"> + <div class="drive-welcome header"></div> + <div class="volume-warning" id="volume-space-warning" hidden></div> + <div class="volume-warning" id="drive-auth-failed-warning" hidden> + <div class="drive-icon"></div> + <div class="drive-text" id="drive-auth-failed-warning-text"></div> + </div> + <div id="list-container"> + <div class="detail-table" id="detail-table" tabindex="1" autofocus> + </div> + <grid class="thumbnail-grid" tabindex="2"></grid> + <div class="spinner-layer"></div> + <div class="drive-welcome page"></div> + <div id="no-search-results"></div> + </div> + <div class="downloads-warning" hidden></div> + </div> + </div> + <div class="preview-panel progressable" visibility="hidden"> + <div class="left"> + <div> + <div class="preview-thumbnails"></div> + </div> + <div id="preview-lines"> + <div class="preview-summary"> + <span class="preview-text"></span> + <span class="calculating-size"></span> + </div> + <div id="search-breadcrumbs" class="breadcrumbs"></div> + </div> + </div> + <div class="right buttonbar" visibleif="full-page"> + <button id="share-button" command="#share" tabindex="4" + i18n-values="aria-label:SHARE_BUTTON_LABEL"></button> + <button id="tasks" class="combobutton" menu="#tasks-menu" + tabindex="5"></button> + <button id="delete-button" command="#delete" tabindex="6" + i18n-values="aria-label:DELETE_BUTTON_LABEL"></button> + </div> + <div class="preparing-label" i18n-content="PREPARING_LABEL"></div> + <div class="progress-bar"> + <div class="progress-track"></div> + </div> + <div class="right buttonbar" id="open-panel" + visibleif="open-file open-multi-file"> + <select class="file-type" hidden></select> + <button class="ok" disabled tabindex="7"></button> + <button class="cancel" i18n-content="CANCEL_LABEL" tabindex="8"></button> + </div> + </div> + <div id="unmounted-panel"></div> + <div id="format-panel"> + <div class="error"></div> + <button id="format-button" command="#format"></button> + </div> + </div> + </div> + </div> + <div class="dialog-footer progressable" tabindex="-1" + visibleif="saveas-file folder upload-folder"> + <div class="left"> + <button id="new-folder-button" i18n-content="NEW_FOLDER_BUTTON_LABEL" + visibleif="saveas-file folder" command="#new-folder" + tabindex="9"> + </button> + <div id="filename-input-box"> + <div class="filename-label" i18n-content="FILENAME_LABEL"></div> + <input type="text" spellcheck="false" tabindex="10"> + </div> + <div class="preparing-label" i18n-content="PREPARING_LABEL"></div> + <div class="progress-bar"> + <div class="progress-track"></div> + </div> + </div> + <div class="right buttonbar"> + <select class="file-type" hidden></select> + <button class="ok" disabled tabindex="11"></button> + <button class="cancel" i18n-content="CANCEL_LABEL" tabindex="12"></button> + </div> + </div> + <div id="drag-container"></div> + <iframe id="command-dispatcher" hidden></iframe> + <div class="overlay-pane" id="iframe-drag-area" hidden></div> + </body> +</html> diff --git a/chromium/chrome/browser/resources/file_manager/manifest.json b/chromium/chrome/browser/resources/file_manager/manifest.json new file mode 100644 index 00000000000..a5a58c94645 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/manifest.json @@ -0,0 +1,231 @@ +{ + // chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj/ + "key": "MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQDlhpGghtnNJ7pluQN0RDwbUxwwi99oM35ZEaFYvxPLrf0fIEC18cfDdJi6u4aJ+UoSpgzK731L0P/k4LvK2Rz9kVKOy0+IvuRrWkT7lbrLfA1UEBh02OA1AAshjmyRg4IxCqgl8ia8XWq6HKegS1y1KXZYGgb4qp7Bh9VC4cIzswIBIw==", + "manifest_version": 2, + "name": "Files", + "version": "3.0", + "description": "File Manager", + "incognito" : "split", + "icons": { + "16": "common/images/icon16.png", + "32": "common/images/icon32.png", + "48": "common/images/icon48.png", + "64": "common/images/icon64.png", + "96": "common/images/icon96.png", + "128": "common/images/icon128.png", + "256": "common/images/icon256.png" + }, + "permissions": [ + "contextMenus", + "experimental", + "echoPrivate", + "storage", + "fileBrowserHandler", + "fileBrowserPrivate", + "fullscreen", + "mediaPlayerPrivate", + "mediaGalleriesPrivate", + "metricsPrivate", + "notifications", + "commandLinePrivate", + "unlimitedStorage", + "webview", + // Comment out chrome:// permissions to debug on a desktop browser. + "chrome://extension-icon/", + "chrome://resources/", + "chrome://theme/", + "tabs", + "clipboardWrite", + "clipboardRead", + "power", + "https://docs.google.com/", + "https://*.googleusercontent.com/", + "https://drive.google.com/" + ], + "file_browser_handlers": [ + { + "id": "play", + "default_title": "__MSG_PLAY_MEDIA__", + "default_icon": "common/images/file_types/200/audio.png", + "file_filters": [ + "filesystem:*.amr", + "filesystem:*.flac", + "filesystem:*.m4a", + "filesystem:*.mp3", + "filesystem:*.oga", + "filesystem:*.ogg", + "filesystem:*.wav" + ] + }, + { + "id": "watch", + "default_title": "__MSG_PLAY_MEDIA__", + "default_icon": "common/images/file_types/200/video.png", + "file_filters": [ + "filesystem:*.3gp", + "filesystem:*.avi", + "filesystem:*.m4v", + "filesystem:*.mkv", + "filesystem:*.mov", + "filesystem:*.mp4", + "filesystem:*.mpeg", + "filesystem:*.mpeg4", + "filesystem:*.mpg", + "filesystem:*.mpg4", + "filesystem:*.ogm", + "filesystem:*.ogv", + "filesystem:*.ogx", + "filesystem:*.webm" + ] + }, + { + "id": "mount-archive", + "default_title": "__MSG_MOUNT_ARCHIVE__", + "default_icon": "common/images/file_types/200/archive.png", + "file_filters": [ + "filesystem:*.rar", + "filesystem:*.zip" + ] + }, + { + "id": "view-pdf", + "default_title": "__MSG_OPEN_ACTION__", + "default_icon": "common/images/file_types/200/pdf.png", + "file_filters": [ + "filesystem:*.pdf" + ] + }, + { + "id": "view-swf", + "default_title": "__MSG_OPEN_ACTION__", + "default_icon": "common/images/file_types/200/generic.png", + "file_filters": [ + "filesystem:*.swf" + ] + }, + { + "id": "view-in-browser", + "default_title": "__MSG_OPEN_ACTION__", + "default_icon": "common/images/file_types/200/generic.png", + "file_filters": [ + "filesystem:*.htm", + "filesystem:*.html", + "filesystem:*.mht", + "filesystem:*.mhtml", + "filesystem:*.txt" + ] + }, + { + "id": "gallery", + "default_title": "__MSG_OPEN_ACTION__", + "default_icon": "common/images/file_types/200/image.png", + "file_filters": [ + // Image formats + "filesystem:*.bmp", + "filesystem:*.gif", + "filesystem:*.ico", + "filesystem:*.jpg", + "filesystem:*.jpeg", + "filesystem:*.png", + "filesystem:*.webp", + // Video formats + "filesystem:*.3gp", + "filesystem:*.avi", + "filesystem:*.m4v", + "filesystem:*.mkv", + "filesystem:*.mov", + "filesystem:*.mp4", + "filesystem:*.mpeg", + "filesystem:*.mpeg4", + "filesystem:*.mpg", + "filesystem:*.mpg4", + "filesystem:*.ogm", + "filesystem:*.ogv", + "filesystem:*.ogx", + "filesystem:*.webm" + ] + }, + { + "id": "open-hosted-generic", + "default_title": "__MSG_HOSTED__", + "default_icon": "common/images/file_types/200generic.png", + "file_filters": [ + "filesystem:*.gdraw", + "filesystem:*.gtable", + "filesystem:*.gform" + ] + }, + { + "id": "open-hosted-gdoc", + "default_title": "__MSG_HOSTED__", + "default_icon": "common/images/file_types/200/generic.png", + "file_filters": [ + "filesystem:*.gdoc" + ] + }, + { + "id": "open-hosted-gsheet", + "default_title": "__MSG_HOSTED__", + "default_icon": "common/images/file_types/200/generic.png", + "file_filters": [ + "filesystem:*.gsheet" + ] + }, + { + "id": "open-hosted-gslides", + "default_title": "__MSG_HOSTED__", + "default_icon": "common/images/file_types/200/generic.png", + "file_filters": [ + "filesystem:*.gslides" + ] + }, + // The following handlers are used only internally, therefore they do not + // have any file filter. + // Automatically opens a volume and later close Files.app when unmounted. + { + "id": "auto-open", + "default_title": "__MSG_OPEN_ACTION__", + "default_icon": "common/images/file_types/200/generic.png", + "file_filters": [] + }, + // Selects the passed file after launching Files.app. + { + "id": "select", + "default_title": "__MSG_OPEN_ACTION__", + "default_icon": "common/images/file_types/200/generic.png", + "file_filters": [] + }, + // Opens the passed directory after launching Files.app. + { + "id": "open", + "default_title": "__MSG_OPEN_ACTION__", + "default_icon": "common/images/file_types/200/generic.png", + "file_filters": [] + } + ], + // Required to import scripts in a web worker. Note, that in Apps v2, it is + // enough that anything is passed to web_accessible_resources. If there is + // at least any file, then all files are allowed. http://crbug.com/179127. + "web_accessible_resources": ["foreground/js/metadata/byte_reader.js"], + "app": { + "background": { + "scripts": [ + "chrome://resources/js/load_time_data.js", + "chrome://resources/js/cr.js", + "chrome://resources/js/cr/event_target.js", + "chrome://resources/js/cr/ui/array_data_model.js", + "common/js/async_util.js", + "common/js/path_util.js", + "common/js/progress_center_common.js", + "common/js/util.js", + "background/js/volume_manager.js", + "background/js/file_operation_handler.js", + "background/js/file_operation_manager.js", + "background/js/test_util.js", + "background/js/progress_center.js", + "background/js/background.js"] + }, + // chrome-extension://pmfjbimdmchhbnneeidfognadeopoehp is the image loader extension. + "content_security_policy": "default-src 'none'; script-src 'self' chrome://resources chrome-extension://pmfjbimdmchhbnneeidfognadeopoehp; style-src 'self' 'unsafe-inline' chrome://resources; frame-src 'self' about:; img-src 'self' chrome://resources chrome://theme data: https://docs.google.com https://*.googleusercontent.com chrome://extension-icon; media-src 'self' https://*.googleusercontent.com; connect-src https://drive.google.com; object-src 'self'" + } +} diff --git a/chromium/chrome/browser/resources/file_manager/mediaplayer.html b/chromium/chrome/browser/resources/file_manager/mediaplayer.html new file mode 100644 index 00000000000..80e2730d09e --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/mediaplayer.html @@ -0,0 +1,47 @@ +<!-- + -- 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. + --> +<!DOCTYPE HTML> +<html> +<head> + <!-- We have to set some default title, or chrome will use the page name. + -- As soon as the i18n'd strings are loaded we replace it with the correct + -- string. Until then, use an invisible non-whitespace character. + --> + <title></title> + <link rel="icon" type="image/png" href="foreground/images/media/audio_player.png"> + <link rel="stylesheet" type="text/css" href="foreground/css/media_controls.css"> + <link rel="stylesheet" type="text/css" href="foreground/css/audio_player.css"> + + <!-- Don't load mediaplayer_scripts.js when flattening is disabled --> + <if expr="0"><!-- </if> + <script src="foreground/js/media/mediaplayer_scripts.js"></script> + <if expr="0"> --></if> + + <if expr="0"> + <!-- This section is used when the file manager is loaded with + 'filemgr-ext-path' command-line flag. --> + <!-- Keep the list in sync with mediaplayer_scripts.js. --> + <script src="chrome://resources/js/cr.js"></script> + <script src="chrome://resources/js/cr/event_target.js"></script> + <script src="chrome://resources/js/cr/ui/array_data_model.js"></script> + + <script src="common/js/async_util.js"></script> + <script src="common/js/util.js"></script> + <script src="common/js/path_util.js"></script> + + <script src="foreground/js/file_type.js"></script> + <script src="foreground/js/volume_manager_wrapper.js"></script> + <script src="foreground/js/metadata/metadata_cache.js"></script> + + <script src="foreground/js/media/media_controls.js"></script> + <script src="foreground/js/media/audio_player.js"></script> + <script src="foreground/js/media/player_testapi.js"></script> + </if> +</head> +<body> + <div class="audio-player"></div> +</body> +</html> diff --git a/chromium/chrome/browser/resources/file_manager/video_player.html b/chromium/chrome/browser/resources/file_manager/video_player.html new file mode 100644 index 00000000000..46d4fa82ef8 --- /dev/null +++ b/chromium/chrome/browser/resources/file_manager/video_player.html @@ -0,0 +1,57 @@ +<!-- + -- 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. + --> +<html> +<head> + <!-- We have to set some default title, or chrome will use the page name. + -- As soon as the i18n'd strings are loaded we replace it with the correct + -- string. Until then, use an invisible non-whitespace character. + --> + <title></title> + <link rel="icon" type="image/png" href="chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_VIDEO"> + <link rel="stylesheet" type="text/css" href="foreground/css/media_controls.css"> + <link rel="stylesheet" type="text/css" href="foreground/css/video_player.css"> + + <!-- Don't load video_player_scripts.js when flattening is disabled --> + <if expr="0"><!-- </if> + <script src="foreground/js/media/video_player_scripts.js"></script> + <if expr="0"> --></if> + + <if expr="0"> + <!-- This section is used when the file manager is loaded with + 'filemgr-ext-path' command-line flag. --> + <!-- Keep the list in sync with video_player_scripts.js. --> + <script src="chrome://resources/js/cr.js"></script> + <script src="chrome://resources/js/cr/event_target.js"></script> + <script src="chrome://resources/js/cr/ui/array_data_model.js"></script> + <script src="chrome://resources/js/load_time_data.js"></script> + + <script src="common/js/async_util.js"></script> + <script src="common/js/util.js"></script> + <script src="common/js/path_util.js"></script> + + <script src="foreground/js/file_type.js"></script> + <script src="foreground/js/volume_manager_wrapper.js"></script> + <script src="foreground/js/metadata/metadata_cache.js"></script> + + <script src="foreground/js/media/media_controls.js"></script> + <script src="foreground/js/media/util.js"></script> + <script src="foreground/js/media/video_player.js"></script> + <script src="foreground/js/media/player_testapi.js"></script> + </if> +</head> +<body> + <div id="video-player" tools> + <div id="video-container"> + </div> + <div id="controls-wrapper"> + <div id="controls" class="tool"></div> + </div> + <div id="error-wrapper"> + <div id="error"></div> + </div> + </div> +</body> +</html> |