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+.
This commit is contained in:
parent
2d10aa8b61
commit
93ad75eb35
323
fiv.c
323
fiv.c
@ -101,16 +101,16 @@ struct key_section {
|
|||||||
static struct key help_keys_general[] = {
|
static struct key help_keys_general[] = {
|
||||||
{"F1", "Show help"},
|
{"F1", "Show help"},
|
||||||
{"F10", "Open menu"},
|
{"F10", "Open menu"},
|
||||||
{"<Control>comma", "Preferences"},
|
{"<Primary>comma", "Preferences"},
|
||||||
{"<Control>question", "Keyboard shortcuts"},
|
{"<Primary>question", "Keyboard shortcuts"},
|
||||||
{"q <Control>q", "Quit"},
|
{"q <Primary>q", "Quit"},
|
||||||
{"<Control>w", "Quit"},
|
{"<Primary>w", "Quit"},
|
||||||
{}
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
static struct key help_keys_navigation[] = {
|
static struct key help_keys_navigation[] = {
|
||||||
{"<Control>l", "Open location..."},
|
{"<Primary>l", "Open location..."},
|
||||||
{"<Control>n", "Open a new window"},
|
{"<Primary>n", "Open a new window"},
|
||||||
{"<Alt>Left", "Go back in history"},
|
{"<Alt>Left", "Go back in history"},
|
||||||
{"<Alt>Right", "Go forward in history"},
|
{"<Alt>Right", "Go forward in history"},
|
||||||
{}
|
{}
|
||||||
@ -1462,92 +1462,17 @@ toggle_sunlight(void)
|
|||||||
g_object_set(settings, property, !set, NULL);
|
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
|
static gboolean
|
||||||
on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
|
on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
|
||||||
G_GNUC_UNUSED gpointer data)
|
G_GNUC_UNUSED gpointer data)
|
||||||
{
|
{
|
||||||
switch (event->state & gtk_accelerator_get_default_mod_mask()) {
|
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:
|
||||||
case GDK_CONTROL_MASK | GDK_SHIFT_MASK:
|
|
||||||
switch (event->keyval) {
|
switch (event->keyval) {
|
||||||
case GDK_KEY_h:
|
case GDK_KEY_h:
|
||||||
|
// XXX: Command-H is already occupied on macOS.
|
||||||
gtk_button_clicked(GTK_BUTTON(g.browsebar[BROWSEBAR_FILTER]));
|
gtk_button_clicked(GTK_BUTTON(g.browsebar[BROWSEBAR_FILTER]));
|
||||||
return TRUE;
|
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);
|
gtk_accelerator_parse(accelerator, &key, &mods);
|
||||||
g_free(accelerator);
|
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();
|
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_widget_show(g.menu);
|
||||||
|
|
||||||
// _gtk_menu_shell_set_keyboard_mode() is private.
|
// _gtk_menu_shell_set_keyboard_mode() is private.
|
||||||
@ -1573,6 +1505,17 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
|
|||||||
return FALSE;
|
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
|
static gboolean
|
||||||
on_key_press_view(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
|
on_key_press_view(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
|
||||||
G_GNUC_UNUSED gpointer data)
|
G_GNUC_UNUSED gpointer data)
|
||||||
@ -2079,41 +2022,178 @@ make_browser_sidebar(FivIoModel *model)
|
|||||||
return sidebar;
|
return sidebar;
|
||||||
}
|
}
|
||||||
|
|
||||||
static GtkWidget *
|
// --- Actions -----------------------------------------------------------------
|
||||||
make_menu_bar(void)
|
|
||||||
|
#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 *[]) {"<Primary>comma", NULL}},
|
||||||
|
{"new-window", on_action_new_window,
|
||||||
|
(const char *[]) {"<Primary>n", NULL}},
|
||||||
|
{"open", on_open,
|
||||||
|
(const char *[]) {"<Primary>o", "o", NULL}},
|
||||||
|
{"quit", on_action_quit,
|
||||||
|
(const char *[]) {"<Primary>q", "<Primary>w", "q", NULL}},
|
||||||
|
{"toggle-fullscreen", toggle_fullscreen,
|
||||||
|
(const char *[]) {"F11", "f", NULL}},
|
||||||
|
{"toggle-sunlight", toggle_sunlight,
|
||||||
|
(const char *[]) {"<Alt><Shift>d", NULL}},
|
||||||
|
{"go-back", go_back,
|
||||||
|
(const char *[]) {"<Alt>Left", "BackSpace", NULL}},
|
||||||
|
{"go-forward", go_forward,
|
||||||
|
(const char *[]) {"<Alt>Right", NULL}},
|
||||||
|
{"go-location", on_action_location,
|
||||||
|
(const char *[]) {"<Primary>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 *[]) {"<Primary>question", "<Primary>F1", NULL}},
|
||||||
|
{"about", on_action_about,
|
||||||
|
(const char *[]) {"<Shift>F1", NULL}},
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
dispatch_action(G_GNUC_UNUSED GSimpleAction *action,
|
||||||
|
G_GNUC_UNUSED GVariant *parameter, gpointer user_data)
|
||||||
{
|
{
|
||||||
g.menu = gtk_menu_bar_new();
|
GCallback callback = user_data;
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
GtkWidget *item_quit = gtk_menu_item_new_with_mnemonic("_Quit");
|
static void
|
||||||
g_signal_connect_swapped(item_quit, "activate",
|
set_up_action(GtkApplication *app, const ActionEntry *a)
|
||||||
G_CALLBACK(gtk_widget_destroy), g.window);
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
GtkWidget *menu_file = gtk_menu_new();
|
gchar *full_name = g_strdup_printf("app.%s", a->name);
|
||||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu_file), item_quit);
|
gtk_application_set_accels_for_action(app, full_name, a->accels);
|
||||||
GtkWidget *item_file = gtk_menu_item_new_with_mnemonic("_File");
|
g_free(full_name);
|
||||||
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");
|
// --- Menu --------------------------------------------------------------------
|
||||||
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();
|
typedef struct {
|
||||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_contents);
|
const char *label; ///< Label, with a mnemonic
|
||||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_shortcuts);
|
const char *action; ///< Prefixed action name
|
||||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_about);
|
gboolean macos; ///< Show in the macOS global menu?
|
||||||
GtkWidget *item_help = gtk_menu_item_new_with_mnemonic("_Help");
|
} MenuItem;
|
||||||
gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_help), menu_help);
|
|
||||||
gtk_menu_shell_append(GTK_MENU_SHELL(g.menu), item_help);
|
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(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.
|
// 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_show_all(g.menu);
|
||||||
gtk_widget_set_no_show_all(g.menu, TRUE);
|
gtk_widget_set_no_show_all(g.menu, TRUE);
|
||||||
gtk_widget_hide(g.menu);
|
gtk_widget_hide(g.menu);
|
||||||
@ -2121,6 +2201,8 @@ make_menu_bar(void)
|
|||||||
return g.menu;
|
return g.menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Application -------------------------------------------------------------
|
||||||
|
|
||||||
// This is incredibly broken https://stackoverflow.com/a/51054396/76313
|
// This is incredibly broken https://stackoverflow.com/a/51054396/76313
|
||||||
// thus resolving the problem using overlaps.
|
// thus resolving the problem using overlaps.
|
||||||
// We're trying to be universal for light and dark themes both. It's hard.
|
// 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_signal_connect(g.window, "window-state-event",
|
||||||
G_CALLBACK(on_window_state_event), NULL);
|
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);
|
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(menu_box), g.stack);
|
||||||
gtk_container_add(GTK_CONTAINER(g.window), menu_box);
|
gtk_container_add(GTK_CONTAINER(g.window), menu_box);
|
||||||
|
g_object_unref(menu);
|
||||||
|
|
||||||
GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
|
GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
|
||||||
if (g_settings_get_boolean(settings, "dark-theme"))
|
if (g_settings_get_boolean(settings, "dark-theme"))
|
||||||
|
Loading…
Reference in New Issue
Block a user