add actions, implement action binding for menu items (GTK)

Fri, 17 Apr 2026 13:21:11 +0200

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Fri, 17 Apr 2026 13:21:11 +0200
changeset 1092
0accf125a65f
parent 1091
1524b5dc4d4d
child 1093
1686de34a489

add actions, implement action binding for menu items (GTK)

application/main.c file | annotate | diff | comparison | revisions
ui/common/action.c file | annotate | diff | comparison | revisions
ui/common/action.h file | annotate | diff | comparison | revisions
ui/common/context.c file | annotate | diff | comparison | revisions
ui/common/context.h file | annotate | diff | comparison | revisions
ui/common/document.c file | annotate | diff | comparison | revisions
ui/common/menu.c file | annotate | diff | comparison | revisions
ui/common/menu.h file | annotate | diff | comparison | revisions
ui/gtk/action.c file | annotate | diff | comparison | revisions
ui/gtk/action.h file | annotate | diff | comparison | revisions
ui/gtk/menu.c file | annotate | diff | comparison | revisions
ui/gtk/objs.mk file | annotate | diff | comparison | revisions
ui/gtk/toolkit.c file | annotate | diff | comparison | revisions
ui/gtk/toolkit.h file | annotate | diff | comparison | revisions
ui/ui/menu.h file | annotate | diff | comparison | revisions
ui/ui/toolkit.h file | annotate | diff | comparison | revisions
--- a/application/main.c	Thu Apr 16 17:41:35 2026 +0200
+++ b/application/main.c	Fri Apr 17 13:21:11 2026 +0200
@@ -309,9 +309,16 @@
     ui_show(obj);
 }
 
+static void mydoc_action_save(UiEvent *event, void *data) {
+    printf("mydoc action save\n");
+    
+}
+
+
 MyDocument* create_doc(void) {
     MyDocument *doc = ui_document_new(sizeof(MyDocument));
     UiContext *docctx = ui_document_context(doc);
+    ui_add_action(docctx, "save", mydoc_action_save, NULL);
     doc->submenulist = ui_list_new(docctx, "sub_menulist");
     ui_list_append(doc->submenulist, "Sub Menu List Item 1");
     ui_list_append(doc->submenulist, "Sub Menu List Item 2");
@@ -631,9 +638,14 @@
     ui_var_set_int(event->obj->ctx, "menu_radio", 5);
 }
 
+static void global_action_save(UiEvent *event, void *data) {
+    printf("global save\n");
+}
+
 void application_startup(UiEvent *event, void *data) {
     // global list
     UiContext *global = ui_global_context();
+    ui_add_action(global, "save", global_action_save, NULL);
     menu_list = ui_list_new(global, "menulist");
     ui_list_append(menu_list, "menu list item 1");
     ui_list_append(menu_list, "menu list item 2");
@@ -920,7 +932,7 @@
     ui_toolbar_appmenu() {
         ui_menuitem("New");
         ui_menuitem("Open");
-        ui_menuitem("Save");
+        ui_menuitem("Save", .action = "save");
         
         ui_menuseparator();
         ui_menu_radioitem(.label = "Option 1", .varname = "menu_radio", .onchange = action_menu_radio);
--- a/ui/common/action.c	Thu Apr 16 17:41:35 2026 +0200
+++ b/ui/common/action.c	Fri Apr 17 13:21:11 2026 +0200
@@ -49,6 +49,7 @@
     action.userdata = userdata;
     action.accelerator = accelerator ? ui_strdup(ctx, accelerator) : NULL;
     action.accelerator_text = accelerator_text ? ui_strdup(ctx, accelerator_text) : NULL;
+    action.ctx = ctx;
     cxMapPut(ctx->actions, name, &action);
     cxMapRehash(ctx->actions);
 }
@@ -57,8 +58,7 @@
         UiContext *ctx,
         const char *action,
         void *bind_obj,
-        ui_action_binding_set_enabled_func set_enabled,
-        ui_action_binding_set_accelerator_text_func set_accelerator_text)
+        ui_enablefunc set_enabled)
 {
     if(!action) {
         return;
@@ -68,6 +68,76 @@
     binding.action = ui_strdup(ctx, action);
     binding.userdata = bind_obj;
     binding.set_enabled = set_enabled;
-    binding.set_accelerator_text = set_accelerator_text;
     cxListAdd(ctx->action_bindings, &binding);
 }
+
+UiAction* uic_resolve_action(UiContext *ctx, const char *action) {
+    UiAction *a = NULL;
+    if(ctx->actions) {
+        a = cxMapGet(ctx->actions, action);
+    }
+    // check if any sub-document defines this action
+    // sub-document actions have precedence, the most specific action will
+    // be returned
+    CxIterator i = cxListIterator(ctx->documents);
+    cx_foreach(void *, doc, i) {
+        UiContext *doc_ctx = ui_document_context(doc);
+        UiAction *sub_action = uic_resolve_action(doc_ctx, action);
+        if(sub_action) {
+            a = sub_action;
+            // if one sub-tree has an action, we don't care about other
+            // subtrees
+            break;
+        }
+    }
+    
+    if(!a && ctx->parent) {
+        // check parents
+        a = uic_resolve_action_from_parents(ctx, action);
+    }
+    
+    return a;
+}
+
+UiAction* uic_resolve_action_from_parents(UiContext *ctx, const char *action) {
+    UiContext *parent = ctx->parent;
+    if(parent == NULL) {
+        return NULL;
+    }
+    if(parent->actions) {
+        UiAction *a = cxMapGet(parent->actions, action);
+        if(a) {
+            return a;
+        }
+    }
+    return uic_resolve_action_from_parents(parent, action);
+}
+
+
+
+void ui_update_action_bindings(UiContext *ctx) {
+    CxIterator i = cxListIterator(ctx->action_bindings);
+    cx_foreach(UiActionBinding*, binding, i) {
+        UiAction *action = uic_resolve_action(ctx, binding->action);
+        if(binding->set_enabled) {
+            binding->set_enabled(binding->userdata, action != NULL);
+        }
+    }
+}
+
+void uic_action_callback(UiEvent *event, const char *action_name) {
+    UiContext *ctx = ui_global_context();
+    if(event->obj) {
+        ctx = event->obj->ctx;
+    }
+    
+    UiAction *action = uic_resolve_action(ctx, action_name);
+    if(action) {
+        // override event document: for actions we know that the event is
+        // for a specific document
+        event->document = action->ctx->self_doc;
+        if(action->callback) {
+            action->callback(event, action->userdata);
+        }
+    }
+}
--- a/ui/common/action.h	Thu Apr 16 17:41:35 2026 +0200
+++ b/ui/common/action.h	Fri Apr 17 13:21:11 2026 +0200
@@ -44,14 +44,12 @@
     char *accelerator_text;
     ui_callback callback;
     void *userdata;
+    UiContext *ctx;
 };
 
-typedef void (*ui_action_binding_set_enabled_func)(void *bind_obj, UiBool enabled);
-typedef void (*ui_action_binding_set_accelerator_text_func)(void *bind_obj, const char *text);
 struct UiActionBinding {
     const char *action;
-    ui_action_binding_set_enabled_func set_enabled;
-    ui_action_binding_set_accelerator_text_func set_accelerator_text;
+    ui_enablefunc set_enabled;
     void *userdata;
 };
 
@@ -67,8 +65,13 @@
         UiContext *ctx,
         const char *action,
         void *bind_obj,
-        ui_action_binding_set_enabled_func set_enabled,
-        ui_action_binding_set_accelerator_text_func set_accelerator_text);
+        ui_enablefunc set_enabled);
+
+UiAction* uic_resolve_action(UiContext *ctx, const char *action);
+UiAction* uic_resolve_action_from_parents(UiContext *ctx, const char *action);
+
+// action event wrapper
+void uic_action_callback(UiEvent *event, const char *action_name);
 
 #ifdef __cplusplus
 }
--- a/ui/common/context.c	Thu Apr 16 17:41:35 2026 +0200
+++ b/ui/common/context.c	Fri Apr 17 13:21:11 2026 +0200
@@ -152,6 +152,8 @@
         
         var_ctx = var_ctx->parent;
     }
+    
+    ui_update_action_bindings(ctx);
 }
 
 static void uic_context_unbind_vars(UiContext *ctx) {
@@ -194,6 +196,8 @@
     UiContext *docctx = ui_document_context(document);
     uic_context_unbind_vars(docctx); // unbind all doc/subdoc vars from the parent
     docctx->parent = NULL;
+    
+    ui_update_action_bindings(ctx);
 }
 
 void uic_context_detach_all(UiContext *ctx) {
@@ -211,6 +215,7 @@
     }
     
     cxListFree(ls);
