| Index: ui/file_manager/file_manager/background/js/background.js
|
| diff --git a/ui/file_manager/file_manager/background/js/background.js b/ui/file_manager/file_manager/background/js/background.js
|
| index f6e574a06cb96943b5e91f1a490a08a1752a27a5..ef7a4a07fc667e4adab2ca84b6fc085bb32b3c15 100644
|
| --- a/ui/file_manager/file_manager/background/js/background.js
|
| +++ b/ui/file_manager/file_manager/background/js/background.js
|
| @@ -17,10 +17,13 @@
|
| /**
|
| * Root class of the background page.
|
| * @constructor
|
| - * @extends {BackgroundBase}
|
| - */
|
| -function FileBrowserBackground() {
|
| - BackgroundBase.call(this);
|
| + */
|
| +function Background() {
|
| + /**
|
| + * Map of all currently open app windows. The key is an app ID.
|
| + * @type {Object.<string, AppWindow>}
|
| + */
|
| + this.appWindows = {};
|
|
|
| /**
|
| * Map of all currently open file dialogs. The key is an app ID.
|
| @@ -140,11 +143,23 @@
|
| * @const
|
| * @private
|
| */
|
| -FileBrowserBackground.CLOSE_DELAY_MS_ = 5000;
|
| -
|
| -FileBrowserBackground.prototype = {
|
| - __proto__: BackgroundBase.prototype
|
| -};
|
| +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;
|
| +};
|
| +
|
| +/**
|
| + * Key for getting and storing the last window state (maximized or not).
|
| + * @const
|
| + * @private
|
| + */
|
| +Background.MAXIMIZED_KEY_ = 'isMaximized';
|
|
|
| /**
|
| * Register callback to be invoked after initialization.
|
| @@ -152,20 +167,19 @@
|
| *
|
| * @param {function()} callback Initialize callback to be registered.
|
| */
|
| -FileBrowserBackground.prototype.ready = function(callback) {
|
| +Background.prototype.ready = function(callback) {
|
| this.stringDataPromise.then(callback);
|
| };
|
|
|
| /**
|
| - * Checks the current condition of background page.
|
| - * @return {boolean} True if the background page is closable, false if not.
|
| - */
|
| -FileBrowserBackground.prototype.canClose = function() {
|
| + * 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.driveSyncHandler_.syncing) {
|
| this.lastTimeCanClose_ = null;
|
| - return false;
|
| + return;
|
| }
|
|
|
| var views = chrome.extension.getViews();
|
| @@ -175,7 +189,7 @@
|
| // closing, the background page cannot close.
|
| if (views[i] !== window && !views[i].closing) {
|
| this.lastTimeCanClose_ = null;
|
| - return false;
|
| + return;
|
| }
|
| closing = closing || views[i].closing;
|
| }
|
| @@ -184,23 +198,36 @@
|
| // 5 seconds ago, We need more time for sure.
|
| if (closing ||
|
| this.lastTimeCanClose_ === null ||
|
| - (Date.now() - this.lastTimeCanClose_ <
|
| - FileBrowserBackground.CLOSE_DELAY_MS_)) {
|
| + Date.now() - this.lastTimeCanClose_ < Background.CLOSE_DELAY_MS_) {
|
| if (this.lastTimeCanClose_ === null)
|
| this.lastTimeCanClose_ = Date.now();
|
| - setTimeout(this.tryClose.bind(this), FileBrowserBackground.CLOSE_DELAY_MS_);
|
| - return false;
|
| + setTimeout(this.tryClose.bind(this), Background.CLOSE_DELAY_MS_);
|
| + return;
|
| }
|
|
|
| // Otherwise we can close the background page.
|
| - return true;
|
| + 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;
|
| };
|
|
|
| /**
|
| * Opens the root directory of the volume in Files.app.
|
| * @param {string} volumeId ID of a volume to be opened.
|
| */
|
| -FileBrowserBackground.prototype.navigateToVolume = function(volumeId) {
|
| +Background.prototype.navigateToVolume = function(volumeId) {
|
| VolumeManager.getInstance().then(function(volumeManager) {
|
| var volumeInfoList = volumeManager.volumeInfoList;
|
| var index = volumeInfoList.findIndex(volumeId);
|
| @@ -214,6 +241,331 @@
|
| }).catch(function(error) {
|
| console.error(error.stack || error);
|
| });
|
| +};
|
| +
|
| +/**
|
| + * 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.saveAppState|.
|
| + * 2. Every time the app state changes the app should update |window.appState|
|
| + * and call |util.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);
|
| +}
|
| +
|
| +AppWindowWrapper.prototype = {
|
| + /**
|
| + * @return {AppWindow} Wrapped application window.
|
| + */
|
| + get rawAppWindow() {
|
| + return this.window_;
|
| + }
|
| +};
|
| +
|
| +/**
|
| + * Focuses the window on the specified desktop.
|
| + * @param {AppWindow} appWindow Application window.
|
| + * @param {string=} opt_profileId The profiled ID of the target window. If it is
|
| + * dropped, the window is focused on the current window.
|
| + */
|
| +AppWindowWrapper.focusOnDesktop = function(appWindow, opt_profileId) {
|
| + new Promise(function(onFulfilled, onRejected) {
|
| + if (opt_profileId) {
|
| + onFulfilled(opt_profileId);
|
| + } else {
|
| + chrome.fileManagerPrivate.getProfiles(function(profiles,
|
| + currentId,
|
| + displayedId) {
|
| + onFulfilled(currentId);
|
| + });
|
| + }
|
| + }).then(function(profileId) {
|
| + appWindow.contentWindow.chrome.fileManagerPrivate.visitDesktop(
|
| + profileId, function() {
|
| + appWindow.focus();
|
| + });
|
| + });
|
| +};
|
| +
|
| +/**
|
| + * Shift distance to avoid overlapping windows.
|
| + * @type {number}
|
| + * @const
|
| + */
|
| +AppWindowWrapper.SHIFT_DISTANCE = 40;
|
| +
|
| +/**
|
| + * Sets the icon of the window.
|
| + * @param {string} iconPath Path of the icon.
|
| + */
|
| +AppWindowWrapper.prototype.setIcon = function(iconPath) {
|
| + this.window_.setIcon(iconPath);
|
| +};
|
| +
|
| +/**
|
| + * Opens the window.
|
| + *
|
| + * @param {Object} appState App state.
|
| + * @param {boolean} reopen True if the launching is triggered automatically.
|
| + * False otherwise.
|
| + * @param {function()=} opt_callback Completion callback.
|
| + */
|
| +AppWindowWrapper.prototype.launch = function(appState, reopen, 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 and window state (maximized or not).
|
| + var lastBounds;
|
| + var isMaximized = false;
|
| + this.queue.run(function(callback) {
|
| + var boundsKey = Background.makeGeometryKey(this.url_);
|
| + var maximizedKey = Background.MAXIMIZED_KEY_;
|
| + chrome.storage.local.get([boundsKey, maximizedKey], function(preferences) {
|
| + if (!chrome.runtime.lastError) {
|
| + lastBounds = preferences[boundsKey];
|
| + isMaximized = preferences[maximizedKey];
|
| + }
|
| + 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;
|
| + if (isMaximized)
|
| + this.options_.state = 'maximized';
|
| +
|
| + // 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.appReopen = reopen;
|
| + contentWindow.appInitialURL = this.url_;
|
| + if (window.IN_TEST)
|
| + contentWindow.IN_TEST = true;
|
| +
|
| + // Register event listeners.
|
| + 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() {
|
| + // Remember the last window state (maximized or normal).
|
| + var preferences = {};
|
| + preferences[Background.MAXIMIZED_KEY_] = this.window_.isMaximized();
|
| + chrome.storage.local.set(preferences);
|
| +
|
| + // 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() {
|
| + if (!this.window_.isMaximized()) {
|
| + 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 {boolean} reopen True if the launching is triggered automatically.
|
| + * False otherwise.
|
| + * @param {function()=} opt_callback Completion callback.
|
| + */
|
| +SingletonAppWindowWrapper.prototype.launch =
|
| + function(appState, reopen, opt_callback) {
|
| + // If the window is not opened yet, just call the parent method.
|
| + if (!this.openingOrOpened_) {
|
| + AppWindowWrapper.prototype.launch.call(
|
| + this, appState, reopen, 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.appReopen = reopen;
|
| + this.window_.contentWindow.reload();
|
| + if (opt_callback)
|
| + opt_callback();
|
| + nextStep();
|
| + }.bind(this));
|
| +};
|
| +
|
| +/**
|
| + * Reopen a window if its state is saved in the local storage.
|
| + * @param {function()=} opt_callback Completion callback.
|
| + */
|
| +SingletonAppWindowWrapper.prototype.reopen = function(opt_callback) {
|
| + chrome.storage.local.get(this.id_, function(items) {
|
| + var value = items[this.id_];
|
| + if (!value) {
|
| + opt_callback && opt_callback();
|
| + return; // No app state persisted.
|
| + }
|
| +
|
| + try {
|
| + var appState = JSON.parse(value);
|
| + } catch (e) {
|
| + console.error('Corrupt launch data for ' + this.id_, value);
|
| + opt_callback && opt_callback();
|
| + return;
|
| + }
|
| + this.launch(appState, true, opt_callback);
|
| + }.bind(this));
|
| };
|
|
|
| /**
|
| @@ -402,7 +754,7 @@
|
| * @param {Object} details Details object.
|
| * @private
|
| */
|
| -FileBrowserBackground.prototype.onExecute_ = function(action, details) {
|
| +Background.prototype.onExecute_ = function(action, details) {
|
| switch (action) {
|
| case 'play':
|
| var urls = util.entriesToURLs(details.entries);
|
| @@ -487,7 +839,7 @@
|
| * Launches the app.
|
| * @private
|
| */
|
| -FileBrowserBackground.prototype.onLaunched_ = function() {
|
| +Background.prototype.onLaunched_ = function() {
|
| if (nextFileManagerWindowID == 0) {
|
| // The app just launched. Remove window state records that are not needed
|
| // any more.
|
| @@ -507,7 +859,7 @@
|
| * Restarted the app, restore windows.
|
| * @private
|
| */
|
| -FileBrowserBackground.prototype.onRestarted_ = function() {
|
| +Background.prototype.onRestarted_ = function() {
|
| // Reopen file manager windows.
|
| chrome.storage.local.get(function(items) {
|
| for (var key in items) {
|
| @@ -543,7 +895,7 @@
|
| * @param {OnClickData} info Event details.
|
| * @private
|
| */
|
| -FileBrowserBackground.prototype.onContextMenuClicked_ = function(info) {
|
| +Background.prototype.onContextMenuClicked_ = function(info) {
|
| if (info.menuItemId == 'new-window') {
|
| // Find the focused window (if any) and use it's current url for the
|
| // new window. If not found, then launch with the default url.
|
| @@ -573,7 +925,7 @@
|
| * Initializes the context menu. Recreates if already exists.
|
| * @private
|
| */
|
| -FileBrowserBackground.prototype.initContextMenu_ = function() {
|
| +Background.prototype.initContextMenu_ = function() {
|
| try {
|
| // According to the spec [1], the callback is optional. But no callback
|
| // causes an error for some reason, so we call it with null-callback to
|
| @@ -593,6 +945,6 @@
|
|
|
| /**
|
| * Singleton instance of Background.
|
| - * @type {FileBrowserBackground}
|
| - */
|
| -window.background = new FileBrowserBackground();
|
| + * @type {Background}
|
| + */
|
| +window.background = new Background();
|
|
|