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 icon.
36 [[NSColor clearColor] set];
37 NSRectFill(self.bounds);
39 // Split the height of this view into four quarters. The middle two quarters
40 // will be covered by the actual progress bar.
41 CGFloat radius = self.bounds.size.height / 4;
42 NSRect barBounds = NSInsetRect(self.bounds, 0, radius);
44 NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:barBounds
48 // Draw a grayish background first.
49 [[NSColor colorWithDeviceWhite:0 alpha:0.1] setFill];
52 // Draw a fill in the control accent color for the progress part.
53 NSRect progressFillRect = self.bounds;
54 progressFillRect.size.width *= mFractionValue;
55 [NSGraphicsContext saveGraphicsState];
56 [NSBezierPath clipRect:progressFillRect];
57 [ControlAccentColor() setFill];
59 [NSGraphicsContext restoreGraphicsState];
61 // Add a shadowy stroke on top.
62 [NSGraphicsContext saveGraphicsState];
64 [[NSColor colorWithDeviceWhite:0 alpha:0.2] setStroke];
65 path.lineWidth = barBounds.size.height / 10;
67 [NSGraphicsContext restoreGraphicsState];
72 nsMacDockSupport::nsMacDockSupport()
73 : mDockTileWrapperView(nil),
74 mProgressDockOverlayView(nil),
75 mProgressState(STATE_NO_PROGRESS),
76 mProgressFraction(0.0) {}
78 nsMacDockSupport::~nsMacDockSupport() {
79 if (mDockTileWrapperView) {
80 [mDockTileWrapperView release];
81 mDockTileWrapperView = nil;
83 if (mProgressDockOverlayView) {
84 [mProgressDockOverlayView release];
85 mProgressDockOverlayView = nil;
90 nsMacDockSupport::GetDockMenu(nsIStandaloneNativeMenu** aDockMenu) {
91 nsCOMPtr<nsIStandaloneNativeMenu> dockMenu(mDockMenu);
92 dockMenu.forget(aDockMenu);
97 nsMacDockSupport::SetDockMenu(nsIStandaloneNativeMenu* aDockMenu) {
98 mDockMenu = aDockMenu;
103 nsMacDockSupport::ActivateApplication(bool aIgnoreOtherApplications) {
104 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
106 [[NSApplication sharedApplication] activateIgnoringOtherApps:aIgnoreOtherApplications];
109 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
113 nsMacDockSupport::SetBadgeText(const nsAString& aBadgeText) {
114 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
116 NSDockTile* tile = [[NSApplication sharedApplication] dockTile];
117 mBadgeText = aBadgeText;
118 if (aBadgeText.IsEmpty())
119 [tile setBadgeLabel:nil];
121 [tile setBadgeLabel:[NSString
122 stringWithCharacters:reinterpret_cast<const unichar*>(mBadgeText.get())
123 length:mBadgeText.Length()]];
126 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
130 nsMacDockSupport::GetBadgeText(nsAString& aBadgeText) {
131 aBadgeText = mBadgeText;
136 nsMacDockSupport::SetProgressState(nsTaskbarProgressState aState, uint64_t aCurrentValue,
137 uint64_t aMaxValue) {
138 NS_ENSURE_ARG_RANGE(aState, 0, STATE_PAUSED);
139 if (aState == STATE_NO_PROGRESS || aState == STATE_INDETERMINATE) {
140 NS_ENSURE_TRUE(aCurrentValue == 0, NS_ERROR_INVALID_ARG);
141 NS_ENSURE_TRUE(aMaxValue == 0, NS_ERROR_INVALID_ARG);
143 if (aCurrentValue > aMaxValue) {
144 return NS_ERROR_ILLEGAL_VALUE;
147 mProgressState = aState;
148 if (aMaxValue == 0) {
149 mProgressFraction = 0;
151 mProgressFraction = (double)aCurrentValue / aMaxValue;
154 return UpdateDockTile();
157 nsresult nsMacDockSupport::UpdateDockTile() {
158 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
160 if (mProgressState == STATE_NORMAL || mProgressState == STATE_INDETERMINATE) {
161 if (!mDockTileWrapperView) {
162 // Create the following NSView hierarchy:
163 // * mDockTileWrapperView (NSView)
164 // * imageView (NSImageView) <- has the application icon
165 // * mProgressDockOverlayView (MOZProgressDockOverlayView) <- draws the progress bar
167 mDockTileWrapperView = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 32, 32)];
168 mDockTileWrapperView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
170 NSImageView* imageView = [[NSImageView alloc] initWithFrame:[mDockTileWrapperView bounds]];
171 imageView.image = [NSImage imageNamed:@"NSApplicationIcon"];
172 imageView.imageScaling = NSImageScaleAxesIndependently;
173 imageView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
174 [mDockTileWrapperView addSubview:imageView];
176 mProgressDockOverlayView =
177 [[MOZProgressDockOverlayView alloc] initWithFrame:NSMakeRect(1, 3, 30, 4)];
178 mProgressDockOverlayView.autoresizingMask = NSViewMinXMargin | NSViewWidthSizable |
179 NSViewMaxXMargin | NSViewMinYMargin |
180 NSViewHeightSizable | NSViewMaxYMargin;
181 [mDockTileWrapperView addSubview:mProgressDockOverlayView];
183 if (NSApp.dockTile.contentView != mDockTileWrapperView) {
184 NSApp.dockTile.contentView = mDockTileWrapperView;
187 if (mProgressState == STATE_NORMAL) {
188 mProgressDockOverlayView.fractionValue = mProgressFraction;
190 // Indeterminate states are rare. Just fill the entire progress bar in
192 mProgressDockOverlayView.fractionValue = 1.0;
194 [NSApp.dockTile display];
195 } else if (NSApp.dockTile.contentView) {
196 NSApp.dockTile.contentView = nil;
197 [NSApp.dockTile display];
202 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
206 // Private CFURL API used by the Dock.
207 CFPropertyListRef _CFURLCopyPropertyListRepresentation(CFURLRef url);
208 CFURLRef _CFURLCreateFromPropertyListRepresentation(CFAllocatorRef alloc,
209 CFPropertyListRef pListRepresentation);
214 const NSArray* const browserAppNames =
215 [NSArray arrayWithObjects:@"Firefox.app", @"Firefox Beta.app", @"Firefox Nightly.app",
216 @"Safari.app", @"WebKit.app", @"Google Chrome.app",
217 @"Google Chrome Canary.app", @"Chromium.app", @"Opera.app", nil];
219 constexpr NSString* const kDockDomainName = @"com.apple.dock";
220 // See https://developer.apple.com/documentation/devicemanagement/dock
221 constexpr NSString* const kDockPersistentAppsKey = @"persistent-apps";
222 // See https://developer.apple.com/documentation/devicemanagement/dock/staticitem
223 constexpr NSString* const kDockTileDataKey = @"tile-data";
224 constexpr NSString* const kDockFileDataKey = @"file-data";
226 NSArray* GetPersistentAppsFromDockPlist(NSDictionary* aDockPlist) {
230 NSArray* persistentApps = [aDockPlist objectForKey:kDockPersistentAppsKey];
231 if (![persistentApps isKindOfClass:[NSArray class]]) {
234 return persistentApps;
237 NSString* GetPathForApp(NSDictionary* aPersistantApp) {
238 if (![aPersistantApp isKindOfClass:[NSDictionary class]]) {
241 NSDictionary* tileData = aPersistantApp[kDockTileDataKey];
242 if (![tileData isKindOfClass:[NSDictionary class]]) {
245 NSDictionary* fileData = tileData[kDockFileDataKey];
246 if (![fileData isKindOfClass:[NSDictionary class]]) {
247 // Some special tiles may not have DockFileData but we can ignore those.
250 NSURL* url = CFBridgingRelease(_CFURLCreateFromPropertyListRepresentation(NULL, fileData));
254 return [url isFileURL] ? [url path] : nullptr;
257 // The only reliable way to get our changes to take effect seems to be to use
259 void RefreshDock(NSDictionary* aDockPlist) {
260 [[NSUserDefaults standardUserDefaults] setPersistentDomain:aDockPlist forName:kDockDomainName];
261 NSRunningApplication* dockApp = [[NSRunningApplication
262 runningApplicationsWithBundleIdentifier:@"com.apple.dock"] firstObject];
266 pid_t pid = [dockApp processIdentifier];
274 nsresult nsMacDockSupport::GetIsAppInDock(bool* aIsInDock) {
275 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
279 NSDictionary* dockPlist =
280 [[NSUserDefaults standardUserDefaults] persistentDomainForName:kDockDomainName];
282 return NS_ERROR_FAILURE;
285 NSArray* persistentApps = GetPersistentAppsFromDockPlist(dockPlist);
286 if (!persistentApps) {
287 return NS_ERROR_FAILURE;
290 NSString* appPath = [[NSBundle mainBundle] bundlePath];
292 for (id app in persistentApps) {
293 NSString* persistentAppPath = GetPathForApp(app);
294 if (persistentAppPath && [appPath isEqual:persistentAppPath]) {
302 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
305 nsresult nsMacDockSupport::EnsureAppIsPinnedToDock(const nsAString& aAppPath,
306 const nsAString& aAppToReplacePath,
308 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
310 MOZ_ASSERT(aAppPath != aAppToReplacePath || !aAppPath.IsEmpty());
315 !aAppPath.IsEmpty() ? nsCocoaUtils::ToNSString(aAppPath) : [[NSBundle mainBundle] bundlePath];
316 NSString* appToReplacePath = nsCocoaUtils::ToNSString(aAppToReplacePath);
318 NSMutableDictionary* dockPlist =
319 [NSMutableDictionary dictionaryWithDictionary:[[NSUserDefaults standardUserDefaults]
320 persistentDomainForName:kDockDomainName]];
322 return NS_ERROR_FAILURE;
325 NSMutableArray* persistentApps =
326 [NSMutableArray arrayWithArray:GetPersistentAppsFromDockPlist(dockPlist)];
327 if (!persistentApps) {
328 return NS_ERROR_FAILURE;
331 // See the comment for this method in the .idl file for the strategy that we
332 // use here to determine where to pin the app.
333 NSUInteger preexistingAppIndex = NSNotFound; // full path matches
334 NSUInteger sameNameAppIndex = NSNotFound; // app name matches only
335 NSUInteger toReplaceAppIndex = NSNotFound;
336 NSUInteger lastBrowserAppIndex = NSNotFound;
337 for (NSUInteger index = 0; index < [persistentApps count]; ++index) {
338 NSString* persistentAppPath = GetPathForApp([persistentApps objectAtIndex:index]);
340 if ([persistentAppPath isEqualToString:appPath]) {
341 preexistingAppIndex = index;
342 } else if (appToReplacePath && [persistentAppPath isEqualToString:appToReplacePath]) {
343 toReplaceAppIndex = index;
345 NSString* appName = [appPath lastPathComponent];
346 NSString* persistentAppName = [persistentAppPath lastPathComponent];
348 if ([persistentAppName isEqual:appName]) {
349 if ([appToReplacePath hasPrefix:@"/private/var/folders/"] &&
350 [appToReplacePath containsString:@"/AppTranslocation/"] &&
351 [persistentAppPath hasPrefix:@"/Volumes/"]) {
352 // This is a special case when an app with the same name was
353 // previously dragged and pinned from a quarantined DMG straight to
354 // the Dock and an attempt is now made to pin the same named app to
355 // the Dock. In this case we want to replace the currently pinned app
357 toReplaceAppIndex = index;
359 sameNameAppIndex = index;
362 if ([browserAppNames containsObject:persistentAppName]) {
363 lastBrowserAppIndex = index;
369 // Special cases where we're not going to add a new Dock tile:
370 if (preexistingAppIndex != NSNotFound) {
371 if (toReplaceAppIndex != NSNotFound) {
372 [persistentApps removeObjectAtIndex:toReplaceAppIndex];
373 [dockPlist setObject:persistentApps forKey:kDockPersistentAppsKey];
374 RefreshDock(dockPlist);
381 NSDictionary* newDockTile = nullptr;
383 NSURL* appUrl = [NSURL fileURLWithPath:appPath isDirectory:YES];
385 CFBridgingRelease(_CFURLCopyPropertyListRepresentation((__bridge CFURLRef)appUrl));
387 return NS_ERROR_FAILURE;
389 NSDictionary* dockTileData = [NSDictionary dictionaryWithObject:dict forKey:kDockFileDataKey];
391 newDockTile = [NSDictionary dictionaryWithObject:dockTileData forKey:kDockTileDataKey];
394 return NS_ERROR_FAILURE;
399 if (toReplaceAppIndex != NSNotFound) {
400 [persistentApps replaceObjectAtIndex:toReplaceAppIndex withObject:newDockTile];
403 if (sameNameAppIndex != NSNotFound) {
404 index = sameNameAppIndex + 1;
405 } else if (lastBrowserAppIndex != NSNotFound) {
406 index = lastBrowserAppIndex + 1;
408 index = [persistentApps count];
410 [persistentApps insertObject:newDockTile atIndex:index];
412 [dockPlist setObject:persistentApps forKey:kDockPersistentAppsKey];
413 RefreshDock(dockPlist);
418 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);