--- a/ui/gtk/menu.c Sat Oct 04 14:54:25 2025 +0200 +++ b/ui/gtk/menu.c Sun Oct 19 21:20:08 2025 +0200 @@ -33,6 +33,7 @@ #include "menu.h" #include "toolkit.h" +#include "widget.h" #include "../common/context.h" #include "../common/menu.h" #include "../common/types.h" @@ -42,6 +43,7 @@ #include <cx/linked_list.h> #include <cx/array_list.h> +#include <cx/printf.h> #if GTK_MAJOR_VERSION <= 3 @@ -135,44 +137,6 @@ } } -/* -void add_menuitem_st_widget( - GtkWidget *parent, - int index, - UiMenuItemI *item, - UiObject *obj) -{ - UiStMenuItem *i = (UiStMenuItem*)item; - - GtkWidget *widget = gtk_image_menu_item_new_from_stock(i->stockid, obj->ctx->accel_group); - - if(i->callback != NULL) { - UiEventData *event = malloc(sizeof(UiEventData)); - event->obj = obj; - event->userdata = i->userdata; - event->callback = i->callback; - event->value = 0; - - g_signal_connect( - widget, - "activate", - G_CALLBACK(ui_menu_event_wrapper), - event); - g_signal_connect( - widget, - "destroy", - G_CALLBACK(ui_destroy_userdata), - event); - } - - gtk_menu_shell_append(GTK_MENU_SHELL(parent), widget); - - if(i->groups) { - uic_add_group_widget(obj->ctx, widget, (ui_enablefunc)ui_set_enabled, i->groups); - } -} -*/ - void add_menuseparator_widget( GtkWidget *parent, int index, @@ -214,25 +178,6 @@ // TODO } -/* -void add_checkitemnv_widget(GtkWidget *p, int index, UiMenuItemI *item, UiObject *obj) { - UiCheckItemNV *ci = (UiCheckItemNV*)item; - GtkWidget *widget = gtk_check_menu_item_new_with_mnemonic(ci->label); - gtk_menu_shell_append(GTK_MENU_SHELL(p), widget); - - UiVar *var = uic_create_var(obj->ctx, ci->varname, UI_VAR_INTEGER); - if(var) { - UiInteger *value = var->value; - value->obj = widget; - value->get = ui_checkitem_get; - value->set = ui_checkitem_set; - value = 0; - } else { - // TODO: error - } -} -*/ - static void menuitem_list_remove_binding(void *obj) { UiActiveMenuItemList *ls = obj; UiList *list = ls->var->value; @@ -261,8 +206,8 @@ ls->oldcount = 0; ls->getvalue = il->getvalue; - //UiVar* var = uic_create_var(ui_global_context(), il->varname, UI_VAR_LIST); - UiVar* var = uic_create_var(obj->ctx, il->varname, UI_VAR_LIST); + UiVar* var = uic_create_var(ui_global_context(), il->varname, UI_VAR_LIST); + //UiVar* var = uic_create_var(obj->ctx, il->varname, UI_VAR_LIST); ls->var = var; if(var) { UiList *list = var->value; @@ -468,6 +413,9 @@ GMenu *section = NULL; while(it) { if(it->type == UI_MENU_SEPARATOR) { + if(section) { + g_object_unref(section); + } section = g_menu_new(); g_menu_append_section(parent, NULL, G_MENU_MODEL(section)); index++; @@ -481,6 +429,9 @@ } it = it->next; } + if(section) { + g_object_unref(section); + } } void ui_gmenu_add_menu(GMenu *parent, int index, UiMenuItemI *item, UiObject *obj) { @@ -488,6 +439,7 @@ GMenu *menu = g_menu_new(); ui_gmenu_add_menu_items(menu, 0, mi, obj); g_menu_append_submenu(parent, mi->label, G_MENU_MODEL(menu)); + g_object_unref(menu); } static void action_enable(GSimpleAction *action, int enabled) { @@ -499,6 +451,7 @@ GSimpleAction *action = g_simple_action_new(item->id, NULL); g_action_map_add_action(obj->ctx->action_map, G_ACTION(action)); + g_object_unref(action); if(i->groups) { CxList *groups = cxArrayListCreateSimple(sizeof(int), i->ngroups); @@ -529,7 +482,8 @@ } char action_name[32]; - snprintf(action_name, 32, "win.%s", item->id); + snprintf(action_name, 32, "win.%s", item->id); + g_menu_append(parent, i->label, action_name); } @@ -543,8 +497,161 @@ // 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 stateful_action_notify_group(UiMenuRadioGroup *group, UiInteger *i) { + 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 = (int)i->value; + event.set = ui_get_setop(); + + 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; + } +} + +static void ui_action_set_state(UiInteger *i, int64_t value) { + i->value = value; + CxList *bindings = i->obj; + CxIterator iter = cxListIterator(bindings); + cx_foreach(UiMenuRadioGroup *, group, iter) { + char buf[32]; + snprintf(buf, 32, "%d", (int)value); + GVariant *state = g_variant_new_string(buf); + g_action_change_state(G_ACTION(group->action), state); + stateful_action_notify_group(group, i); + } +} + +static int64_t ui_action_get_state(UiInteger *i) { + return i->value; +} + +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; + } + + CxList *bindings = i->obj; + CxIterator iter = cxListIterator(bindings); + cx_foreach(UiMenuRadioGroup *, gr, iter) { + stateful_action_notify_group(gr, i); + } +} + + +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)); + g_object_unref(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) { @@ -578,6 +685,7 @@ GSimpleAction *action = g_simple_action_new(item->id, g_variant_type_new("i")); g_action_map_add_action(obj->ctx->action_map, G_ACTION(action)); + g_object_unref(action); snprintf(ls->action, 32, "win.%s", item->id); UiVar* var = uic_create_var(ui_global_context(), il->varname, UI_VAR_LIST); @@ -691,6 +799,7 @@ GVariant *v = g_variant_new("i", i); g_menu_item_set_action_and_target_value(item, list->action, v); g_menu_insert_item(list->menu, list->index+i, item); + g_object_unref(item); elm = ui_list_next(ls); i++; @@ -710,6 +819,7 @@ GMenu *menu = g_menu_new(); ui_gmenu_add_menu_items(menu, 0, builder->menus_begin, obj); GtkWidget *contextmenu = gtk_popover_menu_new_from_model(G_MENU_MODEL(menu)); + g_object_unref(menu); gtk_popover_set_has_arrow(GTK_POPOVER(contextmenu), FALSE); gtk_widget_set_halign(contextmenu, GTK_ALIGN_START); gtk_widget_set_parent(GTK_WIDGET(contextmenu), widget);