[tbb-commits] [torbutton/master] Bug #14429: Quantize content window inner width, height

mikeperry at torproject.org mikeperry at torproject.org
Fri Mar 27 02:41:24 UTC 2015


commit ddd8a6a78c56feeba2c8d5493edb6970ec148588
Author: Arthur Edelstein <arthuredelstein at gmail.com>
Date:   Sun Feb 8 00:13:36 2015 -0800

    Bug #14429: Quantize content window inner width, height
---
 src/chrome/content/content-sizer.js       |  286 +++++++++++++++++++++++++++++
 src/chrome/content/tor-circuit-display.js |   35 +---
 src/chrome/content/torbutton.js           |    2 +
 src/chrome/content/torbutton.xul          |    1 +
 src/defaults/preferences/preferences.js   |    1 +
 src/modules/utils.js                      |   43 +++++
 6 files changed, 336 insertions(+), 32 deletions(-)

diff --git a/src/chrome/content/content-sizer.js b/src/chrome/content/content-sizer.js
new file mode 100644
index 0000000..af6bc80
--- /dev/null
+++ b/src/chrome/content/content-sizer.js
@@ -0,0 +1,286 @@
+// The purpose of this file is to ensure that window.innerWidth and window.innerHeight
+// always return rounded values.
+
+// This file is formatted for docco.js. Later functions call earlier ones.
+
+/* jshint esnext: true */
+
+// __quantizeBrowserSizeOnLoad(window, xStep, yStep)__.
+// Once a window is fully loaded, ensures that gBrowser width and height are multiples of
+// xStep and yStep.
+let quantizeBrowserSizeOnLoad = function (window, xStep, yStep) {
+
+// Use Task.jsm to avoid callback hell.
+Cu.import("resource://gre/modules/Task.jsm");
+
+// Make the TorButton logger available.
+let logger = Cc["@torproject.org/torbutton-logger;1"]
+               .getService(Components.interfaces.nsISupports).wrappedJSObject;
+
+// Utility function
+let { bindPrefAndInit } = Cu.import("resource://torbutton/modules/utils.js");
+
+// __largestMultipleLessThan(factor, max)__.
+// Returns the largest number that is a multiple of factor
+// and is less or equal to max.
+let largestMultipleLessThan = function (factor, max) {
+  return Math.max(1, Math.floor((1 + max) / factor, 1)) * factor;
+};
+
+// __listen(target, eventType, useCapture, timeoutMs)__.
+// Listens for a single event of eventType on target.
+// Returns a Promise that resolves to an Event object, if the event fires.
+// If a timeout occurs, then Promise is rejected with a "Timed out" error.
+let listen = function (target, eventType, useCapture, timeoutMs) {
+  return new Promise(function (resolve, reject) {
+    let listenFunction = function (event) {
+      target.removeEventListener(eventType, listenFunction, useCapture);
+      resolve(event);
+    };
+    target.addEventListener(eventType, listenFunction, useCapture);
+    if (timeoutMs !== undefined && timeoutMs !== null) {
+      window.setTimeout(function () {
+        target.removeEventListener(eventType, listenFunction, useCapture);
+        resolve(new Event("timeout"));
+      }, timeoutMs);
+    }
+  });
+};
+
+// __sleep(time_ms)__.
+// Returns a Promise that sleeps for the specified time interval,
+// and returns an Event object of type "wake".
+let sleep = function (timeoutMs) {
+  return new Promise(function (resolve, reject) {
+    window.setTimeout(function () {
+      resolve(new Event("wake"));
+    }, timeoutMs);
+  });
+};
+
+// __isNumber(value)__.
+// Returns true iff the value is a number.
+let isNumber = x => typeof x === "number";
+
+// __reshape(window, {left, top, width, height}, timeoutMs)__.
+// Reshapes the window to rectangle {left, top, width, height} and yields
+// until the window reaches its target size, or the timeout occurs.
+let reshape = function* (window, {left, top, width, height}, timeoutMs) {
+  let finishTime = Date.now() + timeoutMs,
+      x = isNumber(left) ? left : window.screenX,
+      y = isNumber(top) ? top : window.screenY,
+      w = isNumber(width) ? width : window.outerWidth,
+      h = isNumber(height) ? height : window.outerHeight;
+  // Make sure we are in a new event.
+  yield sleep(0);
+  if (w !== window.outerWidth || h !== window.outerWidth) {
+    window.resizeTo(w, h);
+  }
+  if (x !== window.screenX || y !== window.screenY) {
+    window.moveTo(x, y);
+  }
+  // Yield until we have the correct screen position and size, or
+  // we timeout. Multiple resize events often fire in a resize.
+  while (x !== window.screenX ||
+         y !== window.screenY ||
+         w !== window.outerWidth ||
+         h !== window.outerHeight) {
+    let timeLeft = finishTime - Date.now();
+    if (timeLeft <= 0) break;
+    yield listen(window, "resize", true, timeLeft);
+  }
+};
+
+// __rebuild(window)__.
+// Jog the size of the window slightly, to remind the window manager
+// to redraw the window.
+let rebuild = function* (window) {
+  let h = window.outerHeight;
+  yield reshape(window, {height : (h + 1)}, 300);
+  yield reshape(window, {height : h}, 300);
+};
+
+// __gaps(window)__.
+// Deltas between gBrowser and its container. Returns null if there is no gap.
+let gaps = function (window) {
+  let gBrowser = window.gBrowser,
+      container = gBrowser.parentElement,
+      deltaWidth = Math.max(0, container.clientWidth - gBrowser.clientWidth - 1),
+      deltaHeight = Math.max(0, container.clientHeight - gBrowser.clientHeight - 1);
+  //logger.eclog(3, "gaps " + deltaWidth + "," + deltaHeight);
+  return (deltaWidth === 0 && deltaHeight === 0) ? null
+           : { deltaWidth : deltaWidth, deltaHeight : deltaHeight };
+};
+
+// __shrinkwrap(window)__.
+// Shrinks the window so that it encloses the gBrowser with no gaps.
+let shrinkwrap = function* (window) {
+  // Maximized windows in Linux and Windows need to be demaximized first.
+  if (gaps(window) &&
+      window.windowState === 1 && /* maximized */
+      Services.appinfo.OS !== "Darwin") {
+    if (Services.appinfo.OS !== "WINNT") {
+      // Linux windows need an extra jolt out of maximized mode.
+      window.moveBy(1,1);
+    }
+    // If window has been maximized, demaximize by shrinking it to
+    // fit within the available screen area.
+    yield reshape(window,
+                  {left : window.screen.availLeft + 1,
+                   top : window.screen.availTop + 1,
+                   width : window.screen.availWidth - 2,
+                   height : window.screen.availHeight - 2},
+                  500);
+  }
+  // Figure out what size change we need.
+  let currentGaps = gaps(window);
+  if (currentGaps) {
+    // Now resize to close the gaps.
+    yield reshape(window,
+                  {width : (window.outerWidth - currentGaps.deltaWidth),
+                   height : (window.outerHeight - currentGaps.deltaHeight)},
+                  500);
+  }
+};
+
+// __updateContainerAppearance(container, on)__.
+// Get the color and position of margins right.
+let updateContainerAppearance = function (container, on) {
+  // Align the browser at top left, so any gray margin will be visible
+  // at right and bottom. Except in fullscreen, where we have black
+  // margins and gBrowser in top center.
+  container.align = on ? (window.fullScreen ? "center" : "start")
+                       : "";
+  container.pack = on ? "start" : "";
+  container.style.backgroundColor = on ? (window.fullScreen ? "Black"
+                                                            : "DimGray")
+                                       : "";
+};
+
+// __fixWindow(window)__.
+// An async function for Task.jsm. Makes sure the window looks okay
+// given the quantized browser element.
+let fixWindow = function* (window) {
+  updateContainerAppearance(window.gBrowser.parentElement, true);
+  if (!window.fullScreen) {
+    yield shrinkwrap(window);
+    if (Services.appinfo.OS !== "Darwin" && Services.appinfo.OS !== "WINNT") {
+      // Linux tends to require us to rebuild the window, or we might be
+      // left with a large useless white area on the screen.
+      yield rebuild(window);
+    }
+  }
+};
+
+// __autoresize(window, stepMs)__.
+// Do what it takes to eliminate the gray margin around the gBrowser inside
+// window. Periodically (stepMs) attempt to shrink the window. Runs
+// as a Task.jsm coroutine.
+let autoresize = function (window, stepMs) {
+  let stop = false;
+  Task.spawn(function* () {
+    while (!stop) {
+      // Do nothing until the user starts to resize window.
+      let event = yield listen(window, "resize", true);
+      // Here we wrestle with the window size. If the user has released the
+      // mouse cursor on the window's drag/resize handle, then fixWindow
+      // will resize the window on its first call. Unfortunately, on some
+      // OSs, the window resize fails if the user is still holding on
+      // to the drag-resize handle. Even more unfortunately, the
+      // only way to know that the user no longer has the mouse down
+      // on the window's drag/resize handle is if we detect the mouse
+      // cursor inside the window. So until the window fires a mousemove
+      // event, we repeatedly call fixWindow every stepMs.
+      while (event.type !== "mousemove") {
+        event = yield Promise.race(
+                 [listen(window, "resize", true, stepMs),
+                  listen(window, "mousemove", true, stepMs)]);
+        // If the user has stopped resizing the window after `stepMs`, then we can resize
+        // the window so no gray margin is visible.
+        if (event.type === "timeout" || event.type === "mousemove") {
+          yield fixWindow(window);
+        }
+      }
+    }
+  });
+  return () => { stop = true; };
+};
+
+// __updateDimensions(gBrowser, xStep, yStep)__.
+// Changes the width and height of the gBrowser XUL element to be a multiple of x/yStep.
+let updateDimensions = function (gBrowser, xStep, yStep) {
+  // TODO: Get zooming to work such that it doesn't cause the window
+  // to continuously shrink.
+  // We'll use something like:
+  // let winUtils = gBrowser.contentWindow
+  //                 .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+  //                 .getInterface(Components.interfaces.nsIDOMWindowUtils),
+  //    zoom = winUtils.screenPixelsPerCSSPixel,
+  let zoom = 1,
+      parentWidth = gBrowser.parentElement.clientWidth,
+      parentHeight = gBrowser.parentElement.clientHeight,
+      targetContentWidth = largestMultipleLessThan(xStep, parentWidth / zoom),
+      targetContentHeight = largestMultipleLessThan(yStep, parentHeight / zoom),
+      targetBrowserWidth = targetContentWidth * zoom,
+      targetBrowserHeight = targetContentHeight * zoom;
+  // Because gBrowser is inside a vbox, width and height behave differently. It turns
+  // out we need to set `gBrowser.width` and `gBrowser.maxHeight`.
+  gBrowser.width = targetBrowserWidth;
+  gBrowser.maxHeight = targetBrowserHeight;
+  // If the content window's innerWidth/innerHeight failed to updated correctly,
+  // then jog the gBrowser width/height. (With zoom there may also be a rounding
+  // error, but we can't do much about that.)
+  if (gBrowser.contentWindow.innerWidth !== targetContentWidth ||
+      gBrowser.contentWindow.innerHeight !== targetContentHeight) {
+    gBrowser.width = targetBrowserWidth + 1;
+    gBrowser.maxHeight = gBrowser.targetBrowserHeight + 1;
+    gBrowser.width = targetBrowserWidth;
+    gBrowser.maxHeight = targetBrowserHeight;
+  }
+  logger.eclog(3, "zoom " + zoom + "X" +
+               " chromeWin " + window.outerWidth + "x" +  window.outerHeight +
+               " container " + parentWidth + "x" + parentHeight +
+	       " gBrowser " + gBrowser.clientWidth + "x" + gBrowser.clientHeight +
+               " content " + gBrowser.contentWindow.innerWidth + "x" +  gBrowser.contentWindow.innerHeight);
+};
+
+// __quantizeBrowserSizeNow(window, xStep, yStep)__.
+// Ensures that gBrowser width and height are multiples of xStep and yStep, and always as
+// large as possible inside the chrome window.
+let quantizeBrowserSizeNow = function (window, xStep, yStep) {
+  let gBrowser = window.gBrowser,
+      container = window.gBrowser.parentElement,
+      updater = event => updateDimensions(gBrowser, xStep, yStep),
+      originalMinWidth = gBrowser.minWidth,
+      originalMinHeight = gBrowser.minHeight,
+      stopAutoresizing,
+      activate = function (on) {
+        // Don't let the browser shrink below a single xStep x yStep size.
+        gBrowser.minWidth = on ? xStep : originalMinWidth;
+        gBrowser.minHeight = on ? yStep : originalMinHeight;
+        updateContainerAppearance(container, on);
+        if (on) {
+          // Quantize browser size on activation.
+          updateDimensions(gBrowser, xStep, yStep);
+          shrinkwrap(window);
+          // Quantize browser size at subsequent resize events.
+          window.addEventListener("resize", updater, false);
+          stopAutoresizing = autoresize(window, 250);
+        } else {
+          if (stopAutoresizing) stopAutoresizing();
+          // Ignore future resize events.
+          window.removeEventListener("resize", updater, false);
+          // Let gBrowser expand with its parent vbox.
+          gBrowser.width = "";
+          gBrowser.maxHeight = "";
+        }
+     };
+  bindPrefAndInit("extensions.torbutton.resize_windows", activate);
+};
+
+let onLoad = () => quantizeBrowserSizeNow(window, xStep, yStep);
+window.gBrowser.addEventListener("load", onLoad, true);
+return () => window.gBrowser.removeEventListener("load", onLoad, true);
+
+// quantizeBrowserSizeOnLoad
+};
diff --git a/src/chrome/content/tor-circuit-display.js b/src/chrome/content/tor-circuit-display.js
index 8997757..917f1f0 100644
--- a/src/chrome/content/tor-circuit-display.js
+++ b/src/chrome/content/tor-circuit-display.js
@@ -32,6 +32,9 @@ Cu.import("resource://gre/modules/Task.jsm");
 // Import the controller code.
 let { controller } = Cu.import("resource://torbutton/modules/tor-control-port.js");
 
