summaryrefslogtreecommitdiffstats
path: root/widget/cocoa/nsMacDockSupport.mm
diff options
context:
space:
mode:
Diffstat (limited to 'widget/cocoa/nsMacDockSupport.mm')
-rw-r--r--widget/cocoa/nsMacDockSupport.mm419
1 files changed, 419 insertions, 0 deletions
diff --git a/widget/cocoa/nsMacDockSupport.mm b/widget/cocoa/nsMacDockSupport.mm
new file mode 100644
index 0000000000..e69c0b6259
--- /dev/null
+++ b/widget/cocoa/nsMacDockSupport.mm
@@ -0,0 +1,419 @@
+/* -*- 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 <Cocoa/Cocoa.h>
+#include <CoreFoundation/CoreFoundation.h>
+#include <signal.h>
+
+#include "nsCocoaUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMacDockSupport.h"
+#include "nsObjCExceptions.h"
+#include "nsNativeThemeColors.h"
+#include "nsString.h"
+
+NS_IMPL_ISUPPORTS(nsMacDockSupport, nsIMacDockSupport, nsITaskbarProgress)
+
+// This view is used in the dock tile when we're downloading a file.
+// It draws a progress bar that looks similar to the native progress bar on
+// 10.12. This style of progress bar is not animated, unlike the pre-10.10
+// progress bar look which had to redrawn multiple times per second.
+@interface MOZProgressDockOverlayView : NSView {
+ double mFractionValue;
+}
+@property double fractionValue;
+
+@end
+
+@implementation MOZProgressDockOverlayView
+
+@synthesize fractionValue = mFractionValue;
+
+- (void)drawRect:(NSRect)aRect {
+ // Erase the background behind this view, i.e. cut a rectangle hole in the icon.
+ [[NSColor clearColor] set];
+ NSRectFill(self.bounds);
+
+ // Split the height of this view into four quarters. The middle two quarters
+ // will be covered by the actual progress bar.
+ CGFloat radius = self.bounds.size.height / 4;
+ NSRect barBounds = NSInsetRect(self.bounds, 0, radius);
+
+ NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:barBounds
+ xRadius:radius
+ yRadius:radius];
+
+ // Draw a grayish background first.
+ [[NSColor colorWithDeviceWhite:0 alpha:0.1] setFill];
+ [path fill];
+
+ // Draw a fill in the control accent color for the progress part.
+ NSRect progressFillRect = self.bounds;
+ progressFillRect.size.width *= mFractionValue;
+ [NSGraphicsContext saveGraphicsState];
+ [NSBezierPath clipRect:progressFillRect];
+ [ControlAccentColor() setFill];
+ [path fill];
+ [NSGraphicsContext restoreGraphicsState];
+
+ // Add a shadowy stroke on top.
+ [NSGraphicsContext saveGraphicsState];
+ [path addClip];
+ [[NSColor colorWithDeviceWhite:0 alpha:0.2] setStroke];
+ path.lineWidth = barBounds.size.height / 10;
+ [path stroke];
+ [NSGraphicsContext restoreGraphicsState];
+}
+
+@end
+
+nsMacDockSupport::nsMacDockSupport()
+ : mDockTileWrapperView(nil),
+ mProgressDockOverlayView(nil),
+ mProgressState(STATE_NO_PROGRESS),
+ mProgressFraction(0.0) {}
+
+nsMacDockSupport::~nsMacDockSupport() {
+ if (mDockTileWrapperView) {
+ [mDockTileWrapperView release];
+ mDockTileWrapperView = nil;
+ }
+ if (mProgressDockOverlayView) {
+ [mProgressDockOverlayView release];
+ mProgressDockOverlayView = nil;
+ }
+}
+
+NS_IMETHODIMP
+nsMacDockSupport::GetDockMenu(nsIStandaloneNativeMenu** aDockMenu) {
+ nsCOMPtr<nsIStandaloneNativeMenu> dockMenu(mDockMenu);
+ dockMenu.forget(aDockMenu);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacDockSupport::SetDockMenu(nsIStandaloneNativeMenu* aDockMenu) {
+ mDockMenu = aDockMenu;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacDockSupport::ActivateApplication(bool aIgnoreOtherApplications) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ [[NSApplication sharedApplication] activateIgnoringOtherApps:aIgnoreOtherApplications];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsMacDockSupport::SetBadgeText(const nsAString& aBadgeText) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSDockTile* tile = [[NSApplication sharedApplication] dockTile];
+ mBadgeText = aBadgeText;
+ if (aBadgeText.IsEmpty())
+ [tile setBadgeLabel:nil];
+ else
+ [tile setBadgeLabel:[NSString
+ stringWithCharacters:reinterpret_cast<const unichar*>(mBadgeText.get())
+ length:mBadgeText.Length()]];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsMacDockSupport::GetBadgeText(nsAString& aBadgeText) {
+ aBadgeText = mBadgeText;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacDockSupport::SetProgressState(nsTaskbarProgressState aState, uint64_t aCurrentValue,
+ uint64_t 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;
+ }
+
+ return UpdateDockTile();
+}
+
+nsresult nsMacDockSupport::UpdateDockTile() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (mProgressState == STATE_NORMAL || mProgressState == STATE_INDETERMINATE) {
+ if (!mDockTileWrapperView) {
+ // Create the following NSView hierarchy:
+ // * mDockTileWrapperView (NSView)
+ // * imageView (NSImageView) <- has the application icon
+ // * mProgressDockOverlayView (MOZProgressDockOverlayView) <- draws the progress bar
+
+ mDockTileWrapperView = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 32, 32)];
+ mDockTileWrapperView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
+
+ NSImageView* imageView = [[NSImageView alloc] initWithFrame:[mDockTileWrapperView bounds]];
+ imageView.image = [NSImage imageNamed:@"NSApplicationIcon"];
+ imageView.imageScaling = NSImageScaleAxesIndependently;
+ imageView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
+ [mDockTileWrapperView addSubview:imageView];
+
+ mProgressDockOverlayView =
+ [[MOZProgressDockOverlayView alloc] initWithFrame:NSMakeRect(1, 3, 30, 4)];
+ mProgressDockOverlayView.autoresizingMask = NSViewMinXMargin | NSViewWidthSizable |
+ NSViewMaxXMargin | NSViewMinYMargin |
+ NSViewHeightSizable | NSViewMaxYMargin;
+ [mDockTileWrapperView addSubview:mProgressDockOverlayView];
+ }
+ if (NSApp.dockTile.contentView != mDockTileWrapperView) {
+ NSApp.dockTile.contentView = mDockTileWrapperView;
+ }
+
+ if (mProgressState == STATE_NORMAL) {
+ mProgressDockOverlayView.fractionValue = mProgressFraction;
+ } else {
+ // Indeterminate states are rare. Just fill the entire progress bar in
+ // that case.
+ mProgressDockOverlayView.fractionValue = 1.0;
+ }
+ [NSApp.dockTile display];
+ } else if (NSApp.dockTile.contentView) {
+ NSApp.dockTile.contentView = nil;
+ [NSApp.dockTile display];
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+extern "C" {
+// Private CFURL API used by the Dock.
+CFPropertyListRef _CFURLCopyPropertyListRepresentation(CFURLRef url);
+CFURLRef _CFURLCreateFromPropertyListRepresentation(CFAllocatorRef alloc,
+ CFPropertyListRef pListRepresentation);
+} // extern "C"
+
+namespace {
+
+const NSArray* const browserAppNames =
+ [NSArray arrayWithObjects:@"Firefox.app", @"Firefox Beta.app", @"Firefox Nightly.app",
+ @"Safari.app", @"WebKit.app", @"Google Chrome.app",
+ @"Google Chrome Canary.app", @"Chromium.app", @"Opera.app", nil];
+
+constexpr NSString* const kDockDomainName = @"com.apple.dock";
+// See https://developer.apple.com/documentation/devicemanagement/dock
+constexpr NSString* const kDockPersistentAppsKey = @"persistent-apps";
+// See https://developer.apple.com/documentation/devicemanagement/dock/staticitem
+constexpr NSString* const kDockTileDataKey = @"tile-data";
+constexpr NSString* const kDockFileDataKey = @"file-data";
+
+NSArray* GetPersistentAppsFromDockPlist(NSDictionary* aDockPlist) {
+ if (!aDockPlist) {
+ return nil;
+ }
+ NSArray* persistentApps = [aDockPlist objectForKey:kDockPersistentAppsKey];
+ if (![persistentApps isKindOfClass:[NSArray class]]) {
+ return nil;
+ }
+ return persistentApps;
+}
+
+NSString* GetPathForApp(NSDictionary* aPersistantApp) {
+ if (![aPersistantApp isKindOfClass:[NSDictionary class]]) {
+ return nil;
+ }
+ NSDictionary* tileData = aPersistantApp[kDockTileDataKey];
+ if (![tileData isKindOfClass:[NSDictionary class]]) {
+ return nil;
+ }
+ NSDictionary* fileData = tileData[kDockFileDataKey];
+ if (![fileData isKindOfClass:[NSDictionary class]]) {
+ // Some special tiles may not have DockFileData but we can ignore those.
+ return nil;
+ }
+ NSURL* url = CFBridgingRelease(_CFURLCreateFromPropertyListRepresentation(NULL, fileData));
+ if (!url) {
+ return nil;
+ }
+ return [url isFileURL] ? [url path] : nullptr;
+}
+
+// The only reliable way to get our changes to take effect seems to be to use
+// `kill`.
+void RefreshDock(NSDictionary* aDockPlist) {
+ [[NSUserDefaults standardUserDefaults] setPersistentDomain:aDockPlist forName:kDockDomainName];
+ NSRunningApplication* dockApp = [[NSRunningApplication
+ runningApplicationsWithBundleIdentifier:@"com.apple.dock"] firstObject];
+ if (!dockApp) {
+ return;
+ }
+ pid_t pid = [dockApp processIdentifier];
+ if (pid > 0) {
+ kill(pid, SIGTERM);
+ }
+}
+
+} // namespace
+
+nsresult nsMacDockSupport::GetIsAppInDock(bool* aIsInDock) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ *aIsInDock = false;
+
+ NSDictionary* dockPlist =
+ [[NSUserDefaults standardUserDefaults] persistentDomainForName:kDockDomainName];
+ if (!dockPlist) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSArray* persistentApps = GetPersistentAppsFromDockPlist(dockPlist);
+ if (!persistentApps) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSString* appPath = [[NSBundle mainBundle] bundlePath];
+
+ for (id app in persistentApps) {
+ NSString* persistentAppPath = GetPathForApp(app);
+ if (persistentAppPath && [appPath isEqual:persistentAppPath]) {
+ *aIsInDock = true;
+ break;
+ }
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsMacDockSupport::EnsureAppIsPinnedToDock(const nsAString& aAppPath,
+ const nsAString& aAppToReplacePath,
+ bool* aIsInDock) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_ASSERT(aAppPath != aAppToReplacePath || !aAppPath.IsEmpty());
+
+ *aIsInDock = false;
+
+ NSString* appPath =
+ !aAppPath.IsEmpty() ? nsCocoaUtils::ToNSString(aAppPath) : [[NSBundle mainBundle] bundlePath];
+ NSString* appToReplacePath = nsCocoaUtils::ToNSString(aAppToReplacePath);
+
+ NSMutableDictionary* dockPlist =
+ [NSMutableDictionary dictionaryWithDictionary:[[NSUserDefaults standardUserDefaults]
+ persistentDomainForName:kDockDomainName]];
+ if (!dockPlist) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSMutableArray* persistentApps =
+ [NSMutableArray arrayWithArray:GetPersistentAppsFromDockPlist(dockPlist)];
+ if (!persistentApps) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // See the comment for this method in the .idl file for the strategy that we
+ // use here to determine where to pin the app.
+ NSUInteger preexistingAppIndex = NSNotFound; // full path matches
+ NSUInteger sameNameAppIndex = NSNotFound; // app name matches only
+ NSUInteger toReplaceAppIndex = NSNotFound;
+ NSUInteger lastBrowserAppIndex = NSNotFound;
+ for (NSUInteger index = 0; index < [persistentApps count]; ++index) {
+ NSString* persistentAppPath = GetPathForApp([persistentApps objectAtIndex:index]);
+
+ if ([persistentAppPath isEqualToString:appPath]) {
+ preexistingAppIndex = index;
+ } else if (appToReplacePath && [persistentAppPath isEqualToString:appToReplacePath]) {
+ toReplaceAppIndex = index;
+ } else {
+ NSString* appName = [appPath lastPathComponent];
+ NSString* persistentAppName = [persistentAppPath lastPathComponent];
+
+ if ([persistentAppName isEqual:appName]) {
+ if ([appToReplacePath hasPrefix:@"/private/var/folders/"] &&
+ [appToReplacePath containsString:@"/AppTranslocation/"] &&
+ [persistentAppPath hasPrefix:@"/Volumes/"]) {
+ // This is a special case when an app with the same name was
+ // previously dragged and pinned from a quarantined DMG straight to
+ // the Dock and an attempt is now made to pin the same named app to
+ // the Dock. In this case we want to replace the currently pinned app
+ // icon.
+ toReplaceAppIndex = index;
+ } else {
+ sameNameAppIndex = index;
+ }
+ } else {
+ if ([browserAppNames containsObject:persistentAppName]) {
+ lastBrowserAppIndex = index;
+ }
+ }
+ }
+ }
+
+ // Special cases where we're not going to add a new Dock tile:
+ if (preexistingAppIndex != NSNotFound) {
+ if (toReplaceAppIndex != NSNotFound) {
+ [persistentApps removeObjectAtIndex:toReplaceAppIndex];
+ [dockPlist setObject:persistentApps forKey:kDockPersistentAppsKey];
+ RefreshDock(dockPlist);
+ }
+ *aIsInDock = true;
+ return NS_OK;
+ }
+
+ // Create new tile:
+ NSDictionary* newDockTile = nullptr;
+ {
+ NSURL* appUrl = [NSURL fileURLWithPath:appPath isDirectory:YES];
+ NSDictionary* dict =
+ CFBridgingRelease(_CFURLCopyPropertyListRepresentation((__bridge CFURLRef)appUrl));
+ if (!dict) {
+ return NS_ERROR_FAILURE;
+ }
+ NSDictionary* dockTileData = [NSDictionary dictionaryWithObject:dict forKey:kDockFileDataKey];
+ if (dockTileData) {
+ newDockTile = [NSDictionary dictionaryWithObject:dockTileData forKey:kDockTileDataKey];
+ }
+ if (!newDockTile) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Update the Dock:
+ if (toReplaceAppIndex != NSNotFound) {
+ [persistentApps replaceObjectAtIndex:toReplaceAppIndex withObject:newDockTile];
+ } else {
+ NSUInteger index;
+ if (sameNameAppIndex != NSNotFound) {
+ index = sameNameAppIndex + 1;
+ } else if (lastBrowserAppIndex != NSNotFound) {
+ index = lastBrowserAppIndex + 1;
+ } else {
+ index = [persistentApps count];
+ }
+ [persistentApps insertObject:newDockTile atIndex:index];
+ }
+ [dockPlist setObject:persistentApps forKey:kDockPersistentAppsKey];
+ RefreshDock(dockPlist);
+
+ *aIsInDock = true;
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}