diff --git a/fiv-browser.c b/fiv-browser.c index 3edcf4a..c4475a7 100644 --- a/fiv-browser.c +++ b/fiv-browser.c @@ -29,6 +29,7 @@ #endif // GDK_WINDOWING_QUARTZ #include "fiv-browser.h" +#include "fiv-collection.h" #include "fiv-context-menu.h" #include "fiv-io.h" #include "fiv-thumbnail.h" @@ -102,6 +103,7 @@ static cairo_user_data_key_t fiv_browser_key_mtime_msec; struct entry { gchar *uri; ///< GIO URI + gchar *target_uri; ///< GIO URI for any target gint64 mtime_msec; ///< Modification time in milliseconds cairo_surface_t *thumbnail; ///< Prescaled thumbnail GIcon *icon; ///< If no thumbnail, use this icon @@ -111,6 +113,7 @@ static void entry_free(Entry *self) { g_free(self->uri); + g_free(self->target_uri); g_clear_pointer(&self->thumbnail, cairo_surface_destroy); g_clear_object(&self->icon); } @@ -437,6 +440,17 @@ rescale_thumbnail(cairo_surface_t *thumbnail, double row_height) return scaled; } +static char * +entry_system_wide_uri(const Entry *self) +{ + // "recent" and "trash", e.g., also have "standard::target-uri" set, + // but we'd like to avoid saving their thumbnails. + if (self->target_uri && fiv_collection_uri_matches(self->uri)) + return self->target_uri; + + return self->uri; +} + static void entry_add_thumbnail(gpointer data, gpointer user_data) { @@ -456,7 +470,7 @@ entry_add_thumbnail(gpointer data, gpointer user_data) // unnecessarily; we might also shift the concern there). } else { cairo_surface_t *found = fiv_thumbnail_lookup( - self->uri, self->mtime_msec, browser->item_size); + entry_system_wide_uri(self), self->mtime_msec, browser->item_size); self->thumbnail = rescale_thumbnail(found, browser->item_height); } @@ -589,7 +603,8 @@ thumbnailer_reprocess_entry(FivBrowser *self, GBytes *output, Entry *entry) g_list_append(self->thumbnailers_queue, entry); } - // This choice of mtime favours unnecessary thumbnail reloading. + // This choice of mtime favours unnecessary thumbnail reloading + // over retaining stale data. cairo_surface_set_user_data(entry->thumbnail, &fiv_browser_key_mtime_msec, (void *) (intptr_t) entry->mtime_msec, NULL); @@ -663,12 +678,13 @@ thumbnailer_next(Thumbnailer *t) // - We've found one, but we're not quite happy with it: // always run the full process for a high-quality wide thumbnail. // - We can't end up here in any other cases. + const char *uri = entry_system_wide_uri(t->target); const char *argv_faster[] = {PROJECT_NAME, "--extract-thumbnail", "--thumbnail", fiv_thumbnail_sizes[self->item_size].thumbnail_spec_name, - "--", t->target->uri, NULL}; + "--", uri, NULL}; const char *argv_slower[] = {PROJECT_NAME, "--thumbnail", fiv_thumbnail_sizes[self->item_size].thumbnail_spec_name, - "--", t->target->uri, NULL}; + "--", uri, NULL}; GError *error = NULL; t->minion = g_subprocess_newv(t->target->icon ? argv_faster : argv_slower, @@ -1259,7 +1275,7 @@ fiv_browser_drag_data_get(GtkWidget *widget, FivBrowser *self = FIV_BROWSER(widget); if (self->selected) { (void) gtk_selection_data_set_uris( - data, (gchar *[]){self->selected->uri, NULL}); + data, (gchar *[]) {entry_system_wide_uri(self->selected), NULL}); } } @@ -1466,10 +1482,9 @@ fiv_browser_query_tooltip(GtkWidget *widget, gint x, gint y, return FALSE; GFile *file = g_file_new_for_uri(entry->uri); - GFileInfo *info = g_file_query_info(file, - G_FILE_ATTRIBUTE_STANDARD_NAME - "," G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, - G_FILE_QUERY_INFO_NONE, NULL, NULL); + GFileInfo *info = + g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, + G_FILE_QUERY_INFO_NONE, NULL, NULL); g_object_unref(file); if (!info) return FALSE; @@ -1740,8 +1755,11 @@ on_model_files_changed(FivIoModel *model, FivBrowser *self) gsize len = 0; const FivIoModelEntry *files = fiv_io_model_get_files(self->model, &len); for (gsize i = 0; i < len; i++) { - g_array_append_val(self->entries, ((Entry) {.thumbnail = NULL, - .uri = g_strdup(files[i].uri), .mtime_msec = files[i].mtime_msec})); + Entry e = {.thumbnail = NULL, + .uri = g_strdup(files[i].uri), + .target_uri = g_strdup(files[i].target_uri), + .mtime_msec = files[i].mtime_msec}; + g_array_append_val(self->entries, e); } fiv_browser_select(self, selected_uri); diff --git a/fiv-collection.c b/fiv-collection.c new file mode 100644 index 0000000..13548b9 --- /dev/null +++ b/fiv-collection.c @@ -0,0 +1,725 @@ +// +// fiv-collection.c: GVfs extension for grouping arbitrary files together +// +// Copyright (c) 2022, Přemysl Eric Janouch +// +// 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 + +#include "fiv-collection.h" + +static struct { + GFile **files; + gsize files_len; +} g; + +gboolean +fiv_collection_uri_matches(const char *uri) +{ + static const char prefix[] = FIV_COLLECTION_SCHEME ":"; + return !g_ascii_strncasecmp(uri, prefix, sizeof prefix - 1); +} + +GFile ** +fiv_collection_get_contents(gsize *len) +{ + *len = g.files_len; + return g.files; +} + +void +fiv_collection_reload(gchar **uris) +{ + if (g.files) { + for (gsize i = 0; i < g.files_len; i++) + g_object_unref(g.files[i]); + g_free(g.files); + } + + g.files_len = g_strv_length(uris); + g.files = g_malloc0_n(g.files_len + 1, sizeof *g.files); + for (gsize i = 0; i < g.files_len; i++) + g.files[i] = g_file_new_for_uri(uris[i]); +} + +// --- Declarations ------------------------------------------------------------ + +#define FIV_TYPE_COLLECTION_FILE (fiv_collection_file_get_type()) +G_DECLARE_FINAL_TYPE( + FivCollectionFile, fiv_collection_file, FIV, COLLECTION_FILE, GObject) + +struct _FivCollectionFile { + GObject parent_instance; + + gint index; ///< Original index into g.files, or -1 + GFile *target; ///< The wrapped file, or NULL for root + gchar *subpath; ///< Any subpath, rooted at the target +}; + +#define FIV_TYPE_COLLECTION_ENUMERATOR (fiv_collection_enumerator_get_type()) +G_DECLARE_FINAL_TYPE(FivCollectionEnumerator, fiv_collection_enumerator, FIV, + COLLECTION_ENUMERATOR, GFileEnumerator) + +struct _FivCollectionEnumerator { + GFileEnumerator parent_instance; + + gchar *attributes; ///< Attributes to look up + gsize index; ///< Root: index into g.files + GFileEnumerator *subenumerator; ///< Non-root: a wrapped enumerator +}; + +// --- Enumerator -------------------------------------------------------------- + +G_DEFINE_TYPE( + FivCollectionEnumerator, fiv_collection_enumerator, G_TYPE_FILE_ENUMERATOR) + +static void +fiv_collection_enumerator_finalize(GObject *object) +{ + FivCollectionEnumerator *self = FIV_COLLECTION_ENUMERATOR(object); + g_free(self->attributes); + g_clear_object(&self->subenumerator); +} + +static GFileInfo * +fiv_collection_enumerator_next_file(GFileEnumerator *enumerator, + GCancellable *cancellable, GError **error) +{ + FivCollectionEnumerator *self = FIV_COLLECTION_ENUMERATOR(enumerator); + if (self->subenumerator) { + GFileInfo *info = g_file_enumerator_next_file( + self->subenumerator, cancellable, error); + if (!info) + return NULL; + + // TODO(p): Consider discarding certain classes of attributes + // from the results (adjusting "attributes" is generally unreliable). + GFile *target = g_file_enumerator_get_child(self->subenumerator, info); + gchar *target_uri = g_file_get_uri(target); + g_object_unref(target); + g_file_info_set_attribute_string( + info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI, target_uri); + g_free(target_uri); + return info; + } + + if (self->index >= g.files_len) + return NULL; + + FivCollectionFile *file = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL); + file->index = self->index; + file->target = g_object_ref(g.files[self->index++]); + + GFileInfo *info = g_file_query_info(G_FILE(file), self->attributes, + G_FILE_QUERY_INFO_NONE, cancellable, error); + g_object_unref(file); + return info; +} + +static gboolean +fiv_collection_enumerator_close( + GFileEnumerator *enumerator, GCancellable *cancellable, GError **error) +{ + FivCollectionEnumerator *self = FIV_COLLECTION_ENUMERATOR(enumerator); + if (self->subenumerator) + return g_file_enumerator_close(self->subenumerator, cancellable, error); + return TRUE; +} + +static void +fiv_collection_enumerator_class_init(FivCollectionEnumeratorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + object_class->finalize = fiv_collection_enumerator_finalize; + + GFileEnumeratorClass *enumerator_class = G_FILE_ENUMERATOR_CLASS(klass); + enumerator_class->next_file = fiv_collection_enumerator_next_file; + enumerator_class->close_fn = fiv_collection_enumerator_close; +} + +static void +fiv_collection_enumerator_init(G_GNUC_UNUSED FivCollectionEnumerator *self) +{ +} + +// --- Proxying GFile implementation ------------------------------------------- + +static void fiv_collection_file_file_iface_init(GFileIface *iface); + +G_DEFINE_TYPE_WITH_CODE(FivCollectionFile, fiv_collection_file, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(G_TYPE_FILE, fiv_collection_file_file_iface_init)) + +static void +fiv_collection_file_finalize(GObject *object) +{ + FivCollectionFile *self = FIV_COLLECTION_FILE(object); + if (self->target) + g_object_unref(self->target); + g_free(self->subpath); +} + +static GFile * +fiv_collection_file_dup(GFile *file) +{ + FivCollectionFile *self = FIV_COLLECTION_FILE(file); + FivCollectionFile *new = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL); + if (self->target) + new->target = g_object_ref(self->target); + new->subpath = g_strdup(self->subpath); + return G_FILE(new); +} + +static guint +fiv_collection_file_hash(GFile *file) +{ + FivCollectionFile *self = FIV_COLLECTION_FILE(file); + guint hash = g_int_hash(&self->index); + if (self->target) + hash ^= g_file_hash(self->target); + if (self->subpath) + hash ^= g_str_hash(self->subpath); + return hash; +} + +static gboolean +fiv_collection_file_equal(GFile *file1, GFile *file2) +{ + FivCollectionFile *cf1 = FIV_COLLECTION_FILE(file1); + FivCollectionFile *cf2 = FIV_COLLECTION_FILE(file2); + return cf1->index == cf2->index && cf1->target == cf2->target && + !g_strcmp0(cf1->subpath, cf2->subpath); +} + +static gboolean +fiv_collection_file_is_native(G_GNUC_UNUSED GFile *file) +{ + return FALSE; +} + +static gboolean +fiv_collection_file_has_uri_scheme( + G_GNUC_UNUSED GFile *file, const char *uri_scheme) +{ + return !g_ascii_strcasecmp(uri_scheme, FIV_COLLECTION_SCHEME); +} + +static char * +fiv_collection_file_get_uri_scheme(G_GNUC_UNUSED GFile *file) +{ + return g_strdup(FIV_COLLECTION_SCHEME); +} + +static char * +get_prefixed_name(FivCollectionFile *self, const char *name) +{ + return g_strdup_printf("%d. %s", self->index + 1, name); +} + +static char * +get_target_basename(FivCollectionFile *self) +{ + g_return_val_if_fail(self->target != NULL, g_strdup("")); + + // The "http" scheme doesn't behave nicely, make something up if needed. + // Foreign roots likewise need to be fixed up for our needs. + gchar *basename = g_file_get_basename(self->target); + if (!basename || *basename == '/') { + g_free(basename); + basename = g_file_get_uri_scheme(self->target); + } + + gchar *name = get_prefixed_name(self, basename); + g_free(basename); + return name; +} + +static char * +fiv_collection_file_get_basename(GFile *file) +{ + FivCollectionFile *self = FIV_COLLECTION_FILE(file); + if (!self->target) + return g_strdup("/"); + if (self->subpath) + return g_path_get_basename(self->subpath); + return get_target_basename(self); +} + +static char * +fiv_collection_file_get_path(G_GNUC_UNUSED GFile *file) +{ + // This doesn't seem to be worth implementing (for compatible targets). + return NULL; +} + +static char * +get_unescaped_uri(FivCollectionFile *self) +{ + GString *unescaped = g_string_new(FIV_COLLECTION_SCHEME ":/"); + if (!self->target) + return g_string_free(unescaped, FALSE); + + gchar *basename = get_target_basename(self); + g_string_append(unescaped, basename); + g_free(basename); + if (self->subpath) + g_string_append(g_string_append(unescaped, "/"), self->subpath); + return g_string_free(unescaped, FALSE); +} + +static char * +fiv_collection_file_get_uri(GFile *file) +{ + gchar *unescaped = get_unescaped_uri(FIV_COLLECTION_FILE(file)); + gchar *uri = g_uri_escape_string( + unescaped, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, FALSE); + g_free(unescaped); + return uri; +} + +static char * +fiv_collection_file_get_parse_name(GFile *file) +{ + gchar *unescaped = get_unescaped_uri(FIV_COLLECTION_FILE(file)); + gchar *parse_name = g_uri_escape_string( + unescaped, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH " ", TRUE); + g_free(unescaped); + return parse_name; +} + +static GFile * +fiv_collection_file_get_parent(GFile *file) +{ + FivCollectionFile *self = FIV_COLLECTION_FILE(file); + if (!self->target) + return NULL; + + FivCollectionFile *new = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL); + if (self->subpath) { + new->index = self->index; + new->target = g_object_ref(self->target); + if (strchr(self->subpath, '/')) + new->subpath = g_path_get_dirname(self->subpath); + } + return G_FILE(new); +} + +static gboolean +fiv_collection_file_prefix_matches(GFile *prefix, GFile *file) +{ + FivCollectionFile *self = FIV_COLLECTION_FILE(file); + FivCollectionFile *parent = FIV_COLLECTION_FILE(prefix); + + // The root has no parents. + if (!self->target) + return FALSE; + + // The root prefixes everything that is not the root. + if (!parent->target) + return TRUE; + + if (self->index != parent->index || !self->subpath) + return FALSE; + if (!parent->subpath) + return TRUE; + + return g_str_has_prefix(self->subpath, parent->subpath) && + self->subpath[strlen(parent->subpath)] == '/'; +} + +// This virtual method seems to be intended for local files only, +// and documentation claims that the result is in filesystem encoding. +// For us, paths are mostly opaque strings of arbitrary encoding, however. +static char * +fiv_collection_file_get_relative_path(GFile *parent, GFile *descendant) +{ + FivCollectionFile *self = FIV_COLLECTION_FILE(descendant); + FivCollectionFile *prefix = FIV_COLLECTION_FILE(parent); + if (!fiv_collection_file_prefix_matches(parent, descendant)) + return NULL; + + g_assert((!prefix->target && self->target) || + (prefix->target && self->target && self->subpath)); + + if (!prefix->target) { + gchar *basename = get_target_basename(self); + gchar *path = g_build_path("/", basename, self->subpath, NULL); + g_free(basename); + return path; + } + + return prefix->subpath + ? g_strdup(self->subpath + strlen(prefix->subpath) + 1) + : g_strdup(self->subpath); +} + +static GFile * +get_file_for_path(const char *path) +{ + // Skip all initial slashes, making the result relative to the root. + if (!*(path += strspn(path, "/"))) + return g_object_new(FIV_TYPE_COLLECTION_FILE, NULL); + + char *end = NULL; + guint64 i = g_ascii_strtoull(path, &end, 10); + if (i <= 0 || i > g.files_len || *end != '.') + return g_file_new_for_uri(""); + + FivCollectionFile *new = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL); + new->index = --i; + new->target = g_object_ref(g.files[i]); + + const char *subpath = strchr(path, '/'); + if (subpath && subpath[1]) + new->subpath = g_strdup(++subpath); + return G_FILE(new); +} + +static GFile * +fiv_collection_file_resolve_relative_path( + GFile *file, const char *relative_path) +{ + FivCollectionFile *self = FIV_COLLECTION_FILE(file); + if (!self->target) + return get_file_for_path(relative_path); + + gchar *basename = get_target_basename(self); + gchar *root = g_build_path("/", "/", basename, self->subpath, NULL); + g_free(basename); + gchar *canonicalized = g_canonicalize_filename(relative_path, root); + GFile *result = get_file_for_path(canonicalized); + g_free(canonicalized); + return result; +} + +static GFile * +get_target_subpathed(FivCollectionFile *self) +{ + return self->subpath + ? g_file_resolve_relative_path(self->target, self->subpath) + : g_object_ref(self->target); +} + +static GFile * +fiv_collection_file_get_child_for_display_name( + GFile *file, const char *display_name, GError **error) +{ + FivCollectionFile *self = FIV_COLLECTION_FILE(file); + if (!self->target) + return get_file_for_path(display_name); + + // Implementations often redirect to g_file_resolve_relative_path(). + // We don't want to go up (and possibly receive a "/" basename), + // nor do we want to skip path elements. + // TODO(p): This should still be implementable, via URI inspection. + if (strchr(display_name, '/')) { + g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Display name must not contain path separators"); + return NULL; + } + + GFile *intermediate = get_target_subpathed(self); + GFile *resolved = + g_file_get_child_for_display_name(intermediate, display_name, error); + g_object_unref(intermediate); + if (!resolved) + return NULL; + + // Try to retrieve the display name converted to whatever insanity + // the target might have chosen to encode its paths with. + gchar *converted = g_file_get_basename(resolved); + g_object_unref(resolved); + + FivCollectionFile *new = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL); + new->index = self->index; + new->target = g_object_ref(self->target); + new->subpath = self->subpath + ? g_build_path("/", self->subpath, converted, NULL) + : g_strdup(converted); + g_free(converted); + return G_FILE(new); +} + +static GFileEnumerator * +fiv_collection_file_enumerate_children(GFile *file, const char *attributes, + GFileQueryInfoFlags flags, GCancellable *cancellable, GError **error) +{ + FivCollectionFile *self = FIV_COLLECTION_FILE(file); + FivCollectionEnumerator *enumerator = g_object_new( + FIV_TYPE_COLLECTION_ENUMERATOR, "container", file, NULL); + enumerator->attributes = g_strdup(attributes); + if (self->target) { + GFile *intermediate = get_target_subpathed(self); + enumerator->subenumerator = g_file_enumerate_children( + intermediate, enumerator->attributes, flags, cancellable, error); + g_object_unref(intermediate); + } + return G_FILE_ENUMERATOR(enumerator); +} + +// TODO(p): Implement async variants of this proxying method. +static GFileInfo * +fiv_collection_file_query_info(GFile *file, const char *attributes, + GFileQueryInfoFlags flags, GCancellable *cancellable, + G_GNUC_UNUSED GError **error) +{ + FivCollectionFile *self = FIV_COLLECTION_FILE(file); + GError *e = NULL; + if (!self->target) { + GFileInfo *info = g_file_info_new(); + g_file_info_set_file_type(info, G_FILE_TYPE_DIRECTORY); + g_file_info_set_name(info, "/"); + g_file_info_set_display_name(info, "Collection"); + + GIcon *icon = g_icon_new_for_string("shapes-symbolic", NULL); + if (icon) { + g_file_info_set_symbolic_icon(info, icon); + g_object_unref(icon); + } else { + g_warning("%s", e->message); + g_error_free(e); + } + return info; + } + + // The "http" scheme doesn't behave nicely, make something up if needed. + GFile *intermediate = get_target_subpathed(self); + GFileInfo *info = + g_file_query_info(intermediate, attributes, flags, cancellable, &e); + if (!info) { + g_warning("%s", e->message); + g_error_free(e); + + info = g_file_info_new(); + g_file_info_set_file_type(info, G_FILE_TYPE_REGULAR); + gchar *basename = g_file_get_basename(intermediate); + g_file_info_set_name(info, basename); + + // The display name is "guaranteed to always be set" when queried, + // which is up to implementations. + gchar *safe = g_utf8_make_valid(basename, -1); + g_free(basename); + g_file_info_set_display_name(info, safe); + g_free(safe); + } + + gchar *target_uri = g_file_get_uri(intermediate); + g_file_info_set_attribute_string( + info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI, target_uri); + g_free(target_uri); + g_object_unref(intermediate); + + // Ensure all basenames that might have been set have the numeric prefix. + const char *name = NULL; + if (!self->subpath) { + // Always set this, because various schemes may not do so themselves, + // which then troubles GFileEnumerator. + gchar *basename = get_target_basename(self); + g_file_info_set_name(info, basename); + g_free(basename); + + if ((name = g_file_info_get_display_name(info))) { + gchar *prefixed = get_prefixed_name(self, name); + g_file_info_set_display_name(info, prefixed); + g_free(prefixed); + } + if ((name = g_file_info_get_edit_name(info))) { + gchar *prefixed = get_prefixed_name(self, name); + g_file_info_set_edit_name(info, prefixed); + g_free(prefixed); + } + } + return info; +} + +static GFileInfo * +fiv_collection_file_query_filesystem_info(G_GNUC_UNUSED GFile *file, + G_GNUC_UNUSED const char *attributes, + G_GNUC_UNUSED GCancellable *cancellable, G_GNUC_UNUSED GError **error) +{ + GFileInfo *info = g_file_info_new(); + GFileAttributeMatcher *matcher = g_file_attribute_matcher_new(attributes); + if (g_file_attribute_matcher_matches( + matcher, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE)) { + g_file_info_set_attribute_string( + info, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE, FIV_COLLECTION_SCHEME); + } + if (g_file_attribute_matcher_matches( + matcher, G_FILE_ATTRIBUTE_FILESYSTEM_READONLY)) { + g_file_info_set_attribute_boolean( + info, G_FILE_ATTRIBUTE_FILESYSTEM_READONLY, TRUE); + } + + g_file_attribute_matcher_unref(matcher); + return info; +} + +static GFile * +fiv_collection_file_set_display_name(G_GNUC_UNUSED GFile *file, + G_GNUC_UNUSED const char *display_name, + G_GNUC_UNUSED GCancellable *cancellable, GError **error) +{ + g_set_error_literal( + error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Operation not supported"); + return NULL; +} + +static GFileInputStream * +fiv_collection_file_read(GFile *file, GCancellable *cancellable, GError **error) +{ + FivCollectionFile *self = FIV_COLLECTION_FILE(file); + if (!self->target) { + g_set_error_literal( + error, G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, "Is a directory"); + return NULL; + } + + GFile *intermediate = get_target_subpathed(self); + GFileInputStream *stream = g_file_read(intermediate, cancellable, error); + g_object_unref(intermediate); + return stream; +} + +static void +on_read(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GFile *intermediate = G_FILE(source_object); + GTask *task = G_TASK(user_data); + GError *error = NULL; + GFileInputStream *result = g_file_read_finish(intermediate, res, &error); + if (result) + g_task_return_pointer(task, result, g_object_unref); + else + g_task_return_error(task, error); + g_object_unref(task); +} + +static void +fiv_collection_file_read_async(GFile *file, int io_priority, + GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) +{ + FivCollectionFile *self = FIV_COLLECTION_FILE(file); + GTask *task = g_task_new(file, cancellable, callback, user_data); + g_task_set_name(task, __func__); + g_task_set_priority(task, io_priority); + if (!self->target) { + g_task_return_new_error( + task, G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, "Is a directory"); + g_object_unref(task); + return; + } + + GFile *intermediate = get_target_subpathed(self); + g_file_read_async(intermediate, io_priority, cancellable, on_read, task); + g_object_unref(intermediate); +} + +static GFileInputStream * +fiv_collection_file_read_finish( + G_GNUC_UNUSED GFile *file, GAsyncResult *res, GError **error) +{ + return g_task_propagate_pointer(G_TASK(res), error); +} + +static void +fiv_collection_file_file_iface_init(GFileIface *iface) +{ + // Required methods that would segfault if unimplemented. + iface->dup = fiv_collection_file_dup; + iface->hash = fiv_collection_file_hash; + iface->equal = fiv_collection_file_equal; + iface->is_native = fiv_collection_file_is_native; + iface->has_uri_scheme = fiv_collection_file_has_uri_scheme; + iface->get_uri_scheme = fiv_collection_file_get_uri_scheme; + iface->get_basename = fiv_collection_file_get_basename; + iface->get_path = fiv_collection_file_get_path; + iface->get_uri = fiv_collection_file_get_uri; + iface->get_parse_name = fiv_collection_file_get_parse_name; + iface->get_parent = fiv_collection_file_get_parent; + iface->prefix_matches = fiv_collection_file_prefix_matches; + iface->get_relative_path = fiv_collection_file_get_relative_path; + iface->resolve_relative_path = fiv_collection_file_resolve_relative_path; + iface->get_child_for_display_name = + fiv_collection_file_get_child_for_display_name; + iface->set_display_name = fiv_collection_file_set_display_name; + + // Optional methods. + iface->enumerate_children = fiv_collection_file_enumerate_children; + iface->query_info = fiv_collection_file_query_info; + iface->query_filesystem_info = fiv_collection_file_query_filesystem_info; + iface->read_fn = fiv_collection_file_read; + iface->read_async = fiv_collection_file_read_async; + iface->read_finish = fiv_collection_file_read_finish; + + iface->supports_thread_contexts = TRUE; +} + +static void +fiv_collection_file_class_init(FivCollectionFileClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + object_class->finalize = fiv_collection_file_finalize; +} + +static void +fiv_collection_file_init(FivCollectionFile *self) +{ + self->index = -1; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static GFile * +get_file_for_uri(G_GNUC_UNUSED GVfs *vfs, const char *identifier, + G_GNUC_UNUSED gpointer user_data) +{ + static const char prefix[] = FIV_COLLECTION_SCHEME ":"; + const char *path = identifier + sizeof prefix - 1; + if (!g_str_has_prefix(identifier, prefix)) + return NULL; + + // Specifying the authority is not supported. + if (g_str_has_prefix(path, "//")) + return NULL; + + // Otherwise, it needs to look like an absolute path. + if (!g_str_has_prefix(path, "/")) + return NULL; + + // TODO(p): Figure out what to do about queries and fragments. + // GDummyFile carries them across level, which seems rather arbitrary. + const char *trailing = strpbrk(path, "?#"); + gchar *unescaped = g_uri_unescape_segment(path, trailing, "/"); + if (!unescaped) + return NULL; + + GFile *result = get_file_for_path(unescaped); + g_free(unescaped); + return result; +} + +static GFile * +parse_name(GVfs *vfs, const char *identifier, gpointer user_data) +{ + // get_file_for_uri() already parses a superset of URIs. + return get_file_for_uri(vfs, identifier, user_data); +} + +void +fiv_collection_register(void) +{ + GVfs *vfs = g_vfs_get_default(); + if (!g_vfs_register_uri_scheme(vfs, FIV_COLLECTION_SCHEME, + get_file_for_uri, NULL, NULL, parse_name, NULL, NULL)) + g_warning(FIV_COLLECTION_SCHEME " scheme registration failed"); +} diff --git a/fiv-collection.h b/fiv-collection.h new file mode 100644 index 0000000..62dd336 --- /dev/null +++ b/fiv-collection.h @@ -0,0 +1,25 @@ +// +// fiv-collection.h: GVfs extension for grouping arbitrary files together +// +// Copyright (c) 2022, Přemysl Eric Janouch +// +// 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 + +#define FIV_COLLECTION_SCHEME "collection" + +gboolean fiv_collection_uri_matches(const char *uri); +GFile **fiv_collection_get_contents(gsize *len); +void fiv_collection_reload(gchar **uris); +void fiv_collection_register(void); diff --git a/fiv-context-menu.c b/fiv-context-menu.c index 05f9f5f..223558a 100644 --- a/fiv-context-menu.c +++ b/fiv-context-menu.c @@ -17,6 +17,7 @@ #include "config.h" +#include "fiv-collection.h" #include "fiv-context-menu.h" G_DEFINE_QUARK(fiv-context-menu-cancellable-quark, fiv_context_menu_cancellable) @@ -276,7 +277,7 @@ fiv_context_menu_information(GtkWindow *parent, const char *uri) 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. + // 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( @@ -423,9 +424,10 @@ GtkMenu * 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_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; @@ -437,9 +439,15 @@ fiv_context_menu_new(GtkWidget *widget, GFile *file) // 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); - 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; + 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_ = @@ -483,7 +491,7 @@ fiv_context_menu_new(GtkWidget *widget, GFile *file) ctx, open_context_unref, 0); gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); - if (regular) { + if (type == G_FILE_TYPE_REGULAR) { gtk_menu_shell_append( GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); diff --git a/fiv-io.c b/fiv-io.c index 1e476e9..547727d 100644 --- a/fiv-io.c +++ b/fiv-io.c @@ -2964,6 +2964,7 @@ static void model_entry_finalize(FivIoModelEntry *entry) { g_free(entry->uri); + g_free(entry->target_uri); g_free(entry->collate_key); } @@ -3083,9 +3084,12 @@ model_reload(FivIoModel *self, GError **error) g_array_set_size(self->files, 0); GFileEnumerator *enumerator = g_file_enumerate_children(self->directory, - G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE "," + G_FILE_ATTRIBUTE_STANDARD_TYPE "," + G_FILE_ATTRIBUTE_STANDARD_NAME "," + G_FILE_ATTRIBUTE_STANDARD_TARGET_URI "," G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN "," - G_FILE_ATTRIBUTE_TIME_MODIFIED "," G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC, + G_FILE_ATTRIBUTE_TIME_MODIFIED "," + G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC, G_FILE_QUERY_INFO_NONE, NULL, error); if (!enumerator) { // Note that this has had a side-effect of clearing all entries. @@ -3096,12 +3100,23 @@ model_reload(FivIoModel *self, GError **error) GFileInfo *info = NULL; GFile *child = NULL; - while (g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) && - info) { + GError *e = NULL; + while (TRUE) { + if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, &e) && + e) { + g_warning("%s", e->message); + g_clear_error(&e); + continue; + } + + if (!info) + break; if (self->filtering && g_file_info_get_is_hidden(info)) continue; - FivIoModelEntry entry = {.uri = g_file_get_uri(child)}; + FivIoModelEntry entry = {.uri = g_file_get_uri(child), + .target_uri = g_strdup(g_file_info_get_attribute_string( + info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI))}; GDateTime *mtime = g_file_info_get_modification_date_time(info); if (mtime) { entry.mtime_msec = g_date_time_to_unix(mtime) * 1000 + diff --git a/fiv-io.h b/fiv-io.h index dccdd31..4e031f9 100644 --- a/fiv-io.h +++ b/fiv-io.h @@ -130,6 +130,7 @@ GFile *fiv_io_model_get_location(FivIoModel *self); typedef struct { gchar *uri; ///< GIO URI + gchar *target_uri; ///< GIO URI for any target gchar *collate_key; ///< Collate key for the filename gint64 mtime_msec; ///< Modification time in milliseconds } FivIoModelEntry; diff --git a/fiv-sidebar.c b/fiv-sidebar.c index bc83649..fc63a99 100644 --- a/fiv-sidebar.c +++ b/fiv-sidebar.c @@ -17,6 +17,7 @@ #include +#include "fiv-collection.h" #include "fiv-context-menu.h" #include "fiv-io.h" #include "fiv-sidebar.h" @@ -295,16 +296,49 @@ create_row(FivSidebar *self, GFile *file, const char *icon_name) return row; } +static void +on_update_task(GTask *task, G_GNUC_UNUSED gpointer source_object, + G_GNUC_UNUSED gpointer task_data, G_GNUC_UNUSED GCancellable *cancellable) +{ + g_task_return_boolean(task, TRUE); +} + +static void +on_update_task_done(GObject *source_object, G_GNUC_UNUSED GAsyncResult *res, + G_GNUC_UNUSED gpointer user_data) +{ + FivSidebar *self = FIV_SIDEBAR(source_object); + gtk_places_sidebar_set_location( + self->places, fiv_io_model_get_location(self->model)); +} + static void update_location(FivSidebar *self) { GFile *location = fiv_io_model_get_location(self->model); - if (!location) - return; + + GFile *collection = g_file_new_for_uri(FIV_COLLECTION_SCHEME ":/"); + gtk_places_sidebar_remove_shortcut(self->places, collection); + if (location && g_file_has_uri_scheme(location, FIV_COLLECTION_SCHEME)) { + // add_shortcut() asynchronously requests GFileInfo, and only fills in + // the new row's "uri" data field once that's finished, resulting in + // the immediate set_location() call below failing to find it. + gtk_places_sidebar_add_shortcut(self->places, collection); + + // Queue up a callback using the same mechanism that GFile uses. + GTask *task = g_task_new(self, NULL, on_update_task_done, NULL); + g_task_set_name(task, __func__); + g_task_set_priority(task, G_PRIORITY_LOW); + g_task_run_in_thread(task, on_update_task); + g_object_unref(task); + } + g_object_unref(collection); gtk_places_sidebar_set_location(self->places, location); gtk_container_foreach(GTK_CONTAINER(self->listbox), (GtkCallback) gtk_widget_destroy, NULL); + if (!location) + return; GFile *iter = g_object_ref(location); GtkWidget *row = NULL; diff --git a/fiv-thumbnail.c b/fiv-thumbnail.c index 4899dfe..15a78f1 100644 --- a/fiv-thumbnail.c +++ b/fiv-thumbnail.c @@ -615,7 +615,7 @@ read_png_thumbnail( } cairo_surface_t * -fiv_thumbnail_lookup(char *uri, gint64 mtime_msec, FivThumbnailSize size) +fiv_thumbnail_lookup(const char *uri, gint64 mtime_msec, FivThumbnailSize size) { g_return_val_if_fail(size >= FIV_THUMBNAIL_SIZE_MIN && size <= FIV_THUMBNAIL_SIZE_MAX, NULL); diff --git a/fiv-thumbnail.h b/fiv-thumbnail.h index 1a22c75..d12765a 100644 --- a/fiv-thumbnail.h +++ b/fiv-thumbnail.h @@ -68,7 +68,7 @@ cairo_surface_t *fiv_thumbnail_produce( /// Retrieves a thumbnail of the most appropriate quality and resolution /// for the target file. cairo_surface_t *fiv_thumbnail_lookup( - char *uri, gint64 mtime_msec, FivThumbnailSize size); + const char *uri, gint64 mtime_msec, FivThumbnailSize size); /// Invalidate the wide thumbnail cache. May write to standard streams. void fiv_thumbnail_invalidate(void); diff --git a/fiv.c b/fiv.c index 449d113..26d48a8 100644 --- a/fiv.c +++ b/fiv.c @@ -36,6 +36,7 @@ #include "config.h" #include "fiv-browser.h" +#include "fiv-collection.h" #include "fiv-io.h" #include "fiv-sidebar.h" #include "fiv-thumbnail.h" @@ -59,6 +60,17 @@ exit_fatal(const char *format, ...) exit(EXIT_FAILURE); } +static gchar ** +slist_to_strv(GSList *slist) +{ + gchar **strv = g_malloc0_n(g_slist_length(slist) + 1, sizeof *strv), + **p = strv; + for (GSList *link = slist; link; link = link->next) + *p++ = link->data; + g_slist_free(slist); + return strv; +} + // --- Keyboard shortcuts ------------------------------------------------------ // Fuck XML, this can be easily represented in static structures. // Though it would be nice if the accelerators could be customized. @@ -707,7 +719,7 @@ load_directory_without_switching(const char *uri) GError *error = NULL; GFile *file = g_file_new_for_uri(g.directory); if (fiv_io_model_open(g.model, file, &error)) { - // Handled by the signal callback. + // This is handled by our ::files-changed callback. } else if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) { g_error_free(error); } else { @@ -829,6 +841,7 @@ create_open_dialog(void) "_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, NULL); gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(dialog), FALSE); + gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), TRUE); GtkFileFilter *filter = gtk_file_filter_new(); for (const char **p = fiv_io_supported_media_types; *p; p++) @@ -858,13 +871,20 @@ on_open(void) (void) gtk_file_chooser_set_current_folder_uri( GTK_FILE_CHOOSER(dialog), g.directory); - // The default is local-only, single item. switch (gtk_dialog_run(GTK_DIALOG(dialog))) { - gchar *uri; + GSList *uri_list; case GTK_RESPONSE_ACCEPT: - uri = gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(dialog)); - open_image(uri); - g_free(uri); + if (!(uri_list = gtk_file_chooser_get_uris(GTK_FILE_CHOOSER(dialog)))) + break; + + gchar **uris = slist_to_strv(uri_list); + if (g_strv_length(uris) == 1) { + open_image(uris[0]); + } else { + fiv_collection_reload(uris); + load_directory(FIV_COLLECTION_SCHEME ":/"); + } + g_strfreev(uris); break; case GTK_RESPONSE_NONE: dialog = NULL; @@ -892,16 +912,61 @@ on_next(void) } } +static gchar ** +build_spawn_argv(const char *uri) +{ + // Because we only pass URIs, there is no need to prepend "--" here. + GPtrArray *a = g_ptr_array_new(); + g_ptr_array_add(a, g_strdup(PROJECT_NAME)); + + // Process-local VFS URIs need to be resolved to globally accessible URIs. + // It doesn't seem possible to reliably tell if a GFile is process-local, + // but our collection VFS is the only one to realistically cause problems. + if (!fiv_collection_uri_matches(uri)) { + g_ptr_array_add(a, g_strdup(uri)); + goto out; + } + + GFile *file = g_file_new_for_uri(uri); + GError *error = NULL; + GFileInfo *info = + g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI, + G_FILE_QUERY_INFO_NONE, NULL, &error); + g_object_unref(file); + if (!info) { + g_warning("%s", error->message); + g_error_free(error); + goto out; + } + + const char *target_uri = g_file_info_get_attribute_string( + info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI); + if (target_uri) { + g_ptr_array_add(a, g_strdup(target_uri)); + } else { + gsize len = 0; + GFile **files = fiv_collection_get_contents(&len); + for (gsize i = 0; i < len; i++) + g_ptr_array_add(a, g_file_get_uri(files[i])); + } + g_object_unref(info); + +out: + g_ptr_array_add(a, NULL); + return (gchar **) g_ptr_array_free(a, FALSE); +} + static void spawn_uri(const char *uri) { - char *argv[] = {PROJECT_NAME, (char *) uri, NULL}; + gchar **argv = build_spawn_argv(uri); GError *error = NULL; if (!g_spawn_async( NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, &error)) { g_warning("%s", error->message); g_error_free(error); } + g_strfreev(argv); } static void @@ -1005,8 +1070,13 @@ on_view_drag_data_received(G_GNUC_UNUSED GtkWidget *widget, return; } - // TODO(p): Once we're able to open a virtual directory, open all of them. - GFile *file = g_file_new_for_uri(uris[0]); + GFile *file = NULL; + if (g_strv_length(uris) == 1) { + file = g_file_new_for_uri(uris[0]); + } else { + fiv_collection_reload(uris); + file = g_file_new_for_uri(FIV_COLLECTION_SCHEME ":/"); + } open_any_file(file, FALSE); g_object_unref(file); gtk_drag_finish(context, TRUE, FALSE, time); @@ -1854,10 +1924,12 @@ static const char stylesheet[] = "@define-color fiv-tile @content_view_bg; \ .fiv-information label { padding: 0 4px; }"; static void -output_thumbnail(const char *path_arg, gboolean extract, const char *size_arg) +output_thumbnail(gchar **uris, gboolean extract, const char *size_arg) { - if (!path_arg) - exit_fatal("no path given"); + if (!uris) + exit_fatal("No path given"); + if (uris[1]) + exit_fatal("Only one thumbnail at a time may be produced"); FivThumbnailSize size = FIV_THUMBNAIL_SIZE_COUNT; if (size_arg) { @@ -1875,7 +1947,7 @@ output_thumbnail(const char *path_arg, gboolean extract, const char *size_arg) #endif // G_OS_WIN32 GError *error = NULL; - GFile *file = g_file_new_for_commandline_arg(path_arg); + GFile *file = g_file_new_for_uri(uris[0]); cairo_surface_t *surface = NULL; if (extract && (surface = fiv_thumbnail_extract(file, size, &error))) fiv_io_serialize_to_stdout(surface, FIV_IO_SERIALIZE_LOW_QUALITY); @@ -1898,10 +1970,10 @@ main(int argc, char *argv[]) { gboolean show_version = FALSE, show_supported_media_types = FALSE, invalidate_cache = FALSE, browse = FALSE, extract_thumbnail = FALSE; - gchar **path_args = NULL, *thumbnail_size = NULL; + gchar **args = NULL, *thumbnail_size = NULL; const GOptionEntry options[] = { - {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &path_args, - NULL, "[FILE | DIRECTORY | URI]"}, + {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &args, + NULL, "[PATH | URI]..."}, {"list-supported-media-types", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE, &show_supported_media_types, "Output supported media types and exit", NULL}, @@ -1941,19 +2013,22 @@ main(int argc, char *argv[]) if (!initialized) exit_fatal("%s", error->message); - // NOTE: Firefox and Eye of GNOME both interpret multiple arguments - // in a special way. This is problematic, because one-element lists - // are unrepresentable. - // TODO(p): Require a command line switch, load a virtual folder. - // We may want or need to create a custom GVfs. - // TODO(p): Complain to the user if there's more than one argument. - // Best show the help message, if we can figure that out. - const gchar *path_arg = path_args ? path_args[0] : NULL; + // Normalize all arguments to URIs. + for (gsize i = 0; args && args[i]; i++) { + GFile *resolved = g_file_new_for_commandline_arg(args[i]); + g_free(args[i]); + args[i] = g_file_get_uri(resolved); + g_object_unref(resolved); + } if (extract_thumbnail || thumbnail_size) { - output_thumbnail(path_arg, extract_thumbnail, thumbnail_size); + output_thumbnail(args, extract_thumbnail, thumbnail_size); return 0; } + // It doesn't make much sense to have command line arguments able to + // resolve to the VFS they may end up being contained within. + fiv_collection_register(); + g.model = g_object_new(FIV_TYPE_IO_MODEL, NULL); g_signal_connect(g.model, "files-changed", G_CALLBACK(on_model_files_changed), NULL); @@ -2088,11 +2163,22 @@ main(int argc, char *argv[]) // XXX: The widget wants to read the display's profile. The realize is ugly. gtk_widget_realize(g.view); + // XXX: We follow the behaviour of Firefox and Eye of GNOME, which both + // interpret multiple command line arguments differently, as a collection. + // However, single-element collections are unrepresentable this way. + // Should we allow multiple targets only in a special new mode? g.files = g_ptr_array_new_full(0, g_free); - if (path_arg) { - GFile *file = g_file_new_for_commandline_arg(path_arg); + if (args) { + const gchar *target = *args; + if (args[1]) { + fiv_collection_reload(args); + target = FIV_COLLECTION_SCHEME ":/"; + } + + GFile *file = g_file_new_for_uri(target); open_any_file(file, browse); g_object_unref(file); + g_strfreev(args); } if (!g.directory) { GFile *file = g_file_new_for_path("."); diff --git a/fiv.desktop b/fiv.desktop index e1cdd13..965b646 100644 --- a/fiv.desktop +++ b/fiv.desktop @@ -4,7 +4,7 @@ Name=fiv GenericName=Image Viewer X-GNOME-FullName=fiv Image Viewer Icon=fiv -Exec=fiv -- %u +Exec=fiv -- %U Terminal=false StartupNotify=true Categories=Graphics;2DGraphics;Viewer; diff --git a/meson.build b/meson.build index f8627e8..9c40889 100644 --- a/meson.build +++ b/meson.build @@ -107,9 +107,10 @@ tiff_tables = custom_target('tiff-tables.h', desktops = ['fiv.desktop', 'fiv-browse.desktop'] exe = executable('fiv', 'fiv.c', 'fiv-view.c', 'fiv-io.c', 'fiv-context-menu.c', - 'fiv-browser.c', 'fiv-sidebar.c', 'fiv-thumbnail.c', 'xdg.c', resources, + 'fiv-browser.c', 'fiv-sidebar.c', 'fiv-thumbnail.c', 'fiv-collection.c', + 'xdg.c', resources, install : true, - dependencies : [dependencies]) + dependencies : dependencies) if gdkpixbuf.found() executable('io-benchmark', 'fiv-io-benchmark.c', 'fiv-io.c', 'xdg.c', build_by_default : false, diff --git a/resources/resources.gresource.xml b/resources/resources.gresource.xml index f52bb76..b3d6b1c 100644 --- a/resources/resources.gresource.xml +++ b/resources/resources.gresource.xml @@ -11,5 +11,6 @@ heal-symbolic.svg info-symbolic.svg pin2-symbolic.svg + shapes-symbolic.svg diff --git a/resources/shapes-symbolic.svg b/resources/shapes-symbolic.svg new file mode 100644 index 0000000..fa09c2c --- /dev/null +++ b/resources/shapes-symbolic.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +