aboutsummaryrefslogtreecommitdiff
path: root/fiv-browser.c
diff options
context:
space:
mode:
Diffstat (limited to 'fiv-browser.c')
-rw-r--r--fiv-browser.c487
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;