on_app_activate() currently makes use of the CWD we are launched with, so I'm choosing to not enforce it globally.
		
			
				
	
	
		
			574 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			574 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
//
 | 
						|
// fiv-context-menu.c: popup menu
 | 
						|
//
 | 
						|
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
 | 
						|
//
 | 
						|
// 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-collection.h"
 | 
						|
#include "fiv-context-menu.h"
 | 
						|
 | 
						|
G_DEFINE_QUARK(fiv-context-menu-cancellable-quark, fiv_context_menu_cancellable)
 | 
						|
 | 
						|
static GtkWidget *
 | 
						|
info_start_group(GtkWidget *vbox, const char *group)
 | 
						|
{
 | 
						|
	GtkWidget *label = gtk_label_new(group);
 | 
						|
	gtk_widget_set_hexpand(label, TRUE);
 | 
						|
	gtk_widget_set_halign(label, GTK_ALIGN_FILL);
 | 
						|
	PangoAttrList *attrs = pango_attr_list_new();
 | 
						|
	pango_attr_list_insert(attrs, pango_attr_weight_new(PANGO_WEIGHT_BOLD));
 | 
						|
	gtk_label_set_attributes(GTK_LABEL(label), attrs);
 | 
						|
	pango_attr_list_unref(attrs);
 | 
						|
 | 
						|
	GtkWidget *grid = gtk_grid_new();
 | 
						|
	GtkWidget *expander = gtk_expander_new(NULL);
 | 
						|
	gtk_expander_set_label_widget(GTK_EXPANDER(expander), label);
 | 
						|
	gtk_expander_set_expanded(GTK_EXPANDER(expander), TRUE);
 | 
						|
	gtk_container_add(GTK_CONTAINER(expander), grid);
 | 
						|
	gtk_grid_set_column_spacing(GTK_GRID(grid), 10);
 | 
						|
	gtk_box_pack_start(GTK_BOX(vbox), expander, FALSE, FALSE, 0);
 | 
						|
	return grid;
 | 
						|
}
 | 
						|
 | 
						|
static GtkWidget *
 | 
						|
info_parse(char *tsv)
 | 
						|
{
 | 
						|
	GtkSizeGroup *sg = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
 | 
						|
	GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
 | 
						|
 | 
						|
	const char *last_group = NULL;
 | 
						|
	GtkWidget *grid = NULL;
 | 
						|
	int line = 1, row = 0;
 | 
						|
	for (char *nl; (nl = strchr(tsv, '\n')); line++, tsv = ++nl) {
 | 
						|
		*nl = 0;
 | 
						|
		if (nl > tsv && nl[-1] == '\r')
 | 
						|
			nl[-1] = 0;
 | 
						|
 | 
						|
		char *group = tsv, *tag = strchr(group, '\t');
 | 
						|
		if (!tag) {
 | 
						|
			g_warning("ExifTool parse error on line %d", line);
 | 
						|
			continue;
 | 
						|
		}
 | 
						|
 | 
						|
		*tag++ = 0;
 | 
						|
		for (char *p = group; *p; p++)
 | 
						|
			if (*p == '_')
 | 
						|
				*p = ' ';
 | 
						|
 | 
						|
		char *value = strchr(tag, '\t');
 | 
						|
		if (!value) {
 | 
						|
			g_warning("ExifTool parse error on line %d", line);
 | 
						|
			continue;
 | 
						|
		}
 | 
						|
 | 
						|
		*value++ = 0;
 | 
						|
		if (!last_group || strcmp(last_group, group)) {
 | 
						|
			grid = info_start_group(vbox, (last_group = group));
 | 
						|
			row = 0;
 | 
						|
		}
 | 
						|
 | 
						|
		GtkWidget *a = gtk_label_new(tag);
 | 
						|
		gtk_size_group_add_widget(sg, a);
 | 
						|
		gtk_label_set_selectable(GTK_LABEL(a), TRUE);
 | 
						|
		gtk_label_set_xalign(GTK_LABEL(a), 0.);
 | 
						|
		gtk_grid_attach(GTK_GRID(grid), a, 0, row, 1, 1);
 | 
						|
 | 
						|
		GtkWidget *b = gtk_label_new(value);
 | 
						|
		gtk_label_set_selectable(GTK_LABEL(b), TRUE);
 | 
						|
		gtk_label_set_xalign(GTK_LABEL(b), 0.);
 | 
						|
		gtk_label_set_line_wrap(GTK_LABEL(b), TRUE);
 | 
						|
		gtk_widget_set_hexpand(b, TRUE);
 | 
						|
		gtk_grid_attach(GTK_GRID(grid), b, 1, row, 1, 1);
 | 
						|
		row++;
 | 
						|
	}
 | 
						|
	g_object_unref(sg);
 | 
						|
	return vbox;
 | 
						|
}
 | 
						|
 | 
						|
static GtkWidget *
 | 
						|
info_make_bar(const char *message)
 | 
						|
{
 | 
						|
	GtkWidget *info = gtk_info_bar_new();
 | 
						|
	gtk_info_bar_set_message_type(GTK_INFO_BAR(info), GTK_MESSAGE_WARNING);
 | 
						|
	GtkWidget *info_area = gtk_info_bar_get_content_area(GTK_INFO_BAR(info));
 | 
						|
	// When the label is made selectable, Escape doesn't work when it has focus.
 | 
						|
	gtk_container_add(GTK_CONTAINER(info_area), gtk_label_new(message));
 | 
						|
	return info;
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
info_redirect_error(gpointer dialog, GError *error)
 | 
						|
{
 | 
						|
	// The dialog has been closed and destroyed.
 | 
						|
	if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
 | 
						|
		g_error_free(error);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	GtkContainer *content_area =
 | 
						|
		GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(dialog)));
 | 
						|
	gtk_container_foreach(content_area, (GtkCallback) gtk_widget_destroy, NULL);
 | 
						|
	gtk_container_add(content_area, info_make_bar(error->message));
 | 
						|
	if (g_error_matches(error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT)) {
 | 
						|
		gtk_box_pack_start(GTK_BOX(content_area),
 | 
						|
			gtk_label_new("Please install ExifTool."), TRUE, FALSE, 12);
 | 
						|
	}
 | 
						|
 | 
						|
	g_error_free(error);
 | 
						|
	gtk_widget_show_all(GTK_WIDGET(dialog));
 | 
						|
}
 | 
						|
 | 
						|
static gchar *
 | 
						|
