From 08b16fcbdc4650e12f21d1832c5c1fd809db97be Mon Sep 17 00:00:00 2001 From: Heather Arthur Date: Wed, 2 Nov 2011 12:14:30 -0700 Subject: [PATCH] Bug 644409 - Make scratchpads save their state across restarts --- browser/base/content/browser.js | 14 +- .../sessionstore/src/nsSessionStore.js | 19 +- .../sessionstore/test/browser/Makefile.in | 1 + .../browser/browser_644409-scratchpads.js | 59 ++++++ browser/devtools/Makefile.in | 5 +- browser/devtools/scratchpad/Makefile.in | 53 ++++++ .../scratchpad/scratchpad-manager.jsm | 174 ++++++++++++++++++ browser/devtools/scratchpad/scratchpad.js | 68 ++++++- browser/devtools/scratchpad/test/Makefile.in | 2 + .../test/browser_scratchpad_open.js | 71 +++++++ .../test/browser_scratchpad_restore.js | 101 ++++++++++ 11 files changed, 550 insertions(+), 17 deletions(-) create mode 100644 browser/components/sessionstore/test/browser/browser_644409-scratchpads.js create mode 100644 browser/devtools/scratchpad/Makefile.in create mode 100644 browser/devtools/scratchpad/scratchpad-manager.jsm create mode 100644 browser/devtools/scratchpad/test/browser_scratchpad_open.js create mode 100644 browser/devtools/scratchpad/test/browser_scratchpad_restore.js diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 555804b59d90..1df942b5b5ba 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -8900,14 +8900,16 @@ var Scratchpad = { prefEnabledName: "devtools.scratchpad.enabled", openScratchpad: function SP_openScratchpad() { - const SCRATCHPAD_WINDOW_URL = "chrome://browser/content/scratchpad.xul"; - const SCRATCHPAD_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no"; - - return Services.ww.openWindow(null, SCRATCHPAD_WINDOW_URL, "_blank", - SCRATCHPAD_WINDOW_FEATURES, null); - }, + return this.ScratchpadManager.openScratchpad(); + } }; +XPCOMUtils.defineLazyGetter(Scratchpad, "ScratchpadManager", function() { + let tmp = {}; + Cu.import("resource:///modules/devtools/scratchpad-manager.jsm", tmp); + return tmp.ScratchpadManager; +}); + XPCOMUtils.defineLazyGetter(window, "gShowPageResizers", function () { #ifdef XP_WIN diff --git a/browser/components/sessionstore/src/nsSessionStore.js b/browser/components/sessionstore/src/nsSessionStore.js index 16a47abdab34..c7876fac294b 100644 --- a/browser/components/sessionstore/src/nsSessionStore.js +++ b/browser/components/sessionstore/src/nsSessionStore.js @@ -140,6 +140,11 @@ XPCOMUtils.defineLazyGetter(this, "NetUtil", function() { return NetUtil; }); +XPCOMUtils.defineLazyGetter(this, "ScratchpadManager", function() { + Cu.import("resource:///modules/devtools/scratchpad-manager.jsm"); + return ScratchpadManager; +}); + XPCOMUtils.defineLazyServiceGetter(this, "CookieSvc", "@mozilla.org/cookiemanager;1", "nsICookieManager2"); @@ -1582,6 +1587,10 @@ SessionStoreService.prototype = { this._capClosedWindows(); } + if (lastSessionState.scratchpads) { + ScratchpadManager.restoreSession(lastSessionState.scratchpads); + } + // Set data that persists between sessions this._recentCrashes = lastSessionState.session && lastSessionState.session.recentCrashes || 0; @@ -2487,12 +2496,16 @@ SessionStoreService.prototype = { startTime: this._sessionStartTime, recentCrashes: this._recentCrashes }; + + // get open Scratchpad window states too + var scratchpads = ScratchpadManager.getSessionState(); return { windows: total, selectedWindow: ix + 1, _closedWindows: lastClosedWindowsCopy, - session: session + session: session, + scratchpads: scratchpads }; }, @@ -2700,6 +2713,10 @@ SessionStoreService.prototype = { this.restoreHistoryPrecursor(aWindow, tabs, winData.tabs, (aOverwriteTabs ? (parseInt(winData.selected) || 1) : 0), 0, 0); + if (aState.scratchpads) { + ScratchpadManager.restoreSession(aState.scratchpads); + } + // This will force the keypress listener that Panorama has to attach if it // isn't already. This will be the case if tab view wasn't entered or there // were only visible tabs when TabView.init was first called. diff --git a/browser/components/sessionstore/test/browser/Makefile.in b/browser/components/sessionstore/test/browser/Makefile.in index 35b02622d4c6..ab7f85bc6b7a 100644 --- a/browser/components/sessionstore/test/browser/Makefile.in +++ b/browser/components/sessionstore/test/browser/Makefile.in @@ -149,6 +149,7 @@ _BROWSER_TEST_FILES = \ browser_628270.js \ browser_635418.js \ browser_636279.js \ + browser_644409-scratchpads.js \ browser_645428.js \ browser_659591.js \ browser_662812.js \ diff --git a/browser/components/sessionstore/test/browser/browser_644409-scratchpads.js b/browser/components/sessionstore/test/browser/browser_644409-scratchpads.js new file mode 100644 index 000000000000..a81746e93572 --- /dev/null +++ b/browser/components/sessionstore/test/browser/browser_644409-scratchpads.js @@ -0,0 +1,59 @@ + /* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const testState = { + windows: [{ + tabs: [ + { entries: [{ url: "about:blank" }] }, + ] + }], + scratchpads: [ + { text: "text1", executionContext: 1 }, + { text: "", executionContext: 2, filename: "test.js" } + ] +}; + +// only finish() when correct number of windows opened +var restored = []; +function addState(state) { + restored.push(state); + + if (restored.length == testState.scratchpads.length) { + ok(statesMatch(restored, testState.scratchpads), + "Two scratchpad windows restored"); + + Services.ww.unregisterNotification(windowObserver); + finish(); + } +} + +function test() { + waitForExplicitFinish(); + + Services.ww.registerNotification(windowObserver); + + ss.setBrowserState(JSON.stringify(testState)); +} + +function windowObserver(aSubject, aTopic, aData) { + if (aTopic == "domwindowopened") { + let win = aSubject.QueryInterface(Ci.nsIDOMWindow); + win.addEventListener("load", function() { + if (win.Scratchpad) { + let state = win.Scratchpad.getState(); + win.close(); + addState(state); + } + }, false); + } +} + +function statesMatch(restored, states) { + return states.every(function(state) { + return restored.some(function(restoredState) { + return state.filename == restoredState.filename && + state.text == restoredState.text && + state.executionContext == restoredState.executionContext; + }) + }); +} \ No newline at end of file diff --git a/browser/devtools/Makefile.in b/browser/devtools/Makefile.in index 7b9ab208da68..83271a16c81a 100644 --- a/browser/devtools/Makefile.in +++ b/browser/devtools/Makefile.in @@ -51,11 +51,8 @@ DIRS = \ webconsole \ sourceeditor \ styleinspector \ + scratchpad \ shared \ $(NULL) -ifdef ENABLE_TESTS -DIRS += scratchpad/test -endif - include $(topsrcdir)/config/rules.mk diff --git a/browser/devtools/scratchpad/Makefile.in b/browser/devtools/scratchpad/Makefile.in new file mode 100644 index 000000000000..376ffb1ef0cd --- /dev/null +++ b/browser/devtools/scratchpad/Makefile.in @@ -0,0 +1,53 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is Scratchpad Build Code. +# +# The Initial Developer of the Original Code is The Mozilla Foundation. +# +# Portions created by the Initial Developer are Copyright (C) 2011 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Rob Campbell +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +ifdef ENABLE_TESTS + DIRS += test +endif + +include $(topsrcdir)/config/rules.mk + +libs:: + $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools diff --git a/browser/devtools/scratchpad/scratchpad-manager.jsm b/browser/devtools/scratchpad/scratchpad-manager.jsm new file mode 100644 index 000000000000..42a88d863630 --- /dev/null +++ b/browser/devtools/scratchpad/scratchpad-manager.jsm @@ -0,0 +1,174 @@ +/* vim:set ts=2 sw=2 sts=2 et tw=80: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Scratchpad + * + * The Initial Developer of the Original Code is + * The Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Heather Arthur (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK *****/ + +"use strict"; + +var EXPORTED_SYMBOLS = ["ScratchpadManager"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +const SCRATCHPAD_WINDOW_URL = "chrome://browser/content/scratchpad.xul"; +const SCRATCHPAD_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no"; + +Cu.import("resource://gre/modules/Services.jsm"); + +/** + * The ScratchpadManager object opens new Scratchpad windows and manages the state + * of open scratchpads for session restore. There's only one ScratchpadManager in + * the life of the browser. + */ +var ScratchpadManager = { + + _scratchpads: [], + + /** + * Get the saved states of open scratchpad windows. Called by + * session restore. + * + * @return array + * The array of scratchpad states. + */ + getSessionState: function SPM_getSessionState() + { + return this._scratchpads; + }, + + /** + * Restore scratchpad windows from the scratchpad session store file. + * Called by session restore. + * + * @param function aSession + * The session object with scratchpad states. + * + * @return array + * The restored scratchpad windows. + */ + restoreSession: function SPM_restoreSession(aSession) + { + if (!Array.isArray(aSession)) { + return []; + } + + let wins = []; + aSession.forEach(function(state) { + let win = this.openScratchpad(state); + wins.push(win); + }, this); + + return wins; + }, + + /** + * Iterate through open scratchpad windows and save their states. + */ + saveOpenWindows: function SPM_saveOpenWindows() { + this._scratchpads = []; + + let enumerator = Services.wm.getEnumerator("devtools:scratchpad"); + while (enumerator.hasMoreElements()) { + let win = enumerator.getNext(); + if (!win.closed) { + this._scratchpads.push(win.Scratchpad.getState()); + } + } + }, + + /** + * Open a new scratchpad window with an optional initial state. + * + * @param object aState + * Optional. The initial state of the scratchpad, an object + * with properties filename, text, and executionContext. + * + * @return nsIDomWindow + * The opened scratchpad window. + */ + openScratchpad: function SPM_openScratchpad(aState) + { + let params = null; + if (aState) { + if (typeof aState != 'object') { + return; + } + params = Cc["@mozilla.org/embedcomp/dialogparam;1"] + .createInstance(Ci.nsIDialogParamBlock); + params.SetNumberStrings(1); + params.SetString(0, JSON.stringify(aState)); + } + let win = Services.ww.openWindow(null, SCRATCHPAD_WINDOW_URL, "_blank", + SCRATCHPAD_WINDOW_FEATURES, params); + // Only add shutdown observer if we've opened a scratchpad window + ShutdownObserver.init(); + + return win; + } +}; + + +/** + * The ShutdownObserver listens for app shutdown and saves the current state + * of the scratchpads for session restore. + */ +var ShutdownObserver = { + _initialized: false, + + init: function SDO_init() + { + if (this._initialized) { + return; + } + + Services.obs.addObserver(this, "quit-application-granted", false); + this._initialized = true; + }, + + observe: function SDO_observe(aMessage, aTopic, aData) + { + if (aTopic == "quit-application-granted") { + ScratchpadManager.saveOpenWindows(); + this.uninit(); + } + }, + + uninit: function SDO_uninit() + { + Services.obs.removeObserver(this, "quit-application-granted"); + } +}; diff --git a/browser/devtools/scratchpad/scratchpad.js b/browser/devtools/scratchpad/scratchpad.js index c8238dba6a56..056e7e13201a 100644 --- a/browser/devtools/scratchpad/scratchpad.js +++ b/browser/devtools/scratchpad/scratchpad.js @@ -59,12 +59,12 @@ Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource:///modules/PropertyPanel.jsm"); Cu.import("resource:///modules/source-editor.jsm"); +Cu.import("resource:///modules/devtools/scratchpad-manager.jsm"); + const SCRATCHPAD_CONTEXT_CONTENT = 1; const SCRATCHPAD_CONTEXT_BROWSER = 2; -const SCRATCHPAD_WINDOW_URL = "chrome://browser/content/scratchpad.xul"; const SCRATCHPAD_L10N = "chrome://browser/locale/devtools/scratchpad.properties"; -const SCRATCHPAD_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no"; const DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled"; /** @@ -132,6 +132,55 @@ var Scratchpad = { this.editor.setText(aText, aStart, aEnd); }, + /** + * Set the filename in the scratchpad UI and object + * + * @param string aFilename + * The new filename + */ + setFilename: function SP_setFilename(aFilename) + { + document.title = this.filename = aFilename; + }, + + /** + * Get the current state of the scratchpad. Called by the + * Scratchpad Manager for session storing. + * + * @return object + * An object with 3 properties: filename, text, and + * executionContext. + */ + getState: function SP_getState() + { + return { + filename: this.filename, + text: this.getText(), + executionContext: this.executionContext + }; + }, + + /** + * Set the filename and execution context using the given state. Called + * when scratchpad is being restored from a previous session. + * + * @param object aState + * An object with filename and executionContext properties. + */ + setState: function SP_getState(aState) + { + if (aState.filename) { + this.setFilename(aState.filename); + } + + if (aState.executionContext == SCRATCHPAD_CONTEXT_BROWSER) { + this.setBrowserContext(); + } + else { + this.setContentContext(); + } + }, + /** * Get the most recent chrome window of type navigator:browser. */ @@ -442,8 +491,7 @@ var Scratchpad = { */ openScratchpad: function SP_openScratchpad() { - Services.ww.openWindow(null, SCRATCHPAD_WINDOW_URL, "_blank", - SCRATCHPAD_WINDOW_FEATURES, null); + ScratchpadManager.openScratchpad(); }, /** @@ -541,7 +589,7 @@ var Scratchpad = { Ci.nsIFilePicker.modeOpen); fp.defaultString = ""; if (fp.show() != Ci.nsIFilePicker.returnCancel) { - document.title = this.filename = fp.file.path; + this.setFilename(fp.file.path); this.importFromFile(fp.file); } }, @@ -680,12 +728,20 @@ var Scratchpad = { errorConsoleCommand.removeAttribute("disabled"); } + let initialText = this.strings.GetStringFromName("scratchpadIntro"); + if ("arguments" in window && + window.arguments[0] instanceof Ci.nsIDialogParamBlock) { + let state = JSON.parse(window.arguments[0].GetString(0)); + this.setState(state); + initialText = state.text; + } + this.editor = new SourceEditor(); let config = { mode: SourceEditor.MODES.JAVASCRIPT, showLineNumbers: true, - placeholderText: this.strings.GetStringFromName("scratchpadIntro"), + placeholderText: initialText }; let editorPlaceholder = document.getElementById("scratchpad-editor"); diff --git a/browser/devtools/scratchpad/test/Makefile.in b/browser/devtools/scratchpad/test/Makefile.in index 76a1b8b66729..9578e859ac2d 100644 --- a/browser/devtools/scratchpad/test/Makefile.in +++ b/browser/devtools/scratchpad/test/Makefile.in @@ -53,6 +53,8 @@ _BROWSER_TEST_FILES = \ browser_scratchpad_ui.js \ browser_scratchpad_bug_646070_chrome_context_pref.js \ browser_scratchpad_bug_660560_tab.js \ + browser_scratchpad_open.js \ + browser_scratchpad_restore.js \ libs:: $(_BROWSER_TEST_FILES) $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir) diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_open.js b/browser/devtools/scratchpad/test/browser_scratchpad_open.js new file mode 100644 index 000000000000..3d64b4e8c4a2 --- /dev/null +++ b/browser/devtools/scratchpad/test/browser_scratchpad_open.js @@ -0,0 +1,71 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var ScratchpadManager = Scratchpad.ScratchpadManager; + +// only finish() when correct number of tests are done +const expected = 3; +var count = 0; + +function done() +{ + if (++count == expected) { + finish(); + } +} + + +function test() +{ + waitForExplicitFinish(); + testOpen(); + testOpenWithState(); + testOpenInvalidState(); +} + +function testOpen() +{ + let win = ScratchpadManager.openScratchpad(); + + win.addEventListener("load", function() { + is(win.Scratchpad.filename, undefined, "Default filename is undefined"); + is(win.Scratchpad.getText(), + win.Scratchpad.strings.GetStringFromName("scratchpadIntro"), + "Default text is loaded") + is(win.Scratchpad.executionContext, win.SCRATCHPAD_CONTEXT_CONTENT, + "Default execution context is content"); + + win.close(); + done(); + }); +} + +function testOpenWithState() +{ + let state = { + filename: "testfile", + executionContext: 2, + text: "test text" + }; + + let win = ScratchpadManager.openScratchpad(state); + + win.addEventListener("load", function() { + is(win.Scratchpad.filename, state.filename, "Filename loaded from state"); + is(win.Scratchpad.executionContext, state.executionContext, "Execution context loaded from state"); + is(win.Scratchpad.getText(), state.text, "Content loaded from state"); + + win.close(); + done(); + }); +} + +function testOpenInvalidState() +{ + let state = 7; + + let win = ScratchpadManager.openScratchpad(state); + ok(!win, "no scratchpad opened if state is not an object"); + done(); +} diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_restore.js b/browser/devtools/scratchpad/test/browser_scratchpad_restore.js new file mode 100644 index 000000000000..cf471df8ed60 --- /dev/null +++ b/browser/devtools/scratchpad/test/browser_scratchpad_restore.js @@ -0,0 +1,101 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var ScratchpadManager = Scratchpad.ScratchpadManager; + +/* Call the iterator for each item in the list, + calling the final callback with all the results + after every iterator call has sent its result */ +function asyncMap(items, iterator, callback) +{ + let expected = items.length; + let results = []; + + items.forEach(function(item) { + iterator(item, function(result) { + results.push(result); + if (results.length == expected) { + callback(results); + } + }); + }); +} + +function test() +{ + waitForExplicitFinish(); + testRestore(); +} + +function testRestore() +{ + let states = [ + { + filename: "testfile", + text: "test1", + executionContext: 2 + }, + { + text: "text2", + executionContext: 1 + }, + { + text: "text3", + executionContext: 1 + } + ]; + + asyncMap(states, function(state, done) { + // Open some scratchpad windows + let win = ScratchpadManager.openScratchpad(state); + win.addEventListener("load", function() { + done(win); + }) + }, function(wins) { + // Then save the windows to session store + ScratchpadManager.saveOpenWindows(); + + // Then get their states + let session = ScratchpadManager.getSessionState(); + + // Then close them + wins.forEach(function(win) { + win.close(); + }); + + // Clear out session state for next tests + ScratchpadManager.saveOpenWindows(); + + // Then restore them + let restoredWins = ScratchpadManager.restoreSession(session); + + is(restoredWins.length, 3, "Three scratchad windows restored"); + + asyncMap(restoredWins, function(restoredWin, done) { + restoredWin.addEventListener("load", function() { + let state = restoredWin.Scratchpad.getState(); + restoredWin.close(); + done(state); + }); + }, function(restoredStates) { + // Then make sure they were restored with the right states + ok(statesMatch(restoredStates, states), + "All scratchpad window states restored correctly"); + + // Yay, we're done! + finish(); + }); + }); +} + +function statesMatch(restoredStates, states) +{ + return states.every(function(state) { + return restoredStates.some(function(restoredState) { + return state.filename == restoredState.filename + && state.text == restoredState.text + && state.executionContext == restoredState.executionContext; + }) + }); +}