+    ui_update_action_bindings(ctx);
 }
 
 static UiVar* ctx_getvar(UiContext *ctx, CxHashKey key) {
--- a/ui/common/context.h	Thu Apr 16 17:41:35 2026 +0200
+++ b/ui/common/context.h	Fri Apr 17 13:21:11 2026 +0200
@@ -66,6 +66,9 @@
     const CxAllocator *allocator;
     CxList *destroy_handler;
     
+    // document pointer, if this is a document context
+    void          *self_doc;
+    
     void          *document;
     CxList        *documents;
     
--- a/ui/common/document.c	Thu Apr 16 17:41:35 2026 +0200
+++ b/ui/common/document.c	Fri Apr 17 13:21:11 2026 +0200
@@ -45,6 +45,7 @@
     
     UiDoc *document = cxCalloc(a, sizeof(UiDoc) + size, 1);
     document->ctx = ctx;
+    ctx->self_doc = document;
     return &document->doc;
 }
 
--- a/ui/common/menu.c	Thu Apr 16 17:41:35 2026 +0200
+++ b/ui/common/menu.c	Fri Apr 17 13:21:11 2026 +0200
@@ -148,6 +148,7 @@
     item->item.next = NULL;
     item->item.type = UI_MENU_ITEM;
 
+    item->action = nl_strdup(args->action);
     item->label = nl_strdup(args->label);
     item->icon = nl_strdup(args->icon);
     item->userdata = args->onclickdata;
--- a/ui/common/menu.h	Thu Apr 16 17:41:35 2026 +0200
+++ b/ui/common/menu.h	Fri Apr 17 13:21:11 2026 +0200
@@ -76,6 +76,7 @@
 struct UiMenuItem {
     UiMenuItemI    item;
     ui_callback    callback;
+    char           *action;
     char           *label;
     char           *icon;
     void           *userdata;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/gtk/action.c	Fri Apr 17 13:21:11 2026 +0200
@@ -0,0 +1,53 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2026 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "action.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+
+
+/* ------------------------------ public API ------------------------------ */
+
+void ui_add_action(UiContext *ctx, const char *name, ui_callback callback, void *userdata) {
+    uic_add_action(ctx, name, callback, userdata, NULL, NULL);
+}
+
+void ui_add_action_with_accelerator(
+        UiContext *ctx,
+        const char *name,
+        ui_callback callback,
+        void *userdata,
+        const char *accelerator,
+        const char *accelerator_text)
+{
+    uic_add_action(ctx, name, callback, userdata, accelerator, accelerator_text);
+    
+    // TODO: accelerator
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/gtk/action.h	Fri Apr 17 13:21:11 2026 +0200
@@ -0,0 +1,47 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2026 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef ACTION_H
+#define ACTION_H
+
+#include "../ui/toolkit.h"
+#include "../common/action.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ACTION_H */
+
--- a/ui/gtk/menu.c	Thu Apr 16 17:41:35 2026 +0200
+++ b/ui/gtk/menu.c	Fri Apr 17 13:21:11 2026 +0200
@@ -37,6 +37,7 @@
 #include "../common/context.h"
 #include "../common/menu.h"
 #include "../common/types.h"
+#include "../common/action.h"
 #include "../ui/properties.h"
 #include "../ui/window.h"
 #include "container.h"
@@ -590,7 +591,15 @@
         cxListFree(groups);
     }
     
-    if(i->callback != NULL) {
+    if(i->action) {
+        uic_bind_action(obj->ctx, i->action, action, (ui_enablefunc)action_enable);
+        UiAction *ui_action = uic_resolve_action(obj->ctx, i->action);
+        if(!ui_action) {
+            action_enable(action, FALSE);
+        }
+    }
+     
+    if(i->callback != NULL || i->action) {
         UiEventData *event = malloc(sizeof(UiEventData));
         event->obj = obj;
         event->userdata = i->userdata;
@@ -598,6 +607,7 @@
         event->value = 0;
         event->customdata = NULL;
         event->customint = 0;
+        event->action = i->action ? strdup(i->action) : NULL;
 
         g_signal_connect(
                 action,
@@ -607,13 +617,13 @@
         g_signal_connect(
                 obj->widget,
                 "destroy",
-                G_CALLBACK(ui_destroy_userdata),
+                G_CALLBACK(ui_destroy_eventdata),
                 event);
     }
     
     char action_name[32];
     snprintf(action_name, 32, "win.%s", item->id);
-    
+     
     g_menu_append(parent, i->label, action_name);
 }
 
@@ -888,7 +898,12 @@
         evt.eventdatatype = uic_get_tmp_eventdata_type();
     }
     evt.intval = intval;
-    event->callback(&evt, event->userdata);
+    if(event->callback) {
+        event->callback(&evt, event->userdata);
+    }
+    if(event->action) {
+        uic_action_callback(&evt, event->action);
+    }
     uic_set_tmp_eventdata(NULL, 0);
 }
 