bytes_to_utf8(GBytes *bytes)
 | 
						|
{
 | 
						|
	gsize length = 0;
 | 
						|
	gconstpointer data = g_bytes_get_data(bytes, &length);
 | 
						|
	gchar *utf8 = data ? g_utf8_make_valid(data, length) : g_strdup("");
 | 
						|
	g_bytes_unref(bytes);
 | 
						|
	return utf8;
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
on_info_finished(GObject *source_object, GAsyncResult *res, gpointer user_data)
 | 
						|
{
 | 
						|
	GError *error = NULL;
 | 
						|
	GBytes *bytes_out = NULL, *bytes_err = NULL;
 | 
						|
	if (!g_subprocess_communicate_finish(
 | 
						|
			G_SUBPROCESS(source_object), res, &bytes_out, &bytes_err, &error)) {
 | 
						|
		info_redirect_error(user_data, error);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	gchar *out = bytes_to_utf8(bytes_out);
 | 
						|
	gchar *err = bytes_to_utf8(bytes_err);
 | 
						|
 | 
						|
	GtkWidget *dialog = GTK_WIDGET(user_data);
 | 
						|
	GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
 | 
						|
	gtk_container_foreach(
 | 
						|
		GTK_CONTAINER(content_area), (GtkCallback) gtk_widget_destroy, NULL);
 | 
						|
 | 
						|
	GtkWidget *scroller = gtk_scrolled_window_new(NULL, NULL);
 | 
						|
	gtk_box_pack_start(GTK_BOX(content_area), scroller, TRUE, TRUE, 0);
 | 
						|
	GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
 | 
						|
	gtk_container_add(GTK_CONTAINER(scroller), vbox);
 | 
						|
	if (*err)
 | 
						|
		gtk_container_add(GTK_CONTAINER(vbox), info_make_bar(g_strstrip(err)));
 | 
						|
 | 
						|
	GtkWidget *info = info_parse(out);
 | 
						|
	gtk_style_context_add_class(
 | 
						|
		gtk_widget_get_style_context(info), "fiv-information");
 | 
						|
	gtk_box_pack_start(GTK_BOX(vbox), info, TRUE, TRUE, 0);
 | 
						|
 | 
						|
	g_free(out);
 | 
						|
	g_free(err);
 | 
						|
	gtk_widget_show_all(dialog);
 | 
						|
	gtk_widget_grab_focus(scroller);
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
info_spawn(GtkWidget *dialog, const char *path, GBytes *bytes_in)
 | 
						|
{
 | 
						|
	int flags = G_SUBPROCESS_FLAGS_STDOUT_PIPE | G_SUBPROCESS_FLAGS_STDERR_PIPE;
 | 
						|
	if (bytes_in)
 | 
						|
		flags |= G_SUBPROCESS_FLAGS_STDIN_PIPE;
 | 
						|
 | 
						|
	GSubprocessLauncher *launcher = g_subprocess_launcher_new(flags);
 | 
						|
#ifdef G_OS_WIN32
 | 
						|
	// Both to find wperl, and then to let wperl find the nearby exiftool.
 | 
						|
	gchar *prefix = g_win32_get_package_installation_directory_of_module(NULL);
 | 
						|
	g_subprocess_launcher_set_cwd(launcher, prefix);
 | 
						|
	g_free(prefix);
 | 
						|
#endif
 | 
						|
 | 
						|
	// TODO(p): Add a fallback to internal capabilities.
 | 
						|
	// The simplest is to specify the filename and the resolution.
 | 
						|
	GError *error = NULL;
 | 
						|
	GSubprocess *subprocess = g_subprocess_launcher_spawn(launcher, &error,
 | 
						|
#ifdef G_OS_WIN32
 | 
						|
		"wperl",
 | 
						|
#endif
 | 
						|
		"exiftool", "-tab", "-groupNames", "-duplicates", "-extractEmbedded",
 | 
						|
		"--binary", "-quiet", "--", path, NULL);
 | 
						|
	g_object_unref(launcher);
 | 
						|
	if (error) {
 | 
						|
		info_redirect_error(dialog, error);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	GCancellable *cancellable = g_object_get_qdata(
 | 
						|
		G_OBJECT(dialog), fiv_context_menu_cancellable_quark());
 | 
						|
	g_subprocess_communicate_async(
 | 
						|
		subprocess, bytes_in, cancellable, on_info_finished, dialog);
 | 
						|
	g_object_unref(subprocess);
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
on_info_loaded(GObject *source_object, GAsyncResult *res, gpointer user_data)
 | 
						|
{
 | 
						|
	gchar *file_data = NULL;
 | 
						|
	gsize file_len = 0;
 | 
						|
	GError *error = NULL;
 | 
						|
	if (!g_file_load_contents_finish(
 | 
						|
			G_FILE(source_object), res, &file_data, &file_len, NULL, &error)) {
 | 
						|
		info_redirect_error(user_data, error);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	GtkWidget *dialog = GTK_WIDGET(user_data);
 | 
						|
	GBytes *bytes_in = g_bytes_new_take(file_data, file_len);
 | 
						|
	info_spawn(dialog, "-", bytes_in);
 | 
						|
	g_bytes_unref(bytes_in);
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
on_info_queried(GObject *source_object, GAsyncResult *res, gpointer user_data)
 | 
						|
{
 | 
						|
	GFile *file = G_FILE(source_object);
 | 
						|
	GError *error = NULL;
 | 
						|
	GFileInfo *info = g_file_query_info_finish(file, res, &error);
 | 
						|
	gboolean cancelled =
 | 
						|
		error && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
 | 
						|
	g_clear_error(&error);
 | 
						|
	if (cancelled)
 | 
						|
		return;
 | 
						|
 | 
						|
	gchar *path = NULL;
 | 
						|
	const char *target_uri = g_file_info_get_attribute_string(
 | 
						|
		info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI);
 | 
						|
	if (target_uri) {
 | 
						|
		GFile *target = g_file_new_for_uri(target_uri);
 | 
						|
		path = g_file_get_path(target);
 | 
						|
		g_object_unref(target);
 | 
						|
	}
 | 
						|
	g_object_unref(info);
 | 
						|
 | 
						|
	GtkWidget *dialog = GTK_WIDGET(user_data);
 | 
						|
	GCancellable *cancellable = g_object_get_qdata(
 | 
						|
		G_OBJECT(dialog), fiv_context_menu_cancellable_quark());
 | 
						|
	if (path) {
 | 
						|
		info_spawn(dialog, path, NULL);
 | 
						|
		g_free(path);
 | 
						|
	} else {
 | 
						|
		g_file_load_contents_async(file, cancellable, on_info_loaded, dialog);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void
 | 
						|
fiv_context_menu_information(GtkWindow *parent, const char *uri)
 | 
						|
{
 | 
						|
	GtkWidget *dialog = gtk_widget_new(GTK_TYPE_DIALOG,
 | 
						|
		"use-header-bar", TRUE,
 | 
						|
		"title", "Information",
 | 
						|
		"transient-for", parent,
 | 
						|
		"destroy-with-parent", TRUE, NULL);
 | 
						|
 | 
						|
	// When the window closes, we cancel all asynchronous calls.
 | 
						|
	GCancellable *cancellable = g_cancellable_new();
 | 
						|
	g_object_set_qdata_full(G_OBJECT(dialog),
 | 
						|
		fiv_context_menu_cancellable_quark(), cancellable, g_object_unref);
 | 
						|
	g_signal_connect_swapped(
 | 
						|
		dialog, "destroy", G_CALLBACK(g_cancellable_cancel), cancellable);
 | 
						|
 | 
						|
	GtkWidget *spinner = gtk_spinner_new();
 | 
						|
	gtk_spinner_start(GTK_SPINNER(spinner));
 | 
						|
	gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))),
 | 
						|
		spinner, TRUE, TRUE, 12);
 | 
						|
	gtk_window_set_default_size(GTK_WINDOW(dialog), 600, 800);
 | 
						|
	gtk_widget_show_all(dialog);
 | 
						|
 | 
						|
	// Mostly to identify URIs with no local path--we pipe these into ExifTool.
 | 
						|
	GFile *file = g_file_new_for_uri(uri);
 | 
						|
	gchar *parse_name = g_file_get_parse_name(file);
 | 
						|
	gtk_header_bar_set_subtitle(
 | 
						|
		GTK_HEADER_BAR(gtk_dialog_get_header_bar(GTK_DIALOG(dialog))),
 | 
						|
		parse_name);
 | 
						|
	g_free(parse_name);
 | 
						|
 | 
						|
	gchar *path = g_file_get_path(file);
 | 
						|
	if (path) {
 | 
						|
		info_spawn(dialog, path, NULL);
 | 
						|
		g_free(path);
 | 
						|
	} else {
 | 
						|
		// Several GVfs schemes contain pseudo-symlinks
 | 
						|
		// that don't give out filesystem paths directly.
 | 
						|
		g_file_query_info_async(file, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI,
 | 
						|
			G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, cancellable,
 | 
						|
			on_info_queried, dialog);
 | 
						|
	}
 | 
						|
	g_object_unref(file);
 | 
						|
}
 | 
						|
 | 
						|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
						|
 | 
						|
typedef struct _OpenContext {
 | 
						|
	GWeakRef window;                    ///< Parent window for any dialogs
 | 
						|
	GFile *file;                        ///< The file in question
 | 
						|
	gchar *content_type;
 | 
						|
	GAppInfo *app_info;
 | 
						|
} OpenContext;
 | 
						|
 | 
						|
static void
 | 
						|
open_context_finalize(gpointer data)
 | 
						|
{
 | 
						|
	OpenContext *self = data;
 | 
						|
	g_weak_ref_clear(&self->window);
 | 
						|
	g_clear_object(&self->app_info);
 | 
						|
	g_clear_object(&self->file);
 | 
						|
	g_free(self->content_type);
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
open_context_unref(gpointer data, G_GNUC_UNUSED GClosure *closure)
 | 
						|
{
 | 
						|
	g_rc_box_release_full(data, open_context_finalize);
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
show_error_dialog(GtkWindow *parent, GError *error)
 | 
						|
{
 | 
						|
	GtkWidget *dialog =
 | 
						|
		gtk_message_dialog_new(GTK_WINDOW(parent), GTK_DIALOG_MODAL,
 | 
						|
			GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message);
 | 
						|
	gtk_dialog_run(GTK_DIALOG(dialog));
 | 
						|
	gtk_widget_destroy(dialog);
 | 
						|
	g_error_free(error);
 | 
						|
}
 | 
						|
 | 
						|
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());
 | 
						|
 | 
						|
	GList *files = g_list_append(NULL, self->file);
 | 
						|
	GError *error = NULL;
 | 
						|
	if (g_app_info_launch(
 | 
						|
			self->app_info, files, G_APP_LAUNCH_CONTEXT(context), &error)) {
 | 
						|
		(void) g_app_info_set_as_last_used_for_type(
 | 
						|
			self->app_info, self->content_type, NULL);
 | 
						|
	} else {
 | 
						|
		GtkWindow *window = g_weak_ref_get(&self->window);
 | 
						|
		show_error_dialog(window, error);
 | 
						|
		g_clear_object(&window);
 | 
						|
	}
 | 
						|
	g_list_free(files);
 | 
						|
	g_object_unref(context);
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
append_opener(GtkWidget *menu, GAppInfo *opener, const OpenContext *template)
 | 
						|
{
 | 
						|
	OpenContext *ctx = g_rc_box_alloc0(sizeof *ctx);
 | 
						|
	g_weak_ref_init(&ctx->window, 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_unref, 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 = g_weak_ref_get(&ctx->window);
 | 
						|
	GtkWidget *dialog = gtk_app_chooser_dialog_new_for_content_type(window,
 | 
						|
		GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, ctx->content_type);
 | 
						|
	g_clear_object(&window);
 | 
						|
 | 
						|
#if 0
 | 
						|
	// This exists as a concept in mimeapps.list, but GNOME infuriatingly
 | 
						|
	// infers it from the last used application if missing.
 | 
						|
	gtk_app_chooser_widget_set_show_default(
 | 
						|
		GTK_APP_CHOOSER_WIDGET(gtk_app_chooser_dialog_get_widget(
 | 
						|
			GTK_APP_CHOOSER_DIALOG(dialog))), TRUE);
 | 
						|
#endif
 | 
						|
 | 
						|
	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 void
 | 
						|
on_info_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
 | 
						|
{
 | 
						|
	OpenContext *ctx = user_data;
 | 
						|
	GtkWindow *window = g_weak_ref_get(&ctx->window);
 | 
						|
	gchar *uri = g_file_get_uri(ctx->file);
 | 
						|
	fiv_context_menu_information(window, uri);
 | 
						|
	g_clear_object(&window);
 | 
						|
	g_free(uri);
 | 
						|
}
 | 
						|
 | 
						|
void
 | 
						|
fiv_context_menu_remove(GtkWindow *parent, GFile *file)
 | 
						|
{
 | 
						|
	// TODO(p): Use g_file_trash_async(), for which we need a task manager.
 | 
						|
	GError *error = NULL;
 | 
						|
	if (!g_file_trash(file, NULL, &error))
 | 
						|
		show_error_dialog(parent, error);
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
on_trash_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
 | 
						|
{
 | 
						|
	OpenContext *ctx = user_data;
 | 
						|
	GtkWindow *window = g_weak_ref_get(&ctx->window);
 | 
						|
	fiv_context_menu_remove(window, ctx->file);
 | 
						|
	g_clear_object(&window);
 | 
						|
}
 | 
						|
 | 
						|
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_TYPE
 | 
						|
		"," G_FILE_ATTRIBUTE_STANDARD_NAME
 | 
						|
		"," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE
 | 
						|
		"," G_FILE_ATTRIBUTE_STANDARD_TARGET_URI,
 | 
						|
		G_FILE_QUERY_INFO_NONE, NULL, NULL);
 | 
						|
	if (!info)
 | 
						|
		return NULL;
 | 
						|
 | 
						|
	GtkWindow *window = NULL;
 | 
						|
	if (widget && GTK_IS_WINDOW((widget = gtk_widget_get_toplevel(widget))))
 | 
						|
		window = GTK_WINDOW(widget);
 | 
						|
 | 
						|
	// This will have no application pre-assigned, for use with GTK+'s dialog.
 | 
						|
	OpenContext *ctx = g_rc_box_alloc0(sizeof *ctx);
 | 
						|
	g_weak_ref_init(&ctx->window, window);
 | 
						|
	if (!(ctx->content_type = g_strdup(g_file_info_get_content_type(info))))
 | 
						|
		ctx->content_type = g_content_type_guess(NULL, NULL, 0, NULL);
 | 
						|
 | 
						|
	GFileType type = g_file_info_get_file_type(info);
 | 
						|
	const char *target_uri = g_file_info_get_attribute_string(
 | 
						|
		info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI);
 | 
						|
	ctx->file = target_uri && g_file_has_uri_scheme(file, FIV_COLLECTION_SCHEME)
 | 
						|
		? g_file_new_for_uri(target_uri)
 | 
						|
		: g_object_ref(file);
 | 
						|
	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_unref, 0);
 | 
						|
	gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
 | 
						|
 | 
						|
	// TODO(p): Can we avoid using the "trash" string constant for this check?
 | 
						|
	if (!g_file_has_uri_scheme(file, "trash")) {
 | 
						|
		gtk_menu_shell_append(
 | 
						|
			GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
 | 
						|
 | 
						|
		item = gtk_menu_item_new_with_mnemonic("Move to _Trash");
 | 
						|
		g_signal_connect_data(item, "activate", G_CALLBACK(on_trash_activate),
 | 
						|
			g_rc_box_acquire(ctx), open_context_unref, 0);
 | 
						|
		gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
 | 
						|
	}
 | 
						|
	if (type == G_FILE_TYPE_REGULAR) {
 | 
						|
		gtk_menu_shell_append(
 | 
						|
			GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
 | 
						|
 | 
						|
		item = gtk_menu_item_new_with_mnemonic("_Information");
 | 
						|
		g_signal_connect_data(item, "activate", G_CALLBACK(on_info_activate),
 | 
						|
			g_rc_box_acquire(ctx), open_context_unref, 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);
 | 
						|
}
 |