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"
17 NS_IMPL_ISUPPORTS(nsMacDockSupport, nsIMacDockSupport, nsITaskbarProgress)
19 // This view is used in the dock tile when we're downloading a file.
20 // It draws a progress bar that looks similar to the native progress bar on
21 // 10.12. This style of progress bar is not animated, unlike the pre-10.10
22 // progress bar look which had to redrawn multiple times per second.
23 @interface MOZProgressDockOverlayView : NSView {
24 double mFractionValue;
26 @property double fractionValue;
30 @implementation MOZProgressDockOverlayView
32 @synthesize fractionValue = mFractionValue;
34 - (void)drawRect:(NSRect)aRect {
35 // Erase the background behind this view, i.e. cut a rectangle hole in the
37 [[NSColor clearColor] set];
38 NSRectFill(self.bounds);
40 // Split the height of this view into four quarters. The middle two quarters
41 // will be covered by the actual progress bar.
42 CGFloat radius = self.bounds.size.height / 4;
43 NSRect barBounds = NSInsetRect(self.bounds, 0, radius);
45 NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:barBounds
49 // Draw a grayish background first.
50 [[NSColor colorWithDeviceWhite:0 alpha:0.1] setFill];
53 // Draw a fill in the control accent color for the progress part.
54 NSRect progressFillRect = self.bounds;
55 progressFillRect.size.width *= mFractionValue;
56 [NSGraphicsContext saveGraphicsState];
57 [NSBezierPath clipRect:progressFillRect];
58 [[NSColor controlAccentColor] setFill];
60 [NSGraphicsContext restoreGraphicsState];
62 // Add a shadowy stroke on top.
63 [NSGraphicsContext saveGraphicsState];
65 [[NSColor colorWithDeviceWhite:0 alpha:0.2] setStroke];
66 path.lineWidth = barBounds.size.height / 10;
68 [NSGraphicsContext restoreGraphicsState];
73 nsMacDockSupport::nsMacDockSupport()
74 : mDockTileWrapperView(nil),
75 mProgressDockOverlayView(nil),
76 mProgressState(STATE_NO_PROGRESS),
77 mProgressFraction(0.0) {}
79 nsMacDockSupport::~nsMacDockSupport() {
80 if (mDockTileWrapperView) {
81 [mDockTileWrapperView release];
82 mDockTileWrapperView = nil;
84 if (mProgressDockOverlayView) {
85 [mProgressDockOverlayView release];
86 mProgressDockOverlayView = nil;
91 nsMacDockSupport::GetDockMenu(nsIStandaloneNativeMenu** aDockMenu) {
92 nsCOMPtr<nsIStandaloneNativeMenu> dockMenu(mDockMenu);
93 dockMenu.forget(aDockMenu);
98 nsMacDockSupport::SetDockMenu(nsIStandaloneNativeMenu* aDockMenu) {
99 mDockMenu = aDockMenu;
104 nsMacDockSupport::ActivateApplication(bool aIgnoreOtherApplications) {
105 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
107 [[NSApplication sharedApplication]
108 activateIgnoringOtherApps:aIgnoreOtherApplications];
111 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
115 nsMacDockSupport::SetBadgeText(const nsAString& aBadgeText) {
116 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
118 NSDockTile* tile = [[NSApplication sharedApplication] dockTile];
119 mBadgeText = aBadgeText;
120 if (aBadgeText.IsEmpty())
121 [tile setBadgeLabel:nil];
124 setBadgeLabel:[NSString
125 stringWithCharacters:reinterpret_cast<const unichar*>(
127 length:mBadgeText.Length()]];
130 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
134 nsMacDockSupport::GetBadgeText(nsAString& aBadgeText) {
135 aBadgeText = mBadgeText;
140 nsMacDockSupport::SetProgressState(nsTaskbarProgressState aState,
141 uint64_t aCurrentValue, uint64_t aMaxValue) {
142 NS_ENSURE_ARG_RANGE(aState, 0, STATE_PAUSED);
143 if (aState == STATE_NO_PROGRESS || aState == STATE_INDETERMINATE) {
144 NS_ENSURE_TRUE(aCurrentValue == 0, NS_ERROR_INVALID_ARG);
145 NS_ENSURE_TRUE(aMaxValue == 0, NS_ERROR_INVALID_ARG);
147 if (aCurrentValue > aMaxValue) {
148 return NS_ERROR_ILLEGAL_VALUE;
151 mProgressState = aState;
152 if (aMaxValue == 0) {
153 mProgressFraction = 0;
155 mProgressFraction = (double)aCurrentValue / aMaxValue;
158 return UpdateDockTile();
161 nsresult nsMacDockSupport::UpdateDockTile() {
162 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
164 if (mProgressState == STATE_NORMAL || mProgressState == STATE_INDETERMINATE) {
165 if (!mDockTileWrapperView) {
166 // Create the following NSView hierarchy:
167 // * mDockTileWrapperView (NSView)
168 // * imageView (NSImageView) <- has the application icon
169 // * mProgressDockOverlayView (MOZProgressDockOverlayView) <- draws the
172 mDockTileWrapperView =
173 [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 32, 32)];
174 mDockTileWrapperView.autoresizingMask =
175 NSViewWidthSizable | NSViewHeightSizable;
177 NSImageView* imageView =
178 [[NSImageView alloc] initWithFrame:[mDockTileWrapperView bounds]];
179 imageView.image = [NSImage imageNamed:@"NSApplicationIcon"];
180 imageView.imageScaling = NSImageScaleAxesIndependently;
181 imageView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
182 [mDockTileWrapperView addSubview:imageView];
184 mProgressDockOverlayView = [[MOZProgressDockOverlayView alloc]
185 initWithFrame:NSMakeRect(1, 3, 30, 4)];
186 mProgressDockOverlayView.autoresizingMask =
187 NSViewMinXMargin | NSViewWidthSizable | NSViewMaxXMargin |
188 NSViewMinYMargin | NSViewHeightSizable | NSViewMaxYMargin;
189 [mDockTileWrapperView addSubview:mProgressDockOverlayView];
191 if (NSApp.dockTile.contentView != mDockTileWrapperView) {
192 NSApp.dockTile.contentView = mDockTileWrapperView;
195 if (mProgressState == STATE_NORMAL) {
196 mProgressDockOverlayView.fractionValue = mProgressFraction;
198 // Indeterminate states are rare. Just fill the entire progress bar in
200 mProgressDockOverlayView.fractionValue = 1.0;
202 [NSApp.dockTile display];
203 } else if (NSApp.dockTile.contentView) {
204 NSApp.dockTile.contentView = nil;
205 [NSApp.dockTile display];
210 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
214 // Private CFURL API used by the Dock.
215 CFPropertyListRef _CFURLCopyPropertyListRepresentation(CFURLRef url);
216 CFURLRef _CFURLCreateFromPropertyListRepresentation(
217 CFAllocatorRef alloc, CFPropertyListRef pListRepresentation);
222 const NSArray* const browserAppNames = [NSArray
223 arrayWithObjects:@"Firefox.app", @"Firefox Beta.app",
224 @"Firefox Nightly.app", @"Safari.app", @"WebKit.app",
225 @"Google Chrome.app", @"Google Chrome Canary.app",
226 @"Chromium.app", @"Opera.app", nil];
228 constexpr NSString* const kDockDomainName = @"com.apple.dock";
229 // See https://developer.apple.com/documentation/devicemanagement/dock
230 constexpr NSString* const kDockPersistentAppsKey = @"persistent-apps";
232 // https://developer.apple.com/documentation/devicemanagement/dock/staticitem
233 constexpr NSString* const kDockTileDataKey = @"tile-data";
234 constexpr NSString* const kDockFileDataKey = @"file-data";
236 NSArray* GetPersistentAppsFromDockPlist(NSDictionary* aDockPlist) {
240 NSArray* persistentApps = [aDockPlist objectForKey:kDockPersistentAppsKey];
241 if (![persistentApps isKindOfClass:[NSArray class]]) {
244 return persistentApps;
247 NSString* GetPathForApp(NSDictionary* aPersistantApp) {
248 if (![aPersistantApp isKindOfClass:[NSDictionary class]]) {
251 NSDictionary* tileData = aPersistantApp[kDockTileDataKey];
252 if (![tileData isKindOfClass:[NSDictionary class]]) {
255 NSDictionary* fileData = tileData[kDockFileDataKey];
256 if (![fileData isKindOfClass:[NSDictionary class]]) {
257 // Some special tiles may not have DockFileData but we can ignore those.
260 NSURL* url = CFBridgingRelease(
261 _CFURLCreateFromPropertyListRepresentation(NULL, fileData));
265 return [url isFileURL] ? [url path] : nullptr;
268 // The only reliable way to get our changes to take effect seems to be to use
270 void RefreshDock(NSDictionary* aDockPlist) {
271 [[NSUserDefaults standardUserDefaults] setPersistentDomain:aDockPlist
272 forName:kDockDomainName];
273 NSRunningApplication* dockApp = [[NSRunningApplication
274 runningApplicationsWithBundleIdentifier:@"com.apple.dock"] firstObject];
278 pid_t pid = [dockApp processIdentifier];
286 nsresult nsMacDockSupport::GetIsAppInDock(bool* aIsInDock) {
287 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
291 NSDictionary* dockPlist = [[NSUserDefaults standardUserDefaults]
292 persistentDomainForName:kDockDomainName];
294 return NS_ERROR_FAILURE;
297 NSArray* persistentApps = GetPersistentAppsFromDockPlist(dockPlist);
298 if (!persistentApps) {
299 return NS_ERROR_FAILURE;
302 NSString* appPath = [[NSBundle mainBundle] bundlePath];
304 for (id app in persistentApps) {
305 NSString* persistentAppPath = GetPathForApp(app);
306 if (persistentAppPath && [appPath isEqual:persistentAppPath]) {
314 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
317 nsresult nsMacDockSupport::EnsureAppIsPinnedToDock(
318 const nsAString& aAppPath, const nsAString& aAppToReplacePath,
320 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
322 MOZ_ASSERT(aAppPath != aAppToReplacePath || !aAppPath.IsEmpty());
326 NSString* appPath = !aAppPath.IsEmpty() ? nsCocoaUtils::ToNSString(aAppPath)
327 : [[NSBundle mainBundle] bundlePath];
328 NSString* appToReplacePath = nsCocoaUtils::ToNSString(aAppToReplacePath);
330 NSMutableDictionary* dockPlist = [NSMutableDictionary
331 dictionaryWithDictionary:[[NSUserDefaults standardUserDefaults]
332 persistentDomainForName:kDockDomainName]];
334 return NS_ERROR_FAILURE;
337 NSMutableArray* persistentApps =
338 [NSMutableArray arrayWithArray:GetPersistentAppsFromDockPlist(dockPlist)];
339 if (!persistentApps) {
340 return NS_ERROR_FAILURE;
343 // See the comment for this method in the .idl file for the strategy that we
344 // use here to determine where to pin the app.
345 NSUInteger preexistingAppIndex = NSNotFound; // full path matches
346 NSUInteger sameNameAppIndex = NSNotFound; // app name matches only
347 NSUInteger toReplaceAppIndex = NSNotFound;
348 NSUInteger lastBrowserAppIndex = NSNotFound;
349 for (NSUInteger index = 0; index < [persistentApps count]; ++index) {
350 NSString* persistentAppPath =
351 GetPathForApp([persistentApps objectAtIndex:index]);
353 if ([persistentAppPath isEqualToString:appPath]) {
354 preexistingAppIndex = index;
355 } else if (appToReplacePath &&
356 [persistentAppPath isEqualToString:appToReplacePath]) {
357 toReplaceAppIndex = index;
359 NSString* appName = [appPath lastPathComponent];
360 NSString* persistentAppName = [persistentAppPath lastPathComponent];
362 if ([persistentAppName isEqual:appName]) {
363 if ([appToReplacePath hasPrefix:@"/private/var/folders/"] &&
364 [appToReplacePath containsString:@"/AppTranslocation/"] &&
365 [persistentAppPath hasPrefix:@"/Volumes/"]) {
366 // This is a special case when an app with the same name was
367 // previously dragged and pinned from a quarantined DMG straight to
368 // the Dock and an attempt is now made to pin the same named app to
369 // the Dock. In this case we want to replace the currently pinned app
371 toReplaceAppIndex = index;
373 sameNameAppIndex = index;
376 if ([browserAppNames containsObject:persistentAppName]) {
377 lastBrowserAppIndex = index;
383 // Special cases where we're not going to add a new Dock tile:
384 if (preexistingAppIndex != NSNotFound) {
385 if (toReplaceAppIndex != NSNotFound) {
386 [persistentApps removeObjectAtIndex:toReplaceAppIndex];
387 [dockPlist setObject:persistentApps forKey:kDockPersistentAppsKey];
388 RefreshDock(dockPlist);
395 NSDictionary* newDockTile = nullptr;
397 NSURL* appUrl = [NSURL fileURLWithPath:appPath isDirectory:YES];
398 NSDictionary* dict = CFBridgingRelease(
399 _CFURLCopyPropertyListRepresentation((__bridge CFURLRef)appUrl));
401 return NS_ERROR_FAILURE;
403 NSDictionary* dockTileData =
404 [NSDictionary dictionaryWithObject:dict forKey:kDockFileDataKey];
406 newDockTile = [NSDictionary dictionaryWithObject:dockTileData
407 forKey:kDockTileDataKey];
410 return NS_ERROR_FAILURE;
415 if (toReplaceAppIndex != NSNotFound) {
416 [persistentApps replaceObjectAtIndex:toReplaceAppIndex
417 withObject:newDockTile];
420 if (sameNameAppIndex != NSNotFound) {
421 index = sameNameAppIndex + 1;
422 } else if (lastBrowserAppIndex != NSNotFound) {
423 index = lastBrowserAppIndex + 1;
425 index = [persistentApps count];
427 [persistentApps insertObject:newDockTile atIndex:index];
429 [dockPlist setObject:persistentApps forKey:kDockPersistentAppsKey];
430 RefreshDock(dockPlist);
435 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);