--- a/ui/gtk/objs.mk	Thu Apr 16 17:41:35 2026 +0200
+++ b/ui/gtk/objs.mk	Fri Apr 17 13:21:11 2026 +0200
@@ -48,6 +48,7 @@
 GTKOBJ += headerbar.o
 GTKOBJ += webview.o
 GTKOBJ += widget.o
+GTKOBJ += action.o
 
 TOOLKITOBJS += $(GTKOBJ:%=$(GTK_OBJPRE)%)
 TOOLKITSOURCE += $(GTKOBJ:%.o=gtk/%.c)
--- a/ui/gtk/toolkit.c	Thu Apr 16 17:41:35 2026 +0200
+++ b/ui/gtk/toolkit.c	Fri Apr 17 13:21:11 2026 +0200
@@ -313,6 +313,11 @@
     free(userdata);
 }
 
+void ui_destroy_eventdata(GtkWidget *object, UiEventData *data) {
+    free(data->action);
+    free(data);
+}
+
 void ui_destroy_vardata(GtkWidget *unused, UiVarEventData *data) {
     if(data->var) {
         ui_destroy_boundvar(data->obj->ctx, data->var);
--- a/ui/gtk/toolkit.h	Thu Apr 16 17:41:35 2026 +0200
+++ b/ui/gtk/toolkit.h	Fri Apr 17 13:21:11 2026 +0200
@@ -149,6 +149,7 @@
     int         value;
     int         customint;
     void        *customdata;
+    char        *action;
 } UiEventData;
 
 typedef struct UiEventDataExt {
@@ -200,6 +201,7 @@
 void ui_set_widget_nvisibility_states(UiContext *ctx, GtkWidget *widget, const int *states, size_t ngroups);
 
 void ui_destroy_userdata(GtkWidget *object, void *userdata);
+void ui_destroy_eventdata(GtkWidget *object, UiEventData *data);
 void ui_destroy_vardata(GtkWidget *unused, UiVarEventData *data);
 void ui_destroy_widget_var(GtkWidget *object, UiVar *var);
 void ui_destroy_boundvar(UiContext *ctx, UiVar *var);
--- a/ui/ui/menu.h	Thu Apr 16 17:41:35 2026 +0200
+++ b/ui/ui/menu.h	Fri Apr 17 13:21:11 2026 +0200
@@ -39,7 +39,7 @@
 typedef struct UiMenuItemArgs {
 	const char *label;
 	const char *icon;
-
+        
 	ui_callback onclick;
 	void *onclickdata;
         const char *action;
--- a/ui/ui/toolkit.h	Thu Apr 16 17:41:35 2026 +0200
+++ b/ui/ui/toolkit.h	Fri Apr 17 13:21:11 2026 +0200
@@ -589,6 +589,16 @@
 UIEXPORT void ui_widget_set_states2(UiContext *ctx, UIWIDGET widget, ui_enablefunc enable, const int *states, int nstates);
 UIEXPORT void ui_widget_set_visibility_states(UiContext *ctx, UIWIDGET widget, const int *states, int nstates);
 
+UIEXPORT void ui_add_action(UiContext *ctx, const char *name, ui_callback callback, void *userdata);
+UIEXPORT void ui_add_action_with_accelerator(
+        UiContext *ctx,
+        const char *name,
+        ui_callback callback,
+        void *userdata,
+        const char *accelerator,
+        const char *accelerator_text);
+UIEXPORT void ui_update_action_bindings(UiContext *ctx);
+
 UIEXPORT void ui_set_state(UiContext *ctx, int state);
 UIEXPORT void ui_unset_state(UiContext *ctx, int state);
 UIEXPORT int* ui_active_states(UiContext *ctx, int *nstates);

mercurial