From 2b17ed838afb1ac1d63ca6e4d60945844263ca40 Mon Sep 17 00:00:00 2001 From: Přemysl Eric Janouch Date: Sun, 21 Nov 2021 16:03:54 +0100 Subject: Add ability to use different thumbnail sizes --- fastiv-browser.c | 369 +++++++++++++++++++++++++++++++++---------------------- fastiv-io.c | 50 +++++++- fastiv-io.h | 29 ++++- fastiv.c | 58 +++++++-- 4 files changed, 340 insertions(+), 166 deletions(-) diff --git a/fastiv-browser.c b/fastiv-browser.c index feb4ca3..e025d66 100644 --- a/fastiv-browser.c +++ b/fastiv-browser.c @@ -41,6 +41,8 @@ struct _FastivBrowser { GtkWidget parent_instance; + FastivIoThumbnailSize item_size; ///< Thumbnail height in pixels + GArray *entries; ///< [Entry] GArray *layouted_rows; ///< [Row] int selected; @@ -55,7 +57,6 @@ typedef struct entry Entry; typedef struct item Item; typedef struct row Row; -static const double g_row_height = 256; static const double g_permitted_width_multiplier = 2; // Could be split out to also-idiomatic row-spacing/column-spacing properties. @@ -114,7 +115,7 @@ append_row(FastivBrowser *self, int *y, int x, GArray *items_array) })); // Not trying to pack them vertically, but this would be the place to do it. - *y += g_row_height; + *y += (int) self->item_size; *y += self->item_border_y; } @@ -199,13 +200,13 @@ draw_outer_border(FastivBrowser *self, cairo_t *cr, int width, int height) } static GdkRectangle -item_extents(const Item *item, const Row *row) +item_extents(FastivBrowser *self, const Item *item, const Row *row) { int width = cairo_image_surface_get_width(item->entry->thumbnail); int height = cairo_image_surface_get_height(item->entry->thumbnail); return (GdkRectangle) { .x = row->x_offset + item->x_offset, - .y = row->y_offset + g_row_height - height, + .y = row->y_offset + (int) self->item_size - height, .width = width, .height = height, }; @@ -217,7 +218,7 @@ entry_at(FastivBrowser *self, int x, int y) for (guint i = 0; i < self->layouted_rows->len; i++) { const Row *row = &g_array_index(self->layouted_rows, Row, i); for (Item *item = row->items; item->entry; item++) { - GdkRectangle extents = item_extents(item, row); + GdkRectangle extents = item_extents(self, item, row); if (x >= extents.x && y >= extents.y && x <= extents.x + extents.width && @@ -243,7 +244,7 @@ draw_row(FastivBrowser *self, cairo_t *cr, const Row *row) gtk_style_context_get_border(style, state, &border); for (Item *item = row->items; item->entry; item++) { cairo_save(cr); - GdkRectangle extents = item_extents(item, row); + GdkRectangle extents = item_extents(self, item, row); cairo_translate(cr, extents.x - border.left, extents.y - border.top); gtk_style_context_save(style); @@ -281,6 +282,159 @@ draw_row(FastivBrowser *self, cairo_t *cr, const Row *row) gtk_style_context_restore(style); } +// --- Thumbnails -------------------------------------------------------------- + +// NOTE: "It is important to note that when an image with an alpha channel is +// scaled, linear encoded, pre-multiplied component values must be used!" +static cairo_surface_t * +rescale_thumbnail(cairo_surface_t *thumbnail, double row_height) +{ + if (!thumbnail) + return thumbnail; + + int width = cairo_image_surface_get_width(thumbnail); + int height = cairo_image_surface_get_height(thumbnail); + + double scale_x = 1; + double scale_y = 1; + if (width > g_permitted_width_multiplier * height) { + scale_x = g_permitted_width_multiplier * row_height / width; + scale_y = round(scale_x * height) / height; + } else { + scale_y = row_height / height; + scale_x = round(scale_y * width) / width; + } + if (scale_x == 1 && scale_y == 1) + return thumbnail; + + int projected_width = round(scale_x * width); + int projected_height = round(scale_y * height); + cairo_surface_t *scaled = cairo_image_surface_create( + CAIRO_FORMAT_ARGB32, projected_width, projected_height); + + // pixman can take gamma into account when scaling, unlike Cairo. + struct pixman_f_transform xform_floating; + struct pixman_transform xform; + + // PIXMAN_a8r8g8b8_sRGB can be used for gamma-correct results, + // but it's an incredibly slow transformation + pixman_format_code_t format = PIXMAN_a8r8g8b8; + + pixman_image_t *src = pixman_image_create_bits(format, width, height, + (uint32_t *) cairo_image_surface_get_data(thumbnail), + cairo_image_surface_get_stride(thumbnail)); + pixman_image_t *dest = pixman_image_create_bits(format, + cairo_image_surface_get_width(scaled), + cairo_image_surface_get_height(scaled), + (uint32_t *) cairo_image_surface_get_data(scaled), + cairo_image_surface_get_stride(scaled)); + + pixman_f_transform_init_scale(&xform_floating, scale_x, scale_y); + pixman_f_transform_invert(&xform_floating, &xform_floating); + pixman_transform_from_pixman_f_transform(&xform, &xform_floating); + pixman_image_set_transform(src, &xform); + pixman_image_set_filter(src, PIXMAN_FILTER_BILINEAR, NULL, 0); + pixman_image_set_repeat(src, PIXMAN_REPEAT_PAD); + + pixman_image_composite(PIXMAN_OP_SRC, src, NULL, dest, 0, 0, 0, 0, 0, 0, + projected_width, projected_height); + pixman_image_unref(src); + pixman_image_unref(dest); + + cairo_surface_destroy(thumbnail); + cairo_surface_mark_dirty(scaled); + return scaled; +} + +static void +entry_add_thumbnail(gpointer data, gpointer user_data) +{ + Entry *self = data; + g_clear_object(&self->icon); + if (self->thumbnail) + cairo_surface_destroy(self->thumbnail); + + FastivIoThumbnailSize size = FASTIV_BROWSER(user_data)->item_size; + self->thumbnail = rescale_thumbnail( + fastiv_io_lookup_thumbnail(self->filename, size), (int) size); + if (self->thumbnail) + 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_path(self->filename); + GFileInfo *info = g_file_query_info(file, + G_FILE_ATTRIBUTE_STANDARD_NAME + "," G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON, + G_FILE_QUERY_INFO_NONE, NULL, NULL); + g_object_unref(file); + if (info) { + GIcon *icon = g_file_info_get_symbolic_icon(info); + if (icon) + self->icon = g_object_ref(icon); + g_object_unref(info); + } +} + +static void +materialize_icon(FastivBrowser *self, Entry *entry) +{ + if (!entry->icon) + return; + + // Fucker will still give us non-symbolic icons, no more playing nice. + // TODO(p): Investigate a bit closer. We may want to abandon the idea + // 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, (int) self->item_size / 2, + GTK_ICON_LOOKUP_FORCE_SYMBOLIC); + if (!icon_info) + return; + + // Bílá, bílá, bílá, bílá... komu by se nelíbí-lá... + // We do not want any highlights, nor do we want to remember the style. + const GdkRGBA white = {1, 1, 1, 1}; + GdkPixbuf *pixbuf = gtk_icon_info_load_symbolic( + icon_info, &white, &white, &white, &white, NULL, NULL); + if (pixbuf) { + int outer_size = (int) self->item_size; + entry->thumbnail = + cairo_image_surface_create(CAIRO_FORMAT_A8, outer_size, outer_size); + + // "Note that the resulting pixbuf may not be exactly this size;" + // though GTK_ICON_LOOKUP_FORCE_SIZE is also an option. + int x = (outer_size - gdk_pixbuf_get_width(pixbuf)) / 2; + int y = (outer_size - gdk_pixbuf_get_height(pixbuf)) / 2; + + cairo_t *cr = cairo_create(entry->thumbnail); + gdk_cairo_set_source_pixbuf(cr, pixbuf, x, y); + cairo_paint(cr); + cairo_destroy(cr); + + g_object_unref(pixbuf); + } + g_object_unref(icon_info); +} + +static void +reload_thumbnails(FastivBrowser *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_free(pool, FALSE, TRUE); + + for (guint i = 0; i < self->entries->len; i++) + materialize_icon(self, &g_array_index(self->entries, Entry, i)); + + gtk_widget_queue_resize(GTK_WIDGET(self)); +} + // --- Boilerplate ------------------------------------------------------------- // TODO(p): For proper navigation, we need to implement GtkScrollable. @@ -288,6 +442,13 @@ G_DEFINE_TYPE_EXTENDED(FastivBrowser, fastiv_browser, GTK_TYPE_WIDGET, 0, /* G_IMPLEMENT_INTERFACE(GTK_TYPE_SCROLLABLE, fastiv_browser_scrollable_init) */) +enum { + PROP_THUMBNAIL_SIZE = 1, + N_PROPERTIES +}; + +static GParamSpec *browser_properties[N_PROPERTIES]; + enum { ITEM_ACTIVATED, LAST_SIGNAL, @@ -308,6 +469,37 @@ fastiv_browser_finalize(GObject *gobject) G_OBJECT_CLASS(fastiv_browser_parent_class)->finalize(gobject); } +static void +fastiv_browser_get_property( + GObject *object, guint property_id, GValue *value, GParamSpec *pspec) +{ + FastivBrowser *self = FASTIV_BROWSER(object); + switch (property_id) { + case PROP_THUMBNAIL_SIZE: + g_value_set_enum(value, self->item_size); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + } +} + +static void +fastiv_browser_set_property( + GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) +{ + FastivBrowser *self = FASTIV_BROWSER(object); + switch (property_id) { + case PROP_THUMBNAIL_SIZE: + if (g_value_get_enum(value) != (int) self->item_size) { + self->item_size = g_value_get_enum(value); + reload_thumbnails(self); + } + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + } +} + static GtkSizeRequestMode fastiv_browser_get_request_mode(G_GNUC_UNUSED GtkWidget *widget) { @@ -323,7 +515,8 @@ fastiv_browser_get_preferred_width( GtkBorder padding = {}; gtk_style_context_get_padding(style, GTK_STATE_FLAG_NORMAL, &padding); - *minimum = *natural = g_permitted_width_multiplier * g_row_height + + *minimum = *natural = + g_permitted_width_multiplier * (int) self->item_size + padding.left + 2 * self->item_border_x + padding.right; } @@ -401,7 +594,7 @@ fastiv_browser_draw(GtkWidget *widget, cairo_t *cr) .x = 0, .y = row->y_offset - self->item_border_y, .width = allocation.width, - .height = g_row_height + 2 * self->item_border_y, + .height = (int) self->item_size + 2 * self->item_border_y, }; if (!have_clip || gdk_rectangle_intersect(&clip, &extents, NULL)) draw_row(self, cr, row); @@ -520,6 +713,19 @@ fastiv_browser_class_init(FastivBrowserClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fastiv_browser_finalize; + object_class->get_property = fastiv_browser_get_property; + object_class->set_property = fastiv_browser_set_property; + + browser_properties[PROP_THUMBNAIL_SIZE] = g_param_spec_enum( + "thumbnail-size", "Thumbnail size", "The thumbnail height to use", + FASTIV_TYPE_IO_THUMBNAIL_SIZE, FASTIV_IO_THUMBNAIL_SIZE_NORMAL, + G_PARAM_READWRITE); + g_object_class_install_properties( + object_class, N_PROPERTIES, browser_properties); + + browser_signals[ITEM_ACTIVATED] = g_signal_new("item-activated", + G_TYPE_FROM_CLASS(klass), 0, 0, NULL, NULL, NULL, + G_TYPE_NONE, 2, G_TYPE_STRING, GTK_TYPE_PLACES_OPEN_FLAGS); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); widget_class->get_request_mode = fastiv_browser_get_request_mode; @@ -533,10 +739,6 @@ fastiv_browser_class_init(FastivBrowserClass *klass) widget_class->motion_notify_event = fastiv_browser_motion_notify_event; widget_class->style_updated = fastiv_browser_style_updated; - browser_signals[ITEM_ACTIVATED] = g_signal_new("item-activated", - G_TYPE_FROM_CLASS(klass), 0, 0, NULL, NULL, NULL, - G_TYPE_NONE, 2, G_TYPE_STRING, GTK_TYPE_PLACES_OPEN_FLAGS); - // TODO(p): Later override "screen_changed", recreate Pango layouts there, // if we get to have any, or otherwise reflect DPI changes. gtk_widget_class_set_css_name(widget_class, "fastiv-browser"); @@ -552,96 +754,15 @@ fastiv_browser_init(FastivBrowser *self) self->layouted_rows = g_array_new(FALSE, TRUE, sizeof(Row)); g_array_set_clear_func(self->layouted_rows, (GDestroyNotify) row_free); + self->item_size = FASTIV_IO_THUMBNAIL_SIZE_NORMAL; self->selected = -1; self->glow = cairo_image_surface_create(CAIRO_FORMAT_A1, 0, 0); -} - -// NOTE: "It is important to note that when an image with an alpha channel is -// scaled, linear encoded, pre-multiplied component values must be used!" -static cairo_surface_t * -rescale_thumbnail(cairo_surface_t *thumbnail) -{ - if (!thumbnail) - return thumbnail; - - int width = cairo_image_surface_get_width(thumbnail); - int height = cairo_image_surface_get_height(thumbnail); - - double scale_x = 1; - double scale_y = 1; - if (width > g_permitted_width_multiplier * height) { - scale_x = g_permitted_width_multiplier * g_row_height / width; - scale_y = round(scale_x * height) / height; - } else { - scale_y = g_row_height / height; - scale_x = round(scale_y * width) / width; - } - if (scale_x == 1 && scale_y == 1) - return thumbnail; - - int projected_width = round(scale_x * width); - int projected_height = round(scale_y * height); - cairo_surface_t *scaled = cairo_image_surface_create( - CAIRO_FORMAT_ARGB32, projected_width, projected_height); - - // pixman can take gamma into account when scaling, unlike Cairo. - struct pixman_f_transform xform_floating; - struct pixman_transform xform; - - // PIXMAN_a8r8g8b8_sRGB can be used for gamma-correct results, - // but it's an incredibly slow transformation - pixman_format_code_t format = PIXMAN_a8r8g8b8; - - pixman_image_t *src = pixman_image_create_bits(format, width, height, - (uint32_t *) cairo_image_surface_get_data(thumbnail), - cairo_image_surface_get_stride(thumbnail)); - pixman_image_t *dest = pixman_image_create_bits(format, - cairo_image_surface_get_width(scaled), - cairo_image_surface_get_height(scaled), - (uint32_t *) cairo_image_surface_get_data(scaled), - cairo_image_surface_get_stride(scaled)); - pixman_f_transform_init_scale(&xform_floating, scale_x, scale_y); - pixman_f_transform_invert(&xform_floating, &xform_floating); - pixman_transform_from_pixman_f_transform(&xform, &xform_floating); - pixman_image_set_transform(src, &xform); - pixman_image_set_filter(src, PIXMAN_FILTER_BILINEAR, NULL, 0); - pixman_image_set_repeat(src, PIXMAN_REPEAT_PAD); - - pixman_image_composite(PIXMAN_OP_SRC, src, NULL, dest, 0, 0, 0, 0, 0, 0, - projected_width, projected_height); - pixman_image_unref(src); - pixman_image_unref(dest); - - cairo_surface_destroy(thumbnail); - cairo_surface_mark_dirty(scaled); - return scaled; + g_signal_connect_swapped(gtk_settings_get_default(), + "notify::gtk-icon-theme-name", G_CALLBACK(reload_thumbnails), self); } -static void -entry_add_thumbnail(gpointer data, G_GNUC_UNUSED gpointer user_data) -{ - Entry *self = data; - self->thumbnail = - rescale_thumbnail(fastiv_io_lookup_thumbnail(self->filename)); - if (self->thumbnail) - 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_path(self->filename); - GFileInfo *info = g_file_query_info(file, - G_FILE_ATTRIBUTE_STANDARD_NAME - "," G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON, - G_FILE_QUERY_INFO_NONE, NULL, NULL); - g_object_unref(file); - if (info) { - GIcon *icon = g_file_info_get_symbolic_icon(info); - if (icon) - self->icon = g_object_ref(icon); - g_object_unref(info); - } -} +// --- Public interface -------------------------------------------------------- void fastiv_browser_load( @@ -676,56 +797,6 @@ fastiv_browser_load( } g_object_unref(enumerator); - GThreadPool *pool = g_thread_pool_new( - entry_add_thumbnail, NULL, 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_free(pool, FALSE, TRUE); - - for (guint i = 0; i < self->entries->len; i++) { - Entry *entry = &g_array_index(self->entries, Entry, i); - if (!entry->icon) - continue; - - // Fucker will still give us non-symbolic icons, no more playing nice. - // TODO(p): Investigate a bit closer. We may want to abandon the idea - // 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): GtkSettings -> notify::gtk-icon-theme-name? - // 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, g_row_height / 2, GTK_ICON_LOOKUP_FORCE_SYMBOLIC); - if (!icon_info) - continue; - - // Bílá, bílá, bílá, bílá... komu by se nelíbí-lá... - // We do not want any highlights, nor do we want to remember the style. - const GdkRGBA white = {1, 1, 1, 1}; - GdkPixbuf *pixbuf = gtk_icon_info_load_symbolic( - icon_info, &white, &white, &white, &white, NULL, NULL); - if (pixbuf) { - int outer_size = g_row_height; - entry->thumbnail = cairo_image_surface_create( - CAIRO_FORMAT_A8, outer_size, outer_size); - - // "Note that the resulting pixbuf may not be exactly this size;" - // though GTK_ICON_LOOKUP_FORCE_SIZE is also an option. - int x = (outer_size - gdk_pixbuf_get_width(pixbuf)) / 2; - int y = (outer_size - gdk_pixbuf_get_height(pixbuf)) / 2; - - cairo_t *cr = cairo_create(entry->thumbnail); - gdk_cairo_set_source_pixbuf(cr, pixbuf, x, y); - cairo_paint(cr); - cairo_destroy(cr); - - g_object_unref(pixbuf); - } - g_object_unref(icon_info); - } - - // TODO(p): Sort and filter the entries. - gtk_widget_queue_resize(GTK_WIDGET(self)); + // TODO(p): Sort the entries before. + reload_thumbnails(self); } diff --git a/fastiv-io.c b/fastiv-io.c index e1bb60a..60c0292 100644 --- a/fastiv-io.c +++ b/fastiv-io.c @@ -102,6 +102,29 @@ fastiv_io_all_supported_media_types(void) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +GType +fastiv_io_thumbnail_size_get_type(void) +{ + static gsize guard; + if (g_once_init_enter(&guard)) { +#define XX(name, value, dir) {FASTIV_IO_THUMBNAIL_SIZE_ ## name, \ + "FASTIV_IO_THUMBNAIL_SIZE_" #name, #name}, + static const GEnumValue values[] = {FASTIV_IO_THUMBNAIL_SIZES(XX) {}}; +#undef XX + GType type = g_enum_register_static( + g_intern_static_string("FastivIoThumbnailSize"), values); + g_once_init_leave(&guard, type); + } + return guard; +} + +#define XX(name, value, dir) {FASTIV_IO_THUMBNAIL_SIZE_ ## name, dir}, +FastivIoThumbnailSizeInfo fastiv_io_thumbnail_sizes[] = { + FASTIV_IO_THUMBNAIL_SIZES(XX) {}}; +#undef XX + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + #define FASTIV_IO_ERROR fastiv_io_error_quark() G_DEFINE_QUARK(fastiv-io-error-quark, fastiv_io_error) @@ -981,8 +1004,20 @@ fail_init: } cairo_surface_t * -fastiv_io_lookup_thumbnail(const gchar *target) +fastiv_io_lookup_thumbnail(const gchar *target, FastivIoThumbnailSize size) { + // TODO(p): Consider having linear enum constants, + // and using a trivial lookup table. This is ridiculous. + int size_index = 0; + while (fastiv_io_thumbnail_sizes[size_index].size && + fastiv_io_thumbnail_sizes[size_index].size < size) + size_index++; + int size_count = size_index; + while (fastiv_io_thumbnail_sizes[size_count].size) + size_count++; + + g_return_val_if_fail(fastiv_io_thumbnail_sizes[size_index].size, NULL); + GStatBuf st; if (g_stat(target, &st)) return NULL; @@ -997,17 +1032,22 @@ fastiv_io_lookup_thumbnail(const gchar *target) gchar *cache_dir = get_xdg_home_dir("XDG_CACHE_HOME", ".cache"); cairo_surface_t *result = NULL; - const gchar *sizes[] = {"large", "x-large", "xx-large", "normal"}; GError *error = NULL; - for (gsize i = 0; !result && i < G_N_ELEMENTS(sizes); i++) { - gchar *path = g_strdup_printf( - "%s/thumbnails/%s/%s.png", cache_dir, sizes[i], sum); + for (int i = 0; i < size_count; i++) { + int size_use = size_index + i; + if (size_use >= size_count) + size_use = size_count - i - 1; + + gchar *path = g_strdup_printf("%s/thumbnails/%s/%s.png", cache_dir, + fastiv_io_thumbnail_sizes[size_use].directory_name, sum); result = read_spng_thumbnail(path, uri, st.st_mtim.tv_sec, &error); if (error) { g_debug("%s: %s", path, error->message); g_clear_error(&error); } g_free(path); + if (result) + break; } g_free(cache_dir); diff --git a/fastiv-io.h b/fastiv-io.h index 5a90e91..101db6c 100644 --- a/fastiv-io.h +++ b/fastiv-io.h @@ -19,12 +19,39 @@ #include #include +#include extern const char *fastiv_io_supported_media_types[]; char **fastiv_io_all_supported_media_types(void); +// And this is how you avoid glib-mkenums. +typedef enum _FastivIoThumbnailSize { +#define FASTIV_IO_THUMBNAIL_SIZES(XX) \ + XX(SMALL, 128, "normal") \ + XX(NORMAL, 256, "large") \ + XX(LARGE, 512, "x-large") \ + XX(HUGE, 1024, "xx-large") +#define XX(name, value, dir) FASTIV_IO_THUMBNAIL_SIZE_ ## name = value, + FASTIV_IO_THUMBNAIL_SIZES(XX) +#undef XX + FASTIV_IO_THUMBNAIL_SIZE_MIN = FASTIV_IO_THUMBNAIL_SIZE_SMALL, + FASTIV_IO_THUMBNAIL_SIZE_MAX = FASTIV_IO_THUMBNAIL_SIZE_HUGE +} FastivIoThumbnailSize; + +GType fastiv_io_thumbnail_size_get_type(void) G_GNUC_CONST; +#define FASTIV_TYPE_IO_THUMBNAIL_SIZE (fastiv_io_thumbnail_size_get_type()) + +typedef struct _FastivIoThumbnailSizeInfo { + FastivIoThumbnailSize size; ///< Nominal size in pixels + const char *directory_name; ///< thumbnail-spec directory name +} FastivIoThumbnailSizeInfo; + +// The array is null-terminated. +extern FastivIoThumbnailSizeInfo fastiv_io_thumbnail_sizes[]; + cairo_surface_t *fastiv_io_open(const gchar *path, GError **error); cairo_surface_t *fastiv_io_open_from_data( const char *data, size_t len, const gchar *path, GError **error); -cairo_surface_t *fastiv_io_lookup_thumbnail(const gchar *target); +cairo_surface_t *fastiv_io_lookup_thumbnail( + const gchar *target, FastivIoThumbnailSize size); diff --git a/fastiv.c b/fastiv.c index aa6e3e7..bfc8432 100644 --- a/fastiv.c +++ b/fastiv.c @@ -71,6 +71,8 @@ struct { GtkWidget *browser_scroller; GtkWidget *browser_paned; GtkWidget *browser_sidebar; + GtkWidget *plus; + GtkWidget *minus; } g; static gboolean @@ -330,6 +332,35 @@ on_open_location(G_GNUC_UNUSED GtkPlacesSidebar *sidebar, GFile *location, } } +static void +on_toolbar_zoom(G_GNUC_UNUSED GtkButton *button, gpointer user_data) +{ + FastivIoThumbnailSize size = FASTIV_IO_THUMBNAIL_SIZE_MIN; + g_object_get(g.browser, "thumbnail-size", &size, NULL); + + gintptr position = -1, count = 0, adjustment = (gintptr) user_data; + while (fastiv_io_thumbnail_sizes[count].size) { + if (fastiv_io_thumbnail_sizes[count].size == size) + position = count; + count++; + } + + position += adjustment; + g_return_if_fail(position >= 0 && position < count); + g_object_set(g.browser, "thumbnail-size", + fastiv_io_thumbnail_sizes[position].size, NULL); +} + +static void +on_notify_thumbnail_size( + GObject *object, GParamSpec *param_spec, G_GNUC_UNUSED gpointer user_data) +{ + FastivIoThumbnailSize size = 0; + g_object_get(object, g_param_spec_get_name(param_spec), &size, NULL); + gtk_widget_set_sensitive(g.plus, size < FASTIV_IO_THUMBNAIL_SIZE_MAX); + gtk_widget_set_sensitive(g.minus, size > FASTIV_IO_THUMBNAIL_SIZE_MIN); +} + // Cursor keys, e.g., simply cannot be bound through accelerators // (and GtkWidget::keynav-failed would arguably be an awful solution). // @@ -559,27 +590,30 @@ main(int argc, char *argv[]) g_signal_connect(g.browser_sidebar, "open-location", G_CALLBACK(on_open_location), NULL); - GtkWidget *plus = gtk_button_new_from_icon_name("zoom-in-symbolic", + g.plus = gtk_button_new_from_icon_name("zoom-in-symbolic", GTK_ICON_SIZE_BUTTON); - gtk_widget_set_tooltip_text(plus, "Larger thumbnails"); - GtkWidget *minus = gtk_button_new_from_icon_name("zoom-out-symbolic", + gtk_widget_set_tooltip_text(g.plus, "Larger thumbnails"); + g_signal_connect(g.plus, "clicked", + G_CALLBACK(on_toolbar_zoom), (gpointer) +1); + + g.minus = gtk_button_new_from_icon_name("zoom-out-symbolic", GTK_ICON_SIZE_BUTTON); - gtk_widget_set_tooltip_text(minus, "Smaller thumbnails"); + gtk_widget_set_tooltip_text(g.minus, "Smaller thumbnails"); + g_signal_connect(g.minus, "clicked", + G_CALLBACK(on_toolbar_zoom), (gpointer) -1); GtkWidget *zoom_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); gtk_style_context_add_class( gtk_widget_get_style_context(zoom_group), GTK_STYLE_CLASS_LINKED); - gtk_box_pack_start(GTK_BOX(zoom_group), plus, FALSE, FALSE, 0); - gtk_box_pack_start(GTK_BOX(zoom_group), minus, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(zoom_group), g.plus, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(zoom_group), g.minus, FALSE, FALSE, 0); GtkWidget *funnel = gtk_toggle_button_new(); gtk_container_add(GTK_CONTAINER(funnel), gtk_image_new_from_icon_name("funnel-symbolic", GTK_ICON_SIZE_BUTTON)); gtk_widget_set_tooltip_text(funnel, "Hide unsupported files"); - - // TODO(p): Implement these as well. - gtk_widget_set_sensitive(plus, FALSE); - gtk_widget_set_sensitive(minus, FALSE); + g_signal_connect(funnel, "toggled", + G_CALLBACK(on_filtering_toggled), NULL); GtkBox *toolbar = fastiv_sidebar_get_toolbar(FASTIV_SIDEBAR(g.browser_sidebar)); @@ -611,7 +645,9 @@ main(int argc, char *argv[]) g.supported_globs = extract_mime_globs((const char **) types); g_strfreev(types); - g_signal_connect(funnel, "toggled", G_CALLBACK(on_filtering_toggled), NULL); + g_signal_connect(g.browser, "notify::thumbnail-size", + G_CALLBACK(on_notify_thumbnail_size), NULL); + on_toolbar_zoom(NULL, (gpointer) 0); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(funnel), TRUE); g.files = g_ptr_array_new_full(16, g_free); -- cgit v1.2.3-70-g09d2