Fri, 17 Apr 2026 13:21:11 +0200
add actions, implement action binding for menu items (GTK)
--- 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);