Bug 1491197 - replace progressmeter XBL binding by a custom element, r=paolo
authorAlexander Surkov <surkov.alexander@gmail.com>
Wed, 03 Oct 2018 11:13:05 +0500
changeset 495111 5543e7f5ddadba11a98f54fef214089ea1f8f3a0
parent 495110 0438e11c91251842a22125cbab18f44ef265fa9b
child 495112 cfbdabe4e05df6426e7d49164d77a4767a41b0c4
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspaolo
bugs1491197
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1491197 - replace progressmeter XBL binding by a custom element, r=paolo
accessible/tests/mochitest/value/test_progress.xul
browser/components/downloads/DownloadsViewUI.jsm
browser/components/downloads/content/download.xml
browser/themes/shared/downloads/progressmeter.inc.css
toolkit/content/customElements.js
toolkit/content/jar.mn
toolkit/content/widgets/progressmeter.js
toolkit/content/widgets/progressmeter.xml
toolkit/content/xul.css
toolkit/themes/linux/global/global.css
--- a/accessible/tests/mochitest/value/test_progress.xul
+++ b/accessible/tests/mochitest/value/test_progress.xul
@@ -17,16 +17,17 @@
   <script type="application/javascript">
   <![CDATA[
     function doTest()
     {
       // progressmeter
       testValue("pm1", "50%", 50, 0, 100, 0);
       testValue("pm2", "50%", 500, 0, 1000, 0);
       testValue("pm3", "", 0, 0, 100, 0);
+      testValue("pm4", "", 0, 0, 100, 0);
 
       // aria progressbar
       testValue("ariapb1", "500", 500, 0, 1000, 0);
       testValue("ariapb2", "", 0, 0, 0, 0);
 
       SimpleTest.finish();
     }
 
@@ -45,19 +46,22 @@
       <p id="display"></p>
       <div id="content" style="display: none">
       </div>
       <pre id="test">
       </pre>
     </body>
 
     <!-- progressmeter -->
-    <progressmeter id="pm1" value="50"/>
-    <progressmeter id="pm2" value="500" max="1000"/>
-    <progressmeter id="pm3"/>
+    <vbox>
+      <progressmeter id="pm1" value="50"/>
+      <progressmeter id="pm2" value="500" max="1000"/>
+      <progressmeter id="pm3"/>
+      <progressmeter id="pm4" mode="undetermined"/>
+    </vbox>
 
     <!-- aria -->
     <description id="ariapb1" role="progressbar"
                  aria-valuenow="500" aria-valuemin="0" aria-valuemax="1000"/>
     <description id="ariapb2" role="progressbar"/>
   </hbox>
 
 </window>
--- a/browser/components/downloads/DownloadsViewUI.jsm
+++ b/browser/components/downloads/DownloadsViewUI.jsm
@@ -9,16 +9,17 @@
 
 "use strict";
 
 var EXPORTED_SYMBOLS = [
   "DownloadsViewUI",
 ];
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
   Downloads: "resource://gre/modules/Downloads.jsm",
   DownloadUtils: "resource://gre/modules/DownloadUtils.jsm",
   DownloadsCommon: "resource:///modules/DownloadsCommon.jsm",
   FileUtils: "resource://gre/modules/FileUtils.jsm",
   OS: "resource://gre/modules/osfile.jsm",
@@ -423,18 +424,25 @@ this.DownloadsViewUI.DownloadElementShel
     }
 
     // These attributes are set in all code paths, because they are relevant for
     // downloads that are in progress and for other states where the progress
     // bar is visible.
     if (this.download.hasProgress) {
       this.element.setAttribute("progressmode", "normal");
       this.element.setAttribute("progress", this.download.progress);
+      this.element.removeAttribute("progress-undetermined");
     } else {
-      this.element.setAttribute("progressmode", "undetermined");
+      // Suppress the progress animation on Linux for the Downloads Panel
+      // progress bars when the file size is unknown.
+      this.element.setAttribute("progressmode",
+                                AppConstants.platform == "linux" ? "normal" :
+                                                                   "undetermined");
+      this.element.setAttribute("progress-undetermined", "true");
+      this.element.setAttribute("progress", "100");
     }
 
     if (progressPaused) {
       this.element.setAttribute("progresspaused", "true");
     } else {
       this.element.removeAttribute("progresspaused");
     }
 
--- a/browser/components/downloads/content/download.xml
+++ b/browser/components/downloads/content/download.xml
@@ -38,17 +38,17 @@
                -->
           <xul:description class="downloadTarget"
                            crop="center"
                            xbl:inherits="value=displayName,tooltiptext=displayName"/>
           <xul:progressmeter anonid="progressmeter"
                              class="downloadProgress"
                              min="0"
                              max="100"
-                             xbl:inherits="mode=progressmode,value=progress,paused=progresspaused"/>
+                             xbl:inherits="progress-undetermined,mode=progressmode,value=progress,paused=progresspaused"/>
           <xul:description class="downloadDetails downloadDetailsNormal"
                            crop="end"
                            xbl:inherits="value=status,tooltiptext=status"/>
           <xul:description class="downloadDetails downloadDetailsHover"
                            crop="end"
                            xbl:inherits="value=hoverStatus"/>
           <xul:description class="downloadDetails downloadDetailsButtonHover"
                            crop="end"
--- a/browser/themes/shared/downloads/progressmeter.inc.css
+++ b/browser/themes/shared/downloads/progressmeter.inc.css
@@ -8,33 +8,28 @@
   /* for overriding rules in progressmeter.css */
   -moz-appearance: none;
   border-style: none;
   background-color: transparent;
   min-width: initial;
   min-height: initial;
 }
 
