Bug 883959: Part 1: Show circular download progress indicator. r=sfoster.
authorMarina Samuel <msamuel@mozilla.com>
Thu, 01 Aug 2013 15:17:30 -0400
changeset 140947 9ac580e78e1092d73f72c4e9fc1cdb068c55506e
parent 140946 2ba2c2534fdbb747c4718b7332876e68d9563aa9
child 140948 9aac8dc7a9b9af2fa8ca155e728f35f1c4a936bb
push id2002
push usermsamuel@mozilla.com
push dateThu, 01 Aug 2013 22:17:01 +0000
treeherderfx-team@9aac8dc7a9b9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssfoster
bugs883959
milestone25.0a1
Bug 883959: Part 1: Show circular download progress indicator. r=sfoster.
browser/metro/base/content/bindings/circularprogress.xml
browser/metro/base/content/browser.css
browser/metro/base/content/browser.xul
browser/metro/base/content/downloads.js
browser/metro/base/jar.mn
browser/metro/base/tests/mochitest/Makefile.in
browser/metro/base/tests/mochitest/browser_circular_progress_indicator.js
browser/metro/base/tests/mochitest/browser_progress_indicator.xul
browser/metro/theme/browser.css
browser/metro/theme/images/progresscircle.png
browser/metro/theme/jar.mn
new file mode 100644
--- /dev/null
+++ b/browser/metro/base/content/bindings/circularprogress.xml
@@ -0,0 +1,78 @@
+<?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/. -->
+
+<bindings xmlns="http://www.mozilla.org/xbl"
+          xmlns:xbl="http://www.mozilla.org/xbl"
+          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+          xmlns:html="http://www.w3.org/1999/xhtml">
+  <binding id="circular-progress-indicator">
+    <content>
+      <xul:stack>
+        <xul:toolbarbutton anonid="progressButton" class="circularprogressindicator-progressButton appbar-secondary"/>
+        <html:canvas anonid="progressRing" class="circularprogressindicator-progressRing" width="46" height="46"></html:canvas>
+      </xul:stack>
+    </content>
+    <implementation>
+      <field name="_progressCanvas">
+          document.getAnonymousElementByAttribute(this, "anonid", "progressRing");
+      </field>
+      <field name="_progressCircleCtx">null</field>
+      <field name="_img">null</field>
+      <constructor>
+        <![CDATA[
+          this._progressCircleCtx = this._progressCanvas.getContext('2d');
+          this._img = new Image();
+        ]]>
+      </constructor>
+      <method name="updateProgress">
+        <parameter name="percentComplete"/>
+        <body>
+          <![CDATA[
+            const PROGRESS_RING_IMG = "chrome://browser/skin/images/progresscircle.png";
+
+            let startAngle = 1.5 * Math.PI;
+            let endAngle = startAngle + (2 * Math.PI * (percentComplete / 100));
+
+            let ctx = this._progressCircleCtx;
+            ctx.clearRect(0, 0,
+              this._progressCanvas.width, this._progressCanvas.height);
+
+            // Save the state, so we can undo the clipping
+            ctx.save();
+
+            ctx.beginPath();
+            let center = this._progressCanvas.width / 2;
+            ctx.arc(center, center, center, startAngle, endAngle, false);
+            ctx.lineTo(center, center);
+            ctx.closePath();
+            ctx.clip();
+
+            // Draw circle image.
+            if (this._img && this._img.src) {
+              ctx.drawImage(this._img, 0, 0);
+            } else {
+              this._img.onload = function() {
+                ctx.drawImage(this._img, 0, 0);
+              }.bind(this);
+              this._img.src = PROGRESS_RING_IMG;
+            }
+
+            ctx.restore();
+            return [startAngle, endAngle];
+          ]]>
+        </body>
+      </method>
+      <method name="reset">
+        <body>
+          <![CDATA[
+            this._progressCircleCtx.clearRect(0, 0,
+              this._progressCanvas.width, this._progressCanvas.height);
+          ]]>
+        </body>
+      </method>
+    </implementation>
+  </binding>
+</bindings>
\ No newline at end of file
--- a/browser/metro/base/content/browser.css
+++ b/browser/metro/base/content/browser.css
@@ -37,16 +37,20 @@ settings {
 setting {
   display: none;
 }
 
 autoscroller {
   -moz-binding: url('chrome://browser/content/bindings/popup.xml#element-popup');
 }
 
+circularprogressindicator {
+  -moz-binding: url('chrome://browser/content/bindings/circularprogress.xml#circular-progress-indicator');
+}
+
 setting[type="bool"] {
   display: -moz-box;
   -moz-binding: url("chrome://browser/content/bindings/toggleswitch.xml#setting-fulltoggle-bool");
 }
 
 setting[type="bool"][localized="true"] {
   display: -moz-box;
   -moz-binding: url("chrome://browser/content/bindings/toggleswitch.xml#setting-fulltoggle-localized-bool");
--- a/browser/metro/base/content/browser.xul
+++ b/browser/metro/base/content/browser.xul
@@ -291,18 +291,18 @@
             <toolbarbutton id="reload-button" class="urlbar-button"
                            oncommand="CommandUpdater.doCommand(
                                         event.shiftKey ? 'cmd_forceReload'
                                                        : 'cmd_reload');"/>
             <toolbarbutton id="stop-button" class="urlbar-button"
                            command="cmd_stop"/>
           </hbox>
 
-          <toolbarbutton id="download-button" class="appbar-secondary"
-                         oncommand="Appbar.onDownloadButton()"/>
+          <circularprogressindicator id="download-progress"
+                                     oncommand="Appbar.onDownloadButton()"/>
           <toolbarbutton id="star-button" class="appbar-primary" type="checkbox"
                          oncommand="Appbar.onStarButton()"/>
           <toolbarbutton id="pin-button" class="appbar-primary" type="checkbox"
                          oncommand="Appbar.onPinButton()"/>
           <toolbarbutton id="menu-button" class="appbar-primary"
                          oncommand="Appbar.onMenuButton(event)"/>
         </toolbar>
       </vbox>
--- a/browser/metro/base/content/downloads.js
+++ b/browser/metro/base/content/downloads.js
@@ -52,16 +52,18 @@ var Downloads = {
     Services.obs.addObserver(this, "dl-done", true);
     Services.obs.addObserver(this, "dl-run", true);
     Services.obs.addObserver(this, "dl-failed", true);
 
     this._notificationBox = Browser.getNotificationBox();
 
     this._progress = new DownloadProgressListener(this);
     this.manager.addListener(this._progress);
+
+    this._downloadProgressIndicator = document.getElementById("download-progress");
   },
 
   uninit: function dh_uninit() {
     if (this._inited) {
       Services.obs.removeObserver(this, "dl-start");
       Services.obs.removeObserver(this, "dl-done");
       Services.obs.removeObserver(this, "dl-run");
       Services.obs.removeObserver(this, "dl-failed");
@@ -191,23 +193,25 @@ var Downloads = {
 
     let buttons = [
       {
         isDefault: true,
         label: tryAgainButtonText,
         accessKey: "",
         callback: function() {
           Downloads.manager.retryDownload(aDownload.id);
+          Downloads._downloadProgressIndicator.reset();
         }
       },
       {
         label: cancelButtonText,
         accessKey: "",
         callback: function() {
           Downloads.cancelDownload(aDownload);
+          Downloads._downloadProgressIndicator.reset();
         }
       }
     ];
     this.showNotification("download-failed", message, buttons,
       this._notificationBox.PRIORITY_WARNING_HIGH);
   },
 
   _showDownloadCompleteNotification: function (aDownload) {
@@ -217,16 +221,17 @@ var Downloads = {
     let buttons = [
       {
         label: showInFilesButtonText,
         accessKey: "",
         callback: function() {
           let fileURI = aDownload.target;
           let file = Downloads._getLocalFile(fileURI);
           file.reveal();
+          Downloads._downloadProgressIndicator.reset();
         }
       }
     ];
 
     if (this._downloadCount > 1) {
       message = PluralForm.get(this._downloadCount,
                                Strings.browser.GetStringFromName("alertMultipleDownloadsComplete"))
                                .replace("#1", this._downloadCount)
@@ -237,34 +242,52 @@ var Downloads = {
         [aDownload.displayName], 1);
 
       buttons.unshift({
         isDefault: true,
         label: runButtonText,
         accessKey: "",
         callback: function() {
           Downloads.openDownload(aDownload);
+          Downloads._downloadProgressIndicator.reset();
         }
       });
     }
     this.showNotification("download-complete", message, buttons,
       this._notificationBox.PRIORITY_WARNING_MEDIUM);
   },
 
+  _updateCircularProgressMeter: function dv_updateCircularProgressMeter() {
+    if (!this._progressNotificationInfo) {
+      return;
+    }
+
+    let totPercent = 0;
+    for (let info of this._progressNotificationInfo) {
+      // info[0]          => download guid
+      // info[1].download => nsIDownload
+      totPercent += info[1].download.percentComplete;
+    }
+
+    let percentComplete = totPercent / this._progressNotificationInfo.size;
+    this._downloadProgressIndicator.updateProgress(percentComplete);
+  },
+
   _computeDownloadProgressString: function dv_computeDownloadProgressString(aDownload) {
     let totTransferred = 0, totSize = 0, totSecondsLeft = 0;
     for (let info of this._progressNotificationInfo) {
       let size = info[1].download.size;
       let amountTransferred = info[1].download.amountTransferred;
       let speed = info[1].download.speed;
 
       totTransferred += amountTransferred;
       totSize += size;
       totSecondsLeft += ((size - amountTransferred) / speed);
     }
+
     // Compute progress in bytes.
     let amountTransferred = Util.getDownloadSize(totTransferred);
     let size = Util.getDownloadSize(totSize);
     let progress = amountTransferred + "/" + size;
 
     // Compute progress in time.;
     let [timeLeft, newLast] = DownloadUtils.getTimeLeft(totSecondsLeft, this._lastSec);
     this._lastSec = newLast;
@@ -291,30 +314,32 @@ var Downloads = {
     let infoObj = this._progressNotificationInfo.get(aDownload.guid);
     infoObj.download = aDownload;
     this._progressNotificationInfo.set(aDownload.guid, infoObj);
   },
 
   updateInfobar: function dv_updateInfobar(aDownload) {
     this._saveDownloadData(aDownload);
     let message = this._computeDownloadProgressString(aDownload);
+    this._updateCircularProgressMeter();
 
     if (this._progressNotification == null ||
         !this._notificationBox.getNotificationWithValue("download-progress")) {
 
       let cancelButtonText =
               Strings.browser.GetStringFromName("downloadCancel");
 
       let buttons = [
         {
           isDefault: false,
           label: cancelButtonText,
           accessKey: "",
           callback: function() {
             Downloads.cancelDownloads();
+            Downloads._downloadProgressIndicator.reset();
           }
         }
       ];
 
       this._progressNotification =
         this.showNotification("download-progress", message, buttons,
         this._notificationBox.PRIORITY_WARNING_LOW);
     } else {
@@ -322,16 +347,17 @@ var Downloads = {
     }
   },
 
   updateDownload: function dv_updateDownload(aDownload) {
     if (this._progressNotification != null) {
       this._saveDownloadData(aDownload);
       this._progressNotification.label =
         this._computeDownloadProgressString(aDownload);
+      this._updateCircularProgressMeter();
     }
   },
 
   observe: function (aSubject, aTopic, aData) {
     let message = "";
     let msgTitle = "";
 
     switch (aTopic) {
--- a/browser/metro/base/jar.mn
+++ b/browser/metro/base/jar.mn
@@ -21,16 +21,17 @@ chrome.jar:
   content/bindings/arrowbox.xml                (content/bindings/arrowbox.xml)
   content/bindings/grid.xml                    (content/bindings/grid.xml)
   content/bindings/urlbar.xml                  (content/bindings/urlbar.xml)
   content/bindings/appbar.xml                  (content/bindings/appbar.xml)
   content/bindings/flyoutpanel.xml             (content/bindings/flyoutpanel.xml)
   content/bindings/selectionoverlay.xml        (content/bindings/selectionoverlay.xml)
   content/bindings/cssthrobber.xml             (content/bindings/cssthrobber.xml)
   content/bindings/popup.xml                   (content/bindings/popup.xml)
+  content/bindings/circularprogress.xml        (content/bindings/circularprogress.xml)
 
 * content/flyoutpanels/FlyoutPanelsUI.js       (content/flyoutpanels/FlyoutPanelsUI.js)
 * content/flyoutpanels/AboutFlyoutPanel.js     (content/flyoutpanels/AboutFlyoutPanel.js)
   content/flyoutpanels/PrefsFlyoutPanel.js     (content/flyoutpanels/PrefsFlyoutPanel.js)
 
   content/prompt/alert.xul                     (content/prompt/alert.xul)
   content/prompt/confirm.xul                   (content/prompt/confirm.xul)
   content/prompt/prompt.xul                    (content/prompt/prompt.xul)
--- a/browser/metro/base/tests/mochitest/Makefile.in
+++ b/browser/metro/base/tests/mochitest/Makefile.in
@@ -10,28 +10,30 @@ relativesrcdir  = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_METRO_FILES = \
   head.js \
   browser_urlbar.js \
   browser_bookmarks.js \
   browser_canonizeURL.js \
+  browser_circular_progress_indicator.js \
   browser_context_menu_tests.js \
   browser_context_menu_tests_01.html \
   browser_context_menu_tests_02.html \
   browser_context_menu_tests_03.html \
   browser_context_ui.js \
   browser_downloads.js \
   browser_findbar.js \
   browser_findbar.html \
   browser_history.js \
   browser_onscreen_keyboard.js \
   browser_onscreen_keyboard.html \
   browser_prefs_ui.js \
+  browser_progress_indicator.xul \
   browser_remotetabs.js \
   browser_tabs.js \
   browser_test.js \
   browser_tiles.js \
   browser_tilegrid.xul \
   browser_topsites.js \
   browser_form_auto_complete.js \
   browser_form_auto_complete.html \
new file mode 100644
--- /dev/null
+++ b/browser/metro/base/tests/mochitest/browser_circular_progress_indicator.js
@@ -0,0 +1,59 @@
+/* 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/. */
+
+let doc;
+
+function test() {
+  waitForExplicitFinish();
+  Task.spawn(function(){
+    netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+
+    info(chromeRoot + "browser_progress_indicator.xul");
+    yield addTab(chromeRoot + "browser_progress_indicator.xul");
+    doc = Browser.selectedTab.browser.contentWindow.document;
+  }).then(runTests);
+}
+
+gTests.push({
+  desc: "circular progress indicator binding is applied.",
+  run: function() {
+    ok(doc, "doc got defined");
+
+    let progressIndicator = doc.querySelector("#progress-indicator");
+    ok(progressIndicator, "progress-indicator is found");
+    is(typeof progressIndicator.reset, "function", "#progress-indicator has a reset() function");
+    is(typeof progressIndicator.updateProgress, "function", "#progress-indicator has a updateProgress() function");
+  }
+});
+
+gTests.push({
+  desc: "start and end angles are correct for various percents complete",
+  run: function() {
+    let progressIndicator = doc.querySelector("#progress-indicator");
+    ok(progressIndicator, "progress-indicator is found");
+    is(typeof progressIndicator.updateProgress, "function", "#progress-indicator has a updateProgress() function");
+
+    let expectedStartAngle = 1.5 * Math.PI;
+
+    let percentComplete = 0;
+    let [startAngle, endAngle] = progressIndicator.updateProgress(percentComplete);
+    is(startAngle, expectedStartAngle, "start angle is correct");
+    is(endAngle, startAngle + (2 * Math.PI * (percentComplete / 100)), "end angle is correct");
+
+    percentComplete = 0.05;
+    [startAngle, endAngle] = progressIndicator.updateProgress(percentComplete);
+    is(startAngle, expectedStartAngle, "start angle is correct");
+    is(endAngle, startAngle + (2 * Math.PI * (percentComplete / 100)), "end angle is correct");
+
+    percentComplete = 0.5;
+    [startAngle, endAngle] = progressIndicator.updateProgress(percentComplete);
+    is(startAngle, expectedStartAngle, "start angle is correct");
+    is(endAngle, startAngle + (2 * Math.PI * (percentComplete / 100)), "end angle is correct");
+
+    percentComplete = 1;
+    [startAngle, endAngle] = progressIndicator.updateProgress(percentComplete);
+    is(startAngle, expectedStartAngle, "start angle is correct");
+    is(endAngle, startAngle + (2 * Math.PI * (percentComplete / 100)), "end angle is correct");
+  }
+});
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/metro/base/tests/mochitest/browser_progress_indicator.xul
@@ -0,0 +1,15 @@
+<?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/. -->
+
+<?xml-stylesheet href="chrome://browser/skin/platform.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/browser.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
+
+<!DOCTYPE window []>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <circularprogressindicator id="progress-indicator" oncommand=""/>
+</window>
\ No newline at end of file
--- a/browser/metro/theme/browser.css
+++ b/browser/metro/theme/browser.css
@@ -443,16 +443,22 @@ documenttab[selected] .documenttab-selec
 }
 
 /* Navigation bar ========================================================== */
 
 #navbar[startpage] {
   transform: none;
 }
 
+.circularprogressindicator-progressRing {
+  margin: 0 @toolbar_horizontal_spacing@;
+  pointer-events:none;
+  position: absolute;
+}
+
 /* Progress meter ---------------------------------------------------------- */
 
 #progress-container {
   display: block;
   position: absolute;
   top: -@progress_height@;
   height: @progress_height@;
   width: 100%;
@@ -670,23 +676,24 @@ documenttab[selected] .documenttab-selec
 #urlbar:-moz-any([mode="edit"], [mode="loading"]) > #reload-button,
 #urlbar:-moz-any([mode="edit"], [mode="view"]) > #stop-button,
 #toolbar[viewstate="snapped"] > #urlbar ~ toolbarbutton {
   visibility: collapse;
 }
 
 /* Application-Specific */
 
-#download-button {
+.circularprogressindicator-progressButton {
+  margin: 0 @toolbar_horizontal_spacing@;
   -moz-image-region: rect(0px, 40px, 40px, 0px) !important;
 }
-#download-button:hover {
+.circularprogressindicator-progressButton:hover {
   -moz-image-region: rect(40px, 40px, 80px, 0px) !important;
 }
-#download-button:active {
+.circularprogressindicator-progressButton:active {
   -moz-image-region: rect(80px, 40px, 120px, 0px) !important;
 }
 
 /* Page-Specific */
 
 #pin-button {
   list-style-image: url(chrome://browser/skin/images/navbar-pin.png);
 }
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f0a4dbfdf948a8227a3076f6fa4876ee4b36b210
GIT binary patch
literal 363
zc%17D@N?(olHy`uVBq!ia0vp^dLYcf1|-9GYMTQowj^(N7l!{JxM1({$v_d#0*}aI
z1_s$S5N3>BB-{uTWH0gbb!ETHE+woYAbVaZ5GeG+)5S3)<KEkuCj}21aJU{_uXJ_k
zj*#hkbE?xGZP1r0{C0r3aYEwSg-&_1;@)?Ez5Chm>w}mZ4C*UbZX9IPVl}gH$X(F-
zVM<g*AFEGVMs8!_oE;a}_D{IHb=g+Or5D(uL-^Ki@zh-)x^3dcr;px$jF(Gy;I_P=
zbct)W$NL^X^~SP`X}69^X-Q00i+|ZPE%Q)<#5^tbKMnF9nC1BIE!4bgu)ue-pxIL2
z;yp&O|8BUg-ro7(Ez|1fq8F$AeVRU(<FcL2;t86yY)*$R%UL&F{>xaotT$+S?Yz51
zXLNeIt|!c2nEg4_ORVO^C#_43A4(T)XMV|j>jcxiSi24Uz0OlZC+Y*kfWgz%&t;uc
GLK6Vn&z7qI
--- a/browser/metro/theme/jar.mn
+++ b/browser/metro/theme/jar.mn
@@ -94,12 +94,13 @@ chrome.jar:
   skin/images/exitfullscreen-hdpi.png       (images/exitfullscreen-hdpi.png)
   skin/images/scrubber-hdpi.png             (images/scrubber-hdpi.png)
   skin/images/selection-monocle.png         (images/selection-monocle.png)
   skin/images/appbar-icons.png              (images/appbar-icons.png)
   skin/images/pinned-hdpi.png               (images/pinned-hdpi.png)
   skin/images/tile-selected-check-hdpi.png  (images/tile-selected-check-hdpi.png)
   skin/images/plus-34.png                   (images/plus-34.png)
   skin/images/plus-24.png                   (images/plus-24.png)
+  skin/images/progresscircle.png            (images/progresscircle.png)
 
   skin/images/overlay-back.png              (images/overlay-back.png)
   skin/images/overlay-plus.png              (images/overlay-plus.png)
   skin/images/autoscroll.png                (images/autoscroll.png)