Compare commits
2 Commits
e6341e59bb
...
701846ab39
Author | SHA1 | Date | |
---|---|---|---|
701846ab39 | |||
4927c8c692 |
@ -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,9 +1482,8 @@ 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,
|
||||
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)
|
||||
@ -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
725
fiv-collection.c
Normal 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
25
fiv-collection.h
Normal 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);
|
@ -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());
|
||||
|
||||
|
28
fiv-io.c
28
fiv-io.c
@ -1892,6 +1892,7 @@ open_resvg(
|
||||
|
||||
resvg_options *opt = resvg_options_create();
|
||||
resvg_options_load_system_fonts(opt);
|
||||
if (base_file)
|
||||
resvg_options_set_resources_dir(opt, g_file_peek_path(base_file));
|
||||
if (ctx->screen_dpi)
|
||||
resvg_options_set_dpi(opt, ctx->screen_dpi);
|
||||
@ -1899,7 +1900,7 @@ open_resvg(
|
||||
resvg_render_tree *tree = NULL;
|
||||
int err = resvg_parse_tree_from_data(data, len, opt, &tree);
|
||||
resvg_options_destroy(opt);
|
||||
g_object_unref(base_file);
|
||||
g_clear_object(&base_file);
|
||||
if (err != RESVG_OK) {
|
||||
set_error(error, load_resvg_error(err));
|
||||
return NULL;
|
||||
@ -2963,6 +2964,7 @@ static void
|
||||
model_entry_finalize(FivIoModelEntry *entry)
|
||||
{
|
||||
g_free(entry->uri);
|
||||
g_free(entry->target_uri);
|
||||
g_free(entry->collate_key);
|
||||
}
|
||||
|
||||
@ -3082,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.
|
||||
@ -3095,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 +
|
||||
|
1
fiv-io.h
1
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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
144
fiv.c
144
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.
|
||||
@ -634,7 +646,11 @@ switch_to_view(void)
|
||||
static gchar *
|
||||
parent_uri(GFile *child_file)
|
||||
{
|
||||
// The empty URI results in a convenient dummy GFile implementation.
|
||||
GFile *parent = g_file_get_parent(child_file);
|
||||
if (!parent)
|
||||
return g_strdup("");
|
||||
|
||||
gchar *parent_uri = g_file_get_uri(parent);
|
||||
g_object_unref(parent);
|
||||
return parent_uri;
|
||||
@ -703,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 {
|
||||
@ -825,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++)
|
||||
@ -854,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;
|
||||
@ -888,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
|
||||
@ -1001,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);
|
||||
@ -1850,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) {
|
||||
@ -1871,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);
|
||||
@ -1894,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},
|
||||
@ -1937,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);
|
||||
@ -2084,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(".");
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
154
resources/shapes-symbolic.svg
Normal file
154
resources/shapes-symbolic.svg
Normal 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 |
Loading…
x
Reference in New Issue
Block a user