listview: Port various gridview improvements

- Handle anchor as align + top/bottom
  This fixes behavior for cells that are higher than the view
- Add gtk_list_view_adjustment_is_flipped()
  This should fix RTL handling of horizontal lists
- Fix scrolling
  This should make scrolling more reliable, particularly on short lists
  that are only a few pages long.
This commit is contained in:
Benjamin Otte 2019-10-22 03:21:39 +02:00 committed by Matthias Clasen
parent 6b98948f9a
commit ea390a4a73

View File

@ -71,6 +71,7 @@ struct _GtkListView
GtkListItemTracker *anchor;
double anchor_align;
gboolean anchor_start;
/* the last item that was selected - basically the location to extend selections from */
GtkListItemTracker *selected;
/* the item that has input focus */
@ -272,6 +273,37 @@ list_row_get_y (GtkListView *self,
return y ;
}
static gboolean
gtk_list_view_get_size_at_position (GtkListView *self,
guint pos,
int *offset,
int *height)
{
ListRow *row;
guint skip;
int y;
row = gtk_list_item_manager_get_nth (self->item_manager, pos, &skip);
if (row == NULL)
{
if (offset)
*offset = 0;
if (height)
*height = 0;
return FALSE;
}
y = list_row_get_y (self, row);
y += skip * row->height;
if (offset)
*offset = y;
if (height)
*height = row->height;
return TRUE;
}
static int
gtk_list_view_get_list_height (GtkListView *self)
{
@ -289,81 +321,146 @@ gtk_list_view_get_list_height (GtkListView *self)
static void
gtk_list_view_set_anchor (GtkListView *self,
guint position,
double align)
double align,
gboolean start)
{
gtk_list_item_tracker_set_position (self->item_manager,
self->anchor,
position,
GTK_LIST_VIEW_EXTRA_ITEMS + GTK_LIST_VIEW_MAX_LIST_ITEMS * align,
GTK_LIST_VIEW_EXTRA_ITEMS + GTK_LIST_VIEW_MAX_LIST_ITEMS - 1 - GTK_LIST_VIEW_MAX_LIST_ITEMS * align);
if (self->anchor_align != align)
if (self->anchor_align != align || self->anchor_start != start)
{
self->anchor_align = align;
self->anchor_start = start;
gtk_widget_queue_allocate (GTK_WIDGET (self));
}
}
static gboolean
gtk_list_view_adjustment_is_flipped (GtkListView *self,
GtkOrientation orientation)
{
if (orientation == GTK_ORIENTATION_VERTICAL)
return FALSE;
return gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
}
static void
gtk_list_view_adjustment_value_changed_cb (GtkAdjustment *adjustment,
GtkListView *self)
{
if (adjustment == self->adjustment[self->orientation])
{
int page_size, total_size, value, from_start;
int row_start, row_end;
double align;
gboolean top;
guint pos;
int dy;
pos = gtk_list_view_get_position_at_y (self, gtk_adjustment_get_value (adjustment), &dy, NULL);
g_return_if_fail (pos != GTK_INVALID_LIST_POSITION);
gtk_list_view_set_anchor (self, pos, 0);
page_size = gtk_adjustment_get_page_size (adjustment);
value = gtk_adjustment_get_value (adjustment);
total_size = gtk_adjustment_get_upper (adjustment);
if (gtk_list_view_adjustment_is_flipped (self, self->orientation))
value = total_size - page_size - value;
/* Compute how far down we've scrolled. That's the height
* we want to align to. */
align = (double) value / (total_size - page_size);
from_start = round (align * page_size);
pos = gtk_list_view_get_position_at_y (self,
value + from_start,
&row_start, &row_end);
if (pos != GTK_INVALID_LIST_POSITION)
{
/* offset from value - which is where we wanna scroll to */
row_start = from_start - row_start;
row_end += row_start;
/* find an anchor that is in the visible area */
if (row_start > 0 && row_end < page_size)
top = from_start - row_start <= row_end - from_start;
else if (row_start > 0)
top = TRUE;
else if (row_end < page_size)
top = FALSE;
else
{
/* This is the case where the row occupies the whole visible area.
* It's also the only case where align will not end up in [0..1] */
top = from_start - row_start <= row_end - from_start;
}
/* Now compute the align so that when anchoring to the looked
* up row, the position is pixel-exact.
*/
align = (double) (top ? row_start : row_end) / page_size;
}
else
{
/* Happens if we scroll down to the end - we will query
* exactly the pixel behind the last one we can get a row for.
* So take the last row. */
pos = g_list_model_get_n_items (self->model) - 1;
align = 1.0;
top = FALSE;
}
gtk_list_view_set_anchor (self, pos, align, top);
}
gtk_widget_queue_allocate (GTK_WIDGET (self));
}
static void
static int
gtk_list_view_update_adjustments (GtkListView *self,
GtkOrientation orientation)
{
double upper, page_size, value;
int upper, page_size, value;
page_size = gtk_widget_get_size (GTK_WIDGET (self), orientation);
if (orientation == self->orientation)
{
ListRow *row;
guint anchor;
int offset, size;
guint anchor_pos;
if (self->orientation == GTK_ORIENTATION_VERTICAL)
page_size = gtk_widget_get_height (GTK_WIDGET (self));
else
page_size = gtk_widget_get_width (GTK_WIDGET (self));
upper = gtk_list_view_get_list_height (self);
anchor_pos = gtk_list_item_tracker_get_position (self->item_manager, self->anchor);
anchor = gtk_list_item_tracker_get_position (self->item_manager, self->anchor);
row = gtk_list_item_manager_get_nth (self->item_manager, anchor, NULL);
if (row)
value = list_row_get_y (self, row);
else
value = 0;
value -= self->anchor_align * (page_size - (row ? row->height : 0));
if (!gtk_list_view_get_size_at_position (self,
anchor_pos,
&offset,
&size))
{
g_assert_not_reached ();
}
if (!self->anchor_start)
offset += size;
value = offset - self->anchor_align * page_size;
}
else
{
if (self->orientation == GTK_ORIENTATION_VERTICAL)
page_size = gtk_widget_get_width (GTK_WIDGET (self));
else
page_size = gtk_widget_get_height (GTK_WIDGET (self));
upper = self->list_width;
value = 0;
if (_gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
value = upper - page_size - value;
value = gtk_adjustment_get_value (self->adjustment[orientation]);
if (gtk_list_view_adjustment_is_flipped (self, orientation))
value = upper - value - page_size;
}
upper = MAX (upper, page_size);
value = MAX (value, 0);
value = MIN (value, upper - page_size);
g_signal_handlers_block_by_func (self->adjustment[orientation],
gtk_list_view_adjustment_value_changed_cb,
self);
gtk_adjustment_configure (self->adjustment[orientation],
value,
gtk_list_view_adjustment_is_flipped (self, orientation)
? upper - page_size - value
: value,
0,
upper,
page_size * 0.1,
@ -372,6 +469,8 @@ gtk_list_view_update_adjustments (GtkListView *self,
g_signal_handlers_unblock_by_func (self->adjustment[orientation],
gtk_list_view_adjustment_value_changed_cb,
self);
return value;
}
static int
@ -497,6 +596,43 @@ gtk_list_view_measure (GtkWidget *widget,
gtk_list_view_measure_across (widget, orientation, for_size, minimum, natural);
}
static void
gtk_list_view_size_allocate_child (GtkListView *self,
GtkWidget *child,
int x,
int y,
int width,
int height)
{
GtkAllocation child_allocation;
if (self->orientation == GTK_ORIENTATION_VERTICAL)
{
child_allocation.x = x;
child_allocation.y = y;
child_allocation.width = width;
child_allocation.height = height;
}
else if (_gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_LTR)
{
child_allocation.x = y;
child_allocation.y = x;
child_allocation.width = height;
child_allocation.height = width;
}
else
{
int mirror_point = gtk_widget_get_width (GTK_WIDGET (self));
child_allocation.x = mirror_point - y - height;
child_allocation.y = x;
child_allocation.width = height;
child_allocation.height = width;
}
gtk_widget_size_allocate (child, &child_allocation, -1);
}
static void
gtk_list_view_size_allocate (GtkWidget *widget,
int width,
@ -504,10 +640,10 @@ gtk_list_view_size_allocate (GtkWidget *widget,
int baseline)
{
GtkListView *self = GTK_LIST_VIEW (widget);
GtkAllocation child_allocation = { 0, 0, 0, 0 };
ListRow *row;
GArray *heights;
int min, nat, row_height;
int x, y;
GtkOrientation opposite_orientation;
opposite_orientation = OPPOSITE_ORIENTATION (self->orientation);
@ -570,43 +706,26 @@ gtk_list_view_size_allocate (GtkWidget *widget,
}
/* step 3: update the adjustments */
gtk_list_view_update_adjustments (self, GTK_ORIENTATION_HORIZONTAL);
gtk_list_view_update_adjustments (self, GTK_ORIENTATION_VERTICAL);
x = - gtk_list_view_update_adjustments (self, opposite_orientation);
y = - gtk_list_view_update_adjustments (self, self->orientation);
/* step 4: actually allocate the widgets */
child_allocation.x = - round (gtk_adjustment_get_value (self->adjustment[GTK_ORIENTATION_HORIZONTAL]));
child_allocation.y = - round (gtk_adjustment_get_value (self->adjustment[GTK_ORIENTATION_VERTICAL]));
if (self->orientation == GTK_ORIENTATION_VERTICAL)
{
child_allocation.width = self->list_width;
for (row = gtk_list_item_manager_get_first (self->item_manager);
row != NULL;
row = gtk_rb_tree_node_get_next (row))
{
if (row->parent.widget)
{
child_allocation.height = row->height;
gtk_widget_size_allocate (row->parent.widget, &child_allocation, -1);
gtk_list_view_size_allocate_child (self,
row->parent.widget,
x,
y,
self->list_width,
row->height);
}
child_allocation.y += row->height * row->parent.n_items;
}
}
else
{
child_allocation.height = self->list_width;
for (row = gtk_list_item_manager_get_first (self->item_manager);
row != NULL;
row = gtk_rb_tree_node_get_next (row))
{
if (row->parent.widget)
{
child_allocation.width = row->height;
gtk_widget_size_allocate (row->parent.widget, &child_allocation, -1);
}
child_allocation.x += row->height * row->parent.n_items;
}
y += row->height * row->parent.n_items;
}
}
@ -949,50 +1068,95 @@ gtk_list_view_update_focus_tracker (GtkListView *self)
}
}
static void
gtk_list_view_compute_scroll_align (GtkListView *self,
GtkOrientation orientation,
int cell_start,
int cell_end,
double current_align,
gboolean current_start,
double *new_align,
gboolean *new_start)
{
int visible_start, visible_size, visible_end;
int cell_size;
visible_start = gtk_adjustment_get_value (self->adjustment[orientation]);
visible_size = gtk_adjustment_get_page_size (self->adjustment[orientation]);
if (gtk_list_view_adjustment_is_flipped (self, orientation))
visible_start = gtk_adjustment_get_upper (self->adjustment[orientation]) - visible_size - visible_start;
visible_end = visible_start + visible_size;
cell_size = cell_end - cell_start;
if (cell_size <= visible_size)
{
if (cell_start < visible_start)
{
*new_align = 0.0;
*new_start = TRUE;
}
else if (cell_end > visible_end)
{
*new_align = 1.0;
*new_start = FALSE;
}
else
{
/* XXX: start or end here? */
*new_start = TRUE;
*new_align = (double) (cell_start - visible_start) / visible_size;
}
}
else
{
/* This is the unlikely case of the cell being higher than the visible area */
if (cell_start > visible_start)
{
*new_align = 0.0;
*new_start = TRUE;
}
else if (cell_end < visible_end)
{
*new_align = 1.0;
*new_start = FALSE;
}
else
{
/* the cell already covers the whole screen */
*new_align = current_align;
*new_start = current_start;
}
}
}
static void
gtk_list_view_scroll_to_item (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
GtkListView *self = GTK_LIST_VIEW (widget);
ListRow *row;
int start, end;
double align;
gboolean top;
guint pos;
if (!g_variant_check_format_string (parameter, "u", FALSE))
return;
g_variant_get (parameter, "u", &pos);
row = gtk_list_item_manager_get_nth (self->item_manager, pos, NULL);
if (row == NULL)
/* figure out primary orientation and if position is valid */
if (!gtk_list_view_get_size_at_position (self, pos, &start, &end))
return;
if (row->parent.widget)
{
int y = list_row_get_y (self, row);
int start = gtk_adjustment_get_value (self->adjustment[self->orientation]);
int height;
double align;
end += start;
gtk_list_view_compute_scroll_align (self,
self->orientation,
start, end,
self->anchor_align, self->anchor_start,
&align, &top);
if (self->orientation == GTK_ORIENTATION_VERTICAL)
height = gtk_widget_get_height (GTK_WIDGET (self));
else
height = gtk_widget_get_width (GTK_WIDGET (self));
if (y < start)
align = 0.0;
else if (y + row->height > start + height)
align = 1.0;
else
align = (double) (y - start) / (height - row->height);
gtk_list_view_set_anchor (self, pos, align);
}
else
{
if (pos < gtk_list_item_tracker_get_position (self->item_manager, self->anchor))
gtk_list_view_set_anchor (self, pos, 0.0);
else
gtk_list_view_set_anchor (self, pos, 1.0);
}
gtk_list_view_set_anchor (self, pos, align, top);
/* HACK HACK HACK
*
@ -1599,7 +1763,7 @@ gtk_list_view_set_model (GtkListView *self,
selection_model = GTK_SELECTION_MODEL (gtk_single_selection_new (model));
gtk_list_item_manager_set_model (self->item_manager, selection_model);
gtk_list_view_set_anchor (self, 0, 0.0);
gtk_list_view_set_anchor (self, 0, 0.0, TRUE);
g_object_unref (selection_model);
}