Lagrange [work/v1.7]

macOS: Native context menus

=> e48a9a27bd11dbef9531bd12d3c0c60cc771b2c3

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 @@
-Only in SDL2-2.0.14/src: .DS_Store
-diff -ru SDL2-2.0.14-original/src/events/SDL_mouse.c SDL2-2.0.14/src/events/SDL_mouse.c
---- SDL2-2.0.14-original/src/events/SDL_mouse.c	2020-12-21 19:44:36.000000000 +0200
-+++ SDL2-2.0.14/src/events/SDL_mouse.c	2021-01-06 18:31:18.000000000 +0200
-@@ -647,8 +647,8 @@
+diff --git a/src/events/SDL_mouse.c b/src/events/SDL_mouse.c
+index a776bcca8..2d5e685e3 100644
+--- a/src/events/SDL_mouse.c
++++ b/src/events/SDL_mouse.c
+@@ -647,8 +647,8 @@ SDL_SendMouseWheel(SDL_Window * window, SDL_MouseID mouseID, float x, float y, S
          event.wheel.preciseX = x;
          event.wheel.preciseY = y;
  #endif
@@ -13,11 +13,11 @@ diff -ru SDL2-2.0.14-original/src/events/SDL_mouse.c SDL2-2.0.14/src/events/SDL_
          event.wheel.direction = (Uint32)direction;
          posted = (SDL_PushEvent(&event) > 0);
      }
