Process some GFileMonitor events

So far, it's rather crude.
This commit is contained in:
Přemysl Eric Janouch 2023-05-30 10:36:11 +02:00
parent 2caebb7d19
commit 200485246b
Signed by: p
GPG Key ID: A0420B94F92B9493
6 changed files with 463 additions and 162 deletions

View File

@ -32,6 +32,7 @@
#include "fiv-collection.h" #include "fiv-collection.h"
#include "fiv-context-menu.h" #include "fiv-context-menu.h"
#include "fiv-io.h" #include "fiv-io.h"
#include "fiv-io-model.h"
#include "fiv-thumbnail.h" #include "fiv-thumbnail.h"
// --- Widget ------------------------------------------------------------------ // --- Widget ------------------------------------------------------------------
@ -78,7 +79,7 @@ struct _FivBrowser {
gboolean show_labels; ///< Show labels underneath items gboolean show_labels; ///< Show labels underneath items
FivIoModel *model; ///< Filesystem model FivIoModel *model; ///< Filesystem model
GArray *entries; ///< []Entry GPtrArray *entries; ///< []*Entry
GArray *layouted_rows; ///< []Row GArray *layouted_rows; ///< []Row
const Entry *selected; ///< Selected entry or NULL const Entry *selected; ///< Selected entry or NULL
@ -112,14 +113,25 @@ struct entry {
FivIoModelEntry *e; ///< Reference to model entry FivIoModelEntry *e; ///< Reference to model entry
cairo_surface_t *thumbnail; ///< Prescaled thumbnail cairo_surface_t *thumbnail; ///< Prescaled thumbnail
GIcon *icon; ///< If no thumbnail, use this icon GIcon *icon; ///< If no thumbnail, use this icon
gboolean removed; ///< Model announced removal
}; };
static Entry *
entry_new(FivIoModelEntry *e)
{
Entry *self = g_slice_alloc0(sizeof *self);
self->e = e;
return self;
}
static void static void
entry_free(Entry *self) entry_destroy(Entry *self)
{ {
fiv_io_model_entry_unref(self->e); fiv_io_model_entry_unref(self->e);
g_clear_pointer(&self->thumbnail, cairo_surface_destroy); g_clear_pointer(&self->thumbnail, cairo_surface_destroy);
g_clear_object(&self->icon); g_clear_object(&self->icon);
g_slice_free1(sizeof *self, self);
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -202,7 +214,7 @@ relayout(FivBrowser *self, int width)
GArray *items = g_array_new(TRUE, TRUE, sizeof(Item)); GArray *items = g_array_new(TRUE, TRUE, sizeof(Item));
int x = 0, y = padding.top; int x = 0, y = padding.top;
for (guint i = 0; i < self->entries->len; i++) { for (guint i = 0; i < self->entries->len; i++) {
const Entry *entry = &g_array_index(self->entries, Entry, i); const Entry *entry = self->entries->pdata[i];
if (!entry->thumbnail) if (!entry->thumbnail)
continue; continue;
@ -414,6 +426,15 @@ draw_row(FivBrowser *self, cairo_t *cr, const Row *row)
// the whole rectangle with the selection color. // the whole rectangle with the selection color.
} }
// TODO(p): Come up with a better rendition.
if (item->entry->removed) {
cairo_move_to(cr, 0, border.top + extents.height + border.bottom);
cairo_line_to(cr, border.left + extents.width + border.right, 0);
cairo_set_source_rgb(cr, 0.5, 0.5, 0.5);
cairo_set_line_width(cr, 5);
cairo_stroke(cr);
}
if (self->show_labels) { if (self->show_labels) {
gtk_style_context_save(style); gtk_style_context_save(style);
gtk_style_context_add_class(style, "label"); gtk_style_context_add_class(style, "label");
@ -522,14 +543,9 @@ entry_set_surface_user_data(const Entry *self)
NULL); NULL);
} }
static void static cairo_surface_t *
entry_add_thumbnail(gpointer data, gpointer user_data) entry_lookup_thumbnail(Entry *self, FivBrowser *browser)
{ {
Entry *self = data;
g_clear_object(&self->icon);
g_clear_pointer(&self->thumbnail, cairo_surface_destroy);
FivBrowser *browser = FIV_BROWSER(user_data);
cairo_surface_t *cached = cairo_surface_t *cached =
g_hash_table_lookup(browser->thumbnail_cache, self->e->uri); g_hash_table_lookup(browser->thumbnail_cache, self->e->uri);
if (cached && if (cached &&
@ -537,18 +553,39 @@ entry_add_thumbnail(gpointer data, gpointer user_data)
&fiv_browser_key_mtime_msec) == (intptr_t) self->e->mtime_msec && &fiv_browser_key_mtime_msec) == (intptr_t) self->e->mtime_msec &&
(uintptr_t) cairo_surface_get_user_data(cached, (uintptr_t) cairo_surface_get_user_data(cached,
&fiv_browser_key_filesize) == (uintptr_t) self->e->filesize) { &fiv_browser_key_filesize) == (uintptr_t) self->e->filesize) {
self->thumbnail = cairo_surface_reference(cached);
// TODO(p): If this hit is low-quality, see if a high-quality thumbnail // TODO(p): If this hit is low-quality, see if a high-quality thumbnail
// hasn't been produced without our knowledge (avoid launching a minion // hasn't been produced without our knowledge (avoid launching a minion
// unnecessarily; we might also shift the concern there). // unnecessarily; we might also shift the concern there).
} else { return cairo_surface_reference(cached);
}
cairo_surface_t *found = fiv_thumbnail_lookup( cairo_surface_t *found = fiv_thumbnail_lookup(
entry_system_wide_uri(self), self->e->mtime_msec, self->e->filesize, entry_system_wide_uri(self), self->e->mtime_msec, self->e->filesize,
browser->item_size); browser->item_size);
self->thumbnail = rescale_thumbnail(found, browser->item_height); return rescale_thumbnail(found, browser->item_height);
}
static void
entry_add_thumbnail(gpointer data, gpointer user_data)
{
Entry *self = data;
FivBrowser *browser = FIV_BROWSER(user_data);
if (self->removed) {
// Keep whatever size of thumbnail we had at the time up until reload.
// g_file_query_info() fails for removed files, so keep the icon, too.
if (self->icon) {
g_clear_pointer(&self->thumbnail, cairo_surface_destroy);
} else {
self->thumbnail =
rescale_thumbnail(self->thumbnail, browser->item_height);
}
return;
} }
if (self->thumbnail) { g_clear_object(&self->icon);
g_clear_pointer(&self->thumbnail, cairo_surface_destroy);
if ((self->thumbnail = entry_lookup_thumbnail(self, browser))) {
// Yes, this is a pointless action in case it's been found in the cache. // Yes, this is a pointless action in case it's been found in the cache.
entry_set_surface_user_data(self); entry_set_surface_user_data(self);
return; return;
@ -624,15 +661,15 @@ reload_thumbnails(FivBrowser *self)
GThreadPool *pool = g_thread_pool_new( GThreadPool *pool = g_thread_pool_new(
entry_add_thumbnail, self, g_get_num_processors(), FALSE, NULL); entry_add_thumbnail, self, g_get_num_processors(), FALSE, NULL);
for (guint i = 0; i < self->entries->len; i++) for (guint i = 0; i < self->entries->len; i++)
g_thread_pool_push(pool, &g_array_index(self->entries, Entry, i), NULL); g_thread_pool_push(pool, self->entries->pdata[i], NULL);
g_thread_pool_free(pool, FALSE, TRUE); g_thread_pool_free(pool, FALSE, TRUE);
// Once a URI disappears from the model, its thumbnail is forgotten. // Once a URI disappears from the model, its thumbnail is forgotten.
g_hash_table_remove_all(self->thumbnail_cache); g_hash_table_remove_all(self->thumbnail_cache);
for (guint i = 0; i < self->entries->len; i++) { for (guint i = 0; i < self->entries->len; i++) {
Entry *entry = &g_array_index(self->entries, Entry, i); Entry *entry = self->entries->pdata[i];
if (entry->thumbnail) { if (!entry->removed && entry->thumbnail) {
g_hash_table_insert(self->thumbnail_cache, g_strdup(entry->e->uri), g_hash_table_insert(self->thumbnail_cache, g_strdup(entry->e->uri),
cairo_surface_reference(entry->thumbnail)); cairo_surface_reference(entry->thumbnail));
} }
@ -790,7 +827,10 @@ thumbnailers_start(FivBrowser *self)
GQueue lq = G_QUEUE_INIT; GQueue lq = G_QUEUE_INIT;
for (guint i = 0; i < self->entries->len; i++) { for (guint i = 0; i < self->entries->len; i++) {
Entry *entry = &g_array_index(self->entries, Entry, i); Entry *entry = self->entries->pdata[i];
if (entry->removed)
continue;
if (entry->icon) if (entry->icon)
g_queue_push_tail(&self->thumbnailers_queue, entry); g_queue_push_tail(&self->thumbnailers_queue, entry);
else if (cairo_surface_get_user_data( else if (cairo_surface_get_user_data(
@ -868,7 +908,7 @@ fiv_browser_finalize(GObject *gobject)
{ {
FivBrowser *self = FIV_BROWSER(gobject); FivBrowser *self = FIV_BROWSER(gobject);
thumbnailers_abort(self); thumbnailers_abort(self);
g_array_free(self->entries, TRUE); g_ptr_array_free(self->entries, TRUE);
g_array_free(self->layouted_rows, TRUE); g_array_free(self->layouted_rows, TRUE);
if (self->model) { if (self->model) {
g_signal_handlers_disconnect_by_data(self->model, self); g_signal_handlers_disconnect_by_data(self->model, self);
@ -1774,8 +1814,8 @@ fiv_browser_init(FivBrowser *self)
gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE); gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE);
gtk_widget_set_has_tooltip(GTK_WIDGET(self), TRUE); gtk_widget_set_has_tooltip(GTK_WIDGET(self), TRUE);
self->entries = g_array_new(FALSE, TRUE, sizeof(Entry)); self->entries =
g_array_set_clear_func(self->entries, (GDestroyNotify) entry_free); g_ptr_array_new_with_free_func((GDestroyNotify) entry_destroy);
self->layouted_rows = g_array_new(FALSE, TRUE, sizeof(Row)); self->layouted_rows = g_array_new(FALSE, TRUE, sizeof(Row));
g_array_set_clear_func(self->layouted_rows, (GDestroyNotify) row_free); g_array_set_clear_func(self->layouted_rows, (GDestroyNotify) row_free);
abort_button_tracking(self); abort_button_tracking(self);
@ -1810,9 +1850,8 @@ fiv_browser_init(FivBrowser *self)
// --- Public interface -------------------------------------------------------- // --- Public interface --------------------------------------------------------
// TODO(p): Later implement any arguments of this FivIoModel signal.
static void static void
on_model_files_changed(FivIoModel *model, FivBrowser *self) on_model_reloaded(FivIoModel *model, FivBrowser *self)
{ {
g_return_if_fail(model == self->model); g_return_if_fail(model == self->model);
@ -1821,14 +1860,14 @@ on_model_files_changed(FivIoModel *model, FivBrowser *self)
selected_uri = g_strdup(self->selected->e->uri); selected_uri = g_strdup(self->selected->e->uri);
thumbnailers_abort(self); thumbnailers_abort(self);
g_array_set_size(self->entries, 0);
g_array_set_size(self->layouted_rows, 0); g_array_set_size(self->layouted_rows, 0);
g_ptr_array_set_size(self->entries, 0);
gsize len = 0; gsize len = 0;
FivIoModelEntry *const *files = fiv_io_model_get_files(self->model, &len); FivIoModelEntry *const *files = fiv_io_model_get_files(self->model, &len);
for (gsize i = 0; i < len; i++) { for (gsize i = 0; i < len; i++) {
Entry e = {.e = fiv_io_model_entry_ref(files[i])}; g_ptr_array_add(
g_array_append_val(self->entries, e); self->entries, entry_new(fiv_io_model_entry_ref(files[i])));
} }
fiv_browser_select(self, selected_uri); fiv_browser_select(self, selected_uri);
@ -1838,6 +1877,55 @@ on_model_files_changed(FivIoModel *model, FivBrowser *self)
thumbnailers_start(self); thumbnailers_start(self);
} }
static void
on_model_changed(FivIoModel *model, FivIoModelEntry *old, FivIoModelEntry *new,
FivBrowser *self)
{
g_return_if_fail(model == self->model);
// Add new entries to the end, so as to not disturb the layout.
if (!old) {
g_ptr_array_add(
self->entries, entry_new(fiv_io_model_entry_ref(new)));
// TODO(p): Only process this one item, not everything at once.
// (This mainly has an effect on thumbnail-less entries.)
reload_thumbnails(self);
thumbnailers_start(self);
return;
}
Entry *found = NULL;
for (guint i = 0; i < self->entries->len; i++) {
Entry *entry = self->entries->pdata[i];
if (entry->e == old) {
found = entry;
break;
}
}
if (!found)
return;
// Rename entries in place, so as to not disturb the layout.
// XXX: This behaves differently from FivIoModel, and by extension fiv.c.
if (new) {
fiv_io_model_entry_unref(found->e);
found->e = fiv_io_model_entry_ref(new);
found->removed = FALSE;
// TODO(p): If there is a URI mismatch, don't reload thumbnails,
// so that there's no jumping around. Or, a bit more properly,
// move the thumbnail cache entry to the new URI.
// TODO(p): Only process this one item, not everything at once.
// (This mainly has an effect on thumbnail-less entries.)
reload_thumbnails(self);
thumbnailers_start(self);
} else {
found->removed = TRUE;
gtk_widget_queue_draw(GTK_WIDGET(self));
}
}
GtkWidget * GtkWidget *
fiv_browser_new(FivIoModel *model) fiv_browser_new(FivIoModel *model)
{ {
@ -1846,9 +1934,11 @@ fiv_browser_new(FivIoModel *model)
FivBrowser *self = g_object_new(FIV_TYPE_BROWSER, NULL); FivBrowser *self = g_object_new(FIV_TYPE_BROWSER, NULL);
self->model = g_object_ref(model); self->model = g_object_ref(model);
g_signal_connect( g_signal_connect(self->model, "reloaded",
self->model, "files-changed", G_CALLBACK(on_model_files_changed), self); G_CALLBACK(on_model_reloaded), self);
on_model_files_changed(self->model, self); g_signal_connect(self->model, "files-changed",
G_CALLBACK(on_model_changed), self);
on_model_reloaded(self->model, self);
return GTK_WIDGET(self); return GTK_WIDGET(self);
} }
@ -1877,7 +1967,7 @@ fiv_browser_select(FivBrowser *self, const char *uri)
return; return;
for (guint i = 0; i < self->entries->len; i++) { for (guint i = 0; i < self->entries->len; i++) {
const Entry *entry = &g_array_index(self->entries, Entry, i); const Entry *entry = self->entries->pdata[i];
if (!g_strcmp0(entry->e->uri, uri)) { if (!g_strcmp0(entry->e->uri, uri)) {
self->selected = entry; self->selected = entry;
scroll_to_selection(self); scroll_to_selection(self);

View File

@ -19,12 +19,109 @@
#include "fiv-io-model.h" #include "fiv-io-model.h"
#include "xdg.h" #include "xdg.h"
static GPtrArray * GType
model_entry_array_new(void) fiv_io_model_sort_get_type(void)
{ {
return g_ptr_array_new_with_free_func(g_rc_box_release); static gsize guard;
if (g_once_init_enter(&guard)) {
#define XX(name) {FIV_IO_MODEL_SORT_ ## name, \
"FIV_IO_MODEL_SORT_" #name, #name},
static const GEnumValue values[] = {FIV_IO_MODEL_SORTS(XX) {}};
#undef XX
GType type = g_enum_register_static(
g_intern_static_string("FivIoModelSort"), values);
g_once_init_leave(&guard, type);
}
return guard;
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
G_DEFINE_BOXED_TYPE(FivIoModelEntry, fiv_io_model_entry,
fiv_io_model_entry_ref, fiv_io_model_entry_unref)
FivIoModelEntry *
fiv_io_model_entry_ref(FivIoModelEntry *self)
{
return g_rc_box_acquire(self);
}
void
fiv_io_model_entry_unref(FivIoModelEntry *self)
{
g_rc_box_release(self);
}
static size_t
entry_strsize(const char *string)
{
if (!string)
return 0;
return strlen(string) + 1;
}
static char *
entry_strappend(char **p, const char *string, size_t size)
{
if (!string)
return NULL;
char *destination = memcpy(*p, string, size);
*p += size;
return destination;
}
// See model_load_attributes for a (superset of a) list of required attributes.
static FivIoModelEntry *
entry_new(GFile *file, GFileInfo *info)
{
gchar *uri = g_file_get_uri(file);
const gchar *target_uri = g_file_info_get_attribute_string(
info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI);
const gchar *display_name = g_file_info_get_display_name(info);
// TODO(p): Make it possible to use g_utf8_collate_key() instead,
// which does not use natural sorting.
gchar *parse_name = g_file_get_parse_name(file);
gchar *collate_key = g_utf8_collate_key_for_filename(parse_name, -1);
g_free(parse_name);
// The entries are immutable. Packing them into the structure
// should help memory usage as well as performance.
size_t size_uri = entry_strsize(uri);
size_t size_target_uri = entry_strsize(target_uri);
size_t size_display_name = entry_strsize(display_name);
size_t size_collate_key = entry_strsize(collate_key);
FivIoModelEntry *entry = g_rc_box_alloc0(sizeof *entry +
size_uri +
size_target_uri +
size_display_name +
size_collate_key);
gchar *p = (gchar *) entry + sizeof *entry;
entry->uri = entry_strappend(&p, uri, size_uri);
entry->target_uri = entry_strappend(&p, target_uri, size_target_uri);
entry->display_name = entry_strappend(&p, display_name, size_display_name);
entry->collate_key = entry_strappend(&p, collate_key, size_collate_key);
entry->filesize = (guint64) g_file_info_get_size(info);
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);
}
g_free(uri);
g_free(collate_key);
return entry;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct _FivIoModel { struct _FivIoModel {
GObject parent_instance; GObject parent_instance;
GPatternSpec **supported_patterns; GPatternSpec **supported_patterns;
@ -51,6 +148,7 @@ enum {
static GParamSpec *model_properties[N_PROPERTIES]; static GParamSpec *model_properties[N_PROPERTIES];
enum { enum {
RELOADED,
FILES_CHANGED, FILES_CHANGED,
SUBDIRECTORIES_CHANGED, SUBDIRECTORIES_CHANGED,
LAST_SIGNAL, LAST_SIGNAL,
@ -61,6 +159,13 @@ static guint model_signals[LAST_SIGNAL];
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static GPtrArray *
model_entry_array_new(void)
{
return g_ptr_array_new_with_free_func(
(GDestroyNotify) fiv_io_model_entry_unref);
}
static gboolean static gboolean
model_supports(FivIoModel *self, const char *filename) model_supports(FivIoModel *self, const char *filename)
{ {
@ -124,71 +229,28 @@ model_compare(gconstpointer a, gconstpointer b, gpointer user_data)
return result; return result;
} }
static size_t static const char *model_load_attributes =
model_strsize(const char *string) G_FILE_ATTRIBUTE_STANDARD_TYPE ","
{ G_FILE_ATTRIBUTE_STANDARD_NAME ","
if (!string) G_FILE_ATTRIBUTE_STANDARD_SIZE ","
return 0; G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME ","
G_FILE_ATTRIBUTE_STANDARD_TARGET_URI ","
G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","
G_FILE_ATTRIBUTE_TIME_MODIFIED ","
G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC;
return strlen(string) + 1; static GPtrArray *
} model_decide_placement(
FivIoModel *self, GFileInfo *info, GPtrArray *subdirs, GPtrArray *files)
static char *
model_strappend(char **p, const char *string, size_t size)
{ {
if (!string) if (self->filtering && g_file_info_get_is_hidden(info))
return NULL;
if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY)
return subdirs;
if (!self->filtering ||
model_supports(self, g_file_info_get_name(info)))
return files;
return NULL; return NULL;
char *destination = memcpy(*p, string, size);
*p += size;
return destination;
}
static FivIoModelEntry *
model_entry_new(GFile *file, GFileInfo *info)
{
gchar *uri = g_file_get_uri(file);
const gchar *target_uri = g_file_info_get_attribute_string(
info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI);
const gchar *display_name = g_file_info_get_display_name(info);
// TODO(p): Make it possible to use g_utf8_collate_key() instead,
// which does not use natural sorting.
gchar *parse_name = g_file_get_parse_name(file);
gchar *collate_key = g_utf8_collate_key_for_filename(parse_name, -1);
g_free(parse_name);
// The entries are immutable. Packing them into the structure
// should help memory usage as well as performance.
size_t size_uri = model_strsize(uri);
size_t size_target_uri = model_strsize(target_uri);
size_t size_display_name = model_strsize(display_name);
size_t size_collate_key = model_strsize(collate_key);
FivIoModelEntry *entry = g_rc_box_alloc0(sizeof *entry +
size_uri +
size_target_uri +
size_display_name +
size_collate_key);
gchar *p = (gchar *) entry + sizeof *entry;
entry->uri = model_strappend(&p, uri, size_uri);
entry->target_uri = model_strappend(&p, target_uri, size_target_uri);
entry->display_name = model_strappend(&p, display_name, size_display_name);
entry->collate_key = model_strappend(&p, collate_key, size_collate_key);
entry->filesize = (guint64) g_file_info_get_size(info);
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);
}
g_free(uri);
g_free(collate_key);
return entry;
} }
static gboolean static gboolean
@ -200,16 +262,8 @@ model_reload_to(FivIoModel *self, GFile *directory,
if (files) if (files)
g_ptr_array_set_size(files, 0); g_ptr_array_set_size(files, 0);
GFileEnumerator *enumerator = g_file_enumerate_children(directory, GFileEnumerator *enumerator = g_file_enumerate_children(
G_FILE_ATTRIBUTE_STANDARD_TYPE "," directory, model_load_attributes, G_FILE_QUERY_INFO_NONE, NULL, error);
G_FILE_ATTRIBUTE_STANDARD_NAME ","
G_FILE_ATTRIBUTE_STANDARD_SIZE ","
G_FILE_ATTRIBUTE_STANDARD_DISPLAY_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_QUERY_INFO_NONE, NULL, error);
if (!enumerator) if (!enumerator)
return FALSE; return FALSE;
@ -225,18 +279,11 @@ model_reload_to(FivIoModel *self, GFile *directory,
} }
if (!info) if (!info)
break; break;
if (self->filtering && g_file_info_get_is_hidden(info))
continue;
GPtrArray *target = NULL;
if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY)
target = subdirs;
else if (!self->filtering ||
model_supports(self, g_file_info_get_name(info)))
target = files;
GPtrArray *target =
model_decide_placement(self, info, subdirs, files);
if (target) if (target)
g_ptr_array_add(target, model_entry_new(child, info)); g_ptr_array_add(target, entry_new(child, info));
} }
g_object_unref(enumerator); g_object_unref(enumerator);
@ -253,9 +300,7 @@ model_reload(FivIoModel *self, GError **error)
// Note that this will clear all entries on failure. // Note that this will clear all entries on failure.
gboolean result = model_reload_to( gboolean result = model_reload_to(
self, self->directory, self->subdirs, self->files, error); self, self->directory, self->subdirs, self->files, error);
g_signal_emit(self, model_signals[RELOADED], 0);
g_signal_emit(self, model_signals[FILES_CHANGED], 0);
g_signal_emit(self, model_signals[SUBDIRECTORIES_CHANGED], 0);
return result; return result;
} }
@ -264,9 +309,144 @@ model_resort(FivIoModel *self)
{ {
g_ptr_array_sort_with_data(self->subdirs, model_compare, self); g_ptr_array_sort_with_data(self->subdirs, model_compare, self);
g_ptr_array_sort_with_data(self->files, model_compare, self); g_ptr_array_sort_with_data(self->files, model_compare, self);
g_signal_emit(self, model_signals[RELOADED], 0);
}
g_signal_emit(self, model_signals[FILES_CHANGED], 0); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
g_signal_emit(self, model_signals[SUBDIRECTORIES_CHANGED], 0);
static gint
model_find(const GPtrArray *target, GFile *file, FivIoModelEntry **entry)
{
for (guint i = 0; i < target->len; i++) {
FivIoModelEntry *e = target->pdata[i];
GFile *f = g_file_new_for_uri(e->uri);
gboolean match = g_file_equal(f, file);
g_object_unref(f);
if (match) {
*entry = e;
return i;
}
}
return -1;
}
static void
on_monitor_changed(G_GNUC_UNUSED GFileMonitor *monitor, GFile *file,
GFile *other_file, GFileMonitorEvent event_type, gpointer user_data)
{
FivIoModel *self = user_data;
FivIoModelEntry *old_entry = NULL;
gint files_index = model_find(self->files, file, &old_entry);
gint subdirs_index = model_find(self->subdirs, file, &old_entry);
enum { NONE, CHANGING, RENAMING, REMOVING, ADDING } action = NONE;
GFile *new_entry_file = NULL;
switch (event_type) {
case G_FILE_MONITOR_EVENT_CHANGED:
case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
action = CHANGING;
new_entry_file = file;
break;
case G_FILE_MONITOR_EVENT_RENAMED:
action = RENAMING;
new_entry_file = other_file;
break;
case G_FILE_MONITOR_EVENT_DELETED:
case G_FILE_MONITOR_EVENT_MOVED_OUT:
action = REMOVING;
break;
case G_FILE_MONITOR_EVENT_CREATED:
case G_FILE_MONITOR_EVENT_MOVED_IN:
action = ADDING;
old_entry = NULL;
new_entry_file = file;
break;
case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
// TODO(p): Figure out if we can't make use of _CHANGES_DONE_HINT.
case G_FILE_MONITOR_EVENT_PRE_UNMOUNT:
case G_FILE_MONITOR_EVENT_UNMOUNTED:
// TODO(p): Figure out how to handle _UNMOUNTED sensibly.
case G_FILE_MONITOR_EVENT_MOVED:
return;
}
FivIoModelEntry *new_entry = NULL;
GPtrArray *new_target = NULL;
if (new_entry_file) {
GError *error = NULL;
GFileInfo *info = g_file_query_info(new_entry_file,
model_load_attributes, G_FILE_QUERY_INFO_NONE, NULL, &error);
if (error) {
g_debug("monitor: %s", error->message);
g_error_free(error);
goto run;
}
if ((new_target =
model_decide_placement(self, info, self->subdirs, self->files)))
new_entry = entry_new(new_entry_file, info);
g_object_unref(info);
if ((files_index != -1 && new_target == self->subdirs) ||
(subdirs_index != -1 && new_target == self->files)) {
g_debug("monitor: ignoring transfer between files and subdirs");
goto out;
}
}
run:
// Keep a reference alive so that signal handlers see the new arrays.
if (old_entry)
fiv_io_model_entry_ref(old_entry);
if (files_index != -1 || new_target == self->files) {
if (action == CHANGING) {
g_assert(new_entry != NULL);
fiv_io_model_entry_unref(self->files->pdata[files_index]);
self->files->pdata[files_index] =
fiv_io_model_entry_ref(new_entry);
}
if (action == REMOVING || action == RENAMING)
g_ptr_array_remove_index(self->files, files_index);
if (action == RENAMING || action == ADDING) {
g_assert(new_entry != NULL);
g_ptr_array_add(self->files, fiv_io_model_entry_ref(new_entry));
}
g_signal_emit(self, model_signals[FILES_CHANGED],
0, old_entry, new_entry);
}
if (subdirs_index != -1 || new_target == self->subdirs) {
if (action == CHANGING) {
g_assert(new_entry != NULL);
fiv_io_model_entry_unref(self->subdirs->pdata[subdirs_index]);
self->subdirs->pdata[subdirs_index] =
fiv_io_model_entry_ref(new_entry);
}
if (action == REMOVING || action == RENAMING)
g_ptr_array_remove_index(self->subdirs, subdirs_index);
if (action == RENAMING || action == ADDING) {
g_assert(new_entry != NULL);
g_ptr_array_add(self->subdirs, fiv_io_model_entry_ref(new_entry));
}
g_signal_emit(self, model_signals[SUBDIRECTORIES_CHANGED],
0, old_entry, new_entry);
}
// NOTE: It would make sense to do
// g_ptr_array_sort_with_data(self->{files,subdirs}, model_compare, self);
// but then the iteration behaviour of fiv.c would differ from what's shown
// in the browser. Perhaps we need to use an index-based, fully-synchronized
// interface similar to GListModel::items-changed.
if (old_entry)
fiv_io_model_entry_unref(old_entry);
out:
if (new_entry)
fiv_io_model_entry_unref(new_entry);
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -406,7 +586,7 @@ fiv_io_model_get_property(
g_value_set_boolean(value, self->filtering); g_value_set_boolean(value, self->filtering);
break; break;
case PROP_SORT_FIELD: case PROP_SORT_FIELD:
g_value_set_int(value, self->sort_field); g_value_set_enum(value, self->sort_field);
break; break;
case PROP_SORT_DESCENDING: case PROP_SORT_DESCENDING:
g_value_set_boolean(value, self->sort_descending); g_value_set_boolean(value, self->sort_descending);
@ -430,8 +610,8 @@ fiv_io_model_set_property(
} }
break; break;
case PROP_SORT_FIELD: case PROP_SORT_FIELD:
if ((int) self->sort_field != g_value_get_int(value)) { if ((int) self->sort_field != g_value_get_enum(value)) {
self->sort_field = g_value_get_int(value); self->sort_field = g_value_get_enum(value);
g_object_notify_by_pspec(object, model_properties[property_id]); g_object_notify_by_pspec(object, model_properties[property_id]);
model_resort(self); model_resort(self);
} }
@ -459,24 +639,28 @@ fiv_io_model_class_init(FivIoModelClass *klass)
model_properties[PROP_FILTERING] = g_param_spec_boolean( model_properties[PROP_FILTERING] = g_param_spec_boolean(
"filtering", "Filtering", "Only show non-hidden, supported entries", "filtering", "Filtering", "Only show non-hidden, supported entries",
TRUE, G_PARAM_READWRITE); TRUE, G_PARAM_READWRITE);
// TODO(p): GObject enumerations are annoying, but this should be one. model_properties[PROP_SORT_FIELD] = g_param_spec_enum(
model_properties[PROP_SORT_FIELD] = g_param_spec_int(
"sort-field", "Sort field", "Sort order", "sort-field", "Sort field", "Sort order",
FIV_IO_MODEL_SORT_MIN, FIV_IO_MODEL_SORT_MAX, FIV_TYPE_IO_MODEL_SORT, FIV_IO_MODEL_SORT_NAME, G_PARAM_READWRITE);
FIV_IO_MODEL_SORT_NAME, G_PARAM_READWRITE);
model_properties[PROP_SORT_DESCENDING] = g_param_spec_boolean( model_properties[PROP_SORT_DESCENDING] = g_param_spec_boolean(
"sort-descending", "Sort descending", "Use reverse sort order", "sort-descending", "Sort descending", "Use reverse sort order",
FALSE, G_PARAM_READWRITE); FALSE, G_PARAM_READWRITE);
g_object_class_install_properties( g_object_class_install_properties(
object_class, N_PROPERTIES, model_properties); object_class, N_PROPERTIES, model_properties);
// TODO(p): Arguments something like: index, added, removed. // All entries might have changed.
model_signals[RELOADED] =
g_signal_new("reloaded", G_TYPE_FROM_CLASS(klass), 0, 0,
NULL, NULL, NULL, G_TYPE_NONE, 0);
model_signals[FILES_CHANGED] = model_signals[FILES_CHANGED] =
g_signal_new("files-changed", G_TYPE_FROM_CLASS(klass), 0, 0, g_signal_new("files-changed", G_TYPE_FROM_CLASS(klass), 0, 0,
NULL, NULL, NULL, G_TYPE_NONE, 0); NULL, NULL, NULL,
G_TYPE_NONE, 2, FIV_TYPE_IO_MODEL_ENTRY, FIV_TYPE_IO_MODEL_ENTRY);
model_signals[SUBDIRECTORIES_CHANGED] = model_signals[SUBDIRECTORIES_CHANGED] =
g_signal_new("subdirectories-changed", G_TYPE_FROM_CLASS(klass), 0, 0, g_signal_new("subdirectories-changed", G_TYPE_FROM_CLASS(klass), 0, 0,
NULL, NULL, NULL, G_TYPE_NONE, 0); NULL, NULL, NULL,
G_TYPE_NONE, 2, FIV_TYPE_IO_MODEL_ENTRY, FIV_TYPE_IO_MODEL_ENTRY);
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -511,9 +695,15 @@ fiv_io_model_open(FivIoModel *self, GFile *directory, GError **error)
g_clear_object(&self->monitor); g_clear_object(&self->monitor);
self->directory = g_object_ref(directory); self->directory = g_object_ref(directory);
// TODO(p): Process the ::changed signal. GError *e = NULL;
self->monitor = g_file_monitor_directory( if ((self->monitor = g_file_monitor_directory(
directory, G_FILE_MONITOR_WATCH_MOVES, NULL, NULL /* error */); directory, G_FILE_MONITOR_WATCH_MOVES, NULL, &e))) {
g_signal_connect(self->monitor, "changed",
G_CALLBACK(on_monitor_changed), self);
} else {
g_debug("directory monitoring failed: %s", e->message);
g_error_free(e);
}
return model_reload(self, error); return model_reload(self, error);
} }

View File

@ -20,15 +20,39 @@
#include <gio/gio.h> #include <gio/gio.h>
#include <glib.h> #include <glib.h>
// Avoid glib-mkenums.
typedef enum _FivIoModelSort { typedef enum _FivIoModelSort {
FIV_IO_MODEL_SORT_NAME, #define FIV_IO_MODEL_SORTS(XX) \
FIV_IO_MODEL_SORT_MTIME, XX(NAME) \
FIV_IO_MODEL_SORT_COUNT, XX(MTIME)
#define XX(name) FIV_IO_MODEL_SORT_ ## name,
FIV_IO_MODEL_SORT_MIN = 0, FIV_IO_MODEL_SORTS(XX)
FIV_IO_MODEL_SORT_MAX = FIV_IO_MODEL_SORT_COUNT - 1 #undef XX
FIV_IO_MODEL_SORT_COUNT
} FivIoModelSort; } FivIoModelSort;
GType fiv_io_model_sort_get_type(void) G_GNUC_CONST;
#define FIV_TYPE_IO_MODEL_SORT (fiv_io_model_sort_get_type())
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
typedef struct {
const char *uri; ///< GIO URI
const char *target_uri; ///< GIO URI for any target
const char *display_name; ///< Label for the file
const char *collate_key; ///< Collate key for the filename
guint64 filesize; ///< Filesize in bytes
gint64 mtime_msec; ///< Modification time in milliseconds
} FivIoModelEntry;
GType fiv_io_model_entry_get_type(void) G_GNUC_CONST;
#define FIV_TYPE_IO_MODEL_ENTRY (fiv_io_model_entry_get_type())
FivIoModelEntry *fiv_io_model_entry_ref(FivIoModelEntry *self);
void fiv_io_model_entry_unref(FivIoModelEntry *self);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#define FIV_TYPE_IO_MODEL (fiv_io_model_get_type()) #define FIV_TYPE_IO_MODEL (fiv_io_model_get_type())
G_DECLARE_FINAL_TYPE(FivIoModel, fiv_io_model, FIV, IO_MODEL, GObject) G_DECLARE_FINAL_TYPE(FivIoModel, fiv_io_model, FIV, IO_MODEL, GObject)
@ -44,18 +68,5 @@ GFile *fiv_io_model_get_previous_directory(FivIoModel *self);
/// Returns the next VFS directory in order, or NULL. /// Returns the next VFS directory in order, or NULL.
GFile *fiv_io_model_get_next_directory(FivIoModel *self); GFile *fiv_io_model_get_next_directory(FivIoModel *self);
// These objects are reference-counted using GRcBox.
typedef struct {
const char *uri; ///< GIO URI
const char *target_uri; ///< GIO URI for any target
const char *display_name; ///< Label for the file
const char *collate_key; ///< Collate key for the filename
guint64 filesize; ///< Filesize in bytes
gint64 mtime_msec; ///< Modification time in milliseconds
} FivIoModelEntry;
#define fiv_io_model_entry_ref(e) g_rc_box_acquire(e)
#define fiv_io_model_entry_unref(e) g_rc_box_release(e)
FivIoModelEntry *const *fiv_io_model_get_files(FivIoModel *self, gsize *len); FivIoModelEntry *const *fiv_io_model_get_files(FivIoModel *self, gsize *len);
FivIoModelEntry *const *fiv_io_model_get_subdirs(FivIoModel *self, gsize *len); FivIoModelEntry *const *fiv_io_model_get_subdirs(FivIoModel *self, gsize *len);

View File

@ -623,9 +623,9 @@ fiv_sidebar_new(FivIoModel *model)
gtk_container_set_focus_vadjustment(GTK_CONTAINER(sidebar_port), gtk_container_set_focus_vadjustment(GTK_CONTAINER(sidebar_port),
gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(self))); gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(self)));
// TODO(p): There should be an extra signal to watch location changes only. // TODO(p): Also connect to and process the subdirectories-changed signal.
self->model = g_object_ref(model); self->model = g_object_ref(model);
g_signal_connect_swapped(self->model, "subdirectories-changed", g_signal_connect_swapped(self->model, "reloaded",
G_CALLBACK(update_location), self); G_CALLBACK(update_location), self);
return GTK_WIDGET(self); return GTK_WIDGET(self);

View File

@ -21,7 +21,7 @@
#include <gio/gio.h> #include <gio/gio.h>
#include <glib.h> #include <glib.h>
// And this is how you avoid glib-mkenums. // Avoid glib-mkenums.
typedef enum _FivThumbnailSize { typedef enum _FivThumbnailSize {
#define FIV_THUMBNAIL_SIZES(XX) \ #define FIV_THUMBNAIL_SIZES(XX) \
XX(SMALL, 128, "normal") \ XX(SMALL, 128, "normal") \

18
fiv.c
View File

@ -733,7 +733,7 @@ load_directory_without_switching(const char *uri)
GError *error = NULL; GError *error = NULL;
GFile *file = g_file_new_for_uri(g.directory); GFile *file = g_file_new_for_uri(g.directory);
if (fiv_io_model_open(g.model, file, &error)) { if (fiv_io_model_open(g.model, file, &error)) {
// This is handled by our ::files-changed callback. // This is handled by our ::reloaded callback.
} else if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) { } else if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) {
g_error_free(error); g_error_free(error);
} else { } else {
@ -797,7 +797,7 @@ go_forward(void)
} }
static void static void
on_model_files_changed(FivIoModel *model, G_GNUC_UNUSED gpointer user_data) on_model_reloaded(FivIoModel *model, G_GNUC_UNUSED gpointer user_data)
{ {
g_return_if_fail(model == g.model); g_return_if_fail(model == g.model);
@ -810,6 +810,13 @@ on_model_files_changed(FivIoModel *model, G_GNUC_UNUSED gpointer user_data)
gtk_widget_set_sensitive(g.toolbar[TOOLBAR_FILE_NEXT], files_len > 1); gtk_widget_set_sensitive(g.toolbar[TOOLBAR_FILE_NEXT], files_len > 1);
} }
static void
on_model_files_changed(FivIoModel *model, G_GNUC_UNUSED FivIoModelEntry *old,
G_GNUC_UNUSED FivIoModelEntry *new, G_GNUC_UNUSED gpointer user_data)
{
on_model_reloaded(model, NULL);
}
static void static void
on_sidebar_toggled(GtkToggleButton *button, G_GNUC_UNUSED gpointer user_data) on_sidebar_toggled(GtkToggleButton *button, G_GNUC_UNUSED gpointer user_data)
{ {
@ -838,7 +845,8 @@ on_sort_field(G_GNUC_UNUSED GtkToggleButton *button, gpointer data)
if (!active) if (!active)
return; return;
int old = -1, new = (int) (intptr_t) data; FivIoModelSort old = FIV_IO_MODEL_SORT_COUNT;
FivIoModelSort new = (FivIoModelSort) (intptr_t) data;
g_object_get(g.model, "sort-field", &old, NULL); g_object_get(g.model, "sort-field", &old, NULL);
if (old != new) if (old != new)
g_object_set(g.model, "sort-field", new, NULL); g_object_set(g.model, "sort-field", new, NULL);
@ -1206,7 +1214,7 @@ static void
on_notify_thumbnail_size( on_notify_thumbnail_size(
GObject *object, GParamSpec *param_spec, G_GNUC_UNUSED gpointer user_data) GObject *object, GParamSpec *param_spec, G_GNUC_UNUSED gpointer user_data)
{ {
FivThumbnailSize size = 0; FivThumbnailSize size = FIV_THUMBNAIL_SIZE_COUNT;
g_object_get(object, g_param_spec_get_name(param_spec), &size, NULL); g_object_get(object, g_param_spec_get_name(param_spec), &size, NULL);
gtk_widget_set_sensitive( gtk_widget_set_sensitive(
g.browsebar[BROWSEBAR_PLUS], size < FIV_THUMBNAIL_SIZE_MAX); g.browsebar[BROWSEBAR_PLUS], size < FIV_THUMBNAIL_SIZE_MAX);
@ -2253,6 +2261,8 @@ main(int argc, char *argv[])
fiv_collection_register(); fiv_collection_register();
g.model = g_object_new(FIV_TYPE_IO_MODEL, NULL); g.model = g_object_new(FIV_TYPE_IO_MODEL, NULL);
g_signal_connect(g.model, "reloaded",
G_CALLBACK(on_model_reloaded), NULL);
g_signal_connect(g.model, "files-changed", g_signal_connect(g.model, "files-changed",
G_CALLBACK(on_model_files_changed), NULL); G_CALLBACK(on_model_files_changed), NULL);