-.downloadProgress[mode="undetermined"] {
-  /* for overriding rules on global.css in Linux. */
-  -moz-binding: url("chrome://global/content/bindings/progressmeter.xml#progressmeter");
-}
-
 .downloadProgress > .progress-bar {
   background-color: Highlight;
 
   /* for overriding rules in progressmeter.css */
   -moz-appearance: none;
 }
 
 .downloadProgress[paused="true"] > .progress-bar {
   background-color: GrayText;
 }
 
-.downloadProgress[mode="undetermined"] > .progress-bar {
+.downloadProgress[progress-undetermined] > .progress-bar {
   /* Make a white reflecting animation.
      Create a gradient with 2 identical pattern, and enlarge the size to 200%.
      This allows us to animate background-position with percentage. */
   background-image: linear-gradient(90deg, transparent 0%,
                                            rgba(255,255,255,0.5) 25%,
                                            transparent 50%,
                                            rgba(255,255,255,0.5) 75%,
                                            transparent 100%);
@@ -51,17 +46,17 @@
   border-inline-end-width: 1px;
   background-color: ButtonFace;
 }
 
 .downloadProgress[value="0"] > .progress-remainder {
   border-width: 1px;
 }
 
-.downloadProgress > .progress-remainder[mode="undetermined"] {
+.downloadProgress[progress-undetermined] > .progress-remainder {
   border: none;
 }
 
 @keyframes downloadProgressSlideX {
   0% {
     background-position: 0 0;
   }
   100% {
--- a/toolkit/content/customElements.js
+++ b/toolkit/content/customElements.js
@@ -196,16 +196,17 @@ window.MozXULElement = MozXULElement;
 window.MozBaseControl = MozBaseControl;
 
 // For now, don't load any elements in the extension dummy document.
 // We will want to load <browser> when that's migrated (bug 1441935).
 const isDummyDocument = document.documentURI == "chrome://extensions/content/dummy.xul";
 if (!isDummyDocument) {
   for (let script of [
     "chrome://global/content/elements/general.js",
+    "chrome://global/content/elements/progressmeter.js",
     "chrome://global/content/elements/radio.js",
     "chrome://global/content/elements/textbox.js",
     "chrome://global/content/elements/tabbox.js",
   ]) {
     Services.scriptloader.loadSubScript(script, window);
   }
 
   for (let [tag, script] of [
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -77,17 +77,16 @@ toolkit.jar:
 *  content/global/bindings/dialog.xml          (widgets/dialog.xml)
    content/global/bindings/general.xml         (widgets/general.xml)
    content/global/bindings/groupbox.xml        (widgets/groupbox.xml)
    content/global/bindings/menu.xml            (widgets/menu.xml)
    content/global/bindings/menulist.xml        (widgets/menulist.xml)
    content/global/bindings/notification.xml    (widgets/notification.xml)
    content/global/bindings/numberbox.xml       (widgets/numberbox.xml)
    content/global/bindings/popup.xml           (widgets/popup.xml)
-   content/global/bindings/progressmeter.xml   (widgets/progressmeter.xml)
    content/global/bindings/radio.xml           (widgets/radio.xml)
    content/global/bindings/richlistbox.xml     (widgets/richlistbox.xml)
    content/global/bindings/scrollbox.xml       (widgets/scrollbox.xml)
    content/global/bindings/spinner.js          (widgets/spinner.js)
 *  content/global/bindings/tabbox.xml          (widgets/tabbox.xml)
    content/global/bindings/text.xml            (widgets/text.xml)
 *  content/global/bindings/textbox.xml         (widgets/textbox.xml)
    content/global/bindings/timekeeper.js       (widgets/timekeeper.js)
@@ -95,16 +94,17 @@ toolkit.jar:
    content/global/bindings/toolbar.xml         (widgets/toolbar.xml)
    content/global/bindings/toolbarbutton.xml   (widgets/toolbarbutton.xml)
    content/global/bindings/tree.xml            (widgets/tree.xml)
    content/global/bindings/videocontrols.xml   (widgets/videocontrols.xml)
 *  content/global/bindings/wizard.xml          (widgets/wizard.xml)
    content/global/elements/findbar.js          (widgets/findbar.js)
    content/global/elements/editor.js          (widgets/editor.js)
    content/global/elements/general.js          (widgets/general.js)
+   content/global/elements/progressmeter.js    (widgets/progressmeter.js)
    content/global/elements/radio.js            (widgets/radio.js)
    content/global/elements/stringbundle.js     (widgets/stringbundle.js)
    content/global/elements/tabbox.js           (widgets/tabbox.js)
    content/global/elements/textbox.js          (widgets/textbox.js)
    content/global/elements/videocontrols.js    (widgets/videocontrols.js)
 #ifdef XP_MACOSX
    content/global/macWindowMenu.js
 #endif
rename from toolkit/content/widgets/progressmeter.xml
rename to toolkit/content/widgets/progressmeter.js
--- a/toolkit/content/widgets/progressmeter.xml
+++ b/toolkit/content/widgets/progressmeter.js
@@ -1,112 +1,149 @@
-<?xml version="1.0"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+"use strict";
+
+// This is loaded into chrome windows with the subscript loader. Wrap in
+// a block to prevent accidentally leaking globals onto `window`.
+{
 
-<bindings id="progressmeterBindings"
-   xmlns="http://www.mozilla.org/xbl"
-   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-   xmlns:xbl="http://www.mozilla.org/xbl">
+/**
+ * XUL progressmeter element.
+ */
+class MozProgressmeter extends MozXULElement {
+  get mode() {
+    return this.getAttribute("mode");
+  }
 
-  <binding id="progressmeter">
-    <content>
-      <xul:spacer class="progress-bar" xbl:inherits="mode"/>
-      <xul:spacer class="progress-remainder" xbl:inherits="mode"/>
-    </content>
+  set mode(val) {
+    if (this.mode != val) {
+      this.setAttribute("mode", val);
+    }
+    return val;
+  }
 
-    <implementation>
-      <property name="mode" onset="if (this.mode != val) this.setAttribute('mode', val); return val;"
-                            onget="return this.getAttribute('mode');"/>
+  get value() {
+    return this.getAttribute("value") || "0";
+  }
 
-      <property name="value" onget="return this.getAttribute('value') || '0';">
-        <setter><![CDATA[
-          var p = Math.round(val);
-          var max = Math.round(this.max);
-          if (p < 0)
-            p = 0;
-          else if (p > max)
-            p = max;
-          var c = this.value;
-          if (p != c) {
-            var delta = p - c;
-            if (delta < 0)
-              delta = -delta;
-            if (delta > 3 || p == 0 || p == max) {
-              this.setAttribute("value", p);
-              // Fire DOM event so that accessible value change events occur
-              var event = document.createEvent("Events");
-              event.initEvent("ValueChange", true, true);
-              this.dispatchEvent(event);
-            }
-          }
+  set value(val) {
+    let p = Math.round(val);
+    let max = Math.round(this.max);
+    if (p < 0) {
+      p = 0;
+    } else if (p > max) {
+      p = max;
+    }
+
+    let c = this.value;
+    if (p != c) {
+      let delta = p - c;
+      if (delta < 0) {
+        delta = -delta;
+      }
+      if (delta > 3 || p == 0 || p == max) {
+        this.setAttribute("value", p);
+        // Fire DOM event so that accessible value change events occur
+        let event = document.createEvent("Events");
+        event.initEvent("ValueChange", true, true);
+        this.dispatchEvent(event);
+      }
+    }
+
+    return val;
+  }
+
+  get max() {
+    return this.getAttribute("max") || "100";
+  }
+
+  set max(val) {
+    this.setAttribute("max", isNaN(val) ? 100 : Math.max(val, 1));
+    this.value = this.value;
+    return val;
+  }
 
-          return val;
-        ]]></setter>
-      </property>
-      <property name="max"
-                onget="return this.getAttribute('max') || '100';"
-                onset="this.setAttribute('max', isNaN(val) ? 100 : Math.max(val, 1));
-                       this.value = this.value;
-                       return val;" />
-    </implementation>
-  </binding>
+  isUndetermined() {
+    return this.getAttribute("mode") == "undetermined";
+  }
+
+  connectedCallback() {
+    this._initUI();
+  }
 
-  <binding id="progressmeter-undetermined"
-           extends="chrome://global/content/bindings/progressmeter.xml#progressmeter">
-    <content>
-      <xul:stack class="progress-remainder" flex="1" anonid="stack" style="overflow: -moz-hidden-unscrollable;">
-        <xul:spacer class="progress-bar" anonid="spacer" top="0" style="margin-right: -1000px;"/>
-      </xul:stack>
-    </content>
+  disconnectedCallback() {
+    this.runAnimation = false;
+  }
+
+  static get observedAttributes() {
+    return [ "mode" ];
+  }
+
+  attributeChangedCallback(name, oldValue, newValue) {
+    if (name === "mode" && oldValue != newValue) {
+      this._initUI();
+    }
+  }
 
-    <implementation>
-      <field name="_alive">true</field>
-      <method name="_init">
-        <body><![CDATA[
-          var stack =
-            document.getAnonymousElementByAttribute(this, "anonid", "stack");
-          var spacer =
-            document.getAnonymousElementByAttribute(this, "anonid", "spacer");
-          var isLTR =
-           document.defaultView.getComputedStyle(this).direction == "ltr";
-          var startTime = performance.now();
-          var self = this;
+  _initUI() {
+    let isUndetermined = this.isUndetermined();
+    let content = isUndetermined ?
+      `
+        <spacer class="progress-bar"/>
+        <spacer class="progress-remainder"/>
+      ` :
+      `
+        <stack class="progress-remainder" flex="1" style="overflow: -moz-hidden-unscrollable;">
+          <spacer class="progress-bar" top="0" style="margin-right: -1000px;"/>
+        </stack>
+      `;
+
+    this._stack = null;
+    this._spacer = null;
+    this._runAnimation = isUndetermined;
+
+    this.textContent = "";
+    this.appendChild(MozXULElement.parseXULToFragment(content));
 
-          function nextStep(t) {
-            try {
-              var width = stack.boxObject.width;
-              if (!width) {
-                // Maybe we've been removed from the document.
-                if (self._alive)
-                  requestAnimationFrame(nextStep);
-                return;
-              }
+    if (!isUndetermined) {
+      return;
+    }
+
+    this._stack = this.querySelector(".progress-remainder");
+    this._spacer = this.querySelector(".progress-bar");
+    this._isLTR = document.defaultView.getComputedStyle(this).direction == "ltr";
+    this._startTime = window.performance.now();
 
-              var elapsedTime = t - startTime;
+    let nextStep = (t) => {
+      if (!this._runAnimation) {
+        return;
+      }
 
-              // Width of chunk is 1/5 (determined by the ratio 2000:400) of the
-              // total width of the progress bar. The left edge of the chunk
-              // starts at -1 and moves all the way to 4. It covers the distance
-              // in 2 seconds.
-              var position = isLTR ? ((elapsedTime % 2000) / 400) - 1 :
+      let width = this._stack.boxObject.width;
+      if (width) {
+        let elapsedTime = t - this._startTime;
+
+        // Width of chunk is 1/5 (determined by the ratio 2000:400) of the
+        // total width of the progress bar. The left edge of the chunk
+        // starts at -1 and moves all the way to 4. It covers the distance
+        // in 2 seconds.
+        let position = this._isLTR ? ((elapsedTime % 2000) / 400) - 1 :
                                      ((elapsedTime % 2000) / -400) + 4;
 
-              width = width >> 2;
-              spacer.height = stack.boxObject.height;
-              spacer.width = width;
-              spacer.left = width * position;
+        width = width >> 2;
+        this._spacer.height = this._stack.boxObject.height;
+        this._spacer.width = width;
+        this._spacer.left = width * position;
+      }
 
-              requestAnimationFrame(nextStep);
-            } catch (e) {
-            }
-          }
-          requestAnimationFrame(nextStep);
-        ]]></body>
-      </method>
+      window.requestAnimationFrame(nextStep);
+    };
 
-      <constructor>this._init();</constructor>
-    </implementation>
-  </binding>
+    window.requestAnimationFrame(nextStep);
+  }
+}
 
-</bindings>
+customElements.define("progressmeter", MozProgressmeter);
+
+}
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -583,22 +583,16 @@ tabpanels {
 
 /********** tooltip *********/
 
 tooltip[titletip="true"] {
   /* The width of the tooltip isn't limited on cropped <tree> cells. */
   max-width: none;
 }
 
-/********** progressmeter **********/
-
-progressmeter {
-  -moz-binding: url("chrome://global/content/bindings/progressmeter.xml#progressmeter");
-}
-
 /********** basic rule for anonymous content that needs to pass box properties through
  ********** to an insertion point parent that holds the real kids **************/
 
 .box-inherit {
   -moz-box-orient: inherit;
   -moz-box-pack: inherit;
   -moz-box-align: inherit;
   -moz-box-direction: inherit;
--- a/toolkit/themes/linux/global/global.css
+++ b/toolkit/themes/linux/global/global.css
@@ -13,20 +13,16 @@
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 
 /* ::::: XBL bindings ::::: */
 
 menulist > menupopup {
   -moz-binding: url("chrome://global/content/bindings/popup.xml#popup-scrollbars");
 }
 
-progressmeter[mode="undetermined"] {
-  -moz-binding: url("chrome://global/content/bindings/progressmeter.xml#progressmeter-undetermined");
-}
-
 @media (-moz-menubar-drag) {
   toolbar[type="menubar"]:not([autohide="true"]) {
     -moz-binding: url("chrome://global/content/bindings/toolbar.xml#toolbar-drag");
   }
 }
 
 /* ::::: Variables ::::: */
 :root {