-Only in SDL2-2.0.14/src/video: .DS_Store
-diff -ru SDL2-2.0.14-original/src/video/cocoa/SDL_cocoamouse.m SDL2-2.0.14/src/video/cocoa/SDL_cocoamouse.m
---- SDL2-2.0.14-original/src/video/cocoa/SDL_cocoamouse.m	2020-12-21 19:44:36.000000000 +0200
-+++ SDL2-2.0.14/src/video/cocoa/SDL_cocoamouse.m	2021-01-06 18:31:18.000000000 +0200
-@@ -423,10 +423,16 @@
+diff --git a/src/video/cocoa/SDL_cocoamouse.m b/src/video/cocoa/SDL_cocoamouse.m
+index e9d832d64..4cfa3624b 100644
+--- a/src/video/cocoa/SDL_cocoamouse.m
++++ b/src/video/cocoa/SDL_cocoamouse.m
+@@ -463,10 +463,16 @@ + (NSCursor *)invisibleCursor
      }
  
      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
      if ([event respondsToSelector:@selector(isDirectionInvertedFromDevice)]) {
          if ([event isDirectionInvertedFromDevice] == YES) {
              direction = SDL_MOUSEWHEEL_FLIPPED;
-Only in SDL2-2.0.14/src/video/cocoa: SDL_cocoamouse.m.orig
-diff -ru SDL2-2.0.14-original/src/video/uikit/SDL_uikitviewcontroller.h SDL2-2.0.14/src/video/uikit/SDL_uikitviewcontroller.h
---- SDL2-2.0.14-original/src/video/uikit/SDL_uikitviewcontroller.h	2020-12-21 19:44:36.000000000 +0200
-+++ SDL2-2.0.14/src/video/uikit/SDL_uikitviewcontroller.h	2021-05-17 13:11:13.000000000 +0300
+diff --git a/src/video/cocoa/SDL_cocoawindow.h b/src/video/cocoa/SDL_cocoawindow.h
+index 37bec665e..5e3a3995f 100644
+--- a/src/video/cocoa/SDL_cocoawindow.h
++++ b/src/video/cocoa/SDL_cocoawindow.h
+@@ -109,6 +109,8 @@ typedef enum
+ /* Touch event handling */
+ -(void) handleTouches:(NSTouchPhase) phase withEvent:(NSEvent*) theEvent;
+ 
++-(void) syncMouseButtonState;
++
+ @end
+ /* *INDENT-ON* */
+ 
+diff --git a/src/video/cocoa/SDL_cocoawindow.m b/src/video/cocoa/SDL_cocoawindow.m
+index 7a1446f09..86db35600 100644
+--- a/src/video/cocoa/SDL_cocoawindow.m
++++ b/src/video/cocoa/SDL_cocoawindow.m
+@@ -1073,6 +1073,13 @@ - (void)otherMouseDown:(NSEvent *)theEvent
+     [self mouseDown:theEvent];
+ }
+ 
++- (void)syncMouseButtonState {
++    SDL_Mouse *mouse = SDL_GetMouse();
++    if (mouse) {
++        mouse->buttonstate = SDL_GetGlobalMouseState(NULL, NULL);
++    }
++}
++
+ - (void)mouseUp:(NSEvent *)theEvent
+ {
+     const SDL_Mouse *mouse = SDL_GetMouse();
+diff --git a/src/video/uikit/SDL_uikitviewcontroller.h b/src/video/uikit/SDL_uikitviewcontroller.h
+index f7f4c9de6..50c72aad0 100644
+--- a/src/video/uikit/SDL_uikitviewcontroller.h
++++ b/src/video/uikit/SDL_uikitviewcontroller.h
 @@ -58,10 +58,13 @@
  #if !TARGET_OS_TV
  - (NSUInteger)supportedInterfaceOrientations;
@@ -54,10 +85,11 @@ diff -ru SDL2-2.0.14-original/src/video/uikit/SDL_uikitviewcontroller.h SDL2-2.0
  #endif
  
  #if SDL_IPHONE_KEYBOARD
-diff -ru SDL2-2.0.14-original/src/video/uikit/SDL_uikitviewcontroller.m SDL2-2.0.14/src/video/uikit/SDL_uikitviewcontroller.m
---- SDL2-2.0.14-original/src/video/uikit/SDL_uikitviewcontroller.m	2020-12-21 19:44:36.000000000 +0200
-+++ SDL2-2.0.14/src/video/uikit/SDL_uikitviewcontroller.m	2021-05-17 13:11:58.000000000 +0300
-@@ -104,6 +104,7 @@
+diff --git a/src/video/uikit/SDL_uikitviewcontroller.m b/src/video/uikit/SDL_uikitviewcontroller.m
+index c51d1aed2..cd8db9517 100644
+--- a/src/video/uikit/SDL_uikitviewcontroller.m
++++ b/src/video/uikit/SDL_uikitviewcontroller.m
+@@ -105,6 +105,7 @@ - (instancetype)initWithSDLWindow:(SDL_Window *)_window
  #endif
  
  #if !TARGET_OS_TV
@@ -65,7 +97,7 @@ diff -ru SDL2-2.0.14-original/src/video/uikit/SDL_uikitviewcontroller.m SDL2-2.0
          SDL_AddHintCallback(SDL_HINT_IOS_HIDE_HOME_INDICATOR,
                              SDL_HideHomeIndicatorHintChanged,
                              (__bridge void *) self);
-@@ -229,6 +230,17 @@
+@@ -230,6 +231,17 @@ - (BOOL)prefersHomeIndicatorAutoHidden
      return hidden;
  }
  
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) {
     }
 }
 iLocalDef void postCommand_App(const char *command) {
-    postCommandf_App(command);
+    postCommand_Root(NULL, command);
 }
 
 iDocumentWidget *   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. */
 
 #include "ui/util.h"
 
+iDeclareType(MenuItem)
 iDeclareType(Window)
+iDeclareType(Widget)
 
 /* Platform-specific functionality for macOS */
 
@@ -40,3 +42,5 @@ void    enableMenu_MacOS            (const char *menuLabel, iBool enable);
 void    enableMenuItem_MacOS        (const char *menuItemCommand, iBool enable);
 void    enableMenuItemsByKey_MacOS  (int key, int kmods, iBool enable);
 void    handleCommand_MacOS         (const char *cmd);
+
+void    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) {
     return ver.x > 10 || ver.y > 13;*/
 }
 
