aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2022-07-28 00:37:36 +0200
committerPřemysl Eric Janouch <p@janouch.name>2022-08-08 18:06:50 +0200
commit701846ab398371de5a921b1a561bcc1601cd8297 (patch)
tree04f5a765e0783f403b90e97ac6f6edb058b61e49
parent4927c8c6923991ae68db21e66749c1fb99240b08 (diff)
downloadfiv-701846ab398371de5a921b1a561bcc1601cd8297.tar.gz
fiv-701846ab398371de5a921b1a561bcc1601cd8297.tar.xz
fiv-701846ab398371de5a921b1a561bcc1601cd8297.zip
Support opening collections of files
Implement a process-local VFS to enable grouping together arbitrary URIs passed via program arguments, DnD, or the file open dialog. This VFS contains FivCollectionFile objects, which act as "simple" proxies over arbitrary GFiles. Their true URIs may be retrieved through the "standard::target-uri" attribute, in a similar way to GVfs's "recent" and "trash" backends. (The main reason we proxy rather than just hackishly return foreign GFiles from the VFS is that loading them would switch the current directory, and break iteration as a result. We could also keep the collection outside of GVfs, but that would result in considerable special-casing, and the author wouldn't gain intimate knowledge of GIO.) There is no perceived need to keep old collections when opening new ones, so we simply change and reload the contents when needed. Similarly, there is no intention to make the VFS writeable. The process-locality of this and other URI schemes has proven to be rather annoying when passing files to other applications, however most of the resulting complexity appears to be essential rather than accidental. Note that the GTK+ file chooser widget is retarded, and doesn't recognize URIs that lack the authority part in the location bar.
-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>