2021-10-17 15:38:27 +02:00
|
|
|
//
|
2022-01-05 04:42:01 +01:00
|
|
|
// fiv-browser.c: filesystem browsing widget
|
2021-10-17 15:38:27 +02:00
|
|
|
//
|
2023-03-30 20:52:57 +02:00
|
|
|
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
|
2021-10-17 15:38:27 +02:00
|
|
|
//
|
|
|
|
// Permission to use, copy, modify, and/or distribute this software for any
|
|
|
|
// purpose with or without fee is hereby granted.
|
|
|
|
//
|
|
|
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
|
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
|
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
|
|
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
|
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
|
|
|
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
|
|
|
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
//
|
|
|
|
|
2022-07-11 04:09:49 +02:00
|
|
|
#include "config.h"
|
|
|
|
|
2021-10-17 15:38:27 +02:00
|
|
|
#include <math.h>
|
2021-11-03 12:05:43 +01:00
|
|
|
#include <pixman.h>
|
2021-10-17 15:38:27 +02:00
|
|
|
|
2022-07-11 04:09:49 +02:00
|
|
|
#include <gtk/gtk.h>
|
|
|
|
#ifdef GDK_WINDOWING_X11
|
|
|
|
#include <gdk/gdkx.h>
|
|
|
|
#endif // GDK_WINDOWING_X11
|
2022-07-23 20:39:19 +02:00
|
|
|
#ifdef GDK_WINDOWING_QUARTZ
|
|
|
|
#include <gdk/gdkquartz.h>
|
|
|
|
#endif // GDK_WINDOWING_QUARTZ
|
2021-12-26 19:41:42 +01:00
|
|
|
|
2021-12-18 06:38:30 +01:00
|
|
|
#include "fiv-browser.h"
|
Support opening collections of files
Implement a process-local VFS to enable grouping together arbitrary
URIs passed via program arguments, DnD, or the file open dialog.
This VFS contains FivCollectionFile objects, which act as "simple"
proxies over arbitrary GFiles. Their true URIs may be retrieved
through the "standard::target-uri" attribute, in a similar way to
GVfs's "recent" and "trash" backends.
(The main reason we proxy rather than just hackishly return foreign
GFiles from the VFS is that loading them would switch the current
directory, and break iteration as a result.
We could also keep the collection outside of GVfs, but that would
result in considerable special-casing, and the author wouldn't gain
intimate knowledge of GIO.)
There is no perceived need to keep old collections when opening
new ones, so we simply change and reload the contents when needed.
Similarly, there is no intention to make the VFS writeable.
The process-locality of this and other URI schemes has proven to be
rather annoying when passing files to other applications,
however most of the resulting complexity appears to be essential
rather than accidental.
Note that the GTK+ file chooser widget is retarded, and doesn't
recognize URIs that lack the authority part in the location bar.
2022-07-28 00:37:36 +02:00
|
|
|
#include "fiv-collection.h"
|
2022-07-04 20:16:18 +02:00
|
|
|
#include "fiv-context-menu.h"
|
2021-12-18 06:38:30 +01:00
|
|
|
#include "fiv-io.h"
|
2023-05-30 10:36:11 +02:00
|
|
|
#include "fiv-io-model.h"
|
2021-12-28 19:58:14 +01:00
|
|
|
#include "fiv-thumbnail.h"
|
2021-10-17 15:38:27 +02:00
|
|
|
|
2021-11-04 19:35:08 +01:00
|
|
|
// --- Widget ------------------------------------------------------------------
|
2021-11-08 08:13:07 +01:00
|
|
|
// _________________________________
|
|
|
|
// │ p a d d i n g
|
|
|
|
// │ p ╭───────────────────╮ s ╭┄┄┄┄┄
|
|
|
|
// │ a │ glow border ┊ │ p ┊
|
|
|
|
// │ d │ ┄ ╔═══════════╗ ┄ │ a ┊
|
|
|
|
// │ d │ ║ thumbnail ║ │ c ┊ ...
|
|
|
|
// │ i │ ┄ ╚═══════════╝ ┄ │ i ┊
|
|
|
|
// │ n │ ┊ glow border │ n ┊
|
|
|
|
// │ g ╰───────────────────╯ g ╰┄┄┄┄┄
|
|
|
|
// │ s p a c i n g
|
2023-03-30 20:52:57 +02:00
|
|
|
// │ l a b e l
|
|
|
|
// │ s p a c i n g
|
2021-11-08 08:13:07 +01:00
|
|
|
// │ ╭┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄
|
|
|
|
//
|
2021-11-10 17:58:27 +01:00
|
|
|
// The glow is actually a glowing margin, the border is rendered in two parts.
|
2023-03-30 20:52:57 +02:00
|
|
|
// When labels are hidden, the surrounding spacing is collapsed.
|
2021-11-10 17:58:27 +01:00
|
|
|
//
|
2021-11-04 19:35:08 +01:00
|
|
|
|
2022-01-08 07:44:39 +01:00
|
|
|
typedef struct entry Entry;
|
|
|
|
typedef struct item Item;
|
|
|
|
typedef struct row Row;
|
|
|
|
|
2022-01-10 17:54:41 +01:00
|
|
|
typedef struct {
|
2022-01-08 07:44:39 +01:00
|
|
|
FivBrowser *self; ///< Parent browser
|
|
|
|
Entry *target; ///< Currently processed Entry pointer
|
|
|
|
GSubprocess *minion; ///< A slave for the current queue head
|
|
|
|
GCancellable *cancel; ///< Cancellable handle
|
|
|
|
} Thumbnailer;
|
|
|
|
|
2021-12-18 06:38:30 +01:00
|
|
|
struct _FivBrowser {
|
2021-11-04 19:35:08 +01:00
|
|
|
GtkWidget parent_instance;
|
2022-01-09 10:09:06 +01:00
|
|
|
GtkAdjustment *hadjustment; ///< GtkScrollable boilerplate
|
|
|
|
GtkAdjustment *vadjustment; ///< GtkScrollable boilerplate
|
|
|
|
GtkScrollablePolicy hscroll_policy; ///< GtkScrollable boilerplate
|
|
|
|
GtkScrollablePolicy vscroll_policy; ///< GtkScrollable boilerplate
|
2021-11-04 19:35:08 +01:00
|
|
|
|
2021-12-28 19:58:14 +01:00
|
|
|
FivThumbnailSize item_size; ///< Thumbnail size
|
2021-11-21 20:46:50 +01:00
|
|
|
int item_height; ///< Thumbnail height in pixels
|
2021-11-22 15:08:34 +01:00
|
|
|
int item_spacing; ///< Space between items in pixels
|
2021-11-21 16:03:54 +01:00
|
|
|
|
2023-03-30 20:52:57 +02:00
|
|
|
gboolean show_labels; ///< Show labels underneath items
|
|
|
|
|
2021-12-31 02:19:17 +01:00
|
|
|
FivIoModel *model; ///< Filesystem model
|
2023-05-30 10:36:11 +02:00
|
|
|
GPtrArray *entries; ///< []*Entry
|
2021-12-27 23:19:17 +01:00
|
|
|
GArray *layouted_rows; ///< []Row
|
2022-01-10 17:54:41 +01:00
|
|
|
const Entry *selected; ///< Selected entry or NULL
|
2021-11-08 08:13:07 +01:00
|
|
|
|
2022-02-22 15:35:32 +01:00
|
|
|
guint tracked_button; ///< Pressed mouse button number or 0
|
|
|
|
double drag_begin_x; ///< Viewport start X coordinate or -1
|
|
|
|
double drag_begin_y; ///< Viewport start Y coordinate or -1
|
|
|
|
|
2022-06-04 16:28:18 +02:00
|
|
|
GHashTable *thumbnail_cache; ///< [URI]cairo_surface_t, for item_size
|
|
|
|
|
2022-01-08 07:44:39 +01:00
|
|
|
Thumbnailer *thumbnailers; ///< Parallelized thumbnailers
|
|
|
|
size_t thumbnailers_len; ///< Thumbnailers array size
|
2022-08-09 15:53:21 +02:00
|
|
|
GQueue thumbnailers_queue; ///< Queued up Entry pointers
|
2021-12-26 19:41:42 +01:00
|
|
|
|
2021-11-13 13:40:10 +01:00
|
|
|
GdkCursor *pointer; ///< Cached pointer cursor
|
2022-07-23 20:39:19 +02:00
|
|
|
cairo_pattern_t *glow; ///< CAIRO_FORMAT_A8 mask for corners
|
|
|
|
cairo_pattern_t *glow_padded; ///< CAIRO_FORMAT_A8 mask
|
|
|
|
int glow_w; ///< Glow corner width
|
|
|
|
int glow_h; ///< Glow corner height
|
2021-11-10 17:58:27 +01:00
|
|
|
int item_border_x; ///< L/R .item margin + border
|
|
|
|
int item_border_y; ///< T/B .item margin + border
|
2021-11-04 19:35:08 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
2021-10-17 15:38:27 +02:00
|
|
|
|
2022-06-04 15:24:27 +02:00
|
|
|
/// The "last modified" timestamp of source images for thumbnails.
|
|
|
|
static cairo_user_data_key_t fiv_browser_key_mtime_msec;
|
2023-04-14 03:52:01 +02:00
|
|
|
/// The original file size of source images for thumbnails.
|
|
|
|
static cairo_user_data_key_t fiv_browser_key_filesize;
|
2022-06-04 15:24:27 +02:00
|
|
|
|
2021-10-17 15:38:27 +02:00
|
|
|
struct entry {
|
2023-04-14 07:11:49 +02:00
|
|
|
FivIoModelEntry *e; ///< Reference to model entry
|
2021-11-04 19:35:08 +01:00
|
|
|
cairo_surface_t *thumbnail; ///< Prescaled thumbnail
|
2021-11-20 12:19:09 +01:00
|
|
|
GIcon *icon; ///< If no thumbnail, use this icon
|
2023-05-30 10:36:11 +02:00
|
|
|
|
|
|
|
gboolean removed; ///< Model announced removal
|
2021-10-17 15:38:27 +02:00
|
|
|
};
|
|
|
|
|
2023-05-30 10:36:11 +02:00
|
|
|
static Entry *
|
|
|
|
entry_new(FivIoModelEntry *e)
|
|
|
|
{
|
|
|
|
Entry *self = g_slice_alloc0(sizeof *self);
|
|
|
|
self->e = e;
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
2021-10-17 15:38:27 +02:00
|
|
|
static void
|
2023-05-30 10:36:11 +02:00
|
|
|
entry_destroy(Entry *self)
|
2021-10-17 15:38:27 +02:00
|
|
|
{
|
2023-04-14 07:11:49 +02:00
|
|
|
fiv_io_model_entry_unref(self->e);
|
2021-11-23 17:12:31 +01:00
|
|
|
g_clear_pointer(&self->thumbnail, cairo_surface_destroy);
|
2021-11-20 12:19:09 +01:00
|
|
|
g_clear_object(&self->icon);
|
2023-05-30 10:36:11 +02:00
|
|
|
g_slice_free1(sizeof *self, self);
|
2021-10-17 15:38:27 +02:00
|
|
|
}
|
|
|
|
|
2021-11-04 19:35:08 +01:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
2021-10-17 15:38:27 +02:00
|
|
|
|
2021-11-04 19:35:08 +01:00
|
|
|
struct item {
|
|
|
|
const Entry *entry;
|
2023-03-30 20:52:57 +02:00
|
|
|
PangoLayout *label; ///< Label
|
|
|
|
int x_offset; ///< X offset within the row
|
2021-11-04 19:35:08 +01:00
|
|
|
};
|
2021-10-17 15:38:27 +02:00
|
|
|
|
2021-11-04 19:35:08 +01:00
|
|
|
struct row {
|
|
|
|
Item *items; ///< Ends with a NULL entry
|
2022-01-11 14:33:35 +01:00
|
|
|
gsize len; ///< Length of items
|
2021-11-10 17:58:27 +01:00
|
|
|
int x_offset; ///< Start position outside borders
|
|
|
|
int y_offset; ///< Start position inside borders
|
2021-10-17 15:38:27 +02:00
|
|
|
};
|
|
|
|
|
2021-11-04 19:35:08 +01:00
|
|
|
static void
|
|
|
|
row_free(Row *self)
|
|
|
|
{
|
2023-03-30 20:52:57 +02:00
|
|
|
for (gsize i = 0; i < self->len; i++)
|
|
|
|
g_clear_object(&self->items[i].label);
|
2021-11-04 19:35:08 +01:00
|
|
|
g_free(self->items);
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2023-03-30 20:52:57 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-11-04 19:35:08 +01:00
|
|
|
static void
|
2021-12-18 06:38:30 +01:00
|
|
|
append_row(FivBrowser *self, int *y, int x, GArray *items_array)
|
2021-11-04 19:35:08 +01:00
|
|
|
{
|
|
|
|
if (self->layouted_rows->len)
|
2021-11-22 15:08:34 +01:00
|
|
|
*y += self->item_spacing;
|
2021-11-04 19:35:08 +01:00
|
|
|
|
2021-11-10 17:58:27 +01:00
|
|
|
*y += self->item_border_y;
|
2022-01-11 14:33:35 +01:00
|
|
|
Row row = {.x_offset = x, .y_offset = *y};
|
|
|
|
row.items = g_array_steal(items_array, &row.len),
|
|
|
|
g_array_append_val(self->layouted_rows, row);
|
2021-11-04 19:35:08 +01:00
|
|
|
|
|
|
|
// Not trying to pack them vertically, but this would be the place to do it.
|
2021-11-21 20:46:50 +01:00
|
|
|
*y += self->item_height;
|
2021-11-10 17:58:27 +01:00
|
|
|
*y += self->item_border_y;
|
2023-03-30 20:52:57 +02:00
|
|
|
*y += row_subheight(self, &row);
|
2021-11-04 19:35:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
2021-12-18 06:38:30 +01:00
|
|
|
relayout(FivBrowser *self, int width)
|
2021-11-04 19:35:08 +01:00
|
|
|
{
|
|
|
|
GtkWidget *widget = GTK_WIDGET(self);
|
2021-11-10 17:58:27 +01:00
|
|
|
GtkStyleContext *style = gtk_widget_get_style_context(widget);
|
2021-11-04 19:35:08 +01:00
|
|
|
|
|
|
|
GtkBorder padding = {};
|
2021-11-10 17:58:27 +01:00
|
|
|
gtk_style_context_get_padding(style, GTK_STATE_FLAG_NORMAL, &padding);
|
2022-07-21 15:31:47 +02:00
|
|
|
int available_width = width - padding.left - padding.right, max_width = 0;
|
2021-11-04 19:35:08 +01:00
|
|
|
|
|
|
|
g_array_set_size(self->layouted_rows, 0);
|
2022-02-24 21:48:26 +01:00
|
|
|
// Whatever self->drag_begin_* used to point at might no longer be there,
|
|
|
|
// but thumbnail reloading would disrupt mouse clicks if we cleared them.
|
2021-11-04 19:35:08 +01:00
|
|
|
|
|
|
|
GArray *items = g_array_new(TRUE, TRUE, sizeof(Item));
|
|
|
|
int x = 0, y = padding.top;
|
|
|
|
for (guint i = 0; i < self->entries->len; i++) {
|
2023-05-30 10:36:11 +02:00
|
|
|
const Entry *entry = self->entries->pdata[i];
|
2021-11-04 19:35:08 +01:00
|
|
|
if (!entry->thumbnail)
|
|
|
|
continue;
|
|
|
|
|
2021-11-10 17:58:27 +01:00
|
|
|
int width = cairo_image_surface_get_width(entry->thumbnail) +
|
|
|
|
2 * self->item_border_x;
|
2021-11-04 19:35:08 +01:00
|
|
|
if (!items->len) {
|
|
|
|
// Just insert it, whether or not there's any space.
|
2021-11-22 15:08:34 +01:00
|
|
|
} else if (x + self->item_spacing + width <= available_width) {
|
|
|
|
x += self->item_spacing;
|
2021-11-04 19:35:08 +01:00
|
|
|
} else {
|
|
|
|
append_row(self, &y,
|
|
|
|
padding.left + MAX(0, available_width - x) / 2, items);
|
|
|
|
x = 0;
|
|
|
|
}
|
|
|
|
|
2023-03-30 20:52:57 +02:00
|
|
|
PangoLayout *label = NULL;
|
|
|
|
if (self->show_labels) {
|
2023-04-14 07:11:49 +02:00
|
|
|
label = gtk_widget_create_pango_layout(
|
|
|
|
widget, entry->e->display_name);
|
2023-03-30 20:52:57 +02:00
|
|
|
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);
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
g_array_append_val(items, ((Item) {
|
|
|
|
.entry = entry,
|
|
|
|
.label = label,
|
|
|
|
.x_offset = x + self->item_border_x,
|
|
|
|
}));
|
2022-07-21 15:31:47 +02:00
|
|
|
|
2021-11-04 19:35:08 +01:00
|
|
|
x += width;
|
2022-07-21 15:31:47 +02:00
|
|
|
if (max_width < width)
|
|
|
|
max_width = width;
|
2021-11-04 19:35:08 +01:00
|
|
|
}
|
|
|
|
if (items->len) {
|
|
|
|
append_row(self, &y,
|
|
|
|
padding.left + MAX(0, available_width - x) / 2, items);
|
|
|
|
}
|
|
|
|
|
|
|
|
g_array_free(items, TRUE);
|
2022-01-09 10:09:06 +01:00
|
|
|
int total_height = y + padding.bottom;
|
|
|
|
if (self->hadjustment) {
|
2022-07-17 13:47:16 +02:00
|
|
|
gtk_adjustment_set_lower(self->hadjustment, 0);
|
2022-07-21 15:31:47 +02:00
|
|
|
gtk_adjustment_set_upper(self->hadjustment,
|
|
|
|
width + MAX(0, max_width - available_width));
|
2022-07-17 13:47:16 +02:00
|
|
|
gtk_adjustment_set_step_increment(self->hadjustment, width * 0.1);
|
|
|
|
gtk_adjustment_set_page_increment(self->hadjustment, width * 0.9);
|
|
|
|
gtk_adjustment_set_page_size(self->hadjustment, width);
|
2022-01-09 10:09:06 +01:00
|
|
|
}
|
|
|
|
if (self->vadjustment) {
|
|
|
|
gtk_adjustment_set_lower(self->vadjustment, 0);
|
|
|
|
gtk_adjustment_set_upper(self->vadjustment, total_height);
|
|
|
|
gtk_adjustment_set_step_increment(self->vadjustment,
|
|
|
|
self->item_height + self->item_spacing + 2 * self->item_border_y);
|
2022-07-16 16:49:47 +02:00
|
|
|
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));
|
2022-01-09 10:09:06 +01:00
|
|
|
}
|
|
|
|
return total_height;
|
2021-11-04 19:35:08 +01:00
|
|
|
}
|
|
|
|
|
2021-11-08 08:13:07 +01:00
|
|
|
static void
|
2021-12-18 06:38:30 +01:00
|
|
|
draw_outer_border(FivBrowser *self, cairo_t *cr, int width, int height)
|
2021-11-08 08:13:07 +01:00
|
|
|
{
|
|
|
|
cairo_matrix_t matrix;
|
|
|
|
|
|
|
|
cairo_save(cr);
|
2022-07-23 20:39:19 +02:00
|
|
|
cairo_translate(cr, -self->glow_w, -self->glow_h);
|
|
|
|
cairo_rectangle(cr, 0, 0, self->glow_w + width, self->glow_h + height);
|
2021-11-08 08:13:07 +01:00
|
|
|
cairo_clip(cr);
|
2022-07-23 20:39:19 +02:00
|
|
|
cairo_mask(cr, self->glow_padded);
|
2021-11-08 08:13:07 +01:00
|
|
|
cairo_restore(cr);
|
|
|
|
cairo_save(cr);
|
2022-07-23 20:39:19 +02:00
|
|
|
cairo_translate(cr, width + self->glow_w, height + self->glow_h);
|
|
|
|
cairo_rectangle(cr, 0, 0, -self->glow_w - width, -self->glow_h - height);
|
2021-11-08 08:13:07 +01:00
|
|
|
cairo_clip(cr);
|
2021-11-09 03:07:24 +01:00
|
|
|
cairo_scale(cr, -1, -1);
|
2022-07-23 20:39:19 +02:00
|
|
|
cairo_mask(cr, self->glow_padded);
|
2021-11-08 08:13:07 +01:00
|
|
|
cairo_restore(cr);
|
|
|
|
|
|
|
|
cairo_matrix_init_scale(&matrix, -1, 1);
|
2022-07-23 20:39:19 +02:00
|
|
|
cairo_matrix_translate(&matrix, -width - self->glow_w, self->glow_h);
|
|
|
|
cairo_pattern_set_matrix(self->glow, &matrix);
|
|
|
|
cairo_mask(cr, self->glow);
|
2021-11-08 08:13:07 +01:00
|
|
|
cairo_matrix_init_scale(&matrix, 1, -1);
|
2022-07-23 20:39:19 +02:00
|
|
|
cairo_matrix_translate(&matrix, self->glow_w, -height - self->glow_h);
|
|
|
|
cairo_pattern_set_matrix(self->glow, &matrix);
|
|
|
|
cairo_mask(cr, self->glow);
|
2021-11-08 08:13:07 +01:00
|
|
|
}
|
|
|
|
|
2021-11-08 08:00:18 +01:00
|
|
|
static GdkRectangle
|
2021-12-18 06:38:30 +01:00
|
|
|
item_extents(FivBrowser *self, const Item *item, const Row *row)
|
2021-11-08 08:00:18 +01:00
|
|
|
{
|
|
|
|
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,
|
2022-07-31 04:33:39 +02:00
|
|
|
.y = row->y_offset + (self->item_height - height) / 2,
|
2021-11-08 08:00:18 +01:00
|
|
|
.width = width,
|
|
|
|
.height = height,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
static const Entry *
|
2021-12-18 06:38:30 +01:00
|
|
|
entry_at(FivBrowser *self, int x, int y)
|
2021-11-08 08:00:18 +01:00
|
|
|
{
|
2022-02-22 15:35:32 +01:00
|
|
|
if (self->hadjustment)
|
|
|
|
x += round(gtk_adjustment_get_value(self->hadjustment));
|
2022-01-09 10:09:06 +01:00
|
|
|
if (self->vadjustment)
|
2022-01-10 17:54:41 +01:00
|
|
|
y += round(gtk_adjustment_get_value(self->vadjustment));
|
2022-01-09 10:09:06 +01:00
|
|
|
|
2021-11-08 08:00:18 +01:00
|
|
|
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++) {
|
2021-11-21 16:03:54 +01:00
|
|
|
GdkRectangle extents = item_extents(self, item, row);
|
2021-11-08 08:00:18 +01:00
|
|
|
if (x >= extents.x &&
|
|
|
|
y >= extents.y &&
|
|
|
|
x <= extents.x + extents.width &&
|
|
|
|
y <= extents.y + extents.height)
|
|
|
|
return item->entry;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2022-01-11 12:24:49 +01:00
|
|
|
static GdkRectangle
|
|
|
|
entry_rect(FivBrowser *self, const Entry *entry)
|
|
|
|
{
|
|
|
|
GdkRectangle rect = {};
|
|
|
|
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++) {
|
|
|
|
if (item->entry == entry) {
|
|
|
|
rect = item_extents(self, item, row);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-02-22 15:35:32 +01:00
|
|
|
if (self->hadjustment)
|
|
|
|
rect.x -= round(gtk_adjustment_get_value(self->hadjustment));
|
2022-01-11 12:24:49 +01:00
|
|
|
if (self->vadjustment)
|
|
|
|
rect.y -= round(gtk_adjustment_get_value(self->vadjustment));
|
|
|
|
return rect;
|
|
|
|
}
|
|
|
|
|
2021-11-09 03:07:24 +01:00
|
|
|
static void
|
2021-12-18 06:38:30 +01:00
|
|
|
draw_row(FivBrowser *self, cairo_t *cr, const Row *row)
|
2021-11-09 03:07:24 +01:00
|
|
|
{
|
2021-11-10 17:58:27 +01:00
|
|
|
GtkStyleContext *style = gtk_widget_get_style_context(GTK_WIDGET(self));
|
|
|
|
gtk_style_context_save(style);
|
|
|
|
gtk_style_context_add_class(style, "item");
|
|
|
|
|
|
|
|
GtkBorder border;
|
2022-01-10 17:54:41 +01:00
|
|
|
GtkStateFlags common_state = gtk_style_context_get_state(style);
|
|
|
|
gtk_style_context_get_border(style, common_state, &border);
|
2021-11-09 03:07:24 +01:00
|
|
|
for (Item *item = row->items; item->entry; item++) {
|
2021-11-10 17:58:27 +01:00
|
|
|
cairo_save(cr);
|
2021-11-21 16:03:54 +01:00
|
|
|
GdkRectangle extents = item_extents(self, item, row);
|
2021-11-10 17:58:27 +01:00
|
|
|
cairo_translate(cr, extents.x - border.left, extents.y - border.top);
|
2021-11-09 03:07:24 +01:00
|
|
|
|
2022-01-10 17:54:41 +01:00
|
|
|
GtkStateFlags state = common_state;
|
|
|
|
if (item->entry == self->selected)
|
|
|
|
state |= GTK_STATE_FLAG_SELECTED;
|
|
|
|
|
2021-11-20 12:19:09 +01:00
|
|
|
gtk_style_context_save(style);
|
2022-01-10 17:54:41 +01:00
|
|
|
gtk_style_context_set_state(style, state);
|
2021-11-20 12:19:09 +01:00
|
|
|
if (item->entry->icon) {
|
|
|
|
gtk_style_context_add_class(style, "symbolic");
|
|
|
|
} else {
|
2022-01-10 17:54:41 +01:00
|
|
|
GdkRGBA glow_color = {};
|
|
|
|
gtk_style_context_get_color(style, state, &glow_color);
|
2021-11-20 12:19:09 +01:00
|
|
|
gdk_cairo_set_source_rgba(cr, &glow_color);
|
|
|
|
draw_outer_border(self, cr,
|
|
|
|
border.left + extents.width + border.right,
|
|
|
|
border.top + extents.height + border.bottom);
|
|
|
|
}
|
2021-11-09 03:07:24 +01:00
|
|
|
|
2021-12-20 04:29:19 +01:00
|
|
|
// Performance optimization--specifically targeting the checkerboard.
|
|
|
|
if (cairo_image_surface_get_format(item->entry->thumbnail) !=
|
2023-06-01 18:17:20 +02:00
|
|
|
CAIRO_FORMAT_RGB24 || item->entry->removed) {
|
2021-12-20 04:29:19 +01:00
|
|
|
gtk_render_background(style, cr, border.left, border.top,
|
|
|
|
extents.width, extents.height);
|
|
|
|
}
|
2021-11-10 17:58:27 +01:00
|
|
|
|
|
|
|
gtk_render_frame(style, cr, 0, 0,
|
|
|
|
border.left + extents.width + border.right,
|
|
|
|
border.top + extents.height + border.bottom);
|
2021-11-09 03:07:24 +01:00
|
|
|
|
2021-11-20 12:19:09 +01:00
|
|
|
if (item->entry->icon) {
|
|
|
|
GdkRGBA color = {};
|
|
|
|
gtk_style_context_get_color(style, state, &color);
|
|
|
|
gdk_cairo_set_source_rgba(cr, &color);
|
|
|
|
cairo_mask_surface(
|
|
|
|
cr, item->entry->thumbnail, border.left, border.top);
|
|
|
|
} else {
|
2023-06-01 18:17:20 +02:00
|
|
|
// Distinguish removed items by rendering them only faintly.
|
|
|
|
if (item->entry->removed)
|
|
|
|
cairo_push_group(cr);
|
|
|
|
|
2021-11-20 12:19:09 +01:00
|
|
|
cairo_set_source_surface(
|
|
|
|
cr, item->entry->thumbnail, border.left, border.top);
|
|
|
|
cairo_paint(cr);
|
2022-01-10 17:54:41 +01:00
|
|
|
|
2023-06-01 18:17:20 +02:00
|
|
|
// Here, we could also consider multiplying
|
2022-01-10 17:54:41 +01:00
|
|
|
// the whole rectangle with the selection color.
|
2023-06-01 18:17:20 +02:00
|
|
|
if (item->entry->removed) {
|
|
|
|
cairo_pop_group_to_source(cr);
|
|
|
|
cairo_paint_with_alpha(cr, 0.25);
|
|
|
|
}
|
2021-11-20 12:19:09 +01:00
|
|
|
}
|
|
|
|
|
2023-06-01 18:17:20 +02:00
|
|
|
// 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.
|
2023-05-30 10:36:11 +02:00
|
|
|
if (item->entry->removed) {
|
2023-06-01 18:17:20 +02:00
|
|
|
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);
|
|
|
|
}
|
2023-05-30 10:36:11 +02:00
|
|
|
}
|
|
|
|
|
2023-03-30 20:52:57 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2021-11-09 03:07:24 +01:00
|
|
|
cairo_restore(cr);
|
2021-11-20 12:19:09 +01:00
|
|
|
gtk_style_context_restore(style);
|
2021-11-09 03:07:24 +01:00
|
|
|
}
|
2021-11-10 17:58:27 +01:00
|
|
|
gtk_style_context_restore(style);
|
2021-11-09 03:07:24 +01:00
|
|
|
}
|
|
|
|
|
2021-11-21 16:03:54 +01:00
|
|
|
// --- 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;
|
2021-12-28 19:58:14 +01:00
|
|
|
if (width > FIV_THUMBNAIL_WIDE_COEFFICIENT * height) {
|
|
|
|
scale_x = FIV_THUMBNAIL_WIDE_COEFFICIENT * row_height / width;
|
2021-11-21 16:03:54 +01:00
|
|
|
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);
|
2021-12-20 04:29:19 +01:00
|
|
|
cairo_format_t cairo_format = cairo_image_surface_get_format(thumbnail);
|
2021-11-21 16:03:54 +01:00
|
|
|
cairo_surface_t *scaled = cairo_image_surface_create(
|
2021-12-20 04:29:19 +01:00
|
|
|
cairo_format, projected_width, projected_height);
|
2021-11-21 16:03:54 +01:00
|
|
|
|
|
|
|
// 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
|
2021-12-20 04:29:19 +01:00
|
|
|
pixman_format_code_t format =
|
|
|
|
cairo_format == CAIRO_FORMAT_RGB24 ? PIXMAN_x8r8g8b8 : PIXMAN_a8r8g8b8;
|
2021-11-21 16:03:54 +01:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
2021-12-27 23:51:38 +01:00
|
|
|
cairo_surface_set_user_data(
|
2021-12-28 19:58:14 +01:00
|
|
|
scaled, &fiv_thumbnail_key_lq, (void *) (intptr_t) 1, NULL);
|
2021-11-21 16:03:54 +01:00
|
|
|
cairo_surface_destroy(thumbnail);
|
|
|
|
cairo_surface_mark_dirty(scaled);
|
|
|
|
return scaled;
|
|
|
|
}
|
|
|
|
|
2023-04-14 07:11:49 +02:00
|
|
|
static const char *
|
Support opening collections of files
Implement a process-local VFS to enable grouping together arbitrary
URIs passed via program arguments, DnD, or the file open dialog.
This VFS contains FivCollectionFile objects, which act as "simple"
proxies over arbitrary GFiles. Their true URIs may be retrieved
through the "standard::target-uri" attribute, in a similar way to
GVfs's "recent" and "trash" backends.
(The main reason we proxy rather than just hackishly return foreign
GFiles from the VFS is that loading them would switch the current
directory, and break iteration as a result.
We could also keep the collection outside of GVfs, but that would
result in considerable special-casing, and the author wouldn't gain
intimate knowledge of GIO.)
There is no perceived need to keep old collections when opening
new ones, so we simply change and reload the contents when needed.
Similarly, there is no intention to make the VFS writeable.
The process-locality of this and other URI schemes has proven to be
rather annoying when passing files to other applications,
however most of the resulting complexity appears to be essential
rather than accidental.
Note that the GTK+ file chooser widget is retarded, and doesn't
recognize URIs that lack the authority part in the location bar.
2022-07-28 00:37:36 +02:00
|
|
|
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.
|
2023-04-14 07:11:49 +02:00
|
|
|
if (self->e->target_uri && fiv_collection_uri_matches(self->e->uri))
|
|
|
|
return self->e->target_uri;
|
Support opening collections of files
Implement a process-local VFS to enable grouping together arbitrary
URIs passed via program arguments, DnD, or the file open dialog.
This VFS contains FivCollectionFile objects, which act as "simple"
proxies over arbitrary GFiles. Their true URIs may be retrieved
through the "standard::target-uri" attribute, in a similar way to
GVfs's "recent" and "trash" backends.
(The main reason we proxy rather than just hackishly return foreign
GFiles from the VFS is that loading them would switch the current
directory, and break iteration as a result.
We could also keep the collection outside of GVfs, but that would
result in considerable special-casing, and the author wouldn't gain
intimate knowledge of GIO.)
There is no perceived need to keep old collections when opening
new ones, so we simply change and reload the contents when needed.
Similarly, there is no intention to make the VFS writeable.
The process-locality of this and other URI schemes has proven to be
rather annoying when passing files to other applications,
however most of the resulting complexity appears to be essential
rather than accidental.
Note that the GTK+ file chooser widget is retarded, and doesn't
recognize URIs that lack the authority part in the location bar.
2022-07-28 00:37:36 +02:00
|
|
|
|
2023-04-14 07:11:49 +02:00
|
|
|
return self->e->uri;
|
Support opening collections of files
Implement a process-local VFS to enable grouping together arbitrary
URIs passed via program arguments, DnD, or the file open dialog.
This VFS contains FivCollectionFile objects, which act as "simple"
proxies over arbitrary GFiles. Their true URIs may be retrieved
through the "standard::target-uri" attribute, in a similar way to
GVfs's "recent" and "trash" backends.
(The main reason we proxy rather than just hackishly return foreign
GFiles from the VFS is that loading them would switch the current
directory, and break iteration as a result.
We could also keep the collection outside of GVfs, but that would
result in considerable special-casing, and the author wouldn't gain
intimate knowledge of GIO.)
There is no perceived need to keep old collections when opening
new ones, so we simply change and reload the contents when needed.
Similarly, there is no intention to make the VFS writeable.
The process-locality of this and other URI schemes has proven to be
rather annoying when passing files to other applications,
however most of the resulting complexity appears to be essential
rather than accidental.
Note that the GTK+ file chooser widget is retarded, and doesn't
recognize URIs that lack the authority part in the location bar.
2022-07-28 00:37:36 +02:00
|
|
|
}
|
|
|
|
|
2023-04-14 03:52:01 +02:00
|
|
|
static void
|
|
|
|
entry_set_surface_user_data(const Entry *self)
|
|
|
|
{
|
|
|
|
// This choice of mtime favours unnecessary thumbnail reloading
|
|
|
|
// over retaining stale data (consider both calling functions).
|
|
|
|
cairo_surface_set_user_data(self->thumbnail,
|
2023-04-14 07:11:49 +02:00
|
|
|
&fiv_browser_key_mtime_msec, (void *) (intptr_t) self->e->mtime_msec,
|
2023-04-14 03:52:01 +02:00
|
|
|
NULL);
|
|
|
|
cairo_surface_set_user_data(self->thumbnail,
|
2023-04-14 07:11:49 +02:00
|
|
|
&fiv_browser_key_filesize, (void *) (uintptr_t) self->e->filesize,
|
2023-04-14 03:52:01 +02:00
|
|
|
NULL);
|
|
|
|
}
|
|
|
|
|
2023-05-30 10:36:11 +02:00
|
|
|
static cairo_surface_t *
|
|
|
|
entry_lookup_thumbnail(Entry *self, FivBrowser *browser)
|
2021-11-21 16:03:54 +01:00
|
|
|
{
|
2022-06-04 16:28:18 +02:00
|
|
|
cairo_surface_t *cached =
|
2023-04-14 07:11:49 +02:00
|
|
|
g_hash_table_lookup(browser->thumbnail_cache, self->e->uri);
|
2022-06-04 16:28:18 +02:00
|
|
|
if (cached &&
|
2023-04-14 03:52:01 +02:00
|
|
|
(intptr_t) cairo_surface_get_user_data(cached,
|
2023-04-14 07:11:49 +02:00
|
|
|
&fiv_browser_key_mtime_msec) == (intptr_t) self->e->mtime_msec &&
|
2023-04-14 03:52:01 +02:00
|
|
|
(uintptr_t) cairo_surface_get_user_data(cached,
|
2023-04-14 07:11:49 +02:00
|
|
|
&fiv_browser_key_filesize) == (uintptr_t) self->e->filesize) {
|
2022-06-08 01:05:04 +02:00
|
|
|
// 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).
|
2023-05-30 10:36:11 +02:00
|
|
|
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;
|
2022-06-04 16:28:18 +02:00
|
|
|
}
|
|
|
|
|
2023-05-30 10:36:11 +02:00
|
|
|
g_clear_object(&self->icon);
|
|
|
|
g_clear_pointer(&self->thumbnail, cairo_surface_destroy);
|
|
|
|
|
|
|
|
if ((self->thumbnail = entry_lookup_thumbnail(self, browser))) {
|
2023-04-14 03:52:01 +02:00
|
|
|
// Yes, this is a pointless action in case it's been found in the cache.
|
|
|
|
entry_set_surface_user_data(self);
|
2022-06-04 15:06:10 +02:00
|
|
|
return;
|
2022-06-04 15:24:27 +02:00
|
|
|
}
|
2021-11-21 16:03:54 +01:00
|
|
|
|
|
|
|
// Fall back to symbolic icons, though there's only so much we can do
|
|
|
|
// in parallel--GTK+ isn't thread-safe.
|
2023-04-14 07:11:49 +02:00
|
|
|
GFile *file = g_file_new_for_uri(self->e->uri);
|
2021-11-21 16:03:54 +01:00
|
|
|
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);
|
2022-06-04 15:06:10 +02:00
|
|
|
g_object_unref(file);
|
2021-11-21 16:03:54 +01:00
|
|
|
if (info) {
|
|
|
|
GIcon *icon = g_file_info_get_symbolic_icon(info);
|
|
|
|
if (icon)
|
|
|
|
self->icon = g_object_ref(icon);
|
|
|
|
g_object_unref(info);
|
|
|
|
}
|
2022-01-05 07:45:23 +01:00
|
|
|
|
|
|
|
// The GVfs backend may not be friendly.
|
|
|
|
if (!self->icon)
|
|
|
|
self->icon = g_icon_new_for_string("text-x-generic-symbolic", NULL);
|
2021-11-21 16:03:54 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2021-12-18 06:38:30 +01:00
|
|
|
materialize_icon(FivBrowser *self, Entry *entry)
|
2021-11-21 16:03:54 +01:00
|
|
|
{
|
|
|
|
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(
|
2021-11-21 20:46:50 +01:00
|
|
|
gtk_icon_theme_get_default(), entry->icon, self->item_height / 2,
|
2021-11-21 16:03:54 +01:00
|
|
|
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) {
|
2021-11-21 20:46:50 +01:00
|
|
|
int outer_size = self->item_height;
|
2021-11-21 16:03:54 +01:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2023-06-01 20:55:41 +02:00
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
2021-11-21 16:03:54 +01:00
|
|
|
static void
|
2021-12-18 06:38:30 +01:00
|
|
|
reload_thumbnails(FivBrowser *self)
|
2021-11-21 16:03:54 +01:00
|
|
|
{
|
|
|
|
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++)
|
2023-05-30 10:36:11 +02:00
|
|
|
g_thread_pool_push(pool, self->entries->pdata[i], NULL);
|
2021-11-21 16:03:54 +01:00
|
|
|
g_thread_pool_free(pool, FALSE, TRUE);
|
|
|
|
|
2022-06-04 16:28:18 +02:00
|
|
|
// Once a URI disappears from the model, its thumbnail is forgotten.
|
|
|
|
g_hash_table_remove_all(self->thumbnail_cache);
|
2023-06-01 20:55:41 +02:00
|
|
|
for (guint i = 0; i < self->entries->len; i++)
|
|
|
|
reload_one_thumbnail_finish(self, self->entries->pdata[i]);
|
2021-11-21 16:03:54 +01:00
|
|
|
|
|
|
|
gtk_widget_queue_resize(GTK_WIDGET(self));
|
|
|
|
}
|
|
|
|
|
2022-01-08 07:44:39 +01:00
|
|
|
// --- Minion management -------------------------------------------------------
|
2021-12-26 19:41:42 +01:00
|
|
|
|
2022-02-20 19:43:21 +01:00
|
|
|
#if !GLIB_CHECK_VERSION(2, 70, 0)
|
|
|
|
#define g_spawn_check_wait_status g_spawn_check_exit_status
|
|
|
|
#endif
|
|
|
|
|
|
|
|
static gboolean thumbnailer_next(Thumbnailer *t);
|
2021-12-26 19:41:42 +01:00
|
|
|
|
|
|
|
static void
|
2022-02-20 19:43:21 +01:00
|
|
|
thumbnailer_reprocess_entry(FivBrowser *self, GBytes *output, Entry *entry)
|
2021-12-26 19:41:42 +01:00
|
|
|
{
|
2022-02-20 19:43:21 +01:00
|
|
|
g_clear_object(&entry->icon);
|
|
|
|
g_clear_pointer(&entry->thumbnail, cairo_surface_destroy);
|
2022-06-08 01:05:04 +02:00
|
|
|
|
|
|
|
gtk_widget_queue_resize(GTK_WIDGET(self));
|
|
|
|
|
|
|
|
guint64 flags = 0;
|
2022-02-20 19:43:21 +01:00
|
|
|
if (!output || !(entry->thumbnail = rescale_thumbnail(
|
2022-06-08 01:05:04 +02:00
|
|
|
fiv_io_deserialize(output, &flags), self->item_height))) {
|
2022-02-20 19:43:21 +01:00
|
|
|
entry_add_thumbnail(entry, self);
|
|
|
|
materialize_icon(self, entry);
|
2022-06-08 01:05:04 +02:00
|
|
|
return;
|
2022-02-20 19:43:21 +01:00
|
|
|
}
|
2022-06-08 01:05:04 +02:00
|
|
|
if ((flags & FIV_IO_SERIALIZE_LOW_QUALITY)) {
|
|
|
|
cairo_surface_set_user_data(entry->thumbnail, &fiv_thumbnail_key_lq,
|
|
|
|
(void *) (intptr_t) 1, NULL);
|
2022-08-09 15:53:21 +02:00
|
|
|
g_queue_push_tail(&self->thumbnailers_queue, entry);
|
2022-06-08 01:05:04 +02:00
|
|
|
}
|
|
|
|
|
2023-04-14 03:52:01 +02:00
|
|
|
entry_set_surface_user_data(entry);
|
2023-04-14 07:11:49 +02:00
|
|
|
g_hash_table_insert(self->thumbnail_cache, g_strdup(entry->e->uri),
|
2022-06-08 01:05:04 +02:00
|
|
|
cairo_surface_reference(entry->thumbnail));
|
2021-12-26 19:41:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
on_thumbnailer_ready(GObject *object, GAsyncResult *res, gpointer user_data)
|
|
|
|
{
|
|
|
|
GSubprocess *subprocess = G_SUBPROCESS(object);
|
2022-02-20 19:43:21 +01:00
|
|
|
Thumbnailer *t = user_data;
|
2022-01-08 07:44:39 +01:00
|
|
|
|
2022-02-20 19:43:21 +01:00
|
|
|
// Reading out pixel data directly from a thumbnailer serves two purposes:
|
|
|
|
// 1. it avoids pointless delays with large thumbnail sizes,
|
|
|
|
// 2. it enables thumbnailing things that cannot be placed in the cache.
|
2021-12-26 19:41:42 +01:00
|
|
|
GError *error = NULL;
|
2022-02-20 19:43:21 +01:00
|
|
|
GBytes *out = NULL;
|
2022-02-20 21:28:52 +01:00
|
|
|
gboolean succeeded = FALSE;
|
2022-02-20 19:43:21 +01:00
|
|
|
if (!g_subprocess_communicate_finish(subprocess, res, &out, NULL, &error)) {
|
2021-12-28 23:47:36 +01:00
|
|
|
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
|
|
|
|
g_error_free(error);
|
|
|
|
return;
|
|
|
|
}
|
2022-02-20 19:43:21 +01:00
|
|
|
} else if (!g_subprocess_get_if_exited(subprocess)) {
|
|
|
|
// If it exited, it probably printed its own message.
|
|
|
|
g_spawn_check_wait_status(g_subprocess_get_status(subprocess), &error);
|
2022-02-20 21:28:52 +01:00
|
|
|
} else {
|
|
|
|
succeeded = g_subprocess_get_exit_status(subprocess) == EXIT_SUCCESS;
|
2022-02-20 19:43:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
g_warning("%s", error->message);
|
2021-12-26 19:41:42 +01:00
|
|
|
g_error_free(error);
|
|
|
|
}
|
|
|
|
|
2022-02-20 19:43:21 +01:00
|
|
|
g_return_if_fail(subprocess == t->minion);
|
|
|
|
g_clear_object(&t->minion);
|
|
|
|
if (!t->target) {
|
2021-12-26 19:41:42 +01:00
|
|
|
g_warning("finished thumbnailing an unknown image");
|
2022-02-20 19:43:21 +01:00
|
|
|
g_clear_pointer(&out, g_bytes_unref);
|
2021-12-26 19:41:42 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (succeeded)
|
2022-02-20 19:43:21 +01:00
|
|
|
thumbnailer_reprocess_entry(t->self, out, t->target);
|
|
|
|
else
|
|
|
|
g_clear_pointer(&out, g_bytes_unref);
|
2021-12-26 19:41:42 +01:00
|
|
|
|
2022-02-20 19:43:21 +01:00
|
|
|
t->target = NULL;
|
|
|
|
thumbnailer_next(t);
|
2021-12-26 19:41:42 +01:00
|
|
|
}
|
|
|
|
|
2022-01-08 07:44:39 +01:00
|
|
|
static gboolean
|
2022-02-20 19:43:21 +01:00
|
|
|
thumbnailer_next(Thumbnailer *t)
|
2021-12-26 19:41:42 +01:00
|
|
|
{
|
2022-02-20 19:43:21 +01:00
|
|
|
// TODO(p): Try to keep the minions alive (stdout will be a problem).
|
|
|
|
FivBrowser *self = t->self;
|
2022-08-09 15:53:21 +02:00
|
|
|
if (!(t->target = g_queue_pop_head(&self->thumbnailers_queue)))
|
2022-01-08 07:44:39 +01:00
|
|
|
return FALSE;
|
|
|
|
|
2022-06-08 01:05:04 +02:00
|
|
|
// Case analysis:
|
|
|
|
// - We haven't found any thumbnail for the entry at all
|
|
|
|
// (and it has a symbolic icon as a result):
|
|
|
|
// we want to fill the void ASAP, so go for embedded thumbnails first.
|
|
|
|
// - We've found one, but we're not quite happy with it:
|
|
|
|
// always run the full process for a high-quality wide thumbnail.
|
|
|
|
// - We can't end up here in any other cases.
|
Support opening collections of files
Implement a process-local VFS to enable grouping together arbitrary
URIs passed via program arguments, DnD, or the file open dialog.
This VFS contains FivCollectionFile objects, which act as "simple"
proxies over arbitrary GFiles. Their true URIs may be retrieved
through the "standard::target-uri" attribute, in a similar way to
GVfs's "recent" and "trash" backends.
(The main reason we proxy rather than just hackishly return foreign
GFiles from the VFS is that loading them would switch the current
directory, and break iteration as a result.
We could also keep the collection outside of GVfs, but that would
result in considerable special-casing, and the author wouldn't gain
intimate knowledge of GIO.)
There is no perceived need to keep old collections when opening
new ones, so we simply change and reload the contents when needed.
Similarly, there is no intention to make the VFS writeable.
The process-locality of this and other URI schemes has proven to be
rather annoying when passing files to other applications,
however most of the resulting complexity appears to be essential
rather than accidental.
Note that the GTK+ file chooser widget is retarded, and doesn't
recognize URIs that lack the authority part in the location bar.
2022-07-28 00:37:36 +02:00
|
|
|
const char *uri = entry_system_wide_uri(t->target);
|
2022-06-08 01:05:04 +02:00
|
|
|
const char *argv_faster[] = {PROJECT_NAME, "--extract-thumbnail",
|
|
|
|
"--thumbnail", fiv_thumbnail_sizes[self->item_size].thumbnail_spec_name,
|
Support opening collections of files
Implement a process-local VFS to enable grouping together arbitrary
URIs passed via program arguments, DnD, or the file open dialog.
This VFS contains FivCollectionFile objects, which act as "simple"
proxies over arbitrary GFiles. Their true URIs may be retrieved
through the "standard::target-uri" attribute, in a similar way to
GVfs's "recent" and "trash" backends.
(The main reason we proxy rather than just hackishly return foreign
GFiles from the VFS is that loading them would switch the current
directory, and break iteration as a result.
We could also keep the collection outside of GVfs, but that would
result in considerable special-casing, and the author wouldn't gain
intimate knowledge of GIO.)
There is no perceived need to keep old collections when opening
new ones, so we simply change and reload the contents when needed.
Similarly, there is no intention to make the VFS writeable.
The process-locality of this and other URI schemes has proven to be
rather annoying when passing files to other applications,
however most of the resulting complexity appears to be essential
rather than accidental.
Note that the GTK+ file chooser widget is retarded, and doesn't
recognize URIs that lack the authority part in the location bar.
2022-07-28 00:37:36 +02:00
|
|
|
"--", uri, NULL};
|
2022-06-08 01:05:04 +02:00
|
|
|
const char *argv_slower[] = {PROJECT_NAME,
|
|
|
|
"--thumbnail", fiv_thumbnail_sizes[self->item_size].thumbnail_spec_name,
|
Support opening collections of files
Implement a process-local VFS to enable grouping together arbitrary
URIs passed via program arguments, DnD, or the file open dialog.
This VFS contains FivCollectionFile objects, which act as "simple"
proxies over arbitrary GFiles. Their true URIs may be retrieved
through the "standard::target-uri" attribute, in a similar way to
GVfs's "recent" and "trash" backends.
(The main reason we proxy rather than just hackishly return foreign
GFiles from the VFS is that loading them would switch the current
directory, and break iteration as a result.
We could also keep the collection outside of GVfs, but that would
result in considerable special-casing, and the author wouldn't gain
intimate knowledge of GIO.)
There is no perceived need to keep old collections when opening
new ones, so we simply change and reload the contents when needed.
Similarly, there is no intention to make the VFS writeable.
The process-locality of this and other URI schemes has proven to be
rather annoying when passing files to other applications,
however most of the resulting complexity appears to be essential
rather than accidental.
Note that the GTK+ file chooser widget is retarded, and doesn't
recognize URIs that lack the authority part in the location bar.
2022-07-28 00:37:36 +02:00
|
|
|
"--", uri, NULL};
|
2022-06-08 01:05:04 +02:00
|
|
|
|
2021-12-26 19:41:42 +01:00
|
|
|
GError *error = NULL;
|
2022-06-08 01:05:04 +02:00
|
|
|
t->minion = g_subprocess_newv(t->target->icon ? argv_faster : argv_slower,
|
|
|
|
G_SUBPROCESS_FLAGS_STDOUT_PIPE, &error);
|
2021-12-26 19:41:42 +01:00
|
|
|
if (error) {
|
|
|
|
g_warning("%s", error->message);
|
|
|
|
g_error_free(error);
|
2022-01-08 07:44:39 +01:00
|
|
|
return FALSE;
|
2021-12-26 19:41:42 +01:00
|
|
|
}
|
|
|
|
|
2022-02-20 19:43:21 +01:00
|
|
|
t->cancel = g_cancellable_new();
|
|
|
|
g_subprocess_communicate_async(
|
|
|
|
t->minion, NULL, t->cancel, on_thumbnailer_ready, t);
|
2022-01-08 07:44:39 +01:00
|
|
|
return TRUE;
|
2021-12-26 19:41:42 +01:00
|
|
|
}
|
|
|
|
|
2022-01-08 07:44:39 +01:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2021-12-26 19:41:42 +01:00
|
|
|
static void
|
2022-01-08 07:44:39 +01:00
|
|
|
thumbnailers_abort(FivBrowser *self)
|
2021-12-26 19:41:42 +01:00
|
|
|
{
|
2022-08-09 15:53:21 +02:00
|
|
|
g_queue_clear(&self->thumbnailers_queue);
|
2022-01-08 07:44:39 +01:00
|
|
|
|
|
|
|
for (size_t i = 0; i < self->thumbnailers_len; i++) {
|
2022-02-20 19:43:21 +01:00
|
|
|
Thumbnailer *t = self->thumbnailers + i;
|
|
|
|
if (t->cancel) {
|
|
|
|
g_cancellable_cancel(t->cancel);
|
|
|
|
g_clear_object(&t->cancel);
|
2022-01-08 07:44:39 +01:00
|
|
|
}
|
2021-12-26 19:41:42 +01:00
|
|
|
|
2022-01-08 07:44:39 +01:00
|
|
|
// Just let them exit on their own.
|
2022-02-20 19:43:21 +01:00
|
|
|
g_clear_object(&t->minion);
|
|
|
|
t->target = NULL;
|
2022-01-08 07:44:39 +01:00
|
|
|
}
|
2021-12-27 23:19:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2022-01-08 07:44:39 +01:00
|
|
|
thumbnailers_start(FivBrowser *self)
|
2021-12-27 23:19:17 +01:00
|
|
|
{
|
2022-01-08 07:44:39 +01:00
|
|
|
thumbnailers_abort(self);
|
2022-02-13 12:47:35 +01:00
|
|
|
if (!self->model)
|
|
|
|
return;
|
2021-12-27 23:19:17 +01:00
|
|
|
|
2022-08-09 15:53:21 +02:00
|
|
|
GQueue lq = G_QUEUE_INIT;
|
|
|
|
for (guint i = 0; i < self->entries->len; i++) {
|
2023-05-30 10:36:11 +02:00
|
|
|
Entry *entry = self->entries->pdata[i];
|
|
|
|
if (entry->removed)
|
|
|
|
continue;
|
|
|
|
|
2021-12-27 23:19:17 +01:00
|
|
|
if (entry->icon)
|
2022-08-09 15:53:21 +02:00
|
|
|
g_queue_push_tail(&self->thumbnailers_queue, entry);
|
2021-12-27 23:51:38 +01:00
|
|
|
else if (cairo_surface_get_user_data(
|
2021-12-28 19:58:14 +01:00
|
|
|
entry->thumbnail, &fiv_thumbnail_key_lq))
|
2022-08-09 15:53:21 +02:00
|
|
|
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));
|
2021-12-26 19:41:42 +01:00
|
|
|
}
|
|
|
|
|
2022-01-08 07:44:39 +01:00
|
|
|
for (size_t i = 0; i < self->thumbnailers_len; i++) {
|
|
|
|
if (!thumbnailer_next(self->thumbnailers + i))
|
|
|
|
break;
|
|
|
|
}
|
2021-12-26 19:41:42 +01:00
|
|
|
}
|
|
|
|
|
2021-11-04 19:35:08 +01:00
|
|
|
// --- Boilerplate -------------------------------------------------------------
|
|
|
|
|
2021-12-18 06:38:30 +01:00
|
|
|
G_DEFINE_TYPE_EXTENDED(FivBrowser, fiv_browser, GTK_TYPE_WIDGET, 0,
|
2022-01-09 10:09:06 +01:00
|
|
|
G_IMPLEMENT_INTERFACE(GTK_TYPE_SCROLLABLE, NULL))
|
2021-10-17 15:38:27 +02:00
|
|
|
|
2021-11-21 16:03:54 +01:00
|
|
|
enum {
|
|
|
|
PROP_THUMBNAIL_SIZE = 1,
|
2023-03-30 20:52:57 +02:00
|
|
|
PROP_SHOW_LABELS,
|
2022-01-09 10:09:06 +01:00
|
|
|
N_PROPERTIES,
|
|
|
|
|
|
|
|
// These are overriden, we do not register them.
|
|
|
|
PROP_HADJUSTMENT,
|
|
|
|
PROP_VADJUSTMENT,
|
|
|
|
PROP_HSCROLL_POLICY,
|
|
|
|
PROP_VSCROLL_POLICY,
|
2021-11-21 16:03:54 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
static GParamSpec *browser_properties[N_PROPERTIES];
|
|
|
|
|
2021-10-17 15:38:27 +02:00
|
|
|
enum {
|
|
|
|
ITEM_ACTIVATED,
|
|
|
|
LAST_SIGNAL,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Globals are, sadly, the canonical way of storing signal numbers.
|
|
|
|
static guint browser_signals[LAST_SIGNAL];
|
|
|
|
|
2022-01-09 10:09:06 +01:00
|
|
|
static void
|
|
|
|
on_adjustment_value_changed(
|
|
|
|
G_GNUC_UNUSED GtkAdjustment *adjustment, gpointer user_data)
|
|
|
|
{
|
2022-07-16 16:49:47 +02:00
|
|
|
gtk_widget_queue_draw(GTK_WIDGET(user_data));
|
2022-01-09 10:09:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
static gboolean
|
|
|
|
replace_adjustment(
|
|
|
|
FivBrowser *self, GtkAdjustment **adjustment, GtkAdjustment *replacement)
|
|
|
|
{
|
|
|
|
if (*adjustment == replacement)
|
|
|
|
return FALSE;
|
|
|
|
|
|
|
|
if (*adjustment) {
|
|
|
|
g_signal_handlers_disconnect_by_func(
|
|
|
|
*adjustment, on_adjustment_value_changed, self);
|
|
|
|
g_clear_object(adjustment);
|
|
|
|
}
|
|
|
|
if (replacement) {
|
|
|
|
*adjustment = g_object_ref(replacement);
|
|
|
|
g_signal_connect(*adjustment, "value-changed",
|
|
|
|
G_CALLBACK(on_adjustment_value_changed), self);
|
|
|
|
// TODO(p): We should set it up, as it is done in relayout().
|
|
|
|
}
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
2021-10-17 15:38:27 +02:00
|
|
|
static void
|
2021-12-18 06:38:30 +01:00
|
|
|
fiv_browser_finalize(GObject *gobject)
|
2021-10-17 15:38:27 +02:00
|
|
|
{
|
2021-12-18 06:38:30 +01:00
|
|
|
FivBrowser *self = FIV_BROWSER(gobject);
|
2022-01-08 07:44:39 +01:00
|
|
|
thumbnailers_abort(self);
|
2023-05-30 10:36:11 +02:00
|
|
|
g_ptr_array_free(self->entries, TRUE);
|
2021-11-08 08:13:07 +01:00
|
|
|
g_array_free(self->layouted_rows, TRUE);
|
2021-12-31 02:19:17 +01:00
|
|
|
if (self->model) {
|
|
|
|
g_signal_handlers_disconnect_by_data(self->model, self);
|
|
|
|
g_clear_object(&self->model);
|
|
|
|
}
|
|
|
|
|
2022-06-04 16:28:18 +02:00
|
|
|
g_hash_table_destroy(self->thumbnail_cache);
|
|
|
|
|
2022-07-23 20:39:19 +02:00
|
|
|
cairo_pattern_destroy(self->glow_padded);
|
|
|
|
cairo_pattern_destroy(self->glow);
|
2021-11-13 13:40:10 +01:00
|
|
|
g_clear_object(&self->pointer);
|
2021-11-08 08:13:07 +01:00
|
|
|
|
2022-01-09 10:09:06 +01:00
|
|
|
replace_adjustment(self, &self->hadjustment, NULL);
|
|
|
|
replace_adjustment(self, &self->vadjustment, NULL);
|
|
|
|
|
2021-12-18 06:38:30 +01:00
|
|
|
G_OBJECT_CLASS(fiv_browser_parent_class)->finalize(gobject);
|
2021-10-17 15:38:27 +02:00
|
|
|
}
|
|
|
|
|
2021-11-21 16:03:54 +01:00
|
|
|
static void
|
2021-12-18 06:38:30 +01:00
|
|
|
fiv_browser_get_property(
|
2021-11-21 16:03:54 +01:00
|
|
|
GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
|
|
|
|
{
|
2021-12-18 06:38:30 +01:00
|
|
|
FivBrowser *self = FIV_BROWSER(object);
|
2021-11-21 16:03:54 +01:00
|
|
|
switch (property_id) {
|
|
|
|
case PROP_THUMBNAIL_SIZE:
|
|
|
|
g_value_set_enum(value, self->item_size);
|
|
|
|
break;
|
2023-03-30 20:52:57 +02:00
|
|
|
case PROP_SHOW_LABELS:
|
|
|
|
g_value_set_boolean(value, self->show_labels);
|
|
|
|
break;
|
2022-01-09 10:09:06 +01:00
|
|
|
case PROP_HADJUSTMENT:
|
|
|
|
g_value_set_object(value, self->hadjustment);
|
|
|
|
break;
|
|
|
|
case PROP_VADJUSTMENT:
|
|
|
|
g_value_set_object(value, self->vadjustment);
|
|
|
|
break;
|
|
|
|
case PROP_HSCROLL_POLICY:
|
|
|
|
g_value_set_enum(value, self->hscroll_policy);
|
|
|
|
break;
|
|
|
|
case PROP_VSCROLL_POLICY:
|
|
|
|
g_value_set_enum(value, self->vscroll_policy);
|
|
|
|
break;
|
2021-11-21 16:03:54 +01:00
|
|
|
default:
|
|
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-22 15:33:08 +01:00
|
|
|
static void
|
2021-12-28 19:58:14 +01:00
|
|
|
set_item_size(FivBrowser *self, FivThumbnailSize size)
|
2021-11-22 15:33:08 +01:00
|
|
|
{
|
2021-12-28 19:58:14 +01:00
|
|
|
if (size < FIV_THUMBNAIL_SIZE_MIN || size > FIV_THUMBNAIL_SIZE_MAX)
|
2021-11-22 15:33:08 +01:00
|
|
|
return;
|
|
|
|
|
|
|
|
if (size != self->item_size) {
|
|
|
|
self->item_size = size;
|
2021-12-28 19:58:14 +01:00
|
|
|
self->item_height = fiv_thumbnail_sizes[self->item_size].size;
|
2022-06-04 16:28:18 +02:00
|
|
|
|
|
|
|
g_hash_table_remove_all(self->thumbnail_cache);
|
2021-11-22 15:33:08 +01:00
|
|
|
reload_thumbnails(self);
|
2022-02-13 12:47:35 +01:00
|
|
|
thumbnailers_start(self);
|
2021-11-22 15:33:08 +01:00
|
|
|
|
|
|
|
g_object_notify_by_pspec(
|
|
|
|
G_OBJECT(self), browser_properties[PROP_THUMBNAIL_SIZE]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-21 16:03:54 +01:00
|
|
|
static void
|
2021-12-18 06:38:30 +01:00
|
|
|
fiv_browser_set_property(
|
2021-11-21 16:03:54 +01:00
|
|
|
GObject *object, guint property_id, const GValue *value, GParamSpec *pspec)
|
|
|
|
{
|
2021-12-18 06:38:30 +01:00
|
|
|
FivBrowser *self = FIV_BROWSER(object);
|
2021-11-21 16:03:54 +01:00
|
|
|
switch (property_id) {
|
|
|
|
case PROP_THUMBNAIL_SIZE:
|
2021-11-22 15:33:08 +01:00
|
|
|
set_item_size(self, g_value_get_enum(value));
|
2021-11-21 16:03:54 +01:00
|
|
|
break;
|
2023-03-30 20:52:57 +02:00
|
|
|
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;
|
2022-01-09 10:09:06 +01:00
|
|
|
case PROP_HADJUSTMENT:
|
|
|
|
if (replace_adjustment(
|
|
|
|
self, &self->hadjustment, g_value_get_object(value)))
|
|
|
|
g_object_notify_by_pspec(object, pspec);
|
|
|
|
break;
|
|
|
|
case PROP_VADJUSTMENT:
|
|
|
|
if (replace_adjustment(
|
|
|
|
self, &self->vadjustment, g_value_get_object(value)))
|
|
|
|
g_object_notify_by_pspec(object, pspec);
|
|
|
|
break;
|
|
|
|
case PROP_HSCROLL_POLICY:
|
|
|
|
if ((gint) self->hscroll_policy != g_value_get_enum(value)) {
|
|
|
|
self->hscroll_policy = g_value_get_enum(value);
|
|
|
|
gtk_widget_queue_resize(GTK_WIDGET(self));
|
|
|
|
g_object_notify_by_pspec(object, pspec);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case PROP_VSCROLL_POLICY:
|
|
|
|
if ((gint) self->vscroll_policy != g_value_get_enum(value)) {
|
|
|
|
self->vscroll_policy = g_value_get_enum(value);
|
|
|
|
gtk_widget_queue_resize(GTK_WIDGET(self));
|
|
|
|
g_object_notify_by_pspec(object, pspec);
|
|
|
|
}
|
|
|
|
break;
|
2021-11-21 16:03:54 +01:00
|
|
|
default:
|
|
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-17 15:38:27 +02:00
|
|
|
static GtkSizeRequestMode
|
2021-12-18 06:38:30 +01:00
|
|
|
fiv_browser_get_request_mode(G_GNUC_UNUSED GtkWidget *widget)
|
2021-10-17 15:38:27 +02:00
|
|
|
{
|
|
|
|
return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2021-12-18 06:38:30 +01:00
|
|
|
fiv_browser_get_preferred_width(GtkWidget *widget, gint *minimum, gint *natural)
|
2021-10-17 15:38:27 +02:00
|
|
|
{
|
2021-12-18 06:38:30 +01:00
|
|
|
FivBrowser *self = FIV_BROWSER(widget);
|
2021-11-10 17:58:27 +01:00
|
|
|
GtkStyleContext *style = gtk_widget_get_style_context(widget);
|
|
|
|
|
2021-11-04 19:35:08 +01:00
|
|
|
GtkBorder padding = {};
|
2021-11-10 17:58:27 +01:00
|
|
|
gtk_style_context_get_padding(style, GTK_STATE_FLAG_NORMAL, &padding);
|
2022-07-21 15:31:47 +02:00
|
|
|
|
|
|
|
// This should ideally reflect thumbnails, but we're in a GtkScrolledWindow,
|
|
|
|
// making this rather inconsequential.
|
|
|
|
int fluff = padding.left + 2 * self->item_border_x + padding.right;
|
|
|
|
*minimum = fluff + self->item_height /* Icons are rectangular. */;
|
|
|
|
*natural = fluff + FIV_THUMBNAIL_WIDE_COEFFICIENT * self->item_height;
|
2021-10-17 15:38:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2021-12-18 06:38:30 +01:00
|
|
|
fiv_browser_get_preferred_height_for_width(
|
2021-11-04 19:35:08 +01:00
|
|
|
GtkWidget *widget, gint width, gint *minimum, gint *natural)
|
2021-10-17 15:38:27 +02:00
|
|
|
{
|
2021-11-04 19:35:08 +01:00
|
|
|
// XXX: This is rather ugly, the caller is only asking.
|
2021-12-18 06:38:30 +01:00
|
|
|
*minimum = *natural = relayout(FIV_BROWSER(widget), width);
|
2021-10-17 15:38:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2021-12-18 06:38:30 +01:00
|
|
|
fiv_browser_realize(GtkWidget *widget)
|
2021-10-17 15:38:27 +02:00
|
|
|
{
|
|
|
|
GtkAllocation allocation;
|
|
|
|
gtk_widget_get_allocation(widget, &allocation);
|
|
|
|
|
|
|
|
GdkWindowAttr attributes = {
|
|
|
|
.window_type = GDK_WINDOW_CHILD,
|
2021-11-01 04:59:38 +01:00
|
|
|
.x = allocation.x,
|
|
|
|
.y = allocation.y,
|
|
|
|
.width = allocation.width,
|
|
|
|
.height = allocation.height,
|
2021-10-17 15:38:27 +02:00
|
|
|
|
|
|
|
// Input-only would presumably also work (as in GtkPathBar, e.g.),
|
|
|
|
// but it merely seems to involve more work.
|
2021-11-01 04:59:38 +01:00
|
|
|
.wclass = GDK_INPUT_OUTPUT,
|
2021-10-17 15:38:27 +02:00
|
|
|
|
2021-11-01 04:59:38 +01:00
|
|
|
.visual = gtk_widget_get_visual(widget),
|
2022-02-14 02:05:14 +01:00
|
|
|
.event_mask = gtk_widget_get_events(widget) | GDK_POINTER_MOTION_MASK |
|
|
|
|
GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_SCROLL_MASK |
|
|
|
|
GDK_KEY_PRESS_MASK,
|
2021-10-17 15:38:27 +02:00
|
|
|
};
|
|
|
|
|
2022-07-11 04:09:49 +02:00
|
|
|
// On Wayland, touchpad scrolling doesn't emulate the scroll wheel,
|
|
|
|
// making GDK_SMOOTH_SCROLL_MASK necessary for our GtkScrolledWindow.
|
|
|
|
// On X11 and Windows, this merely makes touchpad scrolling smoother.
|
|
|
|
//
|
|
|
|
// Note that Apple Magic Mouse's touchpad also sends out smooth scrolling
|
|
|
|
// events, and is indistinguishable from a mouse wheel (GDK_SOURCE_MOUSE,
|
|
|
|
// sends LIBINPUT_EVENT_POINTER_SCROLL_WHEEL). Yet, curiously,
|
|
|
|
// something in the stack on Wayland makes scrolling events discrete.
|
|
|
|
#ifdef GDK_WINDOWING_X11
|
|
|
|
// XXX: On X11 (at least, not on Wayland or Windows), the first scroll wheel
|
|
|
|
// event only produces a smooth stop event. Not our bug, yet annoying.
|
|
|
|
// We might make smooth scrolling support optional.
|
|
|
|
if (!GDK_IS_X11_WINDOW(gtk_widget_get_parent_window(widget)))
|
|
|
|
#endif // GDK_WINDOWING_X11
|
|
|
|
attributes.event_mask |= GDK_SMOOTH_SCROLL_MASK;
|
|
|
|
|
2021-10-17 15:38:27 +02:00
|
|
|
// We need this window to receive input events at all.
|
|
|
|
GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget),
|
|
|
|
&attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL);
|
|
|
|
gtk_widget_register_window(widget, window);
|
|
|
|
gtk_widget_set_window(widget, window);
|
|
|
|
gtk_widget_set_realized(widget, TRUE);
|
2021-11-13 13:40:10 +01:00
|
|
|
|
2021-12-18 06:38:30 +01:00
|
|
|
FivBrowser *self = FIV_BROWSER(widget);
|
2021-11-13 13:40:10 +01:00
|
|
|
g_clear_object(&self->pointer);
|
|
|
|
self->pointer =
|
|
|
|
gdk_cursor_new_from_name(gdk_window_get_display(window), "pointer");
|
2021-10-17 15:38:27 +02:00
|
|
|
}
|
|
|
|
|
2021-11-04 19:35:08 +01:00
|
|
|
static void
|
2021-12-18 06:38:30 +01:00
|
|
|
fiv_browser_size_allocate(GtkWidget *widget, GtkAllocation *allocation)
|
2021-11-04 19:35:08 +01:00
|
|
|
{
|
2022-01-12 10:40:19 +01:00
|
|
|
FivBrowser *self = FIV_BROWSER(widget);
|
2021-12-18 06:38:30 +01:00
|
|
|
GTK_WIDGET_CLASS(fiv_browser_parent_class)
|
2021-11-04 19:35:08 +01:00
|
|
|
->size_allocate(widget, allocation);
|
|
|
|
|
2022-07-21 15:31:47 +02:00
|
|
|
relayout(FIV_BROWSER(widget), allocation->width);
|
2022-01-12 10:40:19 +01:00
|
|
|
|
|
|
|
// Avoid fresh blank space.
|
2022-07-21 15:31:47 +02:00
|
|
|
if (self->hadjustment) {
|
|
|
|
gtk_adjustment_set_value(
|
|
|
|
self->hadjustment, gtk_adjustment_get_value(self->hadjustment));
|
|
|
|
}
|
2022-01-12 10:40:19 +01:00
|
|
|
if (self->vadjustment) {
|
2022-07-21 15:31:47 +02:00
|
|
|
gtk_adjustment_set_value(
|
|
|
|
self->vadjustment, gtk_adjustment_get_value(self->vadjustment));
|
2022-01-12 10:40:19 +01:00
|
|
|
}
|
2021-11-04 19:35:08 +01:00
|
|
|
}
|
|
|
|
|
2021-10-17 15:38:27 +02:00
|
|
|
static gboolean
|
2021-12-18 06:38:30 +01:00
|
|
|
fiv_browser_draw(GtkWidget *widget, cairo_t *cr)
|
2021-10-17 15:38:27 +02:00
|
|
|
{
|
2021-12-18 06:38:30 +01:00
|
|
|
FivBrowser *self = FIV_BROWSER(widget);
|
2021-10-17 15:38:27 +02:00
|
|
|
if (!gtk_cairo_should_draw_window(cr, gtk_widget_get_window(widget)))
|
|
|
|
return TRUE;
|
|
|
|
|
|
|
|
GtkAllocation allocation;
|
|
|
|
gtk_widget_get_allocation(widget, &allocation);
|
2021-11-01 04:59:38 +01:00
|
|
|
gtk_render_background(gtk_widget_get_style_context(widget), cr, 0, 0,
|
|
|
|
allocation.width, allocation.height);
|
2021-10-17 15:38:27 +02:00
|
|
|
|
2022-02-22 15:35:32 +01:00
|
|
|
if (self->hadjustment)
|
|
|
|
cairo_translate(
|
|
|
|
cr, -round(gtk_adjustment_get_value(self->hadjustment)), 0);
|
|
|
|
if (self->vadjustment)
|
|
|
|
cairo_translate(
|
|
|
|
cr, 0, -round(gtk_adjustment_get_value(self->vadjustment)));
|
2022-01-09 10:09:06 +01:00
|
|
|
|
2021-11-09 04:13:37 +01:00
|
|
|
GdkRectangle clip = {};
|
|
|
|
gboolean have_clip = gdk_cairo_get_clip_rectangle(cr, &clip);
|
|
|
|
|
2021-11-04 19:35:08 +01:00
|
|
|
for (guint i = 0; i < self->layouted_rows->len; i++) {
|
2021-11-09 04:13:37 +01:00
|
|
|
const Row *row = &g_array_index(self->layouted_rows, Row, i);
|
|
|
|
GdkRectangle extents = {
|
|
|
|
.x = 0,
|
2021-11-10 17:58:27 +01:00
|
|
|
.y = row->y_offset - self->item_border_y,
|
2021-11-09 04:13:37 +01:00
|
|
|
.width = allocation.width,
|
2021-11-21 20:46:50 +01:00
|
|
|
.height = self->item_height + 2 * self->item_border_y,
|
2021-11-09 04:13:37 +01:00
|
|
|
};
|
|
|
|
if (!have_clip || gdk_rectangle_intersect(&clip, &extents, NULL))
|
|
|
|
draw_row(self, cr, row);
|
2021-10-17 15:38:27 +02:00
|
|
|
}
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
2021-11-23 00:37:38 +01:00
|
|
|
static gboolean
|
|
|
|
open_entry(GtkWidget *self, const Entry *entry, gboolean new_window)
|
|
|
|
{
|
2023-04-14 07:11:49 +02:00
|
|
|
GFile *location = g_file_new_for_uri(entry->e->uri);
|
2021-11-27 02:33:28 +01:00
|
|
|
g_signal_emit(self, browser_signals[ITEM_ACTIVATED], 0, location,
|
2021-11-23 00:37:38 +01:00
|
|
|
new_window ? GTK_PLACES_OPEN_NEW_WINDOW : GTK_PLACES_OPEN_NORMAL);
|
2021-11-27 02:33:28 +01:00
|
|
|
g_object_unref(location);
|
2021-11-23 00:37:38 +01:00
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
2022-07-04 20:16:18 +02:00
|
|
|
static void
|
|
|
|
show_context_menu(GtkWidget *widget, GFile *file)
|
|
|
|
{
|
2023-05-31 14:45:46 +02:00
|
|
|
GtkMenu *menu = fiv_context_menu_new(widget, file);
|
|
|
|
if (menu)
|
|
|
|
gtk_menu_popup_at_pointer(menu, NULL);
|
2022-07-04 20:16:18 +02:00
|
|
|
}
|
|
|
|
|
2022-02-24 21:48:26 +01:00
|
|
|
static void
|
|
|
|
abort_button_tracking(FivBrowser *self)
|
|
|
|
{
|
|
|
|
self->tracked_button = 0;
|
|
|
|
self->drag_begin_x = self->drag_begin_y = -1;
|
|
|
|
}
|
|
|
|
|
2021-11-08 08:00:18 +01:00
|
|
|
static gboolean
|
2021-12-18 06:38:30 +01:00
|
|
|
fiv_browser_button_press_event(GtkWidget *widget, GdkEventButton *event)
|
2021-11-08 08:00:18 +01:00
|
|
|
{
|
2022-02-22 15:35:32 +01:00
|
|
|
FivBrowser *self = FIV_BROWSER(widget);
|
2022-07-21 14:36:01 +02:00
|
|
|
if (GTK_WIDGET_CLASS(fiv_browser_parent_class)
|
|
|
|
->button_press_event(widget, event))
|
|
|
|
return GDK_EVENT_STOP;
|
2021-11-20 18:45:33 +01:00
|
|
|
|
2022-02-22 15:35:32 +01:00
|
|
|
// Make pressing multiple mouse buttons at once cancel a click.
|
|
|
|
if (self->tracked_button) {
|
|
|
|
abort_button_tracking(self);
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2022-02-22 15:35:32 +01:00
|
|
|
}
|
2021-11-23 00:37:38 +01:00
|
|
|
if (event->type != GDK_BUTTON_PRESS)
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_PROPAGATE;
|
2021-11-23 00:37:38 +01:00
|
|
|
|
|
|
|
guint state = event->state & gtk_accelerator_get_default_mod_mask();
|
|
|
|
if (event->button == GDK_BUTTON_PRIMARY && state == 0 &&
|
2021-11-12 07:23:24 +01:00
|
|
|
gtk_widget_get_focus_on_click(widget))
|
2021-11-20 18:45:33 +01:00
|
|
|
gtk_widget_grab_focus(widget);
|
2021-11-08 08:00:18 +01:00
|
|
|
|
2022-08-03 21:36:30 +02:00
|
|
|
// In accordance with Nautilus, Thunar, and the mildly confusing
|
|
|
|
// Apple Human Interface Guidelines, but not with the ugly Windows User
|
|
|
|
// Experience Interaction Guidelines, open the context menu on button press.
|
|
|
|
// (Originally our own behaviour, but the GDK3 function also does this.)
|
|
|
|
gboolean triggers_menu =
|
|
|
|
gdk_event_triggers_context_menu((const GdkEvent *) event);
|
|
|
|
|
2021-11-08 08:00:18 +01:00
|
|
|
const Entry *entry = entry_at(self, event->x, event->y);
|
2022-08-03 21:36:30 +02:00
|
|
|
if (!entry) {
|
|
|
|
if (triggers_menu)
|
2022-01-10 17:54:41 +01:00
|
|
|
show_context_menu(widget, fiv_io_model_get_location(self->model));
|
2022-08-03 21:36:30 +02:00
|
|
|
else if (state || event->button != GDK_BUTTON_PRIMARY)
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_PROPAGATE;
|
2022-01-10 17:54:41 +01:00
|
|
|
|
|
|
|
if (self->selected) {
|
|
|
|
self->selected = NULL;
|
|
|
|
gtk_widget_queue_draw(widget);
|
|
|
|
}
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2021-12-20 10:15:15 +01:00
|
|
|
}
|
2021-11-08 08:00:18 +01:00
|
|
|
|
2022-08-03 21:36:30 +02:00
|
|
|
if (entry && triggers_menu) {
|
2022-01-10 17:54:41 +01:00
|
|
|
self->selected = entry;
|
|
|
|
gtk_widget_queue_draw(widget);
|
|
|
|
|
2021-11-23 14:59:25 +01:00
|
|
|
// 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);
|
2021-12-31 02:19:17 +01:00
|
|
|
|
2023-04-14 07:11:49 +02:00
|
|
|
GFile *file = g_file_new_for_uri(entry->e->uri);
|
2021-12-31 02:19:17 +01:00
|
|
|
show_context_menu(widget, file);
|
|
|
|
g_object_unref(file);
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2021-11-20 12:45:05 +01:00
|
|
|
}
|
2022-02-22 15:35:32 +01:00
|
|
|
|
|
|
|
// gtk_drag_source_set() would span the whole widget area, we'd have to
|
|
|
|
// un/set it as needed, in particular to handle empty space.
|
|
|
|
// It might be a good idea to use GtkGestureDrag instead.
|
|
|
|
if (event->button == GDK_BUTTON_PRIMARY ||
|
|
|
|
event->button == GDK_BUTTON_MIDDLE) {
|
|
|
|
self->tracked_button = event->button;
|
|
|
|
self->drag_begin_x = event->x;
|
|
|
|
self->drag_begin_y = event->y;
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2022-02-22 15:35:32 +01:00
|
|
|
}
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_PROPAGATE;
|
2022-02-14 02:05:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
static gboolean
|
|
|
|
fiv_browser_button_release_event(GtkWidget *widget, GdkEventButton *event)
|
|
|
|
{
|
2022-02-22 15:35:32 +01:00
|
|
|
FivBrowser *self = FIV_BROWSER(widget);
|
2022-07-21 14:36:01 +02:00
|
|
|
if (GTK_WIDGET_CLASS(fiv_browser_parent_class)
|
|
|
|
->button_release_event(widget, event))
|
|
|
|
return GDK_EVENT_STOP;
|
2022-02-22 15:35:32 +01:00
|
|
|
if (event->button != self->tracked_button)
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_PROPAGATE;
|
2022-02-14 02:05:14 +01:00
|
|
|
|
2022-02-22 15:35:32 +01:00
|
|
|
// Middle clicks should only work on the starting entry.
|
|
|
|
const Entry *entry = entry_at(self, self->drag_begin_x, self->drag_begin_y);
|
|
|
|
abort_button_tracking(self);
|
|
|
|
if (!entry || entry != entry_at(self, event->x, event->y))
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_PROPAGATE;
|
2022-02-14 02:05:14 +01:00
|
|
|
|
|
|
|
guint state = event->state & gtk_accelerator_get_default_mod_mask();
|
|
|
|
if ((event->button == GDK_BUTTON_PRIMARY && state == 0))
|
|
|
|
return open_entry(widget, entry, FALSE);
|
|
|
|
if ((event->button == GDK_BUTTON_PRIMARY && state == GDK_CONTROL_MASK) ||
|
|
|
|
(event->button == GDK_BUTTON_MIDDLE && state == 0))
|
|
|
|
return open_entry(widget, entry, TRUE);
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_PROPAGATE;
|
2021-11-08 08:00:18 +01:00
|
|
|
}
|
|
|
|
|
2022-01-09 18:36:27 +01:00
|
|
|
static gboolean
|
2021-12-18 06:38:30 +01:00
|
|
|
fiv_browser_motion_notify_event(GtkWidget *widget, GdkEventMotion *event)
|
2021-11-13 09:20:37 +01:00
|
|
|
{
|
2022-02-22 15:35:32 +01:00
|
|
|
FivBrowser *self = FIV_BROWSER(widget);
|
2022-07-21 14:36:01 +02:00
|
|
|
if (GTK_WIDGET_CLASS(fiv_browser_parent_class)
|
|
|
|
->motion_notify_event(widget, event))
|
|
|
|
return GDK_EVENT_STOP;
|
2021-11-20 18:45:33 +01:00
|
|
|
|
2022-07-21 14:36:01 +02:00
|
|
|
// Touch screen dragging is how you scroll the parent GtkScrolledWindow,
|
|
|
|
// don't steal that gesture.
|
|
|
|
//
|
|
|
|
// While we could allow dragging items that have been selected,
|
|
|
|
// it's currently impossible to select on touch screens without opening,
|
|
|
|
// and this behaviour/distinction seems unintuitive/surprising.
|
2022-02-22 15:35:32 +01:00
|
|
|
guint state = event->state & gtk_accelerator_get_default_mod_mask();
|
|
|
|
if (state != 0 || self->tracked_button != GDK_BUTTON_PRIMARY ||
|
|
|
|
!gtk_drag_check_threshold(widget, self->drag_begin_x,
|
2022-07-21 14:36:01 +02:00
|
|
|
self->drag_begin_y, event->x, event->y) ||
|
|
|
|
gdk_device_get_source(gdk_event_get_source_device(
|
|
|
|
(GdkEvent *) event)) == GDK_SOURCE_TOUCHSCREEN) {
|
2022-02-22 15:35:32 +01:00
|
|
|
const Entry *entry = entry_at(self, event->x, event->y);
|
|
|
|
GdkWindow *window = gtk_widget_get_window(widget);
|
|
|
|
gdk_window_set_cursor(window, entry ? self->pointer : NULL);
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_PROPAGATE;
|
2022-02-22 15:35:32 +01:00
|
|
|
}
|
2021-11-13 09:20:37 +01:00
|
|
|
|
2022-02-22 15:35:32 +01:00
|
|
|
// The "correct" behaviour is to set the selection on a left mouse button
|
|
|
|
// press immediately, but that is regarded as visual noise.
|
|
|
|
const Entry *entry = entry_at(self, self->drag_begin_x, self->drag_begin_y);
|
|
|
|
abort_button_tracking(self);
|
|
|
|
if (!entry)
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2022-02-22 15:35:32 +01:00
|
|
|
|
|
|
|
self->selected = entry;
|
|
|
|
gtk_widget_queue_draw(widget);
|
|
|
|
|
|
|
|
GtkTargetList *target_list = gtk_target_list_new(NULL, 0);
|
|
|
|
gtk_target_list_add_uri_targets(target_list, 0);
|
|
|
|
GdkDragAction actions = GDK_ACTION_COPY | GDK_ACTION_MOVE |
|
|
|
|
GDK_ACTION_LINK | GDK_ACTION_ASK;
|
|
|
|
gtk_drag_begin_with_coordinates(widget, target_list, actions,
|
|
|
|
self->tracked_button, (GdkEvent *) event, event->x, event->y);
|
|
|
|
gtk_target_list_unref(target_list);
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2021-11-13 09:20:37 +01:00
|
|
|
}
|
|
|
|
|
2021-11-22 15:33:08 +01:00
|
|
|
static gboolean
|
2021-12-18 06:38:30 +01:00
|
|
|
fiv_browser_scroll_event(GtkWidget *widget, GdkEventScroll *event)
|
2021-11-22 15:33:08 +01:00
|
|
|
{
|
2021-12-18 06:38:30 +01:00
|
|
|
FivBrowser *self = FIV_BROWSER(widget);
|
2022-02-22 15:35:32 +01:00
|
|
|
abort_button_tracking(self);
|
2021-11-22 15:33:08 +01:00
|
|
|
if ((event->state & gtk_accelerator_get_default_mod_mask()) !=
|
|
|
|
GDK_CONTROL_MASK)
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_PROPAGATE;
|
2021-11-22 15:33:08 +01:00
|
|
|
|
2022-07-11 04:09:49 +02:00
|
|
|
static double delta = 0;
|
2021-11-22 15:33:08 +01:00
|
|
|
switch (event->direction) {
|
|
|
|
case GDK_SCROLL_UP:
|
|
|
|
set_item_size(self, self->item_size + 1);
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2021-11-22 15:33:08 +01:00
|
|
|
case GDK_SCROLL_DOWN:
|
|
|
|
set_item_size(self, self->item_size - 1);
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2022-07-11 04:09:49 +02:00
|
|
|
case GDK_SCROLL_SMOOTH:
|
|
|
|
// On GDK/Wayland, the mouse wheel will typically create 1.5 deltas,
|
|
|
|
// after dividing a 15 degree click angle from libinput by 10.
|
2022-07-24 22:45:26 +02:00
|
|
|
// (Noticed on Arch + Sway, cannot reproduce on Ubuntu 22.04.)
|
2022-07-11 04:09:49 +02:00
|
|
|
// On X11, as libinput(4) indicates, the delta will always be 1.0.
|
|
|
|
if ((delta += event->delta_y) <= -1)
|
|
|
|
set_item_size(self, self->item_size + 1);
|
|
|
|
else if (delta >= +1)
|
|
|
|
set_item_size(self, self->item_size - 1);
|
|
|
|
else if (!event->is_stop)
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2022-07-11 04:09:49 +02:00
|
|
|
|
|
|
|
delta = 0;
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2021-11-22 15:33:08 +01:00
|
|
|
default:
|
|
|
|
// Left/right are good to steal from GtkScrolledWindow for consistency.
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2021-11-22 15:33:08 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-22 15:35:32 +01:00
|
|
|
static void
|
|
|
|
fiv_browser_drag_begin(GtkWidget *widget, GdkDragContext *context)
|
|
|
|
{
|
|
|
|
FivBrowser *self = FIV_BROWSER(widget);
|
|
|
|
if (self->selected) {
|
|
|
|
// There doesn't seem to be a size limit.
|
|
|
|
gtk_drag_set_icon_surface(context, self->selected->thumbnail);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
fiv_browser_drag_data_get(GtkWidget *widget,
|
|
|
|
G_GNUC_UNUSED GdkDragContext *context, GtkSelectionData *data,
|
|
|
|
G_GNUC_UNUSED guint info, G_GNUC_UNUSED guint time)
|
|
|
|
{
|
|
|
|
FivBrowser *self = FIV_BROWSER(widget);
|
|
|
|
if (self->selected) {
|
2023-04-14 07:11:49 +02:00
|
|
|
(void) gtk_selection_data_set_uris(data, (gchar *[])
|
|
|
|
{(gchar *) entry_system_wide_uri(self->selected), NULL});
|
2022-02-22 15:35:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-11 14:33:35 +01:00
|
|
|
static void
|
|
|
|
select_closest(FivBrowser *self, const Row *row, int target)
|
|
|
|
{
|
|
|
|
int closest = G_MAXINT;
|
|
|
|
for (guint i = 0; i < row->len; i++) {
|
|
|
|
GdkRectangle extents = item_extents(self, row->items + i, row);
|
|
|
|
int distance = ABS(extents.x + extents.width / 2 - target);
|
|
|
|
if (distance > closest)
|
|
|
|
break;
|
|
|
|
self->selected = row->items[i].entry;
|
|
|
|
closest = distance;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
scroll_to_row(FivBrowser *self, const Row *row)
|
|
|
|
{
|
|
|
|
if (!self->vadjustment)
|
|
|
|
return;
|
|
|
|
|
|
|
|
double y1 = gtk_adjustment_get_value(self->vadjustment);
|
|
|
|
double ph = gtk_adjustment_get_page_size(self->vadjustment);
|
2023-03-30 20:52:57 +02:00
|
|
|
double sh = self->item_border_y + row_subheight(self, row);
|
2022-01-11 14:33:35 +01:00
|
|
|
if (row->y_offset < y1) {
|
|
|
|
gtk_adjustment_set_value(
|
|
|
|
self->vadjustment, row->y_offset - self->item_border_y);
|
2023-03-30 20:52:57 +02:00
|
|
|
} 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);
|
2022-01-11 14:33:35 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
move_selection(FivBrowser *self, GtkDirectionType dir)
|
|
|
|
{
|
|
|
|
GtkWidget *widget = GTK_WIDGET(self);
|
|
|
|
if (!self->layouted_rows->len)
|
|
|
|
return;
|
|
|
|
|
|
|
|
const Row *selected_row = NULL;
|
|
|
|
if (!self->selected) {
|
|
|
|
switch (dir) {
|
|
|
|
case GTK_DIR_RIGHT:
|
|
|
|
case GTK_DIR_DOWN:
|
|
|
|
selected_row = &g_array_index(self->layouted_rows, Row, 0);
|
|
|
|
self->selected = selected_row->items->entry;
|
|
|
|
goto adjust;
|
|
|
|
case GTK_DIR_LEFT:
|
|
|
|
case GTK_DIR_UP:
|
|
|
|
selected_row = &g_array_index(
|
|
|
|
self->layouted_rows, Row, self->layouted_rows->len - 1);
|
|
|
|
self->selected = selected_row->items[selected_row->len - 1].entry;
|
|
|
|
goto adjust;
|
|
|
|
default:
|
|
|
|
g_assert_not_reached();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
gsize x = 0, y = 0;
|
|
|
|
int target_offset = 0;
|
|
|
|
for (y = 0; y < self->layouted_rows->len; y++) {
|
|
|
|
const Row *row = &g_array_index(self->layouted_rows, Row, y);
|
|
|
|
for (x = 0; x < row->len; x++) {
|
|
|
|
const Item *item = row->items + x;
|
|
|
|
if (item->entry == self->selected) {
|
|
|
|
GdkRectangle extents = item_extents(self, item, row);
|
|
|
|
target_offset = extents.x + extents.width / 2;
|
|
|
|
goto found;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
found:
|
|
|
|
g_return_if_fail(y < self->layouted_rows->len);
|
|
|
|
selected_row = &g_array_index(self->layouted_rows, Row, y);
|
|
|
|
|
|
|
|
switch (dir) {
|
|
|
|
case GTK_DIR_LEFT:
|
|
|
|
if (x > 0) {
|
|
|
|
self->selected = selected_row->items[--x].entry;
|
|
|
|
} else if (y-- > 0) {
|
|
|
|
selected_row = &g_array_index(self->layouted_rows, Row, y);
|
|
|
|
self->selected = selected_row->items[selected_row->len - 1].entry;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case GTK_DIR_RIGHT:
|
|
|
|
if (++x < selected_row->len) {
|
|
|
|
self->selected = selected_row->items[x].entry;
|
|
|
|
} else if (++y < self->layouted_rows->len) {
|
|
|
|
selected_row = &g_array_index(self->layouted_rows, Row, y);
|
|
|
|
self->selected = selected_row->items[0].entry;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case GTK_DIR_UP:
|
|
|
|
if (y-- > 0) {
|
|
|
|
selected_row = &g_array_index(self->layouted_rows, Row, y);
|
|
|
|
select_closest(self, selected_row, target_offset);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case GTK_DIR_DOWN:
|
|
|
|
if (++y < self->layouted_rows->len) {
|
|
|
|
selected_row = &g_array_index(self->layouted_rows, Row, y);
|
|
|
|
select_closest(self, selected_row, target_offset);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
g_assert_not_reached();
|
|
|
|
}
|
|
|
|
|
|
|
|
adjust:
|
|
|
|
// TODO(p): We should also do it horizontally, although we don't use it.
|
|
|
|
scroll_to_row(self, selected_row);
|
|
|
|
gtk_widget_queue_draw(widget);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
move_selection_home(FivBrowser *self)
|
|
|
|
{
|
|
|
|
if (self->layouted_rows->len) {
|
|
|
|
const Row *row = &g_array_index(self->layouted_rows, Row, 0);
|
|
|
|
self->selected = row->items[0].entry;
|
|
|
|
scroll_to_row(self, row);
|
|
|
|
gtk_widget_queue_draw(GTK_WIDGET(self));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
move_selection_end(FivBrowser *self)
|
|
|
|
{
|
|
|
|
if (self->layouted_rows->len) {
|
|
|
|
const Row *row = &g_array_index(
|
|
|
|
self->layouted_rows, Row, self->layouted_rows->len - 1);
|
|
|
|
self->selected = row->items[row->len - 1].entry;
|
|
|
|
scroll_to_row(self, row);
|
|
|
|
gtk_widget_queue_draw(GTK_WIDGET(self));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-10 17:54:41 +01:00
|
|
|
static gboolean
|
|
|
|
fiv_browser_key_press_event(GtkWidget *widget, GdkEventKey *event)
|
|
|
|
{
|
|
|
|
FivBrowser *self = FIV_BROWSER(widget);
|
2022-01-23 06:44:34 +01:00
|
|
|
switch ((event->state & gtk_accelerator_get_default_mod_mask())) {
|
|
|
|
case 0:
|
2022-01-11 14:33:35 +01:00
|
|
|
switch (event->keyval) {
|
|
|
|
case GDK_KEY_Return:
|
|
|
|
if (self->selected)
|
|
|
|
return open_entry(widget, self->selected, FALSE);
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2022-01-11 14:33:35 +01:00
|
|
|
case GDK_KEY_Left:
|
|
|
|
move_selection(self, GTK_DIR_LEFT);
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2022-01-11 14:33:35 +01:00
|
|
|
case GDK_KEY_Right:
|
|
|
|
move_selection(self, GTK_DIR_RIGHT);
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2022-01-11 14:33:35 +01:00
|
|
|
case GDK_KEY_Up:
|
|
|
|
move_selection(self, GTK_DIR_UP);
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2022-01-11 14:33:35 +01:00
|
|
|
case GDK_KEY_Down:
|
|
|
|
move_selection(self, GTK_DIR_DOWN);
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2022-01-11 14:33:35 +01:00
|
|
|
case GDK_KEY_Home:
|
|
|
|
move_selection_home(self);
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2022-01-11 14:33:35 +01:00
|
|
|
case GDK_KEY_End:
|
|
|
|
move_selection_end(self);
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2022-01-11 14:33:35 +01:00
|
|
|
}
|
2022-01-23 06:44:34 +01:00
|
|
|
break;
|
2022-08-06 07:31:17 +02:00
|
|
|
case GDK_MOD1_MASK:
|
|
|
|
switch (event->keyval) {
|
|
|
|
case GDK_KEY_Return:
|
|
|
|
if (self->selected) {
|
|
|
|
GtkWindow *window = GTK_WINDOW(gtk_widget_get_toplevel(widget));
|
2023-04-14 07:11:49 +02:00
|
|
|
fiv_context_menu_information(window, self->selected->e->uri);
|
2022-08-06 07:31:17 +02:00
|
|
|
}
|
|
|
|
return GDK_EVENT_STOP;
|
|
|
|
}
|
|
|
|
break;
|
2022-01-23 06:44:34 +01:00
|
|
|
case GDK_CONTROL_MASK:
|
|
|
|
case GDK_CONTROL_MASK | GDK_SHIFT_MASK:
|
|
|
|
switch (event->keyval) {
|
|
|
|
case GDK_KEY_plus:
|
|
|
|
set_item_size(self, self->item_size + 1);
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2022-01-23 06:44:34 +01:00
|
|
|
case GDK_KEY_minus:
|
|
|
|
set_item_size(self, self->item_size - 1);
|
2022-07-21 14:46:17 +02:00
|
|
|
return GDK_EVENT_STOP;
|
2022-01-23 06:44:34 +01:00
|
|
|
}
|
2022-01-11 14:33:35 +01:00
|
|
|
}
|
2022-01-10 17:54:41 +01:00
|
|
|
|
|
|
|
return GTK_WIDGET_CLASS(fiv_browser_parent_class)
|
|
|
|
->key_press_event(widget, event);
|
|
|
|
}
|
|
|
|
|
2021-11-22 15:18:58 +01:00
|
|
|
static gboolean
|
2021-12-18 06:38:30 +01:00
|
|
|
fiv_browser_query_tooltip(GtkWidget *widget, gint x, gint y,
|
2021-11-22 15:18:58 +01:00
|
|
|
G_GNUC_UNUSED gboolean keyboard_tooltip, GtkTooltip *tooltip)
|
|
|
|
{
|
2021-12-18 06:38:30 +01:00
|
|
|
FivBrowser *self = FIV_BROWSER(widget);
|
2023-03-30 20:52:57 +02:00
|
|
|
|
|
|
|
// TODO(p): Consider getting rid of tooltips altogether.
|
|
|
|
if (self->show_labels)
|
2021-11-22 15:18:58 +01:00
|
|
|
return FALSE;
|
|
|
|
|
2023-03-30 20:52:57 +02:00
|
|
|
const Entry *entry = entry_at(self, x, y);
|
|
|
|
if (!entry)
|
2021-11-22 15:18:58 +01:00
|
|
|
return FALSE;
|
|
|
|
|
2023-04-14 07:11:49 +02:00
|
|
|
gtk_tooltip_set_text(tooltip, entry->e->display_name);
|
2021-11-22 15:18:58 +01:00
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
2022-01-11 12:24:49 +01:00
|
|
|
static gboolean
|
|
|
|
fiv_browser_popup_menu(GtkWidget *widget)
|
|
|
|
{
|
|
|
|
FivBrowser *self = FIV_BROWSER(widget);
|
|
|
|
|
|
|
|
// This is what Windows Explorer does, and what you want to be done.
|
|
|
|
// Although invoking the menu outside the widget is questionable.
|
|
|
|
GFile *file = NULL;
|
|
|
|
GdkRectangle rect = {};
|
|
|
|
if (self->selected) {
|
2023-04-14 07:11:49 +02:00
|
|
|
file = g_file_new_for_uri(self->selected->e->uri);
|
2022-01-11 12:24:49 +01:00
|
|
|
rect = entry_rect(self, self->selected);
|
|
|
|
rect.x += rect.width / 2;
|
|
|
|
rect.y += rect.height / 2;
|
|
|
|
} else {
|
|
|
|
file = g_object_ref(fiv_io_model_get_location(self->model));
|
|
|
|
}
|
|
|
|
|
2022-07-04 20:16:18 +02:00
|
|
|
gtk_menu_popup_at_rect(fiv_context_menu_new(widget, file),
|
2022-01-11 12:24:49 +01:00
|
|
|
gtk_widget_get_window(widget), &rect, GDK_GRAVITY_NORTH_WEST,
|
|
|
|
GDK_GRAVITY_NORTH_WEST, NULL);
|
|
|
|
g_object_unref(file);
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
2022-07-21 11:09:16 +02:00
|
|
|
static void
|
|
|
|
on_long_press(GtkGestureLongPress *lp, gdouble x, gdouble y, gpointer user_data)
|
|
|
|
{
|
|
|
|
FivBrowser *self = FIV_BROWSER(user_data);
|
|
|
|
const Entry *entry = entry_at(self, x, y);
|
|
|
|
abort_button_tracking(self);
|
|
|
|
if (!entry)
|
|
|
|
return;
|
|
|
|
|
|
|
|
GtkGesture *gesture = GTK_GESTURE(lp);
|
|
|
|
GdkEventSequence *sequence = gtk_gesture_get_last_updated_sequence(gesture);
|
|
|
|
const GdkEvent *event = gtk_gesture_get_last_event(gesture, sequence);
|
|
|
|
|
|
|
|
GtkWidget *widget = GTK_WIDGET(self);
|
|
|
|
GdkWindow *window = gtk_widget_get_window(widget);
|
|
|
|
#ifdef GDK_WINDOWING_X11
|
|
|
|
// FIXME: Once the finger is lifted, this menu is immediately closed.
|
|
|
|
if (GDK_IS_X11_WINDOW(window))
|
|
|
|
return;
|
|
|
|
#endif // GDK_WINDOWING_X11
|
|
|
|
|
2022-07-21 14:36:01 +02:00
|
|
|
// It might also be possible to have long-press just select items,
|
|
|
|
// and show some kind of toolbar with available actions.
|
2023-04-14 07:11:49 +02:00
|
|
|
GFile *file = g_file_new_for_uri(entry->e->uri);
|
2022-07-21 11:09:16 +02:00
|
|
|
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);
|
|
|
|
g_object_unref(file);
|
|
|
|
|
|
|
|
self->selected = entry;
|
|
|
|
gtk_widget_queue_draw(widget);
|
|
|
|
}
|
|
|
|
|
2021-11-10 17:58:27 +01:00
|
|
|
static void
|
2021-12-18 06:38:30 +01:00
|
|
|
fiv_browser_style_updated(GtkWidget *widget)
|
2021-11-10 17:58:27 +01:00
|
|
|
{
|
2021-12-18 06:38:30 +01:00
|
|
|
GTK_WIDGET_CLASS(fiv_browser_parent_class)->style_updated(widget);
|
2021-11-10 17:58:27 +01:00
|
|
|
|
2021-12-18 06:38:30 +01:00
|
|
|
FivBrowser *self = FIV_BROWSER(widget);
|
2021-11-10 17:58:27 +01:00
|
|
|
GtkStyleContext *style = gtk_widget_get_style_context(widget);
|
|
|
|
GtkBorder border = {}, margin = {};
|
|
|
|
|
2021-11-22 15:08:34 +01:00
|
|
|
int item_spacing = self->item_spacing;
|
|
|
|
gtk_widget_style_get(widget, "spacing", &self->item_spacing, NULL);
|
|
|
|
if (item_spacing != self->item_spacing)
|
|
|
|
gtk_widget_queue_resize(widget);
|
|
|
|
|
2021-11-10 17:58:27 +01:00
|
|
|
// Using a pseudo-class, because GTK+ regions are deprecated.
|
|
|
|
gtk_style_context_save(style);
|
|
|
|
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);
|
2023-03-30 20:52:57 +02:00
|
|
|
// 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.
|
2021-11-10 17:58:27 +01:00
|
|
|
gtk_style_context_restore(style);
|
|
|
|
|
2022-07-23 20:39:19 +02:00
|
|
|
self->glow_w = (margin.left + margin.right) / 2;
|
|
|
|
self->glow_h = (margin.top + margin.bottom) / 2;
|
2021-11-10 17:58:27 +01:00
|
|
|
|
|
|
|
// Don't set different opposing sides, it will misrender, your problem.
|
|
|
|
// When the style of the class changes, this virtual method isn't invoked,
|
|
|
|
// so the update check is mildly pointless.
|
2022-07-23 20:39:19 +02:00
|
|
|
int item_border_x = self->glow_w + (border.left + border.right) / 2;
|
|
|
|
int item_border_y = self->glow_h + (border.top + border.bottom) / 2;
|
2021-11-10 17:58:27 +01:00
|
|
|
if (item_border_x != self->item_border_x ||
|
|
|
|
item_border_y != self->item_border_y) {
|
|
|
|
self->item_border_x = item_border_x;
|
|
|
|
self->item_border_y = item_border_y;
|
|
|
|
gtk_widget_queue_resize(widget);
|
|
|
|
}
|
|
|
|
|
2022-07-23 20:39:19 +02:00
|
|
|
if (self->glow_padded)
|
|
|
|
cairo_pattern_destroy(self->glow_padded);
|
2021-11-10 17:58:27 +01:00
|
|
|
if (self->glow)
|
2022-07-23 20:39:19 +02:00
|
|
|
cairo_pattern_destroy(self->glow);
|
2021-11-10 17:58:27 +01:00
|
|
|
|
2022-07-23 20:39:19 +02:00
|
|
|
cairo_surface_t *corner = cairo_image_surface_create(
|
|
|
|
CAIRO_FORMAT_A8, MAX(0, self->glow_w), MAX(0, self->glow_h));
|
|
|
|
unsigned char *data = cairo_image_surface_get_data(corner);
|
|
|
|
int stride = cairo_image_surface_get_stride(corner);
|
2021-11-10 17:58:27 +01:00
|
|
|
|
|
|
|
// Smooth out the curve, so that the edge of the glow isn't too jarring.
|
|
|
|
const double fade_factor = 1.5;
|
|
|
|
|
2022-07-23 20:39:19 +02:00
|
|
|
const int x_max = self->glow_w - 1;
|
|
|
|
const int y_max = self->glow_h - 1;
|
2021-11-10 17:58:27 +01:00
|
|
|
const double x_scale = 1. / MAX(1, x_max);
|
|
|
|
const double y_scale = 1. / MAX(1, y_max);
|
|
|
|
for (int y = 0; y <= y_max; y++)
|
|
|
|
for (int x = 0; x <= x_max; x++) {
|
|
|
|
const double xn = x_scale * (x_max - x);
|
|
|
|
const double yn = y_scale * (y_max - y);
|
|
|
|
double v = MIN(sqrt(xn * xn + yn * yn), 1);
|
|
|
|
data[y * stride + x] = round(pow(1 - v, fade_factor) * 255);
|
|
|
|
}
|
2022-07-23 20:39:19 +02:00
|
|
|
|
|
|
|
cairo_surface_mark_dirty(corner);
|
|
|
|
self->glow = cairo_pattern_create_for_surface(corner);
|
|
|
|
self->glow_padded = cairo_pattern_create_for_surface(corner);
|
|
|
|
cairo_pattern_set_extend(self->glow_padded, CAIRO_EXTEND_PAD);
|
|
|
|
|
|
|
|
#ifdef GDK_WINDOWING_QUARTZ
|
|
|
|
// Cairo's Quartz backend doesn't support CAIRO_EXTEND_PAD, work around it.
|
|
|
|
if (GDK_IS_QUARTZ_DISPLAY(gtk_widget_get_display(widget))) {
|
|
|
|
int max_size = fiv_thumbnail_sizes[FIV_THUMBNAIL_SIZE_MAX].size;
|
|
|
|
cairo_surface_t *padded = cairo_image_surface_create(CAIRO_FORMAT_A8,
|
|
|
|
cairo_image_surface_get_width(corner) +
|
|
|
|
max_size * FIV_THUMBNAIL_WIDE_COEFFICIENT,
|
|
|
|
cairo_image_surface_get_height(corner) + max_size);
|
|
|
|
cairo_t *cr = cairo_create(padded);
|
|
|
|
cairo_set_source(cr, self->glow_padded);
|
|
|
|
cairo_paint(cr);
|
|
|
|
cairo_destroy(cr);
|
|
|
|
|
|
|
|
cairo_pattern_destroy(self->glow_padded);
|
|
|
|
self->glow_padded = cairo_pattern_create_for_surface(padded);
|
|
|
|
cairo_surface_destroy(padded);
|
|
|
|
}
|
|
|
|
#endif // GDK_WINDOWING_QUARTZ
|
|
|
|
|
|
|
|
cairo_surface_destroy(corner);
|
2021-11-10 17:58:27 +01:00
|
|
|
}
|
|
|
|
|
2021-10-17 15:38:27 +02:00
|
|
|
static void
|
2021-12-18 06:38:30 +01:00
|
|
|
fiv_browser_class_init(FivBrowserClass *klass)
|
2021-10-17 15:38:27 +02:00
|
|
|
{
|
|
|
|
GObjectClass *object_class = G_OBJECT_CLASS(klass);
|
2021-12-18 06:38:30 +01:00
|
|
|
object_class->finalize = fiv_browser_finalize;
|
|
|
|
object_class->get_property = fiv_browser_get_property;
|
|
|
|
object_class->set_property = fiv_browser_set_property;
|
2021-11-21 16:03:54 +01:00
|
|
|
|
|
|
|
browser_properties[PROP_THUMBNAIL_SIZE] = g_param_spec_enum(
|
|
|
|
"thumbnail-size", "Thumbnail size", "The thumbnail height to use",
|
2021-12-28 19:58:14 +01:00
|
|
|
FIV_TYPE_THUMBNAIL_SIZE, FIV_THUMBNAIL_SIZE_NORMAL,
|
2021-11-21 16:03:54 +01:00
|
|
|
G_PARAM_READWRITE);
|
2023-03-30 20:52:57 +02:00
|
|
|
browser_properties[PROP_SHOW_LABELS] = g_param_spec_boolean(
|
|
|
|
"show-labels", "Show labels", "Whether to show filename labels",
|
|
|
|
FALSE, G_PARAM_READWRITE);
|
2021-11-21 16:03:54 +01:00
|
|
|
g_object_class_install_properties(
|
|
|
|
object_class, N_PROPERTIES, browser_properties);
|
|
|
|
|
2022-01-09 10:09:06 +01:00
|
|
|
g_object_class_override_property(
|
|
|
|
object_class, PROP_HADJUSTMENT, "hadjustment");
|
|
|
|
g_object_class_override_property(
|
|
|
|
object_class, PROP_VADJUSTMENT, "vadjustment");
|
|
|
|
g_object_class_override_property(
|
|
|
|
object_class, PROP_HSCROLL_POLICY, "hscroll-policy");
|
|
|
|
g_object_class_override_property(
|
|
|
|
object_class, PROP_VSCROLL_POLICY, "vscroll-policy");
|
|
|
|
|
2021-11-21 16:03:54 +01:00
|
|
|
browser_signals[ITEM_ACTIVATED] = g_signal_new("item-activated",
|
|
|
|
G_TYPE_FROM_CLASS(klass), 0, 0, NULL, NULL, NULL,
|
2021-11-27 02:33:28 +01:00
|
|
|
G_TYPE_NONE, 2, G_TYPE_FILE, GTK_TYPE_PLACES_OPEN_FLAGS);
|
2021-10-17 15:38:27 +02:00
|
|
|
|
|
|
|
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
|
2021-12-18 06:38:30 +01:00
|
|
|
widget_class->get_request_mode = fiv_browser_get_request_mode;
|
|
|
|
widget_class->get_preferred_width = fiv_browser_get_preferred_width;
|
2021-10-17 15:38:27 +02:00
|
|
|
widget_class->get_preferred_height_for_width =
|
2021-12-18 06:38:30 +01:00
|
|
|
fiv_browser_get_preferred_height_for_width;
|
|
|
|
widget_class->realize = fiv_browser_realize;
|
|
|
|
widget_class->draw = fiv_browser_draw;
|
|
|
|
widget_class->size_allocate = fiv_browser_size_allocate;
|
|
|
|
widget_class->button_press_event = fiv_browser_button_press_event;
|
2022-02-14 02:05:14 +01:00
|
|
|
widget_class->button_release_event = fiv_browser_button_release_event;
|
2021-12-18 06:38:30 +01:00
|
|
|
widget_class->motion_notify_event = fiv_browser_motion_notify_event;
|
2022-02-22 15:35:32 +01:00
|
|
|
widget_class->drag_begin = fiv_browser_drag_begin;
|
|
|
|
widget_class->drag_data_get = fiv_browser_drag_data_get;
|
2021-12-18 06:38:30 +01:00
|
|
|
widget_class->scroll_event = fiv_browser_scroll_event;
|
2022-01-10 17:54:41 +01:00
|
|
|
widget_class->key_press_event = fiv_browser_key_press_event;
|
2021-12-18 06:38:30 +01:00
|
|
|
widget_class->query_tooltip = fiv_browser_query_tooltip;
|
2022-01-11 12:24:49 +01:00
|
|
|
widget_class->popup_menu = fiv_browser_popup_menu;
|
2021-12-18 06:38:30 +01:00
|
|
|
widget_class->style_updated = fiv_browser_style_updated;
|
2021-10-17 15:38:27 +02:00
|
|
|
|
2021-11-22 15:08:34 +01:00
|
|
|
// Could be split to also-idiomatic row-spacing/column-spacing properties.
|
|
|
|
// The GParamSpec is sinked by this call.
|
|
|
|
gtk_widget_class_install_style_property(widget_class,
|
|
|
|
g_param_spec_int("spacing", "Spacing", "Space between items",
|
|
|
|
0, G_MAXINT, 1, G_PARAM_READWRITE));
|
|
|
|
|
2021-10-17 15:38:27 +02:00
|
|
|
// TODO(p): Later override "screen_changed", recreate Pango layouts there,
|
|
|
|
// if we get to have any, or otherwise reflect DPI changes.
|
2021-12-18 06:38:30 +01:00
|
|
|
gtk_widget_class_set_css_name(widget_class, "fiv-browser");
|
2021-10-17 15:38:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2021-12-18 06:38:30 +01:00
|
|
|
fiv_browser_init(FivBrowser *self)
|
2021-10-17 15:38:27 +02:00
|
|
|
{
|
|
|
|
gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE);
|
2021-11-22 15:18:58 +01:00
|
|
|
gtk_widget_set_has_tooltip(GTK_WIDGET(self), TRUE);
|
2021-10-17 15:38:27 +02:00
|
|
|
|
2023-05-30 10:36:11 +02:00
|
|
|
self->entries =
|
|
|
|
g_ptr_array_new_with_free_func((GDestroyNotify) entry_destroy);
|
2021-11-04 19:35:08 +01:00
|
|
|
self->layouted_rows = g_array_new(FALSE, TRUE, sizeof(Row));
|
|
|
|
g_array_set_clear_func(self->layouted_rows, (GDestroyNotify) row_free);
|
2022-02-22 15:35:32 +01:00
|
|
|
abort_button_tracking(self);
|
2022-01-08 07:44:39 +01:00
|
|
|
|
2022-06-04 16:28:18 +02:00
|
|
|
self->thumbnail_cache = g_hash_table_new_full(g_str_hash, g_str_equal,
|
|
|
|
g_free, (GDestroyNotify) cairo_surface_destroy);
|
|
|
|
|
2022-01-08 07:44:39 +01:00
|
|
|
self->thumbnailers_len = g_get_num_processors();
|
|
|
|
self->thumbnailers =
|
|
|
|
g_malloc0_n(self->thumbnailers_len, sizeof *self->thumbnailers);
|
|
|
|
for (size_t i = 0; i < self->thumbnailers_len; i++)
|
|
|
|
self->thumbnailers[i].self = self;
|
2022-08-09 15:53:21 +02:00
|
|
|
g_queue_init(&self->thumbnailers_queue);
|
2021-11-04 19:35:08 +01:00
|
|
|
|
2021-12-28 19:58:14 +01:00
|
|
|
set_item_size(self, FIV_THUMBNAIL_SIZE_NORMAL);
|
2023-03-30 20:52:57 +02:00
|
|
|
self->show_labels = FALSE;
|
2022-07-23 20:39:19 +02:00
|
|
|
self->glow_padded = cairo_pattern_create_rgba(0, 0, 0, 0);
|
|
|
|
self->glow = cairo_pattern_create_rgba(0, 0, 0, 0);
|
2021-11-03 14:15:05 +01:00
|
|
|
|
2021-11-21 16:03:54 +01:00
|
|
|
g_signal_connect_swapped(gtk_settings_get_default(),
|
|
|
|
"notify::gtk-icon-theme-name", G_CALLBACK(reload_thumbnails), self);
|
2022-07-21 11:09:16 +02:00
|
|
|
|
|
|
|
GtkGesture *lp = gtk_gesture_long_press_new(GTK_WIDGET(self));
|
|
|
|
gtk_gesture_single_set_touch_only(GTK_GESTURE_SINGLE(lp), TRUE);
|
|
|
|
gtk_event_controller_set_propagation_phase(
|
|
|
|
GTK_EVENT_CONTROLLER(lp), GTK_PHASE_BUBBLE);
|
|
|
|
g_object_set_data_full(
|
|
|
|
G_OBJECT(self), "fiv-browser-long-press-gesture", lp, g_object_unref);
|
|
|
|
|
|
|
|
g_signal_connect(lp, "pressed", G_CALLBACK(on_long_press), self);
|
2021-11-03 14:15:05 +01:00
|
|
|
}
|
|
|
|
|
2021-11-21 16:03:54 +01:00
|
|
|
// --- Public interface --------------------------------------------------------
|
2021-11-12 12:20:39 +01:00
|
|
|
|
2021-12-31 02:19:17 +01:00
|
|
|
static void
|
2023-05-30 10:36:11 +02:00
|
|
|
on_model_reloaded(FivIoModel *model, FivBrowser *self)
|
2021-10-17 15:38:27 +02:00
|
|
|
{
|
2021-12-31 02:19:17 +01:00
|
|
|
g_return_if_fail(model == self->model);
|
2021-12-27 23:19:17 +01:00
|
|
|
|
2022-01-10 17:54:41 +01:00
|
|
|
gchar *selected_uri = NULL;
|
2022-01-12 11:12:01 +01:00
|
|
|
if (self->selected)
|
2023-04-14 07:11:49 +02:00
|
|
|
selected_uri = g_strdup(self->selected->e->uri);
|
2022-01-10 17:54:41 +01:00
|
|
|
|
2022-01-08 07:44:39 +01:00
|
|
|
thumbnailers_abort(self);
|
2021-11-20 13:18:05 +01:00
|
|
|
g_array_set_size(self->layouted_rows, 0);
|
2023-05-30 10:36:11 +02:00
|
|
|
g_ptr_array_set_size(self->entries, 0);
|
2021-11-21 00:21:52 +01:00
|
|
|
|
2022-06-04 01:19:56 +02:00
|
|
|
gsize len = 0;
|
2023-04-14 07:11:49 +02:00
|
|
|
FivIoModelEntry *const *files = fiv_io_model_get_files(self->model, &len);
|
2022-06-04 01:19:56 +02:00
|
|
|
for (gsize i = 0; i < len; i++) {
|
2023-05-30 10:36:11 +02:00
|
|
|
g_ptr_array_add(
|
|
|
|
self->entries, entry_new(fiv_io_model_entry_ref(files[i])));
|
2021-10-17 15:38:27 +02:00
|
|
|
}
|
2021-11-21 21:05:45 +01:00
|
|
|
|
2022-01-12 11:12:01 +01:00
|
|
|
fiv_browser_select(self, selected_uri);
|
2022-01-10 17:54:41 +01:00
|
|
|
g_free(selected_uri);
|
|
|
|
|
2021-11-21 16:03:54 +01:00
|
|
|
reload_thumbnails(self);
|
2022-01-08 07:44:39 +01:00
|
|
|
thumbnailers_start(self);
|
2021-10-17 15:38:27 +02:00
|
|
|
}
|
2021-12-31 02:19:17 +01:00
|
|
|
|
2023-05-30 10:36:11 +02:00
|
|
|
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) {
|
2023-06-01 20:55:41 +02:00
|
|
|
Entry *entry = entry_new(fiv_io_model_entry_ref(new));
|
|
|
|
g_ptr_array_add(self->entries, entry);
|
2023-05-30 10:36:11 +02:00
|
|
|
|
2023-06-01 20:55:41 +02:00
|
|
|
reload_one_thumbnail(self, entry);
|
|
|
|
// TODO(p): Try to add to thumbnailer queue if already started.
|
2023-05-30 10:36:11 +02:00
|
|
|
thumbnailers_start(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.
|
2023-06-01 20:55:41 +02:00
|
|
|
reload_one_thumbnail(self, found);
|
|
|
|
// TODO(p): Try to add to thumbnailer queue if already started.
|
2023-05-30 10:36:11 +02:00
|
|
|
thumbnailers_start(self);
|
|
|
|
} else {
|
|
|
|
found->removed = TRUE;
|
|
|
|
gtk_widget_queue_draw(GTK_WIDGET(self));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-31 02:19:17 +01:00
|
|
|
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);
|
|
|
|
|
2023-05-30 10:36:11 +02:00
|
|
|
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);
|
2021-12-31 02:19:17 +01:00
|
|
|
return GTK_WIDGET(self);
|
|
|
|
}
|
2022-01-12 11:12:01 +01:00
|
|
|
|
2022-02-13 13:18:16 +01:00
|
|
|
static void
|
|
|
|
scroll_to_selection(FivBrowser *self)
|
|
|
|
{
|
|
|
|
for (gsize y = 0; y < self->layouted_rows->len; y++) {
|
|
|
|
const Row *row = &g_array_index(self->layouted_rows, Row, y);
|
|
|
|
for (gsize x = 0; x < row->len; x++) {
|
|
|
|
if (row->items[x].entry == self->selected) {
|
|
|
|
scroll_to_row(self, row);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-12 11:12:01 +01:00
|
|
|
void
|
|
|
|
fiv_browser_select(FivBrowser *self, const char *uri)
|
|
|
|
{
|
|
|
|
g_return_if_fail(FIV_IS_BROWSER(self));
|
|
|
|
|
|
|
|
self->selected = NULL;
|
|
|
|
gtk_widget_queue_draw(GTK_WIDGET(self));
|
|
|
|
if (!uri)
|
|
|
|
return;
|
|
|
|
|
|
|
|
for (guint i = 0; i < self->entries->len; i++) {
|
2023-05-30 10:36:11 +02:00
|
|
|
const Entry *entry = self->entries->pdata[i];
|
2023-04-14 07:11:49 +02:00
|
|
|
if (!g_strcmp0(entry->e->uri, uri)) {
|
2022-01-12 11:12:01 +01:00
|
|
|
self->selected = entry;
|
2022-02-13 13:18:16 +01:00
|
|
|
scroll_to_selection(self);
|
2022-01-12 11:12:01 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|