commit ddd8a6a78c56feeba2c8d5493edb6970ec148588
Author: Arthur Edelstein <arthuredelstein(a)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"];