aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2021-11-23 14:59:25 +0100
committerPřemysl Eric Janouch <p@janouch.name>2021-11-23 20:50:01 +0100
commit1c40fa8adbb80197e8b911571128920b2e2b6597 (patch)
tree83de7976d4c6f137ad1a0cb64299cfc5b1ab000e
parentfee901a5901d4bbacbf3ce90bcc3321a5c5e721e (diff)
downloadfiv-1c40fa8adbb80197e8b911571128920b2e2b6597.tar.gz
fiv-1c40fa8adbb80197e8b911571128920b2e2b6597.tar.xz
fiv-1c40fa8adbb80197e8b911571128920b2e2b6597.zip
Add an "Open With" context menu to browser items
-rw-r--r--fastiv-browser.c168
-rw-r--r--fastiv.c6
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;
}
diff --git a/fastiv.c b/fastiv.c
index 5aabd46..98dd0cb 100644
--- a/fastiv.c
+++ b/fastiv.c
@@ -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: