From 93ad75eb3555361fd1ceec4004da2c1cf6fe605c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?= Date: Fri, 2 Jun 2023 13:02:50 +0200 Subject: Switch to a GAction-based menu The new menu has a few more entries, and shows accelerators. Most shortcuts have now moved from on_key_press() to actions, and Alt-Shift-D has started working on macOS. This also adds support for the global menu in macOS, and moves some accelerators/key equivalents to the Command key. There is no other easy way of accessing that global menu in GTK+. --- fiv.c | 331 +++++++++++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 215 insertions(+), 116 deletions(-) diff --git a/fiv.c b/fiv.c index 01843c4..7213418 100644 --- a/fiv.c +++ b/fiv.c @@ -101,16 +101,16 @@ struct key_section { static struct key help_keys_general[] = { {"F1", "Show help"}, {"F10", "Open menu"}, - {"comma", "Preferences"}, - {"question", "Keyboard shortcuts"}, - {"q q", "Quit"}, - {"w", "Quit"}, + {"comma", "Preferences"}, + {"question", "Keyboard shortcuts"}, + {"q q", "Quit"}, + {"w", "Quit"}, {} }; static struct key help_keys_navigation[] = { - {"l", "Open location..."}, - {"n", "Open a new window"}, + {"l", "Open location..."}, + {"n", "Open a new window"}, {"Left", "Go back in history"}, {"Right", "Go forward in history"}, {} @@ -1462,92 +1462,17 @@ toggle_sunlight(void) g_object_set(settings, property, !set, NULL); } -// Cursor keys, e.g., simply cannot be bound through accelerators -// (and GtkWidget::keynav-failed would arguably be an awful solution). -// -// GtkBindingSets can be added directly through GtkStyleContext, -// but that would still require setting up action signals on the widget class, -// which is extremely cumbersome. GtkWidget::move-focus has no return value, -// so we can't override that and abort further handling. -// -// Therefore, bind directly to keypresses. Order can be fine-tuned with -// g_signal_connect{,after}(), or overriding the handler and either tactically -// chaining up or using gtk_window_propagate_key_event(). static gboolean on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event, G_GNUC_UNUSED gpointer data) { switch (event->state & gtk_accelerator_get_default_mod_mask()) { - case GDK_MOD1_MASK | GDK_SHIFT_MASK: - if (event->keyval == GDK_KEY_D) - toggle_sunlight(); - break; case GDK_CONTROL_MASK: - case GDK_CONTROL_MASK | GDK_SHIFT_MASK: switch (event->keyval) { case GDK_KEY_h: + // XXX: Command-H is already occupied on macOS. gtk_button_clicked(GTK_BUTTON(g.browsebar[BROWSEBAR_FILTER])); return TRUE; - case GDK_KEY_l: - fiv_sidebar_show_enter_location(FIV_SIDEBAR(g.browser_sidebar)); - return TRUE; - case GDK_KEY_n: - if (gtk_stack_get_visible_child(GTK_STACK(g.stack)) == g.view_box) - spawn_uri(g.uri); - else - spawn_uri(g.directory); - return TRUE; - case GDK_KEY_o: - on_open(); - return TRUE; - case GDK_KEY_q: - case GDK_KEY_w: - gtk_widget_destroy(g.window); - return TRUE; - - case GDK_KEY_question: - show_help_shortcuts(); - return TRUE; - case GDK_KEY_comma: - show_preferences(g.window); - return TRUE; - } - break; - case GDK_MOD1_MASK: - switch (event->keyval) { - case GDK_KEY_Left: - go_back(); - return TRUE; - case GDK_KEY_Right: - go_forward(); - return TRUE; - } - break; - case GDK_SHIFT_MASK: - switch (event->keyval) { - case GDK_KEY_F1: - show_about_dialog(g.window); - return TRUE; - } - break; - case 0: - switch (event->keyval) { - case GDK_KEY_BackSpace: - go_back(); - return TRUE; - case GDK_KEY_q: - gtk_widget_destroy(g.window); - return TRUE; - case GDK_KEY_o: - on_open(); - return TRUE; - case GDK_KEY_F1: - show_help_contents(); - return TRUE; - case GDK_KEY_F11: - case GDK_KEY_f: - toggle_fullscreen(); - return TRUE; } } @@ -1562,8 +1487,15 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event, gtk_accelerator_parse(accelerator, &key, &mods); g_free(accelerator); + // TODO(p): See how Unity 7 behaves, + // we might want to keep GtkApplicationWindow:show-menubar then. + gboolean shell_shows_menubar = FALSE; + (void) g_object_get(gtk_settings_get_default(), + "gtk-shell-shows-menubar", &shell_shows_menubar, NULL); + guint mask = gtk_accelerator_get_default_mod_mask(); - if (key && event->keyval == key && (event->state & mask) == mods) { + if (key && event->keyval == key && (event->state & mask) == mods && + !shell_shows_menubar) { gtk_widget_show(g.menu); // _gtk_menu_shell_set_keyboard_mode() is private. @@ -1573,6 +1505,17 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event, return FALSE; } +// Cursor keys, e.g., simply cannot be bound through accelerators +// (and GtkWidget::keynav-failed would arguably be an awful solution). +// +// GtkBindingSets can be added directly through GtkStyleContext, +// but that would still require setting up action signals on the widget class, +// which is extremely cumbersome. GtkWidget::move-focus has no return value, +// so we can't override that and abort further handling. +// +// Therefore, bind directly to keypresses. Order can be fine-tuned with +// g_signal_connect{,after}(), or overriding the handler and either tactically +// chaining up or using gtk_window_propagate_key_event(). static gboolean on_key_press_view(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event, G_GNUC_UNUSED gpointer data) @@ -2079,41 +2022,178 @@ make_browser_sidebar(FivIoModel *model) return sidebar; } +// --- Actions ----------------------------------------------------------------- + +#define ACTION(name) static void on_action_ ## name(void) + +ACTION(new_window) { + if (gtk_stack_get_visible_child(GTK_STACK(g.stack)) == g.view_box) + spawn_uri(g.uri); + else + spawn_uri(g.directory); +} + +ACTION(quit) { + gtk_widget_destroy(g.window); +} + +ACTION(location) { + fiv_sidebar_show_enter_location(FIV_SIDEBAR(g.browser_sidebar)); +} + +ACTION(preferences) { + show_preferences(g.window); +} + +ACTION(about) { + show_about_dialog(g.window); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +typedef struct { + const char *name; ///< Unprefixed action name + GCallback callback; ///< Simple callback + const char **accels; ///< NULL-terminated accelerator list +} ActionEntry; + +static ActionEntry g_actions[] = { + {"preferences", on_action_preferences, + (const char *[]) {"comma", NULL}}, + {"new-window", on_action_new_window, + (const char *[]) {"n", NULL}}, + {"open", on_open, + (const char *[]) {"o", "o", NULL}}, + {"quit", on_action_quit, + (const char *[]) {"q", "w", "q", NULL}}, + {"toggle-fullscreen", toggle_fullscreen, + (const char *[]) {"F11", "f", NULL}}, + {"toggle-sunlight", toggle_sunlight, + (const char *[]) {"d", NULL}}, + {"go-back", go_back, + (const char *[]) {"Left", "BackSpace", NULL}}, + {"go-forward", go_forward, + (const char *[]) {"Right", NULL}}, + {"go-location", on_action_location, + (const char *[]) {"l", NULL}}, + {"help", show_help_contents, + (const char *[]) {"F1", NULL}}, + {"shortcuts", show_help_shortcuts, + // Similar to win.show-help-overlay in gtkapplication.c. + (const char *[]) {"question", "F1", NULL}}, + {"about", on_action_about, + (const char *[]) {"F1", NULL}}, + {} +}; + +static void +dispatch_action(G_GNUC_UNUSED GSimpleAction *action, + G_GNUC_UNUSED GVariant *parameter, gpointer user_data) +{ + GCallback callback = user_data; + callback(); +} + +static void +set_up_action(GtkApplication *app, const ActionEntry *a) +{ + GSimpleAction *action = g_simple_action_new(a->name, NULL); + g_signal_connect(action, "activate", + G_CALLBACK(dispatch_action), a->callback); + g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(action)); + g_object_unref(action); + + gchar *full_name = g_strdup_printf("app.%s", a->name); + gtk_application_set_accels_for_action(app, full_name, a->accels); + g_free(full_name); +} + +// --- Menu -------------------------------------------------------------------- + +typedef struct { + const char *label; ///< Label, with a mnemonic + const char *action; ///< Prefixed action name + gboolean macos; ///< Show in the macOS global menu? +} MenuItem; + +typedef struct { + const char *label; ///< Label, with a mnemonic + const MenuItem *items; ///< ""-sectioned menu items +} MenuRoot; + +// We're single-instance, skip the "win" namespace for simplicity. +static MenuRoot g_menu[] = { + {"_File", (MenuItem[]) { + {"_New Window", "app.new-window", TRUE}, + {"_Open...", "app.open", TRUE}, + {"", NULL, TRUE}, + {"_Quit", "app.quit", FALSE}, + {} + }}, + {"_Go", (MenuItem[]) { + {"_Back", "app.go-back", TRUE}, + {"_Forward", "app.go-forward", TRUE}, + {"", NULL, TRUE}, + {"_Location...", "app.go-location", TRUE}, + {} + }}, + {"_Help", (MenuItem[]) { + {"_Contents", "app.help", TRUE}, + {"_Keyboard Shortcuts", "app.shortcuts", TRUE}, + {"_About", "app.about", FALSE}, + {} + }}, + {} +}; + +static GMenuModel * +make_submenu(const MenuItem *items) +{ + GMenu *menu = g_menu_new(); + while (items->label) { + GMenu *section = g_menu_new(); + for (; items->label; items++) { + // Empty strings are interpreted as separators. + if (!*items->label) { + items++; + break; + } + + GMenuItem *subitem = g_menu_item_new(items->label, items->action); + if (!items->macos) { + g_menu_item_set_attribute( + subitem, "hidden-when", "s", "macos-menubar"); + } + + g_menu_append_item(section, subitem); + g_object_unref(subitem); + } + g_menu_append_section(menu, NULL, G_MENU_MODEL(section)); + g_object_unref(section); + } + return G_MENU_MODEL(menu); +} + +static GMenuModel * +make_menu_model(void) +{ + GMenu *menu = g_menu_new(); + for (const MenuRoot *root = g_menu; root->label; root++) { + GMenuModel *submenu = make_submenu(root->items); + g_menu_append_submenu(menu, root->label, submenu); + g_object_unref(submenu); + } + return G_MENU_MODEL(menu); +} + static GtkWidget * -make_menu_bar(void) -{ - g.menu = gtk_menu_bar_new(); - - GtkWidget *item_quit = gtk_menu_item_new_with_mnemonic("_Quit"); - g_signal_connect_swapped(item_quit, "activate", - G_CALLBACK(gtk_widget_destroy), g.window); - - GtkWidget *menu_file = gtk_menu_new(); - gtk_menu_shell_append(GTK_MENU_SHELL(menu_file), item_quit); - GtkWidget *item_file = gtk_menu_item_new_with_mnemonic("_File"); - gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_file), menu_file); - gtk_menu_shell_append(GTK_MENU_SHELL(g.menu), item_file); - - GtkWidget *item_contents = gtk_menu_item_new_with_mnemonic("_Contents"); - g_signal_connect_swapped(item_contents, "activate", - G_CALLBACK(show_help_contents), NULL); - GtkWidget *item_shortcuts = - gtk_menu_item_new_with_mnemonic("_Keyboard Shortcuts"); - g_signal_connect_swapped(item_shortcuts, "activate", - G_CALLBACK(show_help_shortcuts), NULL); - GtkWidget *item_about = gtk_menu_item_new_with_mnemonic("_About"); - g_signal_connect_swapped(item_about, "activate", - G_CALLBACK(show_about_dialog), g.window); - - GtkWidget *menu_help = gtk_menu_new(); - gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_contents); - gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_shortcuts); - gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_about); - GtkWidget *item_help = gtk_menu_item_new_with_mnemonic("_Help"); - gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_help), menu_help); - gtk_menu_shell_append(GTK_MENU_SHELL(g.menu), item_help); +make_menu_bar(GMenuModel *model) +{ + g.menu = gtk_menu_bar_new_from_model(model); // Don't let it take up space by default. Firefox sets a precedent here. + // (gtk_application_window_set_show_menubar() doesn't seem viable for use + // for this purpose.) gtk_widget_show_all(g.menu); gtk_widget_set_no_show_all(g.menu, TRUE); gtk_widget_hide(g.menu); @@ -2121,6 +2201,8 @@ make_menu_bar(void) return g.menu; } +// --- Application ------------------------------------------------------------- + // This is incredibly broken https://stackoverflow.com/a/51054396/76313 // thus resolving the problem using overlaps. // We're trying to be universal for light and dark themes both. It's hard. @@ -2305,10 +2387,27 @@ on_app_startup(GApplication *app, G_GNUC_UNUSED gpointer user_data) g_signal_connect(g.window, "window-state-event", G_CALLBACK(on_window_state_event), NULL); + for (const ActionEntry *a = g_actions; a->name; a++) + set_up_action(GTK_APPLICATION(app), a); + + // GtkApplicationWindow overrides GtkContainer/GtkWidget virtual methods + // so that it has the menu bar as an extra child (if it so decides). + // However, we currently want this menu bar to only show on a key press, + // and to hide as soon as it's no longer being used. + // Messing with the window's internal state seems at best quirky, + // so we'll manage the menu entirely by ourselves. + gtk_application_window_set_show_menubar( + GTK_APPLICATION_WINDOW(g.window), FALSE); + + GMenuModel *menu = make_menu_model(); + gtk_application_set_menubar(GTK_APPLICATION(app), menu); + // The default "app menu" is good, in particular for macOS, so keep it. + GtkWidget *menu_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - gtk_container_add(GTK_CONTAINER(menu_box), make_menu_bar()); + gtk_container_add(GTK_CONTAINER(menu_box), make_menu_bar(menu)); gtk_container_add(GTK_CONTAINER(menu_box), g.stack); gtk_container_add(GTK_CONTAINER(g.window), menu_box); + g_object_unref(menu); GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME); if (g_settings_get_boolean(settings, "dark-theme")) -- cgit v1.2.3