Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / widget / cocoa / nsMacDockSupport.mm
blob685b330407c3a31d3302cc20ac9d695e5597fc4d
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"
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;
32 @end
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
40   // icon.
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
50                                                        xRadius:radius
51                                                        yRadius:radius];
53   // Draw a grayish background first.
54   [[NSColor colorWithDeviceWhite:0 alpha:0.1] setFill];
55   [path fill];
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];
63   [path fill];
64   [NSGraphicsContext restoreGraphicsState];
66   // Add a shadowy stroke on top.
67   [NSGraphicsContext saveGraphicsState];
68   [path addClip];
69   [[NSColor colorWithDeviceWhite:0 alpha:0.2] setStroke];
70   path.lineWidth = barBounds.size.height / 10;
71   [path stroke];
72   [NSGraphicsContext restoreGraphicsState];
75 @end
77 nsMacDockSupport::nsMacDockSupport()
78     : mHasBadgeImage(false),
79       mDockTileWrapperView(nil),
80       mDockBadgeView(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;
89   }
90   if (mDockBadgeView) {
91     [mDockBadgeView release];
92     mDockBadgeView = nil;
93   }
94   if (mProgressDockOverlayView) {
95     [mProgressDockOverlayView release];
96     mProgressDockOverlayView = nil;
97   }
100 NS_IMETHODIMP
101 nsMacDockSupport::GetDockMenu(nsIStandaloneNativeMenu** aDockMenu) {
102   nsCOMPtr<nsIStandaloneNativeMenu> dockMenu(mDockMenu);
103   dockMenu.forget(aDockMenu);
104   return NS_OK;
107 NS_IMETHODIMP
108 nsMacDockSupport::SetDockMenu(nsIStandaloneNativeMenu* aDockMenu) {
109   mDockMenu = aDockMenu;
110   return NS_OK;
113 NS_IMETHODIMP
114 nsMacDockSupport::ActivateApplication(bool aIgnoreOtherApplications) {
115   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
117   [[NSApplication sharedApplication]
118       activateIgnoringOtherApps:aIgnoreOtherApplications];
119   return NS_OK;
121   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
124 NS_IMETHODIMP
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];
132   } else {
133     SetBadgeImage(nullptr, nullptr);
135     [tile
136         setBadgeLabel:[NSString
137                           stringWithCharacters:reinterpret_cast<const unichar*>(
138                                                    mBadgeText.get())
139                                         length:mBadgeText.Length()]];
140   }
142   return NS_OK;
144   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
147 NS_IMETHODIMP
148 nsMacDockSupport::GetBadgeText(nsAString& aBadgeText) {
149   aBadgeText = mBadgeText;
150   return NS_OK;
153 NS_IMETHODIMP
154 nsMacDockSupport::SetBadgeImage(imgIContainer* aImage,
155                                 nsISVGPaintContext* aPaintContext) {
156   if (!aImage) {
157     mHasBadgeImage = false;
158     if (mDockBadgeView) {
159       mDockBadgeView.image = nullptr;
160     }
162     return UpdateDockTile();
163   }
165   if (!mBadgeText.IsEmpty()) {
166     mBadgeText.Truncate();
167     NSDockTile* tile = [[NSApplication sharedApplication] dockTile];
168     [tile setBadgeLabel:nil];
169   }
171   NS_OBJC_BEGIN_TRY_BLOCK_RETURN
173   mHasBadgeImage = true;
174   BuildDockTile();
176   mozilla::SVGImageContext svgContext;
177   mozilla::SVGImageContext::MaybeStoreContextPaint(svgContext, aPaintContext,
178                                                    aImage);
179   NSImage* image =
180       [MOZIconHelper iconImageFromImageContainer:aImage
181                                         withSize:NSMakeSize(256, 256)
182                                       svgContext:&svgContext
183                                      scaleFactor:0.0];
184   image.resizingMode = NSImageResizingModeStretch;
185   mDockBadgeView.image = image;
187   return UpdateDockTile();
189   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
192 NS_IMETHODIMP
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);
199   }
200   if (aCurrentValue > aMaxValue) {
201     return NS_ERROR_ILLEGAL_VALUE;
202   }
204   mProgressState = aState;
205   if (aMaxValue == 0) {
206     mProgressFraction = 0;
207   } else {
208     mProgressFraction = (double)aCurrentValue / aMaxValue;
209   }
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
221     //    progress bar
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];
235     mDockBadgeView =
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];
249   }
252 nsresult nsMacDockSupport::UpdateDockTile() {
253   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
255   if (mProgressState == STATE_NORMAL || mProgressState == STATE_INDETERMINATE ||
256       mHasBadgeImage) {
257     BuildDockTile();
259     if (NSApp.dockTile.contentView != mDockTileWrapperView) {
260       NSApp.dockTile.contentView = mDockTileWrapperView;
261     }
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
270       // that case.
271       mProgressDockOverlayView.fractionValue = 1.0;
272       mProgressDockOverlayView.hidden = false;
273     } else {
274       mProgressDockOverlayView.hidden = true;
275     }
276     [NSApp.dockTile display];
277   } else if (NSApp.dockTile.contentView) {
278     NSApp.dockTile.contentView = nil;
279     [NSApp.dockTile display];
280   }
282   return NS_OK;
284   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
287 extern "C" {
288 // Private CFURL API used by the Dock.
289 CFPropertyListRef _CFURLCopyPropertyListRepresentation(CFURLRef url);
290 CFURLRef _CFURLCreateFromPropertyListRepresentation(
291     CFAllocatorRef alloc, CFPropertyListRef pListRepresentation);
292 }  // extern "C"
294 namespace {
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";
305 // See
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) {
311   if (!aDockPlist) {
312     return nil;
313   }
314   NSArray* persistentApps = [aDockPlist objectForKey:kDockPersistentAppsKey];
315   if (![persistentApps isKindOfClass:[NSArray class]]) {
316     return nil;
317   }
318   return persistentApps;
321 NSString* GetPathForApp(NSDictionary* aPersistantApp) {
322   if (![aPersistantApp isKindOfClass:[NSDictionary class]]) {
323     return nil;
324   }
325   NSDictionary* tileData = aPersistantApp[kDockTileDataKey];
326   if (![tileData isKindOfClass:[NSDictionary class]]) {
327     return nil;
328   }
329   NSDictionary* fileData = tileData[kDockFileDataKey];
330   if (![fileData isKindOfClass:[NSDictionary class]]) {
331     // Some special tiles may not have DockFileData but we can ignore those.
332     return nil;
333   }
334   NSURL* url = CFBridgingRelease(
335       _CFURLCreateFromPropertyListRepresentation(NULL, fileData));
336   if (!url) {
337     return nil;
338   }
339   return [url isFileURL] ? [url path] : nullptr;
342 // The only reliable way to get our changes to take effect seems to be to use
343 // `kill`.
344 void RefreshDock(NSDictionary* aDockPlist) {
345   [[NSUserDefaults standardUserDefaults] setPersistentDomain:aDockPlist
346                                                      forName:kDockDomainName];
347   NSRunningApplication* dockApp = [[NSRunningApplication
348       runningApplicationsWithBundleIdentifier:@"com.apple.dock"] firstObject];
349   if (!dockApp) {
350     return;
351   }
352   pid_t pid = [dockApp processIdentifier];
353   if (pid > 0) {
354     kill(pid, SIGTERM);
355   }
358 }  // namespace
360 nsresult nsMacDockSupport::GetIsAppInDock(bool* aIsInDock) {
361   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
363   *aIsInDock = false;
365   NSDictionary* dockPlist = [[NSUserDefaults standardUserDefaults]
366       persistentDomainForName:kDockDomainName];
367   if (!dockPlist) {
368     return NS_ERROR_FAILURE;
369   }
371   NSArray* persistentApps = GetPersistentAppsFromDockPlist(dockPlist);
372   if (!persistentApps) {
373     return NS_ERROR_FAILURE;
374   }
376   NSString* appPath = [[NSBundle mainBundle] bundlePath];
378   for (id app in persistentApps) {
379     NSString* persistentAppPath = GetPathForApp(app);
380     if (persistentAppPath && [appPath isEqual:persistentAppPath]) {
381       *aIsInDock = true;
382       break;
383     }
384   }
386   return NS_OK;
388   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
391 nsresult nsMacDockSupport::EnsureAppIsPinnedToDock(
392     const nsAString& aAppPath, const nsAString& aAppToReplacePath,
393     bool* aIsInDock) {
394   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
396   MOZ_ASSERT(aAppPath != aAppToReplacePath || !aAppPath.IsEmpty());
398   *aIsInDock = false;
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]];
407   if (!dockPlist) {
408     return NS_ERROR_FAILURE;
409   }
411   NSMutableArray* persistentApps =
412       [NSMutableArray arrayWithArray:GetPersistentAppsFromDockPlist(dockPlist)];
413   if (!persistentApps) {
414     return NS_ERROR_FAILURE;
415   }
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;
432     } else {
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
444           // icon.
445           toReplaceAppIndex = index;
446         } else {
447           sameNameAppIndex = index;
448         }
449       } else {
450         if ([browserAppNames containsObject:persistentAppName]) {
451           lastBrowserAppIndex = index;
452         }
453       }
454     }
455   }
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);
463     }
464     *aIsInDock = true;
465     return NS_OK;
466   }
468   // Create new tile:
469   NSDictionary* newDockTile = nullptr;
470   {
471     NSURL* appUrl = [NSURL fileURLWithPath:appPath isDirectory:YES];
472     NSDictionary* dict = CFBridgingRelease(
473         _CFURLCopyPropertyListRepresentation((__bridge CFURLRef)appUrl));
474     if (!dict) {
475       return NS_ERROR_FAILURE;
476     }
477     NSDictionary* dockTileData =
478         [NSDictionary dictionaryWithObject:dict forKey:kDockFileDataKey];
479     if (dockTileData) {
480       newDockTile = [NSDictionary dictionaryWithObject:dockTileData
481                                                 forKey:kDockTileDataKey];
482     }
483     if (!newDockTile) {
484       return NS_ERROR_FAILURE;
485     }
486   }
488   // Update the Dock:
489   if (toReplaceAppIndex != NSNotFound) {
490     [persistentApps replaceObjectAtIndex:toReplaceAppIndex
491                               withObject:newDockTile];
492   } else {
493     NSUInteger index;
494     if (sameNameAppIndex != NSNotFound) {
495       index = sameNameAppIndex + 1;
496     } else if (lastBrowserAppIndex != NSNotFound) {
497       index = lastBrowserAppIndex + 1;
498     } else {
499       index = [persistentApps count];
500     }
501     [persistentApps insertObject:newDockTile atIndex:index];
502   }
503   [dockPlist setObject:persistentApps forKey:kDockPersistentAppsKey];
504   RefreshDock(dockPlist);
506   *aIsInDock = true;
507   return NS_OK;
509   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);