bug 386363 - Remember zoom value on a site-specific basis, use toolkit ZoomManager, r=Neil
authorRobert Kaiser <kairo@kairo.at>
Thu, 09 Dec 2010 17:57:22 +0100
changeset 6808 61c697891f6c6ca8ccb02a4b2f2e09b260843659
parent 6807 73479070b3a19eedf749672d522c43c69a5151d1
child 6809 350d86993c5c0618079077db666ecb8dd2c0dbd5
push idunknown
push userunknown
push dateunknown
reviewersNeil
bugs386363
bug 386363 - Remember zoom value on a site-specific basis, use toolkit ZoomManager, r=Neil
suite/browser/browser-prefs.js
suite/browser/nsBrowserStatusHandler.js
suite/common/viewZoomOverlay.js
suite/common/viewZoomOverlay.xul
suite/locales/en-US/chrome/common/viewZoomOverlay.properties
--- a/suite/browser/browser-prefs.js
+++ b/suite/browser/browser-prefs.js
@@ -311,18 +311,26 @@ pref("browser.translation.serviceDomain"
 pref("browser.backspace_action", 0);
 
 // Controls behavior of the "Add Exception" dialog launched from SSL error pages:
 // 0 - don't pre-populate anything.
 // 1 - pre-populate site URL, but don't fetch certificate.
 // 2 - pre-populate site URL and pre-fetch certificate.
 pref("browser.ssl_override_behavior", 2);
 
+// if true, use full page zoom instead of text zoom
 pref("browser.zoom.full", true);
 
+// Whether or not to save and restore zoom levels on a per-site basis.
+pref("browser.zoom.siteSpecific", true);
+
+// Whether or not to update background tabs to the current zoom level
+// once they come to the foreground (i.e. get activated).
+pref("browser.zoom.updateBackgroundTabs", true);
+
 pref("javascript.options.showInConsole",    true);
 
 pref("offline.startup_state",            0);
 pref("offline.send.unsent_messages",            0);
 pref("offline.download.download_messages",  0);
 
 pref("browser.formfill.expire_days",        180);
 
--- a/suite/browser/nsBrowserStatusHandler.js
+++ b/suite/browser/nsBrowserStatusHandler.js
@@ -373,16 +373,21 @@ nsBrowserStatusHandler.prototype =
            (aRequest && !Components.isSuccessCode(aRequest.status))))
         this.popupNotifications.locationChange();
 
       PlacesStarButton.updateState();
 
       this.feedsMenu.setAttribute("disabled", "true");
       this.feedsButton.hidden = true;
       this.feeds = [];
+
+      // When background tab comes into foreground or loading a new page
+      // (aRequest set), might want to update zoom.
+      if (FullZoom.updateBackgroundTabs || aRequest)
+        FullZoom.onLocationChange(getBrowser().currentURI, !aRequest, browser);
     }
     UpdateBackForwardButtons();
 
     UpdateStatusBarPopupIcon();
   },
 
   onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage)
   {
--- a/suite/common/viewZoomOverlay.js
+++ b/suite/common/viewZoomOverlay.js
@@ -18,294 +18,419 @@
  * The Initial Developer of the Original Code is
  * Peter Annema.
  * Portions created by the Initial Developer are Copyright (C) 2000
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *   Peter Annema <disttsc@bart.nl> (Original Author)
  *   Jonas Sicking <sicking@bigfoot.com>
+ *   Myk Melez <myk@mozilla.org>
+ *   Dão Gottwald <dao@mozilla.com>
+ *   Ehsan Akhgari <ehsan.akhgari@gmail.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either of 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 ***** */
 
-/** Document Zoom Management Code
- *
- * To use this, you'll need to have a <menu id="menu_zoom"/>
- * and a getMarkupDocumentViewer() function which returns a
- * nsIMarkupDocumentViewer.
- *
- **/
+// One of the possible values for the mousewheel.* preferences.
+// From nsEventStateManager.cpp.
+const MOUSE_SCROLL_ZOOM = 3;
 
-function ZoomManager() {
-  this.bundle = document.getElementById("bundle_viewZoom");
+/**
+ * Controls the "full zoom" setting and its site-specific preferences.
+ */
+var FullZoom = {
+  // Identifies the setting in the content prefs database.
+  name: "browser.content.full-zoom",
 
-  // factorAnchor starts on factorOther
-  this.factorOther = parseInt(this.bundle.getString("valueOther"));
-  this.factorAnchor = this.factorOther;
-}
+  // The global value (if any) for the setting.  Lazily loaded from the service
+  // when first requested, then updated by the pref change listener as it changes.
+  // If there is no global value, then this should be undefined.
+  get globalValue() {
+    var globalValue = Services.contentPrefs.getPref(null, this.name);
+    if (typeof globalValue != "undefined")
+      globalValue = this._ensureValid(globalValue);
+    delete this.globalValue;
+    return this.globalValue = globalValue;
+  },
 
-ZoomManager.prototype = {
-  instance : null,
+  // browser.zoom.siteSpecific preference cache
+  _siteSpecificPref: undefined,
 
-  getInstance : function() {
-    if (!ZoomManager.prototype.instance)
-      ZoomManager.prototype.instance = new ZoomManager();
+  // browser.zoom.updateBackgroundTabs preference cache
+  updateBackgroundTabs: undefined,
 
-    return ZoomManager.prototype.instance;
+  get siteSpecific() {
+    return this._siteSpecificPref;
   },
 
-  MIN : 1,
-  MAX : 2000,
+  //**************************************************************************//
+  // nsISupports
 
-  bundle : null,
+  QueryInterface:
+  XPCOMUtils.generateQI([Components.interfaces.nsIDOMEventListener,
+                         Components.interfaces.nsIObserver,
+                         Components.interfaces.nsIContentPrefObserver,
+                         Components.interfaces.nsISupportsWeakReference,
+                         Components.interfaces.nsISupports]),
 
-  zoomFactorsString : "", // cache
-  zoomFactors : null,
+  //**************************************************************************//
+  // Initialization & Destruction
+
+  init: function FullZoom_init() {
+    // Listen for scrollwheel events so we can save scrollwheel-based changes.
+    window.addEventListener("DOMMouseScroll", this, false);
 
-  factorOther : 300,
-  factorAnchor : 300,
-  steps : 0,
+    // Register ourselves with the service so we know when our pref changes.
+    Services.contentPrefs.addObserver(this.name, this);
 
-  pref : Components.classes["@mozilla.org/preferences-service;1"]
-                   .getService(Components.interfaces.nsIPrefService)
-                   .getBranch(null),
+    this._siteSpecificPref =
+      Services.prefs.getBoolPref("browser.zoom.siteSpecific");
+    this.updateBackgroundTabs =
+      Services.prefs.getBoolPref("browser.zoom.updateBackgroundTabs");
+    // Listen for changes to the browser.zoom branch so we can enable/disable
+    // updating background tabs and per-site saving and restoring of zoom levels.
+    Services.prefs.addObserver("browser.zoom.", this, true);
+  },
 
-  get zoomType() {
-    return this.pref.getBoolPref("browser.zoom.full") ? "fullZoom" : "textZoom";
+  destroy: function FullZoom_destroy() {
+    Services.prefs.removeObserver("browser.zoom.", this);
+    Services.contentPrefs.removeObserver(this.name, this);
+    window.removeEventListener("DOMMouseScroll", this, false);
   },
 
-  get currentZoom() {
-    var currentZoom;
-    try {
-      currentZoom = Math.round(getMarkupDocumentViewer()[this.zoomType] * 100);
-      if (this.indexOf(currentZoom) == -1) {
-        if (currentZoom != this.factorOther) {
-          this.factorOther = currentZoom;
-          this.factorAnchor = this.factorOther;
-        }
-      }
-    } catch (e) {
-      currentZoom = 100;
+
+  //**************************************************************************//
+  // Event Handlers
+
+  // nsIDOMEventListener
+
+  handleEvent: function FullZoom_handleEvent(event) {
+    switch (event.type) {
+      case "DOMMouseScroll":
+        this._handleMouseScrolled(event);
+        break;
     }
-    return currentZoom;
-  },
-
-  set currentZoom(aZoom) {
-    if (aZoom < this.MIN || aZoom > this.MAX)
-      throw Components.results.NS_ERROR_INVALID_ARG;
-
-    getMarkupDocumentViewer()[this.zoomType] = aZoom / 100;
-  },
-
-  enlarge : function() {
-    this.jump(1);
-  },
-
-  reduce : function() {
-    this.jump(-1);
   },
 
-  reset : function() {
-    this.currentZoom = 100;
-  },
+  _handleMouseScrolled: function FullZoom_handleMouseScrolled(event) {
+    // Construct the "mousewheel action" pref key corresponding to this event.
+    // Based on nsEventStateManager::GetBasePrefKeyForMouseWheel.
+    var pref = "mousewheel";
+    if (event.axis == event.HORIZONTAL_AXIS)
+      pref += ".horizscroll";
+
+    if (event.shiftKey)
+      pref += ".withshiftkey";
+    else if (event.ctrlKey)
+      pref += ".withcontrolkey";
+    else if (event.altKey)
+      pref += ".withaltkey";
+    else if (event.metaKey)
+      pref += ".withmetakey";
+    else
+      pref += ".withnokey";
 
-  getZoomFactors : function() {
-    this.ensureZoomFactors();
+    pref += ".action";
 
-    return this.zoomFactors;
+    // Don't do anything if this isn't a "zoom" scroll event.
+    var isZoomEvent = false;
+    try {
+      isZoomEvent = (Services.prefs.getIntPref(pref) == MOUSE_SCROLL_ZOOM);
+    } catch (e) {}
+    if (!isZoomEvent)
+      return;
+
+    // XXX Lazily cache all the possible action prefs so we don't have to get
+    // them anew from the pref service for every scroll event?  We'd have to
+    // make sure to observe them so we can update the cache when they change.
+
+    // We have to call _applySettingToPref in a timeout because we handle
+    // the event before the event state manager has a chance to apply the zoom
+    // during nsEventStateManager::PostHandleEvent.
+    window.setTimeout(function (self) { self._applySettingToPref() }, 0, this);
   },
 
-  indexOf : function(aZoom) {
-    this.ensureZoomFactors();
+  // nsIObserver
 
-    var index = -1;
-    if (this.isZoomInRange(aZoom)) {
-      index = this.zoomFactors.length - 1;
-      while (index >= 0 && this.zoomFactors[index] != aZoom)
-        --index;
+  observe: function (aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case "nsPref:changed":
+        switch (aData) {
+          case "browser.zoom.siteSpecific":
+            this._siteSpecificPref =
+              Services.prefs.getBoolPref("browser.zoom.siteSpecific");
+            break;
+          case "browser.zoom.updateBackgroundTabs":
+            this.updateBackgroundTabs =
+              Services.prefs.getBoolPref("browser.zoom.updateBackgroundTabs");
+            break;
+        }
+        break;
     }
-
-    return index;
   },
 
-  /***** internal helper functions below here *****/
+  // nsIContentPrefObserver
 
-  ensureZoomFactors : function() {
-    var zoomFactorsString = this.bundle.getString("values");
-    if (this.zoomFactorsString != zoomFactorsString) {
-      this.zoomFactorsString = zoomFactorsString;
-      this.zoomFactors = zoomFactorsString.split(",");
-      for (var i = 0; i<this.zoomFactors.length; ++i)
-        this.zoomFactors[i] = parseInt(this.zoomFactors[i]);
+  onContentPrefSet: function FullZoom_onContentPrefSet(aGroup, aName, aValue) {
+    if (aGroup == Services.contentPrefs.grouper.group(getBrowser().currentURI))
+      this._applyPrefToSetting(aValue);
+    else if (aGroup == null) {
+      this.globalValue = this._ensureValid(aValue);
+
+      // If the current page doesn't have a site-specific preference,
+      // then its zoom should be set to the new global preference now that
+      // the global preference has changed.
+      if (!Services.contentPrefs.hasPref(getBrowser().currentURI, this.name))
+        this._applyPrefToSetting();
     }
   },
 
-  isLevelInRange : function(aLevel) {
-    return (aLevel >= 0 && aLevel < this.zoomFactors.length);
-  },
+  onContentPrefRemoved: function FullZoom_onContentPrefRemoved(aGroup, aName) {
+    if (aGroup == Services.contentPrefs.grouper.group(getBrowser().currentURI))
+      this._applyPrefToSetting();
+    else if (aGroup == null) {
+      this.globalValue = undefined;
 
-  isZoomInRange : function(aZoom) {
-    return (aZoom >= this.zoomFactors[0] && aZoom <= this.zoomFactors[this.zoomFactors.length - 1]);
+      // If the current page doesn't have a site-specific preference,
+      // then its zoom should be set to the default preference now that
+      // the global preference has changed.
+      if (!Services.contentPrefs.hasPref(getBrowser().currentURI, this.name))
+        this._applyPrefToSetting();
+    }
   },
 
-  jump : function(aDirection) {
-    if (aDirection != -1 && aDirection != 1)
-      throw Components.results.NS_ERROR_INVALID_ARG;
-
-    this.ensureZoomFactors();
+  // location change observer
 
-    var currentZoom = this.currentZoom;
-    var insertIndex = -1;
-    var stepFactor = parseFloat(this.bundle.getString("stepFactor"));
+  /**
+   * Called when the location of a tab changes.
+   * When that happens, we need to update the current zoom level if appropriate.
+   *
+   * @param aURI
+   *        A URI object representing the new location.
+   * @param aIsTabSwitch
+   *        Whether this location change has happened because of a tab switch.
+   * @param aBrowser
+   *        (optional) browser object displaying the document
+   */
+  onLocationChange: function FullZoom_onLocationChange(aURI, aIsTabSwitch, aBrowser) {
+    if (!aURI || !this.siteSpecific)
+      return;
 
-    // temporarily add factorOther to list
-    if (this.isZoomInRange(this.factorOther)) {
-      insertIndex = 0;
-      while (this.zoomFactors[insertIndex] < this.factorOther)
-        ++insertIndex;
-
-      if (this.zoomFactors[insertIndex] != this.factorOther)
-        this.zoomFactors.splice(insertIndex, 0, this.factorOther);
+    // Avoid the cps roundtrip and apply the default/global pref.
+    if (aURI.spec == "about:blank") {
+      this._applyPrefToSetting(undefined, aBrowser);
+      return;
     }
 
-    var factor;
-    var done = false;
+    var self = this;
+    Services.contentPrefs.getPref(aURI, this.name, function (aResult) {
+      // Check that we're still where we expect to be in case this took a while.
+      if (!aBrowser || aURI.equals(aBrowser.currentURI))
+        self._applyPrefToSetting(aResult, aBrowser);
+    });
+  },
 
-    if (this.isZoomInRange(currentZoom)) {
-      var index = this.indexOf(currentZoom);
-      if (aDirection == -1 && index == 0 ||
-          aDirection ==  1 && index == this.zoomFactors.length - 1) {
-        this.steps = 0;
-        this.factorAnchor = this.zoomFactors[index];
-      } else {
-        factor = this.zoomFactors[index + aDirection];
-        done = true;
-      }
-    }
+  // update state of zoom type menu item
+
+  updateMenu: function FullZoom_updateMenu() {
+    var menuItem = document.getElementById("toggle_zoom");
+
+    menuItem.setAttribute("checked", !ZoomManager.useFullZoom);
+  },
+
+  //**************************************************************************//
+  // Setting & Pref Manipulation
 
-    if (!done) {
-      this.steps += aDirection;
-      factor = this.factorAnchor * Math.pow(stepFactor, this.steps);
-      if (factor < this.MIN || factor > this.MAX) {
-        this.steps -= aDirection;
-        factor = this.factorAnchor * Math.pow(stepFactor, this.steps);
-      }
-      factor = Math.round(factor);
-      if (this.isZoomInRange(factor))
-        factor = this.snap(factor);
-      else
-        this.factorOther = factor;
-    }
+  reduce: function FullZoom_reduce() {
+    ZoomManager.reduce();
+    this._applySettingToPref();
+  },
+
+  enlarge: function FullZoom_enlarge() {
+    ZoomManager.enlarge();
+    this._applySettingToPref();
+  },
 
-    if (insertIndex != -1)
-      this.zoomFactors.splice(insertIndex, 1);
+  zoom: function FullZoom_zoom(aZoomValue) {
+    ZoomManager.zoom = aZoomValue;
+    this._applySettingToPref();
+  },
 
-    this.currentZoom = factor;
+  reset: function FullZoom_reset() {
+    if (typeof this.globalValue != "undefined")
+      ZoomManager.zoom = this.globalValue;
+    else
+      ZoomManager.reset();
+
+    this._removePref();
   },
 
-  snap : function(aZoom) {
-    if (this.isZoomInRange(aZoom)) {
-      var level = 0;
-      while (this.zoomFactors[level + 1] < aZoom)
-        ++level;
+  /**
+   * Set the zoom level for the current tab.
+   *
+   * Per nsPresContext::setFullZoom, we can set the zoom to its current value
+   * without significant impact on performance, as the setting is only applied
+   * if it differs from the current setting.  In fact getting the zoom and then
+   * checking ourselves if it differs costs more.
+   * 
+   * And perhaps we should always set the zoom even if it was more expensive,
+   * since DocumentViewerImpl::SetTextZoom claims that child documents can have
+   * a different text zoom (although it would be unusual), and it implies that
+   * those child text zooms should get updated when the parent zoom gets set,
+   * and perhaps the same is true for full zoom
+   * (although DocumentViewerImpl::SetFullZoom doesn't mention it).
+   *
+   * So when we apply new zoom values to the browser, we simply set the zoom.
+   * We don't check first to see if the new value is the same as the current
+   * one.
+   **/
+  _applyPrefToSetting: function FullZoom_applyPrefToSetting(aValue, aBrowser) {
+    if (!this.siteSpecific || window.gInPrintPreviewMode)
+      return;
 
-      // if aZoom closer to [level + 1] than [level], snap to [level + 1]
-      if ((this.zoomFactors[level + 1] - aZoom) < (aZoom - this.zoomFactors[level]))
-        ++level;
+    var browser = aBrowser || (getBrowser() && getBrowser().selectedBrowser);
+    try {
+      if (browser.contentDocument instanceof Components.interfaces.nsIImageDocument)
+        ZoomManager.setZoomForBrowser(browser, 1);
+      else if (typeof aValue != "undefined")
+        ZoomManager.setZoomForBrowser(browser, this._ensureValid(aValue));
+      else if (typeof this.globalValue != "undefined")
+        ZoomManager.setZoomForBrowser(browser, this.globalValue);
+      else
+        ZoomManager.setZoomForBrowser(browser, 1);
+    }
+    catch(ex) {}
+  },
+
+  _applySettingToPref: function FullZoom_applySettingToPref() {
+    if (!this.siteSpecific || window.gInPrintPreviewMode ||
+        content.document instanceof Components.interfaces.nsIImageDocument)
+      return;
+
+    var zoomLevel = ZoomManager.zoom;
+    Services.contentPrefs.setPref(getBrowser().currentURI, this.name, zoomLevel);
+  },
 
-      aZoom = this.zoomFactors[level];
-    }
+  _removePref: function FullZoom_removePref() {
+    if (!(content.document instanceof Components.interfaces.nsIImageDocument))
+      Services.contentPrefs.removePref(getBrowser().currentURI, this.name);
+  },
+
+
+  //**************************************************************************//
+  // Utilities
 
-    return aZoom;
+  _ensureValid: function FullZoom_ensureValid(aValue) {
+    if (isNaN(aValue))
+      return 1;
+
+    if (aValue < ZoomManager.MIN)
+      return ZoomManager.MIN;
+
+    if (aValue > ZoomManager.MAX)
+      return ZoomManager.MAX;
+
+    return aValue;
   }
-}
+};
 
 /***** init and helper functions for viewZoomOverlay.xul *****/
 window.addEventListener("load", registerZoomManager, false);
+window.addEventListener("unload", unregisterZoomManager, false);
 
-function registerZoomManager()
-{
+function registerZoomManager() {
+  FullZoom.init();
+
+  var zoomBundle = document.getElementById("bundle_viewZoom");
   var zoomMenu = document.getElementById("menu_zoom");
-  var zoom = ZoomManager.prototype.getInstance();
-
   var parentMenu = zoomMenu.parentNode;
   parentMenu.addEventListener("popupshowing", updateViewMenu, false);
 
+  var accessKeys = zoomBundle.getString("accessKeys").split(",");
+  var zoomFactors = zoomBundle.getString("values").split(",");
+
+  // Make sure the zoom manager has the same values as us
+  Services.prefs.setCharPref("toolkit.zoomManager.zoomValues",
+                             zoomFactors.map(function(aVal) {return aVal/100;})
+                                        .join(","));
+
   var insertBefore = document.getElementById("menu_zoomInsertBefore");
   var popup = insertBefore.parentNode;
-  var accessKeys = zoom.bundle.getString("accessKeys").split(",");
-  var zoomFactors = zoom.getZoomFactors();
   for (var i = 0; i < zoomFactors.length; ++i) {
     var menuItem = document.createElement("menuitem");
     menuItem.setAttribute("type", "radio");
     menuItem.setAttribute("name", "zoom");
 
     var label;
     if (zoomFactors[i] == 100) {
-      label = zoom.bundle.getString("labelOriginal");
+      label = zoomBundle.getString("labelOriginal");
       menuItem.setAttribute("key", "key_zoomReset");
     }
     else
-      label = zoom.bundle.getString("label");
+      label = zoomBundle.getString("label");
 
     menuItem.setAttribute("label", label.replace(/%zoom%/, zoomFactors[i]));
     menuItem.setAttribute("accesskey", accessKeys[i]);
     menuItem.setAttribute("value", zoomFactors[i]);
     popup.insertBefore(menuItem, insertBefore);
   }
 }
 
-function updateViewMenu()
-{
-  var zoom = ZoomManager.prototype.getInstance();
+function unregisterZoomManager() {
+  FullZoom.destroy();
+}
 
+function updateViewMenu() {
+  var zoomBundle = document.getElementById("bundle_viewZoom");
   var zoomMenu = document.getElementById("menu_zoom");
-  var menuLabel = zoom.bundle.getString(zoom.zoomType).replace(/%zoom%/, zoom.currentZoom);
+  var zoomType = ZoomManager.useFullZoom ? "fullZoom" : "textZoom";
+  var menuLabel = zoomBundle.getString(zoomType)
+                            .replace(/%zoom%/, Math.round(ZoomManager.zoom * 100));
   zoomMenu.setAttribute("label", menuLabel);
 }
 
-function updateZoomMenu()
-{
-  var zoom = ZoomManager.prototype.getInstance();
-
-  var currentZoom = zoom.currentZoom;
-
+function updateZoomMenu() {
+  var zoomBundle = document.getElementById("bundle_viewZoom");
   var zoomOther = document.getElementById("menu_zoomOther");
-  var label = zoom.bundle.getString("labelOther");
-  zoomOther.setAttribute("label", label.replace(/%zoom%/, zoom.factorOther));
-  zoomOther.setAttribute("value", zoom.factorOther);
+  var label = zoomBundle.getString("labelOther");
+  var factorOther = zoomOther.getAttribute("value") ||
+                    zoomBundle.getString("valueOther");
+  zoomOther.setAttribute("label", label.replace(/%zoom%/, factorOther));
+  zoomOther.setAttribute("value", factorOther);
 
   var popup = document.getElementById("menu_zoomPopup");
   var item = popup.firstChild;
   while (item) {
     if (item.getAttribute("name") == "zoom") {
-      if (item.getAttribute("value") == currentZoom)
+      if (item.getAttribute("value") == Math.round(ZoomManager.zoom * 100))
         item.setAttribute("checked","true");
       else
         item.removeAttribute("checked");
     }
     item = item.nextSibling;
   }
 }
 
-function setZoomOther()
-{
-  var zoom = ZoomManager.prototype.getInstance();
-
+function setZoomOther() {
+  var zoomOther = document.getElementById("menu_zoomOther");
   // open dialog and ask for new value
-  var o = {value: zoom.factorOther, zoomMin: zoom.MIN, zoomMax: zoom.MAX};
+  var o = {value: zoomOther.getAttribute("value"),
+           zoomMin: ZoomManager.MIN * 100,
+           zoomMax: ZoomManager.MAX * 100};
   window.openDialog("chrome://communicator/content/askViewZoom.xul",
                     "", "chrome,modal,centerscreen", o);
-  if (o.zoomOK)
-    zoom.currentZoom = o.value;
+  if (o.zoomOK) {
+    zoomOther.setAttribute("value", o.value);
+    ZoomManager.zoom = o.value / 100;
+  }
 }
--- a/suite/common/viewZoomOverlay.xul
+++ b/suite/common/viewZoomOverlay.xul
@@ -38,37 +38,39 @@
    - ***** END LICENSE BLOCK ***** -->
 
 <!DOCTYPE overlay SYSTEM "chrome://communicator/locale/viewZoomOverlay.dtd">
 
 <overlay id="viewZoomOverlay"
          xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
          xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
+  <script type="application/javascript" src="chrome://global/content/viewZoomOverlay.js"/>
   <script type="application/javascript" src="chrome://communicator/content/viewZoomOverlay.js"/>
 
   <stringbundle id="bundle_viewZoom" src="chrome://communicator/locale/viewZoomOverlay.properties"/>
 
   <keyset id="viewZoomKeys">
     <key id="key_zoomReduce"  key="&zoomReduceCmd.commandkey;"   command="cmd_zoomReduce"  modifiers="accel"/>
     <key id="key_zoomEnlarge" key="&zoomEnlargeCmd.commandkey;"  command="cmd_zoomEnlarge" modifiers="accel"/>
     <key                      key="&zoomEnlargeCmd.commandkey;"  command="cmd_zoomEnlarge" modifiers="accel,shift"/>
     <key                      key="&zoomEnlargeCmd.commandkey2;" command="cmd_zoomEnlarge" modifiers="accel"/>
     <key id="key_zoomReset"   key="&zoomResetCmd.commandkey;"    command="cmd_zoomReset"   modifiers="accel"/>
   </keyset>
 
   <commandset id="viewZoomCommands">
-    <command id="cmd_zoomReduce"  oncommand="ZoomManager.prototype.getInstance().reduce();"/>
-    <command id="cmd_zoomEnlarge" oncommand="ZoomManager.prototype.getInstance().enlarge();"/>
-    <command id="cmd_zoomReset"   oncommand="ZoomManager.prototype.getInstance().reset();"/>
+    <command id="cmd_zoomReduce"  oncommand="FullZoom.reduce();"/>
+    <command id="cmd_zoomEnlarge" oncommand="FullZoom.enlarge();"/>
+    <command id="cmd_zoomReset"   oncommand="FullZoom.reset();"/>
     <command id="cmd_zoomOther"   oncommand="setZoomOther();"/>
+    <command id="cmd_fullZoomToggle" oncommand="ZoomManager.toggleZoom();"/>
   </commandset>
 
   <menu id="menu_zoom" accesskey="&zoomMenu.accesskey;">
-    <menupopup id="menu_zoomPopup" onpopupshowing="updateZoomMenu();" oncommand="ZoomManager.prototype.getInstance().currentZoom = event.target.value;">
+    <menupopup id="menu_zoomPopup" onpopupshowing="updateZoomMenu();" oncommand="FullZoom.zoom(event.target.value / 100);">
       <menuitem id="menu_zoomReduce"
                 key="key_zoomReduce"
                 label="&zoomReduceCmd.label;"
                 accesskey="&zoomReduceCmd.accesskey;"
                 command="cmd_zoomReduce"/>
       <menuitem id="menu_zoomEnlarge"
                 key="key_zoomEnlarge"
                 label="&zoomEnlargeCmd.label;"
--- a/suite/locales/en-US/chrome/common/viewZoomOverlay.properties
+++ b/suite/locales/en-US/chrome/common/viewZoomOverlay.properties
@@ -13,14 +13,8 @@ labelOther=Other (%zoom% %) …
 # "Original size" in {labelOriginal}
 
 values=50,75,90,100,120,150,200
 accessKeys=5,7,9,z,1,0,2
 
 # {valueOther} must be greater than the largest in values
 
 valueOther=300
-
-# {stepFactor} is the factor with which the zoom changes when you're
-# below the lowest or above the highest value in {values}
-
-stepFactor=1.5
-