Bug 510890 - Port Bug 394759 (Add undo close window feature) to SeaMonkey - Backend part, r=neil sr=neil
authorMisak Khachatryan <misak@xter.net>
Tue, 25 Aug 2009 18:15:43 +0200
changeset 3409 c43e364dec915a0aa1fec1110ec71241cd093a5a
parent 3408 477ef62ff985675b8d4ba49efdef75f72f1c75d0
child 3410 0fe6bf0ddb618e9f28848f600d4894d9292245bb
push idunknown
push userunknown
push dateunknown
reviewersneil, neil
bugs510890, 394759
Bug 510890 - Port Bug 394759 (Add undo close window feature) to SeaMonkey - Backend part, r=neil sr=neil
suite/common/public/nsISessionStore.idl
suite/common/src/nsSessionStore.js
--- a/suite/common/public/nsISessionStore.idl
+++ b/suite/common/public/nsISessionStore.idl
@@ -41,17 +41,17 @@ interface nsIDOMWindow;
 interface nsIDOMNode;
 
 /**
  * nsISessionStore keeps track of the current browsing state - i.e.
  * tab history, cookies, scroll state, form data, POSTDATA and window features
  * - and allows to restore everything into one window.
  */
 
-[scriptable, uuid(7e8a0f63-284a-493a-8658-ddb84db42a20)]
+[scriptable, uuid(467425eb-86c7-4e8f-97ed-46e400d21e60)]
 interface nsISessionStore : nsISupports
 {
   /**
    * Initialize the service
    */
   void init(in nsIDOMWindow aWindow);
 
   /**
@@ -167,9 +167,27 @@ interface nsISessionStore : nsISupports
    * @param aKey is the value's name.
    */
   void deleteTabValue(in nsIDOMNode aTab, in AString aKey);
 
   /**
    * @param aName is the name of the attribute to save/restore for all xul:tabs.
    */
   void persistTabAttribute(in AString aName);
+
+  /**
+   * Get the number of restore-able windows
+   */
+  unsigned long getClosedWindowCount();
+
+  /**
+   * Get closed windows data
+   *
+   * @returns a JSON string representing the list of closed windows.
+   */
+  AString getClosedWindowData();
+
+  /**
+   * @param aIndex is the index of the windows to be restored (FIFO ordered).
+   * @returns the nsIDOMWindow object of the reopened window
+   */
+  nsIDOMWindow undoCloseWindow(in unsigned long aIndex);
 };
--- a/suite/common/src/nsSessionStore.js
+++ b/suite/common/src/nsSessionStore.js
@@ -16,16 +16,17 @@
  * The Initial Developer of the Original Code is
  * Simon Bünzli <zeniko@gmail.com>
  * Portions created by the Initial Developer are Copyright (C) 2006
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *   Dietrich Ayala <dietrich@mozilla.com>
  *   Ehsan Akhgari <ehsan.akhgari@gmail.com>
+ *   Paul O’Shannessy <paul@oshannessy.com>
  *
  * 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
@@ -133,19 +134,18 @@ SessionStoreService.prototype = {
   _restoreCount: 0,
 
   // time in milliseconds (Date.now()) when the session was last written to file
   _lastSaveTime: 0,
 
   // states for all currently opened windows
   _windows: {},
 
-  // in case the last closed window ain't a navigator:browser one
-  // (also contains browser popup windows closed after the last non-popup one)
-  _lastClosedWindows: null,
+  // states for all recently closed windows
+  _closedWindows: [],
 
   // not-"dirty" windows usually don't need to have their data updated
   _dirtyWindows: {},
 
   // collection of session states yet to be restored
   _statesToRestore: {},
 
   // counts the number of crashes since the last clean start
@@ -180,16 +180,19 @@ SessionStoreService.prototype = {
     // get interval from prefs - used often, so caching/observing instead of fetching on-demand
     this._interval = this._prefBranch.getIntPref("sessionstore.interval");
     this._prefBranch.addObserver("sessionstore.interval", this, true);
 
     // get crash recovery state from prefs and allow for proper reaction to state changes
     this._resume_from_crash = this._prefBranch.getBoolPref("sessionstore.resume_from_crash");
     this._prefBranch.addObserver("sessionstore.resume_from_crash", this, true);
 
+     // observe prefs changes so we can modify stored data to match
+     this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
+
     // this pref is only read at startup, so no need to observe it
     this._sessionhistory_max_entries =
       this._prefBranch.getIntPref("sessionhistory.max_entries");
 
     // get file references
     var dirService = Components.classes["@mozilla.org/file/directory_service;1"]
                                .getService(Components.interfaces.nsIProperties);
     this._sessionFile = dirService.get("ProfD", Components.interfaces.nsILocalFile);
@@ -321,27 +324,31 @@ SessionStoreService.prototype = {
       });
       // also clear all data about closed tabs and windows
       for (let ix in this._windows) {
         if (ix in openWindows)
           this._windows[ix]._closedTabs = [];
         else
           delete this._windows[ix];
       }
-      this._lastClosedWindows = null;
+      // also clear all data about closed windows
+      this._closedWindows = [];
       this._clearDisk();
       // give the tabbrowsers a chance to clear their histories first
       var win = this._getMostRecentBrowserWindow();
       if (win)
         win.setTimeout(function() { _this.saveState(true); }, 0);
       else if (this._loadState == STATE_RUNNING)
         this.saveState(true);
       break;
     case "nsPref:changed": // catch pref changes
       switch (aData) {
+      case "sessionstore.max_windows_undo":
+        this._capClosedWindows();
+        break;
       case "sessionstore.interval":
         this._interval = this._prefBranch.getIntPref("sessionstore.interval");
         // reset timer and save
         if (this._saveTimer) {
           this._saveTimer.cancel();
           this._saveTimer = null;
         }
         this.saveStateDelayed(null, -1);
@@ -508,27 +515,26 @@ SessionStoreService.prototype = {
     tabbrowser.removeEventListener("TabClose", this, true);
     tabbrowser.removeEventListener("TabSelect", this, true);
 
     let winData = this._windows[aWindow.__SSi];
     if (this._loadState == STATE_RUNNING) { // window not closed during a regular shut-down
       // update all window data for a last time
       this._collectWindowData(aWindow);
 
-      // preserve this window's data (in case it was the last navigator:browser)
-      // if this is a popup window, append it to what we've already got (cf. bug 368677)
-      if (!this._lastClosedWindows || !winData.isPopup)
-        this._lastClosedWindows = [winData];
-      else
-        this._lastClosedWindows.push(winData);
+      if (isFullyLoaded) {
+        winData.title = aWindow.content.document.title || tabbrowser.selectedTab.label;
+        winData.title = this._replaceLoadingTitle(winData.title, tabbrowser,
+                                                  tabbrowser.selectedTab);
+        this._updateCookies([winData]);
+      }
 
-      if (isFullyLoaded) {
-        winData.title = aWindow.content.document.title;
-        this._updateCookies(this._lastClosedWindows);
-      }
+      // store closed-window data for undo
+      this._closedWindows.unshift(winData);
+      this._capClosedWindows();
 
       // clear this window from the list
       delete this._windows[aWindow.__SSi];
 
       // save the state without this window to disk
       this.saveStateDelayed();
     }
 
@@ -611,21 +617,17 @@ SessionStoreService.prototype = {
     // make sure that the tab related data is up-to-date
     var tabState = this._collectTabData(aTab);
     this._updateTextAndScrollDataForTab(aWindow, aTab.linkedBrowser, tabState);
 
     // store closed-tab data for undo
     if (tabState.entries.length > 0) {
       let tabTitle = aTab.label;
       let tabbrowser = aWindow.gBrowser;
-      // replace "Loading..." with the document title (with minimal side-effects)
-      if (tabTitle == tabbrowser.mStringBundle.getString("tabs.loading")) {
-        tabbrowser.setTabTitle(aTab);
-        [tabTitle, aTab.label] = [aTab.label, tabTitle];
-      }
+      tabTitle = this._replaceLoadingTitle(tabTitle, tabbrowser, aTab);
 
       var tabsData = {
           state: tabState,
           title: tabTitle,
           image: aTab.getAttribute("image"),
           pos: tabState.entries.length - 1
       };
       aTab.tabData = tabsData;
@@ -717,16 +719,19 @@ SessionStoreService.prototype = {
 
     // close all other browser windows
     this._forEachBrowserWindow(function(aWindow) {
       if (aWindow != window) {
         aWindow.close();
       }
     });
 
+    // make sure closed window data isn't kept
+    this._closedWindows = [];
+
     // restore to the given state
     this.restoreWindow(window, state, true);
   },
 
   getWindowState: function sss_getWindowState(aWindow) {
     if (!aWindow.__SSi && !aWindow.__SS_dyingCache)
       throw (Components.returnCode = Components.results.NS_ERROR_INVALID_ARG);
 
@@ -815,16 +820,35 @@ SessionStoreService.prototype = {
     let browser = aWindow.gBrowser;
 
     // Seamonkey has it's own undoclosetab functionality
     var newTab = browser.restoreTab(aIndex);
 
     return newTab;
   },
 
+  getClosedWindowCount: function sss_getClosedWindowCount() {
+    return this._closedWindows.length;
+  },
+
+  getClosedWindowData: function sss_getClosedWindowData() {
+    return this._toJSONString(this._closedWindows);
+  },
+
+  undoCloseWindow: function sss_undoCloseWindow(aIndex) {
+    if (!(aIndex in this._closedWindows))
+      return null;
+
+    // reopen the window
+    let state = { windows: this._closedWindows.splice(aIndex, 1) };
+    let window = this._openWindowWithState(state);
+    this.windowToFocus = window;
+    return window;
+  },
+
   getWindowValue: function sss_getWindowValue(aWindow, aKey) {
     if (aWindow.__SSi) {
       var data = this._windows[aWindow.__SSi].extData || {};
       return data[aKey] || "";
     }
     if (aWindow.__SS_dyingCache) {
       data = aWindow.__SS_dyingCache.extData || {};
       return data[aKey] || "";
@@ -1460,30 +1484,36 @@ SessionStoreService.prototype = {
     for (ix in this._statesToRestore) {
       for each (let winData in this._statesToRestore[ix].windows) {
         total.push(winData);
         if (!winData.isPopup)
           nonPopupCount++;
       }
     }
 
+    // shallow copy this._closedWindows to preserve current state
+    let lastClosedWindowsCopy = this._closedWindows.slice();
+
 #ifndef XP_MACOSX
     // if no non-popup browser window remains open, return the state of the last closed window(s)
-    if (nonPopupCount == 0 && this._lastClosedWindows) {
+    if (nonPopupCount == 0 && lastClosedWindowsCopy.length > 0) {
       // prepend the last non-popup browser window, so that if the user loads more tabs
       // at startup we don't accidentally add them to a popup window
-      total = this._lastClosedWindows.concat(total);
+      do {
+        total.unshift(lastClosedWindowsCopy.shift())
+      } while (total[0].isPopup)
     }
+#endif
 
-#endif
     if (activeWindow) {
       this.activeWindowSSiCache = activeWindow.__SSi || "";
     }
     ix = this.activeWindowSSiCache ? windows.indexOf(this.activeWindowSSiCache) : -1;
-    return { windows: total, selectedWindow: ix + 1 };
+
+    return { windows: total, selectedWindow: ix + 1, _closedWindows: lastClosedWindowsCopy };
   },
 
   /**
    * serialize session data for a window
    * @param aWindow
    *        Window reference
    * @returns string
    */
@@ -1546,16 +1576,19 @@ SessionStoreService.prototype = {
       }
     }
     catch (ex) { // invalid state object - don't restore anything
       debug(ex);
       this._notifyIfAllWindowsRestored();
       return;
     }
 
+    if (root._closedWindows)
+      this._closedWindows = root._closedWindows;
+
     var winData;
     if (!aState.selectedWindow) {
       aState.selectedWindow = 0;
     }
     // open new windows for all further window entries of a multi-window session
     // (unless they don't contain any tab data)
     for (var w = 1; w < root.windows.length; w++) {
       winData = root.windows[w];
@@ -2474,16 +2507,54 @@ SessionStoreService.prototype = {
    * gets SeaMonkey's closed tabs
    * param aWindow
    *       Window reference
    */
   _getClosedTabs: function sss_getClosedTabs(aWindow) {
     return aWindow.getBrowser().savedBrowsers.map(function(e) { return e.tabData; });
   },
 
+  /**
+   * Replace "Loading..." with the tab label (with minimal side-effects)
+   * @param aString is the string the title is stored in
+   * @param aTabbrowser is a tabbrowser object, containing aTab
+   * @param aTab is the tab whose title we're updating & using
+   *
+   * @returns aString that has been updated with the new title
+   */
+  _replaceLoadingTitle : function sss_replaceLoadingTitle(aString, aTabbrowser, aTab) {
+    if (aString == aTabbrowser.mStringBundle.getString("tabs.loading")) {
+      aTabbrowser.setTabTitle(aTab);
+      [aString, aTab.label] = [aTab.label, aString];
+    }
+    return aString;
+  },
+
+  /**
+   * Resize this._closedWindows to the value of the pref, except in the case
+   * where we don't have any non-popup windows on Windows and Linux. Then we must
+   * resize such that we have at least one non-popup window.
+   */
+  _capClosedWindows : function sss_capClosedWindows() {
+    let maxWindowsUndo = this._prefBranch.getIntPref("sessionstore.max_windows_undo");
+    if (this._closedWindows.length <= maxWindowsUndo)
+      return;
+    let spliceTo = maxWindowsUndo;
+#ifndef XP_MACOSX
+    let normalWindowIndex = 0;
+    // try to find a non-popup window in this._closedWindows
+    while (normalWindowIndex < this._closedWindows.length &&
+           this._closedWindows[normalWindowIndex].isPopup)
+      normalWindowIndex++;
+    if (normalWindowIndex >= maxWindowsUndo)
+      spliceTo = normalWindowIndex + 1;
+#endif
+    this._closedWindows.splice(spliceTo);
+  },
+
 /* ........ Storage API .............. */
 
   /**
    * write file to disk
    * @param aFile
    *        nsIFile
    * @param aData
    *        String data