diff options
Diffstat (limited to 'fiv-browser.c')
-rw-r--r-- | fiv-browser.c | 487 |
1 files changed, 359 insertions, 128 deletions
diff --git a/fiv-browser.c b/fiv-browser.c index 192c3bc..c9963f4 100644 --- a/fiv-browser.c +++ b/fiv-browser.c @@ -1,7 +1,7 @@ // // fiv-browser.c: filesystem browsing widget // -// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name> +// Copyright (c) 2021 - 2024, 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,9 +17,6 @@ #include "config.h" -#include <math.h> -#include <pixman.h> - #include <gtk/gtk.h> #ifdef GDK_WINDOWING_X11 #include <gdk/gdkx.h> @@ -27,11 +24,16 @@ #ifdef GDK_WINDOWING_QUARTZ #include <gdk/gdkquartz.h> #endif // GDK_WINDOWING_QUARTZ +#include <pixman.h> + +#include <math.h> +#include <stdlib.h> #include "fiv-browser.h" #include "fiv-collection.h" #include "fiv-context-menu.h" #include "fiv-io.h" +#include "fiv-io-model.h" #include "fiv-thumbnail.h" // --- Widget ------------------------------------------------------------------ @@ -45,9 +47,12 @@ // │ n │ ┊ glow border │ n ┊ // │ g ╰───────────────────╯ g ╰┄┄┄┄┄ // │ s p a c i n g +// │ l a b e l +// │ s p a c i n g // │ ╭┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄ // // The glow is actually a glowing margin, the border is rendered in two parts. +// When labels are hidden, the surrounding spacing is collapsed. // typedef struct entry Entry; @@ -72,8 +77,10 @@ struct _FivBrowser { int item_height; ///< Thumbnail height in pixels int item_spacing; ///< Space between items in pixels + 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 @@ -85,7 +92,8 @@ struct _FivBrowser { Thumbnailer *thumbnailers; ///< Parallelized thumbnailers size_t thumbnailers_len; ///< Thumbnailers array size - GQueue thumbnailers_queue; ///< Queued up Entry pointers + GQueue thumbnailers_queue_1; ///< Queued up Entry pointers, hi-prio + GQueue thumbnailers_queue_2; ///< Queued up Entry pointers, lo-prio GdkCursor *pointer; ///< Cached pointer cursor cairo_pattern_t *glow; ///< CAIRO_FORMAT_A8 mask for corners @@ -100,29 +108,40 @@ struct _FivBrowser { /// The "last modified" timestamp of source images for thumbnails. static cairo_user_data_key_t fiv_browser_key_mtime_msec; +/// The original file size of source images for thumbnails. +static cairo_user_data_key_t fiv_browser_key_filesize; struct entry { - gchar *uri; ///< GIO URI - gchar *target_uri; ///< GIO URI for any target - gint64 mtime_msec; ///< Modification time in milliseconds + 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) { - g_free(self->uri); - g_free(self->target_uri); + 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); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct item { const Entry *entry; - int x_offset; ///< Offset within the row + PangoLayout *label; ///< Label + int x_offset; ///< X offset within the row }; struct row { @@ -135,11 +154,34 @@ struct row { static void row_free(Row *self) { + for (gsize i = 0; i < self->len; i++) + g_clear_object(&self->items[i].label); g_free(self->items); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +static double +row_subheight(const FivBrowser *self, const Row *row) +{ + if (!self->show_labels) + return 0; + + // If we didn't ellipsize labels, this should be made to account + // for vertical centering as well. + int tallest_label = 0; + for (gsize i = 0; i < row->len; i++) { + PangoRectangle ink = {}, logical = {}; + pango_layout_get_extents(row->items[i].label, &ink, &logical); + + int height = (logical.y + logical.height) / PANGO_SCALE; + if (tallest_label < height) + tallest_label = height; + } + + return self->item_spacing + tallest_label; +} + static void append_row(FivBrowser *self, int *y, int x, GArray *items_array) { @@ -154,6 +196,7 @@ append_row(FivBrowser *self, int *y, int x, GArray *items_array) // Not trying to pack them vertically, but this would be the place to do it. *y += self->item_height; *y += self->item_border_y; + *y += row_subheight(self, &row); } static int @@ -173,7 +216,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; @@ -189,8 +232,29 @@ relayout(FivBrowser *self, int width) x = 0; } - g_array_append_val(items, - ((Item) {.entry = entry, .x_offset = x + self->item_border_x})); + PangoLayout *label = NULL; + if (self->show_labels) { + label = gtk_widget_create_pango_layout( + widget, entry->e->display_name); + pango_layout_set_width( + label, (width - 2 * self->glow_w) * PANGO_SCALE); + pango_layout_set_alignment(label, PANGO_ALIGN_CENTER); + pango_layout_set_wrap(label, PANGO_WRAP_WORD_CHAR); + pango_layout_set_ellipsize(label, PANGO_ELLIPSIZE_END); + +#if PANGO_VERSION_CHECK(1, 44, 0) + PangoAttrList *attrs = pango_attr_list_new(); + pango_attr_list_insert(attrs, pango_attr_insert_hyphens_new(FALSE)); + pango_layout_set_attributes(label, attrs); + pango_attr_list_unref (attrs); +#endif + } + + g_array_append_val(items, ((Item) { + .entry = entry, + .label = label, + .x_offset = x + self->item_border_x, + })); x += width; if (max_width < width) @@ -212,14 +276,13 @@ relayout(FivBrowser *self, int width) gtk_adjustment_set_page_size(self->hadjustment, width); } if (self->vadjustment) { + int height = gtk_widget_get_allocated_height(widget); gtk_adjustment_set_lower(self->vadjustment, 0); - gtk_adjustment_set_upper(self->vadjustment, total_height); + gtk_adjustment_set_upper(self->vadjustment, MAX(height, total_height)); gtk_adjustment_set_step_increment(self->vadjustment, self->item_height + self->item_spacing + 2 * self->item_border_y); - gtk_adjustment_set_page_increment( - self->vadjustment, gtk_widget_get_allocated_height(widget) * 0.9); - gtk_adjustment_set_page_size( - self->vadjustment, gtk_widget_get_allocated_height(widget)); + gtk_adjustment_set_page_increment(self->vadjustment, height * 0.9); + gtk_adjustment_set_page_size(self->vadjustment, height); } return total_height; } @@ -342,7 +405,7 @@ draw_row(FivBrowser *self, cairo_t *cr, const Row *row) // Performance optimization--specifically targeting the checkerboard. if (cairo_image_surface_get_format(item->entry->thumbnail) != - CAIRO_FORMAT_RGB24) { + CAIRO_FORMAT_RGB24 || item->entry->removed) { gtk_render_background(style, cr, border.left, border.top, extents.width, extents.height); } @@ -358,12 +421,49 @@ draw_row(FivBrowser *self, cairo_t *cr, const Row *row) cairo_mask_surface( cr, item->entry->thumbnail, border.left, border.top); } else { + // Distinguish removed items by rendering them only faintly. + if (item->entry->removed) + cairo_push_group(cr); + cairo_set_source_surface( cr, item->entry->thumbnail, border.left, border.top); cairo_paint(cr); - // Here, we could consider multiplying + // Here, we could also consider multiplying // the whole rectangle with the selection color. + if (item->entry->removed) { + cairo_pop_group_to_source(cr); + cairo_paint_with_alpha(cr, 0.25); + } + } + + // This rendition is about the best I could come up with. + // It might be possible to use more such emblems with entries, + // though they would deserve some kind of a blur-glow. + if (item->entry->removed) { + int size = 32; + cairo_surface_t *cross = gtk_icon_theme_load_surface( + gtk_icon_theme_get_default(), "cross-large-symbolic", + size, gtk_widget_get_scale_factor(GTK_WIDGET(self)), + gtk_widget_get_window(GTK_WIDGET(self)), + GTK_ICON_LOOKUP_FORCE_SYMBOLIC, NULL); + if (cross) { + cairo_set_source_rgb(cr, 1, 0, 0); + cairo_mask_surface(cr, cross, + border.left + extents.width - size - size / 4, + border.top + extents.height - size - size / 4); + cairo_surface_destroy(cross); + } + } + + if (self->show_labels) { + gtk_style_context_save(style); + gtk_style_context_add_class(style, "label"); + gtk_render_layout(style, cr, -border.left, + border.top + extents.height + self->item_border_y + + self->item_spacing, + item->label); + gtk_style_context_restore(style); } cairo_restore(cr); @@ -440,51 +540,81 @@ rescale_thumbnail(cairo_surface_t *thumbnail, double row_height) return scaled; } -static char * +static const char * entry_system_wide_uri(const Entry *self) { // "recent" and "trash", e.g., also have "standard::target-uri" set, // but we'd like to avoid saving their thumbnails. - if (self->target_uri && fiv_collection_uri_matches(self->uri)) - return self->target_uri; + if (self->e->target_uri && fiv_collection_uri_matches(self->e->uri)) + return self->e->target_uri; - return self->uri; + return self->e->uri; } static void -entry_add_thumbnail(gpointer data, gpointer user_data) +entry_set_surface_user_data(const Entry *self) { - Entry *self = data; - g_clear_object(&self->icon); - g_clear_pointer(&self->thumbnail, cairo_surface_destroy); + // This choice of mtime favours unnecessary thumbnail reloading + // over retaining stale data (consider both calling functions). + cairo_surface_set_user_data(self->thumbnail, + &fiv_browser_key_mtime_msec, (void *) (intptr_t) self->e->mtime_msec, + NULL); + cairo_surface_set_user_data(self->thumbnail, + &fiv_browser_key_filesize, (void *) (uintptr_t) self->e->filesize, + NULL); +} - FivBrowser *browser = FIV_BROWSER(user_data); +static cairo_surface_t * +entry_lookup_thumbnail(Entry *self, FivBrowser *browser) +{ cairo_surface_t *cached = - g_hash_table_lookup(browser->thumbnail_cache, self->uri); + g_hash_table_lookup(browser->thumbnail_cache, self->e->uri); if (cached && - (intptr_t) cairo_surface_get_user_data( - cached, &fiv_browser_key_mtime_msec) == self->mtime_msec) { - self->thumbnail = cairo_surface_reference(cached); + (intptr_t) cairo_surface_get_user_data(cached, + &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) { // 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->mtime_msec, 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) { - // This choice of mtime favours unnecessary thumbnail reloading. - cairo_surface_set_user_data(self->thumbnail, - &fiv_browser_key_mtime_msec, (void *) (intptr_t) self->mtime_msec, - NULL); + 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; } // Fall back to symbolic icons, though there's only so much we can do // in parallel--GTK+ isn't thread-safe. - GFile *file = g_file_new_for_uri(self->uri); + GFile *file = g_file_new_for_uri(self->e->uri); GFileInfo *info = g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON, @@ -513,11 +643,15 @@ materialize_icon(FivBrowser *self, Entry *entry) // of using GLib to look up icons for us, derive a list from a guessed // MIME type, with "-symbolic" prefixes and fallbacks, // and use gtk_icon_theme_choose_icon() instead. - // TODO(p): Make sure we have /some/ icon for every entry. // TODO(p): We might want to populate these on an as-needed basis. - GtkIconInfo *icon_info = gtk_icon_theme_lookup_by_gicon( - gtk_icon_theme_get_default(), entry->icon, self->item_height / 2, - GTK_ICON_LOOKUP_FORCE_SYMBOLIC); + GtkIconTheme *theme = gtk_icon_theme_get_default(); + GtkIconInfo *icon_info = gtk_icon_theme_lookup_by_gicon(theme, entry->icon, + self->item_height / 2, GTK_ICON_LOOKUP_FORCE_SYMBOLIC); + if (!icon_info) { + // This icon is included within GTK+. + icon_info = gtk_icon_theme_lookup_icon(theme, "text-x-generic", + self->item_height / 2, GTK_ICON_LOOKUP_FORCE_SYMBOLIC); + } if (!icon_info) return; @@ -547,26 +681,38 @@ materialize_icon(FivBrowser *self, Entry *entry) } static void +reload_one_thumbnail_finish(FivBrowser *self, Entry *entry) +{ + if (!entry->removed && entry->thumbnail) { + g_hash_table_insert(self->thumbnail_cache, g_strdup(entry->e->uri), + cairo_surface_reference(entry->thumbnail)); + } + + materialize_icon(self, entry); +} + +static void +reload_one_thumbnail(FivBrowser *self, Entry *entry) +{ + entry_add_thumbnail(entry, self); + reload_one_thumbnail_finish(self, entry); + + gtk_widget_queue_resize(GTK_WIDGET(self)); +} + +static void 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) { - g_hash_table_insert(self->thumbnail_cache, g_strdup(entry->uri), - cairo_surface_reference(entry->thumbnail)); - } - - materialize_icon(self, entry); - } + for (guint i = 0; i < self->entries->len; i++) + reload_one_thumbnail_finish(self, self->entries->pdata[i]); gtk_widget_queue_resize(GTK_WIDGET(self)); } @@ -597,15 +743,11 @@ thumbnailer_reprocess_entry(FivBrowser *self, GBytes *output, Entry *entry) if ((flags & FIV_IO_SERIALIZE_LOW_QUALITY)) { cairo_surface_set_user_data(entry->thumbnail, &fiv_thumbnail_key_lq, (void *) (intptr_t) 1, NULL); - g_queue_push_tail(&self->thumbnailers_queue, entry); + g_queue_push_tail(&self->thumbnailers_queue_2, entry); } - // This choice of mtime favours unnecessary thumbnail reloading - // over retaining stale data. - cairo_surface_set_user_data(entry->thumbnail, - &fiv_browser_key_mtime_msec, (void *) (intptr_t) entry->mtime_msec, - NULL); - g_hash_table_insert(self->thumbnail_cache, g_strdup(entry->uri), + entry_set_surface_user_data(entry); + g_hash_table_insert(self->thumbnail_cache, g_strdup(entry->e->uri), cairo_surface_reference(entry->thumbnail)); } @@ -655,13 +797,21 @@ on_thumbnailer_ready(GObject *object, GAsyncResult *res, gpointer user_data) thumbnailer_next(t); } +// TODO(p): Try to keep the minions alive (stdout will be a problem). static gboolean thumbnailer_next(Thumbnailer *t) { - // TODO(p): Try to keep the minions alive (stdout will be a problem). + // Already have something to do, not a failure. + if (t->target) + return TRUE; + + // They could have been removed via post-reload changes in the model. FivBrowser *self = t->self; - if (!(t->target = g_queue_pop_head(&self->thumbnailers_queue))) - return FALSE; + do { + if (!(t->target = g_queue_pop_head(&self->thumbnailers_queue_1)) && + !(t->target = g_queue_pop_head(&self->thumbnailers_queue_2))) + return FALSE; + } while (t->target->removed); // Case analysis: // - We haven't found any thumbnail for the entry at all @@ -698,7 +848,8 @@ thumbnailer_next(Thumbnailer *t) static void thumbnailers_abort(FivBrowser *self) { - g_queue_clear(&self->thumbnailers_queue); + g_queue_clear(&self->thumbnailers_queue_1); + g_queue_clear(&self->thumbnailers_queue_2); for (size_t i = 0; i < self->thumbnailers_len; i++) { Thumbnailer *t = self->thumbnailers + i; @@ -714,32 +865,35 @@ thumbnailers_abort(FivBrowser *self) } static void -thumbnailers_start(FivBrowser *self) +thumbnailers_enqueue(FivBrowser *self, Entry *entry) { - thumbnailers_abort(self); - if (!self->model) - return; - - GQueue lq = G_QUEUE_INIT; - for (guint i = 0; i < self->entries->len; i++) { - Entry *entry = &g_array_index(self->entries, Entry, i); + if (!entry->removed) { if (entry->icon) - g_queue_push_tail(&self->thumbnailers_queue, entry); + g_queue_push_tail(&self->thumbnailers_queue_1, entry); else if (cairo_surface_get_user_data( entry->thumbnail, &fiv_thumbnail_key_lq)) - g_queue_push_tail(&lq, entry); - } - while (!g_queue_is_empty(&lq)) { - g_queue_push_tail_link( - &self->thumbnailers_queue, g_queue_pop_head_link(&lq)); + g_queue_push_tail(&self->thumbnailers_queue_2, entry); } +} +static void +thumbnailers_deploy(FivBrowser *self) +{ for (size_t i = 0; i < self->thumbnailers_len; i++) { if (!thumbnailer_next(self->thumbnailers + i)) break; } } +static void +thumbnailers_restart(FivBrowser *self) +{ + thumbnailers_abort(self); + for (guint i = 0; i < self->entries->len; i++) + thumbnailers_enqueue(self, self->entries->pdata[i]); + thumbnailers_deploy(self); +} + // --- Boilerplate ------------------------------------------------------------- G_DEFINE_TYPE_EXTENDED(FivBrowser, fiv_browser, GTK_TYPE_WIDGET, 0, @@ -747,6 +901,7 @@ G_DEFINE_TYPE_EXTENDED(FivBrowser, fiv_browser, GTK_TYPE_WIDGET, 0, enum { PROP_THUMBNAIL_SIZE = 1, + PROP_SHOW_LABELS, N_PROPERTIES, // These are overriden, we do not register them. @@ -799,7 +954,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); @@ -827,6 +982,9 @@ fiv_browser_get_property( case PROP_THUMBNAIL_SIZE: g_value_set_enum(value, self->item_size); break; + case PROP_SHOW_LABELS: + g_value_set_boolean(value, self->show_labels); + break; case PROP_HADJUSTMENT: g_value_set_object(value, self->hadjustment); break; @@ -856,7 +1014,7 @@ set_item_size(FivBrowser *self, FivThumbnailSize size) g_hash_table_remove_all(self->thumbnail_cache); reload_thumbnails(self); - thumbnailers_start(self); + thumbnailers_restart(self); g_object_notify_by_pspec( G_OBJECT(self), browser_properties[PROP_THUMBNAIL_SIZE]); @@ -872,6 +1030,13 @@ fiv_browser_set_property( case PROP_THUMBNAIL_SIZE: set_item_size(self, g_value_get_enum(value)); break; + case PROP_SHOW_LABELS: + if (self->show_labels != g_value_get_boolean(value)) { + self->show_labels = g_value_get_boolean(value); + gtk_widget_queue_resize(GTK_WIDGET(self)); + g_object_notify_by_pspec(object, pspec); + } + break; case PROP_HADJUSTMENT: if (replace_adjustment( self, &self->hadjustment, g_value_get_object(value))) @@ -1042,7 +1207,7 @@ fiv_browser_draw(GtkWidget *widget, cairo_t *cr) static gboolean open_entry(GtkWidget *self, const Entry *entry, gboolean new_window) { - GFile *location = g_file_new_for_uri(entry->uri); + GFile *location = g_file_new_for_uri(entry->e->uri); g_signal_emit(self, browser_signals[ITEM_ACTIVATED], 0, location, new_window ? GTK_PLACES_OPEN_NEW_WINDOW : GTK_PLACES_OPEN_NORMAL); g_object_unref(location); @@ -1052,7 +1217,9 @@ open_entry(GtkWidget *self, const Entry *entry, gboolean new_window) static void show_context_menu(GtkWidget *widget, GFile *file) { - gtk_menu_popup_at_pointer(fiv_context_menu_new(widget, file), NULL); + GtkMenu *menu = fiv_context_menu_new(widget, file); + if (menu) + gtk_menu_popup_at_pointer(menu, NULL); } static void @@ -1112,7 +1279,7 @@ fiv_browser_button_press_event(GtkWidget *widget, GdkEventButton *event) // no matter what its new location is. gdk_window_set_cursor(gtk_widget_get_window(widget), NULL); - GFile *file = g_file_new_for_uri(entry->uri); + GFile *file = g_file_new_for_uri(entry->e->uri); show_context_menu(widget, file); g_object_unref(file); return GDK_EVENT_STOP; @@ -1256,8 +1423,8 @@ fiv_browser_drag_data_get(GtkWidget *widget, { FivBrowser *self = FIV_BROWSER(widget); if (self->selected) { - (void) gtk_selection_data_set_uris( - data, (gchar *[]) {entry_system_wide_uri(self->selected), NULL}); + (void) gtk_selection_data_set_uris(data, (gchar *[]) + {(gchar *) entry_system_wide_uri(self->selected), NULL}); } } @@ -1283,12 +1450,13 @@ scroll_to_row(FivBrowser *self, const Row *row) double y1 = gtk_adjustment_get_value(self->vadjustment); double ph = gtk_adjustment_get_page_size(self->vadjustment); + double sh = self->item_border_y + row_subheight(self, row); if (row->y_offset < y1) { gtk_adjustment_set_value( self->vadjustment, row->y_offset - self->item_border_y); - } else if (row->y_offset + self->item_height > y1 + ph) { - gtk_adjustment_set_value(self->vadjustment, - row->y_offset - ph + self->item_height + self->item_border_y); + } else if (row->y_offset + self->item_height + sh > y1 + ph) { + gtk_adjustment_set_value( + self->vadjustment, row->y_offset - ph + self->item_height + sh); } } @@ -1404,6 +1572,14 @@ fiv_browser_key_press_event(GtkWidget *widget, GdkEventKey *event) switch ((event->state & gtk_accelerator_get_default_mod_mask())) { case 0: switch (event->keyval) { + case GDK_KEY_Delete: + if (self->selected) { + GtkWindow *window = GTK_WINDOW(gtk_widget_get_toplevel(widget)); + GFile *file = g_file_new_for_uri(self->selected->e->uri); + fiv_context_menu_remove(window, file); + g_object_unref(file); + } + return GDK_EVENT_STOP; case GDK_KEY_Return: if (self->selected) return open_entry(widget, self->selected, FALSE); @@ -1433,7 +1609,7 @@ fiv_browser_key_press_event(GtkWidget *widget, GdkEventKey *event) case GDK_KEY_Return: if (self->selected) { GtkWindow *window = GTK_WINDOW(gtk_widget_get_toplevel(widget)); - fiv_context_menu_information(window, self->selected->uri); + fiv_context_menu_information(window, self->selected->e->uri); } return GDK_EVENT_STOP; } @@ -1459,20 +1635,16 @@ fiv_browser_query_tooltip(GtkWidget *widget, gint x, gint y, G_GNUC_UNUSED gboolean keyboard_tooltip, GtkTooltip *tooltip) { FivBrowser *self = FIV_BROWSER(widget); - const Entry *entry = entry_at(self, x, y); - if (!entry) + + // TODO(p): Consider getting rid of tooltips altogether. + if (self->show_labels) return FALSE; - GFile *file = g_file_new_for_uri(entry->uri); - GFileInfo *info = - g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, - G_FILE_QUERY_INFO_NONE, NULL, NULL); - g_object_unref(file); - if (!info) + const Entry *entry = entry_at(self, x, y); + if (!entry) return FALSE; - gtk_tooltip_set_text(tooltip, g_file_info_get_display_name(info)); - g_object_unref(info); + gtk_tooltip_set_text(tooltip, entry->e->display_name); return TRUE; } @@ -1486,7 +1658,7 @@ fiv_browser_popup_menu(GtkWidget *widget) GFile *file = NULL; GdkRectangle rect = {}; if (self->selected) { - file = g_file_new_for_uri(self->selected->uri); + file = g_file_new_for_uri(self->selected->e->uri); rect = entry_rect(self, self->selected); rect.x += rect.width / 2; rect.y += rect.height / 2; @@ -1524,7 +1696,7 @@ on_long_press(GtkGestureLongPress *lp, gdouble x, gdouble y, gpointer user_data) // It might also be possible to have long-press just select items, // and show some kind of toolbar with available actions. - GFile *file = g_file_new_for_uri(entry->uri); + GFile *file = g_file_new_for_uri(entry->e->uri); gtk_menu_popup_at_rect(fiv_context_menu_new(widget, file), window, &(GdkRectangle) {.x = x, .y = y}, GDK_GRAVITY_NORTH_WEST, GDK_GRAVITY_NORTH_WEST, event); @@ -1553,6 +1725,13 @@ fiv_browser_style_updated(GtkWidget *widget) gtk_style_context_add_class(style, "item"); gtk_style_context_get_margin(style, GTK_STATE_FLAG_NORMAL, &margin); gtk_style_context_get_border(style, GTK_STATE_FLAG_NORMAL, &border); + // XXX: Right now, specifying custom fonts within our CSS pseudo-regions + // has no effect, so it might be appropriate to also add .label/.symbolic + // classes here, remember the resulting GTK_STYLE_PROPERTY_FONT, + // and apply them in relayout() with pango_layout_set_font_description(). + // There is virtually nothing to be gained from this flexibility, though. + // XXX: We should also invoke relayout() here, because different states + // might theoretically use different fonts. gtk_style_context_restore(style); self->glow_w = (margin.left + margin.right) / 2; @@ -1634,6 +1813,9 @@ fiv_browser_class_init(FivBrowserClass *klass) "thumbnail-size", "Thumbnail size", "The thumbnail height to use", FIV_TYPE_THUMBNAIL_SIZE, FIV_THUMBNAIL_SIZE_NORMAL, G_PARAM_READWRITE); + browser_properties[PROP_SHOW_LABELS] = g_param_spec_boolean( + "show-labels", "Show labels", "Whether to show filename labels", + FALSE, G_PARAM_READWRITE); g_object_class_install_properties( object_class, N_PROPERTIES, browser_properties); @@ -1686,8 +1868,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); @@ -1700,9 +1882,11 @@ fiv_browser_init(FivBrowser *self) g_malloc0_n(self->thumbnailers_len, sizeof *self->thumbnailers); for (size_t i = 0; i < self->thumbnailers_len; i++) self->thumbnailers[i].self = self; - g_queue_init(&self->thumbnailers_queue); + g_queue_init(&self->thumbnailers_queue_1); + g_queue_init(&self->thumbnailers_queue_2); set_item_size(self, FIV_THUMBNAIL_SIZE_NORMAL); + self->show_labels = FALSE; self->glow_padded = cairo_pattern_create_rgba(0, 0, 0, 0); self->glow = cairo_pattern_create_rgba(0, 0, 0, 0); @@ -1721,35 +1905,80 @@ 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); gchar *selected_uri = NULL; if (self->selected) - selected_uri = g_strdup(self->selected->uri); + 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; - const FivIoModelEntry *files = fiv_io_model_get_files(self->model, &len); + FivIoModelEntry *const *files = fiv_io_model_get_files(self->model, &len); for (gsize i = 0; i < len; i++) { - Entry e = {.thumbnail = NULL, - .uri = g_strdup(files[i].uri), - .target_uri = g_strdup(files[i].target_uri), - .mtime_msec = files[i].mtime_msec}; - 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); g_free(selected_uri); + // Restarting thumbnailers is critical, because they keep Entry pointers. reload_thumbnails(self); - thumbnailers_start(self); + thumbnailers_restart(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) { + Entry *entry = entry_new(fiv_io_model_entry_ref(new)); + g_ptr_array_add(self->entries, entry); + + reload_one_thumbnail(self, entry); + thumbnailers_enqueue(self, entry); + thumbnailers_deploy(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. + reload_one_thumbnail(self, found); + // TODO(p): Rather cancel the entry in any running thumbnailer, + // remove it from queues, and _enqueue() + _deploy(). + thumbnailers_restart(self); + } else { + found->removed = TRUE; + gtk_widget_queue_draw(GTK_WIDGET(self)); + } } GtkWidget * @@ -1760,9 +1989,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); } @@ -1791,8 +2022,8 @@ 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); - if (!g_strcmp0(entry->uri, uri)) { + const Entry *entry = self->entries->pdata[i]; + if (!g_strcmp0(entry->e->uri, uri)) { self->selected = entry; scroll_to_selection(self); break; |