/* * 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 . * * Authors: Benjamin Otte */ #include "config.h" #include "gtklistviewprivate.h" #include "gtkbitset.h" #include "gtklistbaseprivate.h" #include "gtklistheaderwidgetprivate.h" #include "gtklistitemmanagerprivate.h" #include "gtklistitemwidgetprivate.h" #include "gtkmultiselection.h" #include "gtktypebuiltins.h" #include "gtkwidgetprivate.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. */ enum { PROP_0, PROP_ENABLE_RUBBERBAND, PROP_FACTORY, PROP_HEADER_FACTORY, PROP_MODEL, PROP_SHOW_SEPARATORS, PROP_SINGLE_CLICK_ACTIVATE, PROP_TAB_BEHAVIOR, 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 GtkListTile * gtk_list_view_split (GtkListBase *base, GtkListTile *tile, guint n_items) { GtkListView *self = GTK_LIST_VIEW (base); GtkListTile *new_tile; int spacing, row_height; gtk_list_base_get_border_spacing (GTK_LIST_BASE (self), NULL, &spacing); row_height = (tile->area.height - (tile->n_items - 1) * spacing) / tile->n_items; new_tile = gtk_list_tile_split (self->item_manager, tile, n_items); gtk_list_tile_set_area_size (self->item_manager, tile, tile->area.width, row_height * tile->n_items + spacing * (tile->n_items - 1)); gtk_list_tile_set_area (self->item_manager, new_tile, &(GdkRectangle) { tile->area.x, tile->area.y + tile->area.height + spacing, tile->area.width, row_height * new_tile->n_items + spacing * (new_tile->n_items - 1) }); return new_tile; } static void gtk_list_view_prepare_section (GtkListBase *base, GtkListTile *tile, guint position) { } /* We define the listview as **inert** when the factory isn't used. */ static gboolean gtk_list_view_is_inert (GtkListView *self) { GtkWidget *widget = GTK_WIDGET (self); return !gtk_widget_get_visible (widget) || gtk_widget_get_root (widget) == NULL; } static void gtk_list_view_update_factories_with (GtkListView *self, GtkListItemFactory *factory, GtkListItemFactory *header_factory) { GtkListTile *tile; for (tile = gtk_list_item_manager_get_first (self->item_manager); tile != NULL; tile = gtk_rb_tree_node_get_next (tile)) { switch (tile->type) { case GTK_LIST_TILE_ITEM: if (tile->widget) gtk_list_factory_widget_set_factory (GTK_LIST_FACTORY_WIDGET (tile->widget), factory); break; case GTK_LIST_TILE_HEADER: if (tile->widget) gtk_list_header_widget_set_factory (GTK_LIST_HEADER_WIDGET (tile->widget), header_factory); break; case GTK_LIST_TILE_UNMATCHED_HEADER: case GTK_LIST_TILE_FOOTER: case GTK_LIST_TILE_UNMATCHED_FOOTER: case GTK_LIST_TILE_REMOVED: g_assert (tile->widget == NULL); break; default: g_assert_not_reached(); break; } } } static void gtk_list_view_update_factories (GtkListView *self) { gtk_list_view_update_factories_with (self, gtk_list_view_is_inert (self) ? NULL : self->factory, gtk_list_view_is_inert (self) ? NULL : self->header_factory); } static void gtk_list_view_clear_factories (GtkListView *self) { gtk_list_view_update_factories_with (self, NULL, NULL); } static GtkListItemBase * gtk_list_view_create_list_widget (GtkListBase *base) { GtkListView *self = GTK_LIST_VIEW (base); GtkListItemFactory *factory; GtkWidget *result; if (gtk_list_view_is_inert (self)) factory = NULL; else factory = self->factory; result = gtk_list_item_widget_new (factory, "row", GTK_ACCESSIBLE_ROLE_LIST_ITEM); gtk_list_factory_widget_set_single_click_activate (GTK_LIST_FACTORY_WIDGET (result), self->single_click_activate); return GTK_LIST_ITEM_BASE (result); } static GtkListHeaderBase * gtk_list_view_create_header_widget (GtkListBase *base) { GtkListView *self = GTK_LIST_VIEW (base); GtkListItemFactory *factory; if (gtk_list_view_is_inert (self)) factory = NULL; else factory = self->header_factory; return GTK_LIST_HEADER_BASE (gtk_list_header_widget_new (factory)); } static gboolean gtk_list_view_get_allocation (GtkListBase *base, guint pos, GdkRectangle *area) { GtkListView *self = GTK_LIST_VIEW (base); GtkListTile *tile; guint offset; tile = gtk_list_item_manager_get_nth (self->item_manager, pos, &offset); if (tile == NULL) return FALSE; *area = tile->area; if (area->width || area->height) { if (tile->n_items) area->height /= tile->n_items; if (offset) area->y += offset * area->height; } else { /* item is not allocated yet */ GtkListTile *other; int spacing; gtk_list_base_get_border_spacing (GTK_LIST_BASE (self), NULL, &spacing); for (other = gtk_rb_tree_node_get_previous (tile); other; other = gtk_rb_tree_node_get_previous (other)) { if (other->area.width || other->area.height) { area->x = other->area.x; area->width = other->area.width; area->y = other->area.y + other->area.height + spacing; break; } } if (other == NULL) { for (other = gtk_rb_tree_node_get_next (tile); other; other = gtk_rb_tree_node_get_next (other)) { if (other->area.width || other->area.height) { area->x = other->area.x; area->width = other->area.width; area->y = MAX (0, other->area.y - spacing); break; } } } } return TRUE; } 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 x, int y, guint *pos, cairo_rectangle_int_t *area) { GtkListView *self = GTK_LIST_VIEW (base); GtkListTile *tile; tile = gtk_list_item_manager_get_nearest_tile (self->item_manager, x, y); if (tile == NULL) return FALSE; while (tile && tile->n_items == 0) tile = gtk_rb_tree_node_get_previous (tile); if (tile == NULL) { tile = gtk_list_item_manager_get_first (self->item_manager); while (tile && tile->n_items == 0) tile = gtk_rb_tree_node_get_next (tile); if (tile == NULL) return FALSE; } *pos = gtk_list_tile_get_position (self->item_manager, tile); if (area) *area = tile->area; if (tile->n_items > 1) { int row_height, tile_pos, spacing; gtk_list_base_get_border_spacing (GTK_LIST_BASE (self), NULL, &spacing); row_height = (tile->area.height - (tile->n_items - 1) * spacing) / tile->n_items; if (y >= tile->area.y + tile->area.height) tile_pos = tile->n_items - 1; else tile_pos = (y - tile->area.y) / (row_height + spacing); *pos += tile_pos; if (area) { area->y = tile->area.y + tile_pos * (row_height + spacing); area->height = row_height; } } return TRUE; } static GtkBitset * gtk_list_view_get_items_in_rect (GtkListBase *base, const cairo_rectangle_int_t *rect) { guint first, last; cairo_rectangle_int_t area; GtkBitset *result; result = gtk_bitset_new_empty (); if (!gtk_list_view_get_position_from_allocation (base, rect->x, rect->y, &first, &area)) return result; if (area.y + area.height < rect->y) first++; if (!gtk_list_view_get_position_from_allocation (base, rect->x + rect->width - 1, rect->y + rect->height - 1, &last, &area)) return result; if (area.y >= rect->y + rect->height) last--; if (last >= first) gtk_bitset_add_range_closed (result, first, last); return result; } 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); GtkListTile *tile; 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 (tile = gtk_list_item_manager_get_first (self->item_manager); tile != NULL; tile = gtk_rb_tree_node_get_next (tile)) { /* ignore unavailable rows */ if (tile->widget == NULL) continue; gtk_widget_measure (tile->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); GtkListTile *tile; int min, nat, child_min, child_nat, spacing; GArray *min_heights, *nat_heights; guint n_unknown, n_items; n_items = gtk_list_base_get_n_items (GTK_LIST_BASE (self)); if (n_items == 0) return; gtk_list_base_get_border_spacing (GTK_LIST_BASE (self), NULL, &spacing); 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 (tile = gtk_list_item_manager_get_first (self->item_manager); tile != NULL; tile = gtk_rb_tree_node_get_next (tile)) { if (tile->widget) { gtk_widget_measure (tile->widget, orientation, for_size, &child_min, &child_nat, NULL, NULL); if (tile->type == GTK_LIST_TILE_ITEM) { 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 += tile->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 + spacing * (n_items - 1); *natural = nat + spacing * (n_items - 1); } 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); GtkListTile *tile; GArray *heights; int min, nat, row_height, y, list_width, spacing; 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); gtk_list_base_get_border_spacing (GTK_LIST_BASE (self), NULL, &spacing); gtk_list_item_manager_gc_tiles (self->item_manager); /* step 0: exit early if list is empty */ tile = gtk_list_item_manager_get_first (self->item_manager); if (tile == NULL) { gtk_list_base_allocate (GTK_LIST_BASE (self)); return; } /* step 1: determine width of the list */ gtk_list_view_measure_across (widget, opposite_orientation, -1, &min, &nat); list_width = orientation == GTK_ORIENTATION_VERTICAL ? width : height; if (opposite_scroll_policy == GTK_SCROLL_MINIMUM) list_width = MAX (min, list_width); else list_width = MAX (nat, list_width); /* step 2: determine height of known list items and gc the list */ heights = g_array_new (FALSE, FALSE, sizeof (int)); for (; tile != NULL; tile = gtk_rb_tree_node_get_next (tile)) { if (tile->widget == NULL) continue; gtk_widget_measure (tile->widget, orientation, list_width, &min, &nat, NULL, NULL); if (scroll_policy == GTK_SCROLL_MINIMUM) row_height = min; else row_height = nat; gtk_list_tile_set_area_size (self->item_manager, tile, list_width, row_height); if (tile->type == GTK_LIST_TILE_ITEM) g_array_append_val (heights, row_height); } /* step 3: determine height of unknown items and set the positions */ row_height = gtk_list_view_get_unknown_row_height (self, heights); g_array_free (heights, TRUE); y = 0; for (tile = gtk_list_item_manager_get_first (self->item_manager); tile != NULL; tile = gtk_rb_tree_node_get_next (tile)) { gtk_list_tile_set_area_position (self->item_manager, tile, 0, y); if (tile->widget == NULL) { gtk_list_tile_set_area_size (self->item_manager, tile, list_width, row_height * tile->n_items + spacing * (tile->n_items - 1)); } y += tile->area.height + spacing; } /* step 4: allocate the rest */ gtk_list_base_allocate (GTK_LIST_BASE (self)); } static void gtk_list_view_root (GtkWidget *widget) { GtkListView *self = GTK_LIST_VIEW (widget); GTK_WIDGET_CLASS (gtk_list_view_parent_class)->root (widget); if (!gtk_list_view_is_inert (self)) gtk_list_view_update_factories (self); } static void gtk_list_view_unroot (GtkWidget *widget) { GtkListView *self = GTK_LIST_VIEW (widget); if (!gtk_list_view_is_inert (self)) gtk_list_view_clear_factories (self); GTK_WIDGET_CLASS (gtk_list_view_parent_class)->unroot (widget); } static void gtk_list_view_show (GtkWidget *widget) { GtkListView *self = GTK_LIST_VIEW (widget); GTK_WIDGET_CLASS (gtk_list_view_parent_class)->show (widget); if (!gtk_list_view_is_inert (self)) gtk_list_view_update_factories (self); } static void gtk_list_view_hide (GtkWidget *widget) { GtkListView *self = GTK_LIST_VIEW (widget); if (!gtk_list_view_is_inert (self)) gtk_list_view_clear_factories (self); GTK_WIDGET_CLASS (gtk_list_view_parent_class)->hide (widget); } static void gtk_list_view_dispose (GObject *object) { GtkListView *self = GTK_LIST_VIEW (object); self->item_manager = NULL; g_clear_object (&self->factory); g_clear_object (&self->header_factory); 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_ENABLE_RUBBERBAND: g_value_set_boolean (value, gtk_list_base_get_enable_rubberband (GTK_LIST_BASE (self))); break; case PROP_FACTORY: g_value_set_object (value, self->factory); break; case PROP_HEADER_FACTORY: g_value_set_object (value, self->header_factory); 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, self->single_click_activate); break; case PROP_TAB_BEHAVIOR: g_value_set_enum (value, gtk_list_base_get_tab_behavior (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_ENABLE_RUBBERBAND: gtk_list_view_set_enable_rubberband (self, g_value_get_boolean (value)); break; case PROP_FACTORY: gtk_list_view_set_factory (self, g_value_get_object (value)); break; case PROP_HEADER_FACTORY: gtk_list_view_set_header_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_TAB_BEHAVIOR: gtk_list_view_set_tab_behavior (self, g_value_get_enum (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->split = gtk_list_view_split; list_base_class->create_list_widget = gtk_list_view_create_list_widget; list_base_class->prepare_section = gtk_list_view_prepare_section; list_base_class->create_header_widget = gtk_list_view_create_header_widget; list_base_class->get_allocation = gtk_list_view_get_allocation; 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; widget_class->root = gtk_list_view_root; widget_class->unroot = gtk_list_view_unroot; widget_class->show = gtk_list_view_show; widget_class->hide = gtk_list_view_hide; 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: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); /** * 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:header-factory: (attributes org.gtk.Property.get=gtk_list_view_get_header_factory org.gtk.Property.set=gtk_list_view_set_header_factory) * * Factory for creating header widgets. * * Since: 4.12 */ properties[PROP_HEADER_FACTORY] = g_param_spec_object ("header-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:tab-behavior: (attributes org.gtk.Property.get=gtk_list_view_get_tab_behavior org.gtk.Property.set=gtk_list_view_set_tab_behavior) * * Behavior of the Tab key * * Since: 4.12 */ properties[PROP_TAB_BEHAVIOR] = g_param_spec_enum ("tab-behavior", NULL, NULL, GTK_TYPE_LIST_TAB_BEHAVIOR, GTK_LIST_TAB_ALL, 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 self->factory; } /** * 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 (!g_set_object (&self->factory, factory)) return; gtk_list_view_update_factories (self); g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FACTORY]); } /** * gtk_list_view_get_header_factory: (attributes org.gtk.Method.get_property=header-factory) * @self: a `GtkListView` * * Gets the factory that's currently used to populate section headers. * * Returns: (nullable) (transfer none): The factory in use * * Since: 4.12 */ GtkListItemFactory * gtk_list_view_get_header_factory (GtkListView *self) { g_return_val_if_fail (GTK_IS_LIST_VIEW (self), NULL); return self->header_factory; } /** * gtk_list_view_set_header_factory: (attributes org.gtk.Method.set_property=header-factory) * @self: a `GtkListView` * @factory: (nullable) (transfer none): the factory to use * * Sets the `GtkListItemFactory` to use for populating the * [class@Gtk.ListHeader] objects used in section headers. * * If this factory is set to %NULL, the list will not show section headers. * * Since: 4.12 */ void gtk_list_view_set_header_factory (GtkListView *self, GtkListItemFactory *factory) { gboolean had_sections; g_return_if_fail (GTK_IS_LIST_VIEW (self)); g_return_if_fail (factory == NULL || GTK_IS_LIST_ITEM_FACTORY (factory)); had_sections = gtk_list_item_manager_get_has_sections (self->item_manager); if (!g_set_object (&self->header_factory, factory)) return; gtk_list_item_manager_set_has_sections (self->item_manager, factory != NULL); if (!gtk_list_view_is_inert (self) && had_sections && gtk_list_item_manager_get_has_sections (self->item_manager)) { GtkListTile *tile; for (tile = gtk_list_item_manager_get_first (self->item_manager); tile != NULL; tile = gtk_rb_tree_node_get_next (tile)) { if (tile->widget && tile->type == GTK_LIST_TILE_HEADER) gtk_list_header_widget_set_factory (GTK_LIST_HEADER_WIDGET (tile->widget), factory); } } g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_HEADER_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) { GtkListTile *tile; g_return_if_fail (GTK_IS_LIST_VIEW (self)); if (single_click_activate == self->single_click_activate) return; self->single_click_activate = single_click_activate; for (tile = gtk_list_item_manager_get_first (self->item_manager); tile != NULL; tile = gtk_rb_tree_node_get_next (tile)) { if (tile->widget && tile->type == GTK_LIST_TILE_ITEM) gtk_list_factory_widget_set_single_click_activate (GTK_LIST_FACTORY_WIDGET (tile->widget), 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 self->single_click_activate; } /** * 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)); } /** * gtk_list_view_set_tab_behavior: (attributes org.gtk.Method.set_property=tab-behavior) * @self: a `GtkListView` * @tab_behavior: The desired tab behavior * * Sets the behavior of the Tab and Shift+Tab keys. * * Since: 4.12 */ void gtk_list_view_set_tab_behavior (GtkListView *self, GtkListTabBehavior tab_behavior) { g_return_if_fail (GTK_IS_LIST_VIEW (self)); if (tab_behavior == gtk_list_base_get_tab_behavior (GTK_LIST_BASE (self))) return; gtk_list_base_set_tab_behavior (GTK_LIST_BASE (self), tab_behavior); g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TAB_BEHAVIOR]); } /** * gtk_list_view_get_tab_behavior: (attributes org.gtk.Method.get_property=tab-behavior) * @self: a `GtkListView` * * Gets the behavior set for the Tab key. * * Returns: The behavior of the Tab key * * Since: 4.12 */ GtkListTabBehavior gtk_list_view_get_tab_behavior (GtkListView *self) { g_return_val_if_fail (GTK_IS_LIST_VIEW (self), FALSE); return gtk_list_base_get_tab_behavior (GTK_LIST_BASE (self)); } /** * gtk_list_view_scroll_to: * @self: The listview to scroll in * @pos: position of the item * @flags: actions to perform * @scroll: (nullable) (transfer full): details of how to perform * the scroll operation or %NULL to scroll into view * * Scrolls to the item at the given position and performs the actions * specified in @flags. * * This function works no matter if the listview is shown or focused. * If it isn't, then the changes will take effect once that happens. * * Since: 4.12 */ void gtk_list_view_scroll_to (GtkListView *self, guint pos, GtkListScrollFlags flags, GtkScrollInfo *scroll) { g_return_if_fail (GTK_IS_LIST_VIEW (self)); gtk_list_base_scroll_to (GTK_LIST_BASE (self), pos, flags, scroll); }