1/* This file is part of Clementine.
2   Copyright 2010-2011, David Sansome <davidsansome@gmail.com>
3   Copyright 2010-2012, 2014, John Maguire <john.maguire@gmail.com>
4   Copyright 2011, Tyler Rhodes <tyler.s.rhodes@gmail.com>
5
6   Clementine is free software: you can redistribute it and/or modify
7   it under the terms of the GNU General Public License as published by
8   the Free Software Foundation, either version 3 of the License, or
9   (at your option) any later version.
10
11   Clementine is distributed in the hope that it will be useful,
12   but WITHOUT ANY WARRANTY; without even the implied warranty of
13   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14   GNU General Public License for more details.
15
16   You should have received a copy of the GNU General Public License
17   along with Clementine.  If not, see <http://www.gnu.org/licenses/>.
18*/
19
20#import <AppKit/NSApplication.h>
21#import <AppKit/NSEvent.h>
22#import <AppKit/NSGraphics.h>
23#import <AppKit/NSNibDeclarations.h>
24#import <AppKit/NSViewController.h>
25
26#import <Foundation/NSBundle.h>
27#import <Foundation/NSError.h>
28#import <Foundation/NSFileManager.h>
29#import <Foundation/NSPathUtilities.h>
30#import <Foundation/NSProcessInfo.h>
31#import <Foundation/NSThread.h>
32#import <Foundation/NSTimer.h>
33#import <Foundation/NSURL.h>
34
35#import <IOKit/hidsystem/ev_keymap.h>
36
37#import <Kernel/AvailabilityMacros.h>
38
39#import <QuartzCore/CALayer.h>
40
41#import "3rdparty/SPMediaKeyTap/SPMediaKeyTap.h"
42
43#include "config.h"
44#include "globalshortcuts.h"
45#include "mac_delegate.h"
46#include "mac_startup.h"
47#include "mac_utilities.h"
48#include "macglobalshortcutbackend.h"
49#include "utilities.h"
50#include "core/logging.h"
51#include "core/scoped_cftyperef.h"
52#include "core/scoped_nsautorelease_pool.h"
53
54#ifdef HAVE_SPARKLE
55#import <Sparkle/SUUpdater.h>
56#endif
57
58#include <QApplication>
59#include <QCoreApplication>
60#include <QDir>
61#include <QEvent>
62#include <QFile>
63#include <QSettings>
64#include <QWidget>
65
66#include <QtDebug>
67
68QDebug operator<<(QDebug dbg, NSObject* object) {
69  QString ns_format = [[NSString stringWithFormat:@"%@", object] UTF8String];
70  dbg.nospace() << ns_format;
71  return dbg.space();
72}
73
74// Capture global media keys on Mac (Cocoa only!)
75// See:
76// http://www.rogueamoeba.com/utm/2007/09/29/apple-keyboard-media-key-event-handling/
77
78@interface MacApplication : NSApplication {
79  PlatformInterface* application_handler_;
80  AppDelegate* delegate_;
81  // shortcut_handler_ only used to temporarily save it
82  // AppDelegate does all the heavy-shortcut-lifting
83  MacGlobalShortcutBackend* shortcut_handler_;
84}
85
86- (MacGlobalShortcutBackend*)shortcut_handler;
87- (void)SetShortcutHandler:(MacGlobalShortcutBackend*)handler;
88
89- (PlatformInterface*)application_handler;
90- (void)SetApplicationHandler:(PlatformInterface*)handler;
91
92@end
93
94#ifdef HAVE_BREAKPAD
95static bool BreakpadCallback(int, int, mach_port_t, void*) { return true; }
96
97static BreakpadRef InitBreakpad() {
98  ScopedNSAutoreleasePool pool;
99  BreakpadRef breakpad = nil;
100  NSDictionary* plist = [[NSBundle mainBundle] infoDictionary];
101  if (plist) {
102    breakpad = BreakpadCreate(plist);
103    BreakpadSetFilterCallback(breakpad, &BreakpadCallback, nullptr);
104  }
105  [pool release];
106  return breakpad;
107}
108#endif  // HAVE_BREAKPAD
109
110@implementation AppDelegate
111
112- (id)init {
113  if ((self = [super init])) {
114    application_handler_ = nil;
115    shortcut_handler_ = nil;
116    dock_menu_ = nil;
117  }
118  return self;
119}
120
121- (id)initWithHandler:(PlatformInterface*)handler {
122  application_handler_ = handler;
123
124#ifdef HAVE_BREAKPAD
125  breakpad_ = InitBreakpad();
126#endif
127
128  // Register defaults for the whitelist of apps that want to use media keys
129  [[NSUserDefaults standardUserDefaults]
130      registerDefaults:
131          [NSDictionary
132              dictionaryWithObjectsAndKeys:
133                  [SPMediaKeyTap defaultMediaKeyUserBundleIdentifiers],
134                  kMediaKeyUsingBundleIdentifiersDefaultsKey, nil]];
135  return self;
136}
137
138- (BOOL)applicationShouldHandleReopen:(NSApplication*)app
139                    hasVisibleWindows:(BOOL)flag {
140  if (application_handler_) {
141    application_handler_->Activate();
142  }
143  return YES;
144}
145
146- (void)setDockMenu:(NSMenu*)menu {
147  dock_menu_ = menu;
148}
149
150- (NSMenu*)applicationDockMenu:(NSApplication*)sender {
151  return dock_menu_;
152}
153
154- (void)setShortcutHandler:(MacGlobalShortcutBackend*)backend {
155  shortcut_handler_ = backend;
156}
157
158- (MacGlobalShortcutBackend*)shortcut_handler {
159  return shortcut_handler_;
160}
161
162- (void)applicationDidFinishLaunching:(NSNotification*)aNotification {
163  key_tap_ = [[SPMediaKeyTap alloc] initWithDelegate:self];
164  if ([SPMediaKeyTap usesGlobalMediaKeyTap] &&
165      ![[NSProcessInfo processInfo]
166          isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){
167                                              .majorVersion = 10,
168                                              .minorVersion = 12,
169                                              .patchVersion = 0}]) {
170    [key_tap_ startWatchingMediaKeys];
171  } else {
172    qLog(Warning) << "Media key monitoring disabled";
173  }
174}
175
176- (BOOL)application:(NSApplication*)app openFile:(NSString*)filename {
177  qLog(Debug) << "Wants to open:" << [filename UTF8String];
178
179  if (application_handler_->LoadUrl(QString::fromUtf8([filename UTF8String]))) {
180    return YES;
181  }
182
183  return NO;
184}
185
186- (void)application:(NSApplication*)app openFiles:(NSArray*)filenames {
187  qLog(Debug) << "Wants to open:" << filenames;
188  [filenames
189      enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL* stop) {
190          [self application:app openFile:(NSString*)object];
191      }];
192}
193
194- (void)mediaKeyTap:(SPMediaKeyTap*)keyTap
195    receivedMediaKeyEvent:(NSEvent*)event {
196  NSAssert([event type] == NSSystemDefined &&
197               [event subtype] == SPSystemDefinedEventMediaKeys,
198           @"Unexpected NSEvent in mediaKeyTap:receivedMediaKeyEvent:");
199
200  int key_code = (([event data1] & 0xFFFF0000) >> 16);
201  int key_flags = ([event data1] & 0x0000FFFF);
202  BOOL key_is_released = (((key_flags & 0xFF00) >> 8)) == 0xB;
203  // not used. keep just in case
204  //  int key_repeat = (key_flags & 0x1);
205
206  if (!shortcut_handler_) {
207    return;
208  }
209  if (key_is_released) {
210    shortcut_handler_->MacMediaKeyPressed(key_code);
211  }
212}
213
214- (NSApplicationTerminateReply)applicationShouldTerminate:
215                                   (NSApplication*)sender {
216#ifdef HAVE_BREAKPAD
217  BreakpadRelease(breakpad_);
218#endif
219  return NSTerminateNow;
220}
221
222- (BOOL)userNotificationCenter:(id)center
223     shouldPresentNotification:(id)notification {
224  // Always show notifications, even if Clementine is in the foreground.
225  return YES;
226}
227
228@end
229
230@implementation MacApplication
231
232- (id)init {
233  if ((self = [super init])) {
234    [self SetShortcutHandler:nil];
235  }
236  return self;
237}
238
239- (MacGlobalShortcutBackend*)shortcut_handler {
240  // should be the same as delegate_'s shortcut handler
241  return shortcut_handler_;
242}
243
244- (void)SetShortcutHandler:(MacGlobalShortcutBackend*)handler {
245  shortcut_handler_ = handler;
246  if (delegate_) [delegate_ setShortcutHandler:handler];
247}
248
249- (PlatformInterface*)application_handler {
250  return application_handler_;
251}
252
253- (void)SetApplicationHandler:(PlatformInterface*)handler {
254  delegate_ = [[AppDelegate alloc] initWithHandler:handler];
255  // App-shortcut-handler set before delegate is set.
256  // this makes sure the delegate's shortcut_handler is set
257  [delegate_ setShortcutHandler:shortcut_handler_];
258  [self setDelegate:delegate_];
259
260  [[NSUserNotificationCenter defaultUserNotificationCenter]
261      setDelegate:delegate_];
262}
263
264- (void)sendEvent:(NSEvent*)event {
265  // If event tap is not installed, handle events that reach the app instead
266  BOOL shouldHandleMediaKeyEventLocally =
267      ![SPMediaKeyTap usesGlobalMediaKeyTap];
268
269  if (shouldHandleMediaKeyEventLocally && [event type] == NSSystemDefined &&
270      [event subtype] == SPSystemDefinedEventMediaKeys) {
271    [(id)[self delegate] mediaKeyTap:nil receivedMediaKeyEvent:event];
272  }
273
274  [super sendEvent:event];
275}
276
277@end
278
279namespace mac {
280
281void MacMain() {
282  ScopedNSAutoreleasePool pool;
283  // Creates and sets the magic global variable so QApplication will find it.
284  [MacApplication sharedApplication];
285#ifdef HAVE_SPARKLE
286  // Creates and sets the magic global variable for Sparkle.
287  [[SUUpdater sharedUpdater] setDelegate:NSApp];
288#endif
289}
290
291void SetShortcutHandler(MacGlobalShortcutBackend* handler) {
292  [NSApp SetShortcutHandler:handler];
293}
294
295void SetApplicationHandler(PlatformInterface* handler) {
296  [NSApp SetApplicationHandler:handler];
297}
298
299void CheckForUpdates() {
300#ifdef HAVE_SPARKLE
301  [[SUUpdater sharedUpdater] checkForUpdates:NSApp];
302#endif
303}
304
305QString GetBundlePath() {
306  ScopedCFTypeRef<CFURLRef> app_url(
307      CFBundleCopyBundleURL(CFBundleGetMainBundle()));
308  ScopedCFTypeRef<CFStringRef> mac_path(
309      CFURLCopyFileSystemPath(app_url.get(), kCFURLPOSIXPathStyle));
310  const char* path =
311      CFStringGetCStringPtr(mac_path.get(), CFStringGetSystemEncoding());
312  QString bundle_path = QString::fromUtf8(path);
313  return bundle_path;
314}
315
316QString GetResourcesPath() {
317  QString bundle_path = GetBundlePath();
318  return bundle_path + "/Contents/Resources";
319}
320
321QString GetApplicationSupportPath() {
322  ScopedNSAutoreleasePool pool;
323  NSArray* paths = NSSearchPathForDirectoriesInDomains(
324      NSApplicationSupportDirectory, NSUserDomainMask, YES);
325  QString ret;
326  if ([paths count] > 0) {
327    NSString* user_path = [paths objectAtIndex:0];
328    ret = QString::fromUtf8([user_path UTF8String]);
329  } else {
330    ret = "~/Library/Application Support";
331  }
332  return ret;
333}
334
335QString GetMusicDirectory() {
336  ScopedNSAutoreleasePool pool;
337  NSArray* paths = NSSearchPathForDirectoriesInDomains(NSMusicDirectory,
338                                                       NSUserDomainMask, YES);
339  QString ret;
340  if ([paths count] > 0) {
341    NSString* user_path = [paths objectAtIndex:0];
342    ret = QString::fromUtf8([user_path UTF8String]);
343  } else {
344    ret = "~/Music";
345  }
346  return ret;
347}
348
349static int MapFunctionKey(int keycode) {
350  switch (keycode) {
351    // Function keys
352    case NSInsertFunctionKey:
353      return Qt::Key_Insert;
354    case NSDeleteFunctionKey:
355      return Qt::Key_Delete;
356    case NSPauseFunctionKey:
357      return Qt::Key_Pause;
358    case NSPrintFunctionKey:
359      return Qt::Key_Print;
360    case NSSysReqFunctionKey:
361      return Qt::Key_SysReq;
362    case NSHomeFunctionKey:
363      return Qt::Key_Home;
364    case NSEndFunctionKey:
365      return Qt::Key_End;
366    case NSLeftArrowFunctionKey:
367      return Qt::Key_Left;
368    case NSUpArrowFunctionKey:
369      return Qt::Key_Up;
370    case NSRightArrowFunctionKey:
371      return Qt::Key_Right;
372    case NSDownArrowFunctionKey:
373      return Qt::Key_Down;
374    case NSPageUpFunctionKey:
375      return Qt::Key_PageUp;
376    case NSPageDownFunctionKey:
377      return Qt::Key_PageDown;
378    case NSScrollLockFunctionKey:
379      return Qt::Key_ScrollLock;
380    case NSF1FunctionKey:
381      return Qt::Key_F1;
382    case NSF2FunctionKey:
383      return Qt::Key_F2;
384    case NSF3FunctionKey:
385      return Qt::Key_F3;
386    case NSF4FunctionKey:
387      return Qt::Key_F4;
388    case NSF5FunctionKey:
389      return Qt::Key_F5;
390    case NSF6FunctionKey:
391      return Qt::Key_F6;
392    case NSF7FunctionKey:
393      return Qt::Key_F7;
394    case NSF8FunctionKey:
395      return Qt::Key_F8;
396    case NSF9FunctionKey:
397      return Qt::Key_F9;
398    case NSF10FunctionKey:
399      return Qt::Key_F10;
400    case NSF11FunctionKey:
401      return Qt::Key_F11;
402    case NSF12FunctionKey:
403      return Qt::Key_F12;
404    case NSF13FunctionKey:
405      return Qt::Key_F13;
406    case NSF14FunctionKey:
407      return Qt::Key_F14;
408    case NSF15FunctionKey:
409      return Qt::Key_F15;
410    case NSF16FunctionKey:
411      return Qt::Key_F16;
412    case NSF17FunctionKey:
413      return Qt::Key_F17;
414    case NSF18FunctionKey:
415      return Qt::Key_F18;
416    case NSF19FunctionKey:
417      return Qt::Key_F19;
418    case NSF20FunctionKey:
419      return Qt::Key_F20;
420    case NSF21FunctionKey:
421      return Qt::Key_F21;
422    case NSF22FunctionKey:
423      return Qt::Key_F22;
424    case NSF23FunctionKey:
425      return Qt::Key_F23;
426    case NSF24FunctionKey:
427      return Qt::Key_F24;
428    case NSF25FunctionKey:
429      return Qt::Key_F25;
430    case NSF26FunctionKey:
431      return Qt::Key_F26;
432    case NSF27FunctionKey:
433      return Qt::Key_F27;
434    case NSF28FunctionKey:
435      return Qt::Key_F28;
436    case NSF29FunctionKey:
437      return Qt::Key_F29;
438    case NSF30FunctionKey:
439      return Qt::Key_F30;
440    case NSF31FunctionKey:
441      return Qt::Key_F31;
442    case NSF32FunctionKey:
443      return Qt::Key_F32;
444    case NSF33FunctionKey:
445      return Qt::Key_F33;
446    case NSF34FunctionKey:
447      return Qt::Key_F34;
448    case NSF35FunctionKey:
449      return Qt::Key_F35;
450    case NSMenuFunctionKey:
451      return Qt::Key_Menu;
452    case NSHelpFunctionKey:
453      return Qt::Key_Help;
454  }
455
456  return 0;
457}
458
459QKeySequence KeySequenceFromNSEvent(NSEvent* event) {
460  NSString* str = [event charactersIgnoringModifiers];
461  NSString* upper = [str uppercaseString];
462  const char* chars = [upper UTF8String];
463  NSUInteger modifiers = [event modifierFlags];
464  int key = 0;
465  unsigned char c = chars[0];
466  switch (c) {
467    case 0x1b:
468      key = Qt::Key_Escape;
469      break;
470    case 0x09:
471      key = Qt::Key_Tab;
472      break;
473    case 0x0d:
474      key = Qt::Key_Return;
475      break;
476    case 0x08:
477      key = Qt::Key_Backspace;
478      break;
479    case 0x03:
480      key = Qt::Key_Enter;
481      break;
482  }
483
484  if (key == 0) {
485    if (c >= 0x20 && c <= 0x7e) {  // ASCII from space to ~
486      key = c;
487    } else {
488      key = MapFunctionKey([event keyCode]);
489      if (key == 0) {
490        return QKeySequence();
491      }
492    }
493  }
494
495  if (modifiers & NSShiftKeyMask) {
496    key += Qt::SHIFT;
497  }
498  if (modifiers & NSControlKeyMask) {
499    key += Qt::META;
500  }
501  if (modifiers & NSAlternateKeyMask) {
502    key += Qt::ALT;
503  }
504  if (modifiers & NSCommandKeyMask) {
505    key += Qt::CTRL;
506  }
507
508  return QKeySequence(key);
509}
510
511void DumpDictionary(CFDictionaryRef dict) {
512  NSDictionary* d = (NSDictionary*)dict;
513  NSLog(@"%@", d);
514}
515
516// NSWindowCollectionBehaviorFullScreenPrimary
517static const NSUInteger kFullScreenPrimary = 1 << 7;
518
519void EnableFullScreen(const QWidget& main_window) {
520  NSView* view = reinterpret_cast<NSView*>(main_window.winId());
521  NSWindow* window = [view window];
522  [window setCollectionBehavior:kFullScreenPrimary];
523}
524
525float GetDevicePixelRatio(QWidget* widget) {
526  NSView* view = reinterpret_cast<NSView*>(widget->winId());
527  return [[view window] backingScaleFactor];
528}
529
530}  // namespace mac
531