diff options
| author | Přemysl Eric Janouch <p@janouch.name> | 2022-08-05 11:33:23 +0200 | 
|---|---|---|
| committer | Přemysl Eric Janouch <p@janouch.name> | 2022-08-05 11:38:12 +0200 | 
| commit | 086dd66aa9c3cc333304579330b6e773e9c5071f (patch) | |
| tree | e94bc7b32ce75b955f6feb1a2a7598c2aeebe26c /fiv-context-menu.c | |
| parent | 8c6fe0ad321a263ee56a3d6439d7b99630a1755c (diff) | |
| download | fiv-086dd66aa9c3cc333304579330b6e773e9c5071f.tar.gz fiv-086dd66aa9c3cc333304579330b6e773e9c5071f.tar.xz fiv-086dd66aa9c3cc333304579330b6e773e9c5071f.zip  | |
Add the information dialog to context menus
Images don't need to be open for ExifTool to work.
This also enables inspecting unsupported files, such as video.
Diffstat (limited to 'fiv-context-menu.c')
| -rw-r--r-- | fiv-context-menu.c | 342 | 
1 files changed, 324 insertions, 18 deletions
diff --git a/fiv-context-menu.c b/fiv-context-menu.c index 1a4b022..9bcb51f 100644 --- a/fiv-context-menu.c +++ b/fiv-context-menu.c @@ -19,22 +19,306 @@  #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)); +	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); +} + +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; + +	// 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_new(flags, &error, "exiftool", +		"-tab", "-groupNames", "-duplicates", "-extractEmbedded", "--binary", +		"-quiet", "--", path, NULL); +	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 for 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 widget; -	GFile *file; +	GWeakRef window;                    ///< Parent window for any dialogs +	GFile *file;                        ///< The file in question  	gchar *content_type;  	GAppInfo *app_info;  } OpenContext;  static void -open_context_notify(gpointer data, G_GNUC_UNUSED GClosure *closure) +open_context_finalize(gpointer data)  {  	OpenContext *self = data; -	g_weak_ref_clear(&self->widget); +	g_weak_ref_clear(&self->window);  	g_clear_object(&self->app_info);  	g_clear_object(&self->file);  	g_free(self->content_type); -	g_free(self); +} + +static void +open_context_unref(gpointer data, G_GNUC_UNUSED GClosure *closure) +{ +	g_rc_box_release_full(data, open_context_finalize);  }  static void @@ -62,8 +346,8 @@ open_context_launch(GtkWidget *widget, OpenContext *self)  static void  append_opener(GtkWidget *menu, GAppInfo *opener, const OpenContext *template)  { -	OpenContext *ctx = g_malloc0(sizeof *ctx); -	g_weak_ref_init(&ctx->widget, NULL); +	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; @@ -94,7 +378,7 @@ append_opener(GtkWidget *menu, GAppInfo *opener, const OpenContext *template)  	g_free(name);  	g_signal_connect_data(item, "activate", G_CALLBACK(open_context_launch), -		ctx, open_context_notify, 0); +		ctx, open_context_unref, 0);  	gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);  } @@ -102,15 +386,10 @@ 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); -	} - +	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 (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); @@ -118,6 +397,17 @@ on_chooser_activate(GtkMenuItem *item, gpointer user_data)  	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); +} +  static gboolean  destroy_widget_idle_source_func(GtkWidget *widget)  { @@ -132,16 +422,22 @@ fiv_context_menu_new(GtkWidget *widget, GFile *file)  {  	GFileInfo *info = g_file_query_info(file,  		G_FILE_ATTRIBUTE_STANDARD_NAME +		"," G_FILE_ATTRIBUTE_STANDARD_TYPE  		"," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,  		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_malloc0(sizeof *ctx); -	g_weak_ref_init(&ctx->widget, widget); +	OpenContext *ctx = g_rc_box_alloc0(sizeof *ctx); +	g_weak_ref_init(&ctx->window, window);  	ctx->file = g_object_ref(file);  	ctx->content_type = g_strdup(g_file_info_get_content_type(info)); +	gboolean regular = g_file_info_get_file_type(info) == G_FILE_TYPE_REGULAR;  	g_object_unref(info);  	GAppInfo *default_ = @@ -182,9 +478,19 @@ fiv_context_menu_new(GtkWidget *widget, GFile *file)  	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); +		ctx, open_context_unref, 0);  	gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); +	if (regular) { +		gtk_menu_shell_append( +			GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + +		item = gtk_menu_item_new_with_label("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",  | 
