From a1b2225750f5f8e326d5335e0c0d11fffc291a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?=
Date: Mon, 4 Jul 2022 20:16:18 +0200 Subject: [PATCH] Move the browser's popup menu to its own file --- fiv-browser.c | 190 ++------------------------------------------ fiv-context-menu.c | 193 +++++++++++++++++++++++++++++++++++++++++++++ fiv-context-menu.h | 20 +++++ meson.build | 2 +- 4 files changed, 222 insertions(+), 183 deletions(-) create mode 100644 fiv-context-menu.c create mode 100644 fiv-context-menu.h diff --git a/fiv-browser.c b/fiv-browser.c index 10cb17a..2f43d51 100644 --- a/fiv-browser.c +++ b/fiv-browser.c @@ -21,6 +21,7 @@ #include "config.h" #include "fiv-browser.h" +#include "fiv-context-menu.h" #include "fiv-io.h" #include "fiv-thumbnail.h" #include "fiv-view.h" @@ -729,187 +730,6 @@ thumbnailers_start(FivBrowser *self) } } -// --- Context menu------------------------------------------------------------- - -typedef struct _OpenContext { - GWeakRef widget; - GFile *file; - gchar *content_type; - GAppInfo *app_info; -} OpenContext; - -static void -open_context_notify(gpointer data, G_GNUC_UNUSED GClosure *closure) -{ - OpenContext *self = data; - g_weak_ref_clear(&self->widget); - g_clear_object(&self->app_info); - g_clear_object(&self->file); - g_free(self->content_type); - g_free(self); -} - -static void -open_context_launch(GtkWidget *widget, OpenContext *self) -{ - GdkAppLaunchContext *context = - gdk_display_get_app_launch_context(gtk_widget_get_display(widget)); - gdk_app_launch_context_set_screen(context, gtk_widget_get_screen(widget)); - gdk_app_launch_context_set_timestamp(context, gtk_get_current_event_time()); - - // TODO(p): Display errors. - GList *files = g_list_append(NULL, self->file); - if (g_app_info_launch( - self->app_info, files, G_APP_LAUNCH_CONTEXT(context), NULL)) { - g_app_info_set_as_last_used_for_type( - self->app_info, self->content_type, NULL); - } - g_list_free(files); - g_object_unref(context); -} - -static void -append_opener(GtkWidget *menu, GAppInfo *opener, const OpenContext *template) -{ - OpenContext *ctx = g_malloc0(sizeof *ctx); - g_weak_ref_init(&ctx->widget, NULL); - ctx->file = g_object_ref(template->file); - ctx->content_type = g_strdup(template->content_type); - ctx->app_info = opener; - - // On Linux, this prefers the obsoleted X-GNOME-FullName. - gchar *name = - g_strdup_printf("Open With %s", g_app_info_get_display_name(opener)); - - // It's documented that we can touch the child, if we want to use markup. -#if 0 - GtkWidget *item = gtk_menu_item_new_with_label(name); -#else - // GtkImageMenuItem overrides the toggle_size_request class method - // to get the image shown in the "margin"--too much work to duplicate. - G_GNUC_BEGIN_IGNORE_DEPRECATIONS; - - GtkWidget *item = gtk_image_menu_item_new_with_label(name); - GIcon *icon = g_app_info_get_icon(opener); - if (icon) { - GtkWidget *image = gtk_image_new_from_gicon(icon, GTK_ICON_SIZE_MENU); - gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), image); - gtk_image_menu_item_set_always_show_image( - GTK_IMAGE_MENU_ITEM(item), TRUE); - } - - G_GNUC_END_IGNORE_DEPRECATIONS; -#endif - - g_free(name); - g_signal_connect_data(item, "activate", G_CALLBACK(open_context_launch), - ctx, open_context_notify, 0); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); -} - -static void -on_chooser_activate(GtkMenuItem *item, gpointer user_data) -{ - OpenContext *ctx = user_data; - GtkWindow *window = NULL; - GtkWidget *widget = g_weak_ref_get(&ctx->widget); - if (widget) { - if (GTK_IS_WINDOW((widget = gtk_widget_get_toplevel(widget)))) - window = GTK_WINDOW(widget); - } - - GtkWidget *dialog = gtk_app_chooser_dialog_new_for_content_type(window, - GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, ctx->content_type); - if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) { - ctx->app_info = gtk_app_chooser_get_app_info(GTK_APP_CHOOSER(dialog)); - open_context_launch(GTK_WIDGET(item), ctx); - } - gtk_widget_destroy(dialog); -} - -static gboolean -destroy_widget_idle_source_func(GtkWidget *widget) -{ - // The whole menu is deactivated /before/ any item is activated, - // and a destroyed child item will not activate. - gtk_widget_destroy(widget); - return FALSE; -} - -static GtkMenu * -make_context_menu(GtkWidget *widget, GFile *file) -{ - GFileInfo *info = g_file_query_info(file, - G_FILE_ATTRIBUTE_STANDARD_NAME - "," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, - G_FILE_QUERY_INFO_NONE, NULL, NULL); - if (!info) - return NULL; - - // This will have no application pre-assigned, for use with GTK+'s dialog. - OpenContext *ctx = g_malloc0(sizeof *ctx); - g_weak_ref_init(&ctx->widget, widget); - ctx->file = g_object_ref(file); - ctx->content_type = g_strdup(g_file_info_get_content_type(info)); - g_object_unref(info); - - GAppInfo *default_ = - g_app_info_get_default_for_type(ctx->content_type, FALSE); - GList *recommended = g_app_info_get_recommended_for_type(ctx->content_type); - GList *fallback = g_app_info_get_fallback_for_type(ctx->content_type); - - GtkWidget *menu = gtk_menu_new(); - if (default_) { - append_opener(menu, default_, ctx); - gtk_menu_shell_append( - GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - } - - for (GList *iter = recommended; iter; iter = iter->next) { - if (!default_ || !g_app_info_equal(iter->data, default_)) - append_opener(menu, iter->data, ctx); - else - g_object_unref(iter->data); - } - if (recommended) { - g_list_free(recommended); - gtk_menu_shell_append( - GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - } - - for (GList *iter = fallback; iter; iter = iter->next) { - if (!default_ || !g_app_info_equal(iter->data, default_)) - append_opener(menu, iter->data, ctx); - else - g_object_unref(iter->data); - } - if (fallback) { - g_list_free(fallback); - gtk_menu_shell_append( - GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - } - - GtkWidget *item = gtk_menu_item_new_with_label("Open With..."); - g_signal_connect_data(item, "activate", G_CALLBACK(on_chooser_activate), - ctx, open_context_notify, 0); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); - - // As per GTK+ 3 Common Questions, 1.5. - g_object_ref_sink(menu); - g_signal_connect_swapped(menu, "deactivate", - G_CALLBACK(g_idle_add), destroy_widget_idle_source_func); - g_signal_connect(menu, "destroy", G_CALLBACK(g_object_unref), NULL); - - gtk_widget_show_all(menu); - return GTK_MENU(menu); -} - -static void -show_context_menu(GtkWidget *widget, GFile *file) -{ - gtk_menu_popup_at_pointer(make_context_menu(widget, file), NULL); -} - // --- Boilerplate ------------------------------------------------------------- G_DEFINE_TYPE_EXTENDED(FivBrowser, fiv_browser, GTK_TYPE_WIDGET, 0, @@ -1199,6 +1019,12 @@ open_entry(GtkWidget *self, const Entry *entry, gboolean new_window) return TRUE; } +static void +show_context_menu(GtkWidget *widget, GFile *file) +{ + gtk_menu_popup_at_pointer(fiv_context_menu_new(widget, file), NULL); +} + static void abort_button_tracking(FivBrowser *self) { @@ -1600,7 +1426,7 @@ fiv_browser_popup_menu(GtkWidget *widget) file = g_object_ref(fiv_io_model_get_location(self->model)); } - gtk_menu_popup_at_rect(make_context_menu(widget, file), + gtk_menu_popup_at_rect(fiv_context_menu_new(widget, file), gtk_widget_get_window(widget), &rect, GDK_GRAVITY_NORTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL); g_object_unref(file); diff --git a/fiv-context-menu.c b/fiv-context-menu.c new file mode 100644 index 0000000..387969b --- /dev/null +++ b/fiv-context-menu.c @@ -0,0 +1,193 @@ +// +// fiv-context-menu.c: popup menu +// +// Copyright (c) 2021 - 2022, Přemysl Eric Janouch
+// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +#include "config.h" + +#include "fiv-context-menu.h" + +typedef struct _OpenContext { + GWeakRef widget; + GFile *file; + gchar *content_type; + GAppInfo *app_info; +} OpenContext; + +static void +open_context_notify(gpointer data, G_GNUC_UNUSED GClosure *closure) +{ + OpenContext *self = data; + g_weak_ref_clear(&self->widget); + g_clear_object(&self->app_info); + g_clear_object(&self->file); + g_free(self->content_type); + g_free(self); +} + +static void +open_context_launch(GtkWidget *widget, OpenContext *self) +{ + GdkAppLaunchContext *context = + gdk_display_get_app_launch_context(gtk_widget_get_display(widget)); + gdk_app_launch_context_set_screen(context, gtk_widget_get_screen(widget)); + gdk_app_launch_context_set_timestamp(context, gtk_get_current_event_time()); + + // TODO(p): Display errors. + GList *files = g_list_append(NULL, self->file); + if (g_app_info_launch( + self->app_info, files, G_APP_LAUNCH_CONTEXT(context), NULL)) { + g_app_info_set_as_last_used_for_type( + self->app_info, self->content_type, NULL); + } + g_list_free(files); + g_object_unref(context); +} + +static void +append_opener(GtkWidget *menu, GAppInfo *opener, const OpenContext *template) +{ + OpenContext *ctx = g_malloc0(sizeof *ctx); + g_weak_ref_init(&ctx->widget, NULL); + ctx->file = g_object_ref(template->file); + ctx->content_type = g_strdup(template->content_type); + ctx->app_info = opener; + + // On Linux, this prefers the obsoleted X-GNOME-FullName. + gchar *name = + g_strdup_printf("Open With %s", g_app_info_get_display_name(opener)); + + // It's documented that we can touch the child, if we want to use markup. +#if 0 + GtkWidget *item = gtk_menu_item_new_with_label(name); +#else + // GtkImageMenuItem overrides the toggle_size_request class method + // to get the image shown in the "margin"--too much work to duplicate. + G_GNUC_BEGIN_IGNORE_DEPRECATIONS; + + GtkWidget *item = gtk_image_menu_item_new_with_label(name); + GIcon *icon = g_app_info_get_icon(opener); + if (icon) { + GtkWidget *image = gtk_image_new_from_gicon(icon, GTK_ICON_SIZE_MENU); + gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), image); + gtk_image_menu_item_set_always_show_image( + GTK_IMAGE_MENU_ITEM(item), TRUE); + } + + G_GNUC_END_IGNORE_DEPRECATIONS; +#endif + + g_free(name); + g_signal_connect_data(item, "activate", G_CALLBACK(open_context_launch), + ctx, open_context_notify, 0); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); +} + +static void +on_chooser_activate(GtkMenuItem *item, gpointer user_data) +{ + OpenContext *ctx = user_data; + GtkWindow *window = NULL; + GtkWidget *widget = g_weak_ref_get(&ctx->widget); + if (widget) { + if (GTK_IS_WINDOW((widget = gtk_widget_get_toplevel(widget)))) + window = GTK_WINDOW(widget); + } + + GtkWidget *dialog = gtk_app_chooser_dialog_new_for_content_type(window, + GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, ctx->content_type); + if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) { + ctx->app_info = gtk_app_chooser_get_app_info(GTK_APP_CHOOSER(dialog)); + open_context_launch(GTK_WIDGET(item), ctx); + } + gtk_widget_destroy(dialog); +} + +static gboolean +destroy_widget_idle_source_func(GtkWidget *widget) +{ + // The whole menu is deactivated /before/ any item is activated, + // and a destroyed child item will not activate. + gtk_widget_destroy(widget); + return FALSE; +} + +GtkMenu * +fiv_context_menu_new(GtkWidget *widget, GFile *file) +{ + GFileInfo *info = g_file_query_info(file, + G_FILE_ATTRIBUTE_STANDARD_NAME + "," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, + G_FILE_QUERY_INFO_NONE, NULL, NULL); + if (!info) + return NULL; + + // This will have no application pre-assigned, for use with GTK+'s dialog. + OpenContext *ctx = g_malloc0(sizeof *ctx); + g_weak_ref_init(&ctx->widget, widget); + ctx->file = g_object_ref(file); + ctx->content_type = g_strdup(g_file_info_get_content_type(info)); + g_object_unref(info); + + GAppInfo *default_ = + g_app_info_get_default_for_type(ctx->content_type, FALSE); + GList *recommended = g_app_info_get_recommended_for_type(ctx->content_type); + GList *fallback = g_app_info_get_fallback_for_type(ctx->content_type); + + GtkWidget *menu = gtk_menu_new(); + if (default_) { + append_opener(menu, default_, ctx); + gtk_menu_shell_append( + GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + } + + for (GList *iter = recommended; iter; iter = iter->next) { + if (!default_ || !g_app_info_equal(iter->data, default_)) + append_opener(menu, iter->data, ctx); + else + g_object_unref(iter->data); + } + if (recommended) { + g_list_free(recommended); + gtk_menu_shell_append( + GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + } + + for (GList *iter = fallback; iter; iter = iter->next) { + if (!default_ || !g_app_info_equal(iter->data, default_)) + append_opener(menu, iter->data, ctx); + else + g_object_unref(iter->data); + } + if (fallback) { + g_list_free(fallback); + gtk_menu_shell_append( + GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + } + + GtkWidget *item = gtk_menu_item_new_with_label("Open With..."); + g_signal_connect_data(item, "activate", G_CALLBACK(on_chooser_activate), + ctx, open_context_notify, 0); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); + + // As per GTK+ 3 Common Questions, 1.5. + g_object_ref_sink(menu); + g_signal_connect_swapped(menu, "deactivate", + G_CALLBACK(g_idle_add), destroy_widget_idle_source_func); + g_signal_connect(menu, "destroy", G_CALLBACK(g_object_unref), NULL); + + gtk_widget_show_all(menu); + return GTK_MENU(menu); +} diff --git a/fiv-context-menu.h b/fiv-context-menu.h new file mode 100644 index 0000000..a226d0a --- /dev/null +++ b/fiv-context-menu.h @@ -0,0 +1,20 @@ +// +// fiv-context-menu.h: popup menu +// +// Copyright (c) 2022, Přemysl Eric Janouch
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+
+#include