Compare commits

...

1 Commits

Author SHA1 Message Date
Nelson Benítez León
3744f127d2 Add API to center listview/columnview items when scrolling
· A new GtkScrollInfoCenter enum with the different center flags to use
   in scroll_to() functions:

     GTK_SCROLL_INFO_CENTER_ROW: This will center the target
       row item along the visible part of list. If target
       row is already visible, will do nothing.

     GTK_SCROLL_INFO_CENTER_ROW_ALWAYS: Like `GTK_SCROLL_INFO_CENTER_ROW`
       but will work even if target row is already visible.

     GTK_SCROLL_INFO_CENTER_COL: When scrolling to a column
       item in a columnview, this will center it across the
       visible part of list. If target column is already
       visible, will do nothing.

     GTK_SCROLL_INFO_CENTER_COL_ALWAYS: Like `GTK_SCROLL_INFO_CENTER_COL`
       but will work even if target column is already visible.

 · A new GtkScrollInfo public field called `center_flags` to
     store how we want to center target row/column as a set of
     the new GtkScrollInfoCenter enum values.

 · Action "list.scroll-to-item" gains a new GtkScrollInfoCenter
   flag parameter named @center_flags, but ensuring that the
   previous action with no parameter keeps working.

Some widgets are also added to gtk4-demo program to easily
test/demo the scrolling_to() functionality, it's in the panel
"Lists -> Characters" and can be directly run with:

  gtk4-demo --run=listview_ucd
2023-09-14 00:28:11 +01:00
5 changed files with 342 additions and 21 deletions

View File

@@ -1,9 +1,11 @@
/* Lists/Characters
/* Lists/Characters (scroll_to)
*
* This demo shows a multi-column representation of some parts
* of the Unicode Character Database, or UCD.
*
* The dataset used here has 33796 items.
*
* It includes some widgets to demo the scroll_to() API functionality.
*/
#include <gtk/gtk.h>
@@ -18,6 +20,7 @@ struct _UcdItem
GObject parent_instance;
gunichar codepoint;
const char *name;
guint colnumber;
};
struct _UcdItemClass
@@ -39,7 +42,8 @@ ucd_item_class_init (UcdItemClass *class)
static UcdItem *
ucd_item_new (gunichar codepoint,
const char *name)
const char *name,
guint col)
{
UcdItem *item;
@@ -47,6 +51,7 @@ ucd_item_new (gunichar codepoint,
item->codepoint = codepoint;
item->name = name;
item->colnumber = col;
return item;
}
@@ -63,6 +68,12 @@ ucd_item_get_name (UcdItem *item)
return item->name;
}
static guint
ucd_item_get_colnumber (UcdItem *item)
{
return item->colnumber;
}
static GListModel *
ucd_model_new (void)
{
@@ -71,6 +82,7 @@ ucd_model_new (void)
GVariantIter *iter;
GListStore *store;
guint u;
guint colnumber;
char *name;
bytes = g_resources_lookup_data ("/listview_ucd_data/ucdnames.data", 0, NULL);
@@ -79,13 +91,15 @@ ucd_model_new (void)
iter = g_variant_iter_new (v);
store = g_list_store_new (G_TYPE_OBJECT);
colnumber = 1;
while (g_variant_iter_next (iter, "(u&s)", &u, &name))
{
if (u == 0)
continue;
UcdItem *item = ucd_item_new (u, name);
UcdItem *item = ucd_item_new (u, name, colnumber);
g_list_store_append (store, item);
colnumber++;
g_object_unref (item);
}
@@ -127,6 +141,24 @@ setup_ellipsizing_label (GtkSignalListItemFactory *factory,
gtk_list_item_set_child (GTK_LIST_ITEM (listitem), label);
}
static void
bind_colnumber (GtkSignalListItemFactory *factory,
GObject *listitem,
GListModel *ucd_model)
{
GtkWidget *label;
GObject *item;
uint colnumber;
char buffer[16] = { 0, };
label = gtk_list_item_get_child (GTK_LIST_ITEM (listitem));
item = gtk_list_item_get_item (GTK_LIST_ITEM (listitem));
colnumber = ucd_item_get_colnumber (UCD_ITEM (item));
g_snprintf (buffer, 10, "%u", colnumber);
gtk_label_set_label (GTK_LABEL (label), buffer);
}
static void
bind_codepoint (GtkSignalListItemFactory *factory,
GObject *listitem)
@@ -278,6 +310,21 @@ create_ucd_view (GtkWidget *label)
cv = gtk_column_view_new (GTK_SELECTION_MODEL (selection));
gtk_column_view_set_show_column_separators (GTK_COLUMN_VIEW (cv), TRUE);
// HACK: This is a non-visible empty column we add here because later we use this model
// for the GtkDropDown of columns, and we need an empty item to mean "nothing is selected".
column = gtk_column_view_column_new (NULL, NULL);
gtk_column_view_column_set_visible (column, FALSE);
gtk_column_view_append_column (GTK_COLUMN_VIEW (cv), column);
g_object_unref (column);
factory = gtk_signal_list_item_factory_new ();
g_signal_connect (factory, "setup", G_CALLBACK (setup_centered_label), NULL);
g_signal_connect (factory, "bind", G_CALLBACK (bind_colnumber), NULL);
column = gtk_column_view_column_new ("Row nº", factory);
gtk_column_view_append_column (GTK_COLUMN_VIEW (cv), column);
g_object_unref (column);
factory = gtk_signal_list_item_factory_new ();
g_signal_connect (factory, "setup", G_CALLBACK (setup_centered_label), NULL);
g_signal_connect (factory, "bind", G_CALLBACK (bind_codepoint), NULL);
@@ -336,6 +383,7 @@ create_ucd_view (GtkWidget *label)
}
static GtkWidget *window;
static GtkWidget *spin, *check_h, *check_v, *check_ha, *check_va, *list_view, *dropdown_cols;
static void
remove_provider (gpointer data)
@@ -346,13 +394,97 @@ remove_provider (gpointer data)
g_object_unref (provider);
}
static void
dropdown_cols_changed (GObject *object,
GParamSpec *pspec,
gpointer data)
{
guint selected_pos;
gboolean any_col_selected;
selected_pos = gtk_drop_down_get_selected (GTK_DROP_DOWN (object));
any_col_selected = (selected_pos && selected_pos != GTK_INVALID_LIST_POSITION);
if (any_col_selected)
{
gtk_widget_set_sensitive (check_h, TRUE);
gtk_widget_set_sensitive (check_ha, TRUE);
}
else
{
gtk_check_button_set_active (GTK_CHECK_BUTTON (check_h), FALSE);
gtk_check_button_set_active (GTK_CHECK_BUTTON (check_ha), FALSE);
gtk_widget_set_sensitive (check_h, FALSE);
gtk_widget_set_sensitive (check_ha, FALSE);
}
}
static void
scroll_to_cb (GtkWidget *button, gpointer data)
{
GtkColumnViewColumn *center_column;
GtkScrollInfo *scroll_info;
gboolean col, col_always, row, row_always;
guint pos;
GtkListScrollFlags flags = GTK_LIST_SCROLL_SELECT;
row = gtk_check_button_get_active (GTK_CHECK_BUTTON (check_v));
row_always = gtk_check_button_get_active (GTK_CHECK_BUTTON (check_va));
col = gtk_check_button_get_active (GTK_CHECK_BUTTON (check_h));
col_always = gtk_check_button_get_active (GTK_CHECK_BUTTON (check_ha));
scroll_info = NULL;
if (row || row_always || col || col_always)
{
GtkScrollInfoCenter center_flags = GTK_SCROLL_INFO_CENTER_NONE;
if (row)
center_flags |= GTK_SCROLL_INFO_CENTER_ROW;
if (row_always)
center_flags |= GTK_SCROLL_INFO_CENTER_ROW_ALWAYS;
if (col)
center_flags |= GTK_SCROLL_INFO_CENTER_COL;
if (col_always)
center_flags |= GTK_SCROLL_INFO_CENTER_COL_ALWAYS;
scroll_info = gtk_scroll_info_new ();
gtk_scroll_info_set_center_flags (scroll_info, center_flags);
}
center_column = gtk_drop_down_get_selected_item (GTK_DROP_DOWN (dropdown_cols));
if (!gtk_column_view_column_get_visible (center_column))
center_column = NULL;
pos = (guint) gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (spin));
gtk_column_view_scroll_to (GTK_COLUMN_VIEW (list_view), pos - 1, center_column, flags, scroll_info);
}
static GtkWidget *
pack_with_label (const char *str, GtkWidget *widget1, GtkWidget *widget2)
{
GtkWidget *box, *label;
box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
if (str)
{
label = gtk_label_new (str);
gtk_box_append (GTK_BOX (box), label);
}
gtk_box_append (GTK_BOX (box), widget1);
if (widget2)
gtk_box_append (GTK_BOX (box), widget2);
return box;
}
GtkWidget *
do_listview_ucd (GtkWidget *do_widget)
{
if (window == NULL)
{
GtkWidget *listview, *sw;
GtkWidget *box, *label;
GtkWidget *box, *label, *box2, *button;
GtkCssProvider *provider;
window = gtk_window_new ();
@@ -362,6 +494,36 @@ do_listview_ucd (GtkWidget *do_widget)
gtk_widget_get_display (do_widget));
g_object_add_weak_pointer (G_OBJECT (window), (gpointer *) &window);
box2 = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
gtk_widget_set_margin_start (GTK_WIDGET (box2), 20);
gtk_widget_set_margin_top (GTK_WIDGET (box2), 20);
gtk_widget_set_margin_bottom (GTK_WIDGET (box2), 15);
spin = gtk_spin_button_new_with_range (1.0, 33796.0, 1.0);
gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (spin), TRUE);
gtk_spin_button_set_wrap (GTK_SPIN_BUTTON (spin), TRUE);
gtk_widget_set_valign (spin, GTK_ALIGN_CENTER);
check_v = gtk_check_button_new_with_label ("center row");
check_va = gtk_check_button_new_with_label ("center row always");
check_h = gtk_check_button_new_with_label ("center col");
check_ha = gtk_check_button_new_with_label ("center col always");
gtk_widget_set_sensitive (check_h, FALSE);
gtk_widget_set_sensitive (check_ha, FALSE);
button = gtk_button_new_with_label ("Scroll to row / col");
gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
g_signal_connect (button, "clicked", G_CALLBACK (scroll_to_cb), NULL);
dropdown_cols = gtk_drop_down_new (NULL, NULL);
gtk_drop_down_set_show_arrow (GTK_DROP_DOWN (dropdown_cols), FALSE);
gtk_box_append (GTK_BOX (box2), pack_with_label ("Row nº to scroll to:", spin, NULL));
gtk_box_append (GTK_BOX (box2), pack_with_label ("Col to scroll (optional):", dropdown_cols, NULL));
gtk_box_append (GTK_BOX (box2), pack_with_label (NULL, pack_with_label (NULL, check_v, check_va),
pack_with_label (NULL, check_h, check_ha)));
gtk_box_append (GTK_BOX (box2), button);
box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
label = gtk_label_new ("");
gtk_label_set_width_chars (GTK_LABEL (label), 2);
@@ -374,10 +536,18 @@ do_listview_ucd (GtkWidget *do_widget)
sw = gtk_scrolled_window_new ();
gtk_scrolled_window_set_propagate_natural_width (GTK_SCROLLED_WINDOW (sw), TRUE);
gtk_scrolled_window_set_propagate_natural_height (GTK_SCROLLED_WINDOW (sw), TRUE);
listview = create_ucd_view (label);
gtk_drop_down_set_model (GTK_DROP_DOWN (dropdown_cols),
gtk_column_view_get_columns (GTK_COLUMN_VIEW (listview)));
gtk_drop_down_set_expression (GTK_DROP_DOWN (dropdown_cols),
gtk_property_expression_new (GTK_TYPE_COLUMN_VIEW_COLUMN,
NULL, "title"));
g_signal_connect (dropdown_cols, "notify::selected", G_CALLBACK (dropdown_cols_changed), NULL);
list_view = listview;
gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), listview);
gtk_box_prepend (GTK_BOX (box), sw);
gtk_window_set_child (GTK_WINDOW (window), box);
gtk_window_set_child (GTK_WINDOW (window), pack_with_label (NULL, box2, box));
g_object_set_data_full (G_OBJECT (window), "provider", provider, remove_provider);
}

