Bug 548763 - Show download progress in OS X app dock icon. r=dao r=josh
authorDave Vasilevsky <dave@vasilevsky.ca>
Sun, 03 Mar 2013 05:58:00 -0500
changeset 134021 eaa88688e9e86299f81cf3e6e8ff6dda13207cbe
parent 134020 05f6955dad64986165d10ddb42fefa8388412bca
child 134022 e78b82d62d9838cdc2929621658bbb98814fac3b
push id2452
push userlsblakk@mozilla.com
push dateMon, 13 May 2013 16:59:38 +0000
treeherdermozilla-beta@d4b152d29d8d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdao, josh
bugs548763
milestone22.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 548763 - Show download progress in OS X app dock icon. r=dao r=josh
browser/base/content/browser.js
toolkit/mozapps/downloads/DownloadTaskbarProgress.jsm
toolkit/mozapps/downloads/Makefile.in
toolkit/mozapps/downloads/content/downloads.js
toolkit/mozapps/downloads/tests/chrome/Makefile.in
toolkit/mozapps/downloads/tests/chrome/test_taskbarprogress_downloadstates.xul
widget/Makefile.in
widget/cocoa/nsMacDockSupport.h
widget/cocoa/nsMacDockSupport.mm
widget/nsIMacDockSupport.idl
widget/tests/Makefile.in
widget/tests/taskbar_progress.xul
widget/tests/test_taskbar_progress.xul
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1481,24 +1481,19 @@ var gBrowserInit = {
 
     // Initialize the download manager some time after the app starts so that
     // auto-resume downloads begin (such as after crashing or quitting with
     // active downloads) and speeds up the first-load of the download manager UI.
     // If the user manually opens the download manager before the timeout, the
     // downloads will start right away, and getting the service again won't hurt.
     setTimeout(function() {
       Services.downloads;
-
-#ifdef XP_WIN
-      if (Win7Features) {
-        let DownloadTaskbarProgress =
-          Cu.import("resource://gre/modules/DownloadTaskbarProgress.jsm", {}).DownloadTaskbarProgress;
-        DownloadTaskbarProgress.onBrowserWindowLoad(window);
-      }
-#endif
+      let DownloadTaskbarProgress =
+        Cu.import("resource://gre/modules/DownloadTaskbarProgress.jsm", {}).DownloadTaskbarProgress;
+      DownloadTaskbarProgress.onBrowserWindowLoad(window);
     }, 10000);
 
     // The object handling the downloads indicator is also initialized here in the
     // delayed startup function, but the actual indicator element is not loaded
     // unless there are downloads to be displayed.
     DownloadsButton.initializeIndicator();
 
 #ifndef XP_MACOSX
--- a/toolkit/mozapps/downloads/DownloadTaskbarProgress.jsm
+++ b/toolkit/mozapps/downloads/DownloadTaskbarProgress.jsm
@@ -9,33 +9,42 @@ this.EXPORTED_SYMBOLS = [
 ];
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Constants
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
-const kTaskbarID = "@mozilla.org/windows-taskbar;1";
+const kTaskbarIDWin = "@mozilla.org/windows-taskbar;1";
+const kTaskbarIDMac = "@mozilla.org/widget/macdocksupport;1";
 
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadTaskbarProgress Object
 
 this.DownloadTaskbarProgress =
 {
+  init: function DTP_init()
+  {
+    if (DownloadTaskbarProgressUpdater) {
+      DownloadTaskbarProgressUpdater._init();
+    }
+  },
+
   /**
    * Called when a browser window appears. This has an effect only when we
    * don't already have an active window.
    *
    * @param aWindow
    *        The browser window that we'll potentially use to display the
    *        progress.
    */
   onBrowserWindowLoad: function DTP_onBrowserWindowLoad(aWindow)
   {
+    this.init();
     if (!DownloadTaskbarProgressUpdater) {
       return;
     }
     if (!DownloadTaskbarProgressUpdater._activeTaskbarProgress) {
       DownloadTaskbarProgressUpdater._setActiveWindow(aWindow, false);
     }
   },
 
@@ -78,39 +87,51 @@ this.DownloadTaskbarProgress =
 
 };
 
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadTaskbarProgressUpdater Object
 
 var DownloadTaskbarProgressUpdater =
 {
+  /// Whether the taskbar is initialized.
+  _initialized: false,
+
   /// Reference to the taskbar.
   _taskbar: null,
 
   /// Reference to the download manager.
   _dm: null,
 
   /**
    * Initialize and register ourselves as a download progress listener.
    */
   _init: function DTPU_init()
   {
-    if (!(kTaskbarID in Cc)) {
-      // This means that the component isn't available
+    if (this._initialized) {
+      return; // Already initialized
+    }
+    this._initialized = true;
+
+    if (kTaskbarIDWin in Cc) {
+      this._taskbar = Cc[kTaskbarIDWin].getService(Ci.nsIWinTaskbar);
+      if (!this._taskbar.available) {
+        // The Windows version is probably too old
+        DownloadTaskbarProgressUpdater = null;
+        return;
+      }
+    } else if (kTaskbarIDMac in Cc) {
+      this._activeTaskbarProgress = Cc[kTaskbarIDMac].
+                                      getService(Ci.nsITaskbarProgress);
+    } else {
       DownloadTaskbarProgressUpdater = null;
       return;
     }
 
-    this._taskbar = Cc[kTaskbarID].getService(Ci.nsIWinTaskbar);
-    if (!this._taskbar.available) {
-      // The Windows version is probably too old
-      DownloadTaskbarProgressUpdater = null;
-      return;
-    }
+    this._taskbarState = Ci.nsITaskbarProgress.STATE_NO_PROGRESS;
 
     this._dm = Cc["@mozilla.org/download-manager;1"].
                getService(Ci.nsIDownloadManager);
     this._dm.addPrivacyAwareListener(this);
 
     this._os = Cc["@mozilla.org/observer-service;1"].
                getService(Ci.nsIObserverService);
     this._os.addObserver(this, "quit-application-granted", false);
@@ -121,16 +142,18 @@ var DownloadTaskbarProgressUpdater =
   },
 
   /**
    * Unregisters ourselves as a download progress listener.
    */
   _uninit: function DTPU_uninit() {
     this._dm.removeListener(this);
     this._os.removeObserver(this, "quit-application-granted");
+    this._activeTaskbarProgress = null;
+    this._initialized = false;
   },
 
   /**
    * This holds a reference to the taskbar progress for the window we're
    * working with. This window would preferably be download window, but can be
    * another window if it isn't open.
    */
   _activeTaskbarProgress: null,
@@ -145,16 +168,17 @@ var DownloadTaskbarProgressUpdater =
    *
    * @param aWindow
    *        The window to set as active.
    * @param aIsDownloadWindow
    *        Whether this window is a download window.
    */
   _setActiveWindow: function DTPU_setActiveWindow(aWindow, aIsDownloadWindow)
   {
+#ifdef XP_WIN
     // Clear out the taskbar for the old active window. (If there was no active
     // window, this is a no-op.)
     this._clearTaskbar();
 
     this._activeWindowIsDownloadWindow = aIsDownloadWindow;
     if (aWindow) {
       // Get the taskbar progress for this window
       let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
@@ -170,44 +194,54 @@ var DownloadTaskbarProgressUpdater =
       // we've already set this before or not.
       aWindow.addEventListener("unload", function () {
         DownloadTaskbarProgressUpdater._onActiveWindowUnload(taskbarProgress);
       }, false);
     }
     else {
       this._activeTaskbarProgress = null;
     }
+#endif
   },
 
   /// Current state displayed on the active window's taskbar item
-  _taskbarState: Ci.nsITaskbarProgress.STATE_NO_PROGRESS,
+  _taskbarState: null,
   _totalSize: 0,
   _totalTransferred: 0,
 
+  // If the active window is not the download manager window, set the state
+  // only if it is normal or indeterminate.
+  _shouldSetState: function DTPU_shouldSetState()
+  {
+#ifdef XP_WIN
+    return this._activeWindowIsDownloadWindow ||
+           (this._taskbarState == Ci.nsITaskbarProgress.STATE_NORMAL ||
+            this._taskbarState == Ci.nsITaskbarProgress.STATE_INDETERMINATE);
+#else
+    return true;
+#endif
+  },
+
   /**
    * Update the active window's taskbar indicator with the current state. There
    * are two cases here:
    * 1. If the active window is the download window, then we always update
    *    the taskbar indicator.
    * 2. If the active window isn't the download window, then we update only if
    *    the status is normal or indeterminate. i.e. one or more downloads are
    *    currently progressing or in scan mode. If we aren't, then we clear the
    *    indicator.
    */
   _updateTaskbar: function DTPU_updateTaskbar()
   {
     if (!this._activeTaskbarProgress) {
       return;
     }
 
-    // If the active window is not the download manager window, set the state
-    // only if it is normal or indeterminate.
-    if (this._activeWindowIsDownloadWindow ||
-        (this._taskbarState == Ci.nsITaskbarProgress.STATE_NORMAL ||
-         this._taskbarState == Ci.nsITaskbarProgress.STATE_INDETERMINATE)) {
+    if (this._shouldSetState()) {
       this._activeTaskbarProgress.setProgressState(this._taskbarState,
                                                    this._totalTransferred,
                                                    this._totalSize);
     }
     // Clear any state otherwise
     else {
       this._clearTaskbar();
     }
@@ -356,13 +390,8 @@ var DownloadTaskbarProgressUpdater =
   onStateChange: function() { },
 
   observe: function DTPU_observe(aSubject, aTopic, aData) {
     if (aTopic == "quit-application-granted") {
       this._uninit();
     }
   }
 };
-
-////////////////////////////////////////////////////////////////////////////////
-//// Initialization
-
-DownloadTaskbarProgressUpdater._init();
--- a/toolkit/mozapps/downloads/Makefile.in
+++ b/toolkit/mozapps/downloads/Makefile.in
@@ -16,15 +16,11 @@ EXTRA_COMPONENTS = nsHelperAppDlg.manife
 EXTRA_PP_COMPONENTS = nsHelperAppDlg.js
 
 EXTRA_JS_MODULES = \
   DownloadLastDir.jsm \
   DownloadPaths.jsm \
   DownloadUtils.jsm \
   $(NULL)
 
-ifeq ($(OS_ARCH),WINNT)
-EXTRA_JS_MODULES += \
-  DownloadTaskbarProgress.jsm \
-  $(NULL)
-endif
+EXTRA_PP_JS_MODULES = DownloadTaskbarProgress.jsm
 
 include $(topsrcdir)/config/rules.mk
--- a/toolkit/mozapps/downloads/content/downloads.js
+++ b/toolkit/mozapps/downloads/content/downloads.js
@@ -442,21 +442,19 @@ function Startup()
   gSearchBox.addEventListener("keypress", function(e) {
     if (e.keyCode == e.DOM_VK_ESCAPE) {
       // Move focus to the list instead of closing the window
       gDownloadsView.focus();
       e.preventDefault();
     }
   }, false);
 
-#ifdef XP_WIN
-  let tempScope = {};
-  Cu.import("resource://gre/modules/DownloadTaskbarProgress.jsm", tempScope);
-  tempScope.DownloadTaskbarProgress.onDownloadWindowLoad(window);
-#endif
+  let DownloadTaskbarProgress =
+    Cu.import("resource://gre/modules/DownloadTaskbarProgress.jsm", {}).DownloadTaskbarProgress;
+  DownloadTaskbarProgress.onDownloadWindowLoad(window);
 }
 
 function Shutdown()
 {
   gDownloadManager.removeListener(gDownloadListener);
 
   let obs = Cc["@mozilla.org/observer-service;1"].
             getService(Ci.nsIObserverService);
--- a/toolkit/mozapps/downloads/tests/chrome/Makefile.in
+++ b/toolkit/mozapps/downloads/tests/chrome/Makefile.in
@@ -43,16 +43,20 @@ MOCHITEST_CHROME_FILES = \
   utils.js \
   $(NULL)
 ifneq (,$(filter cocoa, $(MOZ_WIDGET_TOOLKIT)))
 MOCHITEST_CHROME_FILES += \
   test_backspace_key_removes.xul \
   $(NULL)
 endif
 
+ifneq (,$(filter WINNT, $(OS_ARCH))$(filter cocoa, $(MOZ_WIDGET_TOOLKIT)))
+MOCHITEST_CHROME_FILES += \
+  test_taskbarprogress_downloadstates.xul \
+  $(NULL)
+endif
 ifeq ($(OS_ARCH),WINNT)
 MOCHITEST_CHROME_FILES += \
-  test_taskbarprogress_downloadstates.xul \
   $(filter disabled-for-very-frequent-orange--bug-630567, test_taskbarprogress_service.xul) \
   $(NULL)
 endif
 
 include $(topsrcdir)/config/rules.mk
--- a/toolkit/mozapps/downloads/tests/chrome/test_taskbarprogress_downloadstates.xul
+++ b/toolkit/mozapps/downloads/tests/chrome/test_taskbarprogress_downloadstates.xul
@@ -161,38 +161,44 @@ function testSetup()
 {
   //Test setup
   let dmui = getDMUI();
   if (!dmui) {
     todo(false, "skip test for toolkit download manager UI");
     return;
   }
 
-  let isWin7OrHigher = false;
-  try {
-    let version = Cc["@mozilla.org/system-info;1"]
-                    .getService(Ci.nsIPropertyBag2)
-                    .getProperty("version");
-    isWin7OrHigher = (parseFloat(version) >= 6.1);
-  } catch (ex) { }
+  let isWin = /Win/.test(navigator.platform);
+  if (isWin) {
+    let isWin7OrHigher = false;
+    try {
+      let version = Cc["@mozilla.org/system-info;1"]
+                      .getService(Ci.nsIPropertyBag2)
+                      .getProperty("version");
+      isWin7OrHigher = (parseFloat(version) >= 6.1);
+    } catch (ex) { }
 
-  if (!isWin7OrHigher) {
-    ok(true, "This test only runs on Windows 7 or higher");
-    return;
+    if (!isWin7OrHigher) {
+      ok(true, "This test only runs on Windows 7 or higher");
+      return;
+    }
   }
 
   let tempScope = {};
   Cu.import("resource://gre/modules/DownloadTaskbarProgress.jsm", tempScope);
   Cu.import("resource://gre/modules/Services.jsm");
 
   DownloadTaskbarProgress = tempScope.DownloadTaskbarProgress;
-  let TaskbarService =  Cc[kTaskbarID].getService(Ci.nsIWinTaskbar);
+  isnot(DownloadTaskbarProgress, null, "Download taskbar progress service exists");
+  DownloadTaskbarProgress.init();
 
-  isnot(DownloadTaskbarProgress, null, "Download taskbar progress service exists");
-  is(TaskbarService.available, true, "Taskbar Service is available");
+  if (isWin) {
+    let TaskbarService =  Cc[kTaskbarID].getService(Ci.nsIWinTaskbar);
+    is(TaskbarService.available, true, "Taskbar Service is available");
+  }
 
   dm = Cc["@mozilla.org/download-manager;1"].
            getService(Ci.nsIDownloadManager);
 
   // First, we clear out the database
   dm.DBConnection.executeSimpleSQL("DELETE FROM moz_downloads");
 
   // See if the DM is already open, and if it is, close it!
--- a/widget/Makefile.in
+++ b/widget/Makefile.in
@@ -117,16 +117,17 @@ XPIDLSRCS += nsIWinMetroUtils.idl \
 		MetroUIUtils.idl \
 		$(NULL)
 endif
 
 ifeq ($(MOZ_WIDGET_TOOLKIT),cocoa)
 XPIDLSRCS +=	nsIMacDockSupport.idl \
 		nsIStandaloneNativeMenu.idl \
 		nsIMacWebAppUtils.idl \
+		nsITaskbarProgress.idl \
 		$(NULL)
 endif
 
 ifeq ($(MOZ_WIDGET_TOOLKIT),os2)
 XPIDLSRCS	+= nsIRwsService.idl
 endif
 
 EXPORTS		:= $(addprefix $(srcdir)/, $(EXPORTS))
--- a/widget/cocoa/nsMacDockSupport.h
+++ b/widget/cocoa/nsMacDockSupport.h
@@ -1,23 +1,40 @@
 /* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
 /* 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/. */
 
+#import <Carbon/Carbon.h>
+
 #include "nsIMacDockSupport.h"
 #include "nsIStandaloneNativeMenu.h"
+#include "nsITaskbarProgress.h"
+#include "nsITimer.h"
 #include "nsCOMPtr.h"
 #include "nsString.h"
 
-class nsMacDockSupport : public nsIMacDockSupport
+class nsMacDockSupport : public nsIMacDockSupport, public nsITaskbarProgress
 {
 public:
-  nsMacDockSupport() {}
-  virtual ~nsMacDockSupport() {}
-  
+  nsMacDockSupport();
+  virtual ~nsMacDockSupport();
+
   NS_DECL_ISUPPORTS
   NS_DECL_NSIMACDOCKSUPPORT
+  NS_DECL_NSITASKBARPROGRESS
 
 protected:
   nsCOMPtr<nsIStandaloneNativeMenu> mDockMenu;
   nsString mBadgeText;
+
+  NSImage *mAppIcon, *mProgressBackground;
+  HIThemeTrackDrawInfo mProgressDrawInfo;
+
+  nsTaskbarProgressState mProgressState;
+  double mProgressFraction;
+  nsCOMPtr<nsITimer> mProgressTimer;
+
+  static void RedrawIconCallback(nsITimer* aTimer, void* aClosure);
+
+  bool InitProgress();
+  nsresult RedrawIcon();
 };
--- a/widget/cocoa/nsMacDockSupport.mm
+++ b/widget/cocoa/nsMacDockSupport.mm
@@ -1,19 +1,45 @@
 /* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
 /* 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/. */
 
+#import <Carbon/Carbon.h>
 #import <Cocoa/Cocoa.h>
 
 #include "nsMacDockSupport.h"
 #include "nsObjCExceptions.h"
 
-NS_IMPL_ISUPPORTS1(nsMacDockSupport, nsIMacDockSupport)
+NS_IMPL_ISUPPORTS2(nsMacDockSupport, nsIMacDockSupport, nsITaskbarProgress)
+
+nsMacDockSupport::nsMacDockSupport()
+: mAppIcon(nil)
+, mProgressBackground(nil)
+, mProgressState(STATE_NO_PROGRESS)
+, mProgressFraction(0.0)
+{
+  mProgressTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+}
+
+nsMacDockSupport::~nsMacDockSupport()
+{
+  if (mAppIcon) {
+    [mAppIcon release];
+    mAppIcon = nil;
+  }
+  if (mProgressBackground) {
+    [mProgressBackground release];
+    mProgressBackground = nil;
+  }
+  if (mProgressTimer) {
+    mProgressTimer->Cancel();
+    mProgressTimer = nullptr;
+  }
+}
 
 NS_IMETHODIMP
 nsMacDockSupport::GetDockMenu(nsIStandaloneNativeMenu ** aDockMenu)
 {
   *aDockMenu = nullptr;
 
   if (mDockMenu)
     return mDockMenu->QueryInterface(NS_GET_IID(nsIStandaloneNativeMenu),
@@ -57,8 +83,121 @@ nsMacDockSupport::SetBadgeText(const nsA
 }
 
 NS_IMETHODIMP
 nsMacDockSupport::GetBadgeText(nsAString& aBadgeText)
 {
   aBadgeText = mBadgeText;
   return NS_OK;
 }
+
+NS_IMETHODIMP
+nsMacDockSupport::SetProgressState(nsTaskbarProgressState aState,
+                                   PRUint64 aCurrentValue,
+                                   PRUint64 aMaxValue)
+{
+  NS_ENSURE_ARG_RANGE(aState, 0, STATE_PAUSED);
+  if (aState == STATE_NO_PROGRESS || aState == STATE_INDETERMINATE) {
+    NS_ENSURE_TRUE(aCurrentValue == 0, NS_ERROR_INVALID_ARG);
+    NS_ENSURE_TRUE(aMaxValue == 0, NS_ERROR_INVALID_ARG);
+  }
+  if (aCurrentValue > aMaxValue) {
+    return NS_ERROR_ILLEGAL_VALUE;
+  }
+
+  mProgressState = aState;
+  if (aMaxValue == 0) {
+    mProgressFraction = 0;
+  } else {
+    mProgressFraction = (double)aCurrentValue / aMaxValue;
+  }
+
+  if (mProgressState == STATE_NORMAL || mProgressState == STATE_INDETERMINATE) {
+    int perSecond = 30;
+    mProgressTimer->InitWithFuncCallback(RedrawIconCallback, this, 1000 / perSecond,
+      nsITimer::TYPE_REPEATING_SLACK);
+    return NS_OK;
+  } else {
+    mProgressTimer->Cancel();
+    return RedrawIcon();
+  }
+}
+
+// static
+void nsMacDockSupport::RedrawIconCallback(nsITimer* aTimer, void* aClosure)
+{
+  static_cast<nsMacDockSupport*>(aClosure)->RedrawIcon();
+}
+
+// Return whether to draw progress
+bool nsMacDockSupport::InitProgress()
+{
+  if (mProgressState != STATE_NORMAL && mProgressState != STATE_INDETERMINATE) {
+    return false;
+  }
+
+  if (!mAppIcon) {
+    mProgressTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+    mAppIcon = [[NSImage imageNamed:@"NSApplicationIcon"] retain];
+    mProgressBackground = [mAppIcon copyWithZone:nil];
+
+    NSSize sz = [mProgressBackground size];
+    mProgressDrawInfo.version = 0;
+    mProgressDrawInfo.min = 0;
+    mProgressDrawInfo.value = 0;
+    mProgressDrawInfo.max = PR_INT32_MAX;
+    mProgressDrawInfo.bounds = CGRectMake(sz.width * 1/32, sz.height * 3/32,
+                                          sz.width * 30/32, sz.height * 2/32);
+    mProgressDrawInfo.attributes = kThemeTrackHorizontal;
+    mProgressDrawInfo.enableState = kThemeTrackActive;
+    mProgressDrawInfo.kind = kThemeLargeProgressBar;
+    mProgressDrawInfo.trackInfo.progress.phase = 0;
+
+    // Draw a light background, to visually distinguish the progress.
+    HIRect bounds;
+    HIThemeGetTrackBounds(&mProgressDrawInfo, &bounds);
+    // Margins within track, empirically. FIXME: Don't hardcode?
+    int mleft = 3, mtop = 3, mright = 3, mbot = 1;
+    bounds.origin.x += mleft;
+    bounds.origin.y += mbot;
+    bounds.size.width -= mleft + mright;
+    bounds.size.height -= mtop + mbot;
+
+    [mProgressBackground lockFocus];
+    [[NSColor whiteColor] set];
+    NSRectFill(NSRectFromCGRect(bounds));
+    [mProgressBackground unlockFocus];
+  }
+  return true;
+}
+
+nsresult
+nsMacDockSupport::RedrawIcon()
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+
+  if (InitProgress()) {
+    // TODO: - Share code with nsNativeThemeCocoa?
+    //       - Implement ERROR and PAUSED states?
+    NSImage *icon = [mProgressBackground copyWithZone:nil];
+
+    bool isIndeterminate = (mProgressState != STATE_NORMAL);
+    mProgressDrawInfo.value = PR_INT32_MAX * mProgressFraction;
+    mProgressDrawInfo.kind = isIndeterminate ? kThemeLargeIndeterminateBar
+                                             : kThemeLargeProgressBar;
+
+    int stepsPerSecond = isIndeterminate ? 60 : 30;
+    mProgressDrawInfo.trackInfo.progress.phase =
+      PR_IntervalToMilliseconds(PR_IntervalNow()) * stepsPerSecond / 1000 % 32;
+
+    [icon lockFocus];
+    CGContextRef ctx = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
+    HIThemeDrawTrack(&mProgressDrawInfo, NULL, ctx, kHIThemeOrientationNormal);
+    [icon unlockFocus];
+    [NSApp setApplicationIconImage:icon];
+    [icon release];
+  } else {
+    [NSApp setApplicationIconImage:mAppIcon];
+  }
+
+  return NS_OK;
+  NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
+}
--- a/widget/nsIMacDockSupport.idl
+++ b/widget/nsIMacDockSupport.idl
@@ -3,16 +3,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 
 interface nsIStandaloneNativeMenu;
 
 /**
  * Allow applications to interface with the Mac OS X Dock.
+ *
+ * Applications may indicate progress on their Dock icon. Only one such
+ * progress indicator is available to the entire application.
  */
 
 [scriptable, uuid(8BE66B0C-5F71-4B74-98CF-6C2551B999B1)]
 interface nsIMacDockSupport : nsISupports
 {
   /**
    * Menu to use for application-specific dock menu items.
    */
--- a/widget/tests/Makefile.in
+++ b/widget/tests/Makefile.in
@@ -75,23 +75,24 @@ MOCHITEST_CHROME_FILES += native_menus_w
                test_platform_colors.xul \
                test_standalone_native_menu.xul \
                standalone_native_menu_window.xul \
                test_bug586713.xul \
                bug586713_window.xul \
                test_key_event_counts.xul \
                test_bug596600.xul \
                test_bug673301.xul \
+               test_taskbar_progress.xul \
                $(NULL)
 endif
 
 ifeq ($(MOZ_WIDGET_TOOLKIT),windows)
 MOCHITEST_CHROME_FILES  += taskbar_previews.xul \
 		window_state_windows.xul \
-		taskbar_progress.xul \
+		$(warning test_taskbar_progress.xul disabled due to regression, see bug 605813) \
 		test_chrome_context_menus_win.xul \
 		test_plugin_input_event.html \
 		chrome_context_menus_win.xul \
 		test_mouse_scroll.xul \
 		window_mouse_scroll_win.html \
 		$(NULL)
 
 MOCHITEST_FILES +=	test_bug565392.html \
rename from widget/tests/taskbar_progress.xul
rename to widget/tests/test_taskbar_progress.xul
--- a/widget/tests/taskbar_progress.xul
+++ b/widget/tests/test_taskbar_progress.xul
@@ -11,54 +11,60 @@
   <script type="application/javascript"
           src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
   <script class="testbody" type="application/javascript">
   <![CDATA[
     let Cc = Components.classes;
     let Ci = Components.interfaces;
     let Cu = Components.utils;
     Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-    let taskbar = Cc["@mozilla.org/windows-taskbar;1"].getService(Ci.nsIWinTaskbar);
-
-    function IsWin7OrHigher() {
-      try {
-        var sysInfo = Cc["@mozilla.org/system-info;1"].
-                      getService(Ci.nsIPropertyBag2);
-        var ver = parseFloat(sysInfo.getProperty("version"));
-        if (ver >= 6.1)
-          return true;
-      } catch (ex) { }
-      return false;
-    }
-
-    SimpleTest.waitForExplicitFinish();
-
-    function loaded()
-    {
+    
+    let TP = Ci.nsITaskbarProgress;
+    
+    function winProgress() {
+      let taskbar = Cc["@mozilla.org/windows-taskbar;1"];
+      if (!taskbar)
+        return null;
+      taskbar = taskbar.getService(Ci.nsIWinTaskbar);
       if (!taskbar.available)
-        SimpleTest.finish();
-
+        return null;
+      
       // HACK from mconnor:
       var wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
       let win = wm.getMostRecentWindow("navigator:browser");
       let docShell = win.gBrowser.docShell;
 
-      let TP = Ci.nsITaskbarProgress;
-
       let progress = taskbar.getTaskbarProgress(docShell);
       isnot(progress, null, "Progress is not null");
 
       try {
         taskbar.getTaskbarProgress(null);
         ok(false, "Got progress for null docshell");
       } catch (e) {
         ok(true, "Cannot get progress for null docshell");
       }
+      
+      return progress;
+    }
+    
+    function macProgress() {
+      let progress = Cc["@mozilla.org/widget/macdocksupport;1"];
+      if (!progress)
+        return null;
+      return progress.getService(TP);
+    }
 
+    SimpleTest.waitForExplicitFinish();
+
+    function loaded()
+    {
+      let progress = winProgress() || macProgress();
+      if (!TP)
+        SimpleTest.finish();
+      
       function shouldThrow(s,c,m) {
         try {
           progress.setProgressState(s,c,m);
           return false;
         } catch (e) {
           return true;
         }
       }