/* * Copyright (C) 2012 Alexander Larsson * * 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 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 . */ #include "config.h" #include "gtklistbox.h" #include "gtkaccessible.h" #include "gtkactionhelperprivate.h" #include "gtkadjustmentprivate.h" #include "gtkbinlayout.h" #include "gtkbuildable.h" #include "gtkgestureclick.h" #include "gtkintl.h" #include "gtkmain.h" #include "gtkmarshalers.h" #include "gtkprivate.h" #include "gtkscrollable.h" #include "gtktypebuiltins.h" #include "gtkwidgetprivate.h" #include #include #include /** * GtkListBox: * * `GtkListBox` is a vertical list. * * A `GtkListBox` only contains `GtkListBoxRow` children. These rows can * by dynamically sorted and filtered, and headers can be added dynamically * depending on the row content. It also allows keyboard and mouse navigation * and selection like a typical list. * * Using `GtkListBox` is often an alternative to `GtkTreeView`, especially * when the list contents has a more complicated layout than what is allowed * by a `GtkCellRenderer`, or when the contents is interactive (i.e. has a * button in it). * * Although a `GtkListBox` must have only `GtkListBoxRow` children, you can * add any kind of widget to it via [method@Gtk.ListBox.prepend], * [method@Gtk.ListBox.append] and [method@Gtk.ListBox.insert] and a * `GtkListBoxRow` widget will automatically be inserted between the list * and the widget. * * `GtkListBoxRows` can be marked as activatable or selectable. If a row is * activatable, [signal@Gtk.ListBox::row-activated] will be emitted for it when * the user tries to activate it. If it is selectable, the row will be marked * as selected when the user tries to select it. * * # GtkListBox as GtkBuildable * * The `GtkListBox` implementation of the `GtkBuildable` interface supports * setting a child as the placeholder by specifying “placeholder” as the “type” * attribute of a element. See [method@Gtk.ListBox.set_placeholder] * for info. * * # CSS nodes * * |[ * list[.separators][.rich-list][.navigation-sidebar] * ╰── row[.activatable] * ]| * * `GtkListBox` uses a single CSS node named list. It may carry the .separators * style class, when the [property@Gtk.ListBox:show-separators] property is set. * Each `GtkListBoxRow` uses a single CSS node named row. The row nodes get the * .activatable style class added when appropriate. * * The main list node may also carry style classes to select * the style of [list presentation](section-list-widget.html#list-styles): * .rich-list, .navigation-sidebar or .data-table. * * # Accessibility * * `GtkListBox` uses the %GTK_ACCESSIBLE_ROLE_LIST role and `GtkListBoxRow` uses * the %GTK_ACCESSIBLE_ROLE_LIST_ITEM role. */ /** * GtkListBoxRow: * * `GtkListBoxRow` is the kind of widget that can be added to a `GtkListBox`. */ typedef struct _GtkListBoxClass GtkListBoxClass; struct _GtkListBox { GtkWidget parent_instance; GSequence *children; GHashTable *header_hash; GtkWidget *placeholder; GtkListBoxSortFunc sort_func; gpointer sort_func_target; GDestroyNotify sort_func_target_destroy_notify; GtkListBoxFilterFunc filter_func; gpointer filter_func_target; GDestroyNotify filter_func_target_destroy_notify; GtkListBoxUpdateHeaderFunc update_header_func; gpointer update_header_func_target; GDestroyNotify update_header_func_target_destroy_notify; GtkListBoxRow *selected_row; GtkListBoxRow *cursor_row; GtkListBoxRow *active_row; GtkSelectionMode selection_mode; gulong adjustment_changed_id; GtkWidget *scrollable_parent; GtkAdjustment *adjustment; gboolean activate_single_click; gboolean accept_unpaired_release; gboolean show_separators; /* DnD */ GtkListBoxRow *drag_highlighted_row; int n_visible_rows; GListModel *bound_model; GtkListBoxCreateWidgetFunc create_widget_func; gpointer create_widget_func_data; GDestroyNotify create_widget_func_data_destroy; }; struct _GtkListBoxClass { GtkWidgetClass parent_class; void (*row_selected) (GtkListBox *box, GtkListBoxRow *row); void (*row_activated) (GtkListBox *box, GtkListBoxRow *row); void (*activate_cursor_row) (GtkListBox *box); void (*toggle_cursor_row) (GtkListBox *box); void (*move_cursor) (GtkListBox *box, GtkMovementStep step, int count, gboolean extend, gboolean modify); void (*selected_rows_changed) (GtkListBox *box); void (*select_all) (GtkListBox *box); void (*unselect_all) (GtkListBox *box); }; typedef struct { GtkWidget *child; GSequenceIter *iter; GtkWidget *header; GtkActionHelper *action_helper; int y; int height; guint visible :1; guint selected :1; guint activatable :1; guint selectable :1; } GtkListBoxRowPrivate; enum { ROW_SELECTED, ROW_ACTIVATED, ACTIVATE_CURSOR_ROW, TOGGLE_CURSOR_ROW, MOVE_CURSOR, SELECTED_ROWS_CHANGED, SELECT_ALL, UNSELECT_ALL, LAST_SIGNAL }; enum { ROW__ACTIVATE, ROW__LAST_SIGNAL }; enum { PROP_0, PROP_SELECTION_MODE, PROP_ACTIVATE_ON_SINGLE_CLICK, PROP_ACCEPT_UNPAIRED_RELEASE, PROP_SHOW_SEPARATORS, LAST_PROPERTY }; enum { ROW_PROP_0, ROW_PROP_ACTIVATABLE, ROW_PROP_SELECTABLE, ROW_PROP_CHILD, /* actionable properties */ ROW_PROP_ACTION_NAME, ROW_PROP_ACTION_TARGET, LAST_ROW_PROPERTY = ROW_PROP_ACTION_NAME }; #define ROW_PRIV(row) ((GtkListBoxRowPrivate*)gtk_list_box_row_get_instance_private ((GtkListBoxRow*)(row))) static GtkBuildableIface *parent_buildable_iface; static void gtk_list_box_buildable_interface_init (GtkBuildableIface *iface); static void gtk_list_box_row_buildable_iface_init (GtkBuildableIface *iface); static void gtk_list_box_row_actionable_iface_init (GtkActionableInterface *iface); G_DEFINE_TYPE_WITH_CODE (GtkListBox, gtk_list_box, GTK_TYPE_WIDGET, G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, gtk_list_box_buildable_interface_init)) G_DEFINE_TYPE_WITH_CODE (GtkListBoxRow, gtk_list_box_row, GTK_TYPE_WIDGET, G_ADD_PRIVATE (GtkListBoxRow) G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, gtk_list_box_row_buildable_iface_init ) G_IMPLEMENT_INTERFACE (GTK_TYPE_ACTIONABLE, gtk_list_box_row_actionable_iface_init)) static void gtk_list_box_apply_filter_all (GtkListBox *box); static void gtk_list_box_update_header (GtkListBox *box, GSequenceIter *iter); static GSequenceIter * gtk_list_box_get_next_visible (GtkListBox *box, GSequenceIter *iter); static void gtk_list_box_apply_filter (GtkListBox *box, GtkListBoxRow *row); static void gtk_list_box_add_move_binding (GtkWidgetClass *widget_class, guint keyval, GdkModifierType modmask, GtkMovementStep step, int count); static void gtk_list_box_update_cursor (GtkListBox *box, GtkListBoxRow *row, gboolean grab_focus); static void gtk_list_box_show (GtkWidget *widget); static gboolean gtk_list_box_focus (GtkWidget *widget, GtkDirectionType direction); static GSequenceIter* gtk_list_box_get_previous_visible (GtkListBox *box, GSequenceIter *iter); static GtkListBoxRow *gtk_list_box_get_first_focusable (GtkListBox *box); static GtkListBoxRow *gtk_list_box_get_last_focusable (GtkListBox *box); static void gtk_list_box_compute_expand (GtkWidget *widget, gboolean *hexpand, gboolean *vexpand); static GtkSizeRequestMode gtk_list_box_get_request_mode (GtkWidget *widget); static void gtk_list_box_size_allocate (GtkWidget *widget, int width, int height, int baseline); static void gtk_list_box_activate_cursor_row (GtkListBox *box); static void gtk_list_box_toggle_cursor_row (GtkListBox *box); static void gtk_list_box_move_cursor (GtkListBox *box, GtkMovementStep step, int count, gboolean extend, gboolean modify); static void gtk_list_box_parent_cb (GObject *object, GParamSpec *pspec, gpointer user_data); static void gtk_list_box_select_row_internal (GtkListBox *box, GtkListBoxRow *row); static void gtk_list_box_unselect_row_internal (GtkListBox *box, GtkListBoxRow *row); static void gtk_list_box_select_all_between (GtkListBox *box, GtkListBoxRow *row1, GtkListBoxRow *row2, gboolean modify); static gboolean gtk_list_box_unselect_all_internal (GtkListBox *box); static void gtk_list_box_selected_rows_changed (GtkListBox *box); static void gtk_list_box_set_accept_unpaired_release (GtkListBox *box, gboolean accept); static void gtk_list_box_click_gesture_pressed (GtkGestureClick *gesture, guint n_press, double x, double y, GtkListBox *box); static void gtk_list_box_click_gesture_released (GtkGestureClick *gesture, guint n_press, double x, double y, GtkListBox *box); static void gtk_list_box_click_unpaired_release (GtkGestureClick *gesture, double x, double y, guint button, GdkEventSequence *sequence, GtkListBox *box); static void gtk_list_box_click_gesture_stopped (GtkGestureClick *gesture, GtkListBox *box); static void gtk_list_box_update_row_styles (GtkListBox *box); static void gtk_list_box_update_row_style (GtkListBox *box, GtkListBoxRow *row); static void gtk_list_box_bound_model_changed (GListModel *list, guint position, guint removed, guint added, gpointer user_data); static void gtk_list_box_check_model_compat (GtkListBox *box); static void gtk_list_box_measure (GtkWidget *widget, GtkOrientation orientation, int for_size, int *minimum, int *natural, int *minimum_baseline, int *natural_baseline); static GParamSpec *properties[LAST_PROPERTY] = { NULL, }; static guint signals[LAST_SIGNAL] = { 0 }; static GParamSpec *row_properties[LAST_ROW_PROPERTY] = { NULL, }; static guint row_signals[ROW__LAST_SIGNAL] = { 0 }; static GtkBuildableIface *parent_row_buildable_iface; static void gtk_list_box_row_buildable_add_child (GtkBuildable *buildable, GtkBuilder *builder, GObject *child, const char *type) { if (GTK_IS_WIDGET (child)) gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (buildable), GTK_WIDGET (child)); else parent_row_buildable_iface->add_child (buildable, builder, child, type); } static void gtk_list_box_row_buildable_iface_init (GtkBuildableIface *iface) { parent_row_buildable_iface = g_type_interface_peek_parent (iface); iface->add_child = gtk_list_box_row_buildable_add_child; } /** * gtk_list_box_new: * * Creates a new `GtkListBox` container. * * Returns: a new `GtkListBox` */ GtkWidget * gtk_list_box_new (void) { return g_object_new (GTK_TYPE_LIST_BOX, NULL); } static void gtk_list_box_get_property (GObject *obj, guint property_id, GValue *value, GParamSpec *pspec) { GtkListBox *box = GTK_LIST_BOX (obj); switch (property_id) { case PROP_SELECTION_MODE: g_value_set_enum (value, box->selection_mode); break; case PROP_ACTIVATE_ON_SINGLE_CLICK: g_value_set_boolean (value, box->activate_single_click); break; case PROP_ACCEPT_UNPAIRED_RELEASE: g_value_set_boolean (value, box->accept_unpaired_release); break; case PROP_SHOW_SEPARATORS: g_value_set_boolean (value, box->show_separators); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec); break; } } static void gtk_list_box_set_property (GObject *obj, guint property_id, const GValue *value, GParamSpec *pspec) { GtkListBox *box = GTK_LIST_BOX (obj); switch (property_id) { case PROP_SELECTION_MODE: gtk_list_box_set_selection_mode (box, g_value_get_enum (value)); break; case PROP_ACTIVATE_ON_SINGLE_CLICK: gtk_list_box_set_activate_on_single_click (box, g_value_get_boolean (value)); break; case PROP_ACCEPT_UNPAIRED_RELEASE: gtk_list_box_set_accept_unpaired_release (box, g_value_get_boolean (value)); break; case PROP_SHOW_SEPARATORS: gtk_list_box_set_show_separators (box, g_value_get_boolean (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec); break; } } static void gtk_list_box_dispose (GObject *object) { GtkWidget *child; while ((child = gtk_widget_get_first_child (GTK_WIDGET (object)))) gtk_list_box_remove (GTK_LIST_BOX (object), child); G_OBJECT_CLASS (gtk_list_box_parent_class)->dispose (object); } static void gtk_list_box_finalize (GObject *obj) { GtkListBox *box = GTK_LIST_BOX (obj); if (box->sort_func_target_destroy_notify != NULL) box->sort_func_target_destroy_notify (box->sort_func_target); if (box->filter_func_target_destroy_notify != NULL) box->filter_func_target_destroy_notify (box->filter_func_target); if (box->update_header_func_target_destroy_notify != NULL) box->update_header_func_target_destroy_notify (box->update_header_func_target); g_clear_object (&box->adjustment); g_clear_object (&box->drag_highlighted_row); g_sequence_free (box->children); g_hash_table_unref (box->header_hash); if (box->bound_model) { if (box->create_widget_func_data_destroy) box->create_widget_func_data_destroy (box->create_widget_func_data); g_signal_handlers_disconnect_by_func (box->bound_model, gtk_list_box_bound_model_changed, obj); g_clear_object (&box->bound_model); } G_OBJECT_CLASS (gtk_list_box_parent_class)->finalize (obj); } static void gtk_list_box_class_init (GtkListBoxClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->get_property = gtk_list_box_get_property; object_class->set_property = gtk_list_box_set_property; object_class->dispose = gtk_list_box_dispose; object_class->finalize = gtk_list_box_finalize; widget_class->show = gtk_list_box_show; widget_class->focus = gtk_list_box_focus; widget_class->grab_focus = gtk_widget_grab_focus_self; widget_class->compute_expand = gtk_list_box_compute_expand; widget_class->get_request_mode = gtk_list_box_get_request_mode; widget_class->measure = gtk_list_box_measure; widget_class->size_allocate = gtk_list_box_size_allocate; klass->activate_cursor_row = gtk_list_box_activate_cursor_row; klass->toggle_cursor_row = gtk_list_box_toggle_cursor_row; klass->move_cursor = gtk_list_box_move_cursor; klass->select_all = gtk_list_box_select_all; klass->unselect_all = gtk_list_box_unselect_all; klass->selected_rows_changed = gtk_list_box_selected_rows_changed; /** * GtkListBox:selection-mode: (attributes org.gtk.Property.get=gtk_list_box_get_selection_mode org.gtk.Property.set=gtk_list_box_set_selection_mode) * * The selection mode used by the list box. */ properties[PROP_SELECTION_MODE] = g_param_spec_enum ("selection-mode", P_("Selection mode"), P_("The selection mode"), GTK_TYPE_SELECTION_MODE, GTK_SELECTION_SINGLE, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); /** * GtkListBox:activate-on-single-click: (attributes org.gtk.Property.get=gtk_list_box_get_activate_on_single_click org.gtk.Property.set=gtk_list_box_set_activate_on_single_click) * * Determines whether children can be activated with a single * click, or require a double-click. */ properties[PROP_ACTIVATE_ON_SINGLE_CLICK] = g_param_spec_boolean ("activate-on-single-click", P_("Activate on Single Click"), P_("Activate row on a single click"), TRUE, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); /** * GtkListBox:accept-unpaired-release: * * Whether to accept unpaired release events. */ properties[PROP_ACCEPT_UNPAIRED_RELEASE] = g_param_spec_boolean ("accept-unpaired-release", P_("Accept unpaired release"), P_("Accept unpaired release"), FALSE, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); /** * GtkListBox:show-separators: (attributes org.gtk.Property.get=gtk_list_box_get_show_separators org.gtk.Property.set=gtk_list_box_set_show_separators) * * Whether to show separators between rows. */ properties[PROP_SHOW_SEPARATORS] = g_param_spec_boolean ("show-separators", P_("Show separators"), P_("Show separators between rows"), FALSE, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); g_object_class_install_properties (object_class, LAST_PROPERTY, properties); /** * GtkListBox::row-selected: * @box: the `GtkListBox` * @row: (nullable): the selected row * * Emitted when a new row is selected, or (with a %NULL @row) * when the selection is cleared. * * When the @box is using %GTK_SELECTION_MULTIPLE, this signal will not * give you the full picture of selection changes, and you should use * the [signal@Gtk.ListBox::selected-rows-changed] signal instead. */ signals[ROW_SELECTED] = g_signal_new (I_("row-selected"), GTK_TYPE_LIST_BOX, G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GtkListBoxClass, row_selected), NULL, NULL, NULL, G_TYPE_NONE, 1, GTK_TYPE_LIST_BOX_ROW); /** * GtkListBox::selected-rows-changed: * @box: the `GtkListBox` on which the signal is emitted * * Emitted when the set of selected rows changes. */ signals[SELECTED_ROWS_CHANGED] = g_signal_new (I_("selected-rows-changed"), GTK_TYPE_LIST_BOX, G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (GtkListBoxClass, selected_rows_changed), NULL, NULL, NULL, G_TYPE_NONE, 0); /** * GtkListBox::select-all: * @box: the `GtkListBox` on which the signal is emitted * * Emitted to select all children of the box, if the selection * mode permits it. * * This is a [keybinding signal](class.SignalAction.html). * * The default binding for this signal is Ctrl-a. */ signals[SELECT_ALL] = g_signal_new (I_("select-all"), GTK_TYPE_LIST_BOX, G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GtkListBoxClass, select_all), NULL, NULL, NULL, G_TYPE_NONE, 0); /** * GtkListBox::unselect-all: * @box: the `GtkListBox` on which the signal is emitted * * Emitted to unselect all children of the box, if the selection * mode permits it. * * This is a [keybinding signal](class.SignalAction.html). * * The default binding for this signal is * Ctrl-Shift-a. */ signals[UNSELECT_ALL] = g_signal_new (I_("unselect-all"), GTK_TYPE_LIST_BOX, G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GtkListBoxClass, unselect_all), NULL, NULL, NULL, G_TYPE_NONE, 0); /** * GtkListBox::row-activated: * @box: the `GtkListBox` * @row: the activated row * * Emitted when a row has been activated by the user. */ signals[ROW_ACTIVATED] = g_signal_new (I_("row-activated"), GTK_TYPE_LIST_BOX, G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GtkListBoxClass, row_activated), NULL, NULL, NULL, G_TYPE_NONE, 1, GTK_TYPE_LIST_BOX_ROW); signals[ACTIVATE_CURSOR_ROW] = g_signal_new (I_("activate-cursor-row"), GTK_TYPE_LIST_BOX, G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GtkListBoxClass, activate_cursor_row), NULL, NULL, NULL, G_TYPE_NONE, 0); signals[TOGGLE_CURSOR_ROW] = g_signal_new (I_("toggle-cursor-row"), GTK_TYPE_LIST_BOX, G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GtkListBoxClass, toggle_cursor_row), NULL, NULL, NULL, G_TYPE_NONE, 0); signals[MOVE_CURSOR] = g_signal_new (I_("move-cursor"), GTK_TYPE_LIST_BOX, G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GtkListBoxClass, move_cursor), NULL, NULL, _gtk_marshal_VOID__ENUM_INT_BOOLEAN_BOOLEAN, G_TYPE_NONE, 4, GTK_TYPE_MOVEMENT_STEP, G_TYPE_INT, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN); g_signal_set_va_marshaller (signals[MOVE_CURSOR], G_TYPE_FROM_CLASS (klass), _gtk_marshal_VOID__ENUM_INT_BOOLEAN_BOOLEANv); gtk_widget_class_set_activate_signal (widget_class, signals[ACTIVATE_CURSOR_ROW]); gtk_list_box_add_move_binding (widget_class, GDK_KEY_Home, 0, GTK_MOVEMENT_BUFFER_ENDS, -1); gtk_list_box_add_move_binding (widget_class, GDK_KEY_KP_Home, 0, GTK_MOVEMENT_BUFFER_ENDS, -1); gtk_list_box_add_move_binding (widget_class, GDK_KEY_End, 0, GTK_MOVEMENT_BUFFER_ENDS, 1); gtk_list_box_add_move_binding (widget_class, GDK_KEY_KP_End, 0, GTK_MOVEMENT_BUFFER_ENDS, 1); gtk_list_box_add_move_binding (widget_class, GDK_KEY_Up, 0, GTK_MOVEMENT_DISPLAY_LINES, -1); gtk_list_box_add_move_binding (widget_class, GDK_KEY_KP_Up, 0, GTK_MOVEMENT_DISPLAY_LINES, -1); gtk_list_box_add_move_binding (widget_class, GDK_KEY_Down, 0, GTK_MOVEMENT_DISPLAY_LINES, 1); gtk_list_box_add_move_binding (widget_class, GDK_KEY_KP_Down, 0, GTK_MOVEMENT_DISPLAY_LINES, 1); gtk_list_box_add_move_binding (widget_class, GDK_KEY_Page_Up, 0, GTK_MOVEMENT_PAGES, -1); gtk_list_box_add_move_binding (widget_class, GDK_KEY_KP_Page_Up, 0, GTK_MOVEMENT_PAGES, -1); gtk_list_box_add_move_binding (widget_class, GDK_KEY_Page_Down, 0, GTK_MOVEMENT_PAGES, 1); gtk_list_box_add_move_binding (widget_class, GDK_KEY_KP_Page_Down, 0, GTK_MOVEMENT_PAGES, 1); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_space, GDK_CONTROL_MASK, "toggle-cursor-row", NULL); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_KP_Space, GDK_CONTROL_MASK, "toggle-cursor-row", NULL); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_a, GDK_CONTROL_MASK, "select-all", NULL); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_a, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "unselect-all", NULL); gtk_widget_class_set_css_name (widget_class, I_("list")); gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_LIST); } static void gtk_list_box_init (GtkListBox *box) { GtkWidget *widget = GTK_WIDGET (box); GtkGesture *gesture; gtk_widget_set_focusable (GTK_WIDGET (box), TRUE); box->selection_mode = GTK_SELECTION_SINGLE; box->activate_single_click = TRUE; box->children = g_sequence_new (NULL); box->header_hash = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, NULL); gesture = gtk_gesture_click_new (); gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (gesture), GTK_PHASE_BUBBLE); gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (gesture), FALSE); gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), GDK_BUTTON_PRIMARY); g_signal_connect (gesture, "pressed", G_CALLBACK (gtk_list_box_click_gesture_pressed), box); g_signal_connect (gesture, "released", G_CALLBACK (gtk_list_box_click_gesture_released), box); g_signal_connect (gesture, "stopped", G_CALLBACK (gtk_list_box_click_gesture_stopped), box); g_signal_connect (gesture, "unpaired-release", G_CALLBACK (gtk_list_box_click_unpaired_release), box); gtk_widget_add_controller (widget, GTK_EVENT_CONTROLLER (gesture)); g_signal_connect (box, "notify::parent", G_CALLBACK (gtk_list_box_parent_cb), NULL); } /** * gtk_list_box_get_selected_row: * @box: a `GtkListBox` * * Gets the selected row, or %NULL if no rows are selected. * * Note that the box may allow multiple selection, in which * case you should use [method@Gtk.ListBox.selected_foreach] to * find all selected rows. * * Returns: (transfer none) (nullable): the selected row */ GtkListBoxRow * gtk_list_box_get_selected_row (GtkListBox *box) { g_return_val_if_fail (GTK_IS_LIST_BOX (box), NULL); return box->selected_row; } /** * gtk_list_box_get_row_at_index: * @box: a `GtkListBox` * @index_: the index of the row * * Gets the n-th child in the list (not counting headers). * * If @index_ is negative or larger than the number of items in the * list, %NULL is returned. * * Returns: (transfer none) (nullable): the child `GtkWidget` */ GtkListBoxRow * gtk_list_box_get_row_at_index (GtkListBox *box, int index_) { GSequenceIter *iter; g_return_val_if_fail (GTK_IS_LIST_BOX (box), NULL); iter = g_sequence_get_iter_at_pos (box->children, index_); if (!g_sequence_iter_is_end (iter)) return g_sequence_get (iter); return NULL; } static int row_y_cmp_func (gconstpointer a, gconstpointer b, gpointer user_data) { int y = GPOINTER_TO_INT (b); GtkListBoxRowPrivate *row_priv = ROW_PRIV (a); if (y < row_priv->y) return 1; else if (y >= row_priv->y + row_priv->height) return -1; return 0; } /** * gtk_list_box_get_row_at_y: * @box: a `GtkListBox` * @y: position * * Gets the row at the @y position. * * Returns: (transfer none) (nullable): the row */ GtkListBoxRow * gtk_list_box_get_row_at_y (GtkListBox *box, int y) { GSequenceIter *iter; g_return_val_if_fail (GTK_IS_LIST_BOX (box), NULL); iter = g_sequence_lookup (box->children, GINT_TO_POINTER (y), row_y_cmp_func, NULL); if (iter) return GTK_LIST_BOX_ROW (g_sequence_get (iter)); return NULL; } /** * gtk_list_box_select_row: * @box: a `GtkListBox` * @row: (nullable): The row to select * * Make @row the currently selected row. */ void gtk_list_box_select_row (GtkListBox *box, GtkListBoxRow *row) { gboolean dirty = FALSE; g_return_if_fail (GTK_IS_LIST_BOX (box)); g_return_if_fail (row == NULL || GTK_IS_LIST_BOX_ROW (row)); if (row) gtk_list_box_select_row_internal (box, row); else dirty = gtk_list_box_unselect_all_internal (box); if (dirty) { g_signal_emit (box, signals[ROW_SELECTED], 0, NULL); g_signal_emit (box, signals[SELECTED_ROWS_CHANGED], 0); } } /** * gtk_list_box_unselect_row: * @box: a `GtkListBox` * @row: the row to unselected * * Unselects a single row of @box, if the selection mode allows it. */ void gtk_list_box_unselect_row (GtkListBox *box, GtkListBoxRow *row) { g_return_if_fail (GTK_IS_LIST_BOX (box)); g_return_if_fail (GTK_IS_LIST_BOX_ROW (row)); gtk_list_box_unselect_row_internal (box, row); } /** * gtk_list_box_select_all: * @box: a `GtkListBox` * * Select all children of @box, if the selection mode allows it. */ void gtk_list_box_select_all (GtkListBox *box) { g_return_if_fail (GTK_IS_LIST_BOX (box)); if (box->selection_mode != GTK_SELECTION_MULTIPLE) return; if (g_sequence_get_length (box->children) > 0) { gtk_list_box_select_all_between (box, NULL, NULL, FALSE); g_signal_emit (box, signals[SELECTED_ROWS_CHANGED], 0); } } /** * gtk_list_box_unselect_all: * @box: a `GtkListBox` * * Unselect all children of @box, if the selection mode allows it. */ void gtk_list_box_unselect_all (GtkListBox *box) { gboolean dirty = FALSE; g_return_if_fail (GTK_IS_LIST_BOX (box)); if (box->selection_mode == GTK_SELECTION_BROWSE) return; dirty = gtk_list_box_unselect_all_internal (box); if (dirty) { g_signal_emit (box, signals[ROW_SELECTED], 0, NULL); g_signal_emit (box, signals[SELECTED_ROWS_CHANGED], 0); } } static void gtk_list_box_selected_rows_changed (GtkListBox *box) { } /** * GtkListBoxForeachFunc: * @box: a `GtkListBox` * @row: a `GtkListBoxRow` * @user_data: (closure): user data * * A function used by gtk_list_box_selected_foreach(). * * It will be called on every selected child of the @box. */ /** * gtk_list_box_selected_foreach: * @box: a `GtkListBox` * @func: (scope call): the function to call for each selected child * @data: user data to pass to the function * * Calls a function for each selected child. * * Note that the selection cannot be modified from within this function. */ void gtk_list_box_selected_foreach (GtkListBox *box, GtkListBoxForeachFunc func, gpointer data) { GtkListBoxRow *row; GSequenceIter *iter; g_return_if_fail (GTK_IS_LIST_BOX (box)); for (iter = g_sequence_get_begin_iter (box->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { row = g_sequence_get (iter); if (gtk_list_box_row_is_selected (row)) (*func) (box, row, data); } } /** * gtk_list_box_get_selected_rows: * @box: a `GtkListBox` * * Creates a list of all selected children. * * Returns: (element-type GtkListBoxRow) (transfer container): * A `GList` containing the `GtkWidget` for each selected child. * Free with g_list_free() when done. */ GList * gtk_list_box_get_selected_rows (GtkListBox *box) { GtkListBoxRow *row; GSequenceIter *iter; GList *selected = NULL; g_return_val_if_fail (GTK_IS_LIST_BOX (box), NULL); for (iter = g_sequence_get_begin_iter (box->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { row = g_sequence_get (iter); if (gtk_list_box_row_is_selected (row)) selected = g_list_prepend (selected, row); } return g_list_reverse (selected); } /** * gtk_list_box_set_placeholder: * @box: a `GtkListBox` * @placeholder: (nullable): a `GtkWidget` * * Sets the placeholder widget that is shown in the list when * it doesn't display any visible children. */ void gtk_list_box_set_placeholder (GtkListBox *box, GtkWidget *placeholder) { g_return_if_fail (GTK_IS_LIST_BOX (box)); if (box->placeholder) { gtk_widget_unparent (box->placeholder); gtk_widget_queue_resize (GTK_WIDGET (box)); } box->placeholder = placeholder; if (placeholder) { gtk_widget_set_parent (placeholder, GTK_WIDGET (box)); gtk_widget_set_child_visible (placeholder, box->n_visible_rows == 0); } } /** * gtk_list_box_set_adjustment: * @box: a `GtkListBox` * @adjustment: (nullable): the adjustment * * Sets the adjustment (if any) that the widget uses to * for vertical scrolling. * * For instance, this is used to get the page size for * PageUp/Down key handling. * * In the normal case when the @box is packed inside * a `GtkScrolledWindow` the adjustment from that will * be picked up automatically, so there is no need * to manually do that. */ void gtk_list_box_set_adjustment (GtkListBox *box, GtkAdjustment *adjustment) { g_return_if_fail (GTK_IS_LIST_BOX (box)); g_return_if_fail (adjustment == NULL || GTK_IS_ADJUSTMENT (adjustment)); if (adjustment) g_object_ref_sink (adjustment); if (box->adjustment) g_object_unref (box->adjustment); box->adjustment = adjustment; } /** * gtk_list_box_get_adjustment: * @box: a `GtkListBox` * * Gets the adjustment (if any) that the widget uses to * for vertical scrolling. * * Returns: (transfer none): the adjustment */ GtkAdjustment * gtk_list_box_get_adjustment (GtkListBox *box) { g_return_val_if_fail (GTK_IS_LIST_BOX (box), NULL); return box->adjustment; } static void adjustment_changed (GObject *object, GParamSpec *pspec, gpointer data) { GtkAdjustment *adjustment; adjustment = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (object)); gtk_list_box_set_adjustment (GTK_LIST_BOX (data), adjustment); } static void gtk_list_box_parent_cb (GObject *object, GParamSpec *pspec, gpointer user_data) { GtkListBox *box = GTK_LIST_BOX (object); GtkWidget *parent; parent = gtk_widget_get_parent (GTK_WIDGET (object)); if (box->adjustment_changed_id != 0 && box->scrollable_parent != NULL) { g_signal_handler_disconnect (box->scrollable_parent, box->adjustment_changed_id); } if (parent && GTK_IS_SCROLLABLE (parent)) { adjustment_changed (G_OBJECT (parent), NULL, object); box->scrollable_parent = parent; box->adjustment_changed_id = g_signal_connect (parent, "notify::vadjustment", G_CALLBACK (adjustment_changed), object); } else { gtk_list_box_set_adjustment (GTK_LIST_BOX (object), NULL); box->adjustment_changed_id = 0; box->scrollable_parent = NULL; } } /** * gtk_list_box_set_selection_mode: (attributes org.gtk.Method.set_property=selection-mode) * @box: a `GtkListBox` * @mode: The `GtkSelectionMode` * * Sets how selection works in the listbox. */ void gtk_list_box_set_selection_mode (GtkListBox *box, GtkSelectionMode mode) { gboolean dirty = FALSE; g_return_if_fail (GTK_IS_LIST_BOX (box)); if (box->selection_mode == mode) return; if (mode == GTK_SELECTION_NONE || box->selection_mode == GTK_SELECTION_MULTIPLE) dirty = gtk_list_box_unselect_all_internal (box); box->selection_mode = mode; gtk_list_box_update_row_styles (box); gtk_accessible_update_property (GTK_ACCESSIBLE (box), GTK_ACCESSIBLE_PROPERTY_MULTI_SELECTABLE, mode == GTK_SELECTION_MULTIPLE, -1); g_object_notify_by_pspec (G_OBJECT (box), properties[PROP_SELECTION_MODE]); if (dirty) { g_signal_emit (box, signals[ROW_SELECTED], 0, NULL); g_signal_emit (box, signals[SELECTED_ROWS_CHANGED], 0); } } /** * gtk_list_box_get_selection_mode: (attributes org.gtk.Method.get_property=selection-mode) * @box: a `GtkListBox` * * Gets the selection mode of the listbox. * * Returns: a `GtkSelectionMode` */ GtkSelectionMode gtk_list_box_get_selection_mode (GtkListBox *box) { g_return_val_if_fail (GTK_IS_LIST_BOX (box), GTK_SELECTION_NONE); return box->selection_mode; } /** * gtk_list_box_set_filter_func: * @box: a `GtkListBox` * @filter_func: (nullable): callback that lets you filter which rows to show * @user_data: (closure): user data passed to @filter_func * @destroy: destroy notifier for @user_data * * By setting a filter function on the @box one can decide dynamically which * of the rows to show. * * For instance, to implement a search function on a list that * filters the original list to only show the matching rows. * * The @filter_func will be called for each row after the call, and * it will continue to be called each time a row changes (via * [method@Gtk.ListBoxRow.changed]) or when [method@Gtk.ListBox.invalidate_filter] * is called. * * Note that using a filter function is incompatible with using a model * (see [method@Gtk.ListBox.bind_model]). */ void gtk_list_box_set_filter_func (GtkListBox *box, GtkListBoxFilterFunc filter_func, gpointer user_data, GDestroyNotify destroy) { g_return_if_fail (GTK_IS_LIST_BOX (box)); if (box->filter_func_target_destroy_notify != NULL) box->filter_func_target_destroy_notify (box->filter_func_target); box->filter_func = filter_func; box->filter_func_target = user_data; box->filter_func_target_destroy_notify = destroy; gtk_list_box_check_model_compat (box); gtk_list_box_invalidate_filter (box); } /** * gtk_list_box_set_header_func: * @box: a `GtkListBox` * @update_header: (nullable): callback that lets you add row headers * @user_data: (closure): user data passed to @update_header * @destroy: destroy notifier for @user_data * * Sets a header function. * * By setting a header function on the @box one can dynamically add headers * in front of rows, depending on the contents of the row and its position * in the list. * * For instance, one could use it to add headers in front of the first item * of a new kind, in a list sorted by the kind. * * The @update_header can look at the current header widget using * [method@Gtk.ListBoxRow.get_header] and either update the state of the widget * as needed, or set a new one using [method@Gtk.ListBoxRow.set_header]. If no * header is needed, set the header to %NULL. * * Note that you may get many calls @update_header to this for a particular * row when e.g. changing things that don’t affect the header. In this case * it is important for performance to not blindly replace an existing header * with an identical one. * * The @update_header function will be called for each row after the call, * and it will continue to be called each time a row changes (via * [method@Gtk.ListBoxRow.changed]) and when the row before changes (either * by [method@Gtk.ListBoxRow.changed] on the previous row, or when the previous * row becomes a different row). It is also called for all rows when * [method@Gtk.ListBox.invalidate_headers] is called. */ void gtk_list_box_set_header_func (GtkListBox *box, GtkListBoxUpdateHeaderFunc update_header, gpointer user_data, GDestroyNotify destroy) { g_return_if_fail (GTK_IS_LIST_BOX (box)); if (box->update_header_func_target_destroy_notify != NULL) box->update_header_func_target_destroy_notify (box->update_header_func_target); box->update_header_func = update_header; box->update_header_func_target = user_data; box->update_header_func_target_destroy_notify = destroy; gtk_list_box_invalidate_headers (box); } /** * gtk_list_box_invalidate_filter: * @box: a `GtkListBox` * * Update the filtering for all rows. * * Call this when result * of the filter function on the @box is changed due * to an external factor. For instance, this would be used * if the filter function just looked for a specific search * string and the entry with the search string has changed. */ void gtk_list_box_invalidate_filter (GtkListBox *box) { g_return_if_fail (GTK_IS_LIST_BOX (box)); gtk_list_box_apply_filter_all (box); gtk_list_box_invalidate_headers (box); gtk_widget_queue_resize (GTK_WIDGET (box)); } static int do_sort (GtkListBoxRow *a, GtkListBoxRow *b, GtkListBox *box) { return box->sort_func (a, b, box->sort_func_target); } static void gtk_list_box_reorder_foreach (gpointer data, gpointer user_data) { GtkWidget **previous = user_data; GtkWidget *row = data; if (*previous) gtk_widget_insert_after (row, _gtk_widget_get_parent (row), *previous); *previous = row; } /** * gtk_list_box_invalidate_sort: * @box: a `GtkListBox` * * Update the sorting for all rows. * * Call this when result * of the sort function on the @box is changed due * to an external factor. */ void gtk_list_box_invalidate_sort (GtkListBox *box) { GtkWidget *previous = NULL; g_return_if_fail (GTK_IS_LIST_BOX (box)); if (box->sort_func == NULL) return; g_sequence_sort (box->children, (GCompareDataFunc)do_sort, box); g_sequence_foreach (box->children, gtk_list_box_reorder_foreach, &previous); gtk_list_box_invalidate_headers (box); gtk_widget_queue_resize (GTK_WIDGET (box)); } static void gtk_list_box_do_reseparate (GtkListBox *box) { GSequenceIter *iter; for (iter = g_sequence_get_begin_iter (box->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) gtk_list_box_update_header (box, iter); gtk_widget_queue_resize (GTK_WIDGET (box)); } /** * gtk_list_box_invalidate_headers: * @box: a `GtkListBox` * * Update the separators for all rows. * * Call this when result * of the header function on the @box is changed due * to an external factor. */ void gtk_list_box_invalidate_headers (GtkListBox *box) { g_return_if_fail (GTK_IS_LIST_BOX (box)); if (!gtk_widget_get_visible (GTK_WIDGET (box))) return; gtk_list_box_do_reseparate (box); } /** * gtk_list_box_set_sort_func: * @box: a `GtkListBox` * @sort_func: (nullable): the sort function * @user_data: (closure): user data passed to @sort_func * @destroy: destroy notifier for @user_data * * Sets a sort function. * * By setting a sort function on the @box one can dynamically reorder * the rows of the list, based on the contents of the rows. * * The @sort_func will be called for each row after the call, and will * continue to be called each time a row changes (via * [method@Gtk.ListBoxRow.changed]) and when [method@Gtk.ListBox.invalidate_sort] * is called. * * Note that using a sort function is incompatible with using a model * (see [method@Gtk.ListBox.bind_model]). */ void gtk_list_box_set_sort_func (GtkListBox *box, GtkListBoxSortFunc sort_func, gpointer user_data, GDestroyNotify destroy) { g_return_if_fail (GTK_IS_LIST_BOX (box)); if (box->sort_func_target_destroy_notify != NULL) box->sort_func_target_destroy_notify (box->sort_func_target); box->sort_func = sort_func; box->sort_func_target = user_data; box->sort_func_target_destroy_notify = destroy; gtk_list_box_check_model_compat (box); gtk_list_box_invalidate_sort (box); } static void gtk_list_box_got_row_changed (GtkListBox *box, GtkListBoxRow *row) { GtkListBoxRowPrivate *row_priv = ROW_PRIV (row); GSequenceIter *prev_next, *next; g_return_if_fail (GTK_IS_LIST_BOX (box)); g_return_if_fail (GTK_IS_LIST_BOX_ROW (row)); prev_next = gtk_list_box_get_next_visible (box, row_priv->iter); if (box->sort_func != NULL) { g_sequence_sort_changed (row_priv->iter, (GCompareDataFunc)do_sort, box); gtk_widget_queue_resize (GTK_WIDGET (box)); } gtk_list_box_apply_filter (box, row); if (gtk_widget_get_visible (GTK_WIDGET (box))) { next = gtk_list_box_get_next_visible (box, row_priv->iter); gtk_list_box_update_header (box, row_priv->iter); gtk_list_box_update_header (box, next); gtk_list_box_update_header (box, prev_next); } } /** * gtk_list_box_set_activate_on_single_click: (attributes org.gtk.Method.set_property=activate-on-single-click) * @box: a `GtkListBox` * @single: a boolean * * If @single is %TRUE, rows will be activated when you click on them, * otherwise you need to double-click. */ void gtk_list_box_set_activate_on_single_click (GtkListBox *box, gboolean single) { g_return_if_fail (GTK_IS_LIST_BOX (box)); single = single != FALSE; if (box->activate_single_click == single) return; box->activate_single_click = single; g_object_notify_by_pspec (G_OBJECT (box), properties[PROP_ACTIVATE_ON_SINGLE_CLICK]); } /** * gtk_list_box_get_activate_on_single_click: (attributes org.gtk.Metthod.get_property=activate-on-single-click) * @box: a `GtkListBox` * * Returns whether rows activate on single clicks. * * Returns: %TRUE if rows are activated on single click, %FALSE otherwise */ gboolean gtk_list_box_get_activate_on_single_click (GtkListBox *box) { g_return_val_if_fail (GTK_IS_LIST_BOX (box), FALSE); return box->activate_single_click; } void gtk_list_box_set_accept_unpaired_release (GtkListBox *box, gboolean accept) { if (box->accept_unpaired_release == accept) return; box->accept_unpaired_release = accept; g_object_notify_by_pspec (G_OBJECT (box), properties[PROP_ACCEPT_UNPAIRED_RELEASE]); } static void gtk_list_box_add_move_binding (GtkWidgetClass *widget_class, guint keyval, GdkModifierType modmask, GtkMovementStep step, int count) { gtk_widget_class_add_binding_signal (widget_class, keyval, modmask, "move-cursor", "(iibb)", step, count, FALSE, FALSE); gtk_widget_class_add_binding_signal (widget_class, keyval, modmask | GDK_SHIFT_MASK, "move-cursor", "(iibb)", step, count, TRUE, FALSE); gtk_widget_class_add_binding_signal (widget_class, keyval, modmask | GDK_CONTROL_MASK, "move-cursor", "(iibb)", step, count, FALSE, TRUE); gtk_widget_class_add_binding_signal (widget_class, keyval, modmask | GDK_SHIFT_MASK | GDK_CONTROL_MASK, "move-cursor", "(iibb)", step, count, TRUE, TRUE); } static void ensure_row_visible (GtkListBox *box, GtkListBoxRow *row) { GtkWidget *header; int y, height; graphene_rect_t rect; if (!box->adjustment) return; if (!gtk_widget_compute_bounds (GTK_WIDGET (row), GTK_WIDGET (box), &rect)) return; y = rect.origin.y; height = rect.size.height; /* If the row has a header, we want to ensure that it is visible as well. */ header = ROW_PRIV (row)->header; if (GTK_IS_WIDGET (header) && gtk_widget_is_drawable (header)) { if (gtk_widget_compute_bounds (header, GTK_WIDGET (box), &rect)) { y = rect.origin.y; height += rect.size.height; } } gtk_adjustment_clamp_page (box->adjustment, y, y + height); } static void gtk_list_box_update_cursor (GtkListBox *box, GtkListBoxRow *row, gboolean grab_focus) { box->cursor_row = row; ensure_row_visible (box, row); if (grab_focus) { GtkWidget *focus; focus = gtk_root_get_focus (gtk_widget_get_root (GTK_WIDGET (box))); if (!focus || !gtk_widget_is_ancestor (focus, GTK_WIDGET (row))) gtk_widget_grab_focus (GTK_WIDGET (row)); } gtk_widget_queue_draw (GTK_WIDGET (row)); } static GtkListBox * gtk_list_box_row_get_box (GtkListBoxRow *row) { GtkWidget *parent; parent = gtk_widget_get_parent (GTK_WIDGET (row)); if (parent && GTK_IS_LIST_BOX (parent)) return GTK_LIST_BOX (parent); return NULL; } static gboolean row_is_visible (GtkListBoxRow *row) { return ROW_PRIV (row)->visible; } static gboolean gtk_list_box_row_set_selected (GtkListBoxRow *row, gboolean selected) { if (!ROW_PRIV (row)->selectable) return FALSE; if (ROW_PRIV (row)->selected != selected) { ROW_PRIV (row)->selected = selected; if (selected) gtk_widget_set_state_flags (GTK_WIDGET (row), GTK_STATE_FLAG_SELECTED, FALSE); else gtk_widget_unset_state_flags (GTK_WIDGET (row), GTK_STATE_FLAG_SELECTED); gtk_accessible_update_state (GTK_ACCESSIBLE (row), GTK_ACCESSIBLE_STATE_SELECTED, selected, -1); return TRUE; } return FALSE; } static gboolean gtk_list_box_unselect_all_internal (GtkListBox *box) { GtkListBoxRow *row; GSequenceIter *iter; gboolean dirty = FALSE; if (box->selection_mode == GTK_SELECTION_NONE) return FALSE; for (iter = g_sequence_get_begin_iter (box->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { row = g_sequence_get (iter); dirty |= gtk_list_box_row_set_selected (row, FALSE); } box->selected_row = NULL; return dirty; } static void gtk_list_box_unselect_row_internal (GtkListBox *box, GtkListBoxRow *row) { if (!ROW_PRIV (row)->selected) return; if (box->selection_mode == GTK_SELECTION_NONE) return; else if (box->selection_mode != GTK_SELECTION_MULTIPLE) gtk_list_box_unselect_all_internal (box); else gtk_list_box_row_set_selected (row, FALSE); g_signal_emit (box, signals[ROW_SELECTED], 0, NULL); g_signal_emit (box, signals[SELECTED_ROWS_CHANGED], 0); } static void gtk_list_box_select_row_internal (GtkListBox *box, GtkListBoxRow *row) { if (!ROW_PRIV (row)->selectable) return; if (ROW_PRIV (row)->selected) return; if (box->selection_mode == GTK_SELECTION_NONE) return; if (box->selection_mode != GTK_SELECTION_MULTIPLE) gtk_list_box_unselect_all_internal (box); gtk_list_box_row_set_selected (row, TRUE); box->selected_row = row; g_signal_emit (box, signals[ROW_SELECTED], 0, row); g_signal_emit (box, signals[SELECTED_ROWS_CHANGED], 0); } static void gtk_list_box_select_all_between (GtkListBox *box, GtkListBoxRow *row1, GtkListBoxRow *row2, gboolean modify) { GSequenceIter *iter, *iter1, *iter2; if (row1) iter1 = ROW_PRIV (row1)->iter; else iter1 = g_sequence_get_begin_iter (box->children); if (row2) iter2 = ROW_PRIV (row2)->iter; else iter2 = g_sequence_get_end_iter (box->children); if (g_sequence_iter_compare (iter2, iter1) < 0) { iter = iter1; iter1 = iter2; iter2 = iter; } for (iter = iter1; !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { GtkListBoxRow *row; row = GTK_LIST_BOX_ROW (g_sequence_get (iter)); if (row_is_visible (row)) { if (modify) gtk_list_box_row_set_selected (row, !ROW_PRIV (row)->selected); else gtk_list_box_row_set_selected (row, TRUE); } if (g_sequence_iter_compare (iter, iter2) == 0) break; } } #define gtk_list_box_update_selection(b,r,m,e) \ gtk_list_box_update_selection_full((b), (r), (m), (e), TRUE) static void gtk_list_box_update_selection_full (GtkListBox *box, GtkListBoxRow *row, gboolean modify, gboolean extend, gboolean grab_cursor) { gtk_list_box_update_cursor (box, row, grab_cursor); if (box->selection_mode == GTK_SELECTION_NONE) return; if (!ROW_PRIV (row)->selectable) return; if (box->selection_mode == GTK_SELECTION_BROWSE) { gtk_list_box_unselect_all_internal (box); gtk_list_box_row_set_selected (row, TRUE); box->selected_row = row; g_signal_emit (box, signals[ROW_SELECTED], 0, row); } else if (box->selection_mode == GTK_SELECTION_SINGLE) { gboolean was_selected; was_selected = ROW_PRIV (row)->selected; gtk_list_box_unselect_all_internal (box); gtk_list_box_row_set_selected (row, modify ? !was_selected : TRUE); box->selected_row = ROW_PRIV (row)->selected ? row : NULL; g_signal_emit (box, signals[ROW_SELECTED], 0, box->selected_row); } else /* GTK_SELECTION_MULTIPLE */ { if (extend) { GtkListBoxRow *selected_row; selected_row = box->selected_row; gtk_list_box_unselect_all_internal (box); if (selected_row == NULL) { gtk_list_box_row_set_selected (row, TRUE); box->selected_row = row; g_signal_emit (box, signals[ROW_SELECTED], 0, row); } else { gtk_list_box_select_all_between (box, selected_row, row, FALSE); box->selected_row = selected_row; } } else { if (modify) { gtk_list_box_row_set_selected (row, !ROW_PRIV (row)->selected); g_signal_emit (box, signals[ROW_SELECTED], 0, ROW_PRIV (row)->selected ? row : NULL); } else { gtk_list_box_unselect_all_internal (box); gtk_list_box_row_set_selected (row, !ROW_PRIV (row)->selected); box->selected_row = row; g_signal_emit (box, signals[ROW_SELECTED], 0, row); } } } g_signal_emit (box, signals[SELECTED_ROWS_CHANGED], 0); } static void gtk_list_box_activate (GtkListBox *box, GtkListBoxRow *row) { if (!gtk_list_box_row_get_activatable (row)) return; if (ROW_PRIV (row)->action_helper) gtk_action_helper_activate (ROW_PRIV (row)->action_helper); else g_signal_emit (box, signals[ROW_ACTIVATED], 0, row); } #define gtk_list_box_select_and_activate(b,r) \ gtk_list_box_select_and_activate_full ((b), (r), TRUE) static void gtk_list_box_select_and_activate_full (GtkListBox *box, GtkListBoxRow *row, gboolean grab_focus) { if (row != NULL) { gtk_list_box_select_row_internal (box, row); gtk_list_box_update_cursor (box, row, grab_focus); gtk_list_box_activate (box, row); } } static void gtk_list_box_click_gesture_pressed (GtkGestureClick *gesture, guint n_press, double x, double y, GtkListBox *box) { GtkListBoxRow *row; box->active_row = NULL; row = gtk_list_box_get_row_at_y (box, y); if (row != NULL && gtk_widget_is_sensitive (GTK_WIDGET (row))) { box->active_row = row; if (n_press == 2 && !box->activate_single_click) gtk_list_box_activate (box, row); } } static void gtk_list_box_click_unpaired_release (GtkGestureClick *gesture, double x, double y, guint button, GdkEventSequence *sequence, GtkListBox *box) { GtkListBoxRow *row; if (!box->activate_single_click || !box->accept_unpaired_release) return; row = gtk_list_box_get_row_at_y (box, y); if (row) gtk_list_box_select_and_activate (box, row); } static void gtk_list_box_click_gesture_released (GtkGestureClick *gesture, guint n_press, double x, double y, GtkListBox *box) { /* Take a ref to protect against reentrancy * (the activation may destroy the widget) */ g_object_ref (box); if (box->active_row != NULL && box->active_row == gtk_list_box_get_row_at_y (box, y)) { gboolean focus_on_click = gtk_widget_get_focus_on_click (GTK_WIDGET (box->active_row)); if (n_press == 1 && box->activate_single_click) gtk_list_box_select_and_activate_full (box, box->active_row, focus_on_click); else { GdkEventSequence *sequence; GdkInputSource source; GdkEvent *event; GdkModifierType state; gboolean extend; gboolean modify; /* With touch, we default to modifying the selection. * You can still clear the selection and start over * by holding Ctrl. */ sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture)); event = gtk_gesture_get_last_event (GTK_GESTURE (gesture), sequence); state = gdk_event_get_modifier_state (event); extend = (state & GDK_SHIFT_MASK) != 0; modify = (state & GDK_CONTROL_MASK) != 0; source = gdk_device_get_source (gdk_event_get_device (event)); if (source == GDK_SOURCE_TOUCHSCREEN) modify = !modify; gtk_list_box_update_selection_full (box, box->active_row, modify, extend, focus_on_click); } } if (box->active_row) { box->active_row = NULL; } g_object_unref (box); } static void gtk_list_box_click_gesture_stopped (GtkGestureClick *gesture, GtkListBox *box) { if (box->active_row) { box->active_row = NULL; gtk_widget_queue_draw (GTK_WIDGET (box)); } } static void gtk_list_box_show (GtkWidget *widget) { gtk_list_box_do_reseparate (GTK_LIST_BOX (widget)); GTK_WIDGET_CLASS (gtk_list_box_parent_class)->show (widget); } static gboolean gtk_list_box_focus (GtkWidget *widget, GtkDirectionType direction) { GtkListBox *box = GTK_LIST_BOX (widget); GtkWidget *focus_child; GtkListBoxRow *next_focus_row; GtkWidget *row; GtkWidget *header; focus_child = gtk_widget_get_focus_child (widget); next_focus_row = NULL; if (focus_child != NULL) { GSequenceIter *i; if (gtk_widget_child_focus (focus_child, direction)) return TRUE; if (direction == GTK_DIR_UP || direction == GTK_DIR_TAB_BACKWARD) { if (GTK_IS_LIST_BOX_ROW (focus_child)) { header = ROW_PRIV (GTK_LIST_BOX_ROW (focus_child))->header; if (header && gtk_widget_child_focus (header, direction)) return TRUE; } if (GTK_IS_LIST_BOX_ROW (focus_child)) row = focus_child; else row = g_hash_table_lookup (box->header_hash, focus_child); if (GTK_IS_LIST_BOX_ROW (row)) i = gtk_list_box_get_previous_visible (box, ROW_PRIV (GTK_LIST_BOX_ROW (row))->iter); else i = NULL; while (i != NULL) { if (gtk_widget_get_sensitive (g_sequence_get (i))) { next_focus_row = g_sequence_get (i); break; } i = gtk_list_box_get_previous_visible (box, i); } } else if (direction == GTK_DIR_DOWN || direction == GTK_DIR_TAB_FORWARD) { if (GTK_IS_LIST_BOX_ROW (focus_child)) i = gtk_list_box_get_next_visible (box, ROW_PRIV (GTK_LIST_BOX_ROW (focus_child))->iter); else { row = g_hash_table_lookup (box->header_hash, focus_child); if (GTK_IS_LIST_BOX_ROW (row)) i = ROW_PRIV (GTK_LIST_BOX_ROW (row))->iter; else i = NULL; } while (!g_sequence_iter_is_end (i)) { if (gtk_widget_get_sensitive (g_sequence_get (i))) { next_focus_row = g_sequence_get (i); break; } i = gtk_list_box_get_next_visible (box, i); } } } else { /* No current focus row */ switch (direction) { case GTK_DIR_UP: case GTK_DIR_TAB_BACKWARD: next_focus_row = box->selected_row; if (next_focus_row == NULL) next_focus_row = gtk_list_box_get_last_focusable (box); break; case GTK_DIR_DOWN: case GTK_DIR_TAB_FORWARD: case GTK_DIR_LEFT: case GTK_DIR_RIGHT: default: next_focus_row = box->selected_row; if (next_focus_row == NULL) next_focus_row = gtk_list_box_get_first_focusable (box); break; } } if (next_focus_row == NULL) { if (direction == GTK_DIR_UP || direction == GTK_DIR_DOWN) { if (gtk_widget_keynav_failed (GTK_WIDGET (box), direction)) return TRUE; } return FALSE; } if (direction == GTK_DIR_DOWN || direction == GTK_DIR_TAB_FORWARD) { header = ROW_PRIV (next_focus_row)->header; if (header && gtk_widget_child_focus (header, direction)) return TRUE; } if (gtk_widget_child_focus (GTK_WIDGET (next_focus_row), direction)) return TRUE; return FALSE; } static void list_box_add_visible_rows (GtkListBox *box, int n) { int was_zero; was_zero = box->n_visible_rows == 0; box->n_visible_rows += n; if (box->placeholder && (was_zero || box->n_visible_rows == 0)) gtk_widget_set_child_visible (GTK_WIDGET (box->placeholder), box->n_visible_rows == 0); } /* Children are visible if they are shown by the app (visible) * and not filtered out (child_visible) by the listbox */ static void update_row_is_visible (GtkListBox *box, GtkListBoxRow *row) { GtkListBoxRowPrivate *row_priv = ROW_PRIV (row); gboolean was_visible; was_visible = row_priv->visible; row_priv->visible = gtk_widget_get_visible (GTK_WIDGET (row)) && gtk_widget_get_child_visible (GTK_WIDGET (row)); if (was_visible && !row_priv->visible) list_box_add_visible_rows (box, -1); if (!was_visible && row_priv->visible) list_box_add_visible_rows (box, 1); } static void gtk_list_box_apply_filter (GtkListBox *box, GtkListBoxRow *row) { gboolean do_show; do_show = TRUE; if (box->filter_func != NULL) do_show = box->filter_func (row, box->filter_func_target); gtk_widget_set_child_visible (GTK_WIDGET (row), do_show); update_row_is_visible (box, row); } static void gtk_list_box_apply_filter_all (GtkListBox *box) { GtkListBoxRow *row; GSequenceIter *iter; for (iter = g_sequence_get_begin_iter (box->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { row = g_sequence_get (iter); gtk_list_box_apply_filter (box, row); } } static GtkListBoxRow * gtk_list_box_get_first_focusable (GtkListBox *box) { GtkListBoxRow *row; GSequenceIter *iter; for (iter = g_sequence_get_begin_iter (box->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { row = g_sequence_get (iter); if (row_is_visible (row) && gtk_widget_is_sensitive (GTK_WIDGET (row))) return row; } return NULL; } static GtkListBoxRow * gtk_list_box_get_last_focusable (GtkListBox *box) { GtkListBoxRow *row; GSequenceIter *iter; iter = g_sequence_get_end_iter (box->children); while (!g_sequence_iter_is_begin (iter)) { iter = g_sequence_iter_prev (iter); row = g_sequence_get (iter); if (row_is_visible (row) && gtk_widget_is_sensitive (GTK_WIDGET (row))) return row; } return NULL; } static GSequenceIter * gtk_list_box_get_previous_visible (GtkListBox *box, GSequenceIter *iter) { GtkListBoxRow *row; if (g_sequence_iter_is_begin (iter)) return NULL; do { iter = g_sequence_iter_prev (iter); row = g_sequence_get (iter); if (row_is_visible (row)) return iter; } while (!g_sequence_iter_is_begin (iter)); return NULL; } static GSequenceIter * gtk_list_box_get_next_visible (GtkListBox *box, GSequenceIter *iter) { GtkListBoxRow *row; if (g_sequence_iter_is_end (iter)) return iter; do { iter = g_sequence_iter_next (iter); if (!g_sequence_iter_is_end (iter)) { row = g_sequence_get (iter); if (row_is_visible (row)) return iter; } } while (!g_sequence_iter_is_end (iter)); return iter; } static GSequenceIter * gtk_list_box_get_last_visible (GtkListBox *box, GSequenceIter *iter) { GSequenceIter *next = NULL; if (g_sequence_iter_is_end (iter)) return NULL; do { next = gtk_list_box_get_next_visible (box, iter); if (!g_sequence_iter_is_end (next)) iter = next; } while (!g_sequence_iter_is_end (next)); return iter; } static void gtk_list_box_update_header (GtkListBox *box, GSequenceIter *iter) { GtkListBoxRow *row; GSequenceIter *before_iter; GtkListBoxRow *before_row; GtkWidget *old_header, *new_header; if (iter == NULL || g_sequence_iter_is_end (iter)) return; row = g_sequence_get (iter); g_object_ref (row); before_iter = gtk_list_box_get_previous_visible (box, iter); before_row = NULL; if (before_iter != NULL) { before_row = g_sequence_get (before_iter); if (before_row) g_object_ref (before_row); } if (box->update_header_func != NULL && row_is_visible (row)) { old_header = ROW_PRIV (row)->header; if (old_header) g_object_ref (old_header); box->update_header_func (row, before_row, box->update_header_func_target); new_header = ROW_PRIV (row)->header; if (old_header != new_header) { if (old_header != NULL && g_hash_table_lookup (box->header_hash, old_header) == row) { /* Only unparent the @old_header if it hasn’t been re-used as the * header for a different row. */ gtk_widget_unparent (old_header); g_hash_table_remove (box->header_hash, old_header); } if (new_header != NULL) { g_hash_table_insert (box->header_hash, new_header, row); gtk_widget_unparent (new_header); gtk_widget_set_parent (new_header, GTK_WIDGET (box)); gtk_widget_show (new_header); } gtk_widget_queue_resize (GTK_WIDGET (box)); } if (old_header) g_object_unref (old_header); } else { if (ROW_PRIV (row)->header != NULL) { g_hash_table_remove (box->header_hash, ROW_PRIV (row)->header); gtk_widget_unparent (ROW_PRIV (row)->header); gtk_list_box_row_set_header (row, NULL); gtk_widget_queue_resize (GTK_WIDGET (box)); } } if (before_row) g_object_unref (before_row); g_object_unref (row); } static void gtk_list_box_row_visibility_changed (GtkListBox *box, GtkListBoxRow *row) { update_row_is_visible (box, row); if (gtk_widget_get_visible (GTK_WIDGET (box))) { gtk_list_box_update_header (box, ROW_PRIV (row)->iter); gtk_list_box_update_header (box, gtk_list_box_get_next_visible (box, ROW_PRIV (row)->iter)); } } /** * gtk_list_box_remove: * @box: a `GtkListBox` * @child: the child to remove * * Removes a child from @box. */ void gtk_list_box_remove (GtkListBox *box, GtkWidget *child) { GtkWidget *widget; gboolean was_visible; gboolean was_selected; GtkListBoxRow *row; GSequenceIter *iter; GSequenceIter *next; g_return_if_fail (GTK_IS_LIST_BOX (box)); g_return_if_fail (GTK_IS_WIDGET (child)); widget = GTK_WIDGET (box); was_visible = gtk_widget_get_visible (child); if (child == box->placeholder) { gtk_widget_unparent (child); box->placeholder = NULL; if (was_visible && gtk_widget_get_visible (widget)) gtk_widget_queue_resize (widget); return; } if (!GTK_IS_LIST_BOX_ROW (child)) { row = g_hash_table_lookup (box->header_hash, child); if (row != NULL) { g_hash_table_remove (box->header_hash, child); g_clear_object (&ROW_PRIV (row)->header); gtk_widget_unparent (child); if (was_visible && gtk_widget_get_visible (widget)) gtk_widget_queue_resize (widget); } else { g_warning ("Tried to remove non-child %p", child); } return; } row = GTK_LIST_BOX_ROW (child); iter = ROW_PRIV (row)->iter; if (g_sequence_iter_get_sequence (iter) != box->children) { g_warning ("Tried to remove non-child %p", child); return; } was_selected = ROW_PRIV (row)->selected; if (ROW_PRIV (row)->visible) list_box_add_visible_rows (box, -1); if (ROW_PRIV (row)->header != NULL) { g_hash_table_remove (box->header_hash, ROW_PRIV (row)->header); gtk_widget_unparent (ROW_PRIV (row)->header); g_clear_object (&ROW_PRIV (row)->header); } if (row == box->selected_row) box->selected_row = NULL; if (row == box->cursor_row) box->cursor_row = NULL; if (row == box->active_row) box->active_row = NULL; if (row == box->drag_highlighted_row) gtk_list_box_drag_unhighlight_row (box); next = gtk_list_box_get_next_visible (box, iter); gtk_widget_unparent (child); g_sequence_remove (iter); /* After unparenting, those values are garbage */ iter = NULL; row = NULL; child = NULL; if (gtk_widget_get_visible (widget)) gtk_list_box_update_header (box, next); if (was_visible && gtk_widget_get_visible (GTK_WIDGET (box))) gtk_widget_queue_resize (widget); if (was_selected && !gtk_widget_in_destruction (widget)) { g_signal_emit (box, signals[ROW_SELECTED], 0, NULL); g_signal_emit (box, signals[SELECTED_ROWS_CHANGED], 0); } } static void gtk_list_box_compute_expand (GtkWidget *widget, gboolean *hexpand_p, gboolean *vexpand_p) { GtkWidget *w; gboolean hexpand = FALSE; gboolean vexpand = FALSE; for (w = gtk_widget_get_first_child (widget); w != NULL; w = gtk_widget_get_next_sibling (w)) { hexpand = hexpand || gtk_widget_compute_expand (w, GTK_ORIENTATION_HORIZONTAL); vexpand = vexpand || gtk_widget_compute_expand (w, GTK_ORIENTATION_VERTICAL); } *hexpand_p = hexpand; *vexpand_p = vexpand; /* We don't expand vertically beyond the minimum size */ if (*vexpand_p) *vexpand_p = FALSE; } static GtkSizeRequestMode gtk_list_box_get_request_mode (GtkWidget *widget) { return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; } static void gtk_list_box_measure (GtkWidget *widget, GtkOrientation orientation, int for_size, int *minimum, int *natural, int *minimum_baseline, int *natural_baseline) { GtkListBox *box = GTK_LIST_BOX (widget); GSequenceIter *iter; if (orientation == GTK_ORIENTATION_HORIZONTAL) { *minimum = 0; *natural = 0; if (box->placeholder && gtk_widget_get_child_visible (box->placeholder)) gtk_widget_measure (box->placeholder, GTK_ORIENTATION_HORIZONTAL, -1, minimum, natural, NULL, NULL); for (iter = g_sequence_get_begin_iter (box->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { GtkListBoxRow *row; int row_min; int row_nat; row = g_sequence_get (iter); /* We *do* take visible but filtered rows into account here so that * the list width doesn't change during filtering */ if (!gtk_widget_get_visible (GTK_WIDGET (row))) continue; gtk_widget_measure (GTK_WIDGET (row), orientation, -1, &row_min, &row_nat, NULL, NULL); *minimum = MAX (*minimum, row_min); *natural = MAX (*natural, row_nat); if (ROW_PRIV (row)->header != NULL) { gtk_widget_measure (ROW_PRIV (row)->header, orientation, -1, &row_min, &row_nat, NULL, NULL); *minimum = MAX (*minimum, row_min); *natural = MAX (*natural, row_nat); } } } else { if (for_size < 0) { int f; gtk_list_box_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1, &f, &for_size, NULL, NULL); } *minimum = 0; if (box->placeholder && gtk_widget_get_child_visible (box->placeholder)) gtk_widget_measure (box->placeholder, orientation, for_size, minimum, NULL, NULL, NULL); for (iter = g_sequence_get_begin_iter (box->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { GtkListBoxRow *row; int row_min = 0; row = g_sequence_get (iter); if (!row_is_visible (row)) continue; if (ROW_PRIV (row)->header != NULL) { gtk_widget_measure (ROW_PRIV (row)->header, orientation, for_size, &row_min, NULL, NULL, NULL); *minimum += row_min; } gtk_widget_measure (GTK_WIDGET (row), orientation, for_size, &row_min, NULL, NULL, NULL); *minimum += row_min; } /* We always allocate the minimum height, since handling expanding rows * is way too costly, and unlikely to be used, as lists are generally put * inside a scrolling window anyway. */ *natural = *minimum; } } static void gtk_list_box_size_allocate (GtkWidget *widget, int width, int height, int baseline) { GtkListBox *box = GTK_LIST_BOX (widget); GtkAllocation child_allocation; GtkAllocation header_allocation; GtkListBoxRow *row; GSequenceIter *iter; int child_min; child_allocation.x = 0; child_allocation.y = 0; child_allocation.width = width; child_allocation.height = 0; header_allocation.x = 0; header_allocation.y = 0; header_allocation.width = width; header_allocation.height = 0; if (box->placeholder && gtk_widget_get_child_visible (box->placeholder)) { gtk_widget_measure (box->placeholder, GTK_ORIENTATION_VERTICAL, width, &child_min, NULL, NULL, NULL); header_allocation.height = height; header_allocation.y = child_allocation.y; gtk_widget_size_allocate (box->placeholder, &header_allocation, -1); child_allocation.y += child_min; } for (iter = g_sequence_get_begin_iter (box->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { row = g_sequence_get (iter); if (!row_is_visible (row)) { ROW_PRIV (row)->y = child_allocation.y; ROW_PRIV (row)->height = 0; continue; } if (ROW_PRIV (row)->header != NULL) { gtk_widget_measure (ROW_PRIV (row)->header, GTK_ORIENTATION_VERTICAL, width, &child_min, NULL, NULL, NULL); header_allocation.height = child_min; header_allocation.y = child_allocation.y; gtk_widget_size_allocate (ROW_PRIV (row)->header, &header_allocation, -1); child_allocation.y += child_min; } ROW_PRIV (row)->y = child_allocation.y; gtk_widget_measure (GTK_WIDGET (row), GTK_ORIENTATION_VERTICAL, child_allocation.width, &child_min, NULL, NULL, NULL); child_allocation.height = child_min; ROW_PRIV (row)->height = child_allocation.height; gtk_widget_size_allocate (GTK_WIDGET (row), &child_allocation, -1); child_allocation.y += child_min; } } /** * gtk_list_box_prepend: * @box: a `GtkListBox` * @child: the `GtkWidget` to add * * Prepend a widget to the list. * * If a sort function is set, the widget will * actually be inserted at the calculated position. */ void gtk_list_box_prepend (GtkListBox *box, GtkWidget *child) { gtk_list_box_insert (box, child, 0); } /** * gtk_list_box_append: * @box: a `GtkListBox` * @child: the `GtkWidget` to add * * Append a widget to the list. * * If a sort function is set, the widget will * actually be inserted at the calculated position. */ void gtk_list_box_append (GtkListBox *box, GtkWidget *child) { gtk_list_box_insert (box, child, -1); } /** * gtk_list_box_insert: * @box: a `GtkListBox` * @child: the `GtkWidget` to add * @position: the position to insert @child in * * Insert the @child into the @box at @position. * * If a sort function is * set, the widget will actually be inserted at the calculated position. * * If @position is -1, or larger than the total number of items in the * @box, then the @child will be appended to the end. */ void gtk_list_box_insert (GtkListBox *box, GtkWidget *child, int position) { GtkListBoxRow *row; GSequenceIter *prev = NULL; GSequenceIter *iter = NULL; g_return_if_fail (GTK_IS_LIST_BOX (box)); g_return_if_fail (GTK_IS_WIDGET (child)); if (GTK_IS_LIST_BOX_ROW (child)) row = GTK_LIST_BOX_ROW (child); else { row = GTK_LIST_BOX_ROW (gtk_list_box_row_new ()); gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), child); } if (box->sort_func != NULL) iter = g_sequence_insert_sorted (box->children, row, (GCompareDataFunc)do_sort, box); else if (position == 0) iter = g_sequence_prepend (box->children, row); else if (position == -1) iter = g_sequence_append (box->children, row); else { GSequenceIter *current_iter; current_iter = g_sequence_get_iter_at_pos (box->children, position); iter = g_sequence_insert_before (current_iter, row); } ROW_PRIV (row)->iter = iter; prev = g_sequence_iter_prev (iter); gtk_widget_insert_after (GTK_WIDGET (row), GTK_WIDGET (box), prev != iter ? g_sequence_get (prev) : NULL); gtk_widget_set_child_visible (GTK_WIDGET (row), TRUE); ROW_PRIV (row)->visible = gtk_widget_get_visible (GTK_WIDGET (row)); if (ROW_PRIV (row)->visible) list_box_add_visible_rows (box, 1); gtk_list_box_apply_filter (box, row); gtk_list_box_update_row_style (box, row); if (gtk_widget_get_visible (GTK_WIDGET (box))) { gtk_list_box_update_header (box, ROW_PRIV (row)->iter); gtk_list_box_update_header (box, gtk_list_box_get_next_visible (box, ROW_PRIV (row)->iter)); } } /** * gtk_list_box_drag_unhighlight_row: * @box: a `GtkListBox` * * If a row has previously been highlighted via gtk_list_box_drag_highlight_row(), * it will have the highlight removed. */ void gtk_list_box_drag_unhighlight_row (GtkListBox *box) { g_return_if_fail (GTK_IS_LIST_BOX (box)); if (box->drag_highlighted_row == NULL) return; gtk_widget_unset_state_flags (GTK_WIDGET (box->drag_highlighted_row), GTK_STATE_FLAG_DROP_ACTIVE); g_clear_object (&box->drag_highlighted_row); } /** * gtk_list_box_drag_highlight_row: * @box: a `GtkListBox` * @row: a `GtkListBoxRow` * * Add a drag highlight to a row. * * This is a helper function for implementing DnD onto a `GtkListBox`. * The passed in @row will be highlighted by setting the * %GTK_STATE_FLAG_DROP_ACTIVE state and any previously highlighted * row will be unhighlighted. * * The row will also be unhighlighted when the widget gets * a drag leave event. */ void gtk_list_box_drag_highlight_row (GtkListBox *box, GtkListBoxRow *row) { g_return_if_fail (GTK_IS_LIST_BOX (box)); g_return_if_fail (GTK_IS_LIST_BOX_ROW (row)); if (box->drag_highlighted_row == row) return; gtk_list_box_drag_unhighlight_row (box); gtk_widget_set_state_flags (GTK_WIDGET (row), GTK_STATE_FLAG_DROP_ACTIVE, FALSE); box->drag_highlighted_row = g_object_ref (row); } static void gtk_list_box_activate_cursor_row (GtkListBox *box) { gtk_list_box_select_and_activate (box, box->cursor_row); } static void gtk_list_box_toggle_cursor_row (GtkListBox *box) { if (box->cursor_row == NULL) return; if ((box->selection_mode == GTK_SELECTION_SINGLE || box->selection_mode == GTK_SELECTION_MULTIPLE) && ROW_PRIV (box->cursor_row)->selected) gtk_list_box_unselect_row_internal (box, box->cursor_row); else gtk_list_box_select_and_activate (box, box->cursor_row); } static void gtk_list_box_move_cursor (GtkListBox *box, GtkMovementStep step, int count, gboolean extend, gboolean modify) { GtkListBoxRow *row; int page_size; GSequenceIter *iter; int start_y; int end_y; int height; row = NULL; switch ((guint) step) { case GTK_MOVEMENT_BUFFER_ENDS: if (count < 0) row = gtk_list_box_get_first_focusable (box); else row = gtk_list_box_get_last_focusable (box); break; case GTK_MOVEMENT_DISPLAY_LINES: if (box->cursor_row != NULL) { int i = count; iter = ROW_PRIV (box->cursor_row)->iter; while (i < 0 && iter != NULL) { iter = gtk_list_box_get_previous_visible (box, iter); i = i + 1; } while (i > 0 && iter != NULL) { iter = gtk_list_box_get_next_visible (box, iter); i = i - 1; } if (iter != NULL && !g_sequence_iter_is_end (iter)) row = g_sequence_get (iter); } break; case GTK_MOVEMENT_PAGES: page_size = 100; if (box->adjustment != NULL) page_size = gtk_adjustment_get_page_increment (box->adjustment); if (box->cursor_row != NULL) { start_y = ROW_PRIV (box->cursor_row)->y; height = gtk_widget_get_height (GTK_WIDGET (box)); end_y = CLAMP (start_y + page_size * count, 0, height - 1); row = gtk_list_box_get_row_at_y (box, end_y); if (!row) { GSequenceIter *cursor_iter; GSequenceIter *next_iter; if (count > 0) { cursor_iter = ROW_PRIV (box->cursor_row)->iter; next_iter = gtk_list_box_get_last_visible (box, cursor_iter); if (next_iter) { row = g_sequence_get (next_iter); end_y = ROW_PRIV (row)->y; } } else { row = gtk_list_box_get_row_at_index (box, 0); end_y = ROW_PRIV (row)->y; } } else if (row == box->cursor_row) { iter = ROW_PRIV (row)->iter; /* Move at least one row. This is important when the cursor_row's height is * greater than page_size */ if (count < 0) iter = g_sequence_iter_prev (iter); else iter = g_sequence_iter_next (iter); if (!g_sequence_iter_is_begin (iter) && !g_sequence_iter_is_end (iter)) { row = g_sequence_get (iter); end_y = ROW_PRIV (row)->y; } } if (end_y != start_y && box->adjustment != NULL) gtk_adjustment_animate_to_value (box->adjustment, end_y); } break; default: return; } if (row == NULL || row == box->cursor_row) { GtkDirectionType direction = count < 0 ? GTK_DIR_UP : GTK_DIR_DOWN; if (!gtk_widget_keynav_failed (GTK_WIDGET (box), direction)) { GtkWidget *toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (box))); if (toplevel) gtk_widget_child_focus (toplevel, direction == GTK_DIR_UP ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD); } return; } gtk_list_box_update_cursor (box, row, TRUE); if (!modify) gtk_list_box_update_selection (box, row, FALSE, extend); } /** * gtk_list_box_row_new: * * Creates a new `GtkListBoxRow`. * * Returns: a new `GtkListBoxRow` */ GtkWidget * gtk_list_box_row_new (void) { return g_object_new (GTK_TYPE_LIST_BOX_ROW, NULL); } /** * gtk_list_box_row_set_child: (attributes org.gtk.Method.set_property=child) * @row: a `GtkListBoxRow` * @child: (nullable): the child widget * * Sets the child widget of @self. */ void gtk_list_box_row_set_child (GtkListBoxRow *row, GtkWidget *child) { GtkListBoxRowPrivate *priv = ROW_PRIV (row); g_clear_pointer (&priv->child, gtk_widget_unparent); priv->child = child; if (child) gtk_widget_set_parent (child, GTK_WIDGET (row)); g_object_notify_by_pspec (G_OBJECT (row), row_properties[ROW_PROP_CHILD]); } /** * gtk_list_box_row_get_child: (attributes org.gtk.Method.get_property=child) * @row: a `GtkListBoxRow` * * Gets the child widget of @row. * * Returns: (nullable) (transfer none): the child widget of @row */ GtkWidget * gtk_list_box_row_get_child (GtkListBoxRow *row) { return ROW_PRIV (row)->child; } static void gtk_list_box_row_set_focus (GtkListBoxRow *row) { GtkListBox *box = gtk_list_box_row_get_box (row); if (!box) return; gtk_list_box_update_selection (box, row, FALSE, FALSE); } static gboolean gtk_list_box_row_focus (GtkWidget *widget, GtkDirectionType direction) { GtkListBoxRow *row = GTK_LIST_BOX_ROW (widget); gboolean had_focus = FALSE; GtkWidget *child = ROW_PRIV (row)->child; GtkWidget *focus_child = gtk_widget_get_focus_child (widget); g_object_get (widget, "has-focus", &had_focus, NULL); /* If a child has focus, always try to navigate within that first. */ if (focus_child != NULL) { if (gtk_widget_child_focus (focus_child, direction)) return TRUE; } /* Otherwise, decide based on the direction. */ if (direction == GTK_DIR_RIGHT || direction == GTK_DIR_TAB_FORWARD) { /* If a child was focused and focus couldn't be moved within that (see * above), let focus leave. */ if (focus_child != NULL) return FALSE; /* If the row is not focused, try to focus it. */ if (!had_focus && gtk_widget_grab_focus (widget)) { gtk_list_box_row_set_focus (row); return TRUE; } /* Finally, try to move focus into the child. */ if (child != NULL && gtk_widget_child_focus (child, direction)) return TRUE; return FALSE; } else if (direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD) { /* If the row itself is focused, let focus leave it. */ if (had_focus) return FALSE; /* Otherwise, let focus enter the child widget, if possible. */ if (child != NULL && gtk_widget_child_focus (child, direction)) return TRUE; /* If that didn't work, try to focus the row itself. */ if (gtk_widget_grab_focus (widget)) { gtk_list_box_row_set_focus (row); return TRUE; } return FALSE; } return FALSE; } static void gtk_list_box_row_activate (GtkListBoxRow *row) { GtkListBox *box; box = gtk_list_box_row_get_box (row); if (box) gtk_list_box_select_and_activate (box, row); } static void gtk_list_box_row_show (GtkWidget *widget) { GtkListBoxRow *row = GTK_LIST_BOX_ROW (widget); GtkListBox *box; GTK_WIDGET_CLASS (gtk_list_box_row_parent_class)->show (widget); box = gtk_list_box_row_get_box (row); if (box) gtk_list_box_row_visibility_changed (box, row); } static void gtk_list_box_row_hide (GtkWidget *widget) { GtkListBoxRow *row = GTK_LIST_BOX_ROW (widget); GtkListBox *box; GTK_WIDGET_CLASS (gtk_list_box_row_parent_class)->hide (widget); box = gtk_list_box_row_get_box (row); if (box) gtk_list_box_row_visibility_changed (box, row); } static void gtk_list_box_row_root (GtkWidget *widget) { GtkListBoxRow *row = GTK_LIST_BOX_ROW (widget); GTK_WIDGET_CLASS (gtk_list_box_row_parent_class)->root (widget); if (ROW_PRIV (row)->selectable) gtk_accessible_update_state (GTK_ACCESSIBLE (row), GTK_ACCESSIBLE_STATE_SELECTED, ROW_PRIV (row)->selected, -1); } /** * gtk_list_box_row_changed: * @row: a `GtkListBoxRow` * * Marks @row as changed, causing any state that depends on this * to be updated. * * This affects sorting, filtering and headers. * * Note that calls to this method must be in sync with the data * used for the row functions. For instance, if the list is * mirroring some external data set, and *two* rows changed in the * external data set then when you call gtk_list_box_row_changed() * on the first row the sort function must only read the new data * for the first of the two changed rows, otherwise the resorting * of the rows will be wrong. * * This generally means that if you don’t fully control the data * model you have to duplicate the data that affects the listbox * row functions into the row widgets themselves. Another alternative * is to call [method@Gtk.ListBox.invalidate_sort] on any model change, * but that is more expensive. */ void gtk_list_box_row_changed (GtkListBoxRow *row) { GtkListBox *box; g_return_if_fail (GTK_IS_LIST_BOX_ROW (row)); box = gtk_list_box_row_get_box (row); if (box) gtk_list_box_got_row_changed (box, row); } /** * gtk_list_box_row_get_header: * @row: a `GtkListBoxRow` * * Returns the current header of the @row. * * This can be used * in a [callback@Gtk.ListBoxUpdateHeaderFunc] to see if * there is a header set already, and if so to update * the state of it. * * Returns: (transfer none) (nullable): the current header */ GtkWidget * gtk_list_box_row_get_header (GtkListBoxRow *row) { g_return_val_if_fail (GTK_IS_LIST_BOX_ROW (row), NULL); return ROW_PRIV (row)->header; } /** * gtk_list_box_row_set_header: * @row: a `GtkListBoxRow` * @header: (nullable): the header * * Sets the current header of the @row. * * This is only allowed to be called * from a [callback@Gtk.ListBoxUpdateHeaderFunc]. * It will replace any existing header in the row, * and be shown in front of the row in the listbox. */ void gtk_list_box_row_set_header (GtkListBoxRow *row, GtkWidget *header) { GtkListBoxRowPrivate *priv = ROW_PRIV (row); g_return_if_fail (GTK_IS_LIST_BOX_ROW (row)); g_return_if_fail (header == NULL || GTK_IS_WIDGET (header)); if (priv->header) g_object_unref (priv->header); priv->header = header; if (header) g_object_ref_sink (header); } /** * gtk_list_box_row_get_index: * @row: a `GtkListBoxRow` * * Gets the current index of the @row in its `GtkListBox` container. * * Returns: the index of the @row, or -1 if the @row is not in a listbox */ int gtk_list_box_row_get_index (GtkListBoxRow *row) { GtkListBoxRowPrivate *priv = ROW_PRIV (row); g_return_val_if_fail (GTK_IS_LIST_BOX_ROW (row), -1); if (priv->iter != NULL) return g_sequence_iter_get_position (priv->iter); return -1; } /** * gtk_list_box_row_is_selected: * @row: a `GtkListBoxRow` * * Returns whether the child is currently selected in its * `GtkListBox` container. * * Returns: %TRUE if @row is selected */ gboolean gtk_list_box_row_is_selected (GtkListBoxRow *row) { g_return_val_if_fail (GTK_IS_LIST_BOX_ROW (row), FALSE); return ROW_PRIV (row)->selected; } static void gtk_list_box_update_row_style (GtkListBox *box, GtkListBoxRow *row) { gboolean can_select; if (box && box->selection_mode != GTK_SELECTION_NONE) can_select = TRUE; else can_select = FALSE; if (ROW_PRIV (row)->activatable || (ROW_PRIV (row)->selectable && can_select)) gtk_widget_add_css_class (GTK_WIDGET (row), "activatable"); else gtk_widget_remove_css_class (GTK_WIDGET (row), "activatable"); } static void gtk_list_box_update_row_styles (GtkListBox *box) { GSequenceIter *iter; GtkListBoxRow *row; for (iter = g_sequence_get_begin_iter (box->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { row = g_sequence_get (iter); gtk_list_box_update_row_style (box, row); } } /** * gtk_list_box_row_set_activatable: (attributes org.gtk.Method.set_property=activatable) * @row: a `GtkListBoxRow` * @activatable: %TRUE to mark the row as activatable * * Set whether the row is activatable. */ void gtk_list_box_row_set_activatable (GtkListBoxRow *row, gboolean activatable) { g_return_if_fail (GTK_IS_LIST_BOX_ROW (row)); activatable = activatable != FALSE; if (ROW_PRIV (row)->activatable != activatable) { ROW_PRIV (row)->activatable = activatable; gtk_list_box_update_row_style (gtk_list_box_row_get_box (row), row); g_object_notify_by_pspec (G_OBJECT (row), row_properties[ROW_PROP_ACTIVATABLE]); } } /** * gtk_list_box_row_get_activatable: (attributes org.gtk.Method.get_property=activatable) * @row: a `GtkListBoxRow` * * Gets whether the row is activatable. * * Returns: %TRUE if the row is activatable */ gboolean gtk_list_box_row_get_activatable (GtkListBoxRow *row) { g_return_val_if_fail (GTK_IS_LIST_BOX_ROW (row), TRUE); return ROW_PRIV (row)->activatable; } /** * gtk_list_box_row_set_selectable: (attributes org.gtk.Method.set_property=selectable) * @row: a `GtkListBoxRow` * @selectable: %TRUE to mark the row as selectable * * Set whether the row can be selected. */ void gtk_list_box_row_set_selectable (GtkListBoxRow *row, gboolean selectable) { g_return_if_fail (GTK_IS_LIST_BOX_ROW (row)); selectable = selectable != FALSE; if (ROW_PRIV (row)->selectable != selectable) { if (!selectable) gtk_list_box_row_set_selected (row, FALSE); ROW_PRIV (row)->selectable = selectable; if (selectable) gtk_accessible_update_state (GTK_ACCESSIBLE (row), GTK_ACCESSIBLE_STATE_SELECTED, FALSE, -1); else gtk_accessible_reset_state (GTK_ACCESSIBLE (row), GTK_ACCESSIBLE_STATE_SELECTED); gtk_list_box_update_row_style (gtk_list_box_row_get_box (row), row); g_object_notify_by_pspec (G_OBJECT (row), row_properties[ROW_PROP_SELECTABLE]); } } /** * gtk_list_box_row_get_selectable: (attributes org.gtk.Method.get_property=selectable) * @row: a `GtkListBoxRow` * * Gets whether the row can be selected. * * Returns: %TRUE if the row is selectable */ gboolean gtk_list_box_row_get_selectable (GtkListBoxRow *row) { g_return_val_if_fail (GTK_IS_LIST_BOX_ROW (row), TRUE); return ROW_PRIV (row)->selectable; } static void gtk_list_box_row_set_action_name (GtkActionable *actionable, const char *action_name) { GtkListBoxRow *row = GTK_LIST_BOX_ROW (actionable); GtkListBoxRowPrivate *priv = ROW_PRIV (row); if (!priv->action_helper) priv->action_helper = gtk_action_helper_new (actionable); gtk_action_helper_set_action_name (priv->action_helper, action_name); } static void gtk_list_box_row_set_action_target_value (GtkActionable *actionable, GVariant *action_target) { GtkListBoxRow *row = GTK_LIST_BOX_ROW (actionable); GtkListBoxRowPrivate *priv = ROW_PRIV (row); if (!priv->action_helper) priv->action_helper = gtk_action_helper_new (actionable); gtk_action_helper_set_action_target_value (priv->action_helper, action_target); } static void gtk_list_box_row_get_property (GObject *obj, guint property_id, GValue *value, GParamSpec *pspec) { GtkListBoxRow *row = GTK_LIST_BOX_ROW (obj); switch (property_id) { case ROW_PROP_ACTIVATABLE: g_value_set_boolean (value, gtk_list_box_row_get_activatable (row)); break; case ROW_PROP_SELECTABLE: g_value_set_boolean (value, gtk_list_box_row_get_selectable (row)); break; case ROW_PROP_ACTION_NAME: g_value_set_string (value, gtk_action_helper_get_action_name (ROW_PRIV (row)->action_helper)); break; case ROW_PROP_ACTION_TARGET: g_value_set_variant (value, gtk_action_helper_get_action_target_value (ROW_PRIV (row)->action_helper)); break; case ROW_PROP_CHILD: g_value_set_object (value, gtk_list_box_row_get_child (row)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec); break; } } static void gtk_list_box_row_set_property (GObject *obj, guint property_id, const GValue *value, GParamSpec *pspec) { GtkListBoxRow *row = GTK_LIST_BOX_ROW (obj); switch (property_id) { case ROW_PROP_ACTIVATABLE: gtk_list_box_row_set_activatable (row, g_value_get_boolean (value)); break; case ROW_PROP_SELECTABLE: gtk_list_box_row_set_selectable (row, g_value_get_boolean (value)); break; case ROW_PROP_ACTION_NAME: gtk_list_box_row_set_action_name (GTK_ACTIONABLE (row), g_value_get_string (value)); break; case ROW_PROP_ACTION_TARGET: gtk_list_box_row_set_action_target_value (GTK_ACTIONABLE (row), g_value_get_variant (value)); break; case ROW_PROP_CHILD: gtk_list_box_row_set_child (row, g_value_get_object (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec); break; } } static const char * gtk_list_box_row_get_action_name (GtkActionable *actionable) { GtkListBoxRow *row = GTK_LIST_BOX_ROW (actionable); return gtk_action_helper_get_action_name (ROW_PRIV (row)->action_helper); } static GVariant * gtk_list_box_row_get_action_target_value (GtkActionable *actionable) { GtkListBoxRow *row = GTK_LIST_BOX_ROW (actionable); return gtk_action_helper_get_action_target_value (ROW_PRIV (row)->action_helper); } static void gtk_list_box_row_actionable_iface_init (GtkActionableInterface *iface) { iface->get_action_name = gtk_list_box_row_get_action_name; iface->set_action_name = gtk_list_box_row_set_action_name; iface->get_action_target_value = gtk_list_box_row_get_action_target_value; iface->set_action_target_value = gtk_list_box_row_set_action_target_value; } static void gtk_list_box_row_finalize (GObject *obj) { g_clear_object (&ROW_PRIV (GTK_LIST_BOX_ROW (obj))->header); G_OBJECT_CLASS (gtk_list_box_row_parent_class)->finalize (obj); } static void gtk_list_box_row_dispose (GObject *object) { GtkListBoxRow *row = GTK_LIST_BOX_ROW (object); GtkListBoxRowPrivate *priv = ROW_PRIV (row); g_clear_object (&priv->action_helper); g_clear_pointer (&priv->child, gtk_widget_unparent); G_OBJECT_CLASS (gtk_list_box_row_parent_class)->dispose (object); } static gboolean gtk_list_box_row_grab_focus (GtkWidget *widget) { GtkListBoxRow *row = GTK_LIST_BOX_ROW (widget); GtkListBox *box = gtk_list_box_row_get_box (row); g_return_val_if_fail (box != NULL, FALSE); if (gtk_widget_grab_focus_self (widget)) { if (box->cursor_row != row) gtk_list_box_update_cursor (box, row, FALSE); return TRUE; } return FALSE; } static void gtk_list_box_row_class_init (GtkListBoxRowClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->get_property = gtk_list_box_row_get_property; object_class->set_property = gtk_list_box_row_set_property; object_class->finalize = gtk_list_box_row_finalize; object_class->dispose = gtk_list_box_row_dispose; widget_class->root = gtk_list_box_row_root; widget_class->show = gtk_list_box_row_show; widget_class->hide = gtk_list_box_row_hide; widget_class->focus = gtk_list_box_row_focus; widget_class->grab_focus = gtk_list_box_row_grab_focus; klass->activate = gtk_list_box_row_activate; /** * GtkListBoxRow::activate: * * This is a keybinding signal, which will cause this row to be activated. * * If you want to be notified when the user activates a row (by key or not), * use the [signal@Gtk.ListBox::row-activated] signal on the row’s parent * `GtkListBox`. */ row_signals[ROW__ACTIVATE] = g_signal_new (I_("activate"), G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GtkListBoxRowClass, activate), NULL, NULL, NULL, G_TYPE_NONE, 0); gtk_widget_class_set_activate_signal (widget_class, row_signals[ROW__ACTIVATE]); /** * GtkListBoxRow:activatable: (attributes org.gtk.Property.get=gtk_list_box_row_get_activatable org.gtk.Property.set=gtk_list_box_row_set_activatable) * * Determines whether the ::row-activated * signal will be emitted for this row. */ row_properties[ROW_PROP_ACTIVATABLE] = g_param_spec_boolean ("activatable", P_("Activatable"), P_("Whether this row can be activated"), TRUE, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); /** * GtkListBoxRow:selectable: (attributes org.gtk.Property.get=gtk_list_box_row_get_selectable org.gtk.Property.set=gtk_list_box_row_set_selectable) * * Determines whether this row can be selected. */ row_properties[ROW_PROP_SELECTABLE] = g_param_spec_boolean ("selectable", P_("Selectable"), P_("Whether this row can be selected"), TRUE, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); /** * GtkListBoxRow:child: (attributes org.gtk.Proeprty.get=gtk_list_box_row_get_child org.gtk.Property.set=gtk_list_box_row_set_child) * * The child widget. */ row_properties[ROW_PROP_CHILD] = g_param_spec_object ("child", P_("Child"), P_("The child widget"), GTK_TYPE_WIDGET, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); g_object_class_install_properties (object_class, LAST_ROW_PROPERTY, row_properties); g_object_class_override_property (object_class, ROW_PROP_ACTION_NAME, "action-name"); g_object_class_override_property (object_class, ROW_PROP_ACTION_TARGET, "action-target"); gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT); gtk_widget_class_set_css_name (widget_class, I_("row")); gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_LIST_ITEM); } static void gtk_list_box_row_init (GtkListBoxRow *row) { ROW_PRIV (row)->activatable = TRUE; ROW_PRIV (row)->selectable = TRUE; gtk_widget_set_focusable (GTK_WIDGET (row), TRUE); gtk_widget_add_css_class (GTK_WIDGET (row), "activatable"); } static void gtk_list_box_buildable_add_child (GtkBuildable *buildable, GtkBuilder *builder, GObject *child, const char *type) { if (type && strcmp (type, "placeholder") == 0) gtk_list_box_set_placeholder (GTK_LIST_BOX (buildable), GTK_WIDGET (child)); else if (GTK_IS_WIDGET (child)) gtk_list_box_insert (GTK_LIST_BOX (buildable), GTK_WIDGET (child), -1); else parent_buildable_iface->add_child (buildable, builder, child, type); } static void gtk_list_box_buildable_interface_init (GtkBuildableIface *iface) { parent_buildable_iface = g_type_interface_peek_parent (iface); iface->add_child = gtk_list_box_buildable_add_child; } static void gtk_list_box_bound_model_changed (GListModel *list, guint position, guint removed, guint added, gpointer user_data) { GtkListBox *box = user_data; guint i; while (removed--) { GtkListBoxRow *row; row = gtk_list_box_get_row_at_index (box, position); gtk_list_box_remove (box, GTK_WIDGET (row)); } for (i = 0; i < added; i++) { GObject *item; GtkWidget *widget; item = g_list_model_get_item (list, position + i); widget = box->create_widget_func (item, box->create_widget_func_data); /* We allow the create_widget_func to either return a full * reference or a floating reference. If we got the floating * reference, then turn it into a full reference now. That means * that gtk_list_box_insert() will take another full reference. * Finally, we'll release this full reference below, leaving only * the one held by the box. */ if (g_object_is_floating (widget)) g_object_ref_sink (widget); gtk_widget_show (widget); gtk_list_box_insert (box, widget, position + i); g_object_unref (widget); g_object_unref (item); } } static void gtk_list_box_check_model_compat (GtkListBox *box) { if (box->bound_model && (box->sort_func || box->filter_func)) g_warning ("GtkListBox with a model will ignore sort and filter functions"); } /** * gtk_list_box_bind_model: * @box: a `GtkListBox` * @model: (nullable): the `GListModel` to be bound to @box * @create_widget_func: (nullable): a function that creates widgets for items * or %NULL in case you also passed %NULL as @model * @user_data: (closure): user data passed to @create_widget_func * @user_data_free_func: function for freeing @user_data * * Binds @model to @box. * * If @box was already bound to a model, that previous binding is * destroyed. * * The contents of @box are cleared and then filled with widgets that * represent items from @model. @box is updated whenever @model changes. * If @model is %NULL, @box is left empty. * * It is undefined to add or remove widgets directly (for example, with * [method@Gtk.ListBox.insert]) while @box is bound to a model. * * Note that using a model is incompatible with the filtering and sorting * functionality in `GtkListBox`. When using a model, filtering and sorting * should be implemented by the model. */ void gtk_list_box_bind_model (GtkListBox *box, GListModel *model, GtkListBoxCreateWidgetFunc create_widget_func, gpointer user_data, GDestroyNotify user_data_free_func) { GSequenceIter *iter; g_return_if_fail (GTK_IS_LIST_BOX (box)); g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model)); g_return_if_fail (model == NULL || create_widget_func != NULL); if (box->bound_model) { if (box->create_widget_func_data_destroy) box->create_widget_func_data_destroy (box->create_widget_func_data); g_signal_handlers_disconnect_by_func (box->bound_model, gtk_list_box_bound_model_changed, box); g_clear_object (&box->bound_model); } iter = g_sequence_get_begin_iter (box->children); while (!g_sequence_iter_is_end (iter)) { GtkWidget *row = g_sequence_get (iter); iter = g_sequence_iter_next (iter); gtk_list_box_remove (box, row); } if (model == NULL) return; box->bound_model = g_object_ref (model); box->create_widget_func = create_widget_func; box->create_widget_func_data = user_data; box->create_widget_func_data_destroy = user_data_free_func; gtk_list_box_check_model_compat (box); g_signal_connect (box->bound_model, "items-changed", G_CALLBACK (gtk_list_box_bound_model_changed), box); gtk_list_box_bound_model_changed (model, 0, 0, g_list_model_get_n_items (model), box); } /** * gtk_list_box_set_show_separators: (attributes org.gtk.Method.set_property=show-separators) * @box: a `GtkListBox` * @show_separators: %TRUE to show separators * * Sets whether the list box should show separators * between rows. */ void gtk_list_box_set_show_separators (GtkListBox *box, gboolean show_separators) { g_return_if_fail (GTK_IS_LIST_BOX (box)); if (box->show_separators == show_separators) return; box->show_separators = show_separators; if (show_separators) gtk_widget_add_css_class (GTK_WIDGET (box), "separators"); else gtk_widget_remove_css_class (GTK_WIDGET (box), "separators"); g_object_notify_by_pspec (G_OBJECT (box), properties[PROP_SHOW_SEPARATORS]); } /** * gtk_list_box_get_show_separators: (attributes org.gtk.Method.get_property=show-separators) * @box: a `GtkListBox` * * Returns whether the list box should show separators * between rows. * * Returns: %TRUE if the list box shows separators */ gboolean gtk_list_box_get_show_separators (GtkListBox *box) { g_return_val_if_fail (GTK_IS_LIST_BOX (box), FALSE); return box->show_separators; }