forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			419 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
			
		
		
	
	
			419 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
| /* -*- 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);
 | |
| }
 | 
