no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / widget / cocoa / nsMacDockSupport.mm
blob5e9da63e10c956b5e52a1108bb8022c8d4164812
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>
8 #include <signal.h>
10 #include "nsCocoaUtils.h"
11 #include "nsComponentManagerUtils.h"
12 #include "nsMacDockSupport.h"
13 #include "nsObjCExceptions.h"
14 #include "nsNativeThemeColors.h"
15 #include "nsString.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;
28 @end
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
36   // icon.
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
46                                                        xRadius:radius
47                                                        yRadius:radius];
49   // Draw a grayish background first.
50   [[NSColor colorWithDeviceWhite:0 alpha:0.1] setFill];
51   [path fill];
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];
59   [path fill];
60   [NSGraphicsContext restoreGraphicsState];
62   // Add a shadowy stroke on top.
63   [NSGraphicsContext saveGraphicsState];
64   [path addClip];
65   [[NSColor colorWithDeviceWhite:0 alpha:0.2] setStroke];
66   path.lineWidth = barBounds.size.height / 10;
67   [path stroke];
68   [NSGraphicsContext restoreGraphicsState];
71 @end
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;
83   }
84   if (mProgressDockOverlayView) {
85     [mProgressDockOverlayView release];
86     mProgressDockOverlayView = nil;
87   }
90 NS_IMETHODIMP
91 nsMacDockSupport::GetDockMenu(nsIStandaloneNativeMenu** aDockMenu) {
92   nsCOMPtr<nsIStandaloneNativeMenu> dockMenu(mDockMenu);
93   dockMenu.forget(aDockMenu);
94   return NS_OK;
97 NS_IMETHODIMP
98 nsMacDockSupport::SetDockMenu(nsIStandaloneNativeMenu* aDockMenu) {
99   mDockMenu = aDockMenu;
100   return NS_OK;
103 NS_IMETHODIMP
104 nsMacDockSupport::ActivateApplication(bool aIgnoreOtherApplications) {
105   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
107   [[NSApplication sharedApplication]
108       activateIgnoringOtherApps:aIgnoreOtherApplications];
109   return NS_OK;
111   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
114 NS_IMETHODIMP
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];
122   else
123     [tile
124         setBadgeLabel:[NSString
125                           stringWithCharacters:reinterpret_cast<const unichar*>(
126                                                    mBadgeText.get())
127                                         length:mBadgeText.Length()]];
128   return NS_OK;
130   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
133 NS_IMETHODIMP
134 nsMacDockSupport::GetBadgeText(nsAString& aBadgeText) {
135   aBadgeText = mBadgeText;
136   return NS_OK;
139 NS_IMETHODIMP
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);
146   }
147   if (aCurrentValue > aMaxValue) {
148     return NS_ERROR_ILLEGAL_VALUE;
149   }
151   mProgressState = aState;
152   if (aMaxValue == 0) {
153     mProgressFraction = 0;
154   } else {
155     mProgressFraction = (double)aCurrentValue / aMaxValue;
156   }
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
170       //    progress bar
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];
190     }
191     if (NSApp.dockTile.contentView != mDockTileWrapperView) {
192       NSApp.dockTile.contentView = mDockTileWrapperView;
193     }
195     if (mProgressState == STATE_NORMAL) {
196       mProgressDockOverlayView.fractionValue = mProgressFraction;
197     } else {
198       // Indeterminate states are rare. Just fill the entire progress bar in
199       // that case.
200       mProgressDockOverlayView.fractionValue = 1.0;
201     }
202     [NSApp.dockTile display];
203   } else if (NSApp.dockTile.contentView) {
204     NSApp.dockTile.contentView = nil;
205     [NSApp.dockTile display];
206   }
208   return NS_OK;
210   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
213 extern "C" {
214 // Private CFURL API used by the Dock.
215 CFPropertyListRef _CFURLCopyPropertyListRepresentation(CFURLRef url);
216 CFURLRef _CFURLCreateFromPropertyListRepresentation(
217     CFAllocatorRef alloc, CFPropertyListRef pListRepresentation);
218 }  // extern "C"
220 namespace {
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";
231 // See
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) {
237   if (!aDockPlist) {
238     return nil;
239   }
240   NSArray* persistentApps = [aDockPlist objectForKey:kDockPersistentAppsKey];
241   if (![persistentApps isKindOfClass:[NSArray class]]) {
242     return nil;
243   }
244   return persistentApps;
247 NSString* GetPathForApp(NSDictionary* aPersistantApp) {
248   if (![aPersistantApp isKindOfClass:[NSDictionary class]]) {
249     return nil;
250   }
251   NSDictionary* tileData = aPersistantApp[kDockTileDataKey];
252   if (![tileData isKindOfClass:[NSDictionary class]]) {
253     return nil;
254   }
255   NSDictionary* fileData = tileData[kDockFileDataKey];
256   if (![fileData isKindOfClass:[NSDictionary class]]) {
257     // Some special tiles may not have DockFileData but we can ignore those.
258     return nil;
259   }
260   NSURL* url = CFBridgingRelease(
261       _CFURLCreateFromPropertyListRepresentation(NULL, fileData));
262   if (!url) {
263     return nil;
264   }
265   return [url isFileURL] ? [url path] : nullptr;
268 // The only reliable way to get our changes to take effect seems to be to use
269 // `kill`.
270 void RefreshDock(NSDictionary* aDockPlist) {
271   [[NSUserDefaults standardUserDefaults] setPersistentDomain:aDockPlist
272                                                      forName:kDockDomainName];
273   NSRunningApplication* dockApp = [[NSRunningApplication
274       runningApplicationsWithBundleIdentifier:@"com.apple.dock"] firstObject];
275   if (!dockApp) {
276     return;
277   }
278   pid_t pid = [dockApp processIdentifier];
279   if (pid > 0) {
280     kill(pid, SIGTERM);
281   }
284 }  // namespace
286 nsresult nsMacDockSupport::GetIsAppInDock(bool* aIsInDock) {
287   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
289   *aIsInDock = false;
291   NSDictionary* dockPlist = [[NSUserDefaults standardUserDefaults]
292       persistentDomainForName:kDockDomainName];
293   if (!dockPlist) {
294     return NS_ERROR_FAILURE;
295   }
297   NSArray* persistentApps = GetPersistentAppsFromDockPlist(dockPlist);
298   if (!persistentApps) {
299     return NS_ERROR_FAILURE;
300   }
302   NSString* appPath = [[NSBundle mainBundle] bundlePath];
304   for (id app in persistentApps) {
305     NSString* persistentAppPath = GetPathForApp(app);
306     if (persistentAppPath && [appPath isEqual:persistentAppPath]) {
307       *aIsInDock = true;
308       break;
309     }
310   }
312   return NS_OK;
314   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
317 nsresult nsMacDockSupport::EnsureAppIsPinnedToDock(
318     const nsAString& aAppPath, const nsAString& aAppToReplacePath,
319     bool* aIsInDock) {
320   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
322   MOZ_ASSERT(aAppPath != aAppToReplacePath || !aAppPath.IsEmpty());
324   *aIsInDock = false;
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]];
333   if (!dockPlist) {
334     return NS_ERROR_FAILURE;
335   }
337   NSMutableArray* persistentApps =
338       [NSMutableArray arrayWithArray:GetPersistentAppsFromDockPlist(dockPlist)];
339   if (!persistentApps) {
340     return NS_ERROR_FAILURE;
341   }
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;
358     } else {
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
370           // icon.
371           toReplaceAppIndex = index;
372         } else {
373           sameNameAppIndex = index;
374         }
375       } else {
376         if ([browserAppNames containsObject:persistentAppName]) {
377           lastBrowserAppIndex = index;
378         }
379       }
380     }
381   }
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);
389     }
390     *aIsInDock = true;
391     return NS_OK;
392   }
394   // Create new tile:
395   NSDictionary* newDockTile = nullptr;
396   {
397     NSURL* appUrl = [NSURL fileURLWithPath:appPath isDirectory:YES];
398     NSDictionary* dict = CFBridgingRelease(
399         _CFURLCopyPropertyListRepresentation((__bridge CFURLRef)appUrl));
400     if (!dict) {
401       return NS_ERROR_FAILURE;
402     }
403     NSDictionary* dockTileData =
404         [NSDictionary dictionaryWithObject:dict forKey:kDockFileDataKey];
405     if (dockTileData) {
406       newDockTile = [NSDictionary dictionaryWithObject:dockTileData
407                                                 forKey:kDockTileDataKey];
408     }
409     if (!newDockTile) {
410       return NS_ERROR_FAILURE;
411     }
412   }
414   // Update the Dock:
415   if (toReplaceAppIndex != NSNotFound) {
416     [persistentApps replaceObjectAtIndex:toReplaceAppIndex
417                               withObject:newDockTile];
418   } else {
419     NSUInteger index;
420     if (sameNameAppIndex != NSNotFound) {
421       index = sameNameAppIndex + 1;
422     } else if (lastBrowserAppIndex != NSNotFound) {
423       index = lastBrowserAppIndex + 1;
424     } else {
425       index = [persistentApps count];
426     }
427     [persistentApps insertObject:newDockTile atIndex:index];
428   }
429   [dockPlist setObject:persistentApps forKey:kDockPersistentAppsKey];
430   RefreshDock(dockPlist);
432   *aIsInDock = true;
433   return NS_OK;
435   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);