+static void ignoreImmediateKeyDownEvents_(void) {
+    /* SDL ignores menu key equivalents so the keydown events will be posted regardless.
+       However, we shouldn't double-activate menu items when a shortcut key is used in our
+       widgets. Quite a kludge: take advantage of Window's focus-acquisition threshold to
+       ignore the immediately following key down events. */
+    get_Window()->focusGainedAt = SDL_GetTicks();
+}
+
 /*----------------------------------------------------------------------------------------------*/
 
 @interface CommandButton : NSCustomTouchBarItem {
@@ -146,11 +154,60 @@ iBool shouldDefaultToMetalRenderer_MacOS(void) {
 
 /*----------------------------------------------------------------------------------------------*/
 
+@interface MenuCommands : NSObject {
+    NSMutableDictionary *commands;
+    iWidget *source;
+}
+@end
+
+@implementation MenuCommands
+
+- (id)init {
+    commands = [[NSMutableDictionary alloc] init];
+    source = NULL;
+    return self;
+}
+
+- (void)setCommand:(NSString *)command forMenuItem:(NSMenuItem *)menuItem {
+    [commands setObject:command forKey:[menuItem title]];
+}
+
+- (void)setSource:(iWidget *)widget {
+    source = widget;
+}
+
+- (void)clear {
+    [commands removeAllObjects];
+}
+
+- (NSString *)commandForMenuItem:(NSMenuItem *)menuItem {
+    return [commands objectForKey:[menuItem title]];
+}
+
+- (void)postMenuItemCommand:(id)sender {
+    NSString *command = [commands objectForKey:[(NSMenuItem *)sender title]];
+    if (command) {
+        const char *cstr = [command cStringUsingEncoding:NSUTF8StringEncoding];
+        if (source) {
+            postCommand_Widget(source, "%s", cstr);
+        }
+        else {
+            postCommand_Root(NULL, cstr);
+        }
+        ignoreImmediateKeyDownEvents_();
+    }
+}
+
+@end
+
+/*----------------------------------------------------------------------------------------------*/
+
 @interface MyDelegate : NSResponder {
     enum iTouchBarVariant touchBarVariant;
     NSString *currentAppearanceName;
     NSObject *sdlDelegate;
-    NSMutableDictionary *menuCommands;
+    //NSMutableDictionary *menuCommands;
+    MenuCommands *menuCommands;
 }
 - (id)initWithSDLDelegate:(NSObject *)sdl;
 - (NSTouchBar *)makeTouchBar;
@@ -165,7 +222,7 @@ iBool shouldDefaultToMetalRenderer_MacOS(void) {
 - (id)initWithSDLDelegate:(NSObject *)sdl {
     [super init];
     currentAppearanceName = nil;
-    menuCommands = [[NSMutableDictionary alloc] init];
+    menuCommands = [[MenuCommands alloc] init];
     touchBarVariant = default_TouchBarVariant;
     sdlDelegate = sdl;
     return self;
@@ -182,6 +239,14 @@ iBool shouldDefaultToMetalRenderer_MacOS(void) {
     self.touchBar = nil;
 }
 
+- (MenuCommands *)menuCommands {
+    return menuCommands;
+}
+
+- (void)postMenuItemCommand:(id)sender {
+    [menuCommands postMenuItemCommand:sender];
+}
+
 static void appearanceChanged_MacOS_(NSString *name) {
     const iBool isDark = [name containsString:@"Dark"];
     const iBool isHighContrast = [name containsString:@"HighContrast"];
@@ -198,10 +263,6 @@ static void appearanceChanged_MacOS_(NSString *name) {
     }
 }
 
-- (void)setCommand:(NSString *)command forMenuItem:(NSMenuItem *)menuItem {
-    [menuCommands setObject:command forKey:[menuItem title]];
-}
-
 - (BOOL)application:(NSApplication *)app openFile:(NSString *)filename {
     return [sdlDelegate application:app openFile:filename];
 }
@@ -258,31 +319,11 @@ static void appearanceChanged_MacOS_(NSString *name) {
     ignoreImmediateKeyDownEvents_();
 }
 
-static void ignoreImmediateKeyDownEvents_(void) {
-    /* SDL ignores menu key equivalents so the keydown events will be posted regardless.
-       However, we shouldn't double-activate menu items when a shortcut key is used in our
-       widgets. Quite a kludge: take advantage of Window's focus-acquisition threshold to
-       ignore the immediately following key down events. */
-    get_Window()->focusGainedAt = SDL_GetTicks();
-}
-
 - (void)closeTab {
     postCommand_App("tabs.close");
     ignoreImmediateKeyDownEvents_();
 }
 
-- (NSString *)commandForItem:(NSMenuItem *)menuItem {
-    return [menuCommands objectForKey:[menuItem title]];
-}
-
-- (void)postMenuItemCommand:(id)sender {
-    NSString *command = [menuCommands objectForKey:[(NSMenuItem *)sender title]];
-    if (command) {
-        postCommand_App([command cStringUsingEncoding:NSUTF8StringEncoding]);
-        ignoreImmediateKeyDownEvents_();
-    }
-}
-
 - (void)sidebarModePressed:(id)sender {
     NSSegmentedControl *seg = sender;
     postCommandf_App("sidebar.mode arg:%d toggle:1", (int) [seg selectedSegment]);
@@ -403,7 +444,7 @@ void enableMenuItem_MacOS(const char *menuItemCommand, iBool enable) {
         NSMenu *menu = mainMenuItem.submenu;
         if (menu) {
             for (NSMenuItem *menuItem in menu.itemArray) {
-                NSString *command = [myDel commandForItem:menuItem];
+                NSString *command = [[myDel menuCommands] commandForMenuItem:menuItem];
                 if (command) {
                     if (!iCmpStr([command cStringUsingEncoding:NSUTF8StringEncoding],
                                  menuItemCommand)) {
@@ -483,18 +524,8 @@ void removeMenu_MacOS(int atIndex) {
     [appMenu removeItemAtIndex:atIndex];
 }
 
-void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *items, size_t count) {
-    NSApplication *app = [NSApplication sharedApplication];
-    MyDelegate *myDel = (MyDelegate *) app.delegate;
-    NSMenu *appMenu = [app mainMenu];
-    menuLabel = translateCStr_Lang(menuLabel);
-    NSMenuItem *mainItem = [appMenu insertItemWithTitle:[NSString stringWithUTF8String:menuLabel]
-                                                 action:nil
-                                          keyEquivalent:@""
-                                                atIndex:atIndex];
-    NSMenu *menu = [[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:menuLabel]];
-    [menu setAutoenablesItems:NO];
-    for (size_t i = 0; i < count; ++i) {
+static void makeMenuItems_(NSMenu *menu, MenuCommands *commands, const iMenuItem *items, size_t n) {
+    for (size_t i = 0; i < n && items[i].label; ++i) {
         const char *label = translateCStr_Lang(items[i].label);
         if (label[0] == '\v') {
             /* Skip the formatting escape. */
@@ -505,13 +536,19 @@ void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *
         }
         else {
             const iBool hasCommand = (items[i].command && items[i].command[0]);
-            NSMenuItem *item = [menu addItemWithTitle:[NSString stringWithUTF8String:label]
+            iString itemTitle;
+            initCStr_String(&itemTitle, label);
+            removeIconPrefix_String(&itemTitle);
+            NSMenuItem *item = [menu addItemWithTitle:[NSString stringWithUTF8String:cstr_String(&itemTitle)]
                                                action:(hasCommand ? @selector(postMenuItemCommand:) : nil)
                                         keyEquivalent:@""];
+            deinit_String(&itemTitle);
+            [item setTarget:commands];
             int key   = items[i].key;
             int kmods = items[i].kmods;
             if (hasCommand) {
-                [myDel setCommand:[NSString stringWithUTF8String:items[i].command] forMenuItem:item];
+                [commands setCommand:[NSString stringWithUTF8String:items[i].command]
+                         forMenuItem:item];
                 /* Bindings may have a different key. */
                 const iBinding *bind = findCommand_Keys(items[i].command);
                 if (bind && bind->id < builtIn_BindingId) {
@@ -522,6 +559,20 @@ void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *
             setShortcut_NSMenuItem_(item, key, kmods);
         }
     }
+}
+
+void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *items, size_t count) {
+    NSApplication *app = [NSApplication sharedApplication];
+    MyDelegate *myDel = (MyDelegate *) app.delegate;
+    NSMenu *appMenu = [app mainMenu];
+    menuLabel = translateCStr_Lang(menuLabel);
+    NSMenuItem *mainItem = [appMenu insertItemWithTitle:[NSString stringWithUTF8String:menuLabel]
+                                                 action:nil
+                                          keyEquivalent:@""
+                                                atIndex:atIndex];
+    NSMenu *menu = [[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:menuLabel]];
+    [menu setAutoenablesItems:NO];
+    makeMenuItems_(menu, [myDel menuCommands], items, count);
     [mainItem setSubmenu:menu];
     [menu release];
 }
@@ -542,7 +593,7 @@ void handleCommand_MacOS(const char *cmd) {
             if (menu) {
                 int itemIndex = 0;
                 for (NSMenuItem *menuItem in menu.itemArray) {
-                    NSString *command = [myDel commandForItem:menuItem];
+                    NSString *command = [[myDel menuCommands] commandForMenuItem:menuItem];
                     if (!command && mainIndex == 6 && itemIndex == 0) {
                         /* Window > Close */
                         command = @"tabs.close";
@@ -568,3 +619,29 @@ void handleCommand_MacOS(const char *cmd) {
 void log_MacOS(const char *msg) {
     NSLog(@"%s", msg);
 }
+
+void showPopupMenu_MacOS(iWidget *source, iInt2 windowCoord, const iMenuItem *items, size_t n) {
+    NSMenu *      menu         = [[NSMenu alloc] init];
+    MenuCommands *menuCommands = [[MenuCommands alloc] init];
+    iWindow *     window       = as_Window(mainWindow_App());
+    NSWindow *    nsWindow     = nsWindow_(window->win);
+    /* View coordinates are flipped. */
+    windowCoord.y = window->size.y - windowCoord.y;
+    windowCoord = divf_I2(windowCoord, window->pixelRatio);
+    NSPoint screenPoint = [nsWindow convertPointToScreen:(CGPoint){ windowCoord.x, windowCoord.y }];
+    makeMenuItems_(menu, menuCommands, items, n);
+    [menuCommands setSource:source];
+    [menu popUpMenuPositioningItem:nil atLocation:screenPoint inView:nil];
+    [menu release];
+    [menuCommands release];
+    /* The right mouse button has now been released so let SDL know about it. The button up event
+       was consumed by the popup menu so it got never passed to SDL. */
+    SEL sel = NSSelectorFromString(@"syncMouseButtonState"); /* custom method */
+    if ([[nsWindow delegate] respondsToSelector:sel]) {
+        NSInvocation *call = [NSInvocation invocationWithMethodSignature:
+                              [NSMethodSignature signatureWithObjCTypes:"v@:"]];
+        [call setSelector:sel];
+        [call invokeWithTarget:[nsWindow delegate]];
+    }
+}
+
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) {
             cursorChar.start = charPos_InputWidget_(d, d->cursor);
             iChar ch = 0;
             int n = decodeBytes_MultibyteChar(cursorChar.start,
-                                      constEnd_String(&constCursorLine_InputWidget_(d)->text),
-                                      &ch);
+                                              constEnd_String(&constCursorLine_InputWidget_(d)->text),
+                                              &ch);
             cursorChar.end = cursorChar.start + iMax(n, 0);
             if (ch) {
                 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) {
         d->icon = 0;
         return iFalse;
     }
-    iStringConstIterator iter;
-    init_StringConstIterator(&iter, &d->label);
-    const iChar icon = iter.value;
-    next_StringConstIterator(&iter);
-    if (iter.value == ' ' && icon >= 0x100) {
-        d->icon = icon;
-        remove_Block(&d->label.chars, 0, iter.next - constBegin_String(&d->label));
-        return iTrue;
-    }
-    else {
-        d->icon = 0;
-    }
-    return iFalse;
+    d->icon = removeIconPrefix_String(&d->label);
+    return d->icon != 0;
 }
 
 iChar 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) {
     setOutline_LabelWidget(toolButton, ident == NULL);
     /* Update menu. */
     iLabelWidget *idItem = child_Widget(findChild_Widget(button, "menu"), 0);
+    if (!idItem) return;
     const iString *subjectName = ident ? name_GmIdentity(ident) : NULL;
     setTextCStr_LabelWidget(
         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)
                 if (d->mode == bookmarks_SidebarMode && d->contextItem) {
                     const iBookmark *bm = get_Bookmarks(bookmarks_App(), d->contextItem->id);
                     if (bm) {
-                        iLabelWidget *menuItem = findMenuItem_Widget(d->menu,
-                                                                     "bookmark.tag tag:homepage");
-                        if (menuItem) {
-                            setTextCStr_LabelWidget(menuItem,
-                                                    hasTag_Bookmark(bm, homepage_BookmarkTag)
-                                                        ? home_Icon " ${bookmark.untag.home}"
-                                                        : home_Icon " ${bookmark.tag.home}");
-                            checkIcon_LabelWidget(menuItem);
-                        }
-                        menuItem = findMenuItem_Widget(d->menu, "bookmark.tag tag:subscribed");
-                        if (menuItem) {
-                            setTextCStr_LabelWidget(menuItem,
-                                                    hasTag_Bookmark(bm, subscribed_BookmarkTag)
+                        updateMenuItemLabel_Widget(d->menu, "bookmark.tag tag:homepage",
+                                                   hasTag_Bookmark(bm, homepage_BookmarkTag)
+                                                       ? home_Icon " ${bookmark.untag.home}"
+                                                       : home_Icon " ${bookmark.tag.home}");
+                        updateMenuItemLabel_Widget(d->menu, "bookmark.tag tag:subscribed",
+                                                   hasTag_Bookmark(bm, subscribed_BookmarkTag)
                                                         ? star_Icon " ${bookmark.untag.sub}"
                                                         : star_Icon " ${bookmark.tag.sub}");
-                            checkIcon_LabelWidget(menuItem);
-                        }
-                        menuItem = findMenuItem_Widget(d->menu, "bookmark.tag tag:remotesource");
-                        if (menuItem) {
-                            setTextCStr_LabelWidget(menuItem,
-                                                    hasTag_Bookmark(bm, remoteSource_BookmarkTag)
+                        updateMenuItemLabel_Widget(d->menu, "bookmark.tag tag:remotesource",
+                                                   hasTag_Bookmark(bm, remoteSource_BookmarkTag)
                                                         ? downArrowBar_Icon " ${bookmark.untag.remote}"
                                                         : downArrowBar_Icon " ${bookmark.tag.remote}");
-                            checkIcon_LabelWidget(menuItem);
-                        }
                     }
                 }
                 else if (d->mode == feeds_SidebarMode && d->contextItem) {
-                    iLabelWidget *menuItem = findMenuItem_Widget(d->menu, "feed.entry.toggleread");
                     const iBool   isRead   = d->contextItem->indent == 0;
-                    setTextCStr_LabelWidget(menuItem,
-                                            isRead ? circle_Icon " ${feeds.entry.markunread}"
-                                                   : circleWhite_Icon " ${feeds.entry.markread}");
-                    checkIcon_LabelWidget(menuItem);
+                    updateMenuItemLabel_Widget(d->menu, "feed.entry.toggleread",
+                                               isRead ? circle_Icon " ${feeds.entry.markunread}"
+                                                      : circleWhite_Icon " ${feeds.entry.markread}");
                 }
                 else if (d->mode == identities_SidebarMode) {
                     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. */
 #   include "../ios.h"
 #endif
 
+#if defined (iPlatformAppleDesktop)
+#   include "macos.h"
+#endif
+
 #include 
 #include 
 #include 
@@ -749,10 +753,65 @@ void makeMenuItems_Widget(iWidget *menu, const iMenuItem *items, size_t n) {
                 }
             }
         }
-    }}
+    }
+}
+
+static iArray *deepCopyMenuItems_(iWidget *menu, const iMenuItem *items, size_t n) {
+    iArray *array = new_Array(sizeof(iMenuItem));
+    iString cmd;
+    init_String(&cmd);
+    for (size_t i = 0; i < n; i++) {
+        const iMenuItem *item = &items[i];
+        const char *itemCommand = item->command;
+#if 0
+        if (itemCommand) {
+            /* Make it appear the command is coming from the right widget. */
+            setCStr_String(&cmd, itemCommand);
+            if (!hasLabel_Command(itemCommand, "ptr")) {
+                size_t firstSpace = indexOf_String(&cmd, ' ');
+                iBlock ptr;
+                init_Block(&ptr, 0);
+                printf_Block(&ptr, " ptr:%p", menu);
+                if (firstSpace != iInvalidPos) {
+                    insertData_Block(&cmd.chars, firstSpace, data_Block(&ptr), size_Block(&ptr));
+                }
+                else {
+                    append_Block(&cmd.chars, &ptr);
+                }
+                deinit_Block(&ptr);
+            }
+            itemCommand = cstr_String(&cmd);
+        }
+#endif
+        pushBack_Array(array, &(iMenuItem){
+            item->label ? strdup(item->label) : NULL,
+            item->key,
+            item->kmods,
+            itemCommand ? strdup(itemCommand) : NULL /* NOTE: Only works with string commands. */
+        });
+    }
+    deinit_String(&cmd);
+    return array;
+}
+
+static void deleteMenuItems_(iArray *items) {
+    iForEach(Array, i, items) {
+        iMenuItem *item = i.value;
+        free((void *) item->label);
+        free((void *) item->command);
+    }
+    delete_Array(items);
+}
 
 iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) {
     iWidget *menu = new_Widget();
+#if defined (iHaveNativeMenus)
+    setFlags_Widget(menu, hidden_WidgetFlag | nativeMenu_WidgetFlag, iTrue);
+    setUserData_Object(menu, deepCopyMenuItems_(menu, items, n));
+    addChild_Widget(parent, menu);
+    iRelease(menu); /* owned by parent now */
+#else
+    /* Non-native custom popup menu. This may still be displayed inside a separate window. */
     setDrawBufferEnabled_Widget(menu, iTrue);
     setBackgroundColor_Widget(menu, uiBackgroundMenu_ColorId);
     if (deviceType_App() != desktop_AppDeviceType) {
@@ -777,6 +836,7 @@ iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) {
     iWidget *cancel = addAction_Widget(menu, SDLK_ESCAPE, 0, "cancel");
     setId_Widget(cancel, "menu.cancel");
     setFlags_Widget(cancel, disabled_WidgetFlag, iTrue);
+#endif
     return menu;
 }
 
@@ -812,11 +872,52 @@ static void updateMenuItemFonts_Widget_(iWidget *d) {
     }
 }
 
+void updateMenuItemLabel_Widget(iWidget *menu, const char *command, const char *newLabel) {
+    if (~flags_Widget(menu) & nativeMenu_WidgetFlag) {
+        iLabelWidget *menuItem = findMenuItem_Widget(menu, command);
+        if (menuItem) {
+            setTextCStr_LabelWidget(menuItem, newLabel);
+            checkIcon_LabelWidget(menuItem);
+        }
+    }
+    else {
+        iArray *items = userData_Object(menu);
+        iAssert(items);
+        iForEach(Array, i, items) {
+            iMenuItem *item = i.value;
+            if (item->command && !iCmpStr(item->command, command)) {
+                free((void *) item->label);
+                item->label = strdup(newLabel);
+                break;
+            }
+        }
+    }
+}
+
 iLocalDef iBool isUsingMenuPopupWindows_(void) {
     return deviceType_App() == desktop_AppDeviceType;
 }
 
+void releaseNativeMenu_Widget(iWidget *d) {
+#if defined (iHaveNativeMenus)
+    iArray *items = userData_Object(d);
+    iAssert(flags_Widget(d) & nativeMenu_WidgetFlag);
+    iAssert(items);
+    deleteMenuItems_(items);
+    setUserData_Object(d, NULL);
+#else
+    iUnused(d);
+#endif
+}
+
 void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) {
+#if defined (iHaveNativeMenus)
+    const iArray *items = userData_Object(d);
+    iAssert(flags_Widget(d) & nativeMenu_WidgetFlag);
+    iAssert(items);
+    showPopupMenu_MacOS(d, mouseCoord_Window(get_Window(), 0),
+                        constData_Array(items), size_Array(items));
+#else
     const iRect rootRect        = rect_Root(d->root);
     const iInt2 rootSize        = rootRect.size;
     const iBool isPortraitPhone = (deviceType_App() == phone_AppDeviceType && isPortrait_App());
@@ -904,9 +1005,13 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) {
         postCommand_Widget(d, "menu.opened");
     }
     setupMenuTransition_Mobile(d, iTrue);
+#endif
 }
 
 void closeMenu_Widget(iWidget *d) {
+    if (flags_Widget(d) & nativeMenu_WidgetFlag) {
+        return; /* Handled natively. */
+    }
     if (d == NULL || flags_Widget(d) & hidden_WidgetFlag) {
         return; /* Already closed. */
     }
@@ -1780,6 +1885,21 @@ size_t findWidestLabel_MenuItem(const iMenuItem *items, size_t num) {
     return widestPos;
 }
 
+iChar removeIconPrefix_String(iString *d) {
+    if (isEmpty_String(d)) {
+        return 0;
+    }
+    iStringConstIterator iter;
+    init_StringConstIterator(&iter, d);
+    iChar icon = iter.value;
+    next_StringConstIterator(&iter);
+    if (iter.value == ' ' && icon >= 0x100) {
+        remove_Block(&d->chars, 0, iter.next - constBegin_String(d));
+        return icon;
+    }
+    return 0;
+}
+
 iWidget *makeDialog_Widget(const char *id,
                            const iMenuItem *itemsNullTerminated,
                            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 {
     };
 };
 
-iWidget *   makeMenu_Widget     (iWidget *parent, const iMenuItem *items, size_t n); /* returns no ref */
-void        makeMenuItems_Widget(iWidget *menu, const iMenuItem *items, size_t n);
-void        openMenu_Widget     (iWidget *, iInt2 windowCoord);
-void        openMenuFlags_Widget(iWidget *, iInt2 windowCoord, iBool postCommands);
-void        closeMenu_Widget    (iWidget *);
+iWidget *   makeMenu_Widget         (iWidget *parent, const iMenuItem *items, size_t n); /* returns no ref */
+void        makeMenuItems_Widget    (iWidget *menu, const iMenuItem *items, size_t n);
+void        openMenu_Widget         (iWidget *, iInt2 windowCoord);
+void        openMenuFlags_Widget    (iWidget *, iInt2 windowCoord, iBool postCommands);
+void        closeMenu_Widget        (iWidget *);
+void        releaseNativeMenu_Widget(iWidget *);
 
 size_t      findWidestLabel_MenuItem        (const iMenuItem *items, size_t num);
 
+iChar       removeIconPrefix_String (iString *);
+
 iLabelWidget *  findMenuItem_Widget         (iWidget *menu, const char *command);
 void            setMenuItemDisabled_Widget  (iWidget *menu, const char *command, iBool disable);
+void            updateMenuItemLabel_Widget  (iWidget *menu, const char *command, const char *newLabel);
 
 int         checkContextMenu_Widget (iWidget *, const SDL_Event *ev); /* see macro below */
 
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) {
     if (win->hover == d) {
         win->hover = NULL;
     }
+    if (d->flags & nativeMenu_WidgetFlag) {
+        releaseNativeMenu_Widget(d);
+    }
     widgetDestroyed_Touch(d);
 }
 
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 {
 #define destroyPending_WidgetFlag           iBit64(61)
 #define leftEdgeDraggable_WidgetFlag        iBit64(62)
 #define refChildrenOffset_WidgetFlag        iBit64(63) /* visual offset determined by the offset of referenced children */
+#define nativeMenu_WidgetFlag               iBit64(64)
 
 enum iWidgetAddPos {
     back_WidgetAddPos,
Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/work%2Fv1.7/cdiff/e48a9a27bd11dbef9531bd12d3c0c60cc771b2c3
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
106.656298 milliseconds
Gemini-to-HTML Time
1.385029 milliseconds

This content has been proxied by September (ba2dc).