diff options
Diffstat (limited to 'src/macos.m')
-rw-r--r-- | src/macos.m | 224 |
1 files changed, 177 insertions, 47 deletions
diff --git a/src/macos.m b/src/macos.m index d588fa4a..53a6da00 100644 --- a/src/macos.m +++ b/src/macos.m | |||
@@ -30,6 +30,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
30 | #include "ui/window.h" | 30 | #include "ui/window.h" |
31 | 31 | ||
32 | #include <SDL_timer.h> | 32 | #include <SDL_timer.h> |
33 | #include <SDL_syswm.h> | ||
33 | 34 | ||
34 | #import <AppKit/AppKit.h> | 35 | #import <AppKit/AppKit.h> |
35 | 36 | ||
@@ -51,6 +52,16 @@ static iInt2 macVer_(void) { | |||
51 | return init_I2(10, 10); | 52 | return init_I2(10, 10); |
52 | } | 53 | } |
53 | 54 | ||
55 | static NSWindow *nsWindow_(SDL_Window *window) { | ||
56 | SDL_SysWMinfo wm; | ||
57 | SDL_VERSION(&wm.version); | ||
58 | if (SDL_GetWindowWMInfo(window, &wm)) { | ||
59 | return wm.info.cocoa.window; | ||
60 | } | ||
61 | iAssert(false); | ||
62 | return nil; | ||
63 | } | ||
64 | |||
54 | static NSString *currentSystemAppearance_(void) { | 65 | static NSString *currentSystemAppearance_(void) { |
55 | /* This API does not exist on 10.13. */ | 66 | /* This API does not exist on 10.13. */ |
56 | if ([NSApp respondsToSelector:@selector(effectiveAppearance)]) { | 67 | if ([NSApp respondsToSelector:@selector(effectiveAppearance)]) { |
@@ -66,6 +77,14 @@ iBool shouldDefaultToMetalRenderer_MacOS(void) { | |||
66 | return ver.x > 10 || ver.y > 13;*/ | 77 | return ver.x > 10 || ver.y > 13;*/ |
67 | } | 78 | } |
68 | 79 | ||
80 | static void ignoreImmediateKeyDownEvents_(void) { | ||
81 | /* SDL ignores menu key equivalents so the keydown events will be posted regardless. | ||
82 | However, we shouldn't double-activate menu items when a shortcut key is used in our | ||
83 | widgets. Quite a kludge: take advantage of Window's focus-acquisition threshold to | ||
84 | ignore the immediately following key down events. */ | ||
85 | get_Window()->focusGainedAt = SDL_GetTicks(); | ||
86 | } | ||
87 | |||
69 | /*----------------------------------------------------------------------------------------------*/ | 88 | /*----------------------------------------------------------------------------------------------*/ |
70 | 89 | ||
71 | @interface CommandButton : NSCustomTouchBarItem { | 90 | @interface CommandButton : NSCustomTouchBarItem { |
@@ -135,11 +154,60 @@ iBool shouldDefaultToMetalRenderer_MacOS(void) { | |||
135 | 154 | ||
136 | /*----------------------------------------------------------------------------------------------*/ | 155 | /*----------------------------------------------------------------------------------------------*/ |
137 | 156 | ||
157 | @interface MenuCommands : NSObject { | ||
158 | NSMutableDictionary<NSString *, NSString *> *commands; | ||
159 | iWidget *source; | ||
160 | } | ||
161 | @end | ||
162 | |||
163 | @implementation MenuCommands | ||
164 | |||
165 | - (id)init { | ||
166 | commands = [[NSMutableDictionary<NSString *, NSString *> alloc] init]; | ||
167 | source = NULL; | ||
168 | return self; | ||
169 | } | ||
170 | |||
171 | - (void)setCommand:(NSString *)command forMenuItem:(NSMenuItem *)menuItem { | ||
172 | [commands setObject:command forKey:[menuItem title]]; | ||
173 | } | ||
174 | |||
175 | - (void)setSource:(iWidget *)widget { | ||
176 | source = widget; | ||
177 | } | ||
178 | |||
179 | - (void)clear { | ||
180 | [commands removeAllObjects]; | ||
181 | } | ||
182 | |||
183 | - (NSString *)commandForMenuItem:(NSMenuItem *)menuItem { | ||
184 | return [commands objectForKey:[menuItem title]]; | ||
185 | } | ||
186 | |||
187 | - (void)postMenuItemCommand:(id)sender { | ||
188 | NSString *command = [commands objectForKey:[(NSMenuItem *)sender title]]; | ||
189 | if (command) { | ||
190 | const char *cstr = [command cStringUsingEncoding:NSUTF8StringEncoding]; | ||
191 | if (source) { | ||
192 | postCommand_Widget(source, "%s", cstr); | ||
193 | } | ||
194 | else { | ||
195 | postCommand_Root(NULL, cstr); | ||
196 | } | ||
197 | ignoreImmediateKeyDownEvents_(); | ||
198 | } | ||
199 | } | ||
200 | |||
201 | @end | ||
202 | |||
203 | /*----------------------------------------------------------------------------------------------*/ | ||
204 | |||
138 | @interface MyDelegate : NSResponder<NSApplicationDelegate, NSTouchBarDelegate> { | 205 | @interface MyDelegate : NSResponder<NSApplicationDelegate, NSTouchBarDelegate> { |
139 | enum iTouchBarVariant touchBarVariant; | 206 | enum iTouchBarVariant touchBarVariant; |
140 | NSString *currentAppearanceName; | 207 | NSString *currentAppearanceName; |
141 | NSObject<NSApplicationDelegate> *sdlDelegate; | 208 | NSObject<NSApplicationDelegate> *sdlDelegate; |
142 | NSMutableDictionary<NSString *, NSString*> *menuCommands; | 209 | //NSMutableDictionary<NSString *, NSString*> *menuCommands; |
210 | MenuCommands *menuCommands; | ||
143 | } | 211 | } |
144 | - (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl; | 212 | - (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl; |
145 | - (NSTouchBar *)makeTouchBar; | 213 | - (NSTouchBar *)makeTouchBar; |
@@ -154,7 +222,7 @@ iBool shouldDefaultToMetalRenderer_MacOS(void) { | |||
154 | - (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl { | 222 | - (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl { |
155 | [super init]; | 223 | [super init]; |
156 | currentAppearanceName = nil; | 224 | currentAppearanceName = nil; |
157 | menuCommands = [[NSMutableDictionary<NSString *, NSString *> alloc] init]; | 225 | menuCommands = [[MenuCommands alloc] init]; |
158 | touchBarVariant = default_TouchBarVariant; | 226 | touchBarVariant = default_TouchBarVariant; |
159 | sdlDelegate = sdl; | 227 | sdlDelegate = sdl; |
160 | return self; | 228 | return self; |
@@ -171,6 +239,14 @@ iBool shouldDefaultToMetalRenderer_MacOS(void) { | |||
171 | self.touchBar = nil; | 239 | self.touchBar = nil; |
172 | } | 240 | } |
173 | 241 | ||
242 | - (MenuCommands *)menuCommands { | ||
243 | return menuCommands; | ||
244 | } | ||
245 | |||
246 | - (void)postMenuItemCommand:(id)sender { | ||
247 | [menuCommands postMenuItemCommand:sender]; | ||
248 | } | ||
249 | |||
174 | static void appearanceChanged_MacOS_(NSString *name) { | 250 | static void appearanceChanged_MacOS_(NSString *name) { |
175 | const iBool isDark = [name containsString:@"Dark"]; | 251 | const iBool isDark = [name containsString:@"Dark"]; |
176 | const iBool isHighContrast = [name containsString:@"HighContrast"]; | 252 | const iBool isHighContrast = [name containsString:@"HighContrast"]; |
@@ -187,10 +263,6 @@ static void appearanceChanged_MacOS_(NSString *name) { | |||
187 | } | 263 | } |
188 | } | 264 | } |
189 | 265 | ||
190 | - (void)setCommand:(NSString *)command forMenuItem:(NSMenuItem *)menuItem { | ||
191 | [menuCommands setObject:command forKey:[menuItem title]]; | ||
192 | } | ||
193 | |||
194 | - (BOOL)application:(NSApplication *)app openFile:(NSString *)filename { | 266 | - (BOOL)application:(NSApplication *)app openFile:(NSString *)filename { |
195 | return [sdlDelegate application:app openFile:filename]; | 267 | return [sdlDelegate application:app openFile:filename]; |
196 | } | 268 | } |
@@ -247,31 +319,11 @@ static void appearanceChanged_MacOS_(NSString *name) { | |||
247 | ignoreImmediateKeyDownEvents_(); | 319 | ignoreImmediateKeyDownEvents_(); |
248 | } | 320 | } |
249 | 321 | ||
250 | static void ignoreImmediateKeyDownEvents_(void) { | ||
251 | /* SDL ignores menu key equivalents so the keydown events will be posted regardless. | ||
252 | However, we shouldn't double-activate menu items when a shortcut key is used in our | ||
253 | widgets. Quite a kludge: take advantage of Window's focus-acquisition threshold to | ||
254 | ignore the immediately following key down events. */ | ||
255 | get_Window()->focusGainedAt = SDL_GetTicks(); | ||
256 | } | ||
257 | |||
258 | - (void)closeTab { | 322 | - (void)closeTab { |
259 | postCommand_App("tabs.close"); | 323 | postCommand_App("tabs.close"); |
260 | ignoreImmediateKeyDownEvents_(); | 324 | ignoreImmediateKeyDownEvents_(); |
261 | } | 325 | } |
262 | 326 | ||
263 | - (NSString *)commandForItem:(NSMenuItem *)menuItem { | ||
264 | return [menuCommands objectForKey:[menuItem title]]; | ||
265 | } | ||
266 | |||
267 | - (void)postMenuItemCommand:(id)sender { | ||
268 | NSString *command = [menuCommands objectForKey:[(NSMenuItem *)sender title]]; | ||
269 | if (command) { | ||
270 | postCommand_App([command cStringUsingEncoding:NSUTF8StringEncoding]); | ||
271 | ignoreImmediateKeyDownEvents_(); | ||
272 | } | ||
273 | } | ||
274 | |||
275 | - (void)sidebarModePressed:(id)sender { | 327 | - (void)sidebarModePressed:(id)sender { |
276 | NSSegmentedControl *seg = sender; | 328 | NSSegmentedControl *seg = sender; |
277 | postCommandf_App("sidebar.mode arg:%d toggle:1", (int) [seg selectedSegment]); | 329 | postCommandf_App("sidebar.mode arg:%d toggle:1", (int) [seg selectedSegment]); |
@@ -370,6 +422,11 @@ void setupApplication_MacOS(void) { | |||
370 | windowCloseItem.action = @selector(closeTab); | 422 | windowCloseItem.action = @selector(closeTab); |
371 | } | 423 | } |
372 | 424 | ||
425 | void hideTitleBar_MacOS(iWindow *window) { | ||
426 | NSWindow *w = nsWindow_(window->win); | ||
427 | w.styleMask = 0; /* borderless */ | ||
428 | } | ||
429 | |||
373 | void enableMenu_MacOS(const char *menuLabel, iBool enable) { | 430 | void enableMenu_MacOS(const char *menuLabel, iBool enable) { |
374 | menuLabel = translateCStr_Lang(menuLabel); | 431 | menuLabel = translateCStr_Lang(menuLabel); |
375 | NSApplication *app = [NSApplication sharedApplication]; | 432 | NSApplication *app = [NSApplication sharedApplication]; |
@@ -377,7 +434,6 @@ void enableMenu_MacOS(const char *menuLabel, iBool enable) { | |||
377 | NSString *label = [NSString stringWithUTF8String:menuLabel]; | 434 | NSString *label = [NSString stringWithUTF8String:menuLabel]; |
378 | NSMenuItem *menuItem = [appMenu itemAtIndex:[appMenu indexOfItemWithTitle:label]]; | 435 | NSMenuItem *menuItem = [appMenu itemAtIndex:[appMenu indexOfItemWithTitle:label]]; |
379 | [menuItem setEnabled:enable]; | 436 | [menuItem setEnabled:enable]; |
380 | [label release]; | ||
381 | } | 437 | } |
382 | 438 | ||
383 | void enableMenuItem_MacOS(const char *menuItemCommand, iBool enable) { | 439 | void enableMenuItem_MacOS(const char *menuItemCommand, iBool enable) { |
@@ -388,7 +444,7 @@ void enableMenuItem_MacOS(const char *menuItemCommand, iBool enable) { | |||
388 | NSMenu *menu = mainMenuItem.submenu; | 444 | NSMenu *menu = mainMenuItem.submenu; |
389 | if (menu) { | 445 | if (menu) { |
390 | for (NSMenuItem *menuItem in menu.itemArray) { | 446 | for (NSMenuItem *menuItem in menu.itemArray) { |
391 | NSString *command = [myDel commandForItem:menuItem]; | 447 | NSString *command = [[myDel menuCommands] commandForMenuItem:menuItem]; |
392 | if (command) { | 448 | if (command) { |
393 | if (!iCmpStr([command cStringUsingEncoding:NSUTF8StringEncoding], | 449 | if (!iCmpStr([command cStringUsingEncoding:NSUTF8StringEncoding], |
394 | menuItemCommand)) { | 450 | menuItemCommand)) { |
@@ -468,35 +524,58 @@ void removeMenu_MacOS(int atIndex) { | |||
468 | [appMenu removeItemAtIndex:atIndex]; | 524 | [appMenu removeItemAtIndex:atIndex]; |
469 | } | 525 | } |
470 | 526 | ||
471 | void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *items, size_t count) { | 527 | enum iColorId removeColorEscapes_String(iString *d) { |
472 | NSApplication *app = [NSApplication sharedApplication]; | 528 | enum iColorId color = none_ColorId; |
473 | MyDelegate *myDel = (MyDelegate *) app.delegate; | 529 | for (;;) { |
474 | NSMenu *appMenu = [app mainMenu]; | 530 | const char *esc = strchr(cstr_String(d), '\v'); |
475 | menuLabel = translateCStr_Lang(menuLabel); | 531 | if (esc) { |
476 | NSMenuItem *mainItem = [appMenu insertItemWithTitle:[NSString stringWithUTF8String:menuLabel] | 532 | const char *endp; |
477 | action:nil | 533 | color = parseEscape_Color(esc, &endp); |
478 | keyEquivalent:@"" | 534 | remove_Block(&d->chars, esc - cstr_String(d), endp - esc); |
479 | atIndex:atIndex]; | ||
480 | NSMenu *menu = [[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:menuLabel]]; | ||
481 | [menu setAutoenablesItems:NO]; | ||
482 | for (size_t i = 0; i < count; ++i) { | ||
483 | const char *label = translateCStr_Lang(items[i].label); | ||
484 | if (label[0] == '\v') { | ||
485 | /* Skip the formatting escape. */ | ||
486 | label += 2; | ||
487 | } | 535 | } |
536 | else break; | ||
537 | } | ||
538 | return color; | ||
539 | } | ||
540 | |||
541 | static void makeMenuItems_(NSMenu *menu, MenuCommands *commands, const iMenuItem *items, size_t n) { | ||
542 | for (size_t i = 0; i < n && items[i].label; ++i) { | ||
543 | const char *label = translateCStr_Lang(items[i].label); | ||
488 | if (equal_CStr(label, "---")) { | 544 | if (equal_CStr(label, "---")) { |
489 | [menu addItem:[NSMenuItem separatorItem]]; | 545 | [menu addItem:[NSMenuItem separatorItem]]; |
490 | } | 546 | } |
491 | else { | 547 | else { |
492 | const iBool hasCommand = (items[i].command && items[i].command[0]); | 548 | const iBool hasCommand = (items[i].command && items[i].command[0]); |
493 | NSMenuItem *item = [menu addItemWithTitle:[NSString stringWithUTF8String:label] | 549 | iBool isChecked = iFalse; |
550 | iBool isDisabled = iFalse; | ||
551 | if (startsWith_CStr(label, "###")) { | ||
552 | isChecked = iTrue; | ||
553 | label += 3; | ||
554 | } | ||
555 | else if (startsWith_CStr(label, "///")) { | ||
556 | isDisabled = iTrue; | ||
557 | label += 3; | ||
558 | } | ||
559 | iString itemTitle; | ||
560 | initCStr_String(&itemTitle, label); | ||
561 | removeIconPrefix_String(&itemTitle); | ||
562 | if (removeColorEscapes_String(&itemTitle) == uiTextCaution_ColorId) { | ||
563 | // prependCStr_String(&itemTitle, "\u26a0\ufe0f "); | ||
564 | } | ||
565 | NSMenuItem *item = [menu addItemWithTitle:[NSString stringWithUTF8String:cstr_String(&itemTitle)] | ||
494 | action:(hasCommand ? @selector(postMenuItemCommand:) : nil) | 566 | action:(hasCommand ? @selector(postMenuItemCommand:) : nil) |
495 | keyEquivalent:@""]; | 567 | keyEquivalent:@""]; |
568 | deinit_String(&itemTitle); | ||
569 | [item setTarget:commands]; | ||
570 | if (isChecked) { | ||
571 | [item setState:NSControlStateValueOn]; | ||
572 | } | ||
573 | [item setEnabled:!isDisabled]; | ||
496 | int key = items[i].key; | 574 | int key = items[i].key; |
497 | int kmods = items[i].kmods; | 575 | int kmods = items[i].kmods; |
498 | if (hasCommand) { | 576 | if (hasCommand) { |
499 | [myDel setCommand:[NSString stringWithUTF8String:items[i].command] forMenuItem:item]; | 577 | [commands setCommand:[NSString stringWithUTF8String:items[i].command] |
578 | forMenuItem:item]; | ||
500 | /* Bindings may have a different key. */ | 579 | /* Bindings may have a different key. */ |
501 | const iBinding *bind = findCommand_Keys(items[i].command); | 580 | const iBinding *bind = findCommand_Keys(items[i].command); |
502 | if (bind && bind->id < builtIn_BindingId) { | 581 | if (bind && bind->id < builtIn_BindingId) { |
@@ -507,6 +586,20 @@ void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem * | |||
507 | setShortcut_NSMenuItem_(item, key, kmods); | 586 | setShortcut_NSMenuItem_(item, key, kmods); |
508 | } | 587 | } |
509 | } | 588 | } |
589 | } | ||
590 | |||
591 | void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *items, size_t count) { | ||
592 | NSApplication *app = [NSApplication sharedApplication]; | ||
593 | MyDelegate *myDel = (MyDelegate *) app.delegate; | ||
594 | NSMenu *appMenu = [app mainMenu]; | ||
595 | menuLabel = translateCStr_Lang(menuLabel); | ||
596 | NSMenuItem *mainItem = [appMenu insertItemWithTitle:[NSString stringWithUTF8String:menuLabel] | ||
597 | action:nil | ||
598 | keyEquivalent:@"" | ||
599 | atIndex:atIndex]; | ||
600 | NSMenu *menu = [[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:menuLabel]]; | ||
601 | [menu setAutoenablesItems:NO]; | ||
602 | makeMenuItems_(menu, [myDel menuCommands], items, count); | ||
510 | [mainItem setSubmenu:menu]; | 603 | [mainItem setSubmenu:menu]; |
511 | [menu release]; | 604 | [menu release]; |
512 | } | 605 | } |
@@ -527,7 +620,7 @@ void handleCommand_MacOS(const char *cmd) { | |||
527 | if (menu) { | 620 | if (menu) { |
528 | int itemIndex = 0; | 621 | int itemIndex = 0; |
529 | for (NSMenuItem *menuItem in menu.itemArray) { | 622 | for (NSMenuItem *menuItem in menu.itemArray) { |
530 | NSString *command = [myDel commandForItem:menuItem]; | 623 | NSString *command = [[myDel menuCommands] commandForMenuItem:menuItem]; |
531 | if (!command && mainIndex == 6 && itemIndex == 0) { | 624 | if (!command && mainIndex == 6 && itemIndex == 0) { |
532 | /* Window > Close */ | 625 | /* Window > Close */ |
533 | command = @"tabs.close"; | 626 | command = @"tabs.close"; |
@@ -553,3 +646,40 @@ void handleCommand_MacOS(const char *cmd) { | |||
553 | void log_MacOS(const char *msg) { | 646 | void log_MacOS(const char *msg) { |
554 | NSLog(@"%s", msg); | 647 | NSLog(@"%s", msg); |
555 | } | 648 | } |
649 | |||
650 | void showPopupMenu_MacOS(iWidget *source, iInt2 windowCoord, const iMenuItem *items, size_t n) { | ||
651 | NSMenu * menu = [[NSMenu alloc] init]; | ||
652 | MenuCommands *menuCommands = [[MenuCommands alloc] init]; | ||
653 | iWindow * window = as_Window(mainWindow_App()); | ||
654 | NSWindow * nsWindow = nsWindow_(window->win); | ||
655 | /* View coordinates are flipped. */ | ||
656 | iBool isCentered = iFalse; | ||
657 | if (isEqual_I2(windowCoord, zero_I2())) { | ||
658 | windowCoord = divi_I2(window->size, 2); | ||
659 | isCentered = iTrue; | ||
660 | } | ||
661 | windowCoord.y = window->size.y - windowCoord.y; | ||
662 | windowCoord = divf_I2(windowCoord, window->pixelRatio); | ||
663 | NSPoint screenPoint = [nsWindow convertPointToScreen:(CGPoint){ windowCoord.x, windowCoord.y }]; | ||
664 | makeMenuItems_(menu, menuCommands, items, n); | ||
665 | [menuCommands setSource:source]; | ||
666 | if (isCentered) { | ||
667 | NSSize menuSize = [menu size]; | ||
668 | screenPoint.x -= menuSize.width / 2; | ||
669 | screenPoint.y += menuSize.height / 2; | ||
670 | } | ||
671 | [menu setAutoenablesItems:NO]; | ||
672 | [menu popUpMenuPositioningItem:nil atLocation:screenPoint inView:nil]; | ||
673 | [menu release]; | ||
674 | [menuCommands release]; | ||
675 | /* The right mouse button has now been released so let SDL know about it. The button up event | ||
676 | was consumed by the popup menu so it got never passed to SDL. */ | ||
677 | SEL sel = NSSelectorFromString(@"syncMouseButtonState"); /* custom method */ | ||
678 | if ([[nsWindow delegate] respondsToSelector:sel]) { | ||
679 | NSInvocation *call = [NSInvocation invocationWithMethodSignature: | ||
680 | [NSMethodSignature signatureWithObjCTypes:"v@:"]]; | ||
681 | [call setSelector:sel]; | ||
682 | [call invokeWithTarget:[nsWindow delegate]]; | ||
683 | } | ||
684 | } | ||
685 | |||