summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-09-21 09:19:21 +0300
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-09-21 09:19:21 +0300
commite48a9a27bd11dbef9531bd12d3c0c60cc771b2c3 (patch)
tree0044e6de2a788c061f0fe0eb239ab96d22acadd4
parentba6d544b1237aea6901cf5b4f6562db9eb0f46c4 (diff)
macOS: Native context menus
Popup context menus now use NSMenu. There are still has a few glitches with the navbar identity button, but most menus are working. SDL required another little tweak to force it to update mouse button state after the synchronously handled context menu goes away. Otherwise SDL's internal mouse button state shows that the right mouse button is held down.
-rw-r--r--sdl2-macos-ios.diff70
-rw-r--r--src/app.h2
-rw-r--r--src/macos.h4
-rw-r--r--src/macos.m161
-rw-r--r--src/ui/inputwidget.c4
-rw-r--r--src/ui/labelwidget.c15
-rw-r--r--src/ui/root.c1
-rw-r--r--src/ui/sidebarwidget.c37
-rw-r--r--src/ui/util.c122
-rw-r--r--src/ui/util.h14
-rw-r--r--src/ui/widget.c3
-rw-r--r--src/ui/widget.h1
12 files changed, 325 insertions, 109 deletions
diff --git a/sdl2-macos-ios.diff b/sdl2-macos-ios.diff
index 04d0f2e5..ecb23824 100644
--- a/sdl2-macos-ios.diff
+++ b/sdl2-macos-ios.diff
@@ -1,8 +1,8 @@
1Only in SDL2-2.0.14/src: .DS_Store 1diff --git a/src/events/SDL_mouse.c b/src/events/SDL_mouse.c
2diff -ru SDL2-2.0.14-original/src/events/SDL_mouse.c SDL2-2.0.14/src/events/SDL_mouse.c 2index a776bcca8..2d5e685e3 100644
3--- SDL2-2.0.14-original/src/events/SDL_mouse.c 2020-12-21 19:44:36.000000000 +0200 3--- a/src/events/SDL_mouse.c
4+++ SDL2-2.0.14/src/events/SDL_mouse.c 2021-01-06 18:31:18.000000000 +0200 4+++ b/src/events/SDL_mouse.c
5@@ -647,8 +647,8 @@ 5@@ -647,8 +647,8 @@ SDL_SendMouseWheel(SDL_Window * window, SDL_MouseID mouseID, float x, float y, S
6 event.wheel.preciseX = x; 6 event.wheel.preciseX = x;
7 event.wheel.preciseY = y; 7 event.wheel.preciseY = y;
8 #endif 8 #endif
@@ -13,11 +13,11 @@ diff -ru SDL2-2.0.14-original/src/events/SDL_mouse.c SDL2-2.0.14/src/events/SDL_
13 event.wheel.direction = (Uint32)direction; 13 event.wheel.direction = (Uint32)direction;
14 posted = (SDL_PushEvent(&event) > 0); 14 posted = (SDL_PushEvent(&event) > 0);
15 } 15 }
16Only in SDL2-2.0.14/src/video: .DS_Store 16diff --git a/src/video/cocoa/SDL_cocoamouse.m b/src/video/cocoa/SDL_cocoamouse.m
17diff -ru SDL2-2.0.14-original/src/video/cocoa/SDL_cocoamouse.m SDL2-2.0.14/src/video/cocoa/SDL_cocoamouse.m 17index e9d832d64..4cfa3624b 100644
18--- SDL2-2.0.14-original/src/video/cocoa/SDL_cocoamouse.m 2020-12-21 19:44:36.000000000 +0200 18--- a/src/video/cocoa/SDL_cocoamouse.m
19+++ SDL2-2.0.14/src/video/cocoa/SDL_cocoamouse.m 2021-01-06 18:31:18.000000000 +0200 19+++ b/src/video/cocoa/SDL_cocoamouse.m
20@@ -423,10 +423,16 @@ 20@@ -463,10 +463,16 @@ + (NSCursor *)invisibleCursor
21 } 21 }
22 22
23 SDL_MouseID mouseID = mouse->mouseID; 23 SDL_MouseID mouseID = mouse->mouseID;
@@ -36,10 +36,41 @@ diff -ru SDL2-2.0.14-original/src/video/cocoa/SDL_cocoamouse.m SDL2-2.0.14/src/v
36 if ([event respondsToSelector:@selector(isDirectionInvertedFromDevice)]) { 36 if ([event respondsToSelector:@selector(isDirectionInvertedFromDevice)]) {
37 if ([event isDirectionInvertedFromDevice] == YES) { 37 if ([event isDirectionInvertedFromDevice] == YES) {
38 direction = SDL_MOUSEWHEEL_FLIPPED; 38 direction = SDL_MOUSEWHEEL_FLIPPED;
39Only in SDL2-2.0.14/src/video/cocoa: SDL_cocoamouse.m.orig 39diff --git a/src/video/cocoa/SDL_cocoawindow.h b/src/video/cocoa/SDL_cocoawindow.h
40diff -ru SDL2-2.0.14-original/src/video/uikit/SDL_uikitviewcontroller.h SDL2-2.0.14/src/video/uikit/SDL_uikitviewcontroller.h 40index 37bec665e..5e3a3995f 100644
41--- SDL2-2.0.14-original/src/video/uikit/SDL_uikitviewcontroller.h 2020-12-21 19:44:36.000000000 +0200 41--- a/src/video/cocoa/SDL_cocoawindow.h
42+++ SDL2-2.0.14/src/video/uikit/SDL_uikitviewcontroller.h 2021-05-17 13:11:13.000000000 +0300 42+++ b/src/video/cocoa/SDL_cocoawindow.h
43@@ -109,6 +109,8 @@ typedef enum
44 /* Touch event handling */
45 -(void) handleTouches:(NSTouchPhase) phase withEvent:(NSEvent*) theEvent;
46
47+-(void) syncMouseButtonState;
48+
49 @end
50 /* *INDENT-ON* */
51
52diff --git a/src/video/cocoa/SDL_cocoawindow.m b/src/video/cocoa/SDL_cocoawindow.m
53index 7a1446f09..86db35600 100644
54--- a/src/video/cocoa/SDL_cocoawindow.m
55+++ b/src/video/cocoa/SDL_cocoawindow.m
56@@ -1073,6 +1073,13 @@ - (void)otherMouseDown:(NSEvent *)theEvent
57 [self mouseDown:theEvent];
58 }
59
60+- (void)syncMouseButtonState {
61+ SDL_Mouse *mouse = SDL_GetMouse();
62+ if (mouse) {
63+ mouse->buttonstate = SDL_GetGlobalMouseState(NULL, NULL);
64+ }
65+}
66+
67 - (void)mouseUp:(NSEvent *)theEvent
68 {
69 const SDL_Mouse *mouse = SDL_GetMouse();
70diff --git a/src/video/uikit/SDL_uikitviewcontroller.h b/src/video/uikit/SDL_uikitviewcontroller.h
71index f7f4c9de6..50c72aad0 100644
72--- a/src/video/uikit/SDL_uikitviewcontroller.h
73+++ b/src/video/uikit/SDL_uikitviewcontroller.h
43@@ -58,10 +58,13 @@ 74@@ -58,10 +58,13 @@
44 #if !TARGET_OS_TV 75 #if !TARGET_OS_TV
45 - (NSUInteger)supportedInterfaceOrientations; 76 - (NSUInteger)supportedInterfaceOrientations;
@@ -54,10 +85,11 @@ diff -ru SDL2-2.0.14-original/src/video/uikit/SDL_uikitviewcontroller.h SDL2-2.0
54 #endif 85 #endif
55 86
56 #if SDL_IPHONE_KEYBOARD 87 #if SDL_IPHONE_KEYBOARD
57diff -ru SDL2-2.0.14-original/src/video/uikit/SDL_uikitviewcontroller.m SDL2-2.0.14/src/video/uikit/SDL_uikitviewcontroller.m 88diff --git a/src/video/uikit/SDL_uikitviewcontroller.m b/src/video/uikit/SDL_uikitviewcontroller.m
58--- SDL2-2.0.14-original/src/video/uikit/SDL_uikitviewcontroller.m 2020-12-21 19:44:36.000000000 +0200 89index c51d1aed2..cd8db9517 100644
59+++ SDL2-2.0.14/src/video/uikit/SDL_uikitviewcontroller.m 2021-05-17 13:11:58.000000000 +0300 90--- a/src/video/uikit/SDL_uikitviewcontroller.m
60@@ -104,6 +104,7 @@ 91+++ b/src/video/uikit/SDL_uikitviewcontroller.m
92@@ -105,6 +105,7 @@ - (instancetype)initWithSDLWindow:(SDL_Window *)_window
61 #endif 93 #endif
62 94
63 #if !TARGET_OS_TV 95 #if !TARGET_OS_TV
@@ -65,7 +97,7 @@ diff -ru SDL2-2.0.14-original/src/video/uikit/SDL_uikitviewcontroller.m SDL2-2.0
65 SDL_AddHintCallback(SDL_HINT_IOS_HIDE_HOME_INDICATOR, 97 SDL_AddHintCallback(SDL_HINT_IOS_HIDE_HOME_INDICATOR,
66 SDL_HideHomeIndicatorHintChanged, 98 SDL_HideHomeIndicatorHintChanged,
67 (__bridge void *) self); 99 (__bridge void *) self);
68@@ -229,6 +230,17 @@ 100@@ -230,6 +231,17 @@ - (BOOL)prefersHomeIndicatorAutoHidden
69 return hidden; 101 return hidden;
70 } 102 }
71 103
diff --git a/src/app.h b/src/app.h
index 8966e8c7..0dff939f 100644
--- a/src/app.h
+++ b/src/app.h
@@ -129,7 +129,7 @@ iLocalDef void postCommandString_Root(iRoot *d, const iString *command) {
129 } 129 }
130} 130}
131iLocalDef void postCommand_App(const char *command) { 131iLocalDef void postCommand_App(const char *command) {
132 postCommandf_App(command); 132 postCommand_Root(NULL, command);
133} 133}
134 134
135iDocumentWidget * document_Command (const char *cmd); 135iDocumentWidget * document_Command (const char *cmd);
diff --git a/src/macos.h b/src/macos.h
index 20b95943..22a6dfff 100644
--- a/src/macos.h
+++ b/src/macos.h
@@ -24,7 +24,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
24 24
25#include "ui/util.h" 25#include "ui/util.h"
26 26
27iDeclareType(MenuItem)
27iDeclareType(Window) 28iDeclareType(Window)
29iDeclareType(Widget)
28 30
29/* Platform-specific functionality for macOS */ 31/* Platform-specific functionality for macOS */
30 32
@@ -40,3 +42,5 @@ void enableMenu_MacOS (const char *menuLabel, iBool enable);
40void enableMenuItem_MacOS (const char *menuItemCommand, iBool enable); 42void enableMenuItem_MacOS (const char *menuItemCommand, iBool enable);
41void enableMenuItemsByKey_MacOS (int key, int kmods, iBool enable); 43void enableMenuItemsByKey_MacOS (int key, int kmods, iBool enable);
42void handleCommand_MacOS (const char *cmd); 44void handleCommand_MacOS (const char *cmd);
45
46void showPopupMenu_MacOS (iWidget *source, iInt2 windowCoord, const iMenuItem *items, size_t n);
diff --git a/src/macos.m b/src/macos.m
index 298db0f8..cec53a7d 100644
--- a/src/macos.m
+++ b/src/macos.m
@@ -77,6 +77,14 @@ iBool shouldDefaultToMetalRenderer_MacOS(void) {
77 return ver.x > 10 || ver.y > 13;*/ 77 return ver.x > 10 || ver.y > 13;*/
78} 78}
79 79
80static 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
80/*----------------------------------------------------------------------------------------------*/ 88/*----------------------------------------------------------------------------------------------*/
81 89
82@interface CommandButton : NSCustomTouchBarItem { 90@interface CommandButton : NSCustomTouchBarItem {
@@ -146,11 +154,60 @@ iBool shouldDefaultToMetalRenderer_MacOS(void) {
146 154
147/*----------------------------------------------------------------------------------------------*/ 155/*----------------------------------------------------------------------------------------------*/
148 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
149@interface MyDelegate : NSResponder<NSApplicationDelegate, NSTouchBarDelegate> { 205@interface MyDelegate : NSResponder<NSApplicationDelegate, NSTouchBarDelegate> {
150 enum iTouchBarVariant touchBarVariant; 206 enum iTouchBarVariant touchBarVariant;
151 NSString *currentAppearanceName; 207 NSString *currentAppearanceName;
152 NSObject<NSApplicationDelegate> *sdlDelegate; 208 NSObject<NSApplicationDelegate> *sdlDelegate;
153 NSMutableDictionary<NSString *, NSString*> *menuCommands; 209 //NSMutableDictionary<NSString *, NSString*> *menuCommands;
210 MenuCommands *menuCommands;
154} 211}
155- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl; 212- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl;
156- (NSTouchBar *)makeTouchBar; 213- (NSTouchBar *)makeTouchBar;
@@ -165,7 +222,7 @@ iBool shouldDefaultToMetalRenderer_MacOS(void) {
165- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl { 222- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl {
166 [super init]; 223 [super init];
167 currentAppearanceName = nil; 224 currentAppearanceName = nil;
168 menuCommands = [[NSMutableDictionary<NSString *, NSString *> alloc] init]; 225 menuCommands = [[MenuCommands alloc] init];
169 touchBarVariant = default_TouchBarVariant; 226 touchBarVariant = default_TouchBarVariant;
170 sdlDelegate = sdl; 227 sdlDelegate = sdl;
171 return self; 228 return self;
@@ -182,6 +239,14 @@ iBool shouldDefaultToMetalRenderer_MacOS(void) {
182 self.touchBar = nil; 239 self.touchBar = nil;
183} 240}
184 241
242- (MenuCommands *)menuCommands {
243 return menuCommands;
244}
245
246- (void)postMenuItemCommand:(id)sender {
247 [menuCommands postMenuItemCommand:sender];
248}
249
185static void appearanceChanged_MacOS_(NSString *name) { 250static void appearanceChanged_MacOS_(NSString *name) {
186 const iBool isDark = [name containsString:@"Dark"]; 251 const iBool isDark = [name containsString:@"Dark"];
187 const iBool isHighContrast = [name containsString:@"HighContrast"]; 252 const iBool isHighContrast = [name containsString:@"HighContrast"];
@@ -198,10 +263,6 @@ static void appearanceChanged_MacOS_(NSString *name) {
198 } 263 }
199} 264}
200 265
201- (void)setCommand:(NSString *)command forMenuItem:(NSMenuItem *)menuItem {
202 [menuCommands setObject:command forKey:[menuItem title]];
203}
204
205- (BOOL)application:(NSApplication *)app openFile:(NSString *)filename { 266- (BOOL)application:(NSApplication *)app openFile:(NSString *)filename {
206 return [sdlDelegate application:app openFile:filename]; 267 return [sdlDelegate application:app openFile:filename];
207} 268}
@@ -258,31 +319,11 @@ static void appearanceChanged_MacOS_(NSString *name) {
258 ignoreImmediateKeyDownEvents_(); 319 ignoreImmediateKeyDownEvents_();
259} 320}
260 321
261static void ignoreImmediateKeyDownEvents_(void) {
262 /* SDL ignores menu key equivalents so the keydown events will be posted regardless.
263 However, we shouldn't double-activate menu items when a shortcut key is used in our
264 widgets. Quite a kludge: take advantage of Window's focus-acquisition threshold to
265 ignore the immediately following key down events. */
266 get_Window()->focusGainedAt = SDL_GetTicks();
267}
268
269- (void)closeTab { 322- (void)closeTab {
270 postCommand_App("tabs.close"); 323 postCommand_App("tabs.close");
271 ignoreImmediateKeyDownEvents_(); 324 ignoreImmediateKeyDownEvents_();
272} 325}
273 326
274- (NSString *)commandForItem:(NSMenuItem *)menuItem {
275 return [menuCommands objectForKey:[menuItem title]];
276}
277
278- (void)postMenuItemCommand:(id)sender {
279 NSString *command = [menuCommands objectForKey:[(NSMenuItem *)sender title]];
280 if (command) {
281 postCommand_App([command cStringUsingEncoding:NSUTF8StringEncoding]);
282 ignoreImmediateKeyDownEvents_();
283 }
284}
285
286- (void)sidebarModePressed:(id)sender { 327- (void)sidebarModePressed:(id)sender {
287 NSSegmentedControl *seg = sender; 328 NSSegmentedControl *seg = sender;
288 postCommandf_App("sidebar.mode arg:%d toggle:1", (int) [seg selectedSegment]); 329 postCommandf_App("sidebar.mode arg:%d toggle:1", (int) [seg selectedSegment]);
@@ -403,7 +444,7 @@ void enableMenuItem_MacOS(const char *menuItemCommand, iBool enable) {
403 NSMenu *menu = mainMenuItem.submenu; 444 NSMenu *menu = mainMenuItem.submenu;
404 if (menu) { 445 if (menu) {
405 for (NSMenuItem *menuItem in menu.itemArray) { 446 for (NSMenuItem *menuItem in menu.itemArray) {
406 NSString *command = [myDel commandForItem:menuItem]; 447 NSString *command = [[myDel menuCommands] commandForMenuItem:menuItem];
407 if (command) { 448 if (command) {
408 if (!iCmpStr([command cStringUsingEncoding:NSUTF8StringEncoding], 449 if (!iCmpStr([command cStringUsingEncoding:NSUTF8StringEncoding],
409 menuItemCommand)) { 450 menuItemCommand)) {
@@ -483,18 +524,8 @@ void removeMenu_MacOS(int atIndex) {
483 [appMenu removeItemAtIndex:atIndex]; 524 [appMenu removeItemAtIndex:atIndex];
484} 525}
485 526
486void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *items, size_t count) { 527static void makeMenuItems_(NSMenu *menu, MenuCommands *commands, const iMenuItem *items, size_t n) {
487 NSApplication *app = [NSApplication sharedApplication]; 528 for (size_t i = 0; i < n && items[i].label; ++i) {
488 MyDelegate *myDel = (MyDelegate *) app.delegate;
489 NSMenu *appMenu = [app mainMenu];
490 menuLabel = translateCStr_Lang(menuLabel);
491 NSMenuItem *mainItem = [appMenu insertItemWithTitle:[NSString stringWithUTF8String:menuLabel]
492 action:nil
493 keyEquivalent:@""
494 atIndex:atIndex];
495 NSMenu *menu = [[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:menuLabel]];
496 [menu setAutoenablesItems:NO];
497 for (size_t i = 0; i < count; ++i) {
498 const char *label = translateCStr_Lang(items[i].label); 529 const char *label = translateCStr_Lang(items[i].label);
499 if (label[0] == '\v') { 530 if (label[0] == '\v') {
500 /* Skip the formatting escape. */ 531 /* Skip the formatting escape. */
@@ -505,13 +536,19 @@ void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *
505 } 536 }
506 else { 537 else {
507 const iBool hasCommand = (items[i].command && items[i].command[0]); 538 const iBool hasCommand = (items[i].command && items[i].command[0]);
508 NSMenuItem *item = [menu addItemWithTitle:[NSString stringWithUTF8String:label] 539 iString itemTitle;
540 initCStr_String(&itemTitle, label);
541 removeIconPrefix_String(&itemTitle);
542 NSMenuItem *item = [menu addItemWithTitle:[NSString stringWithUTF8String:cstr_String(&itemTitle)]
509 action:(hasCommand ? @selector(postMenuItemCommand:) : nil) 543 action:(hasCommand ? @selector(postMenuItemCommand:) : nil)
510 keyEquivalent:@""]; 544 keyEquivalent:@""];
545 deinit_String(&itemTitle);
546 [item setTarget:commands];
511 int key = items[i].key; 547 int key = items[i].key;
512 int kmods = items[i].kmods; 548 int kmods = items[i].kmods;
513 if (hasCommand) { 549 if (hasCommand) {
514 [myDel setCommand:[NSString stringWithUTF8String:items[i].command] forMenuItem:item]; 550 [commands setCommand:[NSString stringWithUTF8String:items[i].command]
551 forMenuItem:item];
515 /* Bindings may have a different key. */ 552 /* Bindings may have a different key. */
516 const iBinding *bind = findCommand_Keys(items[i].command); 553 const iBinding *bind = findCommand_Keys(items[i].command);
517 if (bind && bind->id < builtIn_BindingId) { 554 if (bind && bind->id < builtIn_BindingId) {
@@ -522,6 +559,20 @@ void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *
522 setShortcut_NSMenuItem_(item, key, kmods); 559 setShortcut_NSMenuItem_(item, key, kmods);
523 } 560 }
524 } 561 }
562}
563
564void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *items, size_t count) {
565 NSApplication *app = [NSApplication sharedApplication];
566 MyDelegate *myDel = (MyDelegate *) app.delegate;
567 NSMenu *appMenu = [app mainMenu];
568 menuLabel = translateCStr_Lang(menuLabel);
569 NSMenuItem *mainItem = [appMenu insertItemWithTitle:[NSString stringWithUTF8String:menuLabel]
570 action:nil
571 keyEquivalent:@""
572 atIndex:atIndex];
573 NSMenu *menu = [[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:menuLabel]];
574 [menu setAutoenablesItems:NO];
575 makeMenuItems_(menu, [myDel menuCommands], items, count);
525 [mainItem setSubmenu:menu]; 576 [mainItem setSubmenu:menu];
526 [menu release]; 577 [menu release];
527} 578}
@@ -542,7 +593,7 @@ void handleCommand_MacOS(const char *cmd) {
542 if (menu) { 593 if (menu) {
543 int itemIndex = 0; 594 int itemIndex = 0;
544 for (NSMenuItem *menuItem in menu.itemArray) { 595 for (NSMenuItem *menuItem in menu.itemArray) {
545 NSString *command = [myDel commandForItem:menuItem]; 596 NSString *command = [[myDel menuCommands] commandForMenuItem:menuItem];
546 if (!command && mainIndex == 6 && itemIndex == 0) { 597 if (!command && mainIndex == 6 && itemIndex == 0) {
547 /* Window > Close */ 598 /* Window > Close */
548 command = @"tabs.close"; 599 command = @"tabs.close";
@@ -568,3 +619,29 @@ void handleCommand_MacOS(const char *cmd) {
568void log_MacOS(const char *msg) { 619void log_MacOS(const char *msg) {
569 NSLog(@"%s", msg); 620 NSLog(@"%s", msg);
570} 621}
622
623void showPopupMenu_MacOS(iWidget *source, iInt2 windowCoord, const iMenuItem *items, size_t n) {
624 NSMenu * menu = [[NSMenu alloc] init];
625 MenuCommands *menuCommands = [[MenuCommands alloc] init];
626 iWindow * window = as_Window(mainWindow_App());
627 NSWindow * nsWindow = nsWindow_(window->win);
628 /* View coordinates are flipped. */
629 windowCoord.y = window->size.y - windowCoord.y;
630 windowCoord = divf_I2(windowCoord, window->pixelRatio);
631 NSPoint screenPoint = [nsWindow convertPointToScreen:(CGPoint){ windowCoord.x, windowCoord.y }];
632 makeMenuItems_(menu, menuCommands, items, n);
633 [menuCommands setSource:source];
634 [menu popUpMenuPositioningItem:nil atLocation:screenPoint inView:nil];
635 [menu release];
636 [menuCommands release];
637 /* The right mouse button has now been released so let SDL know about it. The button up event
638 was consumed by the popup menu so it got never passed to SDL. */
639 SEL sel = NSSelectorFromString(@"syncMouseButtonState"); /* custom method */
640 if ([[nsWindow delegate] respondsToSelector:sel]) {
641 NSInvocation *call = [NSInvocation invocationWithMethodSignature:
642 [NSMethodSignature signatureWithObjCTypes:"v@:"]];
643 [call setSelector:sel];
644 [call invokeWithTarget:[nsWindow delegate]];
645 }
646}
647
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index f02bf408..37d655b7 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -2365,8 +2365,8 @@ static void draw_InputWidget_(const iInputWidget *d) {
2365 cursorChar.start = charPos_InputWidget_(d, d->cursor); 2365 cursorChar.start = charPos_InputWidget_(d, d->cursor);
2366 iChar ch = 0; 2366 iChar ch = 0;
2367 int n = decodeBytes_MultibyteChar(cursorChar.start, 2367 int n = decodeBytes_MultibyteChar(cursorChar.start,
2368 constEnd_String(&constCursorLine_InputWidget_(d)->text), 2368 constEnd_String(&constCursorLine_InputWidget_(d)->text),
2369 &ch); 2369 &ch);
2370 cursorChar.end = cursorChar.start + iMax(n, 0); 2370 cursorChar.end = cursorChar.start + iMax(n, 0);
2371 if (ch) { 2371 if (ch) {
2372 if (d->inFlags & isSensitive_InputWidgetFlag) { 2372 if (d->inFlags & isSensitive_InputWidgetFlag) {
diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c
index 30eb1d36..c8748efa 100644
--- a/src/ui/labelwidget.c
+++ b/src/ui/labelwidget.c
@@ -611,19 +611,8 @@ iBool checkIcon_LabelWidget(iLabelWidget *d) {
611 d->icon = 0; 611 d->icon = 0;
612 return iFalse; 612 return iFalse;
613 } 613 }
614 iStringConstIterator iter; 614 d->icon = removeIconPrefix_String(&d->label);
615 init_StringConstIterator(&iter, &d->label); 615 return d->icon != 0;
616 const iChar icon = iter.value;
617 next_StringConstIterator(&iter);
618 if (iter.value == ' ' && icon >= 0x100) {
619 d->icon = icon;
620 remove_Block(&d->label.chars, 0, iter.next - constBegin_String(&d->label));
621 return iTrue;
622 }
623 else {
624 d->icon = 0;
625 }
626 return iFalse;
627} 616}
628 617
629iChar icon_LabelWidget(const iLabelWidget *d) { 618iChar icon_LabelWidget(const iLabelWidget *d) {
diff --git a/src/ui/root.c b/src/ui/root.c
index 9e290b05..595184cc 100644
--- a/src/ui/root.c
+++ b/src/ui/root.c
@@ -435,6 +435,7 @@ static void updateNavBarIdentity_(iWidget *navBar) {
435 setOutline_LabelWidget(toolButton, ident == NULL); 435 setOutline_LabelWidget(toolButton, ident == NULL);
436 /* Update menu. */ 436 /* Update menu. */
437 iLabelWidget *idItem = child_Widget(findChild_Widget(button, "menu"), 0); 437 iLabelWidget *idItem = child_Widget(findChild_Widget(button, "menu"), 0);
438 if (!idItem) return;
438 const iString *subjectName = ident ? name_GmIdentity(ident) : NULL; 439 const iString *subjectName = ident ? name_GmIdentity(ident) : NULL;
439 setTextCStr_LabelWidget( 440 setTextCStr_LabelWidget(
440 idItem, 441 idItem,
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index fe33c540..fdfb5300 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -1457,40 +1457,25 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1457 if (d->mode == bookmarks_SidebarMode && d->contextItem) { 1457 if (d->mode == bookmarks_SidebarMode && d->contextItem) {
1458 const iBookmark *bm = get_Bookmarks(bookmarks_App(), d->contextItem->id); 1458 const iBookmark *bm = get_Bookmarks(bookmarks_App(), d->contextItem->id);
1459 if (bm) { 1459 if (bm) {
1460 iLabelWidget *menuItem = findMenuItem_Widget(d->menu, 1460 updateMenuItemLabel_Widget(d->menu, "bookmark.tag tag:homepage",
1461 "bookmark.tag tag:homepage"); 1461 hasTag_Bookmark(bm, homepage_BookmarkTag)
1462 if (menuItem) { 1462 ? home_Icon " ${bookmark.untag.home}"
1463 setTextCStr_LabelWidget(menuItem, 1463 : home_Icon " ${bookmark.tag.home}");
1464 hasTag_Bookmark(bm, homepage_BookmarkTag) 1464 updateMenuItemLabel_Widget(d->menu, "bookmark.tag tag:subscribed",
1465 ? home_Icon " ${bookmark.untag.home}" 1465 hasTag_Bookmark(bm, subscribed_BookmarkTag)
1466 : home_Icon " ${bookmark.tag.home}");
1467 checkIcon_LabelWidget(menuItem);
1468 }
1469 menuItem = findMenuItem_Widget(d->menu, "bookmark.tag tag:subscribed");
1470 if (menuItem) {
1471 setTextCStr_LabelWidget(menuItem,
1472 hasTag_Bookmark(bm, subscribed_BookmarkTag)
1473 ? star_Icon " ${bookmark.untag.sub}" 1466 ? star_Icon " ${bookmark.untag.sub}"
1474 : star_Icon " ${bookmark.tag.sub}"); 1467 : star_Icon " ${bookmark.tag.sub}");
1475 checkIcon_LabelWidget(menuItem); 1468 updateMenuItemLabel_Widget(d->menu, "bookmark.tag tag:remotesource",
1476 } 1469 hasTag_Bookmark(bm, remoteSource_BookmarkTag)
1477 menuItem = findMenuItem_Widget(d->menu, "bookmark.tag tag:remotesource");
1478 if (menuItem) {
1479 setTextCStr_LabelWidget(menuItem,
1480 hasTag_Bookmark(bm, remoteSource_BookmarkTag)
1481 ? downArrowBar_Icon " ${bookmark.untag.remote}" 1470 ? downArrowBar_Icon " ${bookmark.untag.remote}"
1482 : downArrowBar_Icon " ${bookmark.tag.remote}"); 1471 : downArrowBar_Icon " ${bookmark.tag.remote}");
1483 checkIcon_LabelWidget(menuItem);
1484 }
1485 } 1472 }
1486 } 1473 }
1487 else if (d->mode == feeds_SidebarMode && d->contextItem) { 1474 else if (d->mode == feeds_SidebarMode && d->contextItem) {
1488 iLabelWidget *menuItem = findMenuItem_Widget(d->menu, "feed.entry.toggleread");
1489 const iBool isRead = d->contextItem->indent == 0; 1475 const iBool isRead = d->contextItem->indent == 0;
1490 setTextCStr_LabelWidget(menuItem, 1476 updateMenuItemLabel_Widget(d->menu, "feed.entry.toggleread",
1491 isRead ? circle_Icon " ${feeds.entry.markunread}" 1477 isRead ? circle_Icon " ${feeds.entry.markunread}"
1492 : circleWhite_Icon " ${feeds.entry.markread}"); 1478 : circleWhite_Icon " ${feeds.entry.markread}");
1493 checkIcon_LabelWidget(menuItem);
1494 } 1479 }
1495 else if (d->mode == identities_SidebarMode) { 1480 else if (d->mode == identities_SidebarMode) {
1496 const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d); 1481 const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d);
diff --git a/src/ui/util.c b/src/ui/util.c
index 38977b96..0baf541d 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -44,6 +44,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
44# include "../ios.h" 44# include "../ios.h"
45#endif 45#endif
46 46
47#if defined (iPlatformAppleDesktop)
48# include "macos.h"
49#endif
50
47#include <the_Foundation/math.h> 51#include <the_Foundation/math.h>
48#include <the_Foundation/path.h> 52#include <the_Foundation/path.h>
49#include <SDL_timer.h> 53#include <SDL_timer.h>
@@ -749,10 +753,65 @@ void makeMenuItems_Widget(iWidget *menu, const iMenuItem *items, size_t n) {
749 } 753 }
750 } 754 }
751 } 755 }
752 }} 756 }
757}
758
759static iArray *deepCopyMenuItems_(iWidget *menu, const iMenuItem *items, size_t n) {
760 iArray *array = new_Array(sizeof(iMenuItem));
761 iString cmd;
762 init_String(&cmd);
763 for (size_t i = 0; i < n; i++) {
764 const iMenuItem *item = &items[i];
765 const char *itemCommand = item->command;
766#if 0
767 if (itemCommand) {
768 /* Make it appear the command is coming from the right widget. */
769 setCStr_String(&cmd, itemCommand);
770 if (!hasLabel_Command(itemCommand, "ptr")) {
771 size_t firstSpace = indexOf_String(&cmd, ' ');
772 iBlock ptr;
773 init_Block(&ptr, 0);
774 printf_Block(&ptr, " ptr:%p", menu);
775 if (firstSpace != iInvalidPos) {
776 insertData_Block(&cmd.chars, firstSpace, data_Block(&ptr), size_Block(&ptr));
777 }
778 else {
779 append_Block(&cmd.chars, &ptr);
780 }
781 deinit_Block(&ptr);
782 }
783 itemCommand = cstr_String(&cmd);
784 }
785#endif
786 pushBack_Array(array, &(iMenuItem){
787 item->label ? strdup(item->label) : NULL,
788 item->key,
789 item->kmods,
790 itemCommand ? strdup(itemCommand) : NULL /* NOTE: Only works with string commands. */
791 });
792 }
793 deinit_String(&cmd);
794 return array;
795}
796
797static void deleteMenuItems_(iArray *items) {
798 iForEach(Array, i, items) {
799 iMenuItem *item = i.value;
800 free((void *) item->label);
801 free((void *) item->command);
802 }
803 delete_Array(items);
804}
753 805
754iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) { 806iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) {
755 iWidget *menu = new_Widget(); 807 iWidget *menu = new_Widget();
808#if defined (iHaveNativeMenus)
809 setFlags_Widget(menu, hidden_WidgetFlag | nativeMenu_WidgetFlag, iTrue);
810 setUserData_Object(menu, deepCopyMenuItems_(menu, items, n));
811 addChild_Widget(parent, menu);
812 iRelease(menu); /* owned by parent now */
813#else
814 /* Non-native custom popup menu. This may still be displayed inside a separate window. */
756 setDrawBufferEnabled_Widget(menu, iTrue); 815 setDrawBufferEnabled_Widget(menu, iTrue);
757 setBackgroundColor_Widget(menu, uiBackgroundMenu_ColorId); 816 setBackgroundColor_Widget(menu, uiBackgroundMenu_ColorId);
758 if (deviceType_App() != desktop_AppDeviceType) { 817 if (deviceType_App() != desktop_AppDeviceType) {
@@ -777,6 +836,7 @@ iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) {
777 iWidget *cancel = addAction_Widget(menu, SDLK_ESCAPE, 0, "cancel"); 836 iWidget *cancel = addAction_Widget(menu, SDLK_ESCAPE, 0, "cancel");
778 setId_Widget(cancel, "menu.cancel"); 837 setId_Widget(cancel, "menu.cancel");
779 setFlags_Widget(cancel, disabled_WidgetFlag, iTrue); 838 setFlags_Widget(cancel, disabled_WidgetFlag, iTrue);
839#endif
780 return menu; 840 return menu;
781} 841}
782 842
@@ -812,11 +872,52 @@ static void updateMenuItemFonts_Widget_(iWidget *d) {
812 } 872 }
813} 873}
814 874
875void updateMenuItemLabel_Widget(iWidget *menu, const char *command, const char *newLabel) {
876 if (~flags_Widget(menu) & nativeMenu_WidgetFlag) {
877 iLabelWidget *menuItem = findMenuItem_Widget(menu, command);
878 if (menuItem) {
879 setTextCStr_LabelWidget(menuItem, newLabel);
880 checkIcon_LabelWidget(menuItem);
881 }
882 }
883 else {
884 iArray *items = userData_Object(menu);
885 iAssert(items);
886 iForEach(Array, i, items) {
887 iMenuItem *item = i.value;
888 if (item->command && !iCmpStr(item->command, command)) {
889 free((void *) item->label);
890 item->label = strdup(newLabel);
891 break;
892 }
893 }
894 }
895}
896
815iLocalDef iBool isUsingMenuPopupWindows_(void) { 897iLocalDef iBool isUsingMenuPopupWindows_(void) {
816 return deviceType_App() == desktop_AppDeviceType; 898 return deviceType_App() == desktop_AppDeviceType;
817} 899}
818 900
901void releaseNativeMenu_Widget(iWidget *d) {
902#if defined (iHaveNativeMenus)
903 iArray *items = userData_Object(d);
904 iAssert(flags_Widget(d) & nativeMenu_WidgetFlag);
905 iAssert(items);
906 deleteMenuItems_(items);
907 setUserData_Object(d, NULL);
908#else
909 iUnused(d);
910#endif
911}
912
819void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) { 913void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) {
914#if defined (iHaveNativeMenus)
915 const iArray *items = userData_Object(d);
916 iAssert(flags_Widget(d) & nativeMenu_WidgetFlag);
917 iAssert(items);
918 showPopupMenu_MacOS(d, mouseCoord_Window(get_Window(), 0),
919 constData_Array(items), size_Array(items));
920#else
820 const iRect rootRect = rect_Root(d->root); 921 const iRect rootRect = rect_Root(d->root);
821 const iInt2 rootSize = rootRect.size; 922 const iInt2 rootSize = rootRect.size;
822 const iBool isPortraitPhone = (deviceType_App() == phone_AppDeviceType && isPortrait_App()); 923 const iBool isPortraitPhone = (deviceType_App() == phone_AppDeviceType && isPortrait_App());
@@ -904,9 +1005,13 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) {
904 postCommand_Widget(d, "menu.opened"); 1005 postCommand_Widget(d, "menu.opened");
905 } 1006 }
906 setupMenuTransition_Mobile(d, iTrue); 1007 setupMenuTransition_Mobile(d, iTrue);
1008#endif
907} 1009}
908 1010
909void closeMenu_Widget(iWidget *d) { 1011void closeMenu_Widget(iWidget *d) {
1012 if (flags_Widget(d) & nativeMenu_WidgetFlag) {
1013 return; /* Handled natively. */
1014 }
910 if (d == NULL || flags_Widget(d) & hidden_WidgetFlag) { 1015 if (d == NULL || flags_Widget(d) & hidden_WidgetFlag) {
911 return; /* Already closed. */ 1016 return; /* Already closed. */
912 } 1017 }
@@ -1780,6 +1885,21 @@ size_t findWidestLabel_MenuItem(const iMenuItem *items, size_t num) {
1780 return widestPos; 1885 return widestPos;
1781} 1886}
1782 1887
1888iChar removeIconPrefix_String(iString *d) {
1889 if (isEmpty_String(d)) {
1890 return 0;
1891 }
1892 iStringConstIterator iter;
1893 init_StringConstIterator(&iter, d);
1894 iChar icon = iter.value;
1895 next_StringConstIterator(&iter);
1896 if (iter.value == ' ' && icon >= 0x100) {
1897 remove_Block(&d->chars, 0, iter.next - constBegin_String(d));
1898 return icon;
1899 }
1900 return 0;
1901}
1902
1783iWidget *makeDialog_Widget(const char *id, 1903iWidget *makeDialog_Widget(const char *id,
1784 const iMenuItem *itemsNullTerminated, 1904 const iMenuItem *itemsNullTerminated,
1785 const iMenuItem *actions, size_t numActions) { 1905 const iMenuItem *actions, size_t numActions) {
diff --git a/src/ui/util.h b/src/ui/util.h
index 3dd4e153..d929143f 100644
--- a/src/ui/util.h
+++ b/src/ui/util.h
@@ -226,16 +226,20 @@ struct Impl_MenuItem {
226 }; 226 };
227}; 227};
228 228
229iWidget * makeMenu_Widget (iWidget *parent, const iMenuItem *items, size_t n); /* returns no ref */ 229iWidget * makeMenu_Widget (iWidget *parent, const iMenuItem *items, size_t n); /* returns no ref */
230void makeMenuItems_Widget(iWidget *menu, const iMenuItem *items, size_t n); 230void makeMenuItems_Widget (iWidget *menu, const iMenuItem *items, size_t n);
231void openMenu_Widget (iWidget *, iInt2 windowCoord); 231void openMenu_Widget (iWidget *, iInt2 windowCoord);
232void openMenuFlags_Widget(iWidget *, iInt2 windowCoord, iBool postCommands); 232void openMenuFlags_Widget (iWidget *, iInt2 windowCoord, iBool postCommands);
233void closeMenu_Widget (iWidget *); 233void closeMenu_Widget (iWidget *);
234void releaseNativeMenu_Widget(iWidget *);
234 235
235size_t findWidestLabel_MenuItem (const iMenuItem *items, size_t num); 236size_t findWidestLabel_MenuItem (const iMenuItem *items, size_t num);
236 237
238iChar removeIconPrefix_String (iString *);
239
237iLabelWidget * findMenuItem_Widget (iWidget *menu, const char *command); 240iLabelWidget * findMenuItem_Widget (iWidget *menu, const char *command);
238void setMenuItemDisabled_Widget (iWidget *menu, const char *command, iBool disable); 241void setMenuItemDisabled_Widget (iWidget *menu, const char *command, iBool disable);
242void updateMenuItemLabel_Widget (iWidget *menu, const char *command, const char *newLabel);
239 243
240int checkContextMenu_Widget (iWidget *, const SDL_Event *ev); /* see macro below */ 244int checkContextMenu_Widget (iWidget *, const SDL_Event *ev); /* see macro below */
241 245
diff --git a/src/ui/widget.c b/src/ui/widget.c
index 7b33a752..6b9ee11d 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -163,6 +163,9 @@ void deinit_Widget(iWidget *d) {
163 if (win->hover == d) { 163 if (win->hover == d) {
164 win->hover = NULL; 164 win->hover = NULL;
165 } 165 }
166 if (d->flags & nativeMenu_WidgetFlag) {
167 releaseNativeMenu_Widget(d);
168 }
166 widgetDestroyed_Touch(d); 169 widgetDestroyed_Touch(d);
167} 170}
168 171
diff --git a/src/ui/widget.h b/src/ui/widget.h
index 0eab69c1..9243c00a 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -121,6 +121,7 @@ enum iWidgetFlag {
121#define destroyPending_WidgetFlag iBit64(61) 121#define destroyPending_WidgetFlag iBit64(61)
122#define leftEdgeDraggable_WidgetFlag iBit64(62) 122#define leftEdgeDraggable_WidgetFlag iBit64(62)
123#define refChildrenOffset_WidgetFlag iBit64(63) /* visual offset determined by the offset of referenced children */ 123#define refChildrenOffset_WidgetFlag iBit64(63) /* visual offset determined by the offset of referenced children */
124#define nativeMenu_WidgetFlag iBit64(64)
124 125
125enum iWidgetAddPos { 126enum iWidgetAddPos {
126 back_WidgetAddPos, 127 back_WidgetAddPos,