# HG changeset patch # User Olaf Wintermann # Date 1760544523 -7200 # Node ID 63623ef950e5c129707610d0d19aa2876e99ca7c # Parent 5a4b7248911105b9996e06200f7b0c8aec45af93 implement gtk4 menu radio items diff -r 5a4b72489111 -r 63623ef950e5 application/main.c --- a/application/main.c Tue Oct 14 12:48:55 2025 +0200 +++ b/application/main.c Wed Oct 15 18:08:43 2025 +0200 @@ -863,6 +863,10 @@ ui_menuitem("Save"); ui_menuseparator(); + ui_menu_radioitem(.label = "Option 1", .varname = "menu_radio"); + ui_menu_radioitem(.label = "Option 2", .varname = "menu_radio"); + ui_menu_radioitem(.label = "Option 3", .varname = "menu_radio"); + ui_menuseparator(); ui_menuitem("Close"); } diff -r 5a4b72489111 -r 63623ef950e5 ui/gtk/menu.c --- a/ui/gtk/menu.c Tue Oct 14 12:48:55 2025 +0200 +++ b/ui/gtk/menu.c Wed Oct 15 18:08:43 2025 +0200 @@ -43,6 +43,7 @@ #include #include +#include #if GTK_MAJOR_VERSION <= 3 @@ -487,8 +488,144 @@ // TODO } -void ui_gmenu_add_radioitem(GMenu *p, int index, UiMenuItemI *item, UiObject *obj) { + + +typedef struct UiCallbackData { + ui_callback callback; + void *userdata; +} UiCallbackData; + +static void radiogroup_remove_binding(void *obj) { + UiMenuRadioGroup *group = obj; + if(group->var) { + UiInteger *i = group->var->value; + CxList *bindings = i->obj; + if(bindings) { + (void)cxListFindRemove(bindings, obj); + } + } +} + +static void ui_action_set_state(UiInteger *i, int64_t value) { + +} + +static int64_t ui_action_get_state(UiInteger *i) { + return 0; +} + +static UiMenuRadioGroup* create_radio_group(UiObject *obj, UiMenuRadioItem *item, GSimpleAction *action) { + UiMenuRadioGroup *group = cxZalloc(obj->ctx->allocator, sizeof(UiMenuRadioGroup)); + group->callbacks = cxArrayListCreate(obj->ctx->allocator, NULL, sizeof(UiCallbackData), 8); + group->var = uic_create_var(ui_global_context(), item->varname, UI_VAR_INTEGER); + group->obj = obj; + group->action = action; + if(group->var) { + UiInteger *i = group->var->value; + CxList *bindings = i->obj; + if(!bindings) { + bindings = cxLinkedListCreate(group->var->from_ctx->mp->allocator, NULL, CX_STORE_POINTERS); + i->obj = bindings; + i->set = ui_action_set_state; + i->get = ui_action_get_state; + } + cxListAdd(bindings, group); + // the destruction of the toplevel obj must remove the binding + uic_context_add_destructor(obj->ctx, radiogroup_remove_binding, group); + } + return group; +} + +static void stateful_action_activate( + GSimpleAction *action, + GVariant *state, + UiMenuRadioGroup *group) +{ + g_simple_action_set_state(action, state); + + UiVar *var = group->var; + if(!var) { + return; + } + UiInteger *i = var->value; + + gsize len; + const char *s = g_variant_get_string(state, &len); + cxstring value = cx_strn(s, len); + int v; + if(!cx_strtoi(value, &v, 10)) { + i->value = v; + } + UiEvent event; + event.obj = group->obj; + event.window = event.obj->window; + event.document = event.obj->ctx->document; + event.eventdata = NULL; + event.eventdatatype = 0; + event.intval = v; + event.set = 0; + + CxIterator iter = cxListIterator(group->callbacks); + cx_foreach(UiCallbackData *, cb, iter) { + if(cb->callback) { + cb->callback(&event, cb->userdata); + } + } + + UiObserver *obs = i->observers; + while(obs) { + if(obs->callback) { + obs->callback(&event, obs->data); + } + obs = obs->next; + } +} + + + +void ui_gmenu_add_radioitem(GMenu *parent, int index, UiMenuItemI *item, UiObject *obj) { + UiMenuRadioItem *i = (UiMenuRadioItem*)item; + + if(!i->varname) { + return; + } + + // All radio buttons with the name varname use the same GAction + UiMenuRadioGroup *group = NULL; + GAction *action = g_action_map_lookup_action(obj->ctx->action_map, i->varname); + if(!action) { + GVariant *state = g_variant_new_string("0"); + GSimpleAction *newAction = g_simple_action_new_stateful(i->varname, G_VARIANT_TYPE_STRING, state); + g_action_map_add_action(obj->ctx->action_map, G_ACTION(newAction)); + + group = create_radio_group(obj, i, newAction); + g_object_set_data(G_OBJECT(newAction), "ui_radiogroup", group); + + g_signal_connect( + newAction, + "change-state", + G_CALLBACK(stateful_action_activate), + group); + } else { + group = g_object_get_data(G_OBJECT(action), "ui_radiogroup"); + if(!group) { + fprintf(stderr, "Error: missing ui_radiogroup property for action %s\n", i->varname); + return; // error, should not happen + } + } + + size_t item_index = cxListSize(group->callbacks); + + UiCallbackData cb; + cb.callback = i->callback; + cb.userdata = i->userdata; + cxListAdd(group->callbacks, &cb); + + + cxmutstr action_name = cx_asprintf("win.%s::%d", i->varname, (int)item_index); + g_menu_append(parent, i->label, action_name.ptr); + free(action_name.ptr); } static void menuitem_list_remove_binding(void *obj) { diff -r 5a4b72489111 -r 63623ef950e5 ui/gtk/menu.h --- a/ui/gtk/menu.h Tue Oct 14 12:48:55 2025 +0200 +++ b/ui/gtk/menu.h Wed Oct 15 18:08:43 2025 +0200 @@ -98,6 +98,14 @@ void *userdata; }; +typedef struct UiMenuRadioGroup { + UiObject *obj; + CxList *callbacks; + UiVar *var; + GSimpleAction *action; +} UiMenuRadioGroup; + + void ui_gmenu_add_menu_items(GMenu *parent, int i, UiMenu *menu, UiObject *obj); void ui_gmenu_add_menu(GMenu *parent, int index, UiMenuItemI *item, UiObject *obj);