From b935b0baf868014d741bdbbf288c7b6a82fb0749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?= Date: Fri, 31 Dec 2021 02:19:17 +0100 Subject: [PATCH] Use a unified filesystem model This removes some duplication of effort. So far, sorting adjustments are not exposed in the UI. --- LICENSE | 2 +- fastiv.c | 156 +++++++++--------------- fiv-browser.c | 102 +++++++--------- fiv-browser.h | 9 +- fiv-io.c | 324 +++++++++++++++++++++++++++++++++++++++++++++++--- fiv-io.h | 17 ++- fiv-sidebar.c | 92 ++++++-------- fiv-sidebar.h | 6 +- 8 files changed, 467 insertions(+), 241 deletions(-) diff --git a/LICENSE b/LICENSE index a28ec5a..837a7f9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2021, Přemysl Eric Janouch +Copyright (c) 2021 - 2022, Přemysl Eric Janouch Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. diff --git a/fastiv.c b/fastiv.c index 1d57cca..11c7774 100644 --- a/fastiv.c +++ b/fastiv.c @@ -1,7 +1,7 @@ // // fastiv.c: fast image viewer // -// Copyright (c) 2021, Přemysl Eric Janouch +// Copyright (c) 2021 - 2022, Přemysl Eric Janouch // // 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 #include -#include - #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; + FivIoModel *model; ///< "directory" contents + gchar *directory; ///< URI of the currently browsed directory + 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 - gchar *directory; ///< URI of the currently browsed directory - GList *directory_back; ///< History paths going backwards - GList *directory_forward; ///< History paths going forwards - GPtrArray *files; ///< "directory" contents as URIs 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); } @@ -675,6 +624,15 @@ on_notify_thumbnail_size( gtk_widget_set_sensitive(g.minus, size > FIV_THUMBNAIL_SIZE_MIN); } +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) { @@ -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 +// Copyright (c) 2021 - 2022, Přemysl Eric Janouch // // 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) +static void +on_model_files_changed(FivIoModel *model, FivBrowser *self) { - 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) -{ - 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 +// Copyright (c) 2021 - 2022, Přemysl Eric Janouch // // 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 #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); diff --git a/fiv-io.c b/fiv-io.c index 5f2e10f..518da32 100644 --- a/fiv-io.c +++ b/fiv-io.c @@ -1,7 +1,7 @@ // // fiv-io.c: image operations // -// Copyright (c) 2021, Přemysl Eric Janouch +// Copyright (c) 2021 - 2022, Přemysl Eric Janouch // // 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 + +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 * diff --git a/fiv-io.h b/fiv-io.h index 7e1ee9a..e3deac5 100644 --- a/fiv-io.h +++ b/fiv-io.h @@ -1,7 +1,7 @@ // // fiv-io.h: image operations // -// Copyright (c) 2021, Přemysl Eric Janouch +// Copyright (c) 2021 - 2022, Přemysl Eric Janouch // // 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 +// Copyright (c) 2021 - 2022, Přemysl Eric Janouch // // 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 -#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 +// Copyright (c) 2021 - 2022, Přemysl Eric Janouch // // 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 #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);