Advertisement
Guest User

content-sizer.js

a guest
Nov 9th, 2015
244
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // The purpose of this file is to ensure that window.innerWidth and window.innerHeight
  2. // always return rounded values.
  3.  
  4. // This file is formatted for docco.js. Later functions call earlier ones.
  5.  
  6. /*
  7. TODO:
  8. * Decide on quantization amount. 100x100? 200x100? Maybe gradually increase, like 50, 100, 150, 200, 300, 500, 600, 800, etc.?
  9. * Understand gBrowser.contentWindow.document.body.getBoundingClientRect(). Does this leak some fingerprintable information?
  10. * Modify Tor Browser C++ code to allow precise setting of zoom? (Would allow more precise fit of content height in window.)
  11. */
  12.  
  13. /* jshint esnext: true */
  14.  
  15. // __quantizeBrowserSize(window, xStep, yStep)__.
  16. // Ensures that gBrowser width and height are multiples of
  17. // xStep and yStep.
  18. let quantizeBrowserSize = function (window, xStep, yStep) {
  19. "use strict";
  20.  
  21. // __currentDefaultZoom__.
  22. // The settings of gBrowser.fullZoom used to quantize the content window dimensions,
  23. // except if the user has pressed zoom+ or zoom-. Stateful.
  24. let currentDefaultZoom = 1;
  25.  
  26. // ## Utilities
  27.  
  28. // Mozilla abbreviations.
  29. let {classes: Cc, interfaces: Ci, results: Cr, Constructor: CC, utils: Cu } = Components;
  30.  
  31. // Use Task.jsm to avoid callback hell.
  32. Cu.import("resource://gre/modules/Task.jsm");
  33.  
  34. // Make the TorButton logger available.
  35. let logger = Cc["@torproject.org/torbutton-logger;1"]
  36.                .getService(Ci.nsISupports).wrappedJSObject;
  37.  
  38. // __torbuttonBundle__.
  39. // Bundle of localized strings for torbutton UI.
  40. let torbuttonBundle = Services.strings.createBundle(
  41.                         "chrome://torbutton/locale/torbutton.properties");
  42.  
  43. // Import utility functions
  44. let { bindPrefAndInit, getEnv } = Cu.import("resource://torbutton/modules/utils.js");
  45.  
  46. // __windowUtils(window)__.
  47. // See nsIDOMWindowUtils on MDN.
  48. let windowUtils = window => window.QueryInterface(Ci.nsIInterfaceRequestor)
  49.                                   .getInterface(Ci.nsIDOMWindowUtils);
  50.  
  51. // __isNumber(value)__.
  52. // Returns true iff the value is a number.
  53. let isNumber = x => typeof x === "number";
  54.  
  55. // __sortBy(array, scoreFn)__.
  56. // Returns a copy of the array, sorted from least to best
  57. // according to scoreFn.
  58. let sortBy = function (array, scoreFn) {
  59.   let compareFn = (a, b) => scoreFn(a) - scoreFn(b);
  60.   return array.slice().sort(compareFn);
  61. };
  62.  
  63. // __isMac__.
  64. // Constant, set to true if we are using a Mac (Darwin).
  65. let isMac = Services.appinfo.OS === "Darwin";
  66.  
  67. // __isWindows__.
  68. // Constant, set to true if we are using Windows.
  69. let isWindows = Services.appinfo.OS === "WINNT";
  70.  
  71. // __isTilingWindowManager__.
  72. // Constant, set to true if we are using a (known) tiling window
  73. // manager in linux.
  74. let isTilingWindowManager = (function () {
  75.   if (isMac || isWindows) return false;
  76.   let gdmSession = getEnv("GDMSESSION");
  77.   if (!gdmSession) return false;
  78.   let gdmSessionLower = gdmSession.toLowerCase();
  79.   return ["9wm", "alopex", "awesome", "bspwm", "catwm", "dswm", "dwm",
  80.           "echinus", "euclid-wm", "frankenwm", "herbstluftwm", "i3",
  81.           "i3wm", "ion", "larswm", "monsterwm", "musca", "notion",
  82.           "qtile", "ratpoison", "snapwm", "spectrwm", "stumpwm",
  83.           "subtle", "tinywm", "ttwm", "wingo", "wmfs", "wmii", "xmonad"]
  84.             .filter(x => x.startsWith(gdmSessionLower)).length > 0;
  85. })();
  86.  
  87. // __largestMultipleLessThan(factor, max)__.
  88. // Returns the largest number that is a multiple of factor
  89. // and is less or equal to max.
  90. let largestMultipleLessThan = function (factor, max) {
  91.   return Math.max(1, Math.floor(max / factor, 1)) * factor;
  92. };
  93.  
  94. // ## Task.jsm helper functions
  95.  
  96. // __sleep(timeMs)__.
  97. // Returns a Promise that sleeps for the specified time interval,
  98. // and returns an Event object of type "wake".
  99. let sleep = function (timeMs) {
  100.   return new Promise(function (resolve, reject) {
  101.     window.setTimeout(function () {
  102.       resolve(new Event("wake"));
  103.     }, timeMs);
  104.   });
  105. };
  106.  
  107. // __listen(target, eventType, useCapture, timeoutMs)__.
  108. // Listens for a single event of eventType on target.
  109. // Returns a Promise that resolves to an Event object, if the event fires.
  110. // If a timeout occurs, then Promise is rejected with a "Timed out" error.
  111. let listen = function (target, eventType, useCapture, timeoutMs) {
  112.   return new Promise(function (resolve, reject) {
  113.     let listenFunction = function (event) {
  114.       target.removeEventListener(eventType, listenFunction, useCapture);
  115.       resolve(event);
  116.     };
  117.     target.addEventListener(eventType, listenFunction, useCapture);
  118.     if (timeoutMs !== undefined && timeoutMs !== null) {
  119.       window.setTimeout(function () {
  120.         target.removeEventListener(eventType, listenFunction, useCapture);
  121.         resolve(new Event("timeout"));
  122.       }, timeoutMs);
  123.     }
  124.   });
  125. };
  126.  
  127. // __listenForTrueResize(window, timeoutMs)__.
  128. // Task.jsm function. Call `yield listenForTrueResize(window)` to
  129. // wait until the window changes its outer dimensions. Ignores
  130. // resize events where window dimensions are unchanged. Returns
  131. // the resize event object.
  132. let listenForTrueResize = function* (window, timeoutMs) {
  133.   let [originalWidth, originalHeight] = [window.outerWidth, window.outerHeight],
  134.       event,
  135.       finishTime = timeoutMs ? Date.now() + timeoutMs : null;
  136.   do {
  137.     event = yield listen(window, "resize", true,
  138.              finishTime ? finishTime - Date.now() : undefined);
  139.   } while (event.type === "resize" &&
  140.              originalWidth === window.outerWidth &&
  141.            originalHeight === window.outerHeight);
  142.   return event;
  143. };
  144.  
  145. // ## Window state queries
  146.  
  147. // __trueZoom(window)__.
  148. // Returns the true magnification of the content in the window
  149. // object. (In contrast, the `gBrowser.fullZoom` value is only approximated
  150. // by the display zoom.)
  151. let trueZoom = window => windowUtils(window).screenPixelsPerCSSPixel;
  152.  
  153. // __systemZoom__.
  154. // On Windows, if the user sets the DPI to be 125% or 150% (instead of 100%),
  155. // then we get an overall zoom that needs to be accounted for.
  156. let systemZoom = trueZoom(window);
  157.  
  158. // __canBeResized(window)__.
  159. // Returns true iff the window is in a state that can
  160. // be resized. Namely, not fullscreen, not maximized,
  161. // and not running in a tiling window manager.
  162. let canBeResized = function (window) {
  163.   // Note that window.fullScreen and (window.windowState === window.STATE_FULLSCREEN)
  164.   // sometimes disagree, so we only allow resizing when both are false.
  165.   return !isTilingWindowManager &&
  166.          !window.fullScreen &&
  167.          window.windowState === window.STATE_NORMAL;
  168. };
  169.  
  170. // __isDocked(window)__.
  171. // On Windows and some linux desktops, you can "dock" a window
  172. // at the right or left, so that it is maximized only in height.
  173. // Returns true in this case. (Note we use mozInnerScreenY instead
  174. // of screenY to take into account title bar space sometimes left
  175. // out of outerHeight in certain desktop environments.)
  176. let isDocked = window => ((window.mozInnerScreenY + window.outerHeight) >=
  177.                           (window.screen.availTop + window.screen.availHeight) &&
  178.                          (window.screenY <= window.screen.availTop));
  179.  
  180. // ## Window appearance
  181.  
  182. // __marginToolTip__.
  183. // A constant. The tooltip string shown in the margin.
  184. let marginToolTip = torbuttonBundle.GetStringFromName("torbutton.content_sizer.margin_tooltip");
  185.  
  186. // __updateContainerAppearance(container, on)__.
  187. // Get the color and position of margins correct.
  188. let updateContainerAppearance = function (container, on) {
  189.   // Align the browser at top left, so any gray margin will be visible
  190.   // at right and bottom. Except in fullscreen, where we have black
  191.   // margins and gBrowser in top center, and when using a tiling
  192.   // window manager, when we have gray margins and gBrowser in top
  193.   // center.
  194.   container.align = on ?
  195.                        (canBeResized(window) ? "start" : "center")
  196.                        : "";
  197.   container.pack = on ? "start" : "";
  198.   container.tooltipText = on ? marginToolTip : "";
  199. };
  200.  
  201. // __updateBackground(window)__.
  202. // Sets the margin background to black or dim gray, depending on
  203. // whether the window is full screen.
  204. let updateBackground = function (window) {
  205.   window.gBrowser.parentElement.style
  206.         .backgroundColor = window.fullScreen ? "Black" : "LightGray";
  207. };
  208.  
  209. // ## Window Zooming
  210.  
  211. // __computeTargetZoom(parentWidth, parentHeight, xStep, yStep, fillHeight)__.
  212. // Given a parent width and height for gBrowser's container, returns the
  213. // desired zoom for the content window.
  214. let computeTargetZoom = function (parentWidth, parentHeight, xStep, yStep, fillHeight) {
  215.   if (fillHeight) {
  216.     // Return the estimated zoom need to fill the height of the browser.
  217.     let h = largestMultipleLessThan(yStep, parentHeight);
  218.     return parentHeight / h;
  219.   } else {
  220.     // Here we attempt to find a zoom with the best fit for the window size that will
  221.     // provide a content window with appropriately quantized dimensions.
  222.     let w = largestMultipleLessThan(xStep, parentWidth),
  223.         h = largestMultipleLessThan(yStep, parentHeight),
  224.         parentAspectRatio = parentWidth / parentHeight,
  225.         possibilities = [[w, h],
  226.                          [Math.min(w, w - xStep), h],
  227.                          [w, Math.min(h - yStep)]],
  228.         // Find the [w, h] pair with the closest aspect ratio to the parent window.
  229.         score = ([w, h]) => Math.abs(Math.log(w / h / parentAspectRatio)),
  230.         [W, H] = sortBy(possibilities, score)[0];
  231.     // Return the estimated zoom.
  232.     return Math.min(parentHeight / H, parentWidth / W);
  233.   }
  234. };
  235.  
  236. // __updateDimensions(window, xStep, yStep)__.
  237. // Changes the width and height of the gBrowser XUL element to be a multiple of x/yStep.
  238. let updateDimensions = function (window, xStep, yStep) {
  239.   // Don't run if window is minimized.
  240.   if (window.windowState === window.STATE_MINIMIZED) return;
  241.   let gBrowser = window.gBrowser,
  242.       container = gBrowser.parentElement;
  243.   updateContainerAppearance(container, true);
  244.   let parentWidth = container.clientWidth,
  245.       parentHeight = container.clientHeight,
  246.       longPage = !gBrowser.contentWindow.fullScreen,
  247.       targetZoom = (canBeResized(window) && !isDocked(window)) ?
  248.                      1 : computeTargetZoom(parentWidth,
  249.                                            parentHeight, xStep, yStep, longPage),
  250.       zoomOffset = 1;
  251.   for (let i = 0; i < 8; ++i) {
  252.     // We set `gBrowser.fullZoom` to 99% of the needed zoom, unless
  253.     // it's `1`. That's because the "true zoom" is sometimes larger
  254.     // than fullZoom, and we need to ensure the gBrowser width and
  255.     // height do not exceed the container size.
  256.     gBrowser.fullZoom = (targetZoom === 1 ? 1 : 0.99) * targetZoom * zoomOffset;
  257.     currentDefaultZoom = gBrowser.fullZoom;
  258.     let zoom = trueZoom(gBrowser.contentWindow) / systemZoom,
  259.     targetContentWidth = largestMultipleLessThan(xStep, parentWidth / zoom),
  260.     targetContentHeight = largestMultipleLessThan(yStep, parentHeight / zoom),
  261.     targetBrowserWidth = Math.round(targetContentWidth * zoom),
  262.     targetBrowserHeight = Math.round(targetContentHeight * zoom);
  263.     // Because gBrowser is inside a vbox, width and height behave differently. It turns
  264.     // out we need to set `gBrowser.width` and `gBrowser.maxHeight`.
  265.     gBrowser.width = targetBrowserWidth;
  266.     gBrowser.maxHeight = targetBrowserHeight;
  267.     // When using Windows DPI != 100%, we can get rounding errors. We'll need
  268.     // to try again if we failed to get rounded content width x height.
  269.     // Unfortunately, this is not detectable if search bar or dev console is open.
  270.     if ((// Some weird sidebar is open, or
  271.          gBrowser.clientWidth !== gBrowser.selectedBrowser.clientWidth ||
  272.          // content width is correct.
  273.          gBrowser.contentWindow.innerWidth === targetContentWidth) &&
  274.         (// Search bar or dev console is open, or
  275.          gBrowser.clientHeight !== gBrowser.selectedBrowser.clientHeight ||
  276.          // content height is correct.
  277.          gBrowser.contentWindow.innerHeight === targetContentHeight)) {
  278.       logger.eclog(3,
  279.            " chromeWin " + window.outerWidth + "x" +  window.outerHeight +
  280.            " container " + parentWidth + "x" + parentHeight +
  281.            " gBrowser.fullZoom " + gBrowser.fullZoom + "X" +
  282.            " targetContent " + targetContentWidth + "x" + targetContentHeight +
  283.            " zoom " + zoom + "X" +
  284.            " targetBrowser " + targetBrowserWidth + "x" + targetBrowserHeight +
  285.            " gBrowser " + gBrowser.clientWidth + "x" + gBrowser.clientHeight +
  286.            " content " + gBrowser.contentWindow.innerWidth + "x" +  gBrowser.contentWindow.innerHeight);
  287.          break;
  288.      }
  289.     zoomOffset *= 1.02;
  290.   }
  291. };
  292.  
  293. // __resetZoomOnDomainChanges(gBrowser, on)__.
  294. // If `on` is true, then every time a tab location changes
  295. // to a new domain, the tab's zoom level is set back to the
  296. // "default zoom" level.
  297. let resetZoomOnDomainChanges = (function () {
  298.   let tabToDomainMap = new Map(),
  299.       onLocationChange = function (browser) {
  300.         let lastHost = tabToDomainMap.get(browser),
  301.             currentHost = browser &&
  302.                           browser.currentURI &&
  303.                           browser.currentURI.asciiHost;
  304.         if (lastHost !== currentHost) {
  305.           browser.fullZoom = currentDefaultZoom;
  306.           // Record the tab's current domain, so that we
  307.           // can see when it changes.
  308.           tabToDomainMap.set(browser, currentHost);
  309.         }
  310.       },
  311.       listener = { onLocationChange : onLocationChange };
  312.   return function (gBrowser, on) {
  313.     if (on) {
  314.       gBrowser.addTabsProgressListener(listener);
  315.     } else {
  316.       gBrowser.removeTabsProgressListener(listener);
  317.     }
  318.   };
  319. })();
  320.  
  321. // ## Window Resizing
  322.  
  323. // __reshape(window, {left, top, width, height}, timeoutMs)__.
  324. // Reshapes the window to rectangle {left, top, width, height} and yields
  325. // until the window reaches its target size, or the timeout occurs.
  326. let reshape = function* (window, {left, top, width, height}, timeoutMs) {
  327.   let finishTime = Date.now() + timeoutMs,
  328.       x = isNumber(left) ? left : window.screenX,
  329.       y = isNumber(top) ? top : window.screenY,
  330.       w = isNumber(width) ? width : window.outerWidth,
  331.       h = isNumber(height) ? height : window.outerHeight;
  332.   // Make sure we are in a new event.
  333.   yield sleep(0);
  334.   // Sometimes we get a race condition in linux when maximizing,
  335.   // so check again at the last minute that resizing is allowed.
  336.   if (!canBeResized(window)) return;
  337.   if (w !== window.outerWidth || h !== window.outerHeight) {
  338.     window.resizeTo(w, h);
  339.   }
  340.   if (x !== window.screenX || y !== window.screenY) {
  341.     window.moveTo(x, y);
  342.   }
  343.   // Yield until we have the correct screen position and size, or
  344.   // we timeout. Multiple resize events often fire in a resize.
  345.   while (x !== window.screenX ||
  346.          y !== window.screenY ||
  347.          w !== window.outerWidth ||
  348.          h !== window.outerHeight) {
  349.     let timeLeft = finishTime - Date.now();
  350.     if (timeLeft <= 0) break;
  351.     yield listenForTrueResize(window, timeLeft);
  352.   }
  353. };
  354.  
  355. // __gaps(window)__.
  356. // Deltas between gBrowser and its container. Returns null if there is no gap.
  357. let gaps = function (window) {
  358.   let gBrowser = window.gBrowser,
  359.       container = gBrowser.parentElement,
  360.       deltaWidth = Math.max(0, container.clientWidth - gBrowser.clientWidth),
  361.       deltaHeight = Math.max(0, container.clientHeight - gBrowser.clientHeight);
  362.   return (deltaWidth === 0 && deltaHeight === 0) ? null
  363.            : { deltaWidth : deltaWidth, deltaHeight : deltaHeight };
  364. };
  365.  
  366. // __shrinkwrap(window)__.
  367. // Shrinks the window so that it encloses the gBrowser with no gaps.
  368. let shrinkwrap = function* (window) {
  369.   // Figure out what size change we need.
  370.   let currentGaps = gaps(window),
  371.       screenRightEdge = window.screen.availWidth + window.screen.availLeft,
  372.       windowRightEdge = window.screenX + window.outerWidth;
  373.   if (currentGaps) {
  374.     // Now resize to close the gaps.
  375.     yield reshape(window,
  376.                   {width : (window.outerWidth - currentGaps.deltaWidth),
  377.                    // Shrink in height only if we are not docked.
  378.                    height : !isDocked(window) ?
  379.                               (window.outerHeight -
  380.                                currentGaps.deltaHeight) : null,
  381.                    left : (isDocked(window) &&
  382.                            (windowRightEdge >= screenRightEdge)) ?
  383.                              (window.screenX + currentGaps.deltaWidth)
  384.                              : null },
  385.                   500);
  386.   }
  387. };
  388.  
  389. // __rebuild(window)__.
  390. // Jog the size of the window slightly, to remind the window manager
  391. // to redraw the window.
  392. let rebuild = function* (window) {
  393.   let h = window.outerHeight;
  394.   yield reshape(window, {height : (h + 1)}, 300);
  395.   yield reshape(window, {height : h}, 300);
  396. };
  397.  
  398. // __fixWindow(window)__.
  399. // An async function for Task.jsm. Makes sure the window looks okay
  400. // given the quantized browser element.
  401. let fixWindow = function* (window) {
  402.   if (canBeResized(window)) {
  403.     yield shrinkwrap(window);
  404.     if (!isMac && !isWindows) {
  405.       // Unfortunately, on some linux desktops,
  406.       // the window resize fails if the user is still holding on
  407.       // to the drag-resize handle. Even more unfortunately, the
  408.       // only way to know that the user if finished dragging
  409.       // if we detect the mouse cursor inside the window or the
  410.       // user presses a key.
  411.       // So, after the first mousemove, or keydown event occurs, we
  412.       // rebuild the window.
  413.       let event = yield Promise.race(
  414.         [listen(window, "mousemove", true),
  415.          listen(window, "keydown", true),
  416.          listen(window, "resize", true)]);
  417.       if (event !== "resize") {
  418.         yield rebuild(window);
  419.       }
  420.       return event;
  421.     }
  422.   }
  423. };
  424.  
  425. // __autoresize(window, stepMs)__.
  426. // Automatically resize the gBrowser, and then shrink the window
  427. // if the user has attempted to resize it.
  428. let autoresize = function (window, stepMs, xStep, yStep) {
  429.   let stop = false;
  430.   Task.spawn(function* () {
  431.     // Fix the content dimensions once at startup, and
  432.     // keep updating the dimensions whenever the user resizes
  433.     // the window.
  434.     while (!stop) {
  435.       updateDimensions(window, xStep, yStep);
  436.       let event = yield fixWindow(window);
  437.       // Do nothing until the user starts to resize window.
  438.       if ((!event || event.type !== "resize") && !stop) {
  439.         event = yield listenForTrueResize(window);
  440.       }
  441.       if (!isTilingWindowManager) {
  442.         while (event.type !== "timeout" && !stop) {
  443.           if (!stop) {
  444.             updateDimensions(window, xStep, yStep);
  445.             event = yield listenForTrueResize(window, stepMs);
  446.           }
  447.         }
  448.       }
  449.       // The user has likely released the mouse cursor on the window's
  450.       // drag/resize handle, so loop and call fixWindow.
  451.     }
  452.   });
  453.   return () => { stop = true; };
  454. };
  455.  
  456. // ## Main Function
  457.  
  458. // __quantizeBrowserSizeMain(window, xStep, yStep)__.
  459. // Ensures that gBrowser width and height are multiples of xStep and yStep, and always as
  460. // large as possible inside the chrome window.
  461. let quantizeBrowserSizeMain = function (window, xStep, yStep) {
  462.   let gBrowser = window.gBrowser,
  463.       container = window.gBrowser.parentElement,
  464.       fullscreenHandler = function () {
  465.         // Use setTimeout to make sure we only update dimensions after
  466.         // full screen mode is fully established.
  467.         window.setTimeout(function () {
  468.           updateDimensions(window, xStep, yStep);
  469.             updateBackground(window);
  470.         }, 0);
  471.       },
  472.       originalMinWidth = container.minWidth,
  473.       originalMinHeight = container.minHeight,
  474.       stopAutoresizing,
  475.       activate = function (on) {
  476.         console.log("activate:", on);
  477.         // Don't let the browser shrink below a single xStep x yStep size.
  478.         container.minWidth = on ? xStep : originalMinWidth;
  479.         container.minHeight = on ? yStep : originalMinHeight;
  480.         updateContainerAppearance(container, on);
  481.         updateBackground(window);
  482.         resetZoomOnDomainChanges(gBrowser, on);
  483.         if (on) {
  484.           shrinkwrap(window);
  485.           window.addEventListener("sizemodechange", fullscreenHandler, false);
  486.           stopAutoresizing = autoresize(window,
  487.                                         (isMac || isWindows) ? 250 : 500,
  488.                                         xStep, yStep);
  489.           console.log("activated");
  490.         } else {
  491.           if (stopAutoresizing) stopAutoresizing();
  492.           // Ignore future resize events.
  493.           window.removeEventListener("sizemodechange", fullscreenHandler, false);
  494.           // Let gBrowser expand with its parent vbox.
  495.           gBrowser.width = "";
  496.           gBrowser.maxHeight = "";
  497.           console.log("deactivated");
  498.         }
  499.      };
  500.   let unbind = bindPrefAndInit("extensions.torbutton.resize_windows", activate);
  501.   window.addEventListener("unload", unbind, true);
  502. };
  503.  
  504. quantizeBrowserSizeMain(window, xStep, yStep);
  505.  
  506. // end of quantizeBrowserSize definition
  507. };
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement