1 /* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #import <Cocoa/Cocoa.h>
7 #include <CoreFoundation/CoreFoundation.h>
10 #include "nsCocoaUtils.h"
11 #include "nsComponentManagerUtils.h"
12 #include "nsMacDockSupport.h"
13 #include "nsObjCExceptions.h"
14 #include "nsNativeThemeColors.h"
16 #include "imgLoader.h"
17 #include "MOZIconHelper.h"
18 #include "mozilla/SVGImageContext.h"
19 #include "nsISVGPaintContext.h"
21 NS_IMPL_ISUPPORTS(nsMacDockSupport, nsIMacDockSupport, nsITaskbarProgress)
23 // This view is used in the dock tile when we're downloading a file.
24 // It draws a progress bar that looks similar to the native progress bar on
25 // 10.12. This style of progress bar is not animated, unlike the pre-10.10
26 // progress bar look which had to redrawn multiple times per second.
27 @interface MOZProgressDockOverlayView : NSView {
28 double mFractionValue;
30 @property double fractionValue;
34 @implementation MOZProgressDockOverlayView
36 @synthesize fractionValue = mFractionValue;
38 - (void)drawRect:(NSRect)aRect {
39 // Erase the background behind this view, i.e. cut a rectangle hole in the
41 [[NSColor clearColor] set];
42 NSRectFill(self.bounds);
44 // Split the height of this view into four quarters. The middle two quarters
45 // will be covered by the actual progress bar.
46 CGFloat radius = self.bounds.size.height / 4;
47 NSRect barBounds = NSInsetRect(self.bounds, 0, radius);
49 NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:barBounds
53 // Draw a grayish background first.
54 [[NSColor colorWithDeviceWhite:0 alpha:0.1] setFill];
57 // Draw a fill in the control accent color for the progress part.
58 NSRect progressFillRect = self.bounds;
59 progressFillRect.size.width *= mFractionValue;
60 [NSGraphicsContext saveGraphicsState];
61 [NSBezierPath clipRect:progressFillRect];
62 [[NSColor controlAccentColor] setFill];
64 [NSGraphicsContext restoreGraphicsState];
66 // Add a shadowy stroke on top.
67 [NSGraphicsContext saveGraphicsState];
69 [[NSColor colorWithDeviceWhite:0 alpha:0.2] setStroke];
70 path.lineWidth = barBounds.size.height / 10;
72 [NSGraphicsContext restoreGraphicsState];
77 nsMacDockSupport::nsMacDockSupport()
78 : mHasBadgeImage(false),
79 mDockTileWrapperView(nil),
81 mProgressDockOverlayView(nil),
82 mProgressState(STATE_NO_PROGRESS),
83 mProgressFraction(0.0) {}
85 nsMacDockSupport::~nsMacDockSupport() {
86 if (mDockTileWrapperView) {
87 [mDockTileWrapperView release];
88 mDockTileWrapperView = nil;
91 [mDockBadgeView release];
94 if (mProgressDockOverlayView) {
95 [mProgressDockOverlayView release];
96 mProgressDockOverlayView = nil;
101 nsMacDockSupport::GetDockMenu(nsIStandaloneNativeMenu** aDockMenu) {
102 nsCOMPtr<nsIStandaloneNativeMenu> dockMenu(mDockMenu);
103 dockMenu.forget(aDockMenu);
108 nsMacDockSupport::SetDockMenu(nsIStandaloneNativeMenu* aDockMenu) {
109 mDockMenu = aDockMenu;
114 nsMacDockSupport::ActivateApplication(bool aIgnoreOtherApplications) {
115 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
117 [[NSApplication sharedApplication]
118 activateIgnoringOtherApps:aIgnoreOtherApplications];
121 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
125 nsMacDockSupport::SetBadgeText(const nsAString& aBadgeText) {
126 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
128 NSDockTile* tile = [[NSApplication sharedApplication] dockTile];
129 mBadgeText = aBadgeText;
130 if (aBadgeText.IsEmpty()) {
131 [tile setBadgeLabel:nil];
133 SetBadgeImage(nullptr, nullptr);
136 setBadgeLabel:[NSString
137 stringWithCharacters:reinterpret_cast<const unichar*>(
139 length:mBadgeText.Length()]];
144 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
148 nsMacDockSupport::GetBadgeText(nsAString& aBadgeText) {
149 aBadgeText = mBadgeText;
154 nsMacDockSupport::SetBadgeImage(imgIContainer* aImage,
155 nsISVGPaintContext* aPaintContext) {
157 mHasBadgeImage = false;
158 if (mDockBadgeView) {
159 mDockBadgeView.image = nullptr;
162 return UpdateDockTile();
165 if (!mBadgeText.IsEmpty()) {
166 mBadgeText.Truncate();
167 NSDockTile* tile = [[NSApplication sharedApplication] dockTile];
168 [tile setBadgeLabel:nil];
171 NS_OBJC_BEGIN_TRY_BLOCK_RETURN
173 mHasBadgeImage = true;
176 mozilla::SVGImageContext svgContext;
177 mozilla::SVGImageContext::MaybeStoreContextPaint(svgContext, aPaintContext,
180 [MOZIconHelper iconImageFromImageContainer:aImage
181 withSize:NSMakeSize(256, 256)
182 svgContext:&svgContext
184 image.resizingMode = NSImageResizingModeStretch;
185 mDockBadgeView.image = image;
187 return UpdateDockTile();
189 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
193 nsMacDockSupport::SetProgressState(nsTaskbarProgressState aState,
194 uint64_t aCurrentValue, uint64_t aMaxValue) {
195 NS_ENSURE_ARG_RANGE(aState, 0, STATE_PAUSED);
196 if (aState == STATE_NO_PROGRESS || aState == STATE_INDETERMINATE) {
197 NS_ENSURE_TRUE(aCurrentValue == 0, NS_ERROR_INVALID_ARG);
198 NS_ENSURE_TRUE(aMaxValue == 0, NS_ERROR_INVALID_ARG);
200 if (aCurrentValue > aMaxValue) {
201 return NS_ERROR_ILLEGAL_VALUE;
204 mProgressState = aState;
205 if (aMaxValue == 0) {
206 mProgressFraction = 0;
208 mProgressFraction = (double)aCurrentValue / aMaxValue;
211 return UpdateDockTile();
214 void nsMacDockSupport::BuildDockTile() {
215 if (!mDockTileWrapperView) {
216 // Create the following NSView hierarchy:
217 // * mDockTileWrapperView (NSView)
218 // * imageView (NSImageView) <- has the application icon
219 // * mDockBadgeView (NSImageView) <- has the dock badge
220 // * mProgressDockOverlayView (MOZProgressDockOverlayView) <- draws the
223 mDockTileWrapperView =
224 [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 32, 32)];
225 mDockTileWrapperView.autoresizingMask =
226 NSViewWidthSizable | NSViewHeightSizable;
228 NSImageView* imageView =
229 [[NSImageView alloc] initWithFrame:[mDockTileWrapperView bounds]];
230 imageView.image = [NSImage imageNamed:@"NSApplicationIcon"];
231 imageView.imageScaling = NSImageScaleAxesIndependently;
232 imageView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
233 [mDockTileWrapperView addSubview:imageView];
236 [[NSImageView alloc] initWithFrame:NSMakeRect(19.5, 19.5, 12, 12)];
237 mDockBadgeView.imageScaling = NSImageScaleProportionallyUpOrDown;
238 mDockBadgeView.autoresizingMask = NSViewMinXMargin | NSViewWidthSizable |
239 NSViewMaxXMargin | NSViewMinYMargin |
240 NSViewHeightSizable | NSViewMaxYMargin;
241 [mDockTileWrapperView addSubview:mDockBadgeView];
243 mProgressDockOverlayView = [[MOZProgressDockOverlayView alloc]
244 initWithFrame:NSMakeRect(1, 3, 30, 4)];
245 mProgressDockOverlayView.autoresizingMask =
246 NSViewMinXMargin | NSViewWidthSizable | NSViewMaxXMargin |
247 NSViewMinYMargin | NSViewHeightSizable | NSViewMaxYMargin;
248 [mDockTileWrapperView addSubview:mProgressDockOverlayView];
252 nsresult nsMacDockSupport::UpdateDockTile() {
253 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
255 if (mProgressState == STATE_NORMAL || mProgressState == STATE_INDETERMINATE ||
259 if (NSApp.dockTile.contentView != mDockTileWrapperView) {
260 NSApp.dockTile.contentView = mDockTileWrapperView;
263 mDockBadgeView.hidden = !mHasBadgeImage;
265 if (mProgressState == STATE_NORMAL) {
266 mProgressDockOverlayView.fractionValue = mProgressFraction;
267 mProgressDockOverlayView.hidden = false;
268 } else if (mProgressState == STATE_INDETERMINATE) {
269 // Indeterminate states are rare. Just fill the entire progress bar in
271 mProgressDockOverlayView.fractionValue = 1.0;
272 mProgressDockOverlayView.hidden = false;
274 mProgressDockOverlayView.hidden = true;
276 [NSApp.dockTile display];
277 } else if (NSApp.dockTile.contentView) {
278 NSApp.dockTile.contentView = nil;
279 [NSApp.dockTile display];
284 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
288 // Private CFURL API used by the Dock.
289 CFPropertyListRef _CFURLCopyPropertyListRepresentation(CFURLRef url);
290 CFURLRef _CFURLCreateFromPropertyListRepresentation(
291 CFAllocatorRef alloc, CFPropertyListRef pListRepresentation);
296 MOZ_RUNINIT const NSArray* const browserAppNames = [NSArray
297 arrayWithObjects:@"Firefox.app", @"Firefox Beta.app",
298 @"Firefox Nightly.app", @"Safari.app", @"WebKit.app",
299 @"Google Chrome.app", @"Google Chrome Canary.app",
300 @"Chromium.app", @"Opera.app", nil];
302 constexpr NSString* const kDockDomainName = @"com.apple.dock";
303 // See https://developer.apple.com/documentation/devicemanagement/dock
304 constexpr NSString* const kDockPersistentAppsKey = @"persistent-apps";
306 // https://developer.apple.com/documentation/devicemanagement/dock/staticitem
307 constexpr NSString* const kDockTileDataKey = @"tile-data";
308 constexpr NSString* const kDockFileDataKey = @"file-data";
310 NSArray* GetPersistentAppsFromDockPlist(NSDictionary* aDockPlist) {
314 NSArray* persistentApps = [aDockPlist objectForKey:kDockPersistentAppsKey];
315 if (![persistentApps isKindOfClass:[NSArray class]]) {
318 return persistentApps;
321 NSString* GetPathForApp(NSDictionary* aPersistantApp) {
322 if (![aPersistantApp isKindOfClass:[NSDictionary class]]) {
325 NSDictionary* tileData = aPersistantApp[kDockTileDataKey];
326 if (![tileData isKindOfClass:[NSDictionary class]]) {
329 NSDictionary* fileData = tileData[kDockFileDataKey];
330 if (![fileData isKindOfClass:[NSDictionary class]]) {
331 // Some special tiles may not have DockFileData but we can ignore those.
334 NSURL* url = CFBridgingRelease(
335 _CFURLCreateFromPropertyListRepresentation(NULL, fileData));
339 return [url isFileURL] ? [url path] : nullptr;
342 // The only reliable way to get our changes to take effect seems to be to use
344 void RefreshDock(NSDictionary* aDockPlist) {
345 [[NSUserDefaults standardUserDefaults] setPersistentDomain:aDockPlist
346 forName:kDockDomainName];
347 NSRunningApplication* dockApp = [[NSRunningApplication
348 runningApplicationsWithBundleIdentifier:@"com.apple.dock"] firstObject];
352 pid_t pid = [dockApp processIdentifier];
360 nsresult nsMacDockSupport::GetIsAppInDock(bool* aIsInDock) {
361 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
365 NSDictionary* dockPlist = [[NSUserDefaults standardUserDefaults]
366 persistentDomainForName:kDockDomainName];
368 return NS_ERROR_FAILURE;
371 NSArray* persistentApps = GetPersistentAppsFromDockPlist(dockPlist);
372 if (!persistentApps) {
373 return NS_ERROR_FAILURE;
376 NSString* appPath = [[NSBundle mainBundle] bundlePath];
378 for (id app in persistentApps) {
379 NSString* persistentAppPath = GetPathForApp(app);
380 if (persistentAppPath && [appPath isEqual:persistentAppPath]) {
388 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
391 nsresult nsMacDockSupport::EnsureAppIsPinnedToDock(
392 const nsAString& aAppPath, const nsAString& aAppToReplacePath,
394 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
396 MOZ_ASSERT(aAppPath != aAppToReplacePath || !aAppPath.IsEmpty());
400 NSString* appPath = !aAppPath.IsEmpty() ? nsCocoaUtils::ToNSString(aAppPath)
401 : [[NSBundle mainBundle] bundlePath];
402 NSString* appToReplacePath = nsCocoaUtils::ToNSString(aAppToReplacePath);
404 NSMutableDictionary* dockPlist = [NSMutableDictionary
405 dictionaryWithDictionary:[[NSUserDefaults standardUserDefaults]
406 persistentDomainForName:kDockDomainName]];
408 return NS_ERROR_FAILURE;
411 NSMutableArray* persistentApps =
412 [NSMutableArray arrayWithArray:GetPersistentAppsFromDockPlist(dockPlist)];
413 if (!persistentApps) {
414 return NS_ERROR_FAILURE;
417 // See the comment for this method in the .idl file for the strategy that we
418 // use here to determine where to pin the app.
419 NSUInteger preexistingAppIndex = NSNotFound; // full path matches
420 NSUInteger sameNameAppIndex = NSNotFound; // app name matches only
421 NSUInteger toReplaceAppIndex = NSNotFound;
422 NSUInteger lastBrowserAppIndex = NSNotFound;
423 for (NSUInteger index = 0; index < [persistentApps count]; ++index) {
424 NSString* persistentAppPath =
425 GetPathForApp([persistentApps objectAtIndex:index]);
427 if ([persistentAppPath isEqualToString:appPath]) {
428 preexistingAppIndex = index;
429 } else if (appToReplacePath &&
430 [persistentAppPath isEqualToString:appToReplacePath]) {
431 toReplaceAppIndex = index;
433 NSString* appName = [appPath lastPathComponent];
434 NSString* persistentAppName = [persistentAppPath lastPathComponent];
436 if ([persistentAppName isEqual:appName]) {
437 if ([appToReplacePath hasPrefix:@"/private/var/folders/"] &&
438 [appToReplacePath containsString:@"/AppTranslocation/"] &&
439 [persistentAppPath hasPrefix:@"/Volumes/"]) {
440 // This is a special case when an app with the same name was
441 // previously dragged and pinned from a quarantined DMG straight to
442 // the Dock and an attempt is now made to pin the same named app to
443 // the Dock. In this case we want to replace the currently pinned app
445 toReplaceAppIndex = index;
447 sameNameAppIndex = index;
450 if ([browserAppNames containsObject:persistentAppName]) {
451 lastBrowserAppIndex = index;
457 // Special cases where we're not going to add a new Dock tile:
458 if (preexistingAppIndex != NSNotFound) {
459 if (toReplaceAppIndex != NSNotFound) {
460 [persistentApps removeObjectAtIndex:toReplaceAppIndex];
461 [dockPlist setObject:persistentApps forKey:kDockPersistentAppsKey];
462 RefreshDock(dockPlist);
469 NSDictionary* newDockTile = nullptr;
471 NSURL* appUrl = [NSURL fileURLWithPath:appPath isDirectory:YES];
472 NSDictionary* dict = CFBridgingRelease(
473 _CFURLCopyPropertyListRepresentation((__bridge CFURLRef)appUrl));
475 return NS_ERROR_FAILURE;
477 NSDictionary* dockTileData =
478 [NSDictionary dictionaryWithObject:dict forKey:kDockFileDataKey];
480 newDockTile = [NSDictionary dictionaryWithObject:dockTileData
481 forKey:kDockTileDataKey];
484 return NS_ERROR_FAILURE;
489 if (toReplaceAppIndex != NSNotFound) {
490 [persistentApps replaceObjectAtIndex:toReplaceAppIndex
491 withObject:newDockTile];
494 if (sameNameAppIndex != NSNotFound) {
495 index = sameNameAppIndex + 1;
496 } else if (lastBrowserAppIndex != NSNotFound) {
497 index = lastBrowserAppIndex + 1;
499 index = [persistentApps count];
501 [persistentApps insertObject:newDockTile atIndex:index];
503 [dockPlist setObject:persistentApps forKey:kDockPersistentAppsKey];
504 RefreshDock(dockPlist);
509 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);