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