aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fiv-browser.c40
-rw-r--r--fiv-collection.c725
-rw-r--r--fiv-collection.h25
-rw-r--r--fiv-context-menu.c24
-rw-r--r--fiv-io.c25
-rw-r--r--fiv-io.h1
-rw-r--r--fiv-sidebar.c38
-rw-r--r--fiv-thumbnail.c2
-rw-r--r--fiv-thumbnail.h2
-rw-r--r--fiv.c140
-rw-r--r--fiv.desktop2
-rw-r--r--meson.build5
-rw-r--r--resources/resources.gresource.xml1
-rw-r--r--resources/shapes-symbolic.svg154
14 files changed, 1126 insertions, 58 deletions
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 <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 <gio/gio.h>
+
+#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 <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 <gio/gio.h>
+
+#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 <gtk/gtk.h>
+#include "fiv-collection.h"
#include "fiv-context-menu.h"
#include "fiv-io.h"
#include "fiv-sidebar.h"
@@ -296,15 +297,48 @@ create_row(FivSidebar *self, GFile *file, const char *icon_name)
}
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 @@
<file preprocess="xml-stripblanks">heal-symbolic.svg</file>
<file preprocess="xml-stripblanks">info-symbolic.svg</file>
<file preprocess="xml-stripblanks">pin2-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">shapes-symbolic.svg</file>
</gresource>
</gresources>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <filter id="a" height="100%" width="100%" x="0%" y="0%">
+ <feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
+ </filter>
+ <mask id="b">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
+ </g>
+ </mask>
+ <clipPath id="c">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="d">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
+ </g>
+ </mask>
+ <clipPath id="e">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="f">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
+ </g>
+ </mask>
+ <clipPath id="g">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="h">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
+ </g>
+ </mask>
+ <clipPath id="i">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="j">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
+ </g>
+ </mask>
+ <clipPath id="k">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="l">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
+ </g>
+ </mask>
+ <clipPath id="m">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="n">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
+ </g>
+ </mask>
+ <clipPath id="o">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="p">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
+ </g>
+ </mask>
+ <clipPath id="q">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="r">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
+ </g>
+ </mask>
+ <clipPath id="s">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="t">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
+ </g>
+ </mask>
+ <clipPath id="u">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="v">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
+ </g>
+ </mask>
+ <clipPath id="w">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="x">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
+ </g>
+ </mask>
+ <clipPath id="y">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="z">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
+ </g>
+ </mask>
+ <clipPath id="A">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <g fill="#2e3436">
+ <path d="m 5.191406 1.296875 c -0.390625 -0.390625 -1.023437 -0.390625 -1.414062 0 l -2.5 2.5 c -0.390625 0.390625 -0.390625 1.023437 0 1.414063 l 2.5 2.5 c 0.390625 0.390624 1.023437 0.390624 1.414062 0 l 2.496094 -2.5 c 0.390625 -0.390626 0.390625 -1.023438 0 -1.414063 z m 0 0"/>
+ <path d="m 9.984375 12.003906 c 0 1.65625 -1.34375 3 -3 3 c -1.660156 0 -3 -1.34375 -3 -3 s 1.339844 -3 3 -3 c 1.65625 0 3 1.34375 3 3 z m 0 0"/>
+ <path d="m 11.929688 2.007812 c -0.339844 0.015626 -0.644532 0.203126 -0.8125 0.496094 l -2.320313 4 c -0.386719 0.664063 0.09375 1.5 0.863281 1.5 h 4.644532 c 0.769531 0 1.25 -0.835937 0.863281 -1.5 l -2.320313 -4 c -0.1875 -0.328125 -0.542968 -0.519531 -0.917968 -0.496094 z m 0 0"/>
+ </g>
+ <g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -620 -420)">
+ <path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
+ </g>
+ <g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -620 -420)">
+ <path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
+ </g>
+ <g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -620 -420)">
+ <path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
+ </g>
+ <g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -620 -420)">
+ <path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
+ </g>
+ <g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -620 -420)">
+ <path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
+ </g>
+ <g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -620 -420)">
+ <path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
+ </g>
+ <g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -620 -420)">
+ <path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
+ </g>
+ <g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -620 -420)">
+ <path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
+ </g>
+ <g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -620 -420)">
+ <path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
+ </g>
+ <g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -620 -420)">
+ <path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
+ </g>
+ <g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -620 -420)">
+ <path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
+ </g>
+ <g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -620 -420)">
+ <path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
+ </g>
+ <g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -620 -420)">
+ <path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v -0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
+ </g>
+</svg>