From 200485246bdfda2c77689408dcd407d106639e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?= Date: Tue, 30 May 2023 10:36:11 +0200 Subject: [PATCH] Process some GFileMonitor events So far, it's rather crude. --- fiv-browser.c | 156 +++++++++++++++---- fiv-io-model.c | 396 +++++++++++++++++++++++++++++++++++------------- fiv-io-model.h | 49 +++--- fiv-sidebar.c | 4 +- fiv-thumbnail.h | 2 +- fiv.c | 18 ++- 6 files changed, 463 insertions(+), 162 deletions(-) diff --git a/fiv-browser.c b/fiv-browser.c index 01ff8f0..8c79ca6 100644 --- a/fiv-browser.c +++ b/fiv-browser.c @@ -32,6 +32,7 @@ #include "fiv-collection.h" #include "fiv-context-menu.h" #include "fiv-io.h" +#include "fiv-io-model.h" #include "fiv-thumbnail.h" // --- Widget ------------------------------------------------------------------ @@ -78,7 +79,7 @@ struct _FivBrowser { gboolean show_labels; ///< Show labels underneath items FivIoModel *model; ///< Filesystem model - GArray *entries; ///< []Entry + GPtrArray *entries; ///< []*Entry GArray *layouted_rows; ///< []Row const Entry *selected; ///< Selected entry or NULL @@ -112,14 +113,25 @@ struct entry { FivIoModelEntry *e; ///< Reference to model entry cairo_surface_t *thumbnail; ///< Prescaled thumbnail 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 -entry_free(Entry *self) +entry_destroy(Entry *self) { fiv_io_model_entry_unref(self->e); g_clear_pointer(&self->thumbnail, cairo_surface_destroy); 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)); int x = 0, y = padding.top; 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) continue; @@ -414,6 +426,15 @@ draw_row(FivBrowser *self, cairo_t *cr, const Row *row) // 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) { gtk_style_context_save(style); gtk_style_context_add_class(style, "label"); @@ -522,14 +543,9 @@ entry_set_surface_user_data(const Entry *self) NULL); } -static void -entry_add_thumbnail(gpointer data, gpointer user_data) +static cairo_surface_t * +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 = g_hash_table_lookup(browser->thumbnail_cache, self->e->uri); 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 && (uintptr_t) cairo_surface_get_user_data(cached, &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 // hasn't been produced without our knowledge (avoid launching a minion // unnecessarily; we might also shift the concern there). - } else { - cairo_surface_t *found = fiv_thumbnail_lookup( - entry_system_wide_uri(self), self->e->mtime_msec, self->e->filesize, - browser->item_size); - self->thumbnail = rescale_thumbnail(found, browser->item_height); + return cairo_surface_reference(cached); } - if (self->thumbnail) { + cairo_surface_t *found = fiv_thumbnail_lookup( + entry_system_wide_uri(self), self->e->mtime_msec, self->e->filesize, + browser->item_size); + 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; + } + + 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. entry_set_surface_user_data(self); return; @@ -624,15 +661,15 @@ reload_thumbnails(FivBrowser *self) GThreadPool *pool = g_thread_pool_new( entry_add_thumbnail, self, g_get_num_processors(), FALSE, NULL); 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); // Once a URI disappears from the model, its thumbnail is forgotten. g_hash_table_remove_all(self->thumbnail_cache); for (guint i = 0; i < self->entries->len; i++) { - Entry *entry = &g_array_index(self->entries, Entry, i); - if (entry->thumbnail) { + Entry *entry = self->entries->pdata[i]; + if (!entry->removed && entry->thumbnail) { g_hash_table_insert(self->thumbnail_cache, g_strdup(entry->e->uri), cairo_surface_reference(entry->thumbnail)); } @@ -790,7 +827,10 @@ thumbnailers_start(FivBrowser *self) GQueue lq = G_QUEUE_INIT; 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) g_queue_push_tail(&self->thumbnailers_queue, entry); else if (cairo_surface_get_user_data( @@ -868,7 +908,7 @@ fiv_browser_finalize(GObject *gobject) { FivBrowser *self = FIV_BROWSER(gobject); thumbnailers_abort(self); - g_array_free(self->entries, TRUE); + g_ptr_array_free(self->entries, TRUE); g_array_free(self->layouted_rows, TRUE); if (self->model) { 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_has_tooltip(GTK_WIDGET(self), TRUE); - self->entries = g_array_new(FALSE, TRUE, sizeof(Entry)); - g_array_set_clear_func(self->entries, (GDestroyNotify) entry_free); + self->entries = + g_ptr_array_new_with_free_func((GDestroyNotify) entry_destroy); self->layouted_rows = g_array_new(FALSE, TRUE, sizeof(Row)); g_array_set_clear_func(self->layouted_rows, (GDestroyNotify) row_free); abort_button_tracking(self); @@ -1810,9 +1850,8 @@ fiv_browser_init(FivBrowser *self) // --- Public interface -------------------------------------------------------- -// TODO(p): Later implement any arguments of this FivIoModel signal. static void -on_model_files_changed(FivIoModel *model, FivBrowser *self) +on_model_reloaded(FivIoModel *model, FivBrowser *self) { 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); thumbnailers_abort(self); - g_array_set_size(self->entries, 0); g_array_set_size(self->layouted_rows, 0); + g_ptr_array_set_size(self->entries, 0); gsize len = 0; FivIoModelEntry *const *files = fiv_io_model_get_files(self->model, &len); for (gsize i = 0; i < len; i++) { - Entry e = {.e = fiv_io_model_entry_ref(files[i])}; - g_array_append_val(self->entries, e); + g_ptr_array_add( + self->entries, entry_new(fiv_io_model_entry_ref(files[i]))); } fiv_browser_select(self, selected_uri); @@ -1838,6 +1877,55 @@ on_model_files_changed(FivIoModel *model, FivBrowser *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 * fiv_browser_new(FivIoModel *model) { @@ -1846,9 +1934,11 @@ fiv_browser_new(FivIoModel *model) FivBrowser *self = g_object_new(FIV_TYPE_BROWSER, NULL); self->model = g_object_ref(model); - g_signal_connect( - self->model, "files-changed", G_CALLBACK(on_model_files_changed), self); - on_model_files_changed(self->model, self); + g_signal_connect(self->model, "reloaded", + G_CALLBACK(on_model_reloaded), self); + g_signal_connect(self->model, "files-changed", + G_CALLBACK(on_model_changed), self); + on_model_reloaded(self->model, self); return GTK_WIDGET(self); } @@ -1877,7 +1967,7 @@ fiv_browser_select(FivBrowser *self, const char *uri) return; 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)) { self->selected = entry; scroll_to_selection(self); diff --git a/fiv-io-model.c b/fiv-io-model.c index c133238..58cf632 100644 --- a/fiv-io-model.c +++ b/fiv-io-model.c @@ -19,12 +19,109 @@ #include "fiv-io-model.h" #include "xdg.h" -static GPtrArray * -model_entry_array_new(void) +GType +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 { GObject parent_instance; GPatternSpec **supported_patterns; @@ -51,6 +148,7 @@ enum { static GParamSpec *model_properties[N_PROPERTIES]; enum { + RELOADED, FILES_CHANGED, SUBDIRECTORIES_CHANGED, 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 model_supports(FivIoModel *self, const char *filename) { @@ -124,71 +229,28 @@ model_compare(gconstpointer a, gconstpointer b, gpointer user_data) return result; } -static size_t -model_strsize(const char *string) -{ - if (!string) - return 0; +static const char *model_load_attributes = + G_FILE_ATTRIBUTE_STANDARD_TYPE "," + 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; - return strlen(string) + 1; -} - -static char * -model_strappend(char **p, const char *string, size_t size) +static GPtrArray * +model_decide_placement( + FivIoModel *self, GFileInfo *info, GPtrArray *subdirs, GPtrArray *files) { - if (!string) + if (self->filtering && g_file_info_get_is_hidden(info)) 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; + 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; } static gboolean @@ -200,16 +262,8 @@ model_reload_to(FivIoModel *self, GFile *directory, if (files) g_ptr_array_set_size(files, 0); - GFileEnumerator *enumerator = g_file_enumerate_children(directory, - G_FILE_ATTRIBUTE_STANDARD_TYPE "," - 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); + GFileEnumerator *enumerator = g_file_enumerate_children( + directory, model_load_attributes, G_FILE_QUERY_INFO_NONE, NULL, error); if (!enumerator) return FALSE; @@ -225,18 +279,11 @@ model_reload_to(FivIoModel *self, GFile *directory, } if (!info) 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) - g_ptr_array_add(target, model_entry_new(child, info)); + g_ptr_array_add(target, entry_new(child, info)); } g_object_unref(enumerator); @@ -253,9 +300,7 @@ model_reload(FivIoModel *self, GError **error) // Note that this will clear all entries on failure. gboolean result = model_reload_to( self, self->directory, self->subdirs, self->files, error); - - g_signal_emit(self, model_signals[FILES_CHANGED], 0); - g_signal_emit(self, model_signals[SUBDIRECTORIES_CHANGED], 0); + g_signal_emit(self, model_signals[RELOADED], 0); 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->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); break; case PROP_SORT_FIELD: - g_value_set_int(value, self->sort_field); + g_value_set_enum(value, self->sort_field); break; case PROP_SORT_DESCENDING: g_value_set_boolean(value, self->sort_descending); @@ -430,8 +610,8 @@ fiv_io_model_set_property( } break; case PROP_SORT_FIELD: - if ((int) self->sort_field != g_value_get_int(value)) { - self->sort_field = g_value_get_int(value); + if ((int) self->sort_field != g_value_get_enum(value)) { + self->sort_field = g_value_get_enum(value); g_object_notify_by_pspec(object, model_properties[property_id]); model_resort(self); } @@ -459,24 +639,28 @@ fiv_io_model_class_init(FivIoModelClass *klass) model_properties[PROP_FILTERING] = g_param_spec_boolean( "filtering", "Filtering", "Only show non-hidden, supported entries", TRUE, G_PARAM_READWRITE); - // TODO(p): GObject enumerations are annoying, but this should be one. - model_properties[PROP_SORT_FIELD] = g_param_spec_int( + model_properties[PROP_SORT_FIELD] = g_param_spec_enum( "sort-field", "Sort field", "Sort order", - FIV_IO_MODEL_SORT_MIN, FIV_IO_MODEL_SORT_MAX, - FIV_IO_MODEL_SORT_NAME, G_PARAM_READWRITE); + FIV_TYPE_IO_MODEL_SORT, FIV_IO_MODEL_SORT_NAME, G_PARAM_READWRITE); model_properties[PROP_SORT_DESCENDING] = g_param_spec_boolean( "sort-descending", "Sort descending", "Use reverse sort order", FALSE, G_PARAM_READWRITE); g_object_class_install_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] = 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] = 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); 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 */); + GError *e = NULL; + if ((self->monitor = g_file_monitor_directory( + 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); } diff --git a/fiv-io-model.h b/fiv-io-model.h index 2fb9ad7..c785130 100644 --- a/fiv-io-model.h +++ b/fiv-io-model.h @@ -20,15 +20,39 @@ #include #include +// Avoid glib-mkenums. typedef enum _FivIoModelSort { - FIV_IO_MODEL_SORT_NAME, - FIV_IO_MODEL_SORT_MTIME, - FIV_IO_MODEL_SORT_COUNT, - - FIV_IO_MODEL_SORT_MIN = 0, - FIV_IO_MODEL_SORT_MAX = FIV_IO_MODEL_SORT_COUNT - 1 +#define FIV_IO_MODEL_SORTS(XX) \ + XX(NAME) \ + XX(MTIME) +#define XX(name) FIV_IO_MODEL_SORT_ ## name, + FIV_IO_MODEL_SORTS(XX) +#undef XX + FIV_IO_MODEL_SORT_COUNT } 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()) 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. 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_subdirs(FivIoModel *self, gsize *len); diff --git a/fiv-sidebar.c b/fiv-sidebar.c index 6525067..caf2d86 100644 --- a/fiv-sidebar.c +++ b/fiv-sidebar.c @@ -623,9 +623,9 @@ fiv_sidebar_new(FivIoModel *model) gtk_container_set_focus_vadjustment(GTK_CONTAINER(sidebar_port), 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); - g_signal_connect_swapped(self->model, "subdirectories-changed", + g_signal_connect_swapped(self->model, "reloaded", G_CALLBACK(update_location), self); return GTK_WIDGET(self); diff --git a/fiv-thumbnail.h b/fiv-thumbnail.h index 05c3dc1..0d53c01 100644 --- a/fiv-thumbnail.h +++ b/fiv-thumbnail.h @@ -21,7 +21,7 @@ #include #include -// And this is how you avoid glib-mkenums. +// Avoid glib-mkenums. typedef enum _FivThumbnailSize { #define FIV_THUMBNAIL_SIZES(XX) \ XX(SMALL, 128, "normal") \ diff --git a/fiv.c b/fiv.c index dad88e2..3911cec 100644 --- a/fiv.c +++ b/fiv.c @@ -733,7 +733,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)) { - // 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)) { g_error_free(error); } else { @@ -797,7 +797,7 @@ go_forward(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); @@ -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); } +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 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) 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); if (old != new) g_object_set(g.model, "sort-field", new, NULL); @@ -1206,7 +1214,7 @@ static void on_notify_thumbnail_size( 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); gtk_widget_set_sensitive( g.browsebar[BROWSEBAR_PLUS], size < FIV_THUMBNAIL_SIZE_MAX); @@ -2253,6 +2261,8 @@ main(int argc, char *argv[]) fiv_collection_register(); 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_CALLBACK(on_model_files_changed), NULL);