+// Utility functions
+let { bindPrefAndInit } = Cu.import("resource://torbutton/modules/utils.js");
+
 // Make the TorButton logger available.
 let logger = Cc["@torproject.org/torbutton-logger;1"]
                .getService(Components.interfaces.nsISupports).wrappedJSObject;
@@ -303,38 +306,6 @@ let syncDisplayWithSelectedTab = (function() {
   };
 })();
 
-// ## Pref utils
-
-// __prefs__. A shortcut to Mozilla Services.prefs.
-let prefs = Services.prefs;
-
-// __getPrefValue(prefName)__
-// Returns the current value of a preference, regardless of its type.
-let getPrefValue = function (prefName) {
-  switch(prefs.getPrefType(prefName)) {
-    case prefs.PREF_BOOL: return prefs.getBoolPref(prefName);
-    case prefs.PREF_INT: return prefs.getIntPref(prefName);
-    case prefs.PREF_STRING: return prefs.getCharPref(prefName);
-    default: return null;
-  }
-};
-
-// __bindPrefAndInit(prefName, prefHandler)__
-// Applies prefHandler to the current value of pref specified by prefName.
-// Re-applies prefHandler whenever the value of the pref changes.
-// Returns a zero-arg function that unbinds the pref.
-let bindPrefAndInit = function (prefName, prefHandler) {
-  let update = () => { prefHandler(getPrefValue(prefName)); },
-      observer = { observe : function (subject, topic, data) {
-                     if (data === prefName) {
-                         update();
-                     }
-                   } };
-  prefs.addObserver(prefName, observer, false);
-  update();
-  return () => { prefs.removeObserver(prefName, observer); };
-};
-
 // ## Main function
 
 // setupDisplay(host, port, password, enablePrefName)__.
