init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once

View File

@@ -0,0 +1,202 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_battery_saving_mac.h"
#include "base/battery_saving.h"
#include "base/integration.h"
#include <Cocoa/Cocoa.h>
#include <IOKit/ps/IOPSKeys.h>
#include <IOKit/ps/IOPowerSources.h>
@interface LowPowerModeObserver : NSObject {
}
- (id) initWithCallback:(Fn<void()>)callback;
- (void) powerStateChanged:(NSNotification*)aNotification;
@end // @interface LowPowerModeObserver
@implementation LowPowerModeObserver {
Fn<void()> _callback;
}
- (id) initWithCallback:(Fn<void()>)callback {
if (self = [super init]) {
_callback = std::move(callback);
}
return self;
}
- (void) powerStateChanged:(NSNotification*)aNotification {
_callback();
}
@end // @implementation LowPowerModeObserver
namespace base::Platform {
namespace {
class BatterySaving final : public AbstractBatterySaving {
public:
BatterySaving(Fn<void()> changedCallback);
~BatterySaving();
std::optional<bool> enabled() const override;
private:
static void RunLoopCallback(void *callback) {
const auto observer = static_cast<LowPowerModeObserver*>(callback);
[observer powerStateChanged:nil];
}
LowPowerModeObserver *_observer = nil;
CFRunLoopSourceRef _runLoopSource = nullptr;
};
struct BatteryState {
bool has = false;
bool draining = false;
bool low = false;
};
[[nodiscard]] BatteryState DetectBatteryState() {
CFTypeRef info = IOPSCopyPowerSourcesInfo();
if (!info) {
return {};
}
const auto infoGuard = gsl::finally([&] { CFRelease(info); });
CFArrayRef list = IOPSCopyPowerSourcesList(info);
if (!list) {
return {};
}
const auto listGuard = gsl::finally([&] { CFRelease(list); });
CFIndex count = CFArrayGetCount(list);
auto result = BatteryState();
for (CFIndex i = 0; i < count; ++i) {
const auto description = CFDictionaryRef(
IOPSGetPowerSourceDescription(info, CFArrayGetValueAtIndex(list, i)));
if (!description) {
continue;
}
const auto type = CFStringRef(CFDictionaryGetValue(description, CFSTR(kIOPSTransportTypeKey)));
if (!type) {
continue;
}
const auto isInternal = (CFStringCompare(type, CFSTR(kIOPSInternalType), 0) == kCFCompareEqualTo);
if (!isInternal) {
continue;
}
const auto isPresent = CFBooleanRef(CFDictionaryGetValue(description, CFSTR(kIOPSIsPresentKey)));
if (!isPresent || !CFBooleanGetValue(isPresent)) {
continue;
}
const auto state = CFStringRef(CFDictionaryGetValue(description, CFSTR(kIOPSPowerSourceStateKey)));
if (!state) {
continue;
}
const auto isDraining = (CFStringCompare(state, CFSTR(kIOPSBatteryPowerValue), 0) == kCFCompareEqualTo);
result.has = true;
result.draining = isDraining;
const auto lowWarnLevel = CFNumberRef(CFDictionaryGetValue(description, CFSTR(kIOPSLowWarnLevelKey)));
const auto nowCapacity = CFNumberRef(CFDictionaryGetValue(description, CFSTR(kIOPSCurrentCapacityKey)));
const auto maxCapacity = CFNumberRef(CFDictionaryGetValue(description, CFSTR(kIOPSMaxCapacityKey)));
if (!lowWarnLevel || !nowCapacity || !maxCapacity) {
continue;
}
auto lowWarnLevelValue = int64_t();
auto nowCapacityValue = int64_t();
auto maxCapacityValue = int64_t();
if (!CFNumberGetValue(lowWarnLevel, kCFNumberSInt64Type, &lowWarnLevelValue)
|| !CFNumberGetValue(nowCapacity, kCFNumberSInt64Type, &nowCapacityValue)
|| !CFNumberGetValue(maxCapacity, kCFNumberSInt64Type, &maxCapacityValue)
|| (lowWarnLevelValue <= 0 || lowWarnLevelValue >= 100)
|| (nowCapacityValue < 0 || nowCapacityValue > maxCapacityValue || !maxCapacityValue)) {
continue;
}
result.low = (nowCapacityValue / double(maxCapacityValue) <= lowWarnLevelValue / 100.);
}
return result;
}
BatterySaving::BatterySaving(Fn<void()> changedCallback) {
if (!DetectBatteryState().has) {
return;
}
auto wrapped = [copy = std::move(changedCallback)] {
Integration::Instance().enterFromEventLoop(copy);
};
_observer = [[LowPowerModeObserver alloc] initWithCallback:std::move(wrapped)];
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
if (@available(macOS 12.0, *)) {
[center
addObserver: _observer
selector: @selector(powerStateChanged:)
name: NSProcessInfoPowerStateDidChangeNotification
object: nil];
}
[center
addObserver: _observer
selector: @selector(powerStateChanged:)
name: NSProcessInfoThermalStateDidChangeNotification
object: nil];
_runLoopSource = IOPSNotificationCreateRunLoopSource(
RunLoopCallback,
static_cast<void*>(_observer));
CFRunLoopAddSource(
CFRunLoopGetCurrent(),
_runLoopSource,
kCFRunLoopDefaultMode);
}
BatterySaving::~BatterySaving() {
if (_runLoopSource) {
CFRunLoopRemoveSource(
CFRunLoopGetCurrent(),
_runLoopSource,
kCFRunLoopDefaultMode);
CFRelease(_runLoopSource);
}
if (_observer) {
[_observer release];
}
}
std::optional<bool> BatterySaving::enabled() const {
if (!_observer) {
return std::nullopt;
}
NSProcessInfo *info = [NSProcessInfo processInfo];
if (@available(macOS 12.0, *)) {
if ([info isLowPowerModeEnabled]) {
return true;
}
}
const auto state = DetectBatteryState();
if (!state.has || !state.draining) {
return false;
} else if ([info thermalState] == NSProcessInfoThermalStateCritical) {
return true;
}
return state.low;
}
} // namespace
std::unique_ptr<AbstractBatterySaving> CreateBatterySaving(
Fn<void()> changedCallback) {
return std::make_unique<BatterySaving>(std::move(changedCallback));
}
} // namespace base::Platform

View File

@@ -0,0 +1,14 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
namespace Platform::ConfirmQuit {
[[nodiscard]] bool RunModal(QString text);
[[nodiscard]] QString QuitKeysString();
} // namespace Platform::ConfirmQuit

View File

