mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-27 14:52:16 +00:00
8a0a0f7524
MOZ_RUNINIT => initialized at runtime MOZ_CONSTINIT => initialized at compile time MOZ_GLOBINIT => initialized either at runtime or compile time, depending on template parameter, macro parameter etc This annotation is only understood by our clang-tidy plugin. It has no effect on regular compilation. Differential Revision: https://phabricator.services.mozilla.com/D223341
511 lines
16 KiB
Plaintext
511 lines
16 KiB
Plaintext
/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#import <Cocoa/Cocoa.h>
|
|
#include <CoreFoundation/CoreFoundation.h>
|
|
#include <signal.h>
|
|
|
|
#include "nsCocoaUtils.h"
|
|
#include "nsComponentManagerUtils.h"
|
|
#include "nsMacDockSupport.h"
|
|
#include "nsObjCExceptions.h"
|
|
#include "nsNativeThemeColors.h"
|
|
#include "nsString.h"
|
|
#include "imgLoader.h"
|
|
#include "MOZIconHelper.h"
|
|
#include "mozilla/SVGImageContext.h"
|
|
#include "nsISVGPaintContext.h"
|
|
|
|
NS_IMPL_ISUPPORTS(nsMacDockSupport, nsIMacDockSupport, nsITaskbarProgress)
|
|
|
|
// This view is used in the dock tile when we're downloading a file.
|
|
// It draws a progress bar that looks similar to the native progress bar on
|
|
// 10.12. This style of progress bar is not animated, unlike the pre-10.10
|
|
// progress bar look which had to redrawn multiple times per second.
|
|
@interface MOZProgressDockOverlayView : NSView {
|
|
double mFractionValue;
|
|
}
|
|
@property double fractionValue;
|
|
|
|
@end
|
|
|
|
@implementation MOZProgressDockOverlayView
|
|
|
|
@synthesize fractionValue = mFractionValue;
|
|
|
|
- (void)drawRect:(NSRect)aRect {
|
|
// Erase the background behind this view, i.e. cut a rectangle hole in the
|
|
// icon.
|
|
[[NSColor clearColor] set];
|
|
NSRectFill(self.bounds);
|
|
|
|
// Split the height of this view into four quarters. The middle two quarters
|
|
// will be covered by the actual progress bar.
|
|
CGFloat radius = self.bounds.size.height / 4;
|
|
NSRect barBounds = NSInsetRect(self.bounds, 0, radius);
|
|
|
|
NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:barBounds
|
|
xRadius:radius
|
|
yRadius:radius];
|
|
|
|
// Draw a grayish background first.
|
|
[[NSColor colorWithDeviceWhite:0 alpha:0.1] setFill];
|
|
[path fill];
|
|
|
|
// Draw a fill in the control accent color for the progress part.
|
|
NSRect progressFillRect = self.bounds;
|
|
progressFillRect.size.width *= mFractionValue;
|
|
[NSGraphicsContext saveGraphicsState];
|
|
[NSBezierPath clipRect:progressFillRect];
|
|
[[NSColor controlAccentColor] setFill];
|
|
[path fill];
|
|
[NSGraphicsContext restoreGraphicsState];
|
|
|
|
// Add a shadowy stroke on top.
|
|
[NSGraphicsContext saveGraphicsState];
|
|
[path addClip];
|
|
[[NSColor colorWithDeviceWhite:0 alpha:0.2] setStroke];
|
|
path.lineWidth = barBounds.size.height / 10;
|
|
[path stroke];
|
|
[NSGraphicsContext restoreGraphicsState];
|
|
}
|
|
|
|
@end
|
|
|
|
nsMacDockSupport::nsMacDockSupport()
|
|
: mHasBadgeImage(false),
|
|
mDockTileWrapperView(nil),
|
|
mDockBadgeView(nil),
|
|
mProgressDockOverlayView(nil),
|
|
mProgressState(STATE_NO_PROGRESS),
|
|
mProgressFraction(0.0) {}
|
|
|
|
nsMacDockSupport::~nsMacDockSupport() {
|
|
if (mDockTileWrapperView) {
|
|
[mDockTileWrapperView release];
|
|
mDockTileWrapperView = nil;
|
|
}
|
|
if (mDockBadgeView) {
|
|
[mDockBadgeView release];
|
|
mDockBadgeView = nil;
|
|
}
|
|
if (mProgressDockOverlayView) {
|
|
[mProgressDockOverlayView release];
|
|
mProgressDockOverlayView = nil;
|
|
}
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
nsMacDockSupport::GetDockMenu(nsIStandaloneNativeMenu** aDockMenu) {
|
|
nsCOMPtr<nsIStandaloneNativeMenu> dockMenu(mDockMenu);
|
|
dockMenu.forget(aDockMenu);
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
nsMacDockSupport::SetDockMenu(nsIStandaloneNativeMenu* aDockMenu) {
|
|
mDockMenu = aDockMenu;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
nsMacDockSupport::ActivateApplication(bool aIgnoreOtherApplications) {
|
|
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
|
|
|
|
[[NSApplication sharedApplication]
|
|
activateIgnoringOtherApps:aIgnoreOtherApplications];
|
|
return NS_OK;
|
|
|
|
NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
nsMacDockSupport::SetBadgeText(const nsAString& aBadgeText) {
|
|
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
|
|
|
|
NSDockTile* tile = [[NSApplication sharedApplication] dockTile];
|
|
mBadgeText = aBadgeText;
|
|
if (aBadgeText.IsEmpty()) {
|
|
[tile setBadgeLabel:nil];
|
|
} else {
|
|
SetBadgeImage(nullptr, nullptr);
|
|
|
|
[tile
|
|
setBadgeLabel:[NSString
|
|
stringWithCharacters:reinterpret_cast<const unichar*>(
|
|
mBadgeText.get())
|
|
length:mBadgeText.Length()]];
|
|
}
|
|
|
|
return NS_OK;
|
|
|
|
NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
nsMacDockSupport::GetBadgeText(nsAString& aBadgeText) {
|
|
aBadgeText = mBadgeText;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
nsMacDockSupport::SetBadgeImage(imgIContainer* aImage,
|
|
nsISVGPaintContext* aPaintContext) {
|
|
if (!aImage) {
|
|
mHasBadgeImage = false;
|
|
if (mDockBadgeView) {
|
|
mDockBadgeView.image = nullptr;
|
|
}
|
|
|
|
return UpdateDockTile();
|
|
}
|
|
|
|
if (!mBadgeText.IsEmpty()) {
|
|
mBadgeText.Truncate();
|
|
NSDockTile* tile = [[NSApplication sharedApplication] dockTile];
|
|
[tile setBadgeLabel:nil];
|
|
}
|
|
|
|
NS_OBJC_BEGIN_TRY_BLOCK_RETURN
|
|
|
|
mHasBadgeImage = true;
|
|
BuildDockTile();
|
|
|
|
mozilla::SVGImageContext svgContext;
|
|
mozilla::SVGImageContext::MaybeStoreContextPaint(svgContext, aPaintContext,
|
|
aImage);
|
|
NSImage* image =
|
|
[MOZIconHelper iconImageFromImageContainer:aImage
|
|
withSize:NSMakeSize(256, 256)
|
|
svgContext:&svgContext
|
|
scaleFactor:0.0];
|
|
image.resizingMode = NSImageResizingModeStretch;
|
|
mDockBadgeView.image = image;
|
|
|
|
return UpdateDockTile();
|
|
|
|
NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
nsMacDockSupport::SetProgressState(nsTaskbarProgressState aState,
|
|
uint64_t aCurrentValue, uint64_t aMaxValue) {
|
|
NS_ENSURE_ARG_RANGE(aState, 0, STATE_PAUSED);
|
|
if (aState == STATE_NO_PROGRESS || aState == STATE_INDETERMINATE) {
|
|
NS_ENSURE_TRUE(aCurrentValue == 0, NS_ERROR_INVALID_ARG);
|
|
NS_ENSURE_TRUE(aMaxValue == 0, NS_ERROR_INVALID_ARG);
|
|
}
|
|
if (aCurrentValue > aMaxValue) {
|
|
return NS_ERROR_ILLEGAL_VALUE;
|
|
}
|
|
|
|
mProgressState = aState;
|
|
if (aMaxValue == 0) {
|
|
mProgressFraction = 0;
|
|
} else {
|
|
mProgressFraction = (double)aCurrentValue / aMaxValue;
|
|
}
|
|
|
|
return UpdateDockTile();
|
|
}
|
|
|
|
void nsMacDockSupport::BuildDockTile() {
|
|
if (!mDockTileWrapperView) {
|
|
// Create the following NSView hierarchy:
|
|
// * mDockTileWrapperView (NSView)
|
|
// * imageView (NSImageView) <- has the application icon
|
|
// * mDockBadgeView (NSImageView) <- has the dock badge
|
|
// * mProgressDockOverlayView (MOZProgressDockOverlayView) <- draws the
|
|
// progress bar
|
|
|
|
mDockTileWrapperView =
|
|
[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 32, 32)];
|
|
mDockTileWrapperView.autoresizingMask =
|
|
NSViewWidthSizable | NSViewHeightSizable;
|
|
|
|
NSImageView* imageView =
|
|
[[NSImageView alloc] initWithFrame:[mDockTileWrapperView bounds]];
|
|
imageView.image = [NSImage imageNamed:@"NSApplicationIcon"];
|
|
imageView.imageScaling = NSImageScaleAxesIndependently;
|
|
imageView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
[mDockTileWrapperView addSubview:imageView];
|
|
|
|
mDockBadgeView =
|
|
[[NSImageView alloc] initWithFrame:NSMakeRect(19.5, 19.5, 12, 12)];
|
|
mDockBadgeView.imageScaling = NSImageScaleProportionallyUpOrDown;
|
|
mDockBadgeView.autoresizingMask = NSViewMinXMargin | NSViewWidthSizable |
|
|
NSViewMaxXMargin | NSViewMinYMargin |
|
|
NSViewHeightSizable | NSViewMaxYMargin;
|
|
[mDockTileWrapperView addSubview:mDockBadgeView];
|
|
|
|
mProgressDockOverlayView = [[MOZProgressDockOverlayView alloc]
|
|
initWithFrame:NSMakeRect(1, 3, 30, 4)];
|
|
mProgressDockOverlayView.autoresizingMask =
|
|
NSViewMinXMargin | NSViewWidthSizable | NSViewMaxXMargin |
|
|
NSViewMinYMargin | NSViewHeightSizable | NSViewMaxYMargin;
|
|
[mDockTileWrapperView addSubview:mProgressDockOverlayView];
|
|
}
|
|
}
|
|
|
|
nsresult nsMacDockSupport::UpdateDockTile() {
|
|
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
|
|
|
|
if (mProgressState == STATE_NORMAL || mProgressState == STATE_INDETERMINATE ||
|
|
mHasBadgeImage) {
|
|
BuildDockTile();
|
|
|
|
if (NSApp.dockTile.contentView != mDockTileWrapperView) {
|
|
NSApp.dockTile.contentView = mDockTileWrapperView;
|
|
}
|
|
|
|
mDockBadgeView.hidden = !mHasBadgeImage;
|
|
|
|
if (mProgressState == STATE_NORMAL) {
|
|
mProgressDockOverlayView.fractionValue = mProgressFraction;
|
|
mProgressDockOverlayView.hidden = false;
|
|
} else if (mProgressState == STATE_INDETERMINATE) {
|
|
// Indeterminate states are rare. Just fill the entire progress bar in
|
|
// that case.
|
|
mProgressDockOverlayView.fractionValue = 1.0;
|
|
mProgressDockOverlayView.hidden = false;
|
|
} else {
|
|
mProgressDockOverlayView.hidden = true;
|
|
}
|
|
[NSApp.dockTile display];
|
|
} else if (NSApp.dockTile.contentView) {
|
|
NSApp.dockTile.contentView = nil;
|
|
[NSApp.dockTile display];
|
|
}
|
|
|
|
return NS_OK;
|
|
|
|
NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
|
|
}
|
|
|
|
extern "C" {
|
|
// Private CFURL API used by the Dock.
|
|
CFPropertyListRef _CFURLCopyPropertyListRepresentation(CFURLRef url);
|
|
CFURLRef _CFURLCreateFromPropertyListRepresentation(
|
|
CFAllocatorRef alloc, CFPropertyListRef pListRepresentation);
|
|
} // extern "C"
|
|
|
|
namespace {
|
|
|
|
MOZ_RUNINIT const NSArray* const browserAppNames = [NSArray
|
|
arrayWithObjects:@"Firefox.app", @"Firefox Beta.app",
|
|
@"Firefox Nightly.app", @"Safari.app", @"WebKit.app",
|
|
@"Google Chrome.app", @"Google Chrome Canary.app",
|
|
@"Chromium.app", @"Opera.app", nil];
|
|
|
|
constexpr NSString* const kDockDomainName = @"com.apple.dock";
|
|
// See https://developer.apple.com/documentation/devicemanagement/dock
|
|
constexpr NSString* const kDockPersistentAppsKey = @"persistent-apps";
|
|
// See
|
|
// https://developer.apple.com/documentation/devicemanagement/dock/staticitem
|
|
constexpr NSString* const kDockTileDataKey = @"tile-data";
|
|
constexpr NSString* const kDockFileDataKey = @"file-data";
|
|
|
|
NSArray* GetPersistentAppsFromDockPlist(NSDictionary* aDockPlist) {
|
|
if (!aDockPlist) {
|
|
return nil;
|
|
}
|
|
NSArray* persistentApps = [aDockPlist objectForKey:kDockPersistentAppsKey];
|
|
if (![persistentApps isKindOfClass:[NSArray class]]) {
|
|
return nil;
|
|
}
|
|
return persistentApps;
|
|
}
|
|
|
|
NSString* GetPathForApp(NSDictionary* aPersistantApp) {
|
|
if (![aPersistantApp isKindOfClass:[NSDictionary class]]) {
|
|
return nil;
|
|
}
|
|
NSDictionary* tileData = aPersistantApp[kDockTileDataKey];
|
|
if (![tileData isKindOfClass:[NSDictionary class]]) {
|
|
return nil;
|
|
}
|
|
NSDictionary* fileData = tileData[kDockFileDataKey];
|
|
if (![fileData isKindOfClass:[NSDictionary class]]) {
|
|
// Some special tiles may not have DockFileData but we can ignore those.
|
|
return nil;
|
|
}
|
|
NSURL* url = CFBridgingRelease(
|
|
_CFURLCreateFromPropertyListRepresentation(NULL, fileData));
|
|
if (!url) {
|
|
return nil;
|
|
}
|
|
return [url isFileURL] ? [url path] : nullptr;
|
|
}
|
|
|
|
// The only reliable way to get our changes to take effect seems to be to use
|
|
// `kill`.
|
|
void RefreshDock(NSDictionary* aDockPlist) {
|
|
[[NSUserDefaults standardUserDefaults] setPersistentDomain:aDockPlist
|
|
forName:kDockDomainName];
|
|
NSRunningApplication* dockApp = [[NSRunningApplication
|
|
runningApplicationsWithBundleIdentifier:@"com.apple.dock"] firstObject];
|
|
if (!dockApp) {
|
|
return;
|
|
}
|
|
pid_t pid = [dockApp processIdentifier];
|
|
if (pid > 0) {
|
|
kill(pid, SIGTERM);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
nsresult nsMacDockSupport::GetIsAppInDock(bool* aIsInDock) {
|
|
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
|
|
|
|
*aIsInDock = false;
|
|
|
|
NSDictionary* dockPlist = [[NSUserDefaults standardUserDefaults]
|
|
persistentDomainForName:kDockDomainName];
|
|
if (!dockPlist) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
NSArray* persistentApps = GetPersistentAppsFromDockPlist(dockPlist);
|
|
if (!persistentApps) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
NSString* appPath = [[NSBundle mainBundle] bundlePath];
|
|
|
|
for (id app in persistentApps) {
|
|
NSString* persistentAppPath = GetPathForApp(app);
|
|
if (persistentAppPath && [appPath isEqual:persistentAppPath]) {
|
|
*aIsInDock = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return NS_OK;
|
|
|
|
NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
|
|
}
|
|
|
|
nsresult nsMacDockSupport::EnsureAppIsPinnedToDock(
|
|
const nsAString& aAppPath, const nsAString& aAppToReplacePath,
|
|
bool* aIsInDock) {
|
|
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
|
|
|
|
MOZ_ASSERT(aAppPath != aAppToReplacePath || !aAppPath.IsEmpty());
|
|
|
|
*aIsInDock = false;
|
|
|
|
NSString* appPath = !aAppPath.IsEmpty() ? nsCocoaUtils::ToNSString(aAppPath)
|
|
: [[NSBundle mainBundle] bundlePath];
|
|
NSString* appToReplacePath = nsCocoaUtils::ToNSString(aAppToReplacePath);
|
|
|
|
NSMutableDictionary* dockPlist = [NSMutableDictionary
|
|
dictionaryWithDictionary:[[NSUserDefaults standardUserDefaults]
|
|
persistentDomainForName:kDockDomainName]];
|
|
if (!dockPlist) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
NSMutableArray* persistentApps =
|
|
[NSMutableArray arrayWithArray:GetPersistentAppsFromDockPlist(dockPlist)];
|
|
if (!persistentApps) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
// See the comment for this method in the .idl file for the strategy that we
|
|
// use here to determine where to pin the app.
|
|
NSUInteger preexistingAppIndex = NSNotFound; // full path matches
|
|
NSUInteger sameNameAppIndex = NSNotFound; // app name matches only
|
|
NSUInteger toReplaceAppIndex = NSNotFound;
|
|
NSUInteger lastBrowserAppIndex = NSNotFound;
|
|
for (NSUInteger index = 0; index < [persistentApps count]; ++index) {
|
|
NSString* persistentAppPath =
|
|
GetPathForApp([persistentApps objectAtIndex:index]);
|
|
|
|
if ([persistentAppPath isEqualToString:appPath]) {
|
|
preexistingAppIndex = index;
|
|
} else if (appToReplacePath &&
|
|
[persistentAppPath isEqualToString:appToReplacePath]) {
|
|
toReplaceAppIndex = index;
|
|
} else {
|
|
NSString* appName = [appPath lastPathComponent];
|
|
NSString* persistentAppName = [persistentAppPath lastPathComponent];
|
|
|
|
if ([persistentAppName isEqual:appName]) {
|
|
if ([appToReplacePath hasPrefix:@"/private/var/folders/"] &&
|
|
[appToReplacePath containsString:@"/AppTranslocation/"] &&
|
|
[persistentAppPath hasPrefix:@"/Volumes/"]) {
|
|
// This is a special case when an app with the same name was
|
|
// previously dragged and pinned from a quarantined DMG straight to
|
|
// the Dock and an attempt is now made to pin the same named app to
|
|
// the Dock. In this case we want to replace the currently pinned app
|
|
// icon.
|
|
toReplaceAppIndex = index;
|
|
} else {
|
|
sameNameAppIndex = index;
|
|
}
|
|
} else {
|
|
if ([browserAppNames containsObject:persistentAppName]) {
|
|
lastBrowserAppIndex = index;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Special cases where we're not going to add a new Dock tile:
|
|
if (preexistingAppIndex != NSNotFound) {
|
|
if (toReplaceAppIndex != NSNotFound) {
|
|
[persistentApps removeObjectAtIndex:toReplaceAppIndex];
|
|
[dockPlist setObject:persistentApps forKey:kDockPersistentAppsKey];
|
|
RefreshDock(dockPlist);
|
|
}
|
|
*aIsInDock = true;
|
|
return NS_OK;
|
|
}
|
|
|
|
// Create new tile:
|
|
NSDictionary* newDockTile = nullptr;
|
|
{
|
|
NSURL* appUrl = [NSURL fileURLWithPath:appPath isDirectory:YES];
|
|
NSDictionary* dict = CFBridgingRelease(
|
|
_CFURLCopyPropertyListRepresentation((__bridge CFURLRef)appUrl));
|
|
if (!dict) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
NSDictionary* dockTileData =
|
|
[NSDictionary dictionaryWithObject:dict forKey:kDockFileDataKey];
|
|
if (dockTileData) {
|
|
newDockTile = [NSDictionary dictionaryWithObject:dockTileData
|
|
forKey:kDockTileDataKey];
|
|
}
|
|
if (!newDockTile) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
}
|
|
|
|
// Update the Dock:
|
|
if (toReplaceAppIndex != NSNotFound) {
|
|
[persistentApps replaceObjectAtIndex:toReplaceAppIndex
|
|
withObject:newDockTile];
|
|
} else {
|
|
NSUInteger index;
|
|
if (sameNameAppIndex != NSNotFound) {
|
|
index = sameNameAppIndex + 1;
|
|
} else if (lastBrowserAppIndex != NSNotFound) {
|
|
index = lastBrowserAppIndex + 1;
|
|
} else {
|
|
index = [persistentApps count];
|
|
}
|
|
[persistentApps insertObject:newDockTile atIndex:index];
|
|
}
|
|
[dockPlist setObject:persistentApps forKey:kDockPersistentAppsKey];
|
|
RefreshDock(dockPlist);
|
|
|
|
*aIsInDock = true;
|
|
return NS_OK;
|
|
|
|
NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
|
|
}
|