diff options
| -rw-r--r-- | LICENSE | 2 | ||||
| -rw-r--r-- | fastiv.c | 156 | ||||
| -rw-r--r-- | fiv-browser.c | 102 | ||||
| -rw-r--r-- | fiv-browser.h | 9 | ||||
| -rw-r--r-- | fiv-io.c | 324 | ||||
| -rw-r--r-- | fiv-io.h | 17 | ||||
| -rw-r--r-- | fiv-sidebar.c | 92 | ||||
| -rw-r--r-- | fiv-sidebar.h | 6 | 
8 files changed, 467 insertions, 241 deletions
@@ -1,4 +1,4 @@ -Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name> +Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>  Permission to use, copy, modify, and/or distribute this software for any  purpose with or without fee is hereby granted. @@ -1,7 +1,7 @@  //  // fastiv.c: fast image viewer  // -// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name> +// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>  //  // Permission to use, copy, modify, and/or distribute this software for any  // purpose with or without fee is hereby granted. @@ -25,15 +25,12 @@  #include <stdio.h>  #include <stdlib.h> -#include <fnmatch.h> -  #include "config.h"  #include "fiv-browser.h"  #include "fiv-io.h"  #include "fiv-sidebar.h"  #include "fiv-thumbnail.h"  #include "fiv-view.h" -#include "xdg.h"  // --- Utilities --------------------------------------------------------------- @@ -260,14 +257,13 @@ enum {  };  struct { -	gchar **supported_globs; -	gboolean filtering; - -	gchar *uri;                ///< Current image URI, if any +	FivIoModel *model;         ///< "directory" contents  	gchar *directory;          ///< URI of the currently browsed directory -	GList *directory_back;     ///< History paths going backwards -	GList *directory_forward;  ///< History paths going forwards +	GList *directory_back;     ///< History paths as URIs going backwards +	GList *directory_forward;  ///< History paths as URIs going forwards  	GPtrArray *files;          ///< "directory" contents as URIs + +	gchar *uri;                ///< Current image URI, if any  	gint files_index;          ///< Where "uri" is within "files"  	GtkWidget *window; @@ -286,27 +282,6 @@ struct {  	GtkWidget *view;  } g; -static gboolean -is_supported(const gchar *filename) -{ -	gchar *utf8 = g_filename_to_utf8(filename, -1, NULL, NULL, NULL); -	if (!utf8) -		return FALSE; - -	gchar *lowercased = g_utf8_strdown(utf8, -1); -	g_free(utf8); - -	// XXX: fnmatch() uses the /locale/ encoding, but who cares nowadays. -	for (gchar **p = g.supported_globs; *p; p++) -		if (!fnmatch(*p, lowercased, 0)) { -			g_free(lowercased); -			return TRUE; -		} - -	g_free(lowercased); -	return FALSE; -} -  static void  show_error_dialog(GError *error)  { @@ -346,17 +321,6 @@ switch_to_view(void)  	gtk_widget_grab_focus(g.view);  } -static gint -files_compare(gconstpointer a, gconstpointer b) -{ -	GFile *file1 = g_file_new_for_uri(*(gchar **) a); -	GFile *file2 = g_file_new_for_uri(*(gchar **) b); -	gint result = fiv_io_filecmp(file1, file2); -	g_object_unref(file1); -	g_object_unref(file2); -	return result; -} -  static gchar *  parent_uri(GFile *child_file)  { @@ -415,7 +379,7 @@ load_directory_without_reload(const gchar *uri)  }  static void -load_directory(const gchar *uri) +load_directory_without_switching(const gchar *uri)  {  	if (uri) {  		load_directory_without_reload(uri); @@ -429,42 +393,30 @@ load_directory(const gchar *uri)  	g_ptr_array_set_size(g.files, 0);  	g.files_index = -1; -	GFile *file = g_file_new_for_uri(g.directory); -	fiv_sidebar_set_location(FIV_SIDEBAR(g.browser_sidebar), file); -	fiv_browser_load( -		FIV_BROWSER(g.browser), g.filtering ? is_supported : NULL, g.directory); -  	GError *error = NULL; -	GFileEnumerator *enumerator = g_file_enumerate_children(file, -		G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE, -		G_FILE_QUERY_INFO_NONE, NULL, &error); -	if (enumerator) { -		GFileInfo *info = NULL; -		GFile *child = NULL; -		while ( -			g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) && -			info) { -			// TODO(p): What encoding does g_file_info_get_name() return? -			if (g_file_info_get_file_type(info) != G_FILE_TYPE_DIRECTORY && -				is_supported(g_file_info_get_name(info))) -				g_ptr_array_add(g.files, g_file_get_uri(child)); -		} -		g_object_unref(enumerator); - -		g_ptr_array_sort(g.files, files_compare); +	GFile *file = g_file_new_for_uri(g.directory); +	if (fiv_io_model_open(g.model, file, &error)) { +		g_ptr_array_free(g.files, TRUE); +		g.files = fiv_io_model_get_files(g.model);  		update_files_index();  	} else if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) {  		g_error_free(error);  	} else {  		show_error_dialog(error);  	} +  	g_object_unref(file);  	gtk_widget_set_sensitive(  		g.toolbar[TOOLBAR_FILE_PREVIOUS], g.files->len > 1);  	gtk_widget_set_sensitive(  		g.toolbar[TOOLBAR_FILE_NEXT], g.files->len > 1); -	g_ptr_array_add(g.files, NULL); +} + +static void +load_directory(const gchar *uri) +{ +	load_directory_without_switching(uri);  	// XXX: When something outside the filtered entries is open, the index is  	// kept at -1, and browsing doesn't work. How to behave here? @@ -480,9 +432,8 @@ load_directory(const gchar *uri)  static void  on_filtering_toggled(GtkToggleButton *button, G_GNUC_UNUSED gpointer user_data)  { -	g.filtering = gtk_toggle_button_get_active(button); -	if (g.directory) -		load_directory(NULL); +	g_object_set( +		g.model, "filtering", gtk_toggle_button_get_active(button), NULL);  }  static void @@ -514,7 +465,7 @@ open(const gchar *uri)  	gchar *parent = parent_uri(file);  	if (!g.files->len /* hack to always load the directory after launch */ ||  		!g.directory || strcmp(parent, g.directory)) -		load_directory(parent); +		load_directory_without_switching(parent);  	else  		update_files_index();  	g_free(parent); @@ -579,8 +530,7 @@ static void  on_previous(void)  {  	if (g.files_index >= 0) { -		int previous = -			(g.files->len - 1 + g.files_index - 1) % (g.files->len - 1); +		int previous = (g.files->len + g.files_index - 1) % g.files->len;  		open(g_ptr_array_index(g.files, previous));  	}  } @@ -589,7 +539,7 @@ static void  on_next(void)  {  	if (g.files_index >= 0) { -		int next = (g.files_index + 1) % (g.files->len - 1); +		int next = (g.files_index + 1) % g.files->len;  		open(g_ptr_array_index(g.files, next));  	}  } @@ -616,15 +566,15 @@ on_item_activated(G_GNUC_UNUSED FivBrowser *browser, GFile *location,  	g_free(uri);  } -static gboolean -open_any_uri(const char *uri, gboolean force_browser) +static void +open_any_file(GFile *file, gboolean force_browser)  { -	GFile *file = g_file_new_for_uri(uri); +	gchar *uri = g_file_get_uri(file);  	GFileType type = g_file_query_file_type(file, G_FILE_QUERY_INFO_NONE, NULL); -	gboolean success = type != G_FILE_TYPE_UNKNOWN; -	if (!success) { +	if (type == G_FILE_TYPE_UNKNOWN) { +		errno = ENOENT;  		show_error_dialog(g_error_new(G_FILE_ERROR, -			g_file_error_from_errno(errno), "%s: %s", uri, g_strerror(ENOENT))); +			g_file_error_from_errno(errno), "%s: %s", uri, g_strerror(errno)));  	} else if (type == G_FILE_TYPE_DIRECTORY) {  		load_directory(uri);  	} else if (force_browser) { @@ -636,8 +586,7 @@ open_any_uri(const char *uri, gboolean force_browser)  	} else {  		open(uri);  	} -	g_object_unref(file); -	return success; +	g_free(uri);  }  static void @@ -648,7 +597,7 @@ on_open_location(G_GNUC_UNUSED GtkPlacesSidebar *sidebar, GFile *location,  	if (flags & GTK_PLACES_OPEN_NEW_WINDOW)  		spawn_uri(uri);  	else -		open_any_uri(uri, FALSE); +		open_any_file(location, FALSE);  	g_free(uri);  } @@ -676,6 +625,15 @@ on_notify_thumbnail_size(  }  static void +on_notify_filtering( +	GObject *object, GParamSpec *param_spec, gpointer user_data) +{ +	gboolean b = FALSE; +	g_object_get(object, g_param_spec_get_name(param_spec), &b, NULL); +	gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(user_data), b); +} + +static void  toggle_fullscreen(void)  {  	if (gdk_window_get_state(gtk_widget_get_window(g.window)) & @@ -800,7 +758,11 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,  			}  			return TRUE;  		case GDK_KEY_Home: -			load_directory(g_get_home_dir()); +			if (gtk_stack_get_visible_child(GTK_STACK(g.stack)) != g.view_box) { +				gchar *uri = g_filename_to_uri(g_get_home_dir(), NULL, NULL); +				load_directory(uri); +				g_free(uri); +			}  			return TRUE;  		}  		break; @@ -1265,6 +1227,8 @@ main(int argc, char *argv[])  		return 0;  	} +	g.model = g_object_new(FIV_TYPE_IO_MODEL, NULL); +  	gtk_window_set_default_icon_name(PROJECT_NAME);  	gtk_icon_theme_add_resource_path(  		gtk_icon_theme_get_default(), "/org/gnome/design/IconLibrary/"); @@ -1293,7 +1257,7 @@ main(int argc, char *argv[])  	gtk_box_pack_start(GTK_BOX(g.view_box), view_scroller, TRUE, TRUE, 0);  	g.browser_scroller = gtk_scrolled_window_new(NULL, NULL); -	g.browser = g_object_new(FIV_TYPE_BROWSER, NULL); +	g.browser = fiv_browser_new(g.model);  	gtk_widget_set_vexpand(g.browser, TRUE);  	gtk_widget_set_hexpand(g.browser, TRUE);  	g_signal_connect(g.browser, "item-activated", @@ -1307,7 +1271,7 @@ main(int argc, char *argv[])  	// TODO(p): As with GtkFileChooserWidget, bind C-h to filtering,  	// and mayhaps forward the rest to the sidebar, somehow. -	g.browser_sidebar = g_object_new(FIV_TYPE_SIDEBAR, NULL); +	g.browser_sidebar = fiv_sidebar_new(g.model);  	g_signal_connect(g.browser_sidebar, "open-location",  		G_CALLBACK(on_open_location), NULL); @@ -1375,13 +1339,11 @@ main(int argc, char *argv[])  		G_CALLBACK(on_window_state_event), NULL);  	gtk_container_add(GTK_CONTAINER(g.window), g.stack); -	char **types = fiv_io_all_supported_media_types(); -	g.supported_globs = extract_mime_globs((const char **) types); -	g_strfreev(types); -  	g_signal_connect(g.browser, "notify::thumbnail-size",  		G_CALLBACK(on_notify_thumbnail_size), NULL);  	on_toolbar_zoom(NULL, (gpointer) 0); +	g_signal_connect(g.model, "notify::filtering", +		G_CALLBACK(on_notify_filtering), funnel);  	gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(funnel), TRUE);  	// Try to get half of the screen vertically, in 4:3 aspect ratio. @@ -1401,22 +1363,20 @@ main(int argc, char *argv[])  	unit = MAX(200, unit);  	gtk_window_set_default_size(GTK_WINDOW(g.window), 4 * unit, 3 * unit); -	g.files = g_ptr_array_new_full(16, g_free); -	gchar *cwd = g_get_current_dir(); -	g.directory = g_filename_to_uri(cwd, NULL, NULL /* error */); -	g_free(cwd); -  	// XXX: The widget wants to read the display's profile. The realize is ugly.  	gtk_widget_realize(g.view); -	gchar *uri = NULL; +	g.files = g_ptr_array_new_full(0, g_free);  	if (path_arg) {  		GFile *file = g_file_new_for_commandline_arg(path_arg); -		uri = g_file_get_uri(file); +		open_any_file(file, browse); +		g_object_unref(file); +	} +	if (!g.directory) { +		GFile *file = g_file_new_for_path("."); +		open_any_file(file, FALSE);  		g_object_unref(file);  	} -	if (!uri || !open_any_uri(uri, browse)) -		open_any_uri(g.directory, FALSE);  	gtk_widget_show_all(g.window);  	gtk_main(); diff --git a/fiv-browser.c b/fiv-browser.c index b0ec19e..57e9398 100644 --- a/fiv-browser.c +++ b/fiv-browser.c @@ -1,7 +1,7 @@  //  // fiv-browser.c: fast image viewer - filesystem browser widget  // -// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name> +// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>  //  // Permission to use, copy, modify, and/or distribute this software for any  // purpose with or without fee is hereby granted. @@ -48,7 +48,7 @@ struct _FivBrowser {  	int item_height;                    ///< Thumbnail height in pixels  	int item_spacing;                   ///< Space between items in pixels -	char *uri;                          ///< Current URI +	FivIoModel *model;                  ///< Filesystem model  	GArray *entries;                    ///< []Entry  	GArray *layouted_rows;              ///< []Row  	int selected; @@ -541,9 +541,9 @@ thumbnailer_start(FivBrowser *self)  	gchar *thumbnails_dir = fiv_thumbnail_get_root();  	GFile *thumbnails = g_file_new_for_path(thumbnails_dir);  	g_free(thumbnails_dir); -	GFile *current = g_file_new_for_uri(self->uri); -	gboolean is_a_thumbnail = g_file_has_prefix(current, thumbnails); -	g_object_unref(current); + +	GFile *current = fiv_io_model_get_location(self->model); +	gboolean is_a_thumbnail = current && g_file_has_prefix(current, thumbnails);  	g_object_unref(thumbnails);  	if (is_a_thumbnail)  		return; @@ -651,22 +651,19 @@ destroy_widget_idle_source_func(GtkWidget *widget)  }  static void -show_context_menu(GtkWidget *widget, const char *uri) +show_context_menu(GtkWidget *widget, GFile *file)  { -	GFile *file = g_file_new_for_uri(uri);  	GFileInfo *info = g_file_query_info(file,  		G_FILE_ATTRIBUTE_STANDARD_NAME  		"," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,  		G_FILE_QUERY_INFO_NONE, NULL, NULL); -	if (!info) { -		g_object_unref(file); +	if (!info)  		return; -	}  	// This will have no application pre-assigned, for use with GTK+'s dialog.  	OpenContext *ctx = g_malloc0(sizeof *ctx);  	g_weak_ref_init(&ctx->widget, widget); -	ctx->file = file; +	ctx->file = g_object_ref(file);  	ctx->content_type = g_strdup(g_file_info_get_content_type(info));  	g_object_unref(info); @@ -750,9 +747,13 @@ fiv_browser_finalize(GObject *gobject)  {  	FivBrowser *self = FIV_BROWSER(gobject);  	thumbnailer_abort(self); -	g_free(self->uri);  	g_array_free(self->entries, TRUE);  	g_array_free(self->layouted_rows, TRUE); +	if (self->model) { +		g_signal_handlers_disconnect_by_data(self->model, self); +		g_clear_object(&self->model); +	} +  	cairo_surface_destroy(self->glow);  	g_clear_object(&self->pointer); @@ -931,7 +932,7 @@ fiv_browser_button_press_event(GtkWidget *widget, GdkEventButton *event)  	const Entry *entry = entry_at(self, event->x, event->y);  	if (!entry && event->button == GDK_BUTTON_SECONDARY) { -		show_context_menu(widget, self->uri); +		show_context_menu(widget, fiv_io_model_get_location(self->model));  		return TRUE;  	}  	if (!entry) @@ -952,7 +953,10 @@ fiv_browser_button_press_event(GtkWidget *widget, GdkEventButton *event)  		// On X11, after closing the menu, the pointer otherwise remains,  		// no matter what its new location is.  		gdk_window_set_cursor(gtk_widget_get_window(widget), NULL); -		show_context_menu(widget, entry->uri); + +		GFile *file = g_file_new_for_uri(entry->uri); +		show_context_menu(widget, file); +		g_object_unref(file);  		return TRUE;  	default:  		return FALSE; @@ -1149,62 +1153,38 @@ fiv_browser_init(FivBrowser *self)  // --- Public interface -------------------------------------------------------- -static gint -entry_compare(gconstpointer a, gconstpointer b) -{ -	const Entry *entry1 = a; -	const Entry *entry2 = b; -	GFile *location1 = g_file_new_for_uri(entry1->uri); -	GFile *location2 = g_file_new_for_uri(entry2->uri); -	gint result = fiv_io_filecmp(location1, location2); -	g_object_unref(location1); -	g_object_unref(location2); -	return result; -} - -void -fiv_browser_load( -	FivBrowser *self, FivBrowserFilterCallback cb, const char *uri) +static void +on_model_files_changed(FivIoModel *model, FivBrowser *self)  { -	g_return_if_fail(FIV_IS_BROWSER(self)); +	g_return_if_fail(model == self->model); +	// TODO(p): Later implement arguments.  	thumbnailer_abort(self);  	g_array_set_size(self->entries, 0);  	g_array_set_size(self->layouted_rows, 0); -	g_clear_pointer(&self->uri, g_free); - -	GFile *file = g_file_new_for_uri((self->uri = g_strdup(uri))); - -	GError *error = NULL; -	GFileEnumerator *enumerator = g_file_enumerate_children(file, -		G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE, -		G_FILE_QUERY_INFO_NONE, NULL, &error); -	g_object_unref(file); -	if (!enumerator) { -		// Note that this has had a side-effect of clearing all entries. -		g_error_free(error); -		return; -	} - -	while (TRUE) { -		GFileInfo *info = NULL; -		GFile *child = NULL; -		if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) || -			!info) -			break; -		if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY) -			continue; -		if (cb && !cb(g_file_info_get_name(info))) -			continue; +	GPtrArray *files = fiv_io_model_get_files(self->model); +	for (guint i = 0; i < files->len; i++) {  		g_array_append_val(self->entries, -			((Entry) {.thumbnail = NULL, .uri = g_file_get_uri(child)})); +			((Entry) {.thumbnail = NULL, .uri = files->pdata[i]})); +		files->pdata[i] = NULL;  	} -	g_object_unref(enumerator); - -	// TODO(p): Support being passed a sort function. -	g_array_sort(self->entries, entry_compare); +	g_ptr_array_free(files, TRUE);  	reload_thumbnails(self);  	thumbnailer_start(self);  } + +GtkWidget * +fiv_browser_new(FivIoModel *model) +{ +	g_return_val_if_fail(FIV_IS_IO_MODEL(model), NULL); + +	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); +	return GTK_WIDGET(self); +} diff --git a/fiv-browser.h b/fiv-browser.h index 613728c..2987916 100644 --- a/fiv-browser.h +++ b/fiv-browser.h @@ -1,7 +1,7 @@  //  // fiv-browser.h: fast image viewer - filesystem browser widget  // -// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name> +// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>  //  // Permission to use, copy, modify, and/or distribute this software for any  // purpose with or without fee is hereby granted. @@ -17,12 +17,11 @@  #pragma once +#include "fiv-io.h" +  #include <gtk/gtk.h>  #define FIV_TYPE_BROWSER (fiv_browser_get_type())  G_DECLARE_FINAL_TYPE(FivBrowser, fiv_browser, FIV, BROWSER, GtkWidget) -typedef gboolean (*FivBrowserFilterCallback) (const char *); - -void fiv_browser_load( -	FivBrowser *self, FivBrowserFilterCallback cb, const char *path); +GtkWidget *fiv_browser_new(FivIoModel *model); @@ -1,7 +1,7 @@  //  // fiv-io.c: image operations  // -// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name> +// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>  //  // Permission to use, copy, modify, and/or distribute this software for any  // purpose with or without fee is hereby granted. @@ -132,22 +132,6 @@ fiv_io_all_supported_media_types(void)  	return (char **) g_ptr_array_free(types, FALSE);  } -int -fiv_io_filecmp(GFile *location1, GFile *location2) -{ -	if (g_file_has_prefix(location1, location2)) -		return +1; -	if (g_file_has_prefix(location2, location1)) -		return -1; - -	gchar *name1 = g_file_get_parse_name(location1); -	gchar *name2 = g_file_get_parse_name(location2); -	int result = g_utf8_collate(name1, name2); -	g_free(name1); -	g_free(name2); -	return result; -} -  // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  #define FIV_IO_ERROR fiv_io_error_quark() @@ -2463,6 +2447,312 @@ fiv_io_open_from_data(const char *data, size_t len, const gchar *uri,  	return surface;  } +// --- Filesystem -------------------------------------------------------------- + +#include "xdg.h" + +#include <fnmatch.h> + +typedef struct _ModelEntry { +	gchar *uri;                         ///< GIO URI +	gint64 mtime_msec;                  ///< Modification time in milliseconds +} ModelEntry; + +static void +model_entry_finalize(ModelEntry *entry) +{ +	g_free(entry->uri); +} + +typedef enum _FivIoModelSort { +	FIV_IO_MODEL_SORT_NAME, +	FIV_IO_MODEL_SORT_MTIME, +} FivIoModelSort; + +struct _FivIoModel { +	GObject parent_instance; +	gchar **supported_globs; + +	GFile *directory;                   ///< Currently loaded directory +	GFileMonitor *monitor;              ///< "directory" monitoring +	GArray *subdirs;                    ///< "directory" contents +	GArray *files;                      ///< "directory" contents + +	FivIoModelSort sort;                ///< How to sort +	gboolean sort_descending;           ///< Whether to sort in reverse +	gboolean filtering;                 ///< Only show non-hidden, supported +}; + +G_DEFINE_TYPE(FivIoModel, fiv_io_model, G_TYPE_OBJECT) + +enum { +	PROP_FILTERING = 1, +	N_PROPERTIES +}; + +static GParamSpec *model_properties[N_PROPERTIES]; + +enum { +	FILES_CHANGED, +	SUBDIRECTORIES_CHANGED, +	LAST_SIGNAL, +}; + +// Globals are, sadly, the canonical way of storing signal numbers. +static guint model_signals[LAST_SIGNAL]; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static gboolean +model_supports(FivIoModel *self, const gchar *filename) +{ +	gchar *utf8 = g_filename_to_utf8(filename, -1, NULL, NULL, NULL); +	if (!utf8) +		return FALSE; + +	gchar *lowercased = g_utf8_strdown(utf8, -1); +	g_free(utf8); + +	// XXX: fnmatch() uses the /locale/ encoding, but who cares nowadays. +	// TODO(p): Use GPatternSpec and g_file_info_get_display_name(). +	for (gchar **p = self->supported_globs; *p; p++) +		if (!fnmatch(*p, lowercased, 0)) { +			g_free(lowercased); +			return TRUE; +		} + +	g_free(lowercased); +	return FALSE; +} + +static inline int +model_compare_name(GFile *location1, GFile *location2) +{ +	gchar *name1 = g_file_get_parse_name(location1); +	gchar *name2 = g_file_get_parse_name(location2); +	int result = g_utf8_collate(name1, name2); +	g_free(name1); +	g_free(name2); +	return result; +} + +static inline int +model_compare_entries(FivIoModel *self, const ModelEntry *entry1, GFile *file1, +	const ModelEntry *entry2, GFile *file2) +{ +	if (g_file_has_prefix(file1, file2)) +		return +1; +	if (g_file_has_prefix(file2, file1)) +		return -1; + +	int result = 0; +	switch (self->sort) { +	case FIV_IO_MODEL_SORT_NAME: +		result = model_compare_name(file1, file2); +		break; +	case FIV_IO_MODEL_SORT_MTIME: +		result -= entry1->mtime_msec < entry2->mtime_msec; +		result += entry1->mtime_msec > entry2->mtime_msec; +	} +	return self->sort_descending ? -result : +result; +} + +static gint +model_compare(gconstpointer a, gconstpointer b, gpointer user_data) +{ +	const ModelEntry *entry1 = a; +	const ModelEntry *entry2 = b; +	GFile *file1 = g_file_new_for_uri(entry1->uri); +	GFile *file2 = g_file_new_for_uri(entry2->uri); +	int result = model_compare_entries(user_data, entry1, file1, entry2, file2); +	g_object_unref(file1); +	g_object_unref(file2); +	return result; +} + +static gboolean +model_reload(FivIoModel *self, GError **error) +{ +	g_array_set_size(self->subdirs, 0); +	g_array_set_size(self->files, 0); + +	GFileEnumerator *enumerator = g_file_enumerate_children(self->directory, +		G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE "," +		G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN "," +		G_FILE_ATTRIBUTE_TIME_MODIFIED "," G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC, +		G_FILE_QUERY_INFO_NONE, NULL, error); +	if (!enumerator) { +		// Note that this has had a side-effect of clearing all entries. +		g_signal_emit(self, model_signals[FILES_CHANGED], 0); +		g_signal_emit(self, model_signals[SUBDIRECTORIES_CHANGED], 0); +		return FALSE; +	} + +	GFileInfo *info = NULL; +	GFile *child = NULL; +	while (g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) && +		info) { +		if (self->filtering && g_file_info_get_is_hidden(info)) +			continue; + +		ModelEntry entry = {}; +		GDateTime *mtime = g_file_info_get_modification_date_time(info); +		if (mtime) { +			entry.mtime_msec = g_date_time_to_unix(mtime) * 1000 + +				g_date_time_get_microsecond(mtime) / 1000; +			g_date_time_unref(mtime); +		} + +		const char *name = g_file_info_get_name(info); +		if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY) { +			entry.uri = g_file_get_uri(child); +			g_array_append_val(self->subdirs, entry); +		} else if (!self->filtering || model_supports(self, name)) { +			entry.uri = g_file_get_uri(child); +			g_array_append_val(self->files, entry); +		} +	} +	g_object_unref(enumerator); +	g_array_sort_with_data(self->subdirs, model_compare, self); +	g_array_sort_with_data(self->files, model_compare, self); + +	g_signal_emit(self, model_signals[FILES_CHANGED], 0); +	g_signal_emit(self, model_signals[SUBDIRECTORIES_CHANGED], 0); +	return TRUE; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +fiv_io_model_finalize(GObject *gobject) +{ +	FivIoModel *self = FIV_IO_MODEL(gobject); +	g_clear_object(&self->directory); +	g_clear_object(&self->monitor); +	g_array_free(self->subdirs, TRUE); +	g_array_free(self->files, TRUE); + +	G_OBJECT_CLASS(fiv_io_model_parent_class)->finalize(gobject); +} + +static void +fiv_io_model_get_property( +	GObject *object, guint property_id, GValue *value, GParamSpec *pspec) +{ +	FivIoModel *self = FIV_IO_MODEL(object); +	switch (property_id) { +	case PROP_FILTERING: +		g_value_set_boolean(value, self->filtering); +		break; +	default: +		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); +	} +} + +static void +fiv_io_model_set_property( +	GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) +{ +	FivIoModel *self = FIV_IO_MODEL(object); +	switch (property_id) { +	case PROP_FILTERING: +		if (self->filtering == g_value_get_boolean(value)) +			return; + +		self->filtering = !self->filtering; +		g_object_notify_by_pspec(object, model_properties[PROP_FILTERING]); +		(void) model_reload(self, NULL /* error */); +		break; +	default: +		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); +	} +} + +static void +fiv_io_model_class_init(FivIoModelClass *klass) +{ +	GObjectClass *object_class = G_OBJECT_CLASS(klass); +	object_class->get_property = fiv_io_model_get_property; +	object_class->set_property = fiv_io_model_set_property; +	object_class->finalize = fiv_io_model_finalize; + +	model_properties[PROP_FILTERING] = g_param_spec_boolean( +		"filtering", "Filtering", "Only show non-hidden, supported entries", +		TRUE, G_PARAM_READWRITE); +	g_object_class_install_properties( +		object_class, N_PROPERTIES, model_properties); + +	// TODO(p): Arguments something like: index, added, removed. +	model_signals[FILES_CHANGED] = +		g_signal_new("files-changed", G_TYPE_FROM_CLASS(klass), 0, 0, +			NULL, NULL, NULL, G_TYPE_NONE, 0); +	model_signals[SUBDIRECTORIES_CHANGED] = +		g_signal_new("subdirectories-changed", G_TYPE_FROM_CLASS(klass), 0, 0, +			NULL, NULL, NULL, G_TYPE_NONE, 0); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +fiv_io_model_init(FivIoModel *self) +{ +	self->filtering = TRUE; + +	char **types = fiv_io_all_supported_media_types(); +	self->supported_globs = extract_mime_globs((const char **) types); +	g_strfreev(types); + +	self->files = g_array_new(FALSE, TRUE, sizeof(ModelEntry)); +	self->subdirs = g_array_new(FALSE, TRUE, sizeof(ModelEntry)); +	g_array_set_clear_func( +		self->subdirs, (GDestroyNotify) model_entry_finalize); +	g_array_set_clear_func( +		self->files, (GDestroyNotify) model_entry_finalize); +} + +gboolean +fiv_io_model_open(FivIoModel *self, GFile *directory, GError **error) +{ +	g_return_val_if_fail(FIV_IS_IO_MODEL(self), FALSE); +	g_return_val_if_fail(G_IS_FILE(directory), FALSE); + +	g_clear_object(&self->directory); +	g_clear_object(&self->monitor); +	self->directory = g_object_ref(directory); + +	// TODO(p): Process the ::changed signal. +	self->monitor = g_file_monitor_directory( +		directory, G_FILE_MONITOR_WATCH_MOVES, NULL, NULL /* error */); +	return model_reload(self, error); +} + +GFile * +fiv_io_model_get_location(FivIoModel *self) +{ +	g_return_val_if_fail(FIV_IS_IO_MODEL(self), NULL); +	return self->directory; +} + +GPtrArray * +fiv_io_model_get_files(FivIoModel *self) +{ +	GPtrArray *a = g_ptr_array_new_full(self->files->len, g_free); +	for (guint i = 0; i < self->files->len; i++) +		g_ptr_array_add( +			a, g_strdup(g_array_index(self->files, ModelEntry, i).uri)); +	return a; +} + +GPtrArray * +fiv_io_model_get_subdirectories(FivIoModel *self) +{ +	GPtrArray *a = g_ptr_array_new_full(self->subdirs->len, g_free); +	for (guint i = 0; i < self->subdirs->len; i++) +		g_ptr_array_add( +			a, g_strdup(g_array_index(self->subdirs, ModelEntry, i).uri)); +	return a; +} +  // --- Export ------------------------------------------------------------------  unsigned char * @@ -1,7 +1,7 @@  //  // fiv-io.h: image operations  // -// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name> +// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>  //  // Permission to use, copy, modify, and/or distribute this software for any  // purpose with or without fee is hereby granted. @@ -77,7 +77,20 @@ cairo_surface_t *fiv_io_open(  cairo_surface_t *fiv_io_open_from_data(const char *data, size_t len,  	const gchar *uri, FivIoProfile profile, gboolean enhance, GError **error); -int fiv_io_filecmp(GFile *f1, GFile *f2); +// --- Filesystem -------------------------------------------------------------- + +#define FIV_TYPE_IO_MODEL (fiv_io_model_get_type()) +G_DECLARE_FINAL_TYPE(FivIoModel, fiv_io_model, FIV, IO_MODEL, GObject) + +/// Loads a directory. Clears itself even on failure. +gboolean fiv_io_model_open(FivIoModel *self, GFile *directory, GError **error); + +/// Returns the current location as a GFile. +/// There is no ownership transfer, and the object may be NULL. +GFile *fiv_io_model_get_location(FivIoModel *self); + +GPtrArray *fiv_io_model_get_files(FivIoModel *self); +GPtrArray *fiv_io_model_get_subdirectories(FivIoModel *self);  // --- Export ------------------------------------------------------------------ diff --git a/fiv-sidebar.c b/fiv-sidebar.c index 4c2daea..bddc526 100644 --- a/fiv-sidebar.c +++ b/fiv-sidebar.c @@ -1,7 +1,7 @@  //  // fiv-sidebar.c: molesting GtkPlacesSidebar  // -// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name> +// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>  //  // Permission to use, copy, modify, and/or distribute this software for any  // purpose with or without fee is hereby granted. @@ -17,7 +17,7 @@  #include <gtk/gtk.h> -#include "fiv-io.h"  // fiv_io_filecmp +#include "fiv-io.h"  #include "fiv-sidebar.h"  struct _FivSidebar { @@ -25,7 +25,7 @@ struct _FivSidebar {  	GtkPlacesSidebar *places;  	GtkWidget *toolbar;  	GtkWidget *listbox; -	GFile *location; +	FivIoModel *model;  };  G_DEFINE_TYPE(FivSidebar, fiv_sidebar, GTK_TYPE_SCROLLED_WINDOW) @@ -44,7 +44,10 @@ static void  fiv_sidebar_dispose(GObject *gobject)  {  	FivSidebar *self = FIV_SIDEBAR(gobject); -	g_clear_object(&self->location); +	if (self->model) { +		g_signal_handlers_disconnect_by_data(self->model, self); +		g_clear_object(&self->model); +	}  	G_OBJECT_CLASS(fiv_sidebar_parent_class)->dispose(gobject);  } @@ -128,29 +131,18 @@ create_row(GFile *file, const char *icon_name)  	return row;  } -static gint -listbox_compare( -	GtkListBoxRow *row1, GtkListBoxRow *row2, G_GNUC_UNUSED gpointer user_data) -{ -	return fiv_io_filecmp( -		g_object_get_qdata(G_OBJECT(row1), fiv_sidebar_location_quark()), -		g_object_get_qdata(G_OBJECT(row2), fiv_sidebar_location_quark())); -} -  static void -update_location(FivSidebar *self, GFile *location) +update_location(FivSidebar *self)  { -	if (location) { -		g_clear_object(&self->location); -		self->location = g_object_ref(location); -	} +	GFile *location = fiv_io_model_get_location(self->model); +	if (!location) +		return; -	gtk_places_sidebar_set_location(self->places, self->location); +	gtk_places_sidebar_set_location(self->places, location);  	gtk_container_foreach(GTK_CONTAINER(self->listbox),  		(GtkCallback) gtk_widget_destroy, NULL); -	g_return_if_fail(self->location != NULL); -	GFile *iter = g_object_ref(self->location); +	GFile *iter = g_object_ref(location);  	GtkWidget *row = NULL;  	while (TRUE) {  		GFile *parent = g_file_get_parent(iter); @@ -164,33 +156,17 @@ update_location(FivSidebar *self, GFile *location)  	// Other options are "folder-{visiting,open}-symbolic", though the former  	// is mildly inappropriate (means: open in another window). -	if ((row = create_row(self->location, "circle-filled-symbolic"))) +	if ((row = create_row(location, "circle-filled-symbolic")))  		gtk_container_add(GTK_CONTAINER(self->listbox), row); -	GFileEnumerator *enumerator = g_file_enumerate_children(self->location, -		G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME -		"," G_FILE_ATTRIBUTE_STANDARD_NAME -		"," G_FILE_ATTRIBUTE_STANDARD_TYPE -		"," G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN, -		G_FILE_QUERY_INFO_NONE, NULL, NULL); -	if (!enumerator) -		return; - -	// TODO(p): gtk_list_box_set_filter_func(), or even use a model, -	// which could be shared with FivBrowser. -	while (TRUE) { -		GFileInfo *info = NULL; -		GFile *child = NULL; -		if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) || -			!info) -			break; - -		if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY && -			!g_file_info_get_is_hidden(info) && -			(row = create_row(child, "go-down-symbolic"))) -				gtk_container_add(GTK_CONTAINER(self->listbox), row); +	GPtrArray *subdirs = fiv_io_model_get_subdirectories(self->model); +	for (guint i = 0; i < subdirs->len; i++) { +		GFile *file = g_file_new_for_uri(subdirs->pdata[i]); +		if ((row = create_row(file, "go-down-symbolic"))) +			gtk_container_add(GTK_CONTAINER(self->listbox), row); +		g_object_unref(file);  	} -	g_object_unref(enumerator); +	g_ptr_array_free(subdirs, TRUE);  }  static void @@ -212,7 +188,7 @@ on_open_location(G_GNUC_UNUSED GtkPlacesSidebar *sidebar, GFile *location,  	g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0, location, flags);  	// Deselect the item in GtkPlacesSidebar, if unsuccessful. -	update_location(self, NULL); +	update_location(self);  }  static void @@ -272,8 +248,8 @@ resolve_location(FivSidebar *self, const char *text)  		g_file_peek_path(file))  		return file; -	GFile *absolute = -		g_file_get_child_for_display_name(self->location, text, NULL); +	GFile *absolute = g_file_get_child_for_display_name( +		fiv_io_model_get_location(self->model), text, NULL);  	if (!absolute)  		return file; @@ -355,7 +331,7 @@ on_show_enter_location(  	g_object_unref(completion);  	// Deselect the item in GtkPlacesSidebar, if unsuccessful. -	update_location(self, NULL); +	update_location(self);  }  static void @@ -389,8 +365,6 @@ fiv_sidebar_init(FivSidebar *self)  		GTK_LIST_BOX(self->listbox), GTK_SELECTION_NONE);  	g_signal_connect(self->listbox, "row-activated",  		G_CALLBACK(on_open_breadcrumb), self); -	gtk_list_box_set_sort_func( -		GTK_LIST_BOX(self->listbox), listbox_compare, self, NULL);  	// Fill up what would otherwise be wasted space,  	// as it is in the examples of Nautilus and Thunar. @@ -417,11 +391,19 @@ fiv_sidebar_init(FivSidebar *self)  // --- Public interface -------------------------------------------------------- -void -fiv_sidebar_set_location(FivSidebar *self, GFile *location) +GtkWidget * +fiv_sidebar_new(FivIoModel *model)  { -	g_return_if_fail(FIV_IS_SIDEBAR(self)); -	update_location(self, location); +	g_return_val_if_fail(FIV_IS_IO_MODEL(model), NULL); + +	FivSidebar *self = g_object_new(FIV_TYPE_SIDEBAR, NULL); +	self->model = g_object_ref(model); + +	// TODO(p): There should be an extra signal to watch location changes only. +	g_signal_connect_swapped(self->model, "subdirectories-changed", +		G_CALLBACK(update_location), self); + +	return GTK_WIDGET(self);  }  void diff --git a/fiv-sidebar.h b/fiv-sidebar.h index 8a3f14a..2d0888a 100644 --- a/fiv-sidebar.h +++ b/fiv-sidebar.h @@ -1,7 +1,7 @@  //  // fiv-sidebar.h: molesting GtkPlacesSidebar  // -// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name> +// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>  //  // Permission to use, copy, modify, and/or distribute this software for any  // purpose with or without fee is hereby granted. @@ -17,11 +17,13 @@  #pragma once +#include "fiv-io.h" +  #include <gtk/gtk.h>  #define FIV_TYPE_SIDEBAR (fiv_sidebar_get_type())  G_DECLARE_FINAL_TYPE(FivSidebar, fiv_sidebar, FIV, SIDEBAR, GtkScrolledWindow) -void fiv_sidebar_set_location(FivSidebar *self, GFile *location); +GtkWidget *fiv_sidebar_new(FivIoModel *model);  void fiv_sidebar_show_enter_location(FivSidebar *self);  GtkBox *fiv_sidebar_get_toolbar(FivSidebar *self);  | 
