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
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:
@@ -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
|
||||
|
||||
202
Telegram/lib_base/base/platform/mac/base_battery_saving_mac.mm
Normal file
202
Telegram/lib_base/base/platform/mac/base_battery_saving_mac.mm
Normal 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
|
||||
14
Telegram/lib_base/base/platform/mac/base_confirm_quit.h
Normal file
14
Telegram/lib_base/base/platform/mac/base_confirm_quit.h
Normal 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
|
||||
462
Telegram/lib_base/base/platform/mac/base_confirm_quit.mm
Normal file
462
Telegram/lib_base/base/platform/mac/base_confirm_quit.mm
Normal 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
|
||||
@@ -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"
|
||||
387
Telegram/lib_base/base/platform/mac/base_custom_app_icon_mac.mm
Normal file
387
Telegram/lib_base/base/platform/mac/base_custom_app_icon_mac.mm
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
345
Telegram/lib_base/base/platform/mac/base_global_shortcuts_mac.mm
Normal file
345
Telegram/lib_base/base/platform/mac/base_global_shortcuts_mac.mm
Normal 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
|
||||
9
Telegram/lib_base/base/platform/mac/base_haptic_mac.h
Normal file
9
Telegram/lib_base/base/platform/mac/base_haptic_mac.h
Normal 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"
|
||||
33
Telegram/lib_base/base/platform/mac/base_haptic_mac.mm
Normal file
33
Telegram/lib_base/base/platform/mac/base_haptic_mac.mm
Normal 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
|
||||
51
Telegram/lib_base/base/platform/mac/base_info_mac.h
Normal file
51
Telegram/lib_base/base/platform/mac/base_info_mac.h
Normal 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
|
||||
292
Telegram/lib_base/base/platform/mac/base_info_mac.mm
Normal file
292
Telegram/lib_base/base/platform/mac/base_info_mac.mm
Normal 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 §ion) {
|
||||
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
|
||||
@@ -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
|
||||
|
||||
68
Telegram/lib_base/base/platform/mac/base_last_input_mac.mm
Normal file
68
Telegram/lib_base/base/platform/mac/base_last_input_mac.mm
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
9
Telegram/lib_base/base/platform/mac/base_process_mac.h
Normal file
9
Telegram/lib_base/base/platform/mac/base_process_mac.h
Normal 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"
|
||||
23
Telegram/lib_base/base/platform/mac/base_process_mac.mm
Normal file
23
Telegram/lib_base/base/platform/mac/base_process_mac.mm
Normal 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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
39
Telegram/lib_base/base/platform/mac/base_url_scheme_mac.mm
Normal file
39
Telegram/lib_base/base/platform/mac/base_url_scheme_mac.mm
Normal 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
|
||||
58
Telegram/lib_base/base/platform/mac/base_utilities_mac.h
Normal file
58
Telegram/lib_base/base/platform/mac/base_utilities_mac.h
Normal 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
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user