diff options
| -rw-r--r-- | fiv-browser.c | 156 | ||||
| -rw-r--r-- | fiv-io-model.c | 396 | ||||
| -rw-r--r-- | fiv-io-model.h | 49 | ||||
| -rw-r--r-- | fiv-sidebar.c | 4 | ||||
| -rw-r--r-- | fiv-thumbnail.h | 2 | ||||
| -rw-r--r-- | 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); +	} + +	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;  	} -	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.  		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) +{ +	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)  { -	return g_ptr_array_new_with_free_func(g_rc_box_release); +	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; - -	return strlen(string) + 1; -} +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; -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); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +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[FILES_CHANGED], 0); -	g_signal_emit(self, model_signals[SUBDIRECTORIES_CHANGED], 0); +		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 <gio/gio.h>  #include <glib.h> +// 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 <gio/gio.h>  #include <glib.h> -// And this is how you avoid glib-mkenums. +// Avoid glib-mkenums.  typedef enum _FivThumbnailSize {  #define FIV_THUMBNAIL_SIZES(XX) \  	XX(SMALL,  128, "normal") \ @@ -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); @@ -811,6 +811,13 @@ on_model_files_changed(FivIoModel *model, G_GNUC_UNUSED gpointer user_data)  }  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)  {  	gboolean active = gtk_toggle_button_get_active(button); @@ -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);  | 
