diff options
Diffstat (limited to 'widget/cocoa/nsMacDockSupport.mm')
-rw-r--r-- | widget/cocoa/nsMacDockSupport.mm | 419 |
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); +} |