View File

@@ -1786,16 +1786,36 @@ gtk_column_view_scroll_to_column (GtkColumnView *self,
GtkColumnViewColumn *column,
GtkScrollInfo *scroll_info)
{
int col_x, col_width, new_value;
int col_x, col_width, new_value, viewport_size;
gboolean center, center_always, cell_is_visible;
GtkScrollInfoCenter center_flags;
gtk_column_view_column_get_header_allocation (column, &col_x, &col_width);
viewport_size = gtk_adjustment_get_page_size (self->hadjustment);
new_value = gtk_scroll_info_compute_for_orientation (scroll_info,
GTK_ORIENTATION_HORIZONTAL,
col_x,
col_width,
gtk_adjustment_get_value (self->hadjustment),
gtk_adjustment_get_page_size (self->hadjustment));
viewport_size);
if (scroll_info)
{
center_flags = gtk_scroll_info_get_center_flags (scroll_info);
center = (center_flags & GTK_SCROLL_INFO_CENTER_COL);
center_always = (center_flags & GTK_SCROLL_INFO_CENTER_COL_ALWAYS);
if (new_value == col_x || /* cell aligned at start of viewport */
new_value + viewport_size == col_x + col_width) /* cell aligned at end of viewport */
cell_is_visible = FALSE;
else
cell_is_visible = TRUE;
if (center_always || (center && !cell_is_visible))
new_value = (int) (col_x - ((viewport_size - col_width) / 2));
}
gtk_adjustment_set_value (self->hadjustment, new_value);

