diff options
author | Přemysl Eric Janouch <p@janouch.name> | 2021-11-23 14:59:25 +0100 |
---|---|---|
committer | Přemysl Eric Janouch <p@janouch.name> | 2021-11-23 20:50:01 +0100 |
commit | 1c40fa8adbb80197e8b911571128920b2e2b6597 (patch) | |
tree | 83de7976d4c6f137ad1a0cb64299cfc5b1ab000e | |
parent | fee901a5901d4bbacbf3ce90bcc3321a5c5e721e (diff) | |
download | fiv-1c40fa8adbb80197e8b911571128920b2e2b6597.tar.gz fiv-1c40fa8adbb80197e8b911571128920b2e2b6597.tar.xz fiv-1c40fa8adbb80197e8b911571128920b2e2b6597.zip |
Add an "Open With" context menu to browser items
-rw-r--r-- | fastiv-browser.c | 168 | ||||
-rw-r--r-- | fastiv.c | 6 |
2 files changed, 168 insertions, 6 deletions
diff --git a/fastiv-browser.c b/fastiv-browser.c index 0ed8c50..485ea4b 100644 --- a/fastiv-browser.c +++ b/fastiv-browser.c @@ -432,6 +432,167 @@ reload_thumbnails(FastivBrowser *self) gtk_widget_queue_resize(GTK_WIDGET(self)); } +// --- Context menu------------------------------------------------------------- + +typedef struct _OpenContext { + GWeakRef widget; + GFile *file; + char *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; + + // It's documented that we can touch the child, if we want formatting: + // https://docs.gtk.org/gtk3/class.MenuItem.html + // XXX: Would g_app_info_get_display_name() be any better? + gchar *name = g_strdup_printf("Open With %s", g_app_info_get_name(opener)); + GtkWidget *item = gtk_menu_item_new_with_label(name); + 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 void +show_context_menu(GtkWidget *widget, const char *filename) +{ + GFile *file = g_file_new_for_path(filename); + 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) { + g_object_unref(file); + return; + } + + // 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 = 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 (g_app_info_should_show(iter->data) && + (!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 (g_app_info_should_show(iter->data) && + (!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); + gtk_menu_popup_at_pointer(GTK_MENU(menu), NULL); +} + // --- Boilerplate ------------------------------------------------------------- // TODO(p): For proper navigation, we need to implement GtkScrollable. @@ -653,8 +814,11 @@ fastiv_browser_button_press_event(GtkWidget *widget, GdkEventButton *event) return open_entry(widget, entry, TRUE); return FALSE; case GDK_BUTTON_SECONDARY: - // TODO(p): Context menu. - return FALSE; + // On X11, after closing the menu, the pointer otherwise remains, + // no matter what its new location is. + gdk_window_set_cursor(gtk_widget_get_window(widget), NULL); + show_context_menu(widget, entry->filename); + return TRUE; default: return FALSE; } @@ -438,10 +438,8 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event, return TRUE; case GDK_KEY_F9: - if (gtk_widget_is_visible(g.browser_sidebar)) - gtk_widget_hide(g.browser_sidebar); - else - gtk_widget_show(g.browser_sidebar); + gtk_widget_set_visible(g.browser_sidebar, + !gtk_widget_is_visible(g.browser_sidebar)); return TRUE; case GDK_KEY_F11: |