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.
This commit is contained in:
Přemysl Eric Janouch 2022-07-28 00:37:36 +02:00
parent 4927c8c692
commit 701846ab39
Signed by: p
GPG Key ID: A0420B94F92B9493
14 changed files with 1126 additions and 58 deletions

View File

@ -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);

725
fiv-collection.c Normal file
View File

@ -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");
}

25
fiv-collection.h Normal file
View File

@ -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);

View File

@ -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());

View File

@ -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 +

View File

@ -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;

View File

@ -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"
@ -295,16 +296,49 @@ create_row(FivSidebar *self, GFile *file, const char *icon_name)
return row;
}
static void
on_update_task(GTask *task, G_GNUC_UNUSED gpointer source_object,
G_GNUC_UNUSED gpointer task_data, G_GNUC_UNUSED GCancellable *cancellable)
{
g_task_return_boolean(task, TRUE);
}
static void
on_update_task_done(GObject *source_object, G_GNUC_UNUSED GAsyncResult *res,
G_GNUC_UNUSED gpointer user_data)
{
FivSidebar *self = FIV_SIDEBAR(source_object);
gtk_places_sidebar_set_location(
self->places, fiv_io_model_get_location(self->model));
}
static void
update_location(FivSidebar *self)
{
GFile *location = fiv_io_model_get_location(self->model);
if (!location)
return;
GFile *collection = g_file_new_for_uri(FIV_COLLECTION_SCHEME ":/");
gtk_places_sidebar_remove_shortcut(self->places, collection);
if (location && g_file_has_uri_scheme(location, FIV_COLLECTION_SCHEME)) {
// add_shortcut() asynchronously requests GFileInfo, and only fills in
// the new row's "uri" data field once that's finished, resulting in
// the immediate set_location() call below failing to find it.
gtk_places_sidebar_add_shortcut(self->places, collection);
// Queue up a callback using the same mechanism that GFile uses.
GTask *task = g_task_new(self, NULL, on_update_task_done, NULL);
g_task_set_name(task, __func__);
g_task_set_priority(task, G_PRIORITY_LOW);
g_task_run_in_thread(task, on_update_task);
g_object_unref(task);
}
g_object_unref(collection);
gtk_places_sidebar_set_location(self->places, location);
gtk_container_foreach(GTK_CONTAINER(self->listbox),
(GtkCallback) gtk_widget_destroy, NULL);
if (!location)
return;
GFile *iter = g_object_ref(location);
GtkWidget *row = NULL;

View File

@ -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);

View File

@ -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);

140
fiv.c
View File

@ -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(".");

View File

@ -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;

View File

@ -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,

View File

@ -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>

View File

@ -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>

After

Width:  |  Height:  |  Size: 6.7 KiB