424 lines
11 KiB
Plaintext
424 lines
11 KiB
Plaintext
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#include "platform/mac/specific_mac_p.h"
|
|
|
|
#include "mainwindow.h"
|
|
#include "mainwidget.h"
|
|
#include "calls/calls_instance.h"
|
|
#include "core/sandbox.h"
|
|
#include "core/application.h"
|
|
#include "core/core_settings.h"
|
|
#include "core/crash_reports.h"
|
|
#include "storage/localstorage.h"
|
|
#include "media/audio/media_audio.h"
|
|
#include "media/player/media_player_instance.h"
|
|
#include "window/window_controller.h"
|
|
#include "base/platform/mac/base_utilities_mac.h"
|
|
#include "base/platform/base_platform_info.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "base/timer.h"
|
|
#include "styles/style_window.h"
|
|
#include "platform/platform_specific.h"
|
|
|
|
#include <QtGui/QWindow>
|
|
#include <QtWidgets/QApplication>
|
|
#if __has_include(<QtCore/QOperatingSystemVersion>)
|
|
#include <QtCore/QOperatingSystemVersion>
|
|
#endif // __has_include(<QtCore/QOperatingSystemVersion>)
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
|
|
#include <qpa/qwindowsysteminterface.h>
|
|
#endif // Qt < 6.6.0
|
|
#include <Cocoa/Cocoa.h>
|
|
#include <CoreFoundation/CFURL.h>
|
|
#include <IOKit/IOKitLib.h>
|
|
#include <IOKit/hidsystem/ev_keymap.h>
|
|
|
|
using Platform::Q2NSString;
|
|
using Platform::NS2QString;
|
|
|
|
namespace {
|
|
|
|
constexpr auto kIgnoreActivationTimeoutMs = 500;
|
|
|
|
NSMenuItem *CreateMenuItem(
|
|
QString title,
|
|
rpl::lifetime &lifetime,
|
|
Fn<void()> callback,
|
|
bool enabled = true) {
|
|
id block = [^{
|
|
Core::Sandbox::Instance().customEnterFromEventLoop(callback);
|
|
} copy];
|
|
|
|
NSMenuItem *item = [[NSMenuItem alloc]
|
|
initWithTitle:Q2NSString(title)
|
|
action:@selector(invoke)
|
|
keyEquivalent:@""];
|
|
[item setTarget:block];
|
|
[item setEnabled:enabled];
|
|
|
|
lifetime.add([=] {
|
|
[block release];
|
|
});
|
|
return [item autorelease];
|
|
}
|
|
|
|
} // namespace
|
|
|
|
@interface RpMenu : NSMenu {
|
|
}
|
|
|
|
- (rpl::lifetime &) lifetime;
|
|
|
|
@end // @interface Menu
|
|
|
|
@implementation RpMenu {
|
|
rpl::lifetime _lifetime;
|
|
}
|
|
|
|
- (rpl::lifetime &) lifetime {
|
|
return _lifetime;
|
|
}
|
|
|
|
@end // @implementation Menu
|
|
|
|
@interface qVisualize : NSObject {
|
|
}
|
|
|
|
+ (id)str:(const QString &)str;
|
|
- (id)initWithString:(const QString &)str;
|
|
|
|
+ (id)bytearr:(const QByteArray &)arr;
|
|
- (id)initWithByteArray:(const QByteArray &)arr;
|
|
|
|
- (id)debugQuickLookObject;
|
|
|
|
@end // @interface qVisualize
|
|
|
|
@implementation qVisualize {
|
|
NSString *value;
|
|
|
|
}
|
|
|
|
+ (id)bytearr:(const QByteArray &)arr {
|
|
return [[qVisualize alloc] initWithByteArray:arr];
|
|
}
|
|
- (id)initWithByteArray:(const QByteArray &)arr {
|
|
if (self = [super init]) {
|
|
value = [NSString stringWithUTF8String:arr.constData()];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
+ (id)str:(const QString &)str {
|
|
return [[qVisualize alloc] initWithString:str];
|
|
}
|
|
- (id)initWithString:(const QString &)str {
|
|
if (self = [super init]) {
|
|
value = [NSString stringWithUTF8String:str.toUtf8().constData()];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (id)debugQuickLookObject {
|
|
return value;
|
|
}
|
|
|
|
@end // @implementation qVisualize
|
|
|
|
@interface ApplicationDelegate : NSObject<NSApplicationDelegate> {
|
|
}
|
|
|
|
- (BOOL) applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag;
|
|
- (void) applicationDidBecomeActive:(NSNotification *)aNotification;
|
|
- (void) applicationDidResignActive:(NSNotification *)aNotification;
|
|
- (void) receiveWakeNote:(NSNotification*)note;
|
|
|
|
- (void) ignoreApplicationActivationRightNow;
|
|
|
|
- (NSMenu *) applicationDockMenu:(NSApplication *)sender;
|
|
|
|
@end // @interface ApplicationDelegate
|
|
|
|
ApplicationDelegate *_sharedDelegate = nil;
|
|
|
|
@implementation ApplicationDelegate {
|
|
bool _ignoreActivation;
|
|
base::Timer _ignoreActivationStop;
|
|
}
|
|
|
|
- (instancetype) init {
|
|
_ignoreActivation = false;
|
|
_ignoreActivationStop.setCallback([self] {
|
|
_ignoreActivation = false;
|
|
});
|
|
return [super init];
|
|
}
|
|
|
|
- (BOOL) applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag {
|
|
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
|
|
if (Core::IsAppLaunched()) {
|
|
if (const auto window = Core::App().activeWindow()) {
|
|
if (window->widget()->isHidden()) {
|
|
window->widget()->showFromTray();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
return YES;
|
|
}
|
|
|
|
- (void) applicationDidBecomeActive:(NSNotification *)aNotification {
|
|
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
|
|
if (Core::IsAppLaunched() && !_ignoreActivation) {
|
|
Core::App().handleAppActivated();
|
|
if (const auto window = Core::App().activeWindow()) {
|
|
if (window->widget()->isHidden()) {
|
|
if (Core::App().calls().hasVisiblePanel()) {
|
|
Core::App().calls().activateCurrentCall();
|
|
} else {
|
|
window->widget()->showFromTray();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void) applicationDidResignActive:(NSNotification *)aNotification {
|
|
}
|
|
|
|
- (void) receiveWakeNote:(NSNotification*)aNotification {
|
|
if (!Core::IsAppLaunched()) {
|
|
return;
|
|
}
|
|
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
|
|
Core::App().checkLocalTime();
|
|
|
|
LOG(("Audio Info: "
|
|
"-receiveWakeNote: received, scheduling detach from audio device"));
|
|
Media::Audio::ScheduleDetachFromDeviceSafe();
|
|
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
|
|
Core::App().settings().setSystemDarkMode(Platform::IsDarkMode());
|
|
#elif QT_VERSION < QT_VERSION_CHECK(6, 6, 0) // Qt < 6.5.0
|
|
QWindowSystemInterface::handleThemeChange();
|
|
#endif // Qt < 6.6.0
|
|
});
|
|
}
|
|
|
|
- (void) ignoreApplicationActivationRightNow {
|
|
_ignoreActivation = true;
|
|
_ignoreActivationStop.callOnce(kIgnoreActivationTimeoutMs);
|
|
}
|
|
|
|
- (NSMenu *) applicationDockMenu:(NSApplication *)sender {
|
|
if (!Core::IsAppLaunched()) {
|
|
return nil;
|
|
}
|
|
RpMenu* dockMenu = [[[RpMenu alloc] initWithTitle: @""] autorelease];
|
|
[dockMenu setAutoenablesItems:false];
|
|
|
|
auto notifyCallback = [] {
|
|
auto &settings = Core::App().settings();
|
|
settings.setDesktopNotify(!settings.desktopNotify());
|
|
};
|
|
[dockMenu addItem:CreateMenuItem(
|
|
Core::App().settings().desktopNotify()
|
|
? tr::lng_disable_notifications_from_tray(tr::now)
|
|
: tr::lng_enable_notifications_from_tray(tr::now),
|
|
[dockMenu lifetime],
|
|
std::move(notifyCallback))];
|
|
|
|
using namespace Media::Player;
|
|
const auto state = instance()->getState(instance()->getActiveType());
|
|
if (!IsStoppedOrStopping(state.state)) {
|
|
[dockMenu addItem:[NSMenuItem separatorItem]];
|
|
[dockMenu addItem:CreateMenuItem(
|
|
tr::lng_mac_menu_player_previous(tr::now),
|
|
[dockMenu lifetime],
|
|
[] { instance()->previous(); },
|
|
instance()->previousAvailable(instance()->getActiveType()))];
|
|
[dockMenu addItem:CreateMenuItem(
|
|
IsPausedOrPausing(state.state)
|
|
? tr::lng_mac_menu_player_resume(tr::now)
|
|
: tr::lng_mac_menu_player_pause(tr::now),
|
|
[dockMenu lifetime],
|
|
[] { instance()->playPause(); })];
|
|
[dockMenu addItem:CreateMenuItem(
|
|
tr::lng_mac_menu_player_next(tr::now),
|
|
[dockMenu lifetime],
|
|
[] { instance()->next(); },
|
|
instance()->nextAvailable(instance()->getActiveType()))];
|
|
}
|
|
|
|
return dockMenu;
|
|
}
|
|
|
|
@end // @implementation ApplicationDelegate
|
|
|
|
namespace Platform {
|
|
|
|
void SetApplicationIcon(const QIcon &icon) {
|
|
NSImage *image = nil;
|
|
if (!icon.isNull()) {
|
|
auto pixmap = icon.pixmap(1024, 1024);
|
|
pixmap.setDevicePixelRatio(style::DevicePixelRatio());
|
|
image = Q2NSImage(pixmap.toImage());
|
|
}
|
|
[[NSApplication sharedApplication] setApplicationIconImage:image];
|
|
}
|
|
|
|
} // namespace Platform
|
|
|
|
void objc_debugShowAlert(const QString &str) {
|
|
@autoreleasepool {
|
|
|
|
NSAlert *alert = [[NSAlert alloc] init];
|
|
alert.messageText = @"Debug Message";
|
|
alert.informativeText = Q2NSString(str);
|
|
[alert runModal];
|
|
|
|
}
|
|
}
|
|
|
|
void objc_outputDebugString(const QString &str) {
|
|
@autoreleasepool {
|
|
|
|
NSLog(@"%@", Q2NSString(str));
|
|
|
|
}
|
|
}
|
|
|
|
void objc_start() {
|
|
// Patch: Fix macOS regression. On 10.14.4, it crashes on GPU switches.
|
|
// See https://bugreports.qt.io/browse/QTCREATORBUG-22215
|
|
const auto version = QOperatingSystemVersion::current();
|
|
if (version.majorVersion() == 10
|
|
&& version.minorVersion() == 14
|
|
&& version.microVersion() == 4) {
|
|
qputenv("QT_MAC_PRO_WEBENGINE_WORKAROUND", "1");
|
|
}
|
|
|
|
_sharedDelegate = [[ApplicationDelegate alloc] init];
|
|
[[NSApplication sharedApplication] setDelegate:_sharedDelegate];
|
|
[[[NSWorkspace sharedWorkspace] notificationCenter]
|
|
addObserver: _sharedDelegate
|
|
selector: @selector(receiveWakeNote:)
|
|
name: NSWorkspaceDidWakeNotification object: NULL];
|
|
}
|
|
|
|
void objc_ignoreApplicationActivationRightNow() {
|
|
if (_sharedDelegate) {
|
|
[_sharedDelegate ignoreApplicationActivationRightNow];
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
NSURL *_downloadPathUrl = nil;
|
|
}
|
|
|
|
void objc_finish() {
|
|
[_sharedDelegate release];
|
|
_sharedDelegate = nil;
|
|
if (_downloadPathUrl) {
|
|
[_downloadPathUrl stopAccessingSecurityScopedResource];
|
|
_downloadPathUrl = nil;
|
|
}
|
|
}
|
|
|
|
void objc_activateProgram(WId winId) {
|
|
[NSApp activateIgnoringOtherApps:YES];
|
|
if (winId) {
|
|
NSWindow *w = [reinterpret_cast<NSView*>(winId) window];
|
|
[w makeKeyAndOrderFront:NSApp];
|
|
}
|
|
}
|
|
|
|
bool objc_moveFile(const QString &from, const QString &to) {
|
|
@autoreleasepool {
|
|
|
|
NSString *f = Q2NSString(from), *t = Q2NSString(to);
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath:t]) {
|
|
NSData *data = [NSData dataWithContentsOfFile:f];
|
|
if (data) {
|
|
if ([data writeToFile:t atomically:YES]) {
|
|
if ([[NSFileManager defaultManager] removeItemAtPath:f error:nil]) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if ([[NSFileManager defaultManager] moveItemAtPath:f toPath:t error:nil]) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
}
|
|
return false;
|
|
}
|
|
|
|
double objc_appkitVersion() {
|
|
return NSAppKitVersionNumber;
|
|
}
|
|
|
|
QString objc_documentsPath() {
|
|
NSURL *url = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:nil];
|
|
if (url) {
|
|
return QString::fromUtf8([[url path] fileSystemRepresentation]) + '/';
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
QString objc_appDataPath() {
|
|
NSURL *url = [[NSFileManager defaultManager] URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:nil];
|
|
if (url) {
|
|
return QString::fromUtf8([[url path] fileSystemRepresentation]) + '/' + AppName.utf16() + '/';
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
QByteArray objc_downloadPathBookmark(const QString &path) {
|
|
#ifndef OS_MAC_STORE
|
|
return QByteArray();
|
|
#else // OS_MAC_STORE
|
|
NSURL *url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:path.toUtf8().constData()] isDirectory:YES];
|
|
if (!url) return QByteArray();
|
|
|
|
NSError *error = nil;
|
|
NSData *data = [url bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error];
|
|
return data ? QByteArray::fromNSData(data) : QByteArray();
|
|
#endif // OS_MAC_STORE
|
|
}
|
|
|
|
void objc_downloadPathEnableAccess(const QByteArray &bookmark) {
|
|
#ifdef OS_MAC_STORE
|
|
if (bookmark.isEmpty()) return;
|
|
|
|
BOOL isStale = NO;
|
|
NSError *error = nil;
|
|
NSURL *url = [NSURL URLByResolvingBookmarkData:bookmark.toNSData() options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error];
|
|
if (!url) return;
|
|
|
|
if ([url startAccessingSecurityScopedResource]) {
|
|
if (_downloadPathUrl) {
|
|
[_downloadPathUrl stopAccessingSecurityScopedResource];
|
|
}
|
|
_downloadPathUrl = [url retain];
|
|
|
|
Core::App().settings().setDownloadPath(NS2QString([_downloadPathUrl path]) + '/');
|
|
if (isStale) {
|
|
NSData *data = [_downloadPathUrl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error];
|
|
if (data) {
|
|
Core::App().settings().setDownloadPathBookmark(QByteArray::fromNSData(data));
|
|
Local::writeSettings();
|
|
}
|
|
}
|
|
}
|
|
#endif // OS_MAC_STORE
|
|
}
|