Bug 1761357 [wpt PR 33355] - Fix #33204: Move Safari stable runs to Big Sur, a=testonly
[gecko.git] / widget / cocoa / nsMacDockSupport.mm
blobe69c0b625985b1954ba8e127c7f46bcb69e079b1
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 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
45                                                        xRadius:radius
46                                                        yRadius:radius];
48   // Draw a grayish background first.
49   [[NSColor colorWithDeviceWhite:0 alpha:0.1] setFill];
50   [path fill];
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];
58   [path fill];
59   [NSGraphicsContext restoreGraphicsState];
61   // Add a shadowy stroke on top.
62   [NSGraphicsContext saveGraphicsState];
63   [path addClip];
64   [[NSColor colorWithDeviceWhite:0 alpha:0.2] setStroke];
65   path.lineWidth = barBounds.size.height / 10;
66   [path stroke];
67   [NSGraphicsContext restoreGraphicsState];
70 @end
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;
82   }
83   if (mProgressDockOverlayView) {
84     [mProgressDockOverlayView release];
85     mProgressDockOverlayView = nil;
86   }
89 NS_IMETHODIMP
90 nsMacDockSupport::GetDockMenu(nsIStandaloneNativeMenu** aDockMenu) {
91   nsCOMPtr<nsIStandaloneNativeMenu> dockMenu(mDockMenu);
92   dockMenu.forget(aDockMenu);
93   return NS_OK;
96 NS_IMETHODIMP
97 nsMacDockSupport::SetDockMenu(nsIStandaloneNativeMenu* aDockMenu) {
98   mDockMenu = aDockMenu;
99   return NS_OK;
102 NS_IMETHODIMP
103 nsMacDockSupport::ActivateApplication(bool aIgnoreOtherApplications) {
104   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
106   [[NSApplication sharedApplication] activateIgnoringOtherApps:aIgnoreOtherApplications];
107   return NS_OK;
109   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
112 NS_IMETHODIMP
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];
120   else
121     [tile setBadgeLabel:[NSString
122                             stringWithCharacters:reinterpret_cast<const unichar*>(mBadgeText.get())
123                                           length:mBadgeText.Length()]];
124   return NS_OK;
126   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
129 NS_IMETHODIMP
130 nsMacDockSupport::GetBadgeText(nsAString& aBadgeText) {
131   aBadgeText = mBadgeText;
132   return NS_OK;
135 NS_IMETHODIMP
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);
142   }
143   if (aCurrentValue > aMaxValue) {
144     return NS_ERROR_ILLEGAL_VALUE;
145   }
147   mProgressState = aState;
148   if (aMaxValue == 0) {
149     mProgressFraction = 0;
150   } else {
151     mProgressFraction = (double)aCurrentValue / aMaxValue;
152   }
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];
182     }
183     if (NSApp.dockTile.contentView != mDockTileWrapperView) {
184       NSApp.dockTile.contentView = mDockTileWrapperView;
185     }
187     if (mProgressState == STATE_NORMAL) {
188       mProgressDockOverlayView.fractionValue = mProgressFraction;
189     } else {
190       // Indeterminate states are rare. Just fill the entire progress bar in
191       // that case.
192       mProgressDockOverlayView.fractionValue = 1.0;
193     }
194     [NSApp.dockTile display];
195   } else if (NSApp.dockTile.contentView) {
196     NSApp.dockTile.contentView = nil;
197     [NSApp.dockTile display];
198   }
200   return NS_OK;
202   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
205 extern "C" {
206 // Private CFURL API used by the Dock.
207 CFPropertyListRef _CFURLCopyPropertyListRepresentation(CFURLRef url);
208 CFURLRef _CFURLCreateFromPropertyListRepresentation(CFAllocatorRef alloc,
209                                                     CFPropertyListRef pListRepresentation);
210 }  // extern "C"
212 namespace {
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) {
227   if (!aDockPlist) {
228     return nil;
229   }
230   NSArray* persistentApps = [aDockPlist objectForKey:kDockPersistentAppsKey];
231   if (![persistentApps isKindOfClass:[NSArray class]]) {
232     return nil;
233   }
234   return persistentApps;
237 NSString* GetPathForApp(NSDictionary* aPersistantApp) {
238   if (![aPersistantApp isKindOfClass:[NSDictionary class]]) {
239     return nil;
240   }
241   NSDictionary* tileData = aPersistantApp[kDockTileDataKey];
242   if (![tileData isKindOfClass:[NSDictionary class]]) {
243     return nil;
244   }
245   NSDictionary* fileData = tileData[kDockFileDataKey];
246   if (![fileData isKindOfClass:[NSDictionary class]]) {
247     // Some special tiles may not have DockFileData but we can ignore those.
248     return nil;
249   }
250   NSURL* url = CFBridgingRelease(_CFURLCreateFromPropertyListRepresentation(NULL, fileData));
251   if (!url) {
252     return nil;
253   }
254   return [url isFileURL] ? [url path] : nullptr;
257 // The only reliable way to get our changes to take effect seems to be to use
258 // `kill`.
259 void RefreshDock(NSDictionary* aDockPlist) {
260   [[NSUserDefaults standardUserDefaults] setPersistentDomain:aDockPlist forName:kDockDomainName];
261   NSRunningApplication* dockApp = [[NSRunningApplication
262       runningApplicationsWithBundleIdentifier:@"com.apple.dock"] firstObject];
263   if (!dockApp) {
264     return;
265   }
266   pid_t pid = [dockApp processIdentifier];
267   if (pid > 0) {
268     kill(pid, SIGTERM);
269   }
272 }  // namespace
274 nsresult nsMacDockSupport::GetIsAppInDock(bool* aIsInDock) {
275   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
277   *aIsInDock = false;
279   NSDictionary* dockPlist =
280       [[NSUserDefaults standardUserDefaults] persistentDomainForName:kDockDomainName];
281   if (!dockPlist) {
282     return NS_ERROR_FAILURE;
283   }
285   NSArray* persistentApps = GetPersistentAppsFromDockPlist(dockPlist);
286   if (!persistentApps) {
287     return NS_ERROR_FAILURE;
288   }
290   NSString* appPath = [[NSBundle mainBundle] bundlePath];
292   for (id app in persistentApps) {
293     NSString* persistentAppPath = GetPathForApp(app);
294     if (persistentAppPath && [appPath isEqual:persistentAppPath]) {
295       *aIsInDock = true;
296       break;
297     }
298   }
300   return NS_OK;
302   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
305 nsresult nsMacDockSupport::EnsureAppIsPinnedToDock(const nsAString& aAppPath,
306                                                    const nsAString& aAppToReplacePath,
307                                                    bool* aIsInDock) {
308   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
310   MOZ_ASSERT(aAppPath != aAppToReplacePath || !aAppPath.IsEmpty());
312   *aIsInDock = false;
314   NSString* appPath =
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]];
321   if (!dockPlist) {
322     return NS_ERROR_FAILURE;
323   }
325   NSMutableArray* persistentApps =
326       [NSMutableArray arrayWithArray:GetPersistentAppsFromDockPlist(dockPlist)];
327   if (!persistentApps) {
328     return NS_ERROR_FAILURE;
329   }
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;
344     } else {
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
356           // icon.
357           toReplaceAppIndex = index;
358         } else {
359           sameNameAppIndex = index;
360         }
361       } else {
362         if ([browserAppNames containsObject:persistentAppName]) {
363           lastBrowserAppIndex = index;
364         }
365       }
366     }
367   }
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);
375     }
376     *aIsInDock = true;
377     return NS_OK;
378   }
380   // Create new tile:
381   NSDictionary* newDockTile = nullptr;
382   {
383     NSURL* appUrl = [NSURL fileURLWithPath:appPath isDirectory:YES];
384     NSDictionary* dict =
385         CFBridgingRelease(_CFURLCopyPropertyListRepresentation((__bridge CFURLRef)appUrl));
386     if (!dict) {
387       return NS_ERROR_FAILURE;
388     }
389     NSDictionary* dockTileData = [NSDictionary dictionaryWithObject:dict forKey:kDockFileDataKey];
390     if (dockTileData) {
391       newDockTile = [NSDictionary dictionaryWithObject:dockTileData forKey:kDockTileDataKey];
392     }
393     if (!newDockTile) {
394       return NS_ERROR_FAILURE;
395     }
396   }
398   // Update the Dock:
399   if (toReplaceAppIndex != NSNotFound) {
400     [persistentApps replaceObjectAtIndex:toReplaceAppIndex withObject:newDockTile];
401   } else {
402     NSUInteger index;
403     if (sameNameAppIndex != NSNotFound) {
404       index = sameNameAppIndex + 1;
405     } else if (lastBrowserAppIndex != NSNotFound) {
406       index = lastBrowserAppIndex + 1;
407     } else {
408       index = [persistentApps count];
409     }
410     [persistentApps insertObject:newDockTile atIndex:index];
411   }
412   [dockPlist setObject:persistentApps forKey:kDockPersistentAppsKey];
413   RefreshDock(dockPlist);
415   *aIsInDock = true;
416   return NS_OK;
418   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);