diff --git a/src/chrome/content/torbutton.js b/src/chrome/content/torbutton.js
index 83ea395..44f62aa 100644
--- a/src/chrome/content/torbutton.js
+++ b/src/chrome/content/torbutton.js
@@ -636,6 +636,8 @@ function torbutton_init() {
     createTorCircuitDisplay(m_tb_control_host, m_tb_control_port, m_tb_control_pass,
                             "extensions.torbutton.display_circuit");
 
+    quantizeBrowserSizeOnLoad(window, 200, 100);
+
     torbutton_log(3, 'init completed');
 }
 
diff --git a/src/chrome/content/torbutton.xul b/src/chrome/content/torbutton.xul
index 5da3439..a9d60f6 100644
--- a/src/chrome/content/torbutton.xul
+++ b/src/chrome/content/torbutton.xul
@@ -9,6 +9,7 @@
 
     <script type="application/x-javascript" src="chrome://torbutton/content/torbutton_util.js" />
     <script type="application/x-javascript" src="chrome://torbutton/content/tor-circuit-display.js" />
+    <script type="application/x-javascript" src="chrome://torbutton/content/content-sizer.js" />
     <script type="application/x-javascript" src="chrome://torbutton/content/torbutton.js" />
     <script language="JavaScript">
         //onLoad Hander
