Use a unified filesystem model

This removes some duplication of effort.

So far, sorting adjustments are not exposed in the UI.
This commit is contained in:
2021-12-31 02:19:17 +01:00
parent 2ac918b7ab
commit b935b0baf8
8 changed files with 467 additions and 241 deletions

324
fiv-io.c
View File

@@ -1,7 +1,7 @@
//
// fiv-io.c: image operations
//
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 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.
@@ -132,22 +132,6 @@ fiv_io_all_supported_media_types(void)
return (char **) g_ptr_array_free(types, FALSE);
}
int
fiv_io_filecmp(GFile *location1, GFile *location2)
{
if (g_file_has_prefix(location1, location2))
return +1;
if (g_file_has_prefix(location2, location1))
return -1;
gchar *name1 = g_file_get_parse_name(location1);
gchar *name2 = g_file_get_parse_name(location2);
int result = g_utf8_collate(name1, name2);
g_free(name1);
g_free(name2);
return result;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#define FIV_IO_ERROR fiv_io_error_quark()
@@ -2463,6 +2447,312 @@ fiv_io_open_from_data(const char *data, size_t len, const gchar *uri,
return surface;
}
// --- Filesystem --------------------------------------------------------------
#include "xdg.h"
#include <fnmatch.h>
typedef struct _ModelEntry {
gchar *uri; ///< GIO URI
gint64 mtime_msec; ///< Modification time in milliseconds
} ModelEntry;
static void
model_entry_finalize(ModelEntry *entry)
{
g_free(entry->uri);
}
typedef enum _FivIoModelSort {
FIV_IO_MODEL_SORT_NAME,
FIV_IO_MODEL_SORT_MTIME,
} FivIoModelSort;
struct _FivIoModel {
GObject parent_instance;
gchar **supported_globs;
GFile *directory; ///< Currently loaded directory
GFileMonitor *monitor; ///< "directory" monitoring
GArray *subdirs; ///< "directory" contents
GArray *files; ///< "directory" contents
FivIoModelSort sort; ///< How to sort
gboolean sort_descending; ///< Whether to sort in reverse
gboolean filtering; ///< Only show non-hidden, supported
};
G_DEFINE_TYPE(FivIoModel, fiv_io_model, G_TYPE_OBJECT)
enum {
PROP_FILTERING = 1,
N_PROPERTIES
};
static GParamSpec *model_properties[N_PROPERTIES];
enum {
FILES_CHANGED,
SUBDIRECTORIES_CHANGED,
LAST_SIGNAL,
};
// Globals are, sadly, the canonical way of storing signal numbers.
static guint model_signals[LAST_SIGNAL];
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static gboolean
model_supports(FivIoModel *self, const gchar *filename)
{
gchar *utf8 = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);
if (!utf8)
return FALSE;
gchar *lowercased = g_utf8_strdown(utf8, -1);
g_free(utf8);
// XXX: fnmatch() uses the /locale/ encoding, but who cares nowadays.
// TODO(p): Use GPatternSpec and g_file_info_get_display_name().
for (gchar **p = self->supported_globs; *p; p++)
if (!fnmatch(*p, lowercased, 0)) {
g_free(lowercased);
return TRUE;
}
g_free(lowercased);
return FALSE;
}
static inline int
model_compare_name(GFile *location1, GFile *location2)
{
gchar *name1 = g_file_get_parse_name(location1);
gchar *name2 = g_file_get_parse_name(location2);
int result = g_utf8_collate(name1, name2);
g_free(name1);
g_free(name2);
return result;
}
static inline int
model_compare_entries(FivIoModel *self, const ModelEntry *entry1, GFile *file1,
const ModelEntry *entry2, GFile *file2)
{
if (g_file_has_prefix(file1, file2))
return +1;
if (g_file_has_prefix(file2, file1))
return -1;
int result = 0;
switch (self->sort) {
case FIV_IO_MODEL_SORT_NAME:
result = model_compare_name(file1, file2);
break;
case FIV_IO_MODEL_SORT_MTIME:
result -= entry1->mtime_msec < entry2->mtime_msec;
result += entry1->mtime_msec > entry2->mtime_msec;
}
return self->sort_descending ? -result : +result;
}
static gint
model_compare(gconstpointer a, gconstpointer b, gpointer user_data)
{
const ModelEntry *entry1 = a;
const ModelEntry *entry2 = b;
GFile *file1 = g_file_new_for_uri(entry1->uri);
GFile *file2 = g_file_new_for_uri(entry2->uri);
int result = model_compare_entries(user_data, entry1, file1, entry2, file2);
g_object_unref(file1);
g_object_unref(file2);
return result;
}
static gboolean
model_reload(FivIoModel *self, GError **error)
{
g_array_set_size(self->subdirs, 0);
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_IS_HIDDEN ","
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.
g_signal_emit(self, model_signals[FILES_CHANGED], 0);
g_signal_emit(self, model_signals[SUBDIRECTORIES_CHANGED], 0);
return FALSE;
}
GFileInfo *info = NULL;
GFile *child = NULL;
while (g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) &&
info) {
if (self->filtering && g_file_info_get_is_hidden(info))
continue;
ModelEntry entry = {};
GDateTime *mtime = g_file_info_get_modification_date_time(info);
if (mtime) {
entry.mtime_msec = g_date_time_to_unix(mtime) * 1000 +
g_date_time_get_microsecond(mtime) / 1000;
g_date_time_unref(mtime);
}
const char *name = g_file_info_get_name(info);
if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY) {
entry.uri = g_file_get_uri(child);
g_array_append_val(self->subdirs, entry);
} else if (!self->filtering || model_supports(self, name)) {
entry.uri = g_file_get_uri(child);
g_array_append_val(self->files, entry);
}
}
g_object_unref(enumerator);
g_array_sort_with_data(self->subdirs, model_compare, self);
g_array_sort_with_data(self->files, model_compare, self);
g_signal_emit(self, model_signals[FILES_CHANGED], 0);
g_signal_emit(self, model_signals[SUBDIRECTORIES_CHANGED], 0);
return TRUE;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
fiv_io_model_finalize(GObject *gobject)
{
FivIoModel *self = FIV_IO_MODEL(gobject);
g_clear_object(&self->directory);
g_clear_object(&self->monitor);
g_array_free(self->subdirs, TRUE);
g_array_free(self->files, TRUE);
G_OBJECT_CLASS(fiv_io_model_parent_class)->finalize(gobject);
}
static void
fiv_io_model_get_property(
GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
{
FivIoModel *self = FIV_IO_MODEL(object);
switch (property_id) {
case PROP_FILTERING:
g_value_set_boolean(value, self->filtering);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
}
}
static void
fiv_io_model_set_property(
GObject *object, guint property_id, const GValue *value, GParamSpec *pspec)
{
FivIoModel *self = FIV_IO_MODEL(object);
switch (property_id) {
case PROP_FILTERING:
if (self->filtering == g_value_get_boolean(value))
return;
self->filtering = !self->filtering;
g_object_notify_by_pspec(object, model_properties[PROP_FILTERING]);
(void) model_reload(self, NULL /* error */);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
}
}
static void
fiv_io_model_class_init(FivIoModelClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
object_class->get_property = fiv_io_model_get_property;
object_class->set_property = fiv_io_model_set_property;
object_class->finalize = fiv_io_model_finalize;
model_properties[PROP_FILTERING] = g_param_spec_boolean(
"filtering", "Filtering", "Only show non-hidden, supported entries",
TRUE, G_PARAM_READWRITE);
g_object_class_install_properties(
object_class, N_PROPERTIES, model_properties);
// TODO(p): Arguments something like: index, added, removed.
model_signals[FILES_CHANGED] =
g_signal_new("files-changed", G_TYPE_FROM_CLASS(klass), 0, 0,
NULL, NULL, NULL, G_TYPE_NONE, 0);
model_signals[SUBDIRECTORIES_CHANGED] =
g_signal_new("subdirectories-changed", G_TYPE_FROM_CLASS(klass), 0, 0,
NULL, NULL, NULL, G_TYPE_NONE, 0);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
fiv_io_model_init(FivIoModel *self)
{
self->filtering = TRUE;
char **types = fiv_io_all_supported_media_types();
self->supported_globs = extract_mime_globs((const char **) types);
g_strfreev(types);
self->files = g_array_new(FALSE, TRUE, sizeof(ModelEntry));
self->subdirs = g_array_new(FALSE, TRUE, sizeof(ModelEntry));
g_array_set_clear_func(
self->subdirs, (GDestroyNotify) model_entry_finalize);
g_array_set_clear_func(
self->files, (GDestroyNotify) model_entry_finalize);
}
gboolean
fiv_io_model_open(FivIoModel *self, GFile *directory, GError **error)
{
g_return_val_if_fail(FIV_IS_IO_MODEL(self), FALSE);
g_return_val_if_fail(G_IS_FILE(directory), FALSE);
g_clear_object(&self->directory);
g_clear_object(&self->monitor);
self->directory = g_object_ref(directory);
// TODO(p): Process the ::changed signal.
self->monitor = g_file_monitor_directory(
directory, G_FILE_MONITOR_WATCH_MOVES, NULL, NULL /* error */);
return model_reload(self, error);
}
GFile *
fiv_io_model_get_location(FivIoModel *self)
{
g_return_val_if_fail(FIV_IS_IO_MODEL(self), NULL);
return self->directory;
}
GPtrArray *
fiv_io_model_get_files(FivIoModel *self)
{
GPtrArray *a = g_ptr_array_new_full(self->files->len, g_free);
for (guint i = 0; i < self->files->len; i++)
g_ptr_array_add(
a, g_strdup(g_array_index(self->files, ModelEntry, i).uri));
return a;
}
GPtrArray *
fiv_io_model_get_subdirectories(FivIoModel *self)
{
GPtrArray *a = g_ptr_array_new_full(self->subdirs->len, g_free);
for (guint i = 0; i < self->subdirs->len; i++)
g_ptr_array_add(
a, g_strdup(g_array_index(self->subdirs, ModelEntry, i).uri));
return a;
}
// --- Export ------------------------------------------------------------------
unsigned char *