@@ -0,0 +1,462 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_confirm_quit.h"
#include "base/platform/mac/base_utilities_mac.h"
// Thanks Chromium: chrome/browser/ui/cocoa/confirm_quit*
#import <Cocoa/Cocoa.h>
#import <QuartzCore/QuartzCore.h>
namespace {
// How long the user must hold down Cmd+Q to confirm the quit.
constexpr auto kShowDuration = crl::time(1500);
// Duration of the window fade out animation.
constexpr auto kWindowFadeOutDuration = crl::time(200);
// For metrics recording only: How long the user must hold the keys to
// differentitate kDoubleTap from kTapHold.
constexpr auto kDoubleTapTimeDelta = crl::time(320);
// Leeway between the |targetDate| and the current time that will confirm a
// quit.
constexpr auto kTimeDeltaFuzzFactor = crl::time(1000);
} // namespace
@class ConfirmQuitFrameView;
// The ConfirmQuitPanelController manages the black HUD window that tells users
// to "Hold Cmd+Q to Quit".
@interface ConfirmQuitPanelController : NSWindowController<NSWindowDelegate> {
@private
// The content view of the window that this controller manages.
ConfirmQuitFrameView* _contentView; // Weak, owned by the window.
NSString *_message;
}
// Returns a singleton instance of the Controller. This will create one if it
// does not currently exist.
+ (ConfirmQuitPanelController*)sharedControllerWithMessage:(NSString*)message;
// Runs a modal loop that brings up the panel and handles the logic for if and
// when to terminate. Returns YES if the quit should continue.
- (BOOL)runModalLoopForApplication:(NSApplication*)app;
// Shows the window.
- (void)showWindow:(id)sender;
// If the user did not confirm quit, send this message to give the user
// instructions on how to quit.
- (void)dismissPanel;
@end
// The content view of the window that draws a custom frame.
@interface ConfirmQuitFrameView : NSView {
@private
NSTextField* _message; // Weak, owned by the view hierarchy.
}
- (void)setMessageText:(NSString*)text;
@end
@implementation ConfirmQuitFrameView
- (instancetype)initWithFrame:(NSRect)frameRect {
if ((self = [super initWithFrame:frameRect])) {
// The frame will be fixed up when |-setMessageText:| is called.
_message = [[NSTextField alloc] initWithFrame:NSZeroRect];
[_message setEditable:NO];
[_message setSelectable:NO];
[_message setBezeled:NO];
[_message setDrawsBackground:NO];
[_message setFont:[NSFont boldSystemFontOfSize:24]];
[_message setTextColor:[NSColor whiteColor]];
[self addSubview:_message];
[_message release];
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect {
const CGFloat kCornerRadius = 9.0;
NSBezierPath* path = [NSBezierPath
bezierPathWithRoundedRect:[self bounds]
xRadius:kCornerRadius
yRadius:kCornerRadius];
NSColor* fillColor = [NSColor colorWithCalibratedWhite:0.2 alpha:0.75];
[fillColor set];
[path fill];
}
- (void)setMessageText:(NSString*)text {
const CGFloat kHorizontalPadding = 30; // In view coordinates.
// Style the string.
NSMutableAttributedString *attrString
= [[NSMutableAttributedString alloc] initWithString:text];
NSShadow *textShadow = [[NSShadow alloc] init];
const auto guard = gsl::finally([&] {
[textShadow release];
[attrString release];
});
[textShadow
setShadowColor:[NSColor
colorWithCalibratedWhite:0
alpha:0.6]];
[textShadow setShadowOffset:NSMakeSize(0, -1)];
[textShadow setShadowBlurRadius:1.0];
[attrString addAttribute:NSShadowAttributeName
value:textShadow
range:NSMakeRange(0, [text length])];
[_message setAttributedStringValue:attrString];
// Fixup the frame of the string.
[_message sizeToFit];
NSRect messageFrame = [_message frame];
NSRect frameInViewSpace
= [_message convertRect:[[self window] frame] fromView:nil];
if (NSWidth(messageFrame) > NSWidth(frameInViewSpace)) {
frameInViewSpace.size.width = NSWidth(messageFrame) + kHorizontalPadding;
}
messageFrame.origin.x = NSWidth(frameInViewSpace) / 2 - NSMidX(messageFrame);
messageFrame.origin.y = NSHeight(frameInViewSpace) / 2 - NSMidY(messageFrame);
[[self window]
setFrame:[_message convertRect:frameInViewSpace toView:nil]
display:YES];
[_message setFrame:messageFrame];
}
@end
// Animation ///////////////////////////////////////////////////////////////////
// This animation will run through all the windows of the passed-in
// NSApplication and will fade their alpha value to 0.0. When the animation is
// complete, this will release itself.
@interface FadeAllWindowsAnimation : NSAnimation<NSAnimationDelegate> {
@private
NSApplication* _application;
}
- (instancetype)initWithApplication:(NSApplication*)app
animationDuration:(NSTimeInterval)duration;
@end
@implementation FadeAllWindowsAnimation
- (instancetype)initWithApplication:(NSApplication*)app
animationDuration:(NSTimeInterval)duration {
if ((self = [super initWithDuration:duration
animationCurve:NSAnimationLinear])) {
_application = app;
[self setDelegate:self];
}
return self;
}
- (void)setCurrentProgress:(NSAnimationProgress)progress {
for (NSWindow* window in [_application windows]) {
[window setAlphaValue:1.0 - progress];
}
}
- (void)animationDidStop:(NSAnimation*)anim {
[self autorelease];
}
@end
// Private Interface ///////////////////////////////////////////////////////////
@interface ConfirmQuitPanelController (Private) <CAAnimationDelegate>
- (void)animateFadeOut;
- (NSEvent*)pumpEventQueueForKeyUp:(NSApplication*)app untilDate:(NSDate*)date;
- (void)hideAllWindowsForApplication:(NSApplication*)app
withDuration:(NSTimeInterval)duration;
- (void)sendAccessibilityAnnouncement;
@end
ConfirmQuitPanelController* g_confirmQuitPanelController = nil;
////////////////////////////////////////////////////////////////////////////////
@implementation ConfirmQuitPanelController
+ (ConfirmQuitPanelController*)sharedControllerWithMessage:(NSString*)message {
if (!g_confirmQuitPanelController) {
g_confirmQuitPanelController =
[[ConfirmQuitPanelController alloc] initWithMessage:message];
}
return [[g_confirmQuitPanelController retain] autorelease];
}
- (instancetype)initWithMessage:(NSString*)message {
const NSRect kWindowFrame = NSMakeRect(0, 0, 350, 70);
NSWindow *window
= [[NSWindow alloc]
initWithContentRect:kWindowFrame
styleMask:NSWindowStyleMaskBorderless
backing:NSBackingStoreBuffered
defer:NO];
const auto guard = gsl::finally([&] { [window release]; });
if ((self = [super initWithWindow:window])) {
[window setDelegate:self];
[window setBackgroundColor:[NSColor clearColor]];
[window setOpaque:NO];
[window setHasShadow:NO];
// Create the content view. Take the frame from the existing content view.
NSRect frame = [[window contentView] frame];
_contentView = [[ConfirmQuitFrameView alloc] initWithFrame:frame];
[window setContentView:_contentView];
// Set the proper string.
_message = [message retain];
[_contentView setMessageText:_message];
[_contentView release];
}
return self;
}
- (BOOL)runModalLoopForApplication:(NSApplication*)app {
ConfirmQuitPanelController *keepAlive = [self retain];
const auto guard = gsl::finally([&] { [keepAlive release]; });
// If this is the second of two such attempts to quit within a certain time
// interval, then just quit.
// Time of last quit attempt, if any.
static auto lastQuitAttempt = crl::time();
const auto timeNow = crl::now();
if (lastQuitAttempt && (timeNow - lastQuitAttempt) < kTimeDeltaFuzzFactor) {
// The panel tells users to Hold Cmd+Q. However, we also want to have a
// double-tap shortcut that allows for a quick quit path. For the users who
// tap Cmd+Q and then hold it with the window still open, this double-tap
// logic will run and cause the quit to get committed. If the key
// combination held down, the system will start sending the Cmd+Q event to
// the next key application, and so on. This is bad, so instead we hide all
// the windows (without animation) to look like we've "quit" and then wait
// for the KeyUp event to commit the quit.
[self hideAllWindowsForApplication:app withDuration:0];
NSEvent* nextEvent = [self
pumpEventQueueForKeyUp:app
untilDate:[NSDate distantFuture]];
[app discardEventsMatchingMask:NSEventMaskAny beforeEvent:nextEvent];
return YES;
} else {
lastQuitAttempt = timeNow;
}
// Show the info panel that explains what the user must to do confirm quit.
[self showWindow:self];
// Explicitly announce the hold-to-quit message. For an ordinary modal dialog
// VoiceOver would announce it and read its message, but VoiceOver does not do
// this for windows whose styleMask is NSWindowStyleMaskBorderless, so do it
// manually here. Without this screenreader users have no way to know why
// their quit hotkey seems not to work.
[self sendAccessibilityAnnouncement];
// Spin a nested run loop until the |targetDate| is reached or a KeyUp event
// is sent.
const auto targetDate = crl::now() + kShowDuration;
BOOL willQuit = NO;
NSEvent* nextEvent = nil;
do {
// Dequeue events until a key up is received. To avoid busy waiting, figure
// out the amount of time that the thread can sleep before taking further
// action.
NSDate* waitDate = [NSDate
dateWithTimeIntervalSinceNow:(kShowDuration - kTimeDeltaFuzzFactor) / 1000.];
nextEvent = [self pumpEventQueueForKeyUp:app untilDate:waitDate];
// Wait for the time expiry to happen. Once past the hold threshold,
// commit to quitting and hide all the open windows.
if (!willQuit) {
const auto now = crl::now();
const auto difference = (targetDate - now);
if (difference < kTimeDeltaFuzzFactor) {
willQuit = YES;
// At this point, the quit has been confirmed and windows should all
// fade out to convince the user to release the key combo to finalize
// the quit.
[self
hideAllWindowsForApplication:app
withDuration:kWindowFadeOutDuration / 1000.];
}
}
} while (!nextEvent);
// The user has released the key combo. Discard any events (i.e. the
// repeated KeyDown Cmd+Q).
[app discardEventsMatchingMask:NSEventMaskAny beforeEvent:nextEvent];
if (willQuit) {
// The user held down the combination long enough that quitting should
// happen.
return YES;
} else {
// Slowly fade the confirm window out in case the user doesn't
// understand what they have to do to quit.
[self dismissPanel];
return NO;
}
// Default case: terminate.
return YES;
}
- (void)windowWillClose:(NSNotification*)notif {
// Release all animations because CAAnimation retains its delegate (self),
// which will cause a retain cycle. Break it!
[[self window] setAnimations:@{}];
g_confirmQuitPanelController = nil;
[self autorelease];
}
- (void)showWindow:(id)sender {
// If a panel that is fading out is going to be reused here, make sure it
// does not get released when the animation finishes.
ConfirmQuitPanelController *keepAlive = [self retain];
const auto guard = gsl::finally([&] { [keepAlive release]; });
[[self window] setAnimations:@{}];
[[self window] center];
[[self window] setAlphaValue:1.0];
[super showWindow:sender];
}
- (void)dismissPanel {
[self
performSelector:@selector(animateFadeOut)
withObject:nil
afterDelay:1.0];
}
- (void)animateFadeOut {
NSWindow* window = [self window];
CAAnimation *animation
= [[window animationForKey:@"alphaValue"] copy];
const auto guard = gsl::finally([&] { [animation release]; });
[animation setDelegate:self];
[animation setDuration:0.2];
NSMutableDictionary* dictionary
= [NSMutableDictionary dictionaryWithDictionary:[window animations]];
dictionary[@"alphaValue"] = animation;
[window setAnimations:dictionary];
[[window animator] setAlphaValue:0.0];
}
- (void)animationDidStart:(CAAnimation*)theAnimation {
// CAAnimationDelegate method added on OSX 10.12.
}
- (void)animationDidStop:(CAAnimation*)theAnimation finished:(BOOL)finished {
[self close];
}
// Runs a nested loop that pumps the event queue until the next KeyUp event.
- (NSEvent*)pumpEventQueueForKeyUp:(NSApplication*)app untilDate:(NSDate*)date {
return [app
nextEventMatchingMask:NSEventMaskKeyUp
untilDate:date
inMode:NSEventTrackingRunLoopMode
dequeue:YES];
}
// Iterates through the list of open windows and hides them all.
- (void)hideAllWindowsForApplication:(NSApplication*)app
withDuration:(NSTimeInterval)duration {
FadeAllWindowsAnimation* animation =
[[FadeAllWindowsAnimation alloc] initWithApplication:app
animationDuration:duration];
// Releases itself when the animation stops.
[animation startAnimation];
}
- (void)sendAccessibilityAnnouncement {
NSAccessibilityPostNotificationWithUserInfo(
[NSApp mainWindow], NSAccessibilityAnnouncementRequestedNotification, @{
NSAccessibilityAnnouncementKey : _message,
NSAccessibilityPriorityKey : @(NSAccessibilityPriorityHigh),
});
}
@end
namespace Platform::ConfirmQuit {
namespace {
// This returns the NSMenuItem that quits the application.
[[nodiscard]] NSMenuItem *QuitMenuItem() {
NSMenu* mainMenu = [NSApp mainMenu];
// Get the application menu (i.e. Chromium).
NSMenu* appMenu = [[mainMenu itemAtIndex:0] submenu];
for (NSMenuItem* item in [appMenu itemArray]) {
// Find the Quit item.
if ([item action] == @selector(terminate:)) {
return item;
}
}
// Default to Cmd+Q.
NSMenuItem* item = [[[NSMenuItem alloc]
initWithTitle:@""
action:@selector(terminate:)
keyEquivalent:@"q"] autorelease];
item.keyEquivalentModifierMask = NSEventModifierFlagCommand;
return item;
}
[[nodiscard]] QString KeyCombinationForMenuItem(NSMenuItem *item) {
auto result = QString();
NSUInteger modifiers = item.keyEquivalentModifierMask;
if (modifiers & NSEventModifierFlagCommand) {
result.append(QChar(0x2318));
}
if (modifiers & NSEventModifierFlagControl) {
result.append(QChar(0x2303));
}
if (modifiers & NSEventModifierFlagOption) {
result.append(QChar(0x2325));
}
if (modifiers & NSEventModifierFlagShift) {
result.append(QChar(0x21E7));
}
result.append(NS2QString([item.keyEquivalent uppercaseString]));
return result;
}
// This looks at the Main Menu and determines what the user has set as the
// key combination for quit. It then gets the modifiers and builds a string
// to display them.
[[nodiscard]] QString KeyCommandString() {
return KeyCombinationForMenuItem(QuitMenuItem());
}
} // namespace
bool RunModal(QString text) {
return [[ConfirmQuitPanelController sharedControllerWithMessage:Q2NSString(text)]
runModalLoopForApplication:NSApp];
}
QString QuitKeysString() {
return KeyCommandString();
}
} // namespace Platform::ConfirmQuit

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_custom_app_icon.h"

View File

@@ -0,0 +1,387 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_custom_app_icon_mac.h"
#include "base/debug_log.h"
#include "base/platform/mac/base_utilities_mac.h"
#include <QtGui/QImage>
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QTemporaryFile>
#include <sys/xattr.h>
#include <xxhash.h>
namespace base::Platform {
namespace {
using namespace ::Platform;
constexpr auto kFinderInfo = "com.apple.FinderInfo";
constexpr auto kResourceFork = "com.apple.ResourceFork";
// We want to write [8], so just in case.
constexpr auto kFinderInfoMinSize = 16;
// Usually.
constexpr auto kFinderInfoSize = 32;
// Just in case.
constexpr auto kFinderInfoMaxSize = 256;
// Limit custom icons to 10 MB.
constexpr auto kResourceForkMaxSize = 10 * 1024 * 1024;
[[nodiscard]] QString BundlePath() {
@autoreleasepool {
NSString *path = @"";
@try {
path = [[NSBundle mainBundle] bundlePath];
if (!path) {
Unexpected("Could not get bundled path!");
}
return QFile::decodeName([path fileSystemRepresentation]);
}
@catch (NSException *exception) {
Unexpected("Exception in resource registering.");
}
}
}
int Launch(const QString &command, const QStringList &arguments) {
@autoreleasepool {
@try {
NSMutableArray *list = [NSMutableArray arrayWithCapacity:arguments.size()];
for (const auto &argument : arguments) {
[list addObject:Q2NSString(argument)];
}
NSTask *task = [[NSTask alloc] init];
task.launchPath = Q2NSString(command);
task.arguments = list;
[task launch];
[task waitUntilExit];
return [task terminationStatus];
}
@catch (NSException *exception) {
return -888;
}
@finally {
}
}
}
[[nodiscard]] std::optional<std::string> ReadCustomIconAttribute(const QString &bundle) {
const auto native = QFile::encodeName(bundle);
auto info = std::array<char, kFinderInfoMaxSize>();
const auto result = getxattr(
native.data(),
kFinderInfo,
info.data(),
info.size(),
0, // position
XATTR_NOFOLLOW);
const auto error = (result < 0) ? errno : 0;
if (result < 0) {
if (error == ENOATTR) {
return std::string();
} else {
LOG(("Icon Error: Could not get %1 xattr, error: %2."
).arg(kFinderInfo
).arg(error));
return std::nullopt;
}
} else if (result < kFinderInfoMinSize) {
LOG(("Icon Error: Bad existing %1 xattr size: %2."
).arg(kFinderInfo
).arg(error));
return std::nullopt;
}
return std::string(info.data(), result);
}
[[nodiscard]] bool WriteCustomIconAttribute(
const QString &bundle,
const std::string &value) {
const auto native = QFile::encodeName(bundle);
const auto result = setxattr(
native.data(),
kFinderInfo,
value.data(),
value.size(),
0, // position
XATTR_NOFOLLOW);
if (result != 0) {
LOG(("Icon Error: Could not set %1 xattr, error: %2."
).arg(kFinderInfo
).arg(errno));
return false;
}
return true;
}
[[nodiscard]] bool DeleteCustomIconAttribute(const QString &bundle) {
const auto native = QFile::encodeName(bundle);
const auto result = removexattr(
native.data(),
kFinderInfo,
XATTR_NOFOLLOW);
if (result != 0) {
LOG(("Icon Error: Could not remove %1 xattr, error: %2."
).arg(kFinderInfo
).arg(errno));
return false;
}
return true;
}
[[nodiscard]] bool EnableCustomIcon(const QString &bundle) {
auto info = ReadCustomIconAttribute(bundle);
if (!info.has_value()) {
return false;
} else if (info->empty()) {
*info = std::string(kFinderInfoSize, char(0));
}
if ((*info)[8] & 0x04) {
(*info)[8] &= ~0x04;
if (!WriteCustomIconAttribute(bundle, *info)) {
return false;
}
}
(*info)[8] |= 4;
return WriteCustomIconAttribute(bundle, *info);
}
[[nodiscard]] bool DisableCustomIcon(const QString &bundle) {
auto info = ReadCustomIconAttribute(bundle);
if (!info.has_value()) {
return false;
} else if (info->empty()) {
return true;
}
return DeleteCustomIconAttribute(bundle);
}
[[nodiscard]] bool RefreshDock() {
Launch("/bin/bash", { "-c", "rm /var/folders/*/*/*/com.apple.dock.iconcache" });
const auto killall = Launch("/usr/bin/killall", { "Dock" });
if (killall != 0) {
LOG(("Icon Error: Failed to run `killall Dock`, result: %1.").arg(killall));
return false;
}
return true;
}
[[nodiscard]] QString TempPath(const QString &extension) {
auto file = QTemporaryFile(
QDir::tempPath() + "/custom_icon_XXXXXX." + extension);
file.setAutoRemove(false);
const auto result = file.open() ? file.fileName() : QString();
if (result.isEmpty()) {
LOG(("Icon Error: Could not obtain a temporary file name."));
}
return result;
}
[[nodiscard]] std::optional<std::string> ReadResourceFork(
const QString &path) {
const auto native = QFile::encodeName(path);
auto buffer = std::string(kResourceForkMaxSize + 1, char(0));
const auto result = getxattr(
native.data(),
kResourceFork,
buffer.data(),
buffer.size(),
0, // position
XATTR_NOFOLLOW);
const auto error = (result < 0) ? errno : 0;
if (result < 0) {
if (error == ENOATTR) {
return std::string();
} else {
LOG(("Icon Error: Could not get %1 xattr, error: %2."
).arg(kResourceFork
).arg(error));
return std::nullopt;
}
} else if (result > kResourceForkMaxSize) {
LOG(("Icon Error: Got too large %1 xattr, size: %2."
).arg(kResourceFork
).arg(result));
return std::nullopt;
}
buffer.resize(result);
return buffer;
}
[[nodiscard]] bool WriteResourceFork(
const QString &path,
const std::string &data) {
const auto native = QFile::encodeName(path);
const auto result = setxattr(
native.data(),
kResourceFork,
data.data(),
data.size(),
0, // position
XATTR_NOFOLLOW);
if (result != 0) {
LOG(("Icon Error: Could not set %1 xattr, error: %2."
).arg(kResourceFork
).arg(errno));
return false;
}
return true;
}
[[nodiscard]] uint64 Digest(const std::string &data) {
return XXH64(data.data(), data.size(), 0);
}
[[nodiscard]] std::optional<uint64> SetPreparedIcon(const QString &path) {
const auto sips = Launch("/usr/bin/sips", {
"-i",
path
});
if (sips != 0) {
LOG(("Icon Error: Failed to run `sips -i \"%1\"`, result: %2."
).arg(path
).arg(sips));
return std::nullopt;
}
const auto bundle = BundlePath();
const auto icon = bundle + "/Icon\r";
const auto touch = Launch("/usr/bin/touch", { icon });
if (touch != 0) {
LOG(("Icon Error: Failed to run `touch \"%1\"`, result: %2."
).arg(icon
).arg(touch));
return std::nullopt;
}
#if 0 // Faster, but without a digest.
const auto from = path + "/..namedfork/rsrc";
const auto to = icon + "/..namedfork/rsrc";
const auto cp = Launch("/bin/cp", { from, to });
if (cp != 0) {
LOG(("Icon Error: Failed to run `cp \"%1\" \"%2\"`, result: %3."
).arg(from
).arg(to
).arg(cp));
return false;
}
#endif
auto rsrc = ReadResourceFork(path);
if (!rsrc) {
return false;
} else if (rsrc->empty()) {
LOG(("Icon Error: Empty resource fork after sips in \"%1\".").arg(path));
return false;
} else if (!WriteResourceFork(icon, *rsrc) || !EnableCustomIcon(bundle)) {
return std::nullopt;
}
return RefreshDock()
? std::make_optional(Digest(*rsrc))
: std::nullopt;
}
} // namespace
std::optional<uint64> SetCustomAppIcon(QImage image) {
if (image.isNull()) {
LOG(("Icon Error: Null image received."));
return std::nullopt;
}
if (image.format() != QImage::Format_ARGB32_Premultiplied
&& image.format() != QImage::Format_ARGB32
&& image.format() != QImage::Format_RGB32) {
image = std::move(image).convertToFormat(QImage::Format_ARGB32);
if (image.isNull()) {
LOG(("Icon Error: Failed to convert image to ARGB32."));
return std::nullopt;
}
}
const auto temp = TempPath("icns");
if (temp.isEmpty()) {
return std::nullopt;
}
const auto guard = gsl::finally([&] { QFile::remove(temp); });
if (!image.save(temp, "PNG")) {
LOG(("Icon Error: Failed to save image to \"%1\".").arg(temp));
return std::nullopt;
}
return SetPreparedIcon(temp);
}
std::optional<uint64> SetCustomAppIcon(const QString &path) {
const auto icns = path.endsWith(".icns", Qt::CaseInsensitive);
if (!icns) {
auto image = QImage(path);
if (image.isNull()) {
LOG(("Icon Error: Failed to read image from \"%1\".").arg(path));
return std::nullopt;
}
return SetCustomAppIcon(std::move(image));
}
const auto temp = TempPath("icns");
if (temp.isEmpty()) {
return std::nullopt;
}
const auto guard = gsl::finally([&] { QFile::remove(temp); });
QFile::remove(temp);
if (!QFile(path).copy(temp)) {
LOG(("Icon Error: Failed to copy icon from \"%1\" to \"%2\"."
).arg(path
).arg(temp));
return std::nullopt;
}
return SetPreparedIcon(temp);
}
std::optional<uint64> CurrentCustomAppIconDigest() {
const auto bundle = BundlePath();
const auto icon = bundle + "/Icon\r";
const auto attr = ReadCustomIconAttribute(bundle);
if (!attr) {
return std::nullopt;
} else if (attr->empty()) {
return 0;
}
const auto value = ReadResourceFork(icon);
if (!value) {
return std::nullopt;
} else if (value->empty()) {
return 0;
}
return Digest(*value);
}
bool ClearCustomAppIcon() {
const auto bundle = BundlePath();
const auto icon = bundle + "/Icon\r";
Launch("/bin/rm", { icon });
auto info = ReadCustomIconAttribute(bundle);
if (!info.has_value()) {
return false;
} else if (info->empty()) {
return true;
} else if (!DeleteCustomIconAttribute(bundle)) {
return false;
}
return RefreshDock();
}
} // namespace base::Platform

View File

@@ -0,0 +1,7 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once

View File

@@ -0,0 +1,93 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_file_utilities_mac.h"
#include "base/platform/mac/base_utilities_mac.h"
#include <QtCore/QFileInfo>
#include <sys/xattr.h>
#include <stdio.h>
#include <unistd.h>
namespace base::Platform {
using namespace ::Platform;
void ShowInFolder(const QString &filepath) {
const auto folder = QFileInfo(filepath).absolutePath();
@autoreleasepool {
[[NSWorkspace sharedWorkspace] selectFile:Q2NSString(filepath) inFileViewerRootedAtPath:Q2NSString(folder)];
}
}
void RemoveQuarantine(const QString &path) {
const auto kQuarantineAttribute = "com.apple.quarantine";
const auto local = QFile::encodeName(path);
removexattr(local.data(), kQuarantineAttribute, 0);
}
QString BundledResourcesPath() {
@autoreleasepool {
NSString *path = @"";
@try {
path = [[NSBundle mainBundle] bundlePath];
if (!path) {
Unexpected("Could not get bundled path!");
}
path = [path stringByAppendingString:@"/Contents/Resources"];
return QFile::decodeName([path fileSystemRepresentation]);
}
@catch (NSException *exception) {
Unexpected("Exception in resource registering.");
}
}
}
QString FileNameFromUserString(QString name) {
return name;
}
bool DeleteDirectory(QString path) {
if (path.endsWith('/')) {
path.chop(1);
}
BOOL result = NO;
@autoreleasepool {
result = [[NSFileManager defaultManager] removeItemAtPath:Q2NSString(path) error:nil];
}
return (result != NO);
}
QString CurrentExecutablePath(int argc, char *argv[]) {
return NS2QString([[NSBundle mainBundle] bundlePath]);
}
bool RenameWithOverwrite(const QString &from, const QString &to) {
const auto fromPath = QFile::encodeName(from);
const auto toPath = QFile::encodeName(to);
return (rename(fromPath.constData(), toPath.constData()) == 0);
}
void FlushFileData(QFile &file) {
file.flush();
if (const auto descriptor = file.handle()) {
fsync(descriptor);
}
}
} // namespace base::Platform

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_global_shortcuts.h"

View File

@@ -0,0 +1,345 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_global_shortcuts_mac.h"
#include "base/platform/mac/base_utilities_mac.h"
#include "base/invoke_queued.h"
#include "base/const_string.h"
#include <QtGui/QtEvents>
#include <Carbon/Carbon.h>
#import <Foundation/Foundation.h>
#import <IOKit/hidsystem/IOHIDLib.h>
namespace base::Platform::GlobalShortcuts {
namespace {
constexpr auto kShiftMouseButton = std::numeric_limits<uint64>::max() - 100;
CFMachPortRef EventPort = nullptr;
CFRunLoopSourceRef EventPortSource = nullptr;
CFRunLoopRef ThreadRunLoop = nullptr;
std::thread Thread;
Fn<void(GlobalShortcutKeyGeneric descriptor, bool down)> ProcessCallback;
struct EventData {
GlobalShortcutKeyGeneric descriptor = 0;
bool down = false;
};
using MaybeEventData = std::optional<EventData>;
MaybeEventData ProcessKeyEvent(CGEventType type, CGEventRef event) {
if (CGEventGetIntegerValueField(event, kCGKeyboardEventAutorepeat)) {
return std::nullopt;
}
const auto keycode = CGEventGetIntegerValueField(
event,
kCGKeyboardEventKeycode);
if (keycode == 0xB3) {
// Some KeyDown+KeyUp sent when quickly pressing and releasing Fn.
return std::nullopt;
}
const auto flags = CGEventGetFlags(event);
const auto maybeDown = [&]() -> std::optional<bool> {
if (type == kCGEventKeyDown) {
return true;
} else if (type == kCGEventKeyUp) {
return false;
} else if (type != kCGEventFlagsChanged) {
return std::nullopt;
}
switch (keycode) {
case kVK_CapsLock:
return (flags & kCGEventFlagMaskAlphaShift) != 0;
case kVK_Shift:
case kVK_RightShift:
return (flags & kCGEventFlagMaskShift) != 0;
case kVK_Control:
case kVK_RightControl:
return (flags & kCGEventFlagMaskControl) != 0;
case kVK_Option:
case kVK_RightOption:
return (flags & kCGEventFlagMaskAlternate) != 0;
case kVK_Command:
case kVK_RightCommand:
return (flags & kCGEventFlagMaskCommand) != 0;
case kVK_Function:
return (flags & kCGEventFlagMaskSecondaryFn) != 0;
default:
return std::nullopt;
}
}();
if (!maybeDown) {
return std::nullopt;
}
const auto descriptor = GlobalShortcutKeyGeneric(keycode);
const auto down = *maybeDown;
return EventData{ descriptor, down };
}
MaybeEventData ProcessMouseEvent(CGEventType type, CGEventRef event) {
const auto button = CGEventGetIntegerValueField(
event,
kCGMouseEventButtonNumber);
if (!button) {
return std::nullopt;
}
const auto code = GlobalShortcutKeyGeneric(kShiftMouseButton
+ button
// Increase the value by 1, because the right button = 1.
+ 1);
const auto down = (type == kCGEventOtherMouseDown)
|| (type == kCGEventRightMouseDown);
return EventData{ code, down };
}
CGEventRef EventTapCallback(
CGEventTapProxy,
CGEventType type,
CGEventRef event,
void*) {
const auto isKey = (type == kCGEventKeyDown)
|| (type == kCGEventKeyUp)
|| (type == kCGEventFlagsChanged);
const auto maybeData = isKey
? ProcessKeyEvent(type, event)
: ProcessMouseEvent(type, event);
if (maybeData) {
ProcessCallback(maybeData->descriptor, maybeData->down);
}
return event;
}
} // namespace
bool Available() {
return true;
}
bool Allowed() {
if (@available(macOS 10.15, *)) {
// Input Monitoring is required on macOS 10.15 an later.
// Even if user grants access, restart is required.
static const auto result = IOHIDCheckAccess(
kIOHIDRequestTypeListenEvent);
return (result == kIOHIDAccessTypeGranted);
} else if (@available(macOS 10.14, *)) {
// Accessibility is required on macOS 10.14.
NSDictionary *const options=
@{(__bridge NSString *)kAXTrustedCheckOptionPrompt: @FALSE};
return AXIsProcessTrustedWithOptions(
(__bridge CFDictionaryRef)options);
}
return true;
}
void Start(Fn<void(GlobalShortcutKeyGeneric descriptor, bool down)> process) {
Expects(!EventPort);
Expects(!EventPortSource);
ProcessCallback = std::move(process);
EventPort = CGEventTapCreate(
kCGHIDEventTap,
kCGHeadInsertEventTap,
kCGEventTapOptionListenOnly,
(CGEventMaskBit(kCGEventKeyDown)
| CGEventMaskBit(kCGEventKeyUp)
| CGEventMaskBit(kCGEventOtherMouseDown)
| CGEventMaskBit(kCGEventOtherMouseUp)
| CGEventMaskBit(kCGEventRightMouseDown)
| CGEventMaskBit(kCGEventRightMouseUp)
| CGEventMaskBit(kCGEventFlagsChanged)),
EventTapCallback,
nullptr);
if (!EventPort) {
ProcessCallback = nullptr;
return;
}
EventPortSource = CFMachPortCreateRunLoopSource(
kCFAllocatorDefault,
EventPort,
0);
if (!EventPortSource) {
CFMachPortInvalidate(EventPort);
CFRelease(EventPort);
EventPort = nullptr;
ProcessCallback = nullptr;
return;
}
Thread = std::thread([] {
ThreadRunLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource(
ThreadRunLoop,
EventPortSource,
kCFRunLoopCommonModes);
CGEventTapEnable(EventPort, true);
CFRunLoopRun();
});
}
void Stop() {
if (!EventPort) {
return;
}
CFRunLoopStop(ThreadRunLoop);
Thread.join();
CFMachPortInvalidate(EventPort);
CFRelease(EventPort);
EventPort = nullptr;
CFRelease(EventPortSource);
EventPortSource = nullptr;
ProcessCallback = nullptr;
}
QString KeyName(GlobalShortcutKeyGeneric descriptor) {
static const auto KeyToString = flat_map<uint64, const_string>{
{ kVK_Return, "\xE2\x8F\x8E" },
{ kVK_Tab, "\xE2\x87\xA5" },
{ kVK_Space, "\xE2\x90\xA3" },
{ kVK_Delete, "\xE2\x8C\xAB" },
{ kVK_Escape, "\xE2\x8E\x8B" },
{ kVK_Command, "\xE2\x8C\x98" },
{ kVK_Shift, "\xE2\x87\xA7" },
{ kVK_CapsLock, "Caps Lock" },
{ kVK_Option, "\xE2\x8C\xA5" },
{ kVK_Control, "\xE2\x8C\x83" },
{ kVK_RightCommand, "Right \xE2\x8C\x98" },
{ kVK_RightShift, "Right \xE2\x87\xA7" },
{ kVK_RightOption, "Right \xE2\x8C\xA5" },
{ kVK_RightControl, "Right \xE2\x8C\x83" },
{ kVK_Function, "Fn" },
{ kVK_F17, "F17" },
{ kVK_VolumeUp, "Volume Up" },
{ kVK_VolumeDown, "Volume Down" },
{ kVK_Mute, "Mute" },
{ kVK_F18, "F18" },
{ kVK_F19, "F19" },
{ kVK_F20, "F20" },
{ kVK_F5, "F5" },
{ kVK_F6, "F6" },
{ kVK_F7, "F7" },
{ kVK_F3, "F3" },
{ kVK_F8, "F8" },
{ kVK_F9, "F9" },
{ kVK_F11, "F11" },
{ kVK_F13, "F13" },
{ kVK_F16, "F16" },
{ kVK_F14, "F14" },
{ kVK_F10, "F10" },
{ kVK_F12, "F12" },
{ kVK_F15, "F15" },
{ kVK_Help, "Help" },
{ kVK_Home, "\xE2\x86\x96" },
{ kVK_PageUp, "Page Up" },
{ kVK_ForwardDelete, "\xe2\x8c\xa6" },
{ kVK_F4, "F4" },
{ kVK_End, "\xE2\x86\x98" },
{ kVK_F2, "F2" },
{ kVK_PageDown, "Page Down" },
{ kVK_F1, "F1" },
{ kVK_LeftArrow, "\xE2\x86\x90" },
{ kVK_RightArrow, "\xE2\x86\x92" },
{ kVK_DownArrow, "\xE2\x86\x93" },
{ kVK_UpArrow, "\xE2\x86\x91" },
{ kVK_ANSI_A, "A" },
{ kVK_ANSI_S, "S" },
{ kVK_ANSI_D, "D" },
{ kVK_ANSI_F, "F" },
{ kVK_ANSI_H, "H" },
{ kVK_ANSI_G, "G" },
{ kVK_ANSI_Z, "Z" },
{ kVK_ANSI_X, "X" },
{ kVK_ANSI_C, "C" },
{ kVK_ANSI_V, "V" },
{ kVK_ANSI_B, "B" },
{ kVK_ANSI_Q, "Q" },
{ kVK_ANSI_W, "W" },
{ kVK_ANSI_E, "E" },
{ kVK_ANSI_R, "R" },
{ kVK_ANSI_Y, "Y" },
{ kVK_ANSI_T, "T" },
{ kVK_ANSI_1, "1" },
{ kVK_ANSI_2, "2" },
{ kVK_ANSI_3, "3" },
{ kVK_ANSI_4, "4" },
{ kVK_ANSI_6, "6" },
{ kVK_ANSI_5, "5" },
{ kVK_ANSI_Equal, "=" },
{ kVK_ANSI_9, "9" },
{ kVK_ANSI_7, "7" },
{ kVK_ANSI_Minus, "-" },
{ kVK_ANSI_8, "8" },
{ kVK_ANSI_0, "0" },
{ kVK_ANSI_RightBracket, "]" },
{ kVK_ANSI_O, "O" },
{ kVK_ANSI_U, "U" },
{ kVK_ANSI_LeftBracket, "[" },
{ kVK_ANSI_I, "I" },
{ kVK_ANSI_P, "P" },
{ kVK_ANSI_L, "L" },
{ kVK_ANSI_J, "J" },
{ kVK_ANSI_Quote, "'" },
{ kVK_ANSI_K, "K" },
{ kVK_ANSI_Semicolon, "/" },
{ kVK_ANSI_Backslash, "\\" },
{ kVK_ANSI_Comma, "," },
{ kVK_ANSI_Slash, "/" },
{ kVK_ANSI_N, "N" },
{ kVK_ANSI_M, "M" },
{ kVK_ANSI_Period, "." },
{ kVK_ANSI_Grave, "`" },
{ kVK_ANSI_KeypadDecimal, "Num ." },
{ kVK_ANSI_KeypadMultiply, "Num *" },
{ kVK_ANSI_KeypadPlus, "Num +" },
{ kVK_ANSI_KeypadClear, "Num Clear" },
{ kVK_ANSI_KeypadDivide, "Num /" },
{ kVK_ANSI_KeypadEnter, "Num Enter" },
{ kVK_ANSI_KeypadMinus, "Num -" },
{ kVK_ANSI_KeypadEquals, "Num =" },
{ kVK_ANSI_Keypad0, "Num 0" },
{ kVK_ANSI_Keypad1, "Num 1" },
{ kVK_ANSI_Keypad2, "Num 2" },
{ kVK_ANSI_Keypad3, "Num 3" },
{ kVK_ANSI_Keypad4, "Num 4" },
{ kVK_ANSI_Keypad5, "Num 5" },
{ kVK_ANSI_Keypad6, "Num 6" },
{ kVK_ANSI_Keypad7, "Num 7" },
{ kVK_ANSI_Keypad8, "Num 8" },
{ kVK_ANSI_Keypad9, "Num 9" },
};
if (descriptor > kShiftMouseButton) {
return QString("Mouse %1").arg(descriptor - kShiftMouseButton);
}
const auto i = KeyToString.find(descriptor);
return (i != end(KeyToString))
? i->second.utf16()
: QString("\\x%1").arg(descriptor, 0, 16);
}
bool IsToggleFullScreenKey(not_null<QKeyEvent*> e) {
const auto mods = e->nativeModifiers()
& NSEventModifierFlagDeviceIndependentFlagsMask;
return (mods == NSEventModifierFlagFunction) // Fn
&& (e->nativeVirtualKey() == 3); // F
}
} // namespace base::Platform::GlobalShortcuts

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_haptic.h"

View File

@@ -0,0 +1,33 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_haptic_mac.h"
#include "base/integration.h"
#include <Cocoa/Cocoa.h>
namespace base::Platform {
void Haptic() {
Integration::Instance().enterFromEventLoop([=] {
[[NSHapticFeedbackManager defaultPerformer]
performFeedbackPattern:NSHapticFeedbackPatternGeneric
performanceTime:NSHapticFeedbackPerformanceTimeDrawCompleted];
});
}
bool IsSwipeBackEnabled() {
static const auto cached = [] {
const auto defaults = [NSUserDefaults standardUserDefaults];
NSNumber *setting = [defaults
objectForKey:@"AppleEnableSwipeNavigateWithScrolls"];
return setting ? [setting boolValue] : true;
}();
return cached;
}
} // namespace base::Platform

View File

@@ -0,0 +1,51 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_info.h"
namespace Platform {
inline OutdateReason WhySystemBecomesOutdated() {
return OutdateReason::IsOld;
}
inline constexpr bool IsMac() {
return true;
}
inline constexpr bool IsMacStoreBuild() {
#ifdef OS_MAC_STORE
return true;
#else // OS_MAC_STORE
return false;
#endif // OS_MAC_STORE
}
inline constexpr bool IsWindows() { return false; }
inline constexpr bool IsWindows32Bit() { return false; }
inline constexpr bool IsWindows64Bit() { return false; }
inline constexpr bool IsWindowsARM64() { return false; }
inline constexpr bool IsWindowsStoreBuild() { return false; }
inline bool IsWindows7OrGreater() { return false; }
inline bool IsWindows8OrGreater() { return false; }
inline bool IsWindows8Point1OrGreater() { return false; }
inline bool IsWindows10OrGreater() { return false; }
inline bool IsWindows11OrGreater() { return false; }
inline constexpr bool IsLinux() { return false; }
inline bool IsX11() { return false; }
inline bool IsWayland() { return false; }
inline bool IsXwayland() { return false; }
inline QString GetLibcName() { return QString(); }
inline QString GetLibcVersion() { return QString(); }
inline QString GetWindowManager() { return QString(); }
void OpenInputMonitoringPrivacySettings();
void OpenDesktopCapturePrivacySettings();
void OpenAccessibilityPrivacySettings();
} // namespace Platform

View File

@@ -0,0 +1,292 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_info_mac.h"
#include "base/timer.h"
#include "base/algorithm.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/mac/base_utilities_mac.h"
#include <QtCore/QDate>
#include <QtCore/QProcess>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonValue>
#include <QtCore/QOperatingSystemVersion>
#include <sys/sysctl.h>
#include <Cocoa/Cocoa.h>
#import <IOKit/hidsystem/IOHIDLib.h>
@interface WakeUpObserver : NSObject {
}
- (void) receiveWakeNote:(NSNotification*)note;
@end // @interface WakeUpObserver
@implementation WakeUpObserver {
}
- (void) receiveWakeNote:(NSNotification*)aNotification {
base::CheckLocalTime();
}
@end // @implementation WakeUpObserver
namespace Platform {
namespace {
WakeUpObserver *GlobalWakeUpObserver = nil;
QString FromIdentifier(const QString &model) {
if (model.isEmpty() || model.toLower().indexOf("mac") < 0) {
return QString();
}
QStringList words;
QString word;
for (const QChar &ch : model) {
if (!ch.isLetter()) {
continue;
}
if (ch.isUpper()) {
if (!word.isEmpty()) {
words.push_back(word);
word = QString();
}
}
word.append(ch);
}
if (!word.isEmpty()) {
words.push_back(word);
}
QString result;
for (const QString &word : words) {
if (!result.isEmpty()
&& word != "Mac"
&& word != "Book") {
result.append(' ');
}
result.append(word);
}
return result;
}
[[nodiscard]] int MajorVersion() {
static const auto current = QOperatingSystemVersion::current();
return current.majorVersion();
}
[[nodiscard]] int MinorVersion() {
static const auto current = QOperatingSystemVersion::current();
return current.minorVersion();
}
[[nodiscard]] int PatchVersion() {
static const auto current = QOperatingSystemVersion::current();
return current.microVersion();
}
template <int Major, int Minor>
bool IsMacThatOrGreater() {
static const auto result = (MajorVersion() >= Major)
&& ((MajorVersion() > Major) || (MinorVersion() >= Minor));
return result;
}
template <int Minor>
[[nodiscard]] bool IsMac10ThatOrGreater() {
return IsMacThatOrGreater<10, Minor>();
}
[[nodiscard]] NSURL *PrivacySettingsUrl(const QString &section) {
NSString *url = Q2NSString(
"x-apple.systempreferences:com.apple.preference.security?" + section
);
return [NSURL URLWithString:url];
}
[[nodiscard]] bool RunningThroughRosetta() {
auto result = int(0);
auto size = sizeof(result);
sysctlbyname("sysctl.proc_translated", &result, &size, nullptr, 0);
return (result == 1);
}
[[nodiscard]] QString DeviceFromSystemProfiler() {
// Starting with MacBook M2 the hw.model returns simply Mac[digits],[digits].
// So we try reading "system_profiler" output.
auto process = QProcess();
process.start(
"system_profiler",
{ "-json", "SPHardwareDataType", "-detailLevel", "mini" });
process.waitForFinished();
auto error = QJsonParseError{ 0, QJsonParseError::NoError };
const auto document = QJsonDocument::fromJson(process.readAll(), &error);
if (error.error != QJsonParseError::NoError || !document.isObject()) {
return {};
}
const auto fields = document.object()["SPHardwareDataType"].toArray()[0].toObject();
const auto result = fields["machine_name"].toString();
if (result.isEmpty()) {
return {};
}
const auto chip = fields["chip_type"].toString();
return chip.startsWith("Apple ") ? (result + chip.mid(5)) : result;
}
} // namespace
QString DeviceModelPretty() {
using namespace base::Platform;
static const auto result = FinalizeDeviceModel([&] {
const auto fromSystemProfiler = DeviceFromSystemProfiler();
if (!fromSystemProfiler.isEmpty()) {
return fromSystemProfiler;
}
size_t length = 0;
sysctlbyname("hw.model", nullptr, &length, nullptr, 0);
if (length > 0) {
QByteArray bytes(length, Qt::Uninitialized);
sysctlbyname("hw.model", bytes.data(), &length, nullptr, 0);
const auto parsed = base::CleanAndSimplify(
FromIdentifier(QString::fromUtf8(bytes)));
if (!parsed.isEmpty()) {
return parsed;
}
}
return QString();
}());
return result;
}
QString SystemVersionPretty() {
const auto major = MajorVersion();
const auto minor = MinorVersion();
const auto patch = PatchVersion();
const auto addAsPatch = (patch > 0) ? u".%1"_q.arg(patch) : QString();
if (major < 10) {
return "OS X";
} else if (major == 10 && minor < 12) {
return QString("OS X 10.%1").arg(minor) + addAsPatch;
}
return QString("macOS %1.%2").arg(major).arg(minor) + addAsPatch;
}
QString SystemCountry() {
NSLocale *currentLocale = [NSLocale currentLocale]; // get the current locale.
NSString *countryCode = [currentLocale objectForKey:NSLocaleCountryCode];
return countryCode ? NS2QString(countryCode) : QString();
}
QString SystemLanguage() {
if (auto currentLocale = [NSLocale currentLocale]) { // get the current locale.
if (NSString *collator = [currentLocale objectForKey:NSLocaleCollatorIdentifier]) {
return NS2QString(collator);
}
if (NSString *identifier = [currentLocale objectForKey:NSLocaleIdentifier]) {
return NS2QString(identifier);
}
if (NSString *language = [currentLocale objectForKey:NSLocaleLanguageCode]) {
return NS2QString(language);
}
}
return QString();
}
QDate WhenSystemBecomesOutdated() {
if (!IsMac10_13OrGreater()) {
return QDate(2023, 7, 1);
}
return QDate();
}
int AutoUpdateVersion() {
if (!IsMac10_13OrGreater()) {
return 2;
}
return 4;
}
QString AutoUpdateKey() {
if (QSysInfo::currentCpuArchitecture().startsWith("arm")
|| RunningThroughRosetta()) {
return "armac";
} else {
return "mac";
}
}
bool IsMac10_12OrGreater() {
return IsMac10ThatOrGreater<12>();
}
bool IsMac10_13OrGreater() {
return IsMac10ThatOrGreater<13>();
}
bool IsMac10_14OrGreater() {
return IsMac10ThatOrGreater<14>();
}
bool IsMac10_15OrGreater() {
return IsMac10ThatOrGreater<15>();
}
bool IsMac11_0OrGreater() {
return IsMacThatOrGreater<11, 0>();
}
bool IsMac26_0OrGreater() {
return IsMacThatOrGreater<26, 0>();
}
void Start(QJsonObject settings) {
Expects(GlobalWakeUpObserver == nil);
GlobalWakeUpObserver = [[WakeUpObserver alloc] init];
NSNotificationCenter *center = [[NSWorkspace sharedWorkspace] notificationCenter];
Assert(center != nil);
[center
addObserver: GlobalWakeUpObserver
selector: @selector(receiveWakeNote:)
name: NSWorkspaceDidWakeNotification
object: nil];
Ensures(GlobalWakeUpObserver != nil);
}
void Finish() {
Expects(GlobalWakeUpObserver != nil);
[GlobalWakeUpObserver release];
GlobalWakeUpObserver = nil;
}
void OpenInputMonitoringPrivacySettings() {
if (@available(macOS 10.15, *)) {
IOHIDRequestAccess(kIOHIDRequestTypeListenEvent);
}
[[NSWorkspace sharedWorkspace] openURL:PrivacySettingsUrl("Privacy_ListenEvent")];
}
void OpenDesktopCapturePrivacySettings() {
if (@available(macOS 11.0, *)) {
CGRequestScreenCaptureAccess();
}
[[NSWorkspace sharedWorkspace] openURL:PrivacySettingsUrl("Privacy_ScreenCapture")];
}
void OpenAccessibilityPrivacySettings() {
NSDictionary *const options=@{(__bridge NSString *)kAXTrustedCheckOptionPrompt: @TRUE};
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
}
} // namespace Platform

View File

@@ -0,0 +1,8 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once

View File

@@ -0,0 +1,68 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_last_input_mac.h"
#include <IOKit/IOKitLib.h>
#include <CoreFoundation/CoreFoundation.h>
namespace base::Platform {
// Taken from https://github.com/trueinteractions/tint/issues/53.
std::optional<crl::time> LastUserInputTime() {
CFMutableDictionaryRef properties = 0;
CFTypeRef obj;
mach_port_t masterPort;
io_iterator_t iter;
io_registry_entry_t curObj;
IOMasterPort(MACH_PORT_NULL, &masterPort);
/* Get IOHIDSystem */
IOServiceGetMatchingServices(masterPort, IOServiceMatching("IOHIDSystem"), &iter);
if (iter == 0) {
return std::nullopt;
} else {
curObj = IOIteratorNext(iter);
}
if (IORegistryEntryCreateCFProperties(curObj, &properties, kCFAllocatorDefault, 0) == KERN_SUCCESS && properties != NULL) {
obj = CFDictionaryGetValue(properties, CFSTR("HIDIdleTime"));
CFRetain(obj);
} else {
return std::nullopt;
}
uint64 err = ~0L, idleTime = err;
if (obj) {
CFTypeID type = CFGetTypeID(obj);
if (type == CFDataGetTypeID()) {
CFDataGetBytes((CFDataRef) obj, CFRangeMake(0, sizeof(idleTime)), (UInt8*)&idleTime);
} else if (type == CFNumberGetTypeID()) {
CFNumberGetValue((CFNumberRef)obj, kCFNumberSInt64Type, &idleTime);
} else {
// error
}
CFRelease(obj);
if (idleTime != err) {
idleTime /= 1000000; // return as ms
}
} else {
// error
}
CFRelease((CFTypeRef)properties);
IOObjectRelease(curObj);
IOObjectRelease(iter);
if (idleTime == err) {
return std::nullopt;
}
return (crl::now() - static_cast<crl::time>(idleTime));
}
} // namespace base::Platform

View File

@@ -0,0 +1,8 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once

View File

@@ -0,0 +1,66 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_layout_switch_mac.h"
#include <Carbon/Carbon.h>
#include <Foundation/Foundation.h>
namespace base::Platform {
bool SwitchKeyboardLayoutToEnglish() {
auto result = false;
@autoreleasepool {
const auto kEnglish = base::flat_map<NSString*, int>{
{ @"com.apple.keylayout.USExtended", 0 },
{ @"com.apple.keylayout.ABC", 0 },
{ @"com.apple.keylayout.Australian", 1 },
{ @"com.apple.keylayout.British", 2 },
{ @"com.apple.keylayout.British-PC", 3 },
{ @"com.apple.keylayout.Canadian", 1 },
{ @"com.apple.keylayout.Colemak", 1 },
{ @"com.apple.keylayout.Dvorak", 0 },
{ @"com.apple.keylayout.Dvorak-Left", 0 },
{ @"com.apple.keylayout.DVORAK-QWERTYCMD", 0 },
{ @"com.apple.keylayout.Dvorak-Right", 0 },
{ @"com.apple.keylayout.Irish", 1 },
{ @"com.apple.keylayout.USInternational-PC", 4 },
{ @"com.apple.keylayout.US", 5 },
};
auto selectedLayout = (NSObject*)NULL;
auto selectedLevel = 0;
const auto offer = [&](NSObject *layout, int level) {
if (level > selectedLevel) {
selectedLayout = layout;
selectedLevel = level;
}
};
NSArray *list = [(NSArray *)TISCreateInputSourceList(NULL,NO) autorelease];
for (NSObject *layout in list) {
NSString *layoutId = (NSString*)TISGetInputSourceProperty(
(TISInputSourceRef)layout,
kTISPropertyInputSourceID);
for (const auto &[checkId, checkLevel] : kEnglish) {
if ([layoutId isEqualToString:checkId]) {
offer(layout, checkLevel);
}
}
}
if (selectedLayout != nullptr) {
TISSelectInputSource(TISInputSourceRef(selectedLayout));
result = true;
}
}
return result;
}
} // namespace base::Platform

View File

@@ -0,0 +1,15 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/base_platform_network_reachability.h"
namespace base::Platform {
std::unique_ptr<NetworkReachability> NetworkReachability::Create() {
return nullptr;
}
} // namespace base::Platform

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_power_save_blocker.h"

View File

@@ -0,0 +1,99 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_power_save_blocker_mac.h"
#include "base/debug_log.h"
#include <crl/crl_object_on_thread.h>
// Thanks Chromium: services/device/wake_lock/power_save_blocker
#include <IOKit/pwr_mgt/IOPMLib.h>
namespace base::Platform {
namespace {
// Power management cannot be done on the UI thread. IOPMAssertionCreate does a
// synchronous MIG call to configd, so if it is called on the main thread the UI
// is at the mercy of another process. See http://crbug.com/79559 and
// http://www.opensource.apple.com/source/IOKitUser/IOKitUser-514.16.31/pwr_mgt.subproj/IOPMLibPrivate.c .
class BlockManager final {
public:
explicit BlockManager(crl::weak_on_thread<BlockManager> weak);
void block(PowerSaveBlockType type, const QString &description);
void unblock(PowerSaveBlockType type);
private:
crl::weak_on_thread<BlockManager> _weak;
IOPMAssertionID _assertions[kPowerSaveBlockTypeCount] = {};
};
BlockManager::BlockManager(crl::weak_on_thread<BlockManager> weak)
: _weak(weak) {
}
void BlockManager::block(PowerSaveBlockType type, const QString &description) {
const auto level = CFStringRef([&] {
// See QA1340 <http://developer.apple.com/library/mac/#qa/qa1340/> for more
// details.
switch (type) {
case PowerSaveBlockType::PreventAppSuspension:
return kIOPMAssertionTypeNoIdleSleep;
case PowerSaveBlockType::PreventDisplaySleep:
return kIOPMAssertionTypeNoDisplaySleep;
}
Unexpected("Type in BlockManager::block.");
}());
const auto reason = description.toCFString();
IOReturn result = IOPMAssertionCreateWithName(
level,
kIOPMAssertionLevelOn,
reason,
&_assertions[PowerSaveBlockTypeIndex(type)]);
CFRelease(reason);
if (result != kIOReturnSuccess) {
LOG(("System Error: IOPMAssertionCreate: %1").arg(result));
}
}
void BlockManager::unblock(PowerSaveBlockType type) {
const auto index = PowerSaveBlockTypeIndex(type);
if (_assertions[index] != kIOPMNullAssertionID) {
IOReturn result = IOPMAssertionRelease(_assertions[index]);
_assertions[index] = kIOPMNullAssertionID;
if (result != kIOReturnSuccess) {
LOG(("System Error: IOPMAssertionRelease: %1").arg(result));
}
}
}
[[nodiscard]] crl::object_on_thread<BlockManager> &Manager() {
static auto result = crl::object_on_thread<BlockManager>();
return result;
}
} // namespace
void BlockPowerSave(
PowerSaveBlockType type,
const QString &description,
QPointer<QWindow> window) {
Manager().with([=](BlockManager &instance) {
instance.block(type, description);
});
}
void UnblockPowerSave(PowerSaveBlockType type, QPointer<QWindow> window) {
Manager().with([=](BlockManager &instance) {
instance.unblock(type);
});
}
} // namespace base::Platform

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_process.h"

View File

@@ -0,0 +1,23 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_process_mac.h"
#include <Cocoa/Cocoa.h>
namespace base::Platform {
void ActivateProcessWindow(int64 pid, WId windowId) {
}
void ActivateThisProcessWindow(WId windowId) {
[NSApp activateIgnoringOtherApps:YES];
if (const auto view = reinterpret_cast<NSView*>(windowId)) {
[[view window] makeKeyAndOrderFront:NSApp];
}
}
} // namespace base::Platform

View File

@@ -0,0 +1,379 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/base_platform_system_media_controls.h"
#include "base/integration.h"
#include "base/platform/mac/base_utilities_mac.h"
#import <MediaPlayer/MediaPlayer.h>
#include <QtGui/QImage>
#include <QtWidgets/QWidget>
namespace {
using Command = base::Platform::SystemMediaControls::Command;
using ::Platform::Q2NSString;
using ::Platform::Q2NSImage;
inline auto CommandCenter() {
return [MPRemoteCommandCenter sharedCommandCenter];
}
MPNowPlayingPlaybackState ConvertPlaybackStatus(
base::Platform::SystemMediaControls::PlaybackStatus status) {
using Status = base::Platform::SystemMediaControls::PlaybackStatus;
switch (status) {
case Status::Playing: return MPNowPlayingPlaybackStatePlaying;
case Status::Paused: return MPNowPlayingPlaybackStatePaused;
case Status::Stopped: return MPNowPlayingPlaybackStateStopped;
}
Unexpected("ConvertPlaybackStatus in SystemMediaControls");
}
auto EventToCommand(MPRemoteCommandEvent *event) {
const auto commandCenter = CommandCenter();
const auto command = event.command;
if (command == commandCenter.pauseCommand) {
return Command::Pause;
} else if (command == commandCenter.playCommand) {
return Command::Play;
} else if (command == commandCenter.stopCommand) {
return Command::Stop;
} else if (command == commandCenter.togglePlayPauseCommand) {
return Command::PlayPause;
} else if (command == commandCenter.nextTrackCommand) {
return Command::Next;
} else if (command == commandCenter.previousTrackCommand) {
return Command::Previous;
}
return Command::None;
}
struct RemoteCommand {
const MPRemoteCommand *command;
bool lastEnabled = false;
};
} // namespace
#pragma mark - CommandHandler
@interface CommandHandler : NSObject {
}
@end // @interface CommandHandler
@implementation CommandHandler {
rpl::event_stream<Command> _commandRequests;
rpl::event_stream<int> _seekRequests;
std::vector<RemoteCommand> _commands;
}
- (id)init {
self = [super init];
const auto center = CommandCenter();
_commands = {
{ .command = center.pauseCommand },
{ .command = center.playCommand },
{ .command = center.stopCommand },
{ .command = center.togglePlayPauseCommand },
{ .command = center.nextTrackCommand },
{ .command = center.previousTrackCommand },
{ .command = center.changeRepeatModeCommand },
{ .command = center.changeShuffleModeCommand },
{ .command = center.changePlaybackRateCommand },
{ .command = center.seekBackwardCommand },
{ .command = center.seekForwardCommand },
{ .command = center.skipBackwardCommand },
{ .command = center.skipForwardCommand },
{ .command = center.changePlaybackPositionCommand },
{ .command = center.ratingCommand },
{ .command = center.likeCommand },
{ .command = center.dislikeCommand },
{ .command = center.bookmarkCommand },
{ .command = center.enableLanguageOptionCommand },
{ .command = center.disableLanguageOptionCommand },
};
[self initCommands];
return self;
}
- (void)initCommands {
for (const auto &c : _commands) {
c.command.enabled = c.lastEnabled;
}
const auto center = CommandCenter();
const auto selector = @selector(onCommand:);
[center.pauseCommand addTarget:self action:selector];
[center.playCommand addTarget:self action:selector];
[center.stopCommand addTarget:self action:selector];
[center.togglePlayPauseCommand addTarget:self action:selector];
[center.nextTrackCommand addTarget:self action:selector];
[center.previousTrackCommand addTarget:self action:selector];
[center.changePlaybackPositionCommand
addTarget:self
action:@selector(onSeek:)];
}
- (void)clearCommands {
const auto center = CommandCenter();
for (auto &c : _commands) {
c.lastEnabled = c.command.enabled;
c.command.enabled = false;
}
[center.pauseCommand removeTarget:self];
[center.playCommand removeTarget:self];
[center.stopCommand removeTarget:self];
[center.togglePlayPauseCommand removeTarget:self];
[center.nextTrackCommand removeTarget:self];
[center.previousTrackCommand removeTarget:self];
[center.changePlaybackPositionCommand removeTarget:self];
}
- (rpl::producer<Command>)commandRequests {
return _commandRequests.events();
}
- (rpl::producer<int>)seekRequests {
return _seekRequests.events();
}
- (MPRemoteCommandHandlerStatus)onCommand:(MPRemoteCommandEvent*)event {
base::Integration::Instance().enterFromEventLoop([&] {
self->_commandRequests.fire_copy(EventToCommand(event));
});
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus)onSeek:(
MPChangePlaybackPositionCommandEvent*)event {
base::Integration::Instance().enterFromEventLoop([&] {
self->_seekRequests.fire(event.positionTime * 1000);
});
return MPRemoteCommandHandlerStatusSuccess;
}
- (void)dealloc {
[self clearCommands];
[super dealloc];
}
@end // @@implementation CommandHandler
namespace base::Platform {
struct SystemMediaControls::Private {
Private(
not_null<NSMutableDictionary*> info,
not_null<CommandHandler*> commandHandler)
: info(info)
, commandHandler(commandHandler) {
}
[[nodiscard]] float64 duration() const {
return ((NSNumber*)[info
objectForKey:MPMediaItemPropertyPlaybackDuration]).doubleValue;
}
const not_null<NSMutableDictionary*> info;
const not_null<CommandHandler*> commandHandler;
bool enabled = false;
};
SystemMediaControls::SystemMediaControls()
: _private(std::make_unique<Private>(
[[NSMutableDictionary alloc] init],
[[CommandHandler alloc] init])) {
}
SystemMediaControls::~SystemMediaControls() {
setEnabled(false);
[_private->info release];
[_private->commandHandler release];
}
bool SystemMediaControls::init() {
clearMetadata();
updateDisplay();
return true;
}
void SystemMediaControls::setApplicationName(const QString &name) {
}
void SystemMediaControls::setEnabled(bool enabled) {
if (_private->enabled == enabled) {
return;
}
_private->enabled = enabled;
if (enabled) {
[_private->commandHandler initCommands];
} else {
[_private->commandHandler clearCommands];
}
updateDisplay();
}
void SystemMediaControls::setIsNextEnabled(bool value) {
CommandCenter().nextTrackCommand.enabled = value;
}
void SystemMediaControls::setIsPreviousEnabled(bool value) {
CommandCenter().previousTrackCommand.enabled = value;
}
void SystemMediaControls::setIsPlayPauseEnabled(bool value) {
CommandCenter().togglePlayPauseCommand.enabled = value;
}
void SystemMediaControls::setIsStopEnabled(bool value) {
CommandCenter().stopCommand.enabled = value;
}
void SystemMediaControls::setPlaybackStatus(
SystemMediaControls::PlaybackStatus status) {
[MPNowPlayingInfoCenter defaultCenter].playbackState =
ConvertPlaybackStatus(status);
}
void SystemMediaControls::setLoopStatus(LoopStatus status) {
}
void SystemMediaControls::setShuffle(bool value) {
}
void SystemMediaControls::setTitle(const QString &title) {
[_private->info
setObject:Q2NSString(title)
forKey:MPMediaItemPropertyTitle];
}
void SystemMediaControls::setArtist(const QString &artist) {
[_private->info
setObject:Q2NSString(artist)
forKey:MPMediaItemPropertyArtist];
}
void SystemMediaControls::setThumbnail(const QImage &thumbnail) {
if (thumbnail.isNull()) {
return;
}
if (@available(macOS 10.13.2, *)) {
const auto copy = thumbnail;
[_private->info
setObject:[[[MPMediaItemArtwork alloc]
initWithBoundsSize:CGSizeMake(copy.width(), copy.height())
requestHandler:^NSImage *(CGSize size) {
return Q2NSImage(copy.scaled(
int(size.width),
int(size.height)));
}] autorelease]
forKey:MPMediaItemPropertyArtwork];
updateDisplay();
}
}
void SystemMediaControls::setDuration(int duration) {
[_private->info
setObject:[NSNumber numberWithDouble:(duration / 1000.)]
forKey:MPMediaItemPropertyPlaybackDuration];
}
void SystemMediaControls::setPosition(int position) {
[_private->info
setObject:[NSNumber numberWithDouble:(position / 1000.)]
forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
}
void SystemMediaControls::setVolume(float64 volume) {
}
void SystemMediaControls::clearThumbnail() {
if (@available(macOS 10.13.2, *)) {
[_private->info removeObjectForKey:MPMediaItemPropertyArtwork];
updateDisplay();
}
}
void SystemMediaControls::clearMetadata() {
const auto zeroNumber = [NSNumber numberWithInt:0];
const auto oneNumber = [NSNumber numberWithInt:1];
const auto &info = _private->info;
[info
setObject:zeroNumber
forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
[info setObject:zeroNumber forKey:MPMediaItemPropertyPlaybackDuration];
[info setObject:oneNumber forKey:MPNowPlayingInfoPropertyPlaybackRate];
[info
setObject:oneNumber
forKey:MPNowPlayingInfoPropertyDefaultPlaybackRate];
[info setObject:@"" forKey:MPMediaItemPropertyTitle];
[info setObject:@"" forKey:MPMediaItemPropertyArtist];
[info
setObject:@(MPNowPlayingInfoMediaTypeAudio)
forKey:MPNowPlayingInfoPropertyMediaType];
}
void SystemMediaControls::updateDisplay() {
[[MPNowPlayingInfoCenter defaultCenter]
performSelectorOnMainThread:@selector(setNowPlayingInfo:)
withObject:((_private->enabled && _private->duration())
? _private->info
: nil)
waitUntilDone:false];
}
auto SystemMediaControls::commandRequests() const
-> rpl::producer<SystemMediaControls::Command> {
return [_private->commandHandler commandRequests];
}
rpl::producer<float64> SystemMediaControls::seekRequests() const {
return (
[_private->commandHandler seekRequests]
) | rpl::map([=](int position) {
return float64(position) / (_private->duration() * 1000);
});
}
rpl::producer<float64> SystemMediaControls::volumeChangeRequests() const {
return rpl::never<float64>();
}
rpl::producer<> SystemMediaControls::updatePositionRequests() const {
return rpl::never<>();
}
bool SystemMediaControls::seekingSupported() const {
return true;
}
bool SystemMediaControls::volumeSupported() const {
return false;
}
bool SystemMediaControls::Supported() {
if (@available(macOS 10.12.2, *)) {
return true;
}
return false;
}
} // namespace base::Platform

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/system_unlock.h"

View File

@@ -0,0 +1,87 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_system_unlock_mac.h"
#include "base/platform/mac/base_utilities_mac.h"
#include <Foundation/Foundation.h>
#include <LocalAuthentication/LocalAuthentication.h>
namespace base {
namespace {
[[nodiscard]] bool Available(LAContext *context, LAPolicy policy) {
NSError *error = nil;
return [context canEvaluatePolicy:policy error:&error];
}
[[nodiscard]] SystemUnlockAvailability Available(bool lookupDetails) {
LAContext *context = [[LAContext alloc] init];
auto result = SystemUnlockAvailability{
.known = true,
.available = Available(
context,
LAPolicyDeviceOwnerAuthentication),
.withBiometrics = lookupDetails && Available(
context,
LAPolicyDeviceOwnerAuthenticationWithBiometrics),
};
if (lookupDetails) {
if (@available(macOS 10.15, *)) {
result.withCompanion = Available(
context,
LAPolicyDeviceOwnerAuthenticationWithWatch);
}
}
[context release];
return result;
}
} // namespace
rpl::producer<SystemUnlockAvailability> SystemUnlockStatus(
bool lookupDetails) {
static auto result = rpl::variable<SystemUnlockAvailability>();
auto refreshed = Available(lookupDetails);
if (!lookupDetails) {
const auto now = result.current();
refreshed.withBiometrics = now.available && now.withBiometrics;
refreshed.withCompanion = now.available && now.withCompanion;
}
result = refreshed;
return result.value();
}
void SuggestSystemUnlock(
not_null<QWidget*> parent,
const QString &text,
Fn<void(SystemUnlockResult)> done) {
LAContext *context = [[LAContext alloc] init];
if (Available(context, LAPolicyDeviceOwnerAuthentication)) {
[context
evaluatePolicy:LAPolicyDeviceOwnerAuthentication
localizedReason:Platform::Q2NSString(text)
reply:^(BOOL success, NSError *error) {
const auto code = int(error.code);
if (success) {
done(SystemUnlockResult::Success);
} else if (error.code == LAErrorTouchIDLockout) {
done(SystemUnlockResult::FloodError);
} else {
done(SystemUnlockResult::Cancelled);
}
}];
} else {
done(SystemUnlockResult::Cancelled);
}
}
} // namespace base

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_url_scheme.h"

View File

@@ -0,0 +1,39 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_url_scheme_mac.h"
#include <Carbon/Carbon.h>
#include <CoreFoundation/CoreFoundation.h>
namespace base::Platform {
bool CheckUrlScheme(const UrlSchemeDescriptor &descriptor) {
const auto name = descriptor.protocol.toStdString();
const auto str = CFStringCreateWithCString(nullptr, name.c_str(), kCFStringEncodingASCII);
const auto current = LSCopyDefaultHandlerForURLScheme(str);
const auto result = CFStringCompare(
current,
(CFStringRef)[[NSBundle mainBundle] bundleIdentifier],
kCFCompareCaseInsensitive);
CFRelease(str);
return (result == kCFCompareEqualTo);
}
void RegisterUrlScheme(const UrlSchemeDescriptor &descriptor) {
const auto name = descriptor.protocol.toStdString();
const auto str = CFStringCreateWithCString(nullptr, name.c_str(), kCFStringEncodingASCII);
LSSetDefaultHandlerForURLScheme(
str,
(CFStringRef)[[NSBundle mainBundle] bundleIdentifier]);
CFRelease(str);
}
void UnregisterUrlScheme(const UrlSchemeDescriptor &descriptor) {
// TODO
}
} // namespace base::Platform

View File

@@ -0,0 +1,58 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include <QtGui/QImage>
#include <Cocoa/Cocoa.h>
namespace Platform {
inline NSString *Q2NSString(const QString &str) {
return [NSString stringWithUTF8String:str.toUtf8().constData()];
}
inline NSString *Q2NSString(QStringView str) {
return [NSString stringWithUTF8String:str.toUtf8().constData()];
}
inline QString NS2QString(NSString *str) {
return QString::fromUtf8([str cStringUsingEncoding:NSUTF8StringEncoding]);
}
template <int Size>
inline QString MakeFromLetters(const uint32 (&letters)[Size]) {
QString result;
result.reserve(Size);
for (int32 i = 0; i < Size; ++i) {
auto code = letters[i];
auto salt1 = (code >> 8) & 0xFFU;
auto salt2 = (code >> 24) & 0xFFU;
auto part1 = ((code & 0xFFU) ^ (salt1 ^ salt2)) & 0xFFU;
auto part2 = (((code >> 16) & 0xFFU) ^ (salt1 ^ ~salt2)) & 0xFFU;
result.push_back(QChar((part2 << 8) | part1));
}
return result;
}
inline NSImage *Q2NSImage(const QImage &image) {
if (image.isNull()) {
return nil;
}
CGImageRef cgImage = image.toCGImage();
if (!cgImage) {
return nil;
}
auto nsImage = [[NSImage alloc] initWithSize:NSZeroSize];
auto *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgImage];
imageRep.size = (image.size() / image.devicePixelRatioF()).toCGSize();
[nsImage addRepresentation:[imageRep autorelease]];
CFRelease(cgImage);
return [nsImage autorelease];
}
} // namespace Platform

View File

@@ -0,0 +1,7 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_utilities_mac.h"