View File

@@ -87,6 +87,9 @@ struct _GtkListBasePrivate
GtkGesture *drag_gesture;
RubberbandData *rubberband;
/* Whether the user asked to vertically center target item for current scroll operation */
gboolean center_vertically;
guint autoscroll_id;
double autoscroll_delta_x;
double autoscroll_delta_y;
@@ -825,31 +828,58 @@ gtk_list_base_compute_scroll_align (int cell_start,
int visible_size,
double current_align,
GtkPackType current_side,
GtkScrollInfoCenter center_flags,
double *new_align,
GtkPackType *new_side)
GtkPackType *new_side,
gboolean *do_center)
{
int cell_end, visible_end;
visible_end = visible_start + visible_size;
cell_end = cell_start + cell_size;
gboolean center_always = (center_flags & GTK_SCROLL_INFO_CENTER_ROW_ALWAYS);
gboolean center = (center_flags & GTK_SCROLL_INFO_CENTER_ROW);
if (do_center)
*do_center = FALSE;
if (cell_size <= visible_size)
{
if (cell_start < visible_start)
{
*new_align = 0.0;
*new_side = GTK_PACK_START;
if (center || center_always)
{
*new_align = 0.5;
if (do_center)
*do_center = TRUE;
}
else
*new_align = 0.0;
}
else if (cell_end > visible_end)
{
*new_align = 1.0;
*new_side = GTK_PACK_END;
if (center || center_always)
{
*new_align = 0.5;
if (do_center)
*do_center = TRUE;
}
else
*new_align = 1.0;
}
else
{
/* XXX: start or end here? */
*new_side = GTK_PACK_START;
*new_align = (double) (cell_start - visible_start) / visible_size;
if (center_always || (center && (visible_end == cell_end || visible_start == cell_start)))
{
*new_align = 0.5;
if (do_center)
*do_center = TRUE;
}
else
*new_align = (double) (cell_start - visible_start) / visible_size;
}
}
else
@@ -903,12 +933,14 @@ gtk_list_base_scroll_to_item (GtkListBase *self,
gtk_list_base_compute_scroll_align (area.y, area.height,
y, viewport.height,
priv->anchor_align_along, priv->anchor_side_along,
&align_along, &side_along);
scroll ? gtk_scroll_info_get_center_flags (scroll) : 0,
&align_along, &side_along, &priv->center_vertically);
gtk_list_base_compute_scroll_align (area.x, area.width,
x, viewport.width,
priv->anchor_align_across, priv->anchor_side_across,
&align_across, &side_across);
GTK_SCROLL_INFO_CENTER_NONE,
&align_across, &side_across, NULL);
gtk_list_base_set_anchor (self,
pos,
@@ -924,14 +956,24 @@ gtk_list_base_scroll_to_item_action (GtkWidget *widget,
GVariant *parameter)
{
GtkListBase *self = GTK_LIST_BASE (widget);
GtkScrollInfo *scroll_info = NULL;
GtkScrollInfoCenter center_flags = 0;
guint pos;
if (!g_variant_check_format_string (parameter, "u", FALSE))
if (g_variant_check_format_string (parameter, "(uu)", FALSE))
g_variant_get (parameter, "(uu)", &pos, &center_flags);
else if (g_variant_check_format_string (parameter, "u", FALSE))
g_variant_get (parameter, "u", &pos);
else
return;
g_variant_get (parameter, "u", &pos);
if (center_flags)
{
scroll_info = gtk_scroll_info_new ();
gtk_scroll_info_set_center_flags (scroll_info, center_flags);
}
gtk_list_base_scroll_to_item (self, pos, NULL);
gtk_list_base_scroll_to_item (self, pos, scroll_info);
}
static void
@@ -1255,13 +1297,15 @@ gtk_list_base_class_init (GtkListBaseClass *klass)
/**
* GtkListBase|list.scroll-to-item:
* @position: position of item to scroll to
* @center_flags: a set of `GtkScrollInfoCenter` flags
* Since: 4.14
*
* Moves the visible area to the item given in @position with the minimum amount
* of scrolling required. If the item is already visible, nothing happens.
* Moves the visible area to the item given in @position and according
* to the @center_flags provided.
*/
gtk_widget_class_install_action (widget_class,
"list.scroll-to-item",
"u",
"(uu)",
gtk_list_base_scroll_to_item_action);
/**
@@ -2006,6 +2050,7 @@ gtk_list_base_init_real (GtkListBase *self,
priv->anchor_side_across = GTK_PACK_START;
priv->selected = gtk_list_item_tracker_new (priv->item_manager);
priv->focus = gtk_list_item_tracker_new (priv->item_manager);
priv->center_vertically = FALSE;
priv->adjustment[GTK_ORIENTATION_HORIZONTAL] = gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
g_object_ref_sink (priv->adjustment[GTK_ORIENTATION_HORIZONTAL]);
@@ -2084,10 +2129,14 @@ gtk_list_base_update_adjustments (GtkListBase *self)
{
value_across = area.x;
value_along = area.y;
if (priv->center_vertically && G_APPROX_VALUE (priv->anchor_align_along, 0.5, DBL_EPSILON))
value_along += area.height / 2;
else if (priv->anchor_side_along == GTK_PACK_END)
value_along += area.height;
if (priv->anchor_side_across == GTK_PACK_END)
value_across += area.width;
if (priv->anchor_side_along == GTK_PACK_END)
value_along += area.height;
value_across -= priv->anchor_align_across * page_across;
value_along -= priv->anchor_align_along * page_along;
}
@@ -2098,6 +2147,10 @@ gtk_list_base_update_adjustments (GtkListBase *self)
}
}
/* Can be reset now as we've already use it to calculate current scroll operation values */
if (priv->center_vertically)
priv->center_vertically = FALSE;
gtk_list_base_set_adjustment_values (self,
OPPOSITE_ORIENTATION (priv->orientation),
value_across,

View File

@@ -38,6 +38,7 @@ struct _GtkScrollInfo
guint ref_count;
gboolean enabled[2]; /* directions */
GtkScrollInfoCenter center_flags;
};
static GtkScrollInfo default_scroll_info = {
@@ -219,6 +220,47 @@ gtk_scroll_info_compute_for_orientation (GtkScrollInfo *self,
return viewport_origin + delta;
}
/**
* gtk_scroll_info_set_center_flags:
* @self: a `GtkScrollInfo`
* @flags: a set of `GtkScrollInfoCenter` flags
*
* Sets the flags that @self will use to center a target item (row or column item)
* when scrolling to it.
*
* Since: 4.14
*/
void
gtk_scroll_info_set_center_flags (GtkScrollInfo *self,
GtkScrollInfoCenter flags)
{
g_return_if_fail (self != NULL);
if (self->center_flags == flags)
return;
self->center_flags = flags;
}
/**
* gtk_scroll_info_get_center_flags:
* @self: a `GtkScrollInfo`
*
* Gets the flags that @self will use to center a target item (row or column item)
* when scrolling to it.
*
* Returns: a set of `GtkScrollInfoCenter` flags
*
* Since: 4.14
*/
GtkScrollInfoCenter
gtk_scroll_info_get_center_flags (GtkScrollInfo *self)
{
g_return_val_if_fail (self != NULL, 0);
return self->center_flags;
}
/*<private>
* gtk_scroll_info_compute_scroll:
* @self: a `GtkScrollInfo`

View File

@@ -30,6 +30,36 @@
G_BEGIN_DECLS
/**
* GtkScrollInfoCenter:
* @GTK_SCROLL_INFO_CENTER_NONE: Don't do anything
* @GTK_SCROLL_INFO_CENTER_ROW: When scrolling vertically
* to a row item, this will center the row item along the
* visible part of list. If row item was already in the
* visible part, this will do nothing.
* @GTK_SCROLL_INFO_CENTER_ROW_ALWAYS: Same as `GTK_SCROLL_INFO_CENTER_ROW`
* but will center the item even if it's already in the
* visible part of list.
* @GTK_SCROLL_INFO_CENTER_COL: When scrolling horizontally
* to a column, this will center the column item across the
* visible part of list. If col item was already in the
* visible part of list, this will do nothing.
* @GTK_SCROLL_INFO_CENTER_COL_ALWAYS: Same as `GTK_SCROLL_INFO_CENTER_COL`
* but will center the item even if it's already in the
* visible part of list.
*
* How we would like to center target item when scrolling to it.
*
* Since: 4.14
*/
typedef enum {
GTK_SCROLL_INFO_CENTER_NONE = 0,
GTK_SCROLL_INFO_CENTER_ROW = 1 << 0,
GTK_SCROLL_INFO_CENTER_ROW_ALWAYS = 1 << 1,
GTK_SCROLL_INFO_CENTER_COL = 1 << 2,
GTK_SCROLL_INFO_CENTER_COL_ALWAYS = 1 << 3
} GtkScrollInfoCenter;
#define GTK_TYPE_SCROLL_INFO (gtk_scroll_info_get_type ())
GDK_AVAILABLE_IN_4_12
@@ -54,6 +84,12 @@ void gtk_scroll_info_set_enable_vertical (GtkScrollInfo
GDK_AVAILABLE_IN_4_12
gboolean gtk_scroll_info_get_enable_vertical (GtkScrollInfo *self);
GDK_AVAILABLE_IN_4_14
void gtk_scroll_info_set_center_flags (GtkScrollInfo *self,
GtkScrollInfoCenter flags);
GDK_AVAILABLE_IN_4_14
GtkScrollInfoCenter gtk_scroll_info_get_center_flags (GtkScrollInfo *self);
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GtkScrollInfo, gtk_scroll_info_unref)