// 23 april 2015 #include "uipriv_unix.h" // TODO get rid of the base item and store the GType, the disabled flag, and the checked flag like we do on Windows? static GArray *menus = NULL; static gboolean menusFinalized = FALSE; static gboolean hasQuit = FALSE; static gboolean hasPreferences = FALSE; static gboolean hasAbout = FALSE; struct menu { uiMenu m; char *name; GArray *items; // []*struct menuItem }; struct menuItem { uiMenuItem mi; char *name; int type; void (*onClicked)(uiMenuItem *, uiWindow *, void *); void *onClickedData; GtkWidget *baseItem; // template for new instances; kept in sync with everything else GHashTable *uiWindows; // map[GtkMenuItem]uiWindow // TODO this assumes that a gulong can fit in a gpointer GHashTable *signals; // map[GtkMenuItem]gulong }; enum { typeRegular, typeCheckbox, typeQuit, typePreferences, typeAbout, typeSeparator, }; #define NEWHASH() g_hash_table_new(g_direct_hash, g_direct_equal) // we do NOT want programmatic updates to raise an ::activated signal static void singleSetChecked(GtkCheckMenuItem *menuitem, gboolean checked, gulong signal) { g_signal_handler_block(menuitem, signal); gtk_check_menu_item_set_active(menuitem, checked); g_signal_handler_unblock(menuitem, signal); } static void setChecked(struct menuItem *item, gboolean checked) { GHashTableIter iter; gpointer widget; gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item->baseItem), checked); g_hash_table_iter_init(&iter, item->uiWindows); while (g_hash_table_iter_next(&iter, &widget, NULL)) singleSetChecked(GTK_CHECK_MENU_ITEM(widget), checked, (gulong) g_hash_table_lookup(item->signals, widget)); } static void onClicked(GtkMenuItem *menuitem, gpointer data) { struct menuItem *item = (struct menuItem *) data; uiWindow *w; // we need to manually update the checked states of all menu items if one changes // notice that this is getting the checked state of the menu item that this signal is sent from if (item->type == typeCheckbox) setChecked(item, gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menuitem))); w = uiWindow(g_hash_table_lookup(item->uiWindows, menuitem)); (*(item->onClicked))(uiMenuItem(item), w, item->onClickedData); } static void defaultOnClicked(uiMenuItem *item, uiWindow *w, void *data) { // do nothing } static void menuItemEnableDisable(struct menuItem *item, gboolean enabled) { GHashTableIter iter; gpointer widget; gtk_widget_set_sensitive(item->baseItem, enabled); g_hash_table_iter_init(&iter, item->uiWindows); while (g_hash_table_iter_next(&iter, &widget, NULL)) gtk_widget_set_sensitive(GTK_WIDGET(widget), enabled); } static void menuItemEnable(uiMenuItem *ii) { struct menuItem *item = (struct menuItem *) ii; menuItemEnableDisable(item, TRUE); } static void menuItemDisable(uiMenuItem *ii) { struct menuItem *item = (struct menuItem *) ii; menuItemEnableDisable(item, FALSE); } static void menuItemOnClicked(uiMenuItem *ii, void (*f)(uiMenuItem *, uiWindow *, void *), void *data) { struct menuItem *item = (struct menuItem *) ii; item->onClicked = f; item->onClickedData = data; } static int menuItemChecked(uiMenuItem *ii) { struct menuItem *item = (struct menuItem *) ii; return gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(item->baseItem)) != FALSE; } static void menuItemSetChecked(uiMenuItem *ii, int checked) { struct menuItem *item = (struct menuItem *) ii; gboolean c; // use explicit values c = FALSE; if (checked) c = TRUE; setChecked(item, c); } static uiMenuItem *newItem(struct menu *m, int type, const char *name) { struct menuItem *item; if (menusFinalized) complain("attempt to create a new menu item after menus have been finalized"); item = uiNew(struct menuItem); g_array_append_val(m->items, item); item->type = type; switch (item->type) { case typeQuit: item->name = g_strdup("Quit"); break; case typePreferences: item->name = g_strdup("Preferences..."); break; case typeAbout: item->name = g_strdup("About"); break; case typeSeparator: break; default: item->name = g_strdup(name); break; } item->onClicked = defaultOnClicked; switch (item->type) { case typeCheckbox: item->baseItem = gtk_check_menu_item_new_with_label(item->name); break; case typeSeparator: item->baseItem = gtk_separator_menu_item_new(); break; default: item->baseItem = gtk_menu_item_new_with_label(item->name); break; } item->uiWindows = NEWHASH(); item->signals = NEWHASH(); uiMenuItem(item)->Enable = menuItemEnable; uiMenuItem(item)->Disable = menuItemDisable; uiMenuItem(item)->OnClicked = menuItemOnClicked; uiMenuItem(item)->Checked = menuItemChecked; uiMenuItem(item)->SetChecked = menuItemSetChecked; return uiMenuItem(item); } uiMenuItem *menuAppendItem(uiMenu *mm, const char *name) { return newItem((struct menu *) mm, typeRegular, name); } uiMenuItem *menuAppendCheckItem(uiMenu *mm, const char *name) { return newItem((struct menu *) mm, typeCheckbox, name); } uiMenuItem *menuAppendQuitItem(uiMenu *mm) { if (hasQuit) complain("attempt to add multiple Quit menu items"); hasQuit = TRUE; // TODO conditionally add separator newItem((struct menu *) mm, typeSeparator, NULL); return newItem((struct menu *) mm, typeQuit, NULL); } uiMenuItem *menuAppendPreferencesItem(uiMenu *mm) { if (hasPreferences) complain("attempt to add multiple Preferences menu items"); hasPreferences = TRUE; // TODO conditionally add separator newItem((struct menu *) mm, typeSeparator, NULL); return newItem((struct menu *) mm, typePreferences, NULL); } uiMenuItem *menuAppendAboutItem(uiMenu *mm) { if (hasAbout) complain("attempt to add multiple About menu items"); hasAbout = TRUE; // TODO conditionally add separator newItem((struct menu *) mm, typeSeparator, NULL); return newItem((struct menu *) mm, typeAbout, NULL); } void menuAppendSeparator(uiMenu *mm) { newItem((struct menu *) mm, typeSeparator, NULL); } uiMenu *uiNewMenu(const char *name) { struct menu *m; if (menusFinalized) complain("attempt to create a new menu after menus have been finalized"); if (menus == NULL) menus = g_array_new(FALSE, TRUE, sizeof (struct menu *)); m = uiNew(struct menu); g_array_append_val(menus, m); m->name = g_strdup(name); m->items = g_array_new(FALSE, TRUE, sizeof (struct menuItem *)); uiMenu(m)->AppendItem = menuAppendItem; uiMenu(m)->AppendCheckItem = menuAppendCheckItem; uiMenu(m)->AppendQuitItem = menuAppendQuitItem; uiMenu(m)->AppendPreferencesItem = menuAppendPreferencesItem; uiMenu(m)->AppendAboutItem = menuAppendAboutItem; uiMenu(m)->AppendSeparator = menuAppendSeparator; return uiMenu(m); } static void appendMenuItem(GtkMenuShell *submenu, struct menuItem *item, uiWindow *w) { GtkWidget *menuitem; gulong signal; menuitem = g_object_new(G_OBJECT_TYPE(item->baseItem), NULL); if (item->name != NULL) gtk_menu_item_set_label(GTK_MENU_ITEM(menuitem), item->name); if (item->type != typeSeparator) { signal = g_signal_connect(menuitem, "activate", G_CALLBACK(onClicked), item); gtk_widget_set_sensitive(menuitem, gtk_widget_get_sensitive(item->baseItem)); if (item->type == typeCheckbox) singleSetChecked(GTK_CHECK_MENU_ITEM(menuitem), gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(item->baseItem)), signal); } gtk_menu_shell_append(submenu, menuitem); g_hash_table_insert(item->uiWindows, menuitem, w); g_hash_table_insert(item->signals, menuitem, (gpointer) signal); } // TODO should this return a zero-height widget (or NULL) if there are no menus defined? GtkWidget *makeMenubar(uiWindow *w) { GtkWidget *menubar; guint i, j; struct menu *m; GtkWidget *menuitem; GtkWidget *submenu; menusFinalized = TRUE; menubar = gtk_menu_bar_new(); for (i = 0; i < menus->len; i++) { m = g_array_index(menus, struct menu *, i); menuitem = gtk_menu_item_new_with_label(m->name); submenu = gtk_menu_new(); gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), submenu); for (j = 0; j < m->items->len; j++) appendMenuItem(GTK_MENU_SHELL(submenu), g_array_index(m->items, struct menuItem *, j), w); gtk_menu_shell_append(GTK_MENU_SHELL(menubar), menuitem); } gtk_widget_set_hexpand(menubar, TRUE); gtk_widget_set_halign(menubar, GTK_ALIGN_FILL); return menubar; } struct freeMenuItemData { GArray *items; guint i; }; static void freeMenuItem(GtkWidget *widget, gpointer data) { struct freeMenuItemData *fmi = (struct freeMenuItemData *) data; struct menuItem *item; item = g_array_index(fmi->items, struct menuItem *, fmi->i); if (g_hash_table_remove(item->uiWindows, widget) == FALSE) complain("GtkMenuItem %p not in menu items uiWindows map", widget); if (g_hash_table_remove(item->signals, widget) == FALSE) complain("GtkMenuItem %p not in menu items signals map", widget); fmi->i++; } static void freeMenu(GtkWidget *widget, gpointer data) { guint *i = (guint *) data; struct menu *m; GtkMenuItem *item; GtkWidget *submenu; struct freeMenuItemData fmi; m = g_array_index(menus, struct menu *, *i); item = GTK_MENU_ITEM(widget); submenu = gtk_menu_item_get_submenu(item); fmi.items = m->items; fmi.i = 0; gtk_container_foreach(GTK_CONTAINER(submenu), freeMenuItem, &fmi); (*i)++; } void freeMenubar(GtkWidget *mb) { guint i; i = 0; gtk_container_foreach(GTK_CONTAINER(mb), freeMenu, &i); }