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(); |