diff --git a/src/defaults/preferences/preferences.js b/src/defaults/preferences/preferences.js
index 2c109ee..85fd953 100644
--- a/src/defaults/preferences/preferences.js
+++ b/src/defaults/preferences/preferences.js
@@ -159,6 +159,7 @@ pref("extensions.torbutton.close_newnym",true);
 pref("extensions.torbutton.block_js_history",true);
 pref("extensions.torbutton.resize_on_toggle",true);
 pref("extensions.torbutton.resize_new_windows",true);
+pref("extensions.torbutton.resize_windows",true);
 pref("extensions.torbutton.banned_ports","9050,9051,9150,9151");
 pref("extensions.torbutton.block_tor_file_net",true);
 pref("extensions.torbutton.block_nontor_file_net",false);
diff --git a/src/modules/utils.js b/src/modules/utils.js
new file mode 100644
index 0000000..7a27326
--- /dev/null
+++ b/src/modules/utils.js
@@ -0,0 +1,43 @@
+// # Utils.js
+// Various helpful utility functions.
+
+// ### Shortcut
+const Cu = Components.utils;
+
+// ### Import Mozilla Services
+Cu.import("resource://gre/modules/Services.jsm");
+
+// ## Pref utils
+
+// __prefs__. A shortcut to Mozilla Services.prefs.
+let prefs = Services.prefs;
+
+// __getPrefValue(prefName)__
+// Returns the current value of a preference, regardless of its type.
+let getPrefValue = function (prefName) {
+  switch(prefs.getPrefType(prefName)) {
+    case prefs.PREF_BOOL: return prefs.getBoolPref(prefName);
+    case prefs.PREF_INT: return prefs.getIntPref(prefName);
+    case prefs.PREF_STRING: return prefs.getCharPref(prefName);
+    default: return null;
+  }
+};
+
+// __bindPrefAndInit(prefName, prefHandler)__
+// Applies prefHandler to the current value of pref specified by prefName.
+// Re-applies prefHandler whenever the value of the pref changes.
+// Returns a zero-arg function that unbinds the pref.
+let bindPrefAndInit = function (prefName, prefHandler) {
+  let update = () => { prefHandler(getPrefValue(prefName)); },
+      observer = { observe : function (subject, topic, data) {
+                     if (data === prefName) {
+                         update();
+                     }
+                   } };
+  prefs.addObserver(prefName, observer, false);
+  update();
+  return () => { prefs.removeObserver(prefName, observer); };
+};
+
+// Export utility functions for external use.
+let EXPORTED_SYMBOLS = ["bindPrefAndInit", "getPrefValue"];





More information about the tbb-commits mailing list