Přemysl Eric Janouch
701846ab39
Implement a process-local VFS to enable grouping together arbitrary URIs passed via program arguments, DnD, or the file open dialog. This VFS contains FivCollectionFile objects, which act as "simple" proxies over arbitrary GFiles. Their true URIs may be retrieved through the "standard::target-uri" attribute, in a similar way to GVfs's "recent" and "trash" backends. (The main reason we proxy rather than just hackishly return foreign GFiles from the VFS is that loading them would switch the current directory, and break iteration as a result. We could also keep the collection outside of GVfs, but that would result in considerable special-casing, and the author wouldn't gain intimate knowledge of GIO.) There is no perceived need to keep old collections when opening new ones, so we simply change and reload the contents when needed. Similarly, there is no intention to make the VFS writeable. The process-locality of this and other URI schemes has proven to be rather annoying when passing files to other applications, however most of the resulting complexity appears to be essential rather than accidental. Note that the GTK+ file chooser widget is retarded, and doesn't recognize URIs that lack the authority part in the location bar.
513 lines
16 KiB
C
513 lines
16 KiB
C
//
|
|
// fiv-context-menu.c: popup menu
|
|
//
|
|
// Copyright (c) 2021 - 2022, 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;
|
|
|
|
// 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 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
|
|
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 {
|
|
g_warning("%s", error->message);
|
|
g_error_free(error);
|
|
}
|
|
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 (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);
|
|
}
|
|
|
|
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);
|
|
|
|
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);
|
|
}
|