gtk/gtk/gtklistview.c
Corey Berla 94673707e6 listviews: Reset scrollbar adjustment when list is empty
In a list with a visible scrollbar, the scrollbar usually becomes
invisible when the numbers of items is less than the required amount
to scroll.  If, however, the list is emptied all at once,
the scrollbar remains.  This happens because when there's an empty
list gtk_list_view_size_allocate() returns early before the scrollbar
adjustment is updated.

Given that the list is empty, simply reset the adjustment values
to zero.

Fixes: https://gitlab.gnome.org/GNOME/gtk/-/issues/4370
2022-07-12 12:54:35 -07:00

1157 lines
34 KiB
C

/*
* Copyright © 2018 Benjamin Otte
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#include "config.h"
#include "gtklistviewprivate.h"
#include "gtkbitset.h"
#include "gtkintl.h"
#include "gtklistbaseprivate.h"
#include "gtklistitemmanagerprivate.h"
#include "gtkmain.h"
#include "gtkprivate.h"
#include "gtkrbtreeprivate.h"
#include "gtkwidgetprivate.h"
#include "gtkmultiselection.h"
/* Maximum number of list items created by the listview.
* For debugging, you can set this to G_MAXUINT to ensure
* there's always a list item for every row.
*/
#define GTK_LIST_VIEW_MAX_LIST_ITEMS 200
/* Extra items to keep above + below every tracker */
#define GTK_LIST_VIEW_EXTRA_ITEMS 2
/**
* GtkListView:
*
* `GtkListView` presents a large dynamic list of items.
*
* `GtkListView` uses its factory to generate one row widget for each visible
* item and shows them in a linear display, either vertically or horizontally.
*
* The [property@Gtk.ListView:show-separators] property offers a simple way to
* display separators between the rows.
*
* `GtkListView` allows the user to select items according to the selection
* characteristics of the model. For models that allow multiple selected items,
* it is possible to turn on _rubberband selection_, using
* [property@Gtk.ListView:enable-rubberband].
*
* If you need multiple columns with headers, see [class@Gtk.ColumnView].
*
* To learn more about the list widget framework, see the
* [overview](section-list-widget.html).
*
* An example of using `GtkListView`:
* ```c
* static void
* setup_listitem_cb (GtkListItemFactory *factory,
* GtkListItem *list_item)
* {
* GtkWidget *image;
*
* image = gtk_image_new ();
* gtk_image_set_icon_size (GTK_IMAGE (image), GTK_ICON_SIZE_LARGE);
* gtk_list_item_set_child (list_item, image);
* }
*
* static void
* bind_listitem_cb (GtkListItemFactory *factory,
* GtkListItem *list_item)
* {
* GtkWidget *image;
* GAppInfo *app_info;
*
* image = gtk_list_item_get_child (list_item);
* app_info = gtk_list_item_get_item (list_item);
* gtk_image_set_from_gicon (GTK_IMAGE (image), g_app_info_get_icon (app_info));
* }
*
* static void
* activate_cb (GtkListView *list,
* guint position,
* gpointer unused)
* {
* GAppInfo *app_info;
*
* app_info = g_list_model_get_item (G_LIST_MODEL (gtk_list_view_get_model (list)), position);
* g_app_info_launch (app_info, NULL, NULL, NULL);
* g_object_unref (app_info);
* }
*
* ...
*
* model = create_application_list ();
*
* factory = gtk_signal_list_item_factory_new ();
* g_signal_connect (factory, "setup", G_CALLBACK (setup_listitem_cb), NULL);
* g_signal_connect (factory, "bind", G_CALLBACK (bind_listitem_cb), NULL);
*
* list = gtk_list_view_new (GTK_SELECTION_MODEL (gtk_single_selection_new (model)), factory);
*
* g_signal_connect (list, "activate", G_CALLBACK (activate_cb), NULL);
*
* gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), list);
* ```
*
* # CSS nodes
*
* ```
* listview[.separators][.rich-list][.navigation-sidebar][.data-table]
* ├── row[.activatable]
* │
* ├── row[.activatable]
* │
* ┊
* ╰── [rubberband]
* ```
*
* `GtkListView` uses a single CSS node named `listview`. It may carry the
* `.separators` style class, when [property@Gtk.ListView:show-separators]
* property is set. Each child widget uses a single CSS node named `row`.
* If the [property@Gtk.ListItem:activatable] property is set, the
* corresponding row will have the `.activatable` style class. For
* rubberband selection, a node with name `rubberband` is used.
*
* The main listview node may also carry style classes to select
* the style of [list presentation](ListContainers.html#list-styles):
* .rich-list, .navigation-sidebar or .data-table.
*
* # Accessibility
*
* `GtkListView` uses the %GTK_ACCESSIBLE_ROLE_LIST role, and the list
* items use the %GTK_ACCESSIBLE_ROLE_LIST_ITEM role.
*/
typedef struct _ListRow ListRow;
typedef struct _ListRowAugment ListRowAugment;
struct _ListRow
{
GtkListItemManagerItem parent;
guint height; /* per row */
};
struct _ListRowAugment
{
GtkListItemManagerItemAugment parent;
guint height; /* total */
};
enum
{
PROP_0,
PROP_FACTORY,
PROP_MODEL,
PROP_SHOW_SEPARATORS,
PROP_SINGLE_CLICK_ACTIVATE,
PROP_ENABLE_RUBBERBAND,
N_PROPS
};
enum {
ACTIVATE,
LAST_SIGNAL
};
G_DEFINE_TYPE (GtkListView, gtk_list_view, GTK_TYPE_LIST_BASE)
static GParamSpec *properties[N_PROPS] = { NULL, };
static guint signals[LAST_SIGNAL] = { 0 };
static void G_GNUC_UNUSED
dump (GtkListView *self)
{
ListRow *row;
guint n_widgets, n_list_rows;
n_widgets = 0;
n_list_rows = 0;
//g_print ("ANCHOR: %u - %u\n", self->anchor_start, self->anchor_end);
for (row = gtk_list_item_manager_get_first (self->item_manager);
row;
row = gtk_rb_tree_node_get_next (row))
{
if (row->parent.widget)
n_widgets++;
n_list_rows++;
g_print (" %4u%s (%upx)\n", row->parent.n_items, row->parent.widget ? " (widget)" : "", row->height);
}
g_print (" => %u widgets in %u list rows\n", n_widgets, n_list_rows);
}
static void
list_row_augment (GtkRbTree *tree,
gpointer node_augment,
gpointer node,
gpointer left,
gpointer right)
{
ListRow *row = node;
ListRowAugment *aug = node_augment;
gtk_list_item_manager_augment_node (tree, node_augment, node, left, right);
aug->height = row->height * row->parent.n_items;
if (left)
{
ListRowAugment *left_aug = gtk_rb_tree_get_augment (tree, left);
aug->height += left_aug->height;
}
if (right)
{
ListRowAugment *right_aug = gtk_rb_tree_get_augment (tree, right);
aug->height += right_aug->height;
}
}
static ListRow *
gtk_list_view_get_row_at_y (GtkListView *self,
int y,
int *offset)
{
ListRow *row, *tmp;
row = gtk_list_item_manager_get_root (self->item_manager);
while (row)
{
tmp = gtk_rb_tree_node_get_left (row);
if (tmp)
{
ListRowAugment *aug = gtk_list_item_manager_get_item_augment (self->item_manager, tmp);
if (y < aug->height)
{
row = tmp;
continue;
}
y -= aug->height;
}
if (y < row->height * row->parent.n_items)
break;
y -= row->height * row->parent.n_items;
row = gtk_rb_tree_node_get_right (row);
}
if (offset)
*offset = row ? y : 0;
return row;
}
static int
list_row_get_y (GtkListView *self,
ListRow *row)
{
ListRow *parent, *left;
int y;
left = gtk_rb_tree_node_get_left (row);
if (left)
{
ListRowAugment *aug = gtk_list_item_manager_get_item_augment (self->item_manager, left);
y = aug->height;
}
else
y = 0;
for (parent = gtk_rb_tree_node_get_parent (row);
parent != NULL;
parent = gtk_rb_tree_node_get_parent (row))
{
left = gtk_rb_tree_node_get_left (parent);
if (left != row)
{
if (left)
{
ListRowAugment *aug = gtk_list_item_manager_get_item_augment (self->item_manager, left);
y += aug->height;
}
y += parent->height * parent->parent.n_items;
}
row = parent;
}
return y ;
}
static int
gtk_list_view_get_list_height (GtkListView *self)
{
ListRow *row;
ListRowAugment *aug;
row = gtk_list_item_manager_get_root (self->item_manager);
if (row == NULL)
return 0;
aug = gtk_list_item_manager_get_item_augment (self->item_manager, row);
return aug->height;
}
static gboolean
gtk_list_view_get_allocation_along (GtkListBase *base,
guint pos,
int *offset,
int *size)
{
GtkListView *self = GTK_LIST_VIEW (base);
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 (size)
*size = 0;
return FALSE;
}
y = list_row_get_y (self, row);
y += skip * row->height;
if (offset)
*offset = y;
if (size)
*size = row->height;
return TRUE;
}
static gboolean
gtk_list_view_get_allocation_across (GtkListBase *base,
guint pos,
int *offset,
int *size)
{
GtkListView *self = GTK_LIST_VIEW (base);
if (offset)
*offset = 0;
if (size)
*size = self->list_width;
return TRUE;
}
static GtkBitset *
gtk_list_view_get_items_in_rect (GtkListBase *base,
const cairo_rectangle_int_t *rect)
{
GtkListView *self = GTK_LIST_VIEW (base);
guint first, last, n_items;
GtkBitset *result;
ListRow *row;
result = gtk_bitset_new_empty ();
n_items = gtk_list_base_get_n_items (base);
if (n_items == 0)
return result;
row = gtk_list_view_get_row_at_y (self, rect->y, NULL);
if (row)
first = gtk_list_item_manager_get_item_position (self->item_manager, row);
else
first = rect->y < 0 ? 0 : n_items - 1;
row = gtk_list_view_get_row_at_y (self, rect->y + rect->height, NULL);
if (row)
last = gtk_list_item_manager_get_item_position (self->item_manager, row);
else
last = rect->y < 0 ? 0 : n_items - 1;
gtk_bitset_add_range_closed (result, first, last);
return result;
}
static guint
gtk_list_view_move_focus_along (GtkListBase *base,
guint pos,
int steps)
{
if (steps < 0)
return pos - MIN (pos, -steps);
else
{
pos += MIN (gtk_list_base_get_n_items (base) - pos - 1, steps);
}
return pos;
}
static gboolean
gtk_list_view_get_position_from_allocation (GtkListBase *base,
int across,
int along,
guint *pos,
cairo_rectangle_int_t *area)
{
GtkListView *self = GTK_LIST_VIEW (base);
ListRow *row;
int remaining;
if (across >= self->list_width)
return FALSE;
row = gtk_list_view_get_row_at_y (self, along, &remaining);
if (row == NULL)
return FALSE;
*pos = gtk_list_item_manager_get_item_position (self->item_manager, row);
g_assert (remaining < row->height * row->parent.n_items);
*pos += remaining / row->height;
if (area)
{
area->x = 0;
area->width = self->list_width;
area->y = along - remaining % row->height;
area->height = row->height;
}
return TRUE;
}
static guint
gtk_list_view_move_focus_across (GtkListBase *base,
guint pos,
int steps)
{
return pos;
}
static int
compare_ints (gconstpointer first,
gconstpointer second)
{
return *(int *) first - *(int *) second;
}
static guint
gtk_list_view_get_unknown_row_height (GtkListView *self,
GArray *heights)
{
g_return_val_if_fail (heights->len > 0, 0);
/* return the median and hope rows are generally uniform with few outliers */
g_array_sort (heights, compare_ints);
return g_array_index (heights, int, heights->len / 2);
}
static void
gtk_list_view_measure_across (GtkWidget *widget,
GtkOrientation orientation,
int for_size,
int *minimum,
int *natural)
{
GtkListView *self = GTK_LIST_VIEW (widget);
ListRow *row;
int min, nat, child_min, child_nat;
/* XXX: Figure out how to split a given height into per-row heights.
* Good luck! */
for_size = -1;
min = 0;
nat = 0;
for (row = gtk_list_item_manager_get_first (self->item_manager);
row != NULL;
row = gtk_rb_tree_node_get_next (row))
{
/* ignore unavailable rows */
if (row->parent.widget == NULL)
continue;
gtk_widget_measure (row->parent.widget,
orientation, for_size,
&child_min, &child_nat, NULL, NULL);
min = MAX (min, child_min);
nat = MAX (nat, child_nat);
}
*minimum = min;
*natural = nat;
}
static void
gtk_list_view_measure_list (GtkWidget *widget,
GtkOrientation orientation,
int for_size,
int *minimum,
int *natural)
{
GtkListView *self = GTK_LIST_VIEW (widget);
ListRow *row;
int min, nat, child_min, child_nat;
GArray *min_heights, *nat_heights;
guint n_unknown;
min_heights = g_array_new (FALSE, FALSE, sizeof (int));
nat_heights = g_array_new (FALSE, FALSE, sizeof (int));
n_unknown = 0;
min = 0;
nat = 0;
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)
{
gtk_widget_measure (row->parent.widget,
orientation, for_size,
&child_min, &child_nat, NULL, NULL);
g_array_append_val (min_heights, child_min);
g_array_append_val (nat_heights, child_nat);
min += child_min;
nat += child_nat;
}
else
{
n_unknown += row->parent.n_items;
}
}
if (n_unknown)
{
min += n_unknown * gtk_list_view_get_unknown_row_height (self, min_heights);
nat += n_unknown * gtk_list_view_get_unknown_row_height (self, nat_heights);
}
g_array_free (min_heights, TRUE);
g_array_free (nat_heights, TRUE);
*minimum = min;
*natural = nat;
}
static void
gtk_list_view_measure (GtkWidget *widget,
GtkOrientation orientation,
int for_size,
int *minimum,
int *natural,
int *minimum_baseline,
int *natural_baseline)
{
GtkListView *self = GTK_LIST_VIEW (widget);
if (orientation == gtk_list_base_get_orientation (GTK_LIST_BASE (self)))
gtk_list_view_measure_list (widget, orientation, for_size, minimum, natural);
else
gtk_list_view_measure_across (widget, orientation, for_size, minimum, natural);
}
static void
gtk_list_view_size_allocate (GtkWidget *widget,
int width,
int height,
int baseline)
{
GtkListView *self = GTK_LIST_VIEW (widget);
ListRow *row;
GArray *heights;
int min, nat, row_height;
int x, y;
GtkOrientation orientation, opposite_orientation;
GtkScrollablePolicy scroll_policy, opposite_scroll_policy;
orientation = gtk_list_base_get_orientation (GTK_LIST_BASE (self));
opposite_orientation = OPPOSITE_ORIENTATION (orientation);
scroll_policy = gtk_list_base_get_scroll_policy (GTK_LIST_BASE (self), orientation);
opposite_scroll_policy = gtk_list_base_get_scroll_policy (GTK_LIST_BASE (self), opposite_orientation);
/* step 0: exit early if list is empty */
if (gtk_list_item_manager_get_root (self->item_manager) == NULL)
{
gtk_list_base_update_adjustments (GTK_LIST_BASE (self), 0, 0, 0, 0, &x, &y);
return;
}
/* step 1: determine width of the list */
gtk_widget_measure (widget, opposite_orientation,
-1,
&min, &nat, NULL, NULL);
self->list_width = orientation == GTK_ORIENTATION_VERTICAL ? width : height;
if (opposite_scroll_policy == GTK_SCROLL_MINIMUM)
self->list_width = MAX (min, self->list_width);
else
self->list_width = MAX (nat, self->list_width);
/* step 2: determine height of known list items */
heights = g_array_new (FALSE, FALSE, sizeof (int));
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 == NULL)
continue;
gtk_widget_measure (row->parent.widget, orientation,
self->list_width,
&min, &nat, NULL, NULL);
if (scroll_policy == GTK_SCROLL_MINIMUM)
row_height = min;
else
row_height = nat;
if (row->height != row_height)
{
row->height = row_height;
gtk_rb_tree_node_mark_dirty (row);
}
g_array_append_val (heights, row_height);
}
/* step 3: determine height of unknown items */
row_height = gtk_list_view_get_unknown_row_height (self, heights);
g_array_free (heights, TRUE);
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)
continue;
if (row->height != row_height)
{
row->height = row_height;
gtk_rb_tree_node_mark_dirty (row);
}
}
/* step 3: update the adjustments */
gtk_list_base_update_adjustments (GTK_LIST_BASE (self),
self->list_width,
gtk_list_view_get_list_height (self),
gtk_widget_get_size (widget, opposite_orientation),
gtk_widget_get_size (widget, orientation),
&x, &y);
x = -x;
y = -y;
/* step 4: actually allocate the widgets */
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)
{
gtk_list_base_size_allocate_child (GTK_LIST_BASE (self),
row->parent.widget,
x,
y,
self->list_width,
row->height);
}
y += row->height * row->parent.n_items;
}
gtk_list_base_allocate_rubberband (GTK_LIST_BASE (self));
}
static void
gtk_list_view_dispose (GObject *object)
{
GtkListView *self = GTK_LIST_VIEW (object);
self->item_manager = NULL;
G_OBJECT_CLASS (gtk_list_view_parent_class)->dispose (object);
}
static void
gtk_list_view_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
GtkListView *self = GTK_LIST_VIEW (object);
switch (property_id)
{
case PROP_FACTORY:
g_value_set_object (value, gtk_list_item_manager_get_factory (self->item_manager));
break;
case PROP_MODEL:
g_value_set_object (value, gtk_list_base_get_model (GTK_LIST_BASE (self)));
break;
case PROP_SHOW_SEPARATORS:
g_value_set_boolean (value, self->show_separators);
break;
case PROP_SINGLE_CLICK_ACTIVATE:
g_value_set_boolean (value, gtk_list_item_manager_get_single_click_activate (self->item_manager));
break;
case PROP_ENABLE_RUBBERBAND:
g_value_set_boolean (value, gtk_list_base_get_enable_rubberband (GTK_LIST_BASE (self)));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
gtk_list_view_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
GtkListView *self = GTK_LIST_VIEW (object);
switch (property_id)
{
case PROP_FACTORY:
gtk_list_view_set_factory (self, g_value_get_object (value));
break;
case PROP_MODEL:
gtk_list_view_set_model (self, g_value_get_object (value));
break;
case PROP_SHOW_SEPARATORS:
gtk_list_view_set_show_separators (self, g_value_get_boolean (value));
break;
case PROP_SINGLE_CLICK_ACTIVATE:
gtk_list_view_set_single_click_activate (self, g_value_get_boolean (value));
break;
case PROP_ENABLE_RUBBERBAND:
gtk_list_view_set_enable_rubberband (self, g_value_get_boolean (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
gtk_list_view_activate_item (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
GtkListView *self = GTK_LIST_VIEW (widget);
guint pos;
if (!g_variant_check_format_string (parameter, "u", FALSE))
return;
g_variant_get (parameter, "u", &pos);
if (pos >= gtk_list_base_get_n_items (GTK_LIST_BASE (self)))
return;
g_signal_emit (widget, signals[ACTIVATE], 0, pos);
}
static void
gtk_list_view_class_init (GtkListViewClass *klass)
{
GtkListBaseClass *list_base_class = GTK_LIST_BASE_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
list_base_class->list_item_name = "row";
list_base_class->list_item_role = GTK_ACCESSIBLE_ROLE_LIST_ITEM;
list_base_class->list_item_size = sizeof (ListRow);
list_base_class->list_item_augment_size = sizeof (ListRowAugment);
list_base_class->list_item_augment_func = list_row_augment;
list_base_class->get_allocation_along = gtk_list_view_get_allocation_along;
list_base_class->get_allocation_across = gtk_list_view_get_allocation_across;
list_base_class->get_items_in_rect = gtk_list_view_get_items_in_rect;
list_base_class->get_position_from_allocation = gtk_list_view_get_position_from_allocation;
list_base_class->move_focus_along = gtk_list_view_move_focus_along;
list_base_class->move_focus_across = gtk_list_view_move_focus_across;
widget_class->measure = gtk_list_view_measure;
widget_class->size_allocate = gtk_list_view_size_allocate;
gobject_class->dispose = gtk_list_view_dispose;
gobject_class->get_property = gtk_list_view_get_property;
gobject_class->set_property = gtk_list_view_set_property;
/**
* GtkListView:factory: (attributes org.gtk.Property.get=gtk_list_view_get_factory org.gtk.Property.set=gtk_list_view_set_factory)
*
* Factory for populating list items.
*/
properties[PROP_FACTORY] =
g_param_spec_object ("factory", NULL, NULL,
GTK_TYPE_LIST_ITEM_FACTORY,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
/**
* GtkListView:model: (attributes org.gtk.Property.get=gtk_list_view_get_model org.gtk.Property.set=gtk_list_view_set_model)
*
* Model for the items displayed.
*/
properties[PROP_MODEL] =
g_param_spec_object ("model", NULL, NULL,
GTK_TYPE_SELECTION_MODEL,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
/**
* GtkListView:show-separators: (attributes org.gtk.Property.get=gtk_list_view_get_show_separators org.gtk.Property.set=gtk_list_view_set_show_separators)
*
* Show separators between rows.
*/
properties[PROP_SHOW_SEPARATORS] =
g_param_spec_boolean ("show-separators", NULL, NULL,
FALSE,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
/**
* GtkListView:single-click-activate: (attributes org.gtk.Property.get=gtk_list_view_get_single_click_activate org.gtk.Property.set=gtk_list_view_set_single_click_activate)
*
* Activate rows on single click and select them on hover.
*/
properties[PROP_SINGLE_CLICK_ACTIVATE] =
g_param_spec_boolean ("single-click-activate", NULL, NULL,
FALSE,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
/**
* GtkListView:enable-rubberband: (attributes org.gtk.Property.get=gtk_list_view_get_enable_rubberband org.gtk.Property.set=gtk_list_view_set_enable_rubberband)
*
* Allow rubberband selection.
*/
properties[PROP_ENABLE_RUBBERBAND] =
g_param_spec_boolean ("enable-rubberband", NULL, NULL,
FALSE,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
g_object_class_install_properties (gobject_class, N_PROPS, properties);
/**
* GtkListView::activate:
* @self: The `GtkListView`
* @position: position of item to activate
*
* Emitted when a row has been activated by the user,
* usually via activating the GtkListView|list.activate-item action.
*
* This allows for a convenient way to handle activation in a listview.
* See [method@Gtk.ListItem.set_activatable] for details on how to use
* this signal.
*/
signals[ACTIVATE] =
g_signal_new (I_("activate"),
G_TYPE_FROM_CLASS (gobject_class),
G_SIGNAL_RUN_LAST,
0,
NULL, NULL,
g_cclosure_marshal_VOID__UINT,
G_TYPE_NONE, 1,
G_TYPE_UINT);
g_signal_set_va_marshaller (signals[ACTIVATE],
G_TYPE_FROM_CLASS (gobject_class),
g_cclosure_marshal_VOID__UINTv);
/**
* GtkListView|list.activate-item:
* @position: position of item to activate
*
* Activates the item given in @position by emitting the
* [signal@Gtk.ListView::activate] signal.
*/
gtk_widget_class_install_action (widget_class,
"list.activate-item",
"u",
gtk_list_view_activate_item);
gtk_widget_class_set_css_name (widget_class, I_("listview"));
gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_LIST);
}
static void
gtk_list_view_init (GtkListView *self)
{
self->item_manager = gtk_list_base_get_manager (GTK_LIST_BASE (self));
gtk_list_base_set_anchor_max_widgets (GTK_LIST_BASE (self),
GTK_LIST_VIEW_MAX_LIST_ITEMS,
GTK_LIST_VIEW_EXTRA_ITEMS);
gtk_widget_add_css_class (GTK_WIDGET (self), "view");
}
/**
* gtk_list_view_new:
* @model: (nullable) (transfer full): the model to use
* @factory: (nullable) (transfer full): The factory to populate items with
*
* Creates a new `GtkListView` that uses the given @factory for
* mapping items to widgets.
*
* The function takes ownership of the
* arguments, so you can write code like
* ```c
* list_view = gtk_list_view_new (create_model (),
* gtk_builder_list_item_factory_new_from_resource ("/resource.ui"));
* ```
*
* Returns: a new `GtkListView` using the given @model and @factory
*/
GtkWidget *
gtk_list_view_new (GtkSelectionModel *model,
GtkListItemFactory *factory)
{
GtkWidget *result;
g_return_val_if_fail (model == NULL || GTK_IS_SELECTION_MODEL (model), NULL);
g_return_val_if_fail (factory == NULL || GTK_IS_LIST_ITEM_FACTORY (factory), NULL);
result = g_object_new (GTK_TYPE_LIST_VIEW,
"model", model,
"factory", factory,
NULL);
/* consume the references */
g_clear_object (&model);
g_clear_object (&factory);
return result;
}
/**
* gtk_list_view_get_model: (attributes org.gtk.Method.get_property=model)
* @self: a `GtkListView`
*
* Gets the model that's currently used to read the items displayed.
*
* Returns: (nullable) (transfer none): The model in use
*/
GtkSelectionModel *
gtk_list_view_get_model (GtkListView *self)
{
g_return_val_if_fail (GTK_IS_LIST_VIEW (self), NULL);
return gtk_list_base_get_model (GTK_LIST_BASE (self));
}
/**
* gtk_list_view_set_model: (attributes org.gtk.Method.set_property=model)
* @self: a `GtkListView`
* @model: (nullable) (transfer none): the model to use
*
* Sets the model to use.
*
* This must be a [iface@Gtk.SelectionModel] to use.
*/
void
gtk_list_view_set_model (GtkListView *self,
GtkSelectionModel *model)
{
g_return_if_fail (GTK_IS_LIST_VIEW (self));
g_return_if_fail (model == NULL || GTK_IS_SELECTION_MODEL (model));
if (!gtk_list_base_set_model (GTK_LIST_BASE (self), model))
return;
gtk_accessible_update_property (GTK_ACCESSIBLE (self),
GTK_ACCESSIBLE_PROPERTY_MULTI_SELECTABLE, GTK_IS_MULTI_SELECTION (model),
-1);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MODEL]);
}
/**
* gtk_list_view_get_factory: (attributes org.gtk.Method.get_property=factory)
* @self: a `GtkListView`
*
* Gets the factory that's currently used to populate list items.
*
* Returns: (nullable) (transfer none): The factory in use
*/
GtkListItemFactory *
gtk_list_view_get_factory (GtkListView *self)
{
g_return_val_if_fail (GTK_IS_LIST_VIEW (self), NULL);
return gtk_list_item_manager_get_factory (self->item_manager);
}
/**
* gtk_list_view_set_factory: (attributes org.gtk.Method.set_property=factory)
* @self: a `GtkListView`
* @factory: (nullable) (transfer none): the factory to use
*
* Sets the `GtkListItemFactory` to use for populating list items.
*/
void
gtk_list_view_set_factory (GtkListView *self,
GtkListItemFactory *factory)
{
g_return_if_fail (GTK_IS_LIST_VIEW (self));
g_return_if_fail (factory == NULL || GTK_IS_LIST_ITEM_FACTORY (factory));
if (factory == gtk_list_item_manager_get_factory (self->item_manager))
return;
gtk_list_item_manager_set_factory (self->item_manager, factory);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FACTORY]);
}
/**
* gtk_list_view_set_show_separators: (attributes org.gtk.Method.set_property=show-separators)
* @self: a `GtkListView`
* @show_separators: %TRUE to show separators
*
* Sets whether the list box should show separators
* between rows.
*/
void
gtk_list_view_set_show_separators (GtkListView *self,
gboolean show_separators)
{
g_return_if_fail (GTK_IS_LIST_VIEW (self));
if (self->show_separators == show_separators)
return;
self->show_separators = show_separators;
if (show_separators)
gtk_widget_add_css_class (GTK_WIDGET (self), "separators");
else
gtk_widget_remove_css_class (GTK_WIDGET (self), "separators");
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SHOW_SEPARATORS]);
}
/**
* gtk_list_view_get_show_separators: (attributes org.gtk.Method.get_property=show-separators)
* @self: a `GtkListView`
*
* Returns whether the list box should show separators
* between rows.
*
* Returns: %TRUE if the list box shows separators
*/
gboolean
gtk_list_view_get_show_separators (GtkListView *self)
{
g_return_val_if_fail (GTK_IS_LIST_VIEW (self), FALSE);
return self->show_separators;
}
/**
* gtk_list_view_set_single_click_activate: (attributes org.gtk.Method.set_property=single-click-activate)
* @self: a `GtkListView`
* @single_click_activate: %TRUE to activate items on single click
*
* Sets whether rows should be activated on single click and
* selected on hover.
*/
void
gtk_list_view_set_single_click_activate (GtkListView *self,
gboolean single_click_activate)
{
g_return_if_fail (GTK_IS_LIST_VIEW (self));
if (single_click_activate == gtk_list_item_manager_get_single_click_activate (self->item_manager))
return;
gtk_list_item_manager_set_single_click_activate (self->item_manager, single_click_activate);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SINGLE_CLICK_ACTIVATE]);
}
/**
* gtk_list_view_get_single_click_activate: (attributes org.gtk.Method.set_property=single-click-activate)
* @self: a `GtkListView`
*
* Returns whether rows will be activated on single click and
* selected on hover.
*
* Returns: %TRUE if rows are activated on single click
*/
gboolean
gtk_list_view_get_single_click_activate (GtkListView *self)
{
g_return_val_if_fail (GTK_IS_LIST_VIEW (self), FALSE);
return gtk_list_item_manager_get_single_click_activate (self->item_manager);
}
/**
* gtk_list_view_set_enable_rubberband: (attributes org.gtk.Method.set_property=enable-rubberband)
* @self: a `GtkListView`
* @enable_rubberband: %TRUE to enable rubberband selection
*
* Sets whether selections can be changed by dragging with the mouse.
*/
void
gtk_list_view_set_enable_rubberband (GtkListView *self,
gboolean enable_rubberband)
{
g_return_if_fail (GTK_IS_LIST_VIEW (self));
if (enable_rubberband == gtk_list_base_get_enable_rubberband (GTK_LIST_BASE (self)))
return;
gtk_list_base_set_enable_rubberband (GTK_LIST_BASE (self), enable_rubberband);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ENABLE_RUBBERBAND]);
}
/**
* gtk_list_view_get_enable_rubberband: (attributes org.gtk.Method.get_property=enable-rubberband)
* @self: a `GtkListView`
*
* Returns whether rows can be selected by dragging with the mouse.
*
* Returns: %TRUE if rubberband selection is enabled
*/
gboolean
gtk_list_view_get_enable_rubberband (GtkListView *self)
{
g_return_val_if_fail (GTK_IS_LIST_VIEW (self), FALSE);
return gtk_list_base_get_enable_rubberband (GTK_LIST_BASE (self));
}