/* * Copyright © 2019 Benjamin Otte * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: Benjamin Otte */ #include "config.h" #include "gtkgridview.h" #include "gtkbitset.h" #include "gtklistbaseprivate.h" #include "gtklistitemfactory.h" #include "gtklistitemmanagerprivate.h" #include "gtkmain.h" #include "gtkprivate.h" #include "gtksingleselection.h" #include "gtkwidgetprivate.h" #include "gtkmultiselection.h" /* Maximum number of list items created by the gridview. * For debugging, you can set this to G_MAXUINT to ensure * there's always a list item for every row. * * We multiply this number with GtkGridView:max-columns so * that we can always display at least this many rows. */ #define GTK_GRID_VIEW_MAX_VISIBLE_ROWS (30) #define DEFAULT_MAX_COLUMNS (7) /** * GtkGridView: * * `GtkGridView` presents a large dynamic grid of items. * * `GtkGridView` uses its factory to generate one child widget for each * visible item and shows them in a grid. The orientation of the grid view * determines if the grid reflows vertically or horizontally. * * `GtkGridView` allows the user to select items according to the selection * characteristics of the model. For models that allow multiple selected items, * it is possible to turn on _rubberband selection_, using * [property@Gtk.GridView:enable-rubberband]. * * To learn more about the list widget framework, see the * [overview](section-list-widget.html). * * # CSS nodes * * ``` * gridview * ├── child[.activatable] * │ * ├── child[.activatable] * │ * ┊ * ╰── [rubberband] * ``` * * `GtkGridView` uses a single CSS node with name `gridview`. Each child uses * a single CSS node with name `child`. If the [property@Gtk.ListItem:activatable] * property is set, the corresponding row will have the `.activatable` style * class. For rubberband selection, a subnode with name `rubberband` is used. * * # Accessibility * * `GtkGridView` uses the %GTK_ACCESSIBLE_ROLE_GRID role, and the items * use the %GTK_ACCESSIBLE_ROLE_GRID_CELL role. */ struct _GtkGridView { GtkListBase parent_instance; GtkListItemManager *item_manager; guint min_columns; guint max_columns; /* set in size_allocate */ guint n_columns; int unknown_row_height; double column_width; }; struct _GtkGridViewClass { GtkListBaseClass parent_class; }; enum { PROP_0, PROP_FACTORY, PROP_MAX_COLUMNS, PROP_MIN_COLUMNS, PROP_MODEL, PROP_SINGLE_CLICK_ACTIVATE, PROP_ENABLE_RUBBERBAND, N_PROPS }; enum { ACTIVATE, LAST_SIGNAL }; G_DEFINE_TYPE (GtkGridView, gtk_grid_view, GTK_TYPE_LIST_BASE) static GParamSpec *properties[N_PROPS] = { NULL, }; static guint signals[LAST_SIGNAL] = { 0 }; static void G_GNUC_UNUSED dump (GtkGridView *self) { GtkListTile *tile; guint n_widgets, n_list_rows, n_items; n_widgets = 0; n_list_rows = 0; n_items = 0; //g_print ("ANCHOR: %u - %u\n", self->anchor_start, self->anchor_end); for (tile = gtk_list_item_manager_get_first (self->item_manager); tile; tile = gtk_rb_tree_node_get_next (tile)) { if (tile->widget) n_widgets++; n_list_rows++; n_items += tile->n_items; g_print ("%6u%6u %5ux%3u %s (%d,%d,%d,%d)\n", tile->n_items, n_items, n_items / (self->n_columns ? self->n_columns : self->min_columns), n_items % (self->n_columns ? self->n_columns : self->min_columns), tile->widget ? " (widget)" : "", tile->area.x, tile->area.y, tile->area.width, tile->area.height); } g_print (" => %u widgets in %u list rows\n", n_widgets, n_list_rows); } static gboolean gtk_grid_view_get_allocation_along (GtkListBase *base, guint pos, int *offset, int *size) { GtkGridView *self = GTK_GRID_VIEW (base); GtkListTile *tile; guint remaining; tile = gtk_list_item_manager_get_nth (self->item_manager, pos, &remaining); if (tile->n_items <= self->n_columns) { *offset = tile->area.y; *size = tile->area.height; } else { guint rows_in_tile = tile->n_items / self->n_columns; guint row_height = tile->area.height / rows_in_tile; guint row_index = remaining / self->n_columns; *offset = tile->area.y + row_index * row_height; *size = row_height; } return TRUE; } static gboolean gtk_grid_view_get_allocation_across (GtkListBase *base, guint pos, int *offset, int *size) { GtkGridView *self = GTK_GRID_VIEW (base); guint start; pos %= self->n_columns; start = ceil (self->column_width * pos); if (offset) *offset = start; if (size) *size = ceil (self->column_width * (pos + 1)) - start; return TRUE; } static gboolean gtk_grid_view_get_position_from_allocation (GtkListBase *base, int x, int y, guint *position, cairo_rectangle_int_t *area) { GtkGridView *self = GTK_GRID_VIEW (base); GtkListTile *tile; guint pos; GdkRectangle bounds; gtk_list_item_manager_get_tile_bounds (self->item_manager, &bounds); if (bounds.width <= 0 || bounds.height <= 0) return FALSE; x = CLAMP (x, bounds.x, bounds.x + bounds.width - 1); y = CLAMP (y, bounds.y, bounds.y + bounds.height - 1); tile = gtk_list_item_manager_get_tile_at (self->item_manager, x, y); if (tile == NULL) return FALSE; while (tile && tile->n_items == 0) tile = gtk_rb_tree_node_get_previous (tile); if (tile == NULL) { tile = gtk_list_item_manager_get_first (self->item_manager); while (tile && tile->n_items == 0) tile = gtk_rb_tree_node_get_next (tile); if (tile == NULL) return FALSE; } pos = gtk_list_tile_get_position (self->item_manager, tile); if (tile->n_items > 1) { /* offset in x direction */ pos += (x - tile->area.x) / self->column_width; if (area) { guint col = MIN (x / self->column_width, self->n_columns - 1); area->x = ceil (col * self->column_width); area->width = ceil ((col + 1) * self->column_width) - area->x; } /* offset in y direction */ if (tile->n_items > self->n_columns) { guint rows_in_tile = tile->n_items / self->n_columns; guint row_height = tile->area.height / rows_in_tile; guint row_index = (y - tile->area.y) / row_height; pos += self->n_columns * row_index; if (area) { area->y = tile->area.y + row_index * row_height; area->height = row_height; } } else { if (area) { area->y = tile->area.y; area->height = tile->area.height; } } } else { if (area) *area = tile->area; } *position = pos; return TRUE; } static GtkBitset * gtk_grid_view_get_items_in_rect (GtkListBase *base, const GdkRectangle *rect) { GtkGridView *self = GTK_GRID_VIEW (base); guint first_row, last_row, first_column, last_column; GdkRectangle bounds; GtkBitset *result; result = gtk_bitset_new_empty (); /* limit rect to the region that actually overlaps items */ gtk_list_item_manager_get_tile_bounds (self->item_manager, &bounds); if (!gdk_rectangle_intersect (&bounds, rect, &bounds)) return result; first_column = fmax (floor (bounds.x / self->column_width), 0); last_column = fmin (floor ((bounds.x + bounds.width) / self->column_width), self->n_columns - 1); /* match y = 0 here because we care about the rows, not the cells */ if (!gtk_grid_view_get_position_from_allocation (base, 0, bounds.y, &first_row, NULL)) g_return_val_if_reached (result); if (!gtk_grid_view_get_position_from_allocation (base, 0, bounds.y + bounds.height - 1, &last_row, NULL)) g_return_val_if_reached (result); gtk_bitset_add_rectangle (result, first_row + first_column, last_column - first_column + 1, (last_row - first_row) / self->n_columns + 1, self->n_columns); return result; } static guint gtk_grid_view_move_focus_along (GtkListBase *base, guint pos, int steps) { GtkGridView *self = GTK_GRID_VIEW (base); steps *= self->n_columns; if (steps < 0) { if (pos >= self->n_columns) pos -= MIN (pos, -steps); } else { guint n_items = gtk_list_base_get_n_items (base); if (n_items / self->n_columns > pos / self->n_columns) pos += MIN (n_items - pos - 1, steps); } return pos; } static guint gtk_grid_view_move_focus_across (GtkListBase *base, guint pos, int steps) { if (steps < 0) return pos - MIN (pos, -steps); else { guint n_items = gtk_list_base_get_n_items (base); pos += MIN (n_items - pos - 1, steps); } return pos; } static int compare_ints (gconstpointer first, gconstpointer second) { return *(int *) first - *(int *) second; } static int gtk_grid_view_get_unknown_row_size (GtkGridView *self, GArray *heights) { g_return_val_if_fail (heights->len > 0, 0); /* return the median and hope rows are generally uniform with few outliers */ g_array_sort (heights, compare_ints); return g_array_index (heights, int, heights->len / 2); } static void gtk_grid_view_measure_column_size (GtkGridView *self, int *minimum, int *natural) { GtkOrientation opposite; GtkListTile *tile; int min, nat, child_min, child_nat; min = 0; nat = 0; opposite = gtk_list_base_get_opposite_orientation (GTK_LIST_BASE (self)); for (tile = gtk_list_item_manager_get_first (self->item_manager); tile != NULL; tile = gtk_rb_tree_node_get_next (tile)) { if (tile->widget == NULL) continue; gtk_widget_measure (tile->widget, opposite, -1, &child_min, &child_nat, NULL, NULL); min = MAX (min, child_min); nat = MAX (nat, child_nat); } *minimum = min; *natural = nat; } static void gtk_grid_view_measure_across (GtkWidget *widget, int for_size, int *minimum, int *natural) { GtkGridView *self = GTK_GRID_VIEW (widget); gtk_grid_view_measure_column_size (self, minimum, natural); *minimum *= self->min_columns; *natural *= self->max_columns; } static guint gtk_grid_view_compute_n_columns (GtkGridView *self, guint for_size, int min, int nat) { guint n_columns; /* rounding down is exactly what we want here, so int division works */ if (gtk_list_base_get_scroll_policy (GTK_LIST_BASE (self), gtk_list_base_get_opposite_orientation (GTK_LIST_BASE (self))) == GTK_SCROLL_MINIMUM) n_columns = for_size / MAX (1, min); else n_columns = for_size / MAX (1, nat); n_columns = CLAMP (n_columns, self->min_columns, self->max_columns); g_assert (n_columns > 0); return n_columns; } static void gtk_grid_view_measure_list (GtkWidget *widget, int for_size, int *minimum, int *natural) { GtkGridView *self = GTK_GRID_VIEW (widget); GtkScrollablePolicy scroll_policy; GtkListTile *tile; int height, row_height, child_min, child_nat, column_size, col_min, col_nat; gboolean measured; GArray *heights; guint n_unknown, n_columns; guint i; scroll_policy = gtk_list_base_get_scroll_policy (GTK_LIST_BASE (self), gtk_list_base_get_orientation (GTK_LIST_BASE (self))); heights = g_array_new (FALSE, FALSE, sizeof (int)); n_unknown = 0; height = 0; gtk_grid_view_measure_column_size (self, &col_min, &col_nat); for_size = MAX (for_size, col_min * (int) self->min_columns); n_columns = gtk_grid_view_compute_n_columns (self, for_size, col_min, col_nat); column_size = for_size / n_columns; i = 0; row_height = 0; measured = FALSE; for (tile = gtk_list_item_manager_get_first (self->item_manager); tile != NULL; tile = gtk_rb_tree_node_get_next (tile)) { if (tile->widget) { gtk_widget_measure (tile->widget, gtk_list_base_get_orientation (GTK_LIST_BASE (self)), column_size, &child_min, &child_nat, NULL, NULL); if (scroll_policy == GTK_SCROLL_MINIMUM) row_height = MAX (row_height, child_min); else row_height = MAX (row_height, child_nat); measured = TRUE; } i += tile->n_items; if (i >= n_columns) { if (measured) { g_array_append_val (heights, row_height); i -= n_columns; height += row_height; measured = FALSE; row_height = 0; } n_unknown += i / n_columns; i %= n_columns; } } if (i > 0) { if (measured) { g_array_append_val (heights, row_height); height += row_height; } else n_unknown++; } if (n_unknown) height += n_unknown * gtk_grid_view_get_unknown_row_size (self, heights); g_array_free (heights, TRUE); *minimum = height; *natural = height; } static void gtk_grid_view_measure (GtkWidget *widget, GtkOrientation orientation, int for_size, int *minimum, int *natural, int *minimum_baseline, int *natural_baseline) { GtkGridView *self = GTK_GRID_VIEW (widget); if (orientation == gtk_list_base_get_orientation (GTK_LIST_BASE (self))) gtk_grid_view_measure_list (widget, for_size, minimum, natural); else gtk_grid_view_measure_across (widget, for_size, minimum, natural); } static void gtk_grid_view_size_allocate (GtkWidget *widget, int width, int height, int baseline) { GtkGridView *self = GTK_GRID_VIEW (widget); GtkListTile *tile, *start; GArray *heights; int min_row_height, row_height, col_min, col_nat; GtkOrientation orientation, opposite_orientation; GtkScrollablePolicy scroll_policy; GdkRectangle bounds; int x, y; guint i; orientation = gtk_list_base_get_orientation (GTK_LIST_BASE (self)); scroll_policy = gtk_list_base_get_scroll_policy (GTK_LIST_BASE (self), orientation); opposite_orientation = OPPOSITE_ORIENTATION (orientation); min_row_height = ceil ((double) height / GTK_GRID_VIEW_MAX_VISIBLE_ROWS); /* step 0: exit early if list is empty */ if (gtk_list_item_manager_get_root (self->item_manager) == NULL) { gtk_list_base_update_adjustments (GTK_LIST_BASE (self), 0, 0, 0, 0, &x, &y); return; } /* step 1: determine width of the list */ gtk_grid_view_measure_column_size (self, &col_min, &col_nat); self->n_columns = gtk_grid_view_compute_n_columns (self, orientation == GTK_ORIENTATION_VERTICAL ? width : height, col_min, col_nat); self->column_width = (orientation == GTK_ORIENTATION_VERTICAL ? width : height) / self->n_columns; self->column_width = MAX (self->column_width, col_min); /* step 2: determine height of known rows */ heights = g_array_new (FALSE, FALSE, sizeof (int)); for (tile = gtk_list_tile_gc (self->item_manager, gtk_list_item_manager_get_first (self->item_manager)); tile != NULL; tile = gtk_list_tile_gc (self->item_manager, tile)) { /* if it's a multirow tile, handle it here */ if (tile->n_items > 1 && tile->n_items >= self->n_columns) { if (tile->n_items % self->n_columns) gtk_list_tile_split (self->item_manager, tile, tile->n_items / self->n_columns * self->n_columns); tile = gtk_rb_tree_node_get_next (tile); continue; } /* Not a multirow tile */ i = 0; row_height = 0; for (i = 0, start = tile; i < self->n_columns && tile != NULL; tile = gtk_list_tile_gc (self->item_manager, gtk_rb_tree_node_get_next (tile))) { if (tile->widget) { int min, nat, size; gtk_widget_measure (tile->widget, gtk_list_base_get_orientation (GTK_LIST_BASE (self)), self->column_width, &min, &nat, NULL, NULL); if (scroll_policy == GTK_SCROLL_MINIMUM) size = min; else size = nat; size = MAX (size, min_row_height); g_array_append_val (heights, size); row_height = MAX (row_height, size); } if (tile->n_items > self->n_columns - i) gtk_list_tile_split (self->item_manager, tile, self->n_columns - i); i += tile->n_items; } if (row_height > 0) { for (i = 0; start != tile; start = gtk_rb_tree_node_get_next (start)) { gtk_list_tile_set_area_size (self->item_manager, start, ceil (self->column_width * (i + start->n_items)) - ceil (self->column_width * i), row_height); i += start->n_items; } g_assert (i <= self->n_columns); } } /* step 3: determine height of rows with only unknown items */ self->unknown_row_height = gtk_grid_view_get_unknown_row_size (self, heights); g_array_free (heights, TRUE); /* step 4: determine height for remaining rows and set each row's position */ y = 0; i = 0; for (tile = gtk_list_item_manager_get_first (self->item_manager); tile != NULL; tile = gtk_rb_tree_node_get_next (tile)) { gtk_list_tile_set_area_position (self->item_manager, tile, ceil (self->column_width * i), y); if (tile->n_items >= self->n_columns && tile->widget == NULL) { g_assert (i == 0); g_assert (tile->n_items % self->n_columns == 0); gtk_list_tile_set_area_size (self->item_manager, tile, ceil (self->column_width * self->n_columns), self->unknown_row_height * (tile->n_items / self->n_columns)); y += tile->area.height; } else { if (tile->area.height == 0) { /* this case is for the last row - it may not be a full row so it won't * be a multirow tile but it may have no widgets either */ gtk_list_tile_set_area_size (self->item_manager, tile, ceil (self->column_width * (i + tile->n_items)) - ceil (self->column_width * i), self->unknown_row_height); } i += tile->n_items; } if (i >= self->n_columns) { g_assert (i == self->n_columns); y += tile->area.height; i = 0; } } gtk_list_item_manager_get_tile_bounds (self->item_manager, &bounds); /* step 4: update the adjustments */ gtk_list_base_update_adjustments (GTK_LIST_BASE (self), bounds.width, bounds.height, gtk_widget_get_size (widget, opposite_orientation), gtk_widget_get_size (widget, orientation), &x, &y); gtk_list_base_allocate_children (GTK_LIST_BASE (widget)); gtk_list_base_allocate_rubberband (GTK_LIST_BASE (widget)); } static void gtk_grid_view_dispose (GObject *object) { GtkGridView *self = GTK_GRID_VIEW (object); self->item_manager = NULL; G_OBJECT_CLASS (gtk_grid_view_parent_class)->dispose (object); } static void gtk_grid_view_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { GtkGridView *self = GTK_GRID_VIEW (object); switch (property_id) { case PROP_FACTORY: g_value_set_object (value, gtk_list_item_manager_get_factory (self->item_manager)); break; case PROP_MAX_COLUMNS: g_value_set_uint (value, self->max_columns); break; case PROP_MIN_COLUMNS: g_value_set_uint (value, self->min_columns); break; case PROP_MODEL: g_value_set_object (value, gtk_list_base_get_model (GTK_LIST_BASE (self))); break; case PROP_SINGLE_CLICK_ACTIVATE: g_value_set_boolean (value, gtk_list_item_manager_get_single_click_activate (self->item_manager)); break; case PROP_ENABLE_RUBBERBAND: g_value_set_boolean (value, gtk_list_base_get_enable_rubberband (GTK_LIST_BASE (self))); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void gtk_grid_view_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GtkGridView *self = GTK_GRID_VIEW (object); switch (property_id) { case PROP_FACTORY: gtk_grid_view_set_factory (self, g_value_get_object (value)); break; case PROP_MAX_COLUMNS: gtk_grid_view_set_max_columns (self, g_value_get_uint (value)); break; case PROP_MIN_COLUMNS: gtk_grid_view_set_min_columns (self, g_value_get_uint (value)); break; case PROP_MODEL: gtk_grid_view_set_model (self, g_value_get_object (value)); break; case PROP_SINGLE_CLICK_ACTIVATE: gtk_grid_view_set_single_click_activate (self, g_value_get_boolean (value)); break; case PROP_ENABLE_RUBBERBAND: gtk_grid_view_set_enable_rubberband (self, g_value_get_boolean (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void gtk_grid_view_activate_item (GtkWidget *widget, const char *action_name, GVariant *parameter) { GtkGridView *self = GTK_GRID_VIEW (widget); guint pos; if (!g_variant_check_format_string (parameter, "u", FALSE)) return; g_variant_get (parameter, "u", &pos); if (pos >= gtk_list_base_get_n_items (GTK_LIST_BASE (self))) return; g_signal_emit (widget, signals[ACTIVATE], 0, pos); } static void gtk_grid_view_class_init (GtkGridViewClass *klass) { GtkListBaseClass *list_base_class = GTK_LIST_BASE_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); GObjectClass *gobject_class = G_OBJECT_CLASS (klass); list_base_class->list_item_name = "child"; list_base_class->list_item_role = GTK_ACCESSIBLE_ROLE_GRID_CELL; list_base_class->list_item_size = sizeof (GtkListTile); list_base_class->list_item_augment_size = sizeof (GtkListTileAugment); list_base_class->list_item_augment_func = gtk_list_item_manager_augment_node; list_base_class->get_allocation_along = gtk_grid_view_get_allocation_along; list_base_class->get_allocation_across = gtk_grid_view_get_allocation_across; list_base_class->get_items_in_rect = gtk_grid_view_get_items_in_rect; list_base_class->get_position_from_allocation = gtk_grid_view_get_position_from_allocation; list_base_class->move_focus_along = gtk_grid_view_move_focus_along; list_base_class->move_focus_across = gtk_grid_view_move_focus_across; widget_class->measure = gtk_grid_view_measure; widget_class->size_allocate = gtk_grid_view_size_allocate; gobject_class->dispose = gtk_grid_view_dispose; gobject_class->get_property = gtk_grid_view_get_property; gobject_class->set_property = gtk_grid_view_set_property; /** * GtkGridView:factory: (attributes org.gtk.Property.get=gtk_grid_view_get_factory org.gtk.Property.set=gtk_grid_view_set_factory) * * Factory for populating list items. */ properties[PROP_FACTORY] = g_param_spec_object ("factory", NULL, NULL, GTK_TYPE_LIST_ITEM_FACTORY, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); /** * GtkGridView:max-columns: (attributes org.gtk.Property.get=gtk_grid_view_get_max_columns org.gtk.Property.set=gtk_grid_view_set_max_columns) * * Maximum number of columns per row. * * If this number is smaller than [property@Gtk.GridView:min-columns], * that value is used instead. */ properties[PROP_MAX_COLUMNS] = g_param_spec_uint ("max-columns", NULL, NULL, 1, G_MAXUINT, DEFAULT_MAX_COLUMNS, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); /** * GtkGridView:min-columns: (attributes org.gtk.Property.get=gtk_grid_view_get_min_columns org.gtk.Property.set=gtk_grid_view_set_min_columns) * * Minimum number of columns per row. */ properties[PROP_MIN_COLUMNS] = g_param_spec_uint ("min-columns", NULL, NULL, 1, G_MAXUINT, 1, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); /** * GtkGridView:model: (attributes org.gtk.Property.get=gtk_grid_view_get_model org.gtk.Property.set=gtk_grid_view_set_model) * * Model for the items displayed. */ properties[PROP_MODEL] = g_param_spec_object ("model", NULL, NULL, GTK_TYPE_SELECTION_MODEL, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); /** * GtkGridView:single-click-activate: (attributes org.gtk.Property.get=gtk_grid_view_get_single_click_activate org.gtk.Property.set=gtk_grid_view_set_single_click_activate) * * Activate rows on single click and select them on hover. */ properties[PROP_SINGLE_CLICK_ACTIVATE] = g_param_spec_boolean ("single-click-activate", NULL, NULL, FALSE, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); /** * GtkGridView:enable-rubberband: (attributes org.gtk.Property.get=gtk_grid_view_get_enable_rubberband org.gtk.Property.set=gtk_grid_view_set_enable_rubberband) * * Allow rubberband selection. */ properties[PROP_ENABLE_RUBBERBAND] = g_param_spec_boolean ("enable-rubberband", NULL, NULL, FALSE, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); g_object_class_install_properties (gobject_class, N_PROPS, properties); /** * GtkGridView::activate: * @self: The `GtkGridView` * @position: position of item to activate * * Emitted when a cell has been activated by the user, * usually via activating the GtkGridView|list.activate-item action. * * This allows for a convenient way to handle activation in a gridview. * See [property@Gtk.ListItem:activatable] for details on how to use * this signal. */ signals[ACTIVATE] = g_signal_new (I_("activate"), G_TYPE_FROM_CLASS (gobject_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__UINT, G_TYPE_NONE, 1, G_TYPE_UINT); g_signal_set_va_marshaller (signals[ACTIVATE], G_TYPE_FROM_CLASS (gobject_class), g_cclosure_marshal_VOID__UINTv); /** * GtkGridView|list.activate-item: * @position: position of item to activate * * Activates the item given in @position by emitting the * [signal@Gtk.GridView::activate] signal. */ gtk_widget_class_install_action (widget_class, "list.activate-item", "u", gtk_grid_view_activate_item); gtk_widget_class_set_css_name (widget_class, I_("gridview")); gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_GRID); } static void gtk_grid_view_init (GtkGridView *self) { self->item_manager = gtk_list_base_get_manager (GTK_LIST_BASE (self)); self->min_columns = 1; self->max_columns = DEFAULT_MAX_COLUMNS; self->n_columns = 1; gtk_list_base_set_anchor_max_widgets (GTK_LIST_BASE (self), self->max_columns * GTK_GRID_VIEW_MAX_VISIBLE_ROWS, self->max_columns); gtk_widget_add_css_class (GTK_WIDGET (self), "view"); } /** * gtk_grid_view_new: * @model: (nullable) (transfer full): the model to use * @factory: (nullable) (transfer full): The factory to populate items with * * Creates a new `GtkGridView` that uses the given @factory for * mapping items to widgets. * * The function takes ownership of the * arguments, so you can write code like * ```c * grid_view = gtk_grid_view_new (create_model (), * gtk_builder_list_item_factory_new_from_resource ("/resource.ui")); * ``` * * Returns: a new `GtkGridView` using the given @model and @factory */ GtkWidget * gtk_grid_view_new (GtkSelectionModel *model, GtkListItemFactory *factory) { GtkWidget *result; g_return_val_if_fail (model == NULL || GTK_IS_SELECTION_MODEL (model), NULL); g_return_val_if_fail (factory == NULL || GTK_IS_LIST_ITEM_FACTORY (factory), NULL); result = g_object_new (GTK_TYPE_GRID_VIEW, "model", model, "factory", factory, NULL); /* consume the references */ g_clear_object (&model); g_clear_object (&factory); return result; } /** * gtk_grid_view_get_model: (attributes org.gtk.Method.get_property=model) * @self: a `GtkGridView` * * Gets the model that's currently used to read the items displayed. * * Returns: (nullable) (transfer none): The model in use **/ GtkSelectionModel * gtk_grid_view_get_model (GtkGridView *self) { g_return_val_if_fail (GTK_IS_GRID_VIEW (self), NULL); return gtk_list_base_get_model (GTK_LIST_BASE (self)); } /** * gtk_grid_view_set_model: (attributes org.gtk.Method.set_property=model) * @self: a `GtkGridView` * @model: (nullable) (transfer none): the model to use * * Sets the model to use. * * This must be a [iface@Gtk.SelectionModel]. */ void gtk_grid_view_set_model (GtkGridView *self, GtkSelectionModel *model) { g_return_if_fail (GTK_IS_GRID_VIEW (self)); g_return_if_fail (model == NULL || GTK_IS_SELECTION_MODEL (model)); if (!gtk_list_base_set_model (GTK_LIST_BASE (self), model)) return; gtk_accessible_update_property (GTK_ACCESSIBLE (self), GTK_ACCESSIBLE_PROPERTY_MULTI_SELECTABLE, GTK_IS_MULTI_SELECTION (model), -1); g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MODEL]); } /** * gtk_grid_view_get_factory: (attributes org.gtk.Method.get_property=factory) * @self: a `GtkGridView` * * Gets the factory that's currently used to populate list items. * * Returns: (nullable) (transfer none): The factory in use */ GtkListItemFactory * gtk_grid_view_get_factory (GtkGridView *self) { g_return_val_if_fail (GTK_IS_GRID_VIEW (self), NULL); return gtk_list_item_manager_get_factory (self->item_manager); } /** * gtk_grid_view_set_factory: (attributes org.gtk.Method.set_property=factory) * @self: a `GtkGridView` * @factory: (nullable) (transfer none): the factory to use * * Sets the `GtkListItemFactory` to use for populating list items. */ void gtk_grid_view_set_factory (GtkGridView *self, GtkListItemFactory *factory) { g_return_if_fail (GTK_IS_GRID_VIEW (self)); g_return_if_fail (factory == NULL || GTK_IS_LIST_ITEM_FACTORY (factory)); if (factory == gtk_list_item_manager_get_factory (self->item_manager)) return; gtk_list_item_manager_set_factory (self->item_manager, factory); g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FACTORY]); } /** * gtk_grid_view_get_max_columns: (attributes org.gtk.Method.get_property=max-columns) * @self: a `GtkGridView` * * Gets the maximum number of columns that the grid will use. * * Returns: The maximum number of columns */ guint gtk_grid_view_get_max_columns (GtkGridView *self) { g_return_val_if_fail (GTK_IS_GRID_VIEW (self), DEFAULT_MAX_COLUMNS); return self->max_columns; } /** * gtk_grid_view_set_max_columns: (attributes org.gtk.Method.set_property=max-columns) * @self: a `GtkGridView` * @max_columns: The maximum number of columns * * Sets the maximum number of columns to use. * * This number must be at least 1. * * If @max_columns is smaller than the minimum set via * [method@Gtk.GridView.set_min_columns], that value is used instead. */ void gtk_grid_view_set_max_columns (GtkGridView *self, guint max_columns) { g_return_if_fail (GTK_IS_GRID_VIEW (self)); g_return_if_fail (max_columns > 0); if (self->max_columns == max_columns) return; self->max_columns = max_columns; gtk_list_base_set_anchor_max_widgets (GTK_LIST_BASE (self), self->max_columns * GTK_GRID_VIEW_MAX_VISIBLE_ROWS, self->max_columns); gtk_widget_queue_resize (GTK_WIDGET (self)); g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MAX_COLUMNS]); } /** * gtk_grid_view_get_min_columns: (attributes org.gtk.Method.get_property=min-columns) * @self: a `GtkGridView` * * Gets the minimum number of columns that the grid will use. * * Returns: The minimum number of columns */ guint gtk_grid_view_get_min_columns (GtkGridView *self) { g_return_val_if_fail (GTK_IS_GRID_VIEW (self), 1); return self->min_columns; } /** * gtk_grid_view_set_min_columns: (attributes org.gtk.Method.set_property=min-columns) * @self: a `GtkGridView` * @min_columns: The minimum number of columns * * Sets the minimum number of columns to use. * * This number must be at least 1. * * If @min_columns is smaller than the minimum set via * [method@Gtk.GridView.set_max_columns], that value is ignored. */ void gtk_grid_view_set_min_columns (GtkGridView *self, guint min_columns) { g_return_if_fail (GTK_IS_GRID_VIEW (self)); g_return_if_fail (min_columns > 0); if (self->min_columns == min_columns) return; self->min_columns = min_columns; gtk_widget_queue_resize (GTK_WIDGET (self)); g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MIN_COLUMNS]); } /** * gtk_grid_view_set_single_click_activate: (attributes org.gtk.Method.set_property=single-click-activate) * @self: a `GtkGridView` * @single_click_activate: %TRUE to activate items on single click * * Sets whether items should be activated on single click and * selected on hover. */ void gtk_grid_view_set_single_click_activate (GtkGridView *self, gboolean single_click_activate) { g_return_if_fail (GTK_IS_GRID_VIEW (self)); if (single_click_activate == gtk_list_item_manager_get_single_click_activate (self->item_manager)) return; gtk_list_item_manager_set_single_click_activate (self->item_manager, single_click_activate); g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SINGLE_CLICK_ACTIVATE]); } /** * gtk_grid_view_get_single_click_activate: (attributes org.gtk.Method.get_property=single-click-activate) * @self: a `GtkGridView` * * Returns whether items will be activated on single click and * selected on hover. * * Returns: %TRUE if items are activated on single click */ gboolean gtk_grid_view_get_single_click_activate (GtkGridView *self) { g_return_val_if_fail (GTK_IS_GRID_VIEW (self), FALSE); return gtk_list_item_manager_get_single_click_activate (self->item_manager); } /** * gtk_grid_view_set_enable_rubberband: (attributes org.gtk.Method.set_property=enable-rubberband) * @self: a `GtkGridView` * @enable_rubberband: %TRUE to enable rubberband selection * * Sets whether selections can be changed by dragging with the mouse. */ void gtk_grid_view_set_enable_rubberband (GtkGridView *self, gboolean enable_rubberband) { g_return_if_fail (GTK_IS_GRID_VIEW (self)); if (enable_rubberband == gtk_list_base_get_enable_rubberband (GTK_LIST_BASE (self))) return; gtk_list_base_set_enable_rubberband (GTK_LIST_BASE (self), enable_rubberband); g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ENABLE_RUBBERBAND]); } /** * gtk_grid_view_get_enable_rubberband: (attributes org.gtk.Method.get_property=enable-rubberband) * @self: a `GtkGridView` * * Returns whether rows can be selected by dragging with the mouse. * * Returns: %TRUE if rubberband selection is enabled */ gboolean gtk_grid_view_get_enable_rubberband (GtkGridView *self) { g_return_val_if_fail (GTK_IS_GRID_VIEW (self), FALSE); return gtk_list_base_get_enable_rubberband (GTK_LIST_BASE (self)); }