/* * Copyright (C) 2007-2010 Openismus GmbH * Copyright (C) 2013 Red Hat, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, see . * * Authors: * Tristan Van Berkom * Matthias Clasen * William Jon McCann */ /* Preamble {{{1 */ /** * SECTION:gtkflowbox * @Short_Description: A container that allows reflowing its children * @Title: GtkFlowBox * * A GtkFlowBox positions child widgets in sequence according to its * orientation. * * For instance, with the horizontal orientation, the widgets will be * arranged from left to right, starting a new row under the previous * row when necessary. Reducing the width in this case will require more * rows, so a larger height will be requested. * * Likewise, with the vertical orientation, the widgets will be arranged * from top to bottom, starting a new column to the right when necessary. * Reducing the height will require more columns, so a larger width will * be requested. * * The size request of a GtkFlowBox alone may not be what you expect; if you * need to be able to shrink it along both axes and dynamically reflow its * children, you may have to wrap it in a #GtkScrolledWindow to enable that. * * The children of a GtkFlowBox can be dynamically sorted and filtered. * * Although a GtkFlowBox must have only #GtkFlowBoxChild children, * you can add any kind of widget to it via gtk_container_add(), and * a GtkFlowBoxChild widget will automatically be inserted between * the box and the widget. * * Also see #GtkListBox. * * GtkFlowBox was added in GTK+ 3.12. * * # CSS nodes * * |[ * flowbox * ├── flowboxchild * │ ╰── * ├── flowboxchild * │ ╰── * ┊ * ╰── [rubberband] * ]| * * GtkFlowBox uses a single CSS node with name flowbox. GtkFlowBoxChild * uses a single CSS node with name flowboxchild. * For rubberband selection, a subnode with name rubberband is used. */ #include #include "gtkflowbox.h" #include "gtkmarshalers.h" #include "gtkprivate.h" #include "gtkorientableprivate.h" #include "gtkintl.h" #include "gtkcssnodeprivate.h" #include "gtkwidgetprivate.h" #include "gtkstylecontextprivate.h" #include "gtkcsscustomgadgetprivate.h" #include "gtkcontainerprivate.h" #include "a11y/gtkflowboxaccessibleprivate.h" #include "a11y/gtkflowboxchildaccessible.h" /* Forward declarations and utilities {{{1 */ static void gtk_flow_box_update_cursor (GtkFlowBox *box, GtkFlowBoxChild *child); static void gtk_flow_box_select_and_activate (GtkFlowBox *box, GtkFlowBoxChild *child); static void gtk_flow_box_update_selection (GtkFlowBox *box, GtkFlowBoxChild *child, gboolean modify, gboolean extend); static void gtk_flow_box_apply_filter (GtkFlowBox *box, GtkFlowBoxChild *child); static void gtk_flow_box_apply_sort (GtkFlowBox *box, GtkFlowBoxChild *child); static gint gtk_flow_box_sort (GtkFlowBoxChild *a, GtkFlowBoxChild *b, GtkFlowBox *box); static void gtk_flow_box_bound_model_changed (GListModel *list, guint position, guint removed, guint added, gpointer user_data); static void gtk_flow_box_check_model_compat (GtkFlowBox *box); static void get_current_selection_modifiers (GtkWidget *widget, gboolean *modify, gboolean *extend) { GdkModifierType state = 0; GdkModifierType mask; *modify = FALSE; *extend = FALSE; if (gtk_get_current_event_state (&state)) { mask = gtk_widget_get_modifier_mask (widget, GDK_MODIFIER_INTENT_MODIFY_SELECTION); if ((state & mask) == mask) *modify = TRUE; mask = gtk_widget_get_modifier_mask (widget, GDK_MODIFIER_INTENT_EXTEND_SELECTION); if ((state & mask) == mask) *extend = TRUE; } } static void path_from_horizontal_line_rects (cairo_t *cr, GdkRectangle *lines, gint n_lines) { gint start_line, end_line; GdkRectangle *r; gint i; /* Join rows vertically by extending to the middle */ for (i = 0; i < n_lines - 1; i++) { GdkRectangle *r1 = &lines[i]; GdkRectangle *r2 = &lines[i+1]; gint gap, old; gap = r2->y - (r1->y + r1->height); r1->height += gap / 2; old = r2->y; r2->y = r1->y + r1->height; r2->height += old - r2->y; } cairo_new_path (cr); start_line = 0; do { for (i = start_line; i < n_lines; i++) { r = &lines[i]; if (i == start_line) cairo_move_to (cr, r->x + r->width, r->y); else cairo_line_to (cr, r->x + r->width, r->y); cairo_line_to (cr, r->x + r->width, r->y + r->height); if (i < n_lines - 1 && (r->x + r->width < lines[i+1].x || r->x > lines[i+1].x + lines[i+1].width)) { i++; break; } } end_line = i; for (i = end_line - 1; i >= start_line; i--) { r = &lines[i]; cairo_line_to (cr, r->x, r->y + r->height); cairo_line_to (cr, r->x, r->y); } cairo_close_path (cr); start_line = end_line; } while (end_line < n_lines); } static void path_from_vertical_line_rects (cairo_t *cr, GdkRectangle *lines, gint n_lines) { gint start_line, end_line; GdkRectangle *r; gint i; /* Join rows horizontally by extending to the middle */ for (i = 0; i < n_lines - 1; i++) { GdkRectangle *r1 = &lines[i]; GdkRectangle *r2 = &lines[i+1]; gint gap, old; gap = r2->x - (r1->x + r1->width); r1->width += gap / 2; old = r2->x; r2->x = r1->x + r1->width; r2->width += old - r2->x; } cairo_new_path (cr); start_line = 0; do { for (i = start_line; i < n_lines; i++) { r = &lines[i]; if (i == start_line) cairo_move_to (cr, r->x, r->y + r->height); else cairo_line_to (cr, r->x, r->y + r->height); cairo_line_to (cr, r->x + r->width, r->y + r->height); if (i < n_lines - 1 && (r->y + r->height < lines[i+1].y || r->y > lines[i+1].y + lines[i+1].height)) { i++; break; } } end_line = i; for (i = end_line - 1; i >= start_line; i--) { r = &lines[i]; cairo_line_to (cr, r->x + r->width, r->y); cairo_line_to (cr, r->x, r->y); } cairo_close_path (cr); start_line = end_line; } while (end_line < n_lines); } /* GtkFlowBoxChild {{{1 */ /* GObject boilerplate {{{2 */ enum { CHILD_ACTIVATE, CHILD_LAST_SIGNAL }; static guint child_signals[CHILD_LAST_SIGNAL] = { 0 }; typedef struct _GtkFlowBoxChildPrivate GtkFlowBoxChildPrivate; struct _GtkFlowBoxChildPrivate { GSequenceIter *iter; GtkCssGadget *gadget; gboolean selected; }; #define CHILD_PRIV(child) ((GtkFlowBoxChildPrivate*)gtk_flow_box_child_get_instance_private ((GtkFlowBoxChild*)(child))) G_DEFINE_TYPE_WITH_PRIVATE (GtkFlowBoxChild, gtk_flow_box_child, GTK_TYPE_BIN) /* Internal API {{{2 */ static GtkFlowBox * gtk_flow_box_child_get_box (GtkFlowBoxChild *child) { GtkWidget *parent; parent = gtk_widget_get_parent (GTK_WIDGET (child)); if (parent && GTK_IS_FLOW_BOX (parent)) return GTK_FLOW_BOX (parent); return NULL; } static void gtk_flow_box_child_set_focus (GtkFlowBoxChild *child) { GtkFlowBox *box = gtk_flow_box_child_get_box (child); gboolean modify; gboolean extend; get_current_selection_modifiers (GTK_WIDGET (box), &modify, &extend); if (modify) gtk_flow_box_update_cursor (box, child); else gtk_flow_box_update_selection (box, child, FALSE, FALSE); } /* GtkWidget implementation {{{2 */ static gboolean gtk_flow_box_child_focus (GtkWidget *widget, GtkDirectionType direction) { gboolean had_focus = FALSE; GtkWidget *child; child = gtk_bin_get_child (GTK_BIN (widget)); /* Without "can-focus" flag try to pass the focus to the child immediately */ if (!gtk_widget_get_can_focus (widget)) { if (child) { if (gtk_widget_child_focus (child, direction)) { GtkFlowBox *box; box = gtk_flow_box_child_get_box (GTK_FLOW_BOX_CHILD (widget)); if (box) gtk_flow_box_update_cursor (box, GTK_FLOW_BOX_CHILD (widget)); return TRUE; } } return FALSE; } g_object_get (widget, "has-focus", &had_focus, NULL); if (had_focus) { /* If on row, going right, enter into possible container */ if (child && (direction == GTK_DIR_RIGHT || direction == GTK_DIR_TAB_FORWARD)) { if (gtk_widget_child_focus (GTK_WIDGET (child), direction)) return TRUE; } return FALSE; } else if (gtk_container_get_focus_child (GTK_CONTAINER (widget)) != NULL) { /* Child has focus, always navigate inside it first */ if (gtk_widget_child_focus (child, direction)) return TRUE; /* If exiting child container to the left, select child */ if (direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD) { gtk_flow_box_child_set_focus (GTK_FLOW_BOX_CHILD (widget)); return TRUE; } return FALSE; } else { /* If coming from the left, enter into possible container */ if (child && (direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD)) { if (gtk_widget_child_focus (child, direction)) return TRUE; } gtk_flow_box_child_set_focus (GTK_FLOW_BOX_CHILD (widget)); return TRUE; } } static void gtk_flow_box_child_activate (GtkFlowBoxChild *child) { GtkFlowBox *box; box = gtk_flow_box_child_get_box (child); if (box) gtk_flow_box_select_and_activate (box, child); } static gboolean gtk_flow_box_child_draw (GtkWidget *widget, cairo_t *cr) { gtk_css_gadget_draw (CHILD_PRIV (GTK_FLOW_BOX_CHILD (widget))->gadget, cr); return FALSE; } static gboolean gtk_flow_box_child_render (GtkCssGadget *gadget, cairo_t *cr, int x, int y, int width, int height, gpointer data) { GtkWidget *widget; widget = gtk_css_gadget_get_owner (gadget); GTK_WIDGET_CLASS (gtk_flow_box_child_parent_class)->draw (widget, cr); return gtk_widget_has_visible_focus (widget); } /* Size allocation {{{3 */ static GtkSizeRequestMode gtk_flow_box_child_get_request_mode (GtkWidget *widget) { GtkFlowBox *box; box = gtk_flow_box_child_get_box (GTK_FLOW_BOX_CHILD (widget)); if (box) return gtk_widget_get_request_mode (GTK_WIDGET (box)); else return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; } static void gtk_flow_box_child_get_preferred_height (GtkWidget *widget, gint *minimum, gint *natural) { gtk_css_gadget_get_preferred_size (CHILD_PRIV (GTK_FLOW_BOX_CHILD (widget))->gadget, GTK_ORIENTATION_VERTICAL, -1, minimum, natural, NULL, NULL); } static void gtk_flow_box_child_get_preferred_height_for_width (GtkWidget *widget, gint width, gint *minimum, gint *natural) { gtk_css_gadget_get_preferred_size (CHILD_PRIV (GTK_FLOW_BOX_CHILD (widget))->gadget, GTK_ORIENTATION_VERTICAL, width, minimum, natural, NULL, NULL); } static void gtk_flow_box_child_get_preferred_width (GtkWidget *widget, gint *minimum, gint *natural) { gtk_css_gadget_get_preferred_size (CHILD_PRIV (GTK_FLOW_BOX_CHILD (widget))->gadget, GTK_ORIENTATION_HORIZONTAL, -1, minimum, natural, NULL, NULL); } static void gtk_flow_box_child_get_preferred_width_for_height (GtkWidget *widget, gint height, gint *minimum, gint *natural) { gtk_css_gadget_get_preferred_size (CHILD_PRIV (GTK_FLOW_BOX_CHILD (widget))->gadget, GTK_ORIENTATION_HORIZONTAL, height, minimum, natural, NULL, NULL); } static void gtk_flow_box_child_measure (GtkCssGadget *gadget, GtkOrientation orientation, int for_size, int *minimum, int *natural, int *minimum_baseline, int *natural_baseline, gpointer data) { GtkWidget *widget; GtkWidget *child; widget = gtk_css_gadget_get_owner (gadget); child = gtk_bin_get_child (GTK_BIN (widget)); if (!child || ! gtk_widget_get_visible (child)) { *minimum = *natural = 0; return; } if (orientation == GTK_ORIENTATION_HORIZONTAL) { if (for_size < 0) { if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH) { gtk_widget_get_preferred_width (child, minimum, natural); } else { gint height; gtk_widget_get_preferred_height (child, NULL, &height); gtk_widget_get_preferred_width_for_height (child, height, minimum, natural); } } else { if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH) gtk_widget_get_preferred_width (child, minimum, natural); else gtk_widget_get_preferred_width_for_height (child, for_size, minimum, natural); } } else { if (for_size < 0) { if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH) { gint width; gtk_widget_get_preferred_width (child, NULL, &width); gtk_widget_get_preferred_height_for_width (child, width, minimum, natural); } else { gtk_widget_get_preferred_height (child, minimum, natural); } } else { if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH) gtk_widget_get_preferred_height_for_width (child, for_size, minimum, natural); else gtk_widget_get_preferred_height (child, minimum, natural); } } } static void gtk_flow_box_child_size_allocate (GtkWidget *widget, GtkAllocation *allocation) { GtkAllocation clip; gtk_widget_set_allocation (widget, allocation); gtk_css_gadget_allocate (CHILD_PRIV (GTK_FLOW_BOX_CHILD (widget))->gadget, allocation, gtk_widget_get_allocated_baseline (widget), &clip); gtk_widget_set_clip (widget, &clip); } static void gtk_flow_box_child_allocate (GtkCssGadget *gadget, const GtkAllocation *allocation, int baseline, GtkAllocation *out_clip, gpointer data) { GtkWidget *widget; GtkWidget *child; widget = gtk_css_gadget_get_owner (gadget); child = gtk_bin_get_child (GTK_BIN (widget)); if (child && gtk_widget_get_visible (child)) gtk_widget_size_allocate (child, (GtkAllocation *)allocation); gtk_container_get_children_clip (GTK_CONTAINER (widget), out_clip); } /* GObject implementation {{{2 */ static void gtk_flow_box_child_finalize (GObject *object) { g_clear_object (&CHILD_PRIV (GTK_FLOW_BOX_CHILD (object))->gadget); G_OBJECT_CLASS (gtk_flow_box_child_parent_class)->finalize (object); } static void gtk_flow_box_child_class_init (GtkFlowBoxChildClass *class) { GObjectClass *object_class = G_OBJECT_CLASS (class); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); object_class->finalize = gtk_flow_box_child_finalize; widget_class->draw = gtk_flow_box_child_draw; widget_class->get_request_mode = gtk_flow_box_child_get_request_mode; widget_class->get_preferred_height = gtk_flow_box_child_get_preferred_height; widget_class->get_preferred_height_for_width = gtk_flow_box_child_get_preferred_height_for_width; widget_class->get_preferred_width = gtk_flow_box_child_get_preferred_width; widget_class->get_preferred_width_for_height = gtk_flow_box_child_get_preferred_width_for_height; widget_class->size_allocate = gtk_flow_box_child_size_allocate; widget_class->focus = gtk_flow_box_child_focus; class->activate = gtk_flow_box_child_activate; /** * GtkFlowBoxChild::activate: * @child: The child on which the signal is emitted * * The ::activate signal is emitted when the user activates * a child widget in a #GtkFlowBox, either by clicking or * double-clicking, or by using the Space or Enter key. * * While this signal is used as a * [keybinding signal][GtkBindingSignal], * it can be used by applications for their own purposes. */ child_signals[CHILD_ACTIVATE] = g_signal_new (I_("activate"), G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GtkFlowBoxChildClass, activate), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); widget_class->activate_signal = child_signals[CHILD_ACTIVATE]; gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_LIST_ITEM); gtk_widget_class_set_css_name (widget_class, "flowboxchild"); } static void gtk_flow_box_child_init (GtkFlowBoxChild *child) { gtk_widget_set_can_focus (GTK_WIDGET (child), TRUE); gtk_widget_set_redraw_on_allocate (GTK_WIDGET (child), TRUE); CHILD_PRIV (child)->gadget = gtk_css_custom_gadget_new_for_node (gtk_widget_get_css_node (GTK_WIDGET (child)), GTK_WIDGET (child), gtk_flow_box_child_measure, gtk_flow_box_child_allocate, gtk_flow_box_child_render, NULL, NULL); } /* Public API {{{2 */ /** * gtk_flow_box_child_new: * * Creates a new #GtkFlowBoxChild, to be used as a child * of a #GtkFlowBox. * * Returns: a new #GtkFlowBoxChild * * Since: 3.12 */ GtkWidget * gtk_flow_box_child_new (void) { return g_object_new (GTK_TYPE_FLOW_BOX_CHILD, NULL); } /** * gtk_flow_box_child_get_index: * @child: a #GtkFlowBoxChild * * Gets the current index of the @child in its #GtkFlowBox container. * * Returns: the index of the @child, or -1 if the @child is not * in a flow box. * * Since: 3.12 */ gint gtk_flow_box_child_get_index (GtkFlowBoxChild *child) { GtkFlowBoxChildPrivate *priv; g_return_val_if_fail (GTK_IS_FLOW_BOX_CHILD (child), -1); priv = CHILD_PRIV (child); if (priv->iter != NULL) return g_sequence_iter_get_position (priv->iter); return -1; } /** * gtk_flow_box_child_is_selected: * @child: a #GtkFlowBoxChild * * Returns whether the @child is currently selected in its * #GtkFlowBox container. * * Returns: %TRUE if @child is selected * * Since: 3.12 */ gboolean gtk_flow_box_child_is_selected (GtkFlowBoxChild *child) { g_return_val_if_fail (GTK_IS_FLOW_BOX_CHILD (child), FALSE); return CHILD_PRIV (child)->selected; } /** * gtk_flow_box_child_changed: * @child: a #GtkFlowBoxChild * * Marks @child as changed, causing any state that depends on this * to be updated. This affects sorting and filtering. * * Note that calls to this method must be in sync with the data * used for the sorting and filtering functions. For instance, if * the list is mirroring some external data set, and *two* children * changed in the external data set when you call * gtk_flow_box_child_changed() on the first child, the sort function * must only read the new data for the first of the two changed * children, otherwise the resorting of the children 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 sorting * and filtering functions into the widgets themselves. Another * alternative is to call gtk_flow_box_invalidate_sort() on any * model change, but that is more expensive. * * Since: 3.12 */ void gtk_flow_box_child_changed (GtkFlowBoxChild *child) { GtkFlowBox *box; g_return_if_fail (GTK_IS_FLOW_BOX_CHILD (child)); box = gtk_flow_box_child_get_box (child); if (box == NULL) return; gtk_flow_box_apply_sort (box, child); gtk_flow_box_apply_filter (box, child); } /* GtkFlowBox {{{1 */ /* Constants {{{2 */ #define DEFAULT_MAX_CHILDREN_PER_LINE 7 #define RUBBERBAND_START_DISTANCE 32 #define AUTOSCROLL_FAST_DISTANCE 32 #define AUTOSCROLL_FACTOR 20 #define AUTOSCROLL_FACTOR_FAST 10 /* GObject boilerplate {{{2 */ enum { CHILD_ACTIVATED, SELECTED_CHILDREN_CHANGED, ACTIVATE_CURSOR_CHILD, TOGGLE_CURSOR_CHILD, MOVE_CURSOR, SELECT_ALL, UNSELECT_ALL, LAST_SIGNAL }; static guint signals[LAST_SIGNAL] = { 0 }; enum { PROP_0, PROP_HOMOGENEOUS, PROP_COLUMN_SPACING, PROP_ROW_SPACING, PROP_MIN_CHILDREN_PER_LINE, PROP_MAX_CHILDREN_PER_LINE, PROP_SELECTION_MODE, PROP_ACTIVATE_ON_SINGLE_CLICK, /* orientable */ PROP_ORIENTATION, LAST_PROP = PROP_ORIENTATION }; static GParamSpec *props[LAST_PROP] = { NULL, }; typedef struct _GtkFlowBoxPrivate GtkFlowBoxPrivate; struct _GtkFlowBoxPrivate { GtkOrientation orientation; gboolean homogeneous; guint row_spacing; guint column_spacing; GtkFlowBoxChild *cursor_child; GtkFlowBoxChild *selected_child; gboolean active_child_active; GtkFlowBoxChild *active_child; GtkSelectionMode selection_mode; GtkAdjustment *hadjustment; GtkAdjustment *vadjustment; gboolean activate_on_single_click; guint16 min_children_per_line; guint16 max_children_per_line; guint16 cur_children_per_line; GSequence *children; GtkCssGadget *gadget; GtkFlowBoxFilterFunc filter_func; gpointer filter_data; GDestroyNotify filter_destroy; GtkFlowBoxSortFunc sort_func; gpointer sort_data; GDestroyNotify sort_destroy; GtkGesture *multipress_gesture; GtkGesture *drag_gesture; GtkFlowBoxChild *rubberband_first; GtkFlowBoxChild *rubberband_last; GtkCssNode *rubberband_node; gboolean rubberband_select; gboolean rubberband_modify; gboolean rubberband_extend; GtkScrollType autoscroll_mode; guint autoscroll_id; GListModel *bound_model; GtkFlowBoxCreateWidgetFunc create_widget_func; gpointer create_widget_func_data; GDestroyNotify create_widget_func_data_destroy; }; #define BOX_PRIV(box) ((GtkFlowBoxPrivate*)gtk_flow_box_get_instance_private ((GtkFlowBox*)(box))) G_DEFINE_TYPE_WITH_CODE (GtkFlowBox, gtk_flow_box, GTK_TYPE_CONTAINER, G_ADD_PRIVATE (GtkFlowBox) G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)) /* Internal API, utilities {{{2 */ #define ORIENTATION_ALIGN(box) \ (BOX_PRIV(box)->orientation == GTK_ORIENTATION_HORIZONTAL \ ? gtk_widget_get_halign (GTK_WIDGET (box)) \ : gtk_widget_get_valign (GTK_WIDGET (box))) #define OPPOSING_ORIENTATION_ALIGN(box) \ (BOX_PRIV(box)->orientation == GTK_ORIENTATION_HORIZONTAL \ ? gtk_widget_get_valign (GTK_WIDGET (box)) \ : gtk_widget_get_halign (GTK_WIDGET (box))) /* Children are visible if they are shown by the app (visible) * and not filtered out (child_visible) by the box */ static inline gboolean child_is_visible (GtkWidget *child) { return gtk_widget_get_visible (child) && gtk_widget_get_child_visible (child); } static gint get_visible_children (GtkFlowBox *box) { GSequenceIter *iter; gint i = 0; for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { GtkWidget *child; child = g_sequence_get (iter); if (child_is_visible (child)) i++; } return i; } static void gtk_flow_box_update_active (GtkFlowBox *box, GtkFlowBoxChild *child) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); gboolean val; val = priv->active_child == child; if (priv->active_child != NULL && val != priv->active_child_active) { priv->active_child_active = val; gtk_widget_queue_draw (GTK_WIDGET (box)); } } static void gtk_flow_box_apply_filter (GtkFlowBox *box, GtkFlowBoxChild *child) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); gboolean do_show; do_show = TRUE; if (priv->filter_func != NULL) do_show = priv->filter_func (child, priv->filter_data); gtk_widget_set_child_visible (GTK_WIDGET (child), do_show); } static void gtk_flow_box_apply_filter_all (GtkFlowBox *box) { GSequenceIter *iter; for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { GtkFlowBoxChild *child; child = g_sequence_get (iter); gtk_flow_box_apply_filter (box, child); } gtk_widget_queue_resize (GTK_WIDGET (box)); } static void gtk_flow_box_apply_sort (GtkFlowBox *box, GtkFlowBoxChild *child) { if (BOX_PRIV (box)->sort_func != NULL) { g_sequence_sort_changed (CHILD_PRIV (child)->iter, (GCompareDataFunc)gtk_flow_box_sort, box); gtk_widget_queue_resize (GTK_WIDGET (box)); } } /* Selection utilities {{{3 */ static gboolean gtk_flow_box_child_set_selected (GtkFlowBoxChild *child, gboolean selected) { if (CHILD_PRIV (child)->selected != selected) { CHILD_PRIV (child)->selected = selected; if (selected) gtk_widget_set_state_flags (GTK_WIDGET (child), GTK_STATE_FLAG_SELECTED, FALSE); else gtk_widget_unset_state_flags (GTK_WIDGET (child), GTK_STATE_FLAG_SELECTED); return TRUE; } return FALSE; } static gboolean gtk_flow_box_unselect_all_internal (GtkFlowBox *box) { GtkFlowBoxChild *child; GSequenceIter *iter; gboolean dirty = FALSE; if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_NONE) return FALSE; for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { child = g_sequence_get (iter); dirty |= gtk_flow_box_child_set_selected (child, FALSE); } return dirty; } static void gtk_flow_box_unselect_child_internal (GtkFlowBox *box, GtkFlowBoxChild *child) { if (!CHILD_PRIV (child)->selected) return; if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_NONE) return; else if (BOX_PRIV (box)->selection_mode != GTK_SELECTION_MULTIPLE) gtk_flow_box_unselect_all_internal (box); else gtk_flow_box_child_set_selected (child, FALSE); g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); } static void gtk_flow_box_update_cursor (GtkFlowBox *box, GtkFlowBoxChild *child) { BOX_PRIV (box)->cursor_child = child; gtk_widget_grab_focus (GTK_WIDGET (child)); gtk_widget_queue_draw (GTK_WIDGET (child)); _gtk_flow_box_accessible_update_cursor (GTK_WIDGET (box), GTK_WIDGET (child)); } static void gtk_flow_box_select_child_internal (GtkFlowBox *box, GtkFlowBoxChild *child) { if (CHILD_PRIV (child)->selected) return; if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_NONE) return; if (BOX_PRIV (box)->selection_mode != GTK_SELECTION_MULTIPLE) gtk_flow_box_unselect_all_internal (box); gtk_flow_box_child_set_selected (child, TRUE); BOX_PRIV (box)->selected_child = child; g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); } static void gtk_flow_box_select_all_between (GtkFlowBox *box, GtkFlowBoxChild *child1, GtkFlowBoxChild *child2, gboolean modify) { GSequenceIter *iter, *iter1, *iter2; if (child1) iter1 = CHILD_PRIV (child1)->iter; else iter1 = g_sequence_get_begin_iter (BOX_PRIV (box)->children); if (child2) iter2 = CHILD_PRIV (child2)->iter; else iter2 = g_sequence_get_end_iter (BOX_PRIV (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)) { GtkWidget *child; child = g_sequence_get (iter); if (child_is_visible (child)) { if (modify) gtk_flow_box_child_set_selected (GTK_FLOW_BOX_CHILD (child), !CHILD_PRIV (child)->selected); else gtk_flow_box_child_set_selected (GTK_FLOW_BOX_CHILD (child), TRUE); } if (g_sequence_iter_compare (iter, iter2) == 0) break; } } static void gtk_flow_box_update_selection (GtkFlowBox *box, GtkFlowBoxChild *child, gboolean modify, gboolean extend) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); gtk_flow_box_update_cursor (box, child); if (priv->selection_mode == GTK_SELECTION_NONE) return; if (priv->selection_mode == GTK_SELECTION_BROWSE) { gtk_flow_box_unselect_all_internal (box); gtk_flow_box_child_set_selected (child, TRUE); priv->selected_child = child; } else if (priv->selection_mode == GTK_SELECTION_SINGLE) { gboolean was_selected; was_selected = CHILD_PRIV (child)->selected; gtk_flow_box_unselect_all_internal (box); gtk_flow_box_child_set_selected (child, modify ? !was_selected : TRUE); priv->selected_child = CHILD_PRIV (child)->selected ? child : NULL; } else /* GTK_SELECTION_MULTIPLE */ { if (extend) { gtk_flow_box_unselect_all_internal (box); if (priv->selected_child == NULL) { gtk_flow_box_child_set_selected (child, TRUE); priv->selected_child = child; } else gtk_flow_box_select_all_between (box, priv->selected_child, child, FALSE); } else { if (modify) { gtk_flow_box_child_set_selected (child, !CHILD_PRIV (child)->selected); } else { gtk_flow_box_unselect_all_internal (box); gtk_flow_box_child_set_selected (child, !CHILD_PRIV (child)->selected); priv->selected_child = child; } } } g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); } static void gtk_flow_box_select_and_activate (GtkFlowBox *box, GtkFlowBoxChild *child) { if (child != NULL) { gtk_flow_box_select_child_internal (box, child); gtk_flow_box_update_cursor (box, child); g_signal_emit (box, signals[CHILD_ACTIVATED], 0, child); } } /* Focus utilities {{{3 */ static GSequenceIter * gtk_flow_box_get_previous_focusable (GtkFlowBox *box, GSequenceIter *iter) { GtkFlowBoxChild *child; while (!g_sequence_iter_is_begin (iter)) { iter = g_sequence_iter_prev (iter); child = g_sequence_get (iter); if (child_is_visible (GTK_WIDGET (child)) && gtk_widget_is_sensitive (GTK_WIDGET (child))) return iter; } return NULL; } static GSequenceIter * gtk_flow_box_get_next_focusable (GtkFlowBox *box, GSequenceIter *iter) { GtkFlowBoxChild *child; while (TRUE) { iter = g_sequence_iter_next (iter); if (g_sequence_iter_is_end (iter)) return NULL; child = g_sequence_get (iter); if (child_is_visible (GTK_WIDGET (child)) && gtk_widget_is_sensitive (GTK_WIDGET (child))) return iter; } return NULL; } static GSequenceIter * gtk_flow_box_get_first_focusable (GtkFlowBox *box) { GSequenceIter *iter; GtkFlowBoxChild *child; iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); if (g_sequence_iter_is_end (iter)) return NULL; child = g_sequence_get (iter); if (child_is_visible (GTK_WIDGET (child)) && gtk_widget_is_sensitive (GTK_WIDGET (child))) return iter; return gtk_flow_box_get_next_focusable (box, iter); } static GSequenceIter * gtk_flow_box_get_last_focusable (GtkFlowBox *box) { GSequenceIter *iter; iter = g_sequence_get_end_iter (BOX_PRIV (box)->children); return gtk_flow_box_get_previous_focusable (box, iter); } static GSequenceIter * gtk_flow_box_get_above_focusable (GtkFlowBox *box, GSequenceIter *iter) { GtkFlowBoxChild *child = NULL; gint i; while (TRUE) { i = 0; while (i < BOX_PRIV (box)->cur_children_per_line) { if (g_sequence_iter_is_begin (iter)) return NULL; iter = g_sequence_iter_prev (iter); child = g_sequence_get (iter); if (child_is_visible (GTK_WIDGET (child))) i++; } if (child && gtk_widget_get_sensitive (GTK_WIDGET (child))) return iter; } return NULL; } static GSequenceIter * gtk_flow_box_get_below_focusable (GtkFlowBox *box, GSequenceIter *iter) { GtkFlowBoxChild *child = NULL; gint i; while (TRUE) { i = 0; while (i < BOX_PRIV (box)->cur_children_per_line) { iter = g_sequence_iter_next (iter); if (g_sequence_iter_is_end (iter)) return NULL; child = g_sequence_get (iter); if (child_is_visible (GTK_WIDGET (child))) i++; } if (child && gtk_widget_get_sensitive (GTK_WIDGET (child))) return iter; } return NULL; } /* GtkWidget implementation {{{2 */ /* Size allocation {{{3 */ /* Used in columned modes where all items share at least their * equal widths or heights */ static void get_max_item_size (GtkFlowBox *box, GtkOrientation orientation, gint *min_size, gint *nat_size) { GSequenceIter *iter; gint max_min_size = 0; gint max_nat_size = 0; for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { GtkWidget *child; gint child_min, child_nat; child = g_sequence_get (iter); if (!child_is_visible (child)) continue; if (orientation == GTK_ORIENTATION_HORIZONTAL) gtk_widget_get_preferred_width (child, &child_min, &child_nat); else gtk_widget_get_preferred_height (child, &child_min, &child_nat); max_min_size = MAX (max_min_size, child_min); max_nat_size = MAX (max_nat_size, child_nat); } if (min_size) *min_size = max_min_size; if (nat_size) *nat_size = max_nat_size; } /* Gets the largest minimum/natural size for a given size (used to get * the largest item heights for a fixed item width and the opposite) */ static void get_largest_size_for_opposing_orientation (GtkFlowBox *box, GtkOrientation orientation, gint item_size, gint *min_item_size, gint *nat_item_size) { GSequenceIter *iter; gint max_min_size = 0; gint max_nat_size = 0; for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { GtkWidget *child; gint child_min, child_nat; child = g_sequence_get (iter); if (!child_is_visible (child)) continue; if (orientation == GTK_ORIENTATION_HORIZONTAL) gtk_widget_get_preferred_height_for_width (child, item_size, &child_min, &child_nat); else gtk_widget_get_preferred_width_for_height (child, item_size, &child_min, &child_nat); max_min_size = MAX (max_min_size, child_min); max_nat_size = MAX (max_nat_size, child_nat); } if (min_item_size) *min_item_size = max_min_size; if (nat_item_size) *nat_item_size = max_nat_size; } /* Gets the largest minimum/natural size on a single line for a given size * (used to get the largest line heights for a fixed item width and the opposite * while iterating over a list of children, note the new index is returned) */ static GSequenceIter * get_largest_size_for_line_in_opposing_orientation (GtkFlowBox *box, GtkOrientation orientation, GSequenceIter *cursor, gint line_length, GtkRequestedSize *item_sizes, gint extra_pixels, gint *min_item_size, gint *nat_item_size) { GSequenceIter *iter; gint max_min_size = 0; gint max_nat_size = 0; gint i; i = 0; for (iter = cursor; !g_sequence_iter_is_end (iter) && i < line_length; iter = g_sequence_iter_next (iter)) { GtkWidget *child; gint child_min, child_nat, this_item_size; child = g_sequence_get (iter); if (!child_is_visible (child)) continue; /* Distribute the extra pixels to the first children in the line * (could be fancier and spread them out more evenly) */ this_item_size = item_sizes[i].minimum_size; if (extra_pixels > 0 && ORIENTATION_ALIGN (box) == GTK_ALIGN_FILL) { this_item_size++; extra_pixels--; } if (orientation == GTK_ORIENTATION_HORIZONTAL) gtk_widget_get_preferred_height_for_width (child, this_item_size, &child_min, &child_nat); else gtk_widget_get_preferred_width_for_height (child, this_item_size, &child_min, &child_nat); max_min_size = MAX (max_min_size, child_min); max_nat_size = MAX (max_nat_size, child_nat); i++; } if (min_item_size) *min_item_size = max_min_size; if (nat_item_size) *nat_item_size = max_nat_size; /* Return next item in the list */ return iter; } /* fit_aligned_item_requests() helper */ static gint gather_aligned_item_requests (GtkFlowBox *box, GtkOrientation orientation, gint line_length, gint item_spacing, gint n_children, GtkRequestedSize *item_sizes) { GSequenceIter *iter; gint i; gint extra_items, natural_line_size = 0; extra_items = n_children % line_length; i = 0; for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { GtkWidget *child; GtkAlign item_align; gint child_min, child_nat; gint position; child = g_sequence_get (iter); if (!child_is_visible (child)) continue; if (orientation == GTK_ORIENTATION_HORIZONTAL) gtk_widget_get_preferred_width (child, &child_min, &child_nat); else gtk_widget_get_preferred_height (child, &child_min, &child_nat); /* Get the index and push it over for the last line when spreading to the end */ position = i % line_length; item_align = ORIENTATION_ALIGN (box); if (item_align == GTK_ALIGN_END && i >= n_children - extra_items) position += line_length - extra_items; /* Round up the size of every column/row */ item_sizes[position].minimum_size = MAX (item_sizes[position].minimum_size, child_min); item_sizes[position].natural_size = MAX (item_sizes[position].natural_size, child_nat); i++; } for (i = 0; i < line_length; i++) natural_line_size += item_sizes[i].natural_size; natural_line_size += (line_length - 1) * item_spacing; return natural_line_size; } static GtkRequestedSize * fit_aligned_item_requests (GtkFlowBox *box, GtkOrientation orientation, gint avail_size, gint item_spacing, gint *line_length, /* in-out */ gint items_per_line, gint n_children) { GtkRequestedSize *sizes, *try_sizes; gint try_line_size, try_length; sizes = g_new0 (GtkRequestedSize, *line_length); /* get the sizes for the initial guess */ try_line_size = gather_aligned_item_requests (box, orientation, *line_length, item_spacing, n_children, sizes); /* Try columnizing the whole thing and adding an item to the end of * the line; try to fit as many columns into the available size as * possible */ for (try_length = *line_length + 1; try_line_size < avail_size; try_length++) { try_sizes = g_new0 (GtkRequestedSize, try_length); try_line_size = gather_aligned_item_requests (box, orientation, try_length, item_spacing, n_children, try_sizes); if (try_line_size <= avail_size && items_per_line >= try_length) { *line_length = try_length; g_free (sizes); sizes = try_sizes; } else { /* oops, this one failed; stick to the last size that fit and then return */ g_free (try_sizes); break; } } return sizes; } typedef struct { GArray *requested; gint extra_pixels; } AllocatedLine; static gint get_offset_pixels (GtkAlign align, gint pixels) { gint offset; switch (align) { case GTK_ALIGN_START: case GTK_ALIGN_FILL: offset = 0; break; case GTK_ALIGN_CENTER: offset = pixels / 2; break; case GTK_ALIGN_END: offset = pixels; break; default: g_assert_not_reached (); break; } return offset; } static void gtk_flow_box_size_allocate (GtkWidget *widget, GtkAllocation *allocation) { GtkAllocation clip; GdkWindow *window; GtkAllocation child_allocation; gtk_widget_set_allocation (widget, allocation); window = gtk_widget_get_window (widget); if (window != NULL) gdk_window_move_resize (window, allocation->x, allocation->y, allocation->width, allocation->height); child_allocation.x = 0; child_allocation.y = 0; child_allocation.width = allocation->width; child_allocation.height = allocation->height; gtk_css_gadget_allocate (BOX_PRIV (widget)->gadget, &child_allocation, gtk_widget_get_allocated_baseline (widget), &clip); _gtk_widget_set_simple_clip (widget, &clip); } static void gtk_flow_box_allocate (GtkCssGadget *gadget, const GtkAllocation *allocation, int baseline, GtkAllocation *out_clip, gpointer unused) { GtkWidget *widget = gtk_css_gadget_get_owner (gadget); GtkFlowBox *box = GTK_FLOW_BOX (widget); GtkFlowBoxPrivate *priv = BOX_PRIV (box); GtkAllocation child_allocation; gint avail_size, avail_other_size, min_items, item_spacing, line_spacing; GtkAlign item_align; GtkAlign line_align; GtkRequestedSize *line_sizes = NULL; GtkRequestedSize *item_sizes = NULL; gint min_item_size, nat_item_size; gint line_length; gint item_size = 0; gint line_size = 0, min_fixed_line_size = 0, nat_fixed_line_size = 0; gint line_offset, item_offset, n_children, n_lines, line_count; gint extra_pixels = 0, extra_per_item = 0, extra_extra = 0; gint extra_line_pixels = 0, extra_per_line = 0, extra_line_extra = 0; gint i, this_line_size; GSequenceIter *iter; min_items = MAX (1, priv->min_children_per_line); if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) { avail_size = allocation->width; avail_other_size = allocation->height; item_spacing = priv->column_spacing; line_spacing = priv->row_spacing; } else /* GTK_ORIENTATION_VERTICAL */ { avail_size = allocation->height; avail_other_size = allocation->width; item_spacing = priv->row_spacing; line_spacing = priv->column_spacing; } item_align = ORIENTATION_ALIGN (box); line_align = OPPOSING_ORIENTATION_ALIGN (box); /* Get how many lines we'll be needing to flow */ n_children = get_visible_children (box); if (n_children <= 0) return; /* Deal with ALIGNED/HOMOGENEOUS modes first, start with * initial guesses at item/line sizes */ get_max_item_size (box, priv->orientation, &min_item_size, &nat_item_size); if (nat_item_size <= 0) return; /* By default flow at the natural item width */ line_length = avail_size / (nat_item_size + item_spacing); /* After the above aproximation, check if we cant fit one more on the line */ if (line_length * item_spacing + (line_length + 1) * nat_item_size <= avail_size) line_length++; /* Its possible we were allocated just less than the natural width of the * minimum item flow length */ line_length = MAX (min_items, line_length); line_length = MIN (line_length, priv->max_children_per_line); /* Here we just use the largest height-for-width and use that for the height * of all lines */ if (priv->homogeneous) { n_lines = n_children / line_length; if ((n_children % line_length) > 0) n_lines++; n_lines = MAX (n_lines, 1); /* Now we need the real item allocation size */ item_size = (avail_size - (line_length - 1) * item_spacing) / line_length; /* Cut out the expand space if we're not distributing any */ if (item_align != GTK_ALIGN_FILL) item_size = MIN (item_size, nat_item_size); get_largest_size_for_opposing_orientation (box, priv->orientation, item_size, &min_fixed_line_size, &nat_fixed_line_size); /* resolve a fixed 'line_size' */ line_size = (avail_other_size - (n_lines - 1) * line_spacing) / n_lines; if (line_align != GTK_ALIGN_FILL) line_size = MIN (line_size, nat_fixed_line_size); /* Get the real extra pixels incase of GTK_ALIGN_START lines */ extra_pixels = avail_size - (line_length - 1) * item_spacing - item_size * line_length; extra_line_pixels = avail_other_size - (n_lines - 1) * line_spacing - line_size * n_lines; } else { gboolean first_line = TRUE; /* Find the amount of columns that can fit aligned into the available space * and collect their requests. */ item_sizes = fit_aligned_item_requests (box, priv->orientation, avail_size, item_spacing, &line_length, priv->max_children_per_line, n_children); /* Calculate the number of lines after determining the final line_length */ n_lines = n_children / line_length; if ((n_children % line_length) > 0) n_lines++; n_lines = MAX (n_lines, 1); line_sizes = g_new0 (GtkRequestedSize, n_lines); /* Get the available remaining size */ avail_size -= (line_length - 1) * item_spacing; for (i = 0; i < line_length; i++) avail_size -= item_sizes[i].minimum_size; /* Perform a natural allocation on the columnized items and get the remaining pixels */ if (avail_size > 0) extra_pixels = gtk_distribute_natural_allocation (avail_size, line_length, item_sizes); /* Now that we have the size of each column of items find the size of each individual * line based on the aligned item sizes. */ for (i = 0, iter = g_sequence_get_begin_iter (priv->children); !g_sequence_iter_is_end (iter) && i < n_lines; i++) { iter = get_largest_size_for_line_in_opposing_orientation (box, priv->orientation, iter, line_length, item_sizes, extra_pixels, &line_sizes[i].minimum_size, &line_sizes[i].natural_size); /* Its possible a line is made of completely invisible children */ if (line_sizes[i].natural_size > 0) { if (first_line) first_line = FALSE; else avail_other_size -= line_spacing; avail_other_size -= line_sizes[i].minimum_size; line_sizes[i].data = GINT_TO_POINTER (i); } } /* Distribute space among lines naturally */ if (avail_other_size > 0) extra_line_pixels = gtk_distribute_natural_allocation (avail_other_size, n_lines, line_sizes); } /* * Initial sizes of items/lines guessed at this point, * go on to distribute expand space if needed. */ priv->cur_children_per_line = line_length; /* FIXME: This portion needs to consider which columns * and rows asked for expand space and distribute those * accordingly for the case of ALIGNED allocation. * * If at least one child in a column/row asked for expand; * we should make that row/column expand entirely. */ /* Calculate expand space per item */ if (item_align == GTK_ALIGN_FILL) { extra_per_item = extra_pixels / line_length; extra_extra = extra_pixels % line_length; } /* Calculate expand space per line */ if (line_align == GTK_ALIGN_FILL) { extra_per_line = extra_line_pixels / n_lines; extra_line_extra = extra_line_pixels % n_lines; } /* * Prepare item/line initial offsets and jump into the * real allocation loop. */ line_offset = allocation->y; item_offset = allocation->x; /* prepend extra space to item_offset/line_offset for SPREAD_END */ item_offset += get_offset_pixels (item_align, extra_pixels); line_offset += get_offset_pixels (line_align, extra_line_pixels); /* Get the allocation size for the first line */ if (priv->homogeneous) this_line_size = line_size; else { this_line_size = line_sizes[0].minimum_size; if (line_align == GTK_ALIGN_FILL) { this_line_size += extra_per_line; if (extra_line_extra > 0) this_line_size++; } } i = 0; line_count = 0; for (iter = g_sequence_get_begin_iter (priv->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { GtkWidget *child; gint position; gint this_item_size; child = g_sequence_get (iter); if (!child_is_visible (child)) continue; /* Get item position */ position = i % line_length; /* adjust the line_offset/count at the beginning of each new line */ if (i > 0 && position == 0) { /* Push the line_offset */ line_offset += this_line_size + line_spacing; line_count++; /* Get the new line size */ if (priv->homogeneous) this_line_size = line_size; else { this_line_size = line_sizes[line_count].minimum_size; if (line_align == GTK_ALIGN_FILL) { this_line_size += extra_per_line; if (line_count < extra_line_extra) this_line_size++; } } item_offset = allocation->x; if (item_align == GTK_ALIGN_CENTER) { item_offset += get_offset_pixels (item_align, extra_pixels); } else if (item_align == GTK_ALIGN_END) { item_offset += get_offset_pixels (item_align, extra_pixels); /* If we're on the last line, prepend the space for * any leading items */ if (line_count == n_lines -1) { gint extra_items = n_children % line_length; if (priv->homogeneous) { item_offset += item_size * (line_length - extra_items); item_offset += item_spacing * (line_length - extra_items); } else { gint j; for (j = 0; j < (line_length - extra_items); j++) { item_offset += item_sizes[j].minimum_size; item_offset += item_spacing; } } } } } /* Push the index along for the last line when spreading to the end */ if (item_align == GTK_ALIGN_END && line_count == n_lines -1) { gint extra_items = n_children % line_length; position += line_length - extra_items; } if (priv->homogeneous) this_item_size = item_size; else this_item_size = item_sizes[position].minimum_size; if (item_align == GTK_ALIGN_FILL) { this_item_size += extra_per_item; if (position < extra_extra) this_item_size++; } /* Do the actual allocation */ if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) { child_allocation.x = item_offset; child_allocation.y = line_offset; child_allocation.width = this_item_size; child_allocation.height = this_line_size; } else /* GTK_ORIENTATION_VERTICAL */ { child_allocation.x = line_offset; child_allocation.y = item_offset; child_allocation.width = this_line_size; child_allocation.height = this_item_size; } if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL) child_allocation.x = allocation->width - child_allocation.x - child_allocation.width; gtk_widget_size_allocate (child, &child_allocation); item_offset += this_item_size; item_offset += item_spacing; i++; } g_free (item_sizes); g_free (line_sizes); gtk_container_get_children_clip (GTK_CONTAINER (widget), out_clip); } static GtkSizeRequestMode gtk_flow_box_get_request_mode (GtkWidget *widget) { GtkFlowBox *box = GTK_FLOW_BOX (widget); return (BOX_PRIV (box)->orientation == GTK_ORIENTATION_HORIZONTAL) ? GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH : GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT; } /* Gets the largest minimum and natural length of * 'line_length' consecutive items when aligned into rows/columns */ static void get_largest_aligned_line_length (GtkFlowBox *box, GtkOrientation orientation, gint line_length, gint *min_size, gint *nat_size) { GSequenceIter *iter; gint max_min_size = 0; gint max_nat_size = 0; gint spacing, i; GtkRequestedSize *aligned_item_sizes; if (orientation == GTK_ORIENTATION_HORIZONTAL) spacing = BOX_PRIV (box)->column_spacing; else spacing = BOX_PRIV (box)->row_spacing; aligned_item_sizes = g_new0 (GtkRequestedSize, line_length); /* Get the largest sizes of each index in the line. */ i = 0; for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { GtkWidget *child; gint child_min, child_nat; child = g_sequence_get (iter); if (!child_is_visible (child)) continue; if (orientation == GTK_ORIENTATION_HORIZONTAL) gtk_widget_get_preferred_width (child, &child_min, &child_nat); else /* GTK_ORIENTATION_VERTICAL */ gtk_widget_get_preferred_height (child, &child_min, &child_nat); aligned_item_sizes[i % line_length].minimum_size = MAX (aligned_item_sizes[i % line_length].minimum_size, child_min); aligned_item_sizes[i % line_length].natural_size = MAX (aligned_item_sizes[i % line_length].natural_size, child_nat); i++; } /* Add up the largest indexes */ for (i = 0; i < line_length; i++) { max_min_size += aligned_item_sizes[i].minimum_size; max_nat_size += aligned_item_sizes[i].natural_size; } g_free (aligned_item_sizes); max_min_size += (line_length - 1) * spacing; max_nat_size += (line_length - 1) * spacing; if (min_size) *min_size = max_min_size; if (nat_size) *nat_size = max_nat_size; } static void gtk_flow_box_get_preferred_width (GtkWidget *widget, gint *minimum, gint *natural) { gtk_css_gadget_get_preferred_size (BOX_PRIV (widget)->gadget, GTK_ORIENTATION_HORIZONTAL, -1, minimum, natural, NULL, NULL); } static void gtk_flow_box_get_preferred_height (GtkWidget *widget, gint *minimum, gint *natural) { gtk_css_gadget_get_preferred_size (BOX_PRIV (widget)->gadget, GTK_ORIENTATION_VERTICAL, -1, minimum, natural, NULL, NULL); } static void gtk_flow_box_get_preferred_height_for_width (GtkWidget *widget, gint width, gint *minimum, gint *natural) { gtk_css_gadget_get_preferred_size (BOX_PRIV (widget)->gadget, GTK_ORIENTATION_VERTICAL, width, minimum, natural, NULL, NULL); } static void gtk_flow_box_get_preferred_width_for_height (GtkWidget *widget, gint height, gint *minimum, gint *natural) { gtk_css_gadget_get_preferred_size (BOX_PRIV (widget)->gadget, GTK_ORIENTATION_HORIZONTAL, height, minimum, natural, NULL, NULL); } static void gtk_flow_box_measure (GtkCssGadget *gadget, GtkOrientation orientation, int for_size, int *minimum, int *natural, int *minimum_baseline, int *natural_baseline, gpointer data) { GtkWidget *widget = gtk_css_gadget_get_owner (gadget); GtkFlowBox *box = GTK_FLOW_BOX (widget); GtkFlowBoxPrivate *priv = BOX_PRIV (box); if (orientation == GTK_ORIENTATION_HORIZONTAL) { if (for_size < 0) { gint min_item_width, nat_item_width; gint min_items, nat_items; gint min_width, nat_width; min_items = MAX (1, priv->min_children_per_line); nat_items = MAX (min_items, priv->max_children_per_line); if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) { min_width = nat_width = 0; if (!priv->homogeneous) { /* When not homogeneous; horizontally oriented boxes * need enough width for the widest row */ if (min_items == 1) { get_max_item_size (box, GTK_ORIENTATION_HORIZONTAL, &min_item_width, &nat_item_width); min_width += min_item_width; nat_width += nat_item_width; } else { gint min_line_length, nat_line_length; get_largest_aligned_line_length (box, GTK_ORIENTATION_HORIZONTAL, min_items, &min_line_length, &nat_line_length); if (nat_items > min_items) get_largest_aligned_line_length (box, GTK_ORIENTATION_HORIZONTAL, nat_items, NULL, &nat_line_length); min_width += min_line_length; nat_width += nat_line_length; } } else /* In homogeneous mode; horizontally oriented boxs * give the same width to all children */ { get_max_item_size (box, GTK_ORIENTATION_HORIZONTAL, &min_item_width, &nat_item_width); min_width += min_item_width * min_items; min_width += (min_items -1) * priv->column_spacing; nat_width += nat_item_width * nat_items; nat_width += (nat_items -1) * priv->column_spacing; } } else /* GTK_ORIENTATION_VERTICAL */ { /* Return the width for the minimum height */ gint min_height; gtk_css_gadget_get_preferred_size (gadget, GTK_ORIENTATION_VERTICAL, -1, &min_height, NULL, NULL, NULL); gtk_css_gadget_get_preferred_size (gadget, GTK_ORIENTATION_HORIZONTAL, min_height, &min_width, &nat_width, NULL, NULL); } *minimum = min_width; *natural = nat_width; } else { gint min_item_height, nat_item_height; gint min_items; gint min_width, nat_width; gint avail_size, n_children; min_items = MAX (1, priv->min_children_per_line); min_width = 0; nat_width = 0; if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) { /* Return the minimum width */ gtk_css_gadget_get_preferred_size (gadget, GTK_ORIENTATION_HORIZONTAL, -1, &min_width, &nat_width, NULL, NULL); } else /* GTK_ORIENTATION_VERTICAL */ { gint min_height; gint line_length; gint item_size, extra_pixels; n_children = get_visible_children (box); if (n_children <= 0) goto out_width; /* Make sure its no smaller than the minimum */ gtk_css_gadget_get_preferred_size (gadget, GTK_ORIENTATION_VERTICAL, -1, &min_height, NULL, NULL, NULL); avail_size = MAX (for_size, min_height); if (avail_size <= 0) goto out_width; get_max_item_size (box, GTK_ORIENTATION_VERTICAL, &min_item_height, &nat_item_height); /* By default flow at the natural item width */ line_length = avail_size / (nat_item_height + priv->row_spacing); /* After the above aproximation, check if we cant fit one more on the line */ if (line_length * priv->row_spacing + (line_length + 1) * nat_item_height <= avail_size) line_length++; /* Its possible we were allocated just less than the natural width of the * minimum item flow length */ line_length = MAX (min_items, line_length); line_length = MIN (line_length, priv->max_children_per_line); /* Now we need the real item allocation size */ item_size = (avail_size - (line_length - 1) * priv->row_spacing) / line_length; /* Cut out the expand space if we're not distributing any */ if (gtk_widget_get_valign (widget) != GTK_ALIGN_FILL) { item_size = MIN (item_size, nat_item_height); extra_pixels = 0; } else /* Collect the extra pixels for expand children */ extra_pixels = (avail_size - (line_length - 1) * priv->row_spacing) % line_length; if (priv->homogeneous) { gint min_item_width, nat_item_width; gint lines; /* Here we just use the largest height-for-width and * add up the size accordingly */ get_largest_size_for_opposing_orientation (box, GTK_ORIENTATION_VERTICAL, item_size, &min_item_width, &nat_item_width); /* Round up how many lines we need to allocate for */ n_children = get_visible_children (box); lines = n_children / line_length; if ((n_children % line_length) > 0) lines++; min_width = min_item_width * lines; nat_width = nat_item_width * lines; min_width += (lines - 1) * priv->column_spacing; nat_width += (lines - 1) * priv->column_spacing; } else { gint min_line_width, nat_line_width, i; gboolean first_line = TRUE; GtkRequestedSize *item_sizes; GSequenceIter *iter; /* First get the size each set of items take to span the line * when aligning the items above and below after flowping. */ item_sizes = fit_aligned_item_requests (box, priv->orientation, avail_size, priv->row_spacing, &line_length, priv->max_children_per_line, n_children); /* Get the available remaining size */ avail_size -= (line_length - 1) * priv->column_spacing; for (i = 0; i < line_length; i++) avail_size -= item_sizes[i].minimum_size; if (avail_size > 0) extra_pixels = gtk_distribute_natural_allocation (avail_size, line_length, item_sizes); for (iter = g_sequence_get_begin_iter (priv->children); !g_sequence_iter_is_end (iter);) { iter = get_largest_size_for_line_in_opposing_orientation (box, GTK_ORIENTATION_VERTICAL, iter, line_length, item_sizes, extra_pixels, &min_line_width, &nat_line_width); /* Its possible the last line only had invisible widgets */ if (nat_line_width > 0) { if (first_line) first_line = FALSE; else { min_width += priv->column_spacing; nat_width += priv->column_spacing; } min_width += min_line_width; nat_width += nat_line_width; } } g_free (item_sizes); } } out_width: *minimum = min_width; *natural = nat_width; } } else { if (for_size < 0) { gint min_item_height, nat_item_height; gint min_items, nat_items; gint min_height, nat_height; min_items = MAX (1, priv->min_children_per_line); nat_items = MAX (min_items, priv->max_children_per_line); if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) { /* Return the height for the minimum width */ gint min_width; gtk_css_gadget_get_preferred_size (gadget, GTK_ORIENTATION_HORIZONTAL, -1, &min_width, NULL, NULL, NULL); gtk_css_gadget_get_preferred_size (gadget, GTK_ORIENTATION_VERTICAL, min_width, &min_height, &nat_height, NULL, NULL); } else /* GTK_ORIENTATION_VERTICAL */ { min_height = nat_height = 0; if (! priv->homogeneous) { /* When not homogeneous; vertically oriented boxes * need enough height for the tallest column */ if (min_items == 1) { get_max_item_size (box, GTK_ORIENTATION_VERTICAL, &min_item_height, &nat_item_height); min_height += min_item_height; nat_height += nat_item_height; } else { gint min_line_length, nat_line_length; get_largest_aligned_line_length (box, GTK_ORIENTATION_VERTICAL, min_items, &min_line_length, &nat_line_length); if (nat_items > min_items) get_largest_aligned_line_length (box, GTK_ORIENTATION_VERTICAL, nat_items, NULL, &nat_line_length); min_height += min_line_length; nat_height += nat_line_length; } } else { /* In homogeneous mode; vertically oriented boxes * give the same height to all children */ get_max_item_size (box, GTK_ORIENTATION_VERTICAL, &min_item_height, &nat_item_height); min_height += min_item_height * min_items; min_height += (min_items -1) * priv->row_spacing; nat_height += nat_item_height * nat_items; nat_height += (nat_items -1) * priv->row_spacing; } } *minimum = min_height; *natural = nat_height; } else { gint min_item_width, nat_item_width; gint min_items; gint min_height, nat_height; gint avail_size, n_children; min_items = MAX (1, priv->min_children_per_line); min_height = 0; nat_height = 0; if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) { gint min_width; gint line_length; gint item_size, extra_pixels; n_children = get_visible_children (box); if (n_children <= 0) goto out_height; /* Make sure its no smaller than the minimum */ gtk_css_gadget_get_preferred_size (gadget, GTK_ORIENTATION_HORIZONTAL, -1, &min_width, NULL, NULL, NULL); avail_size = MAX (for_size, min_width); if (avail_size <= 0) goto out_height; get_max_item_size (box, GTK_ORIENTATION_HORIZONTAL, &min_item_width, &nat_item_width); if (nat_item_width <= 0) goto out_height; /* By default flow at the natural item width */ line_length = avail_size / (nat_item_width + priv->column_spacing); /* After the above aproximation, check if we cant fit one more on the line */ if (line_length * priv->column_spacing + (line_length + 1) * nat_item_width <= avail_size) line_length++; /* Its possible we were allocated just less than the natural width of the * minimum item flow length */ line_length = MAX (min_items, line_length); line_length = MIN (line_length, priv->max_children_per_line); /* Now we need the real item allocation size */ item_size = (avail_size - (line_length - 1) * priv->column_spacing) / line_length; /* Cut out the expand space if we're not distributing any */ if (gtk_widget_get_halign (widget) != GTK_ALIGN_FILL) { item_size = MIN (item_size, nat_item_width); extra_pixels = 0; } else /* Collect the extra pixels for expand children */ extra_pixels = (avail_size - (line_length - 1) * priv->column_spacing) % line_length; if (priv->homogeneous) { gint min_item_height, nat_item_height; gint lines; /* Here we just use the largest height-for-width and * add up the size accordingly */ get_largest_size_for_opposing_orientation (box, GTK_ORIENTATION_HORIZONTAL, item_size, &min_item_height, &nat_item_height); /* Round up how many lines we need to allocate for */ lines = n_children / line_length; if ((n_children % line_length) > 0) lines++; min_height = min_item_height * lines; nat_height = nat_item_height * lines; min_height += (lines - 1) * priv->row_spacing; nat_height += (lines - 1) * priv->row_spacing; } else { gint min_line_height, nat_line_height, i; gboolean first_line = TRUE; GtkRequestedSize *item_sizes; GSequenceIter *iter; /* First get the size each set of items take to span the line * when aligning the items above and below after flowping. */ item_sizes = fit_aligned_item_requests (box, priv->orientation, avail_size, priv->column_spacing, &line_length, priv->max_children_per_line, n_children); /* Get the available remaining size */ avail_size -= (line_length - 1) * priv->column_spacing; for (i = 0; i < line_length; i++) avail_size -= item_sizes[i].minimum_size; if (avail_size > 0) extra_pixels = gtk_distribute_natural_allocation (avail_size, line_length, item_sizes); for (iter = g_sequence_get_begin_iter (priv->children); !g_sequence_iter_is_end (iter);) { iter = get_largest_size_for_line_in_opposing_orientation (box, GTK_ORIENTATION_HORIZONTAL, iter, line_length, item_sizes, extra_pixels, &min_line_height, &nat_line_height); /* Its possible the line only had invisible widgets */ if (nat_line_height > 0) { if (first_line) first_line = FALSE; else { min_height += priv->row_spacing; nat_height += priv->row_spacing; } min_height += min_line_height; nat_height += nat_line_height; } } g_free (item_sizes); } } else /* GTK_ORIENTATION_VERTICAL */ { /* Return the minimum height */ gtk_css_gadget_get_preferred_size (gadget, GTK_ORIENTATION_VERTICAL, -1, &min_height, &nat_height, NULL, NULL); } out_height: *minimum = min_height; *natural = nat_height; } } } /* Drawing {{{3 */ static gboolean gtk_flow_box_draw (GtkWidget *widget, cairo_t *cr) { gtk_css_gadget_draw (BOX_PRIV (widget)->gadget, cr); return FALSE; } static gboolean gtk_flow_box_render (GtkCssGadget *gadget, cairo_t *cr, int x, int y, int width, int height, gpointer data) { GtkWidget *widget = gtk_css_gadget_get_owner (gadget); GtkFlowBox *box = GTK_FLOW_BOX (widget); GtkFlowBoxPrivate *priv = BOX_PRIV (box); GTK_WIDGET_CLASS (gtk_flow_box_parent_class)->draw (widget, cr); if (priv->rubberband_first && priv->rubberband_last) { GtkStyleContext *context; GSequenceIter *iter, *iter1, *iter2; GdkRectangle line_rect, rect; GArray *lines; gboolean vertical; context = gtk_widget_get_style_context (GTK_WIDGET (box)); vertical = priv->orientation == GTK_ORIENTATION_VERTICAL; cairo_save (cr); context = gtk_widget_get_style_context (widget); gtk_style_context_save_to_node (context, priv->rubberband_node); iter1 = CHILD_PRIV (priv->rubberband_first)->iter; iter2 = CHILD_PRIV (priv->rubberband_last)->iter; if (g_sequence_iter_compare (iter2, iter1) < 0) { iter = iter1; iter1 = iter2; iter2 = iter; } line_rect.width = 0; lines = g_array_new (FALSE, FALSE, sizeof (GdkRectangle)); for (iter = iter1; !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { GtkWidget *child; child = g_sequence_get (iter); gtk_widget_get_allocation (GTK_WIDGET (child), &rect); if (line_rect.width == 0) line_rect = rect; else { if ((vertical && rect.x == line_rect.x) || (!vertical && rect.y == line_rect.y)) gdk_rectangle_union (&rect, &line_rect, &line_rect); else { g_array_append_val (lines, line_rect); line_rect = rect; } } if (g_sequence_iter_compare (iter, iter2) == 0) break; } if (line_rect.width != 0) g_array_append_val (lines, line_rect); if (lines->len > 0) { GtkStateFlags state; cairo_path_t *path; GtkBorder border; GdkRGBA border_color; if (vertical) path_from_vertical_line_rects (cr, (GdkRectangle *)lines->data, lines->len); else path_from_horizontal_line_rects (cr, (GdkRectangle *)lines->data, lines->len); /* For some reason we need to copy and reapply the path, * or it gets eaten by gtk_render_background() */ path = cairo_copy_path (cr); cairo_save (cr); cairo_clip (cr); gtk_render_background (context, cr, x, y, width, height); cairo_restore (cr); cairo_append_path (cr, path); cairo_path_destroy (path); state = gtk_style_context_get_state (context); G_GNUC_BEGIN_IGNORE_DEPRECATIONS gtk_style_context_get_border_color (context, state, &border_color); G_GNUC_END_IGNORE_DEPRECATIONS gtk_style_context_get_border (context, state, &border); cairo_set_line_width (cr, border.left); gdk_cairo_set_source_rgba (cr, &border_color); cairo_stroke (cr); } g_array_free (lines, TRUE); gtk_style_context_restore (context); cairo_restore (cr); } return gtk_widget_has_visible_focus (widget); } /* Autoscrolling {{{3 */ static void remove_autoscroll (GtkFlowBox *box) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); if (priv->autoscroll_id) { gtk_widget_remove_tick_callback (GTK_WIDGET (box), priv->autoscroll_id); priv->autoscroll_id = 0; } priv->autoscroll_mode = GTK_SCROLL_NONE; } static gboolean autoscroll_cb (GtkWidget *widget, GdkFrameClock *frame_clock, gpointer data) { GtkFlowBox *box = GTK_FLOW_BOX (data); GtkFlowBoxPrivate *priv = BOX_PRIV (box); GtkAdjustment *adjustment; gdouble factor; gdouble increment; gdouble value; if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) adjustment = priv->vadjustment; else adjustment = priv->hadjustment; switch (priv->autoscroll_mode) { case GTK_SCROLL_STEP_FORWARD: factor = AUTOSCROLL_FACTOR; break; case GTK_SCROLL_STEP_BACKWARD: factor = - AUTOSCROLL_FACTOR; break; case GTK_SCROLL_PAGE_FORWARD: factor = AUTOSCROLL_FACTOR_FAST; break; case GTK_SCROLL_PAGE_BACKWARD: factor = - AUTOSCROLL_FACTOR_FAST; break; default: g_assert_not_reached (); } increment = gtk_adjustment_get_step_increment (adjustment) / factor; value = gtk_adjustment_get_value (adjustment); value += increment; gtk_adjustment_set_value (adjustment, value); if (priv->rubberband_select) { GdkEventSequence *sequence; gdouble x, y; GtkFlowBoxChild *child; sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (priv->drag_gesture)); gtk_gesture_get_point (priv->drag_gesture, sequence, &x, &y); child = gtk_flow_box_get_child_at_pos (box, x, y); gtk_flow_box_update_active (box, child); if (child != NULL) priv->rubberband_last = child; } return G_SOURCE_CONTINUE; } static void add_autoscroll (GtkFlowBox *box) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); if (priv->autoscroll_id != 0 || priv->autoscroll_mode == GTK_SCROLL_NONE) return; priv->autoscroll_id = gtk_widget_add_tick_callback (GTK_WIDGET (box), autoscroll_cb, box, NULL); } static gboolean get_view_rect (GtkFlowBox *box, GdkRectangle *rect) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); GtkWidget *parent; GdkWindow *view; parent = gtk_widget_get_parent (GTK_WIDGET (box)); if (GTK_IS_VIEWPORT (parent)) { view = gtk_viewport_get_view_window (GTK_VIEWPORT (parent)); rect->x = rect->y = 0; rect->x = gtk_adjustment_get_value (priv->hadjustment); rect->y = gtk_adjustment_get_value (priv->vadjustment); rect->width = gdk_window_get_width (view); rect->height = gdk_window_get_height (view); return TRUE; } return FALSE; } static void update_autoscroll_mode (GtkFlowBox *box, gint x, gint y) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); GtkScrollType mode = GTK_SCROLL_NONE; GdkRectangle rect; gint size, pos; if (priv->rubberband_select && get_view_rect (box, &rect)) { if (priv->orientation == GTK_ORIENTATION_VERTICAL) { size = rect.width; pos = x - rect.x; } else { size = rect.height; pos = y - rect.y; } if (pos < 0 - AUTOSCROLL_FAST_DISTANCE) mode = GTK_SCROLL_PAGE_BACKWARD; else if (pos > size + AUTOSCROLL_FAST_DISTANCE) mode = GTK_SCROLL_PAGE_FORWARD; else if (pos < 0) mode = GTK_SCROLL_STEP_BACKWARD; else if (pos > size) mode = GTK_SCROLL_STEP_FORWARD; } if (mode != priv->autoscroll_mode) { remove_autoscroll (box); priv->autoscroll_mode = mode; add_autoscroll (box); } } /* Event handling {{{3 */ static gboolean gtk_flow_box_enter_notify_event (GtkWidget *widget, GdkEventCrossing *event) { GtkFlowBox *box = GTK_FLOW_BOX (widget); GtkFlowBoxChild *child; if (event->window != gtk_widget_get_window (GTK_WIDGET (box))) return FALSE; child = gtk_flow_box_get_child_at_pos (box, event->x, event->y); gtk_flow_box_update_active (box, child); return FALSE; } static gboolean gtk_flow_box_leave_notify_event (GtkWidget *widget, GdkEventCrossing *event) { GtkFlowBox *box = GTK_FLOW_BOX (widget); GtkFlowBoxChild *child = NULL; if (event->window != gtk_widget_get_window (GTK_WIDGET (box))) return FALSE; if (event->detail != GDK_NOTIFY_INFERIOR) child = NULL; else child = gtk_flow_box_get_child_at_pos (box, event->x, event->y); gtk_flow_box_update_active (box, child); return FALSE; } static void gtk_flow_box_drag_gesture_update (GtkGestureDrag *gesture, gdouble offset_x, gdouble offset_y, GtkFlowBox *box) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); gdouble start_x, start_y; GtkFlowBoxChild *child; GtkCssNode *widget_node; gtk_gesture_drag_get_start_point (gesture, &start_x, &start_y); if (!priv->rubberband_select && (offset_x * offset_x) + (offset_y * offset_y) > RUBBERBAND_START_DISTANCE * RUBBERBAND_START_DISTANCE) { priv->rubberband_select = TRUE; priv->rubberband_first = gtk_flow_box_get_child_at_pos (box, start_x, start_y); widget_node = gtk_widget_get_css_node (GTK_WIDGET (box)); priv->rubberband_node = gtk_css_node_new (); gtk_css_node_set_name (priv->rubberband_node, I_("rubberband")); gtk_css_node_set_parent (priv->rubberband_node, widget_node); gtk_css_node_set_state (priv->rubberband_node, gtk_css_node_get_state (widget_node)); g_object_unref (priv->rubberband_node); /* Grab focus here, so Escape-to-stop-rubberband works */ if (priv->rubberband_first) gtk_flow_box_update_cursor (box, priv->rubberband_first); gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED); } if (priv->rubberband_select) { child = gtk_flow_box_get_child_at_pos (box, start_x + offset_x, start_y + offset_y); if (priv->rubberband_first == NULL) { priv->rubberband_first = child; if (priv->rubberband_first) gtk_flow_box_update_cursor (box, priv->rubberband_first); } if (child != NULL) priv->rubberband_last = child; update_autoscroll_mode (box, start_x + offset_x, start_y + offset_y); gtk_widget_queue_draw (GTK_WIDGET (box)); } } static gboolean gtk_flow_box_motion_notify_event (GtkWidget *widget, GdkEventMotion *event) { GtkFlowBox *box = GTK_FLOW_BOX (widget); GtkFlowBoxChild *child; GdkWindow *window; GdkWindow *event_window; gint relative_x; gint relative_y; gdouble parent_x; gdouble parent_y; window = gtk_widget_get_window (GTK_WIDGET (box)); event_window = event->window; relative_x = event->x; relative_y = event->y; while ((event_window != NULL) && (event_window != window)) { gdk_window_coords_to_parent (event_window, relative_x, relative_y, &parent_x, &parent_y); relative_x = parent_x; relative_y = parent_y; event_window = gdk_window_get_effective_parent (event_window); } child = gtk_flow_box_get_child_at_pos (box, relative_x, relative_y); gtk_flow_box_update_active (box, child); return GTK_WIDGET_CLASS (gtk_flow_box_parent_class)->motion_notify_event (widget, event); } static void gtk_flow_box_multipress_gesture_pressed (GtkGestureMultiPress *gesture, guint n_press, gdouble x, gdouble y, GtkFlowBox *box) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); GtkFlowBoxChild *child; child = gtk_flow_box_get_child_at_pos (box, x, y); if (child == NULL) return; /* The drag gesture is only triggered by first press */ if (n_press != 1) gtk_gesture_set_state (priv->drag_gesture, GTK_EVENT_SEQUENCE_DENIED); priv->active_child = child; priv->active_child_active = TRUE; gtk_widget_queue_draw (GTK_WIDGET (box)); if (n_press == 2 && !priv->activate_on_single_click) { gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED); g_signal_emit (box, signals[CHILD_ACTIVATED], 0, child); } } static void gtk_flow_box_multipress_gesture_released (GtkGestureMultiPress *gesture, guint n_press, gdouble x, gdouble y, GtkFlowBox *box) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); if (priv->active_child != NULL && priv->active_child_active) { gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED); if (priv->activate_on_single_click) gtk_flow_box_select_and_activate (box, priv->active_child); else { GdkEventSequence *sequence; GdkInputSource source; const GdkEvent *event; gboolean modify; gboolean extend; get_current_selection_modifiers (GTK_WIDGET (box), &modify, &extend); /* 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); source = gdk_device_get_source (gdk_event_get_source_device (event)); if (source == GDK_SOURCE_TOUCHSCREEN) modify = !modify; gtk_flow_box_update_selection (box, priv->active_child, modify, extend); } } } static void gtk_flow_box_multipress_gesture_stopped (GtkGestureMultiPress *gesture, GtkFlowBox *box) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); priv->active_child = NULL; priv->active_child_active = FALSE; gtk_widget_queue_draw (GTK_WIDGET (box)); } static void gtk_flow_box_drag_gesture_begin (GtkGestureDrag *gesture, gdouble start_x, gdouble start_y, GtkWidget *widget) { GtkFlowBoxPrivate *priv = BOX_PRIV (widget); if (priv->selection_mode != GTK_SELECTION_MULTIPLE) { gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED); return; } priv->rubberband_select = FALSE; priv->rubberband_first = NULL; priv->rubberband_last = NULL; get_current_selection_modifiers (widget, &priv->rubberband_modify, &priv->rubberband_extend); } static void gtk_flow_box_stop_rubberband (GtkFlowBox *box) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); priv->rubberband_select = FALSE; priv->rubberband_first = NULL; priv->rubberband_last = NULL; gtk_css_node_set_parent (priv->rubberband_node, NULL); priv->rubberband_node = NULL; remove_autoscroll (box); gtk_widget_queue_draw (GTK_WIDGET (box)); } static void gtk_flow_box_drag_gesture_end (GtkGestureDrag *gesture, gdouble offset_x, gdouble offset_y, GtkFlowBox *box) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); GdkEventSequence *sequence; if (!priv->rubberband_select) return; sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture)); if (gtk_gesture_handles_sequence (GTK_GESTURE (gesture), sequence)) { if (!priv->rubberband_extend && !priv->rubberband_modify) gtk_flow_box_unselect_all_internal (box); if (priv->rubberband_first && priv->rubberband_last) gtk_flow_box_select_all_between (box, priv->rubberband_first, priv->rubberband_last, priv->rubberband_modify); gtk_flow_box_stop_rubberband (box); g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); } else gtk_flow_box_stop_rubberband (box); gtk_widget_queue_draw (GTK_WIDGET (box)); } static gboolean gtk_flow_box_key_press_event (GtkWidget *widget, GdkEventKey *event) { GtkFlowBox *box = GTK_FLOW_BOX (widget); GtkFlowBoxPrivate *priv = BOX_PRIV (box); if (priv->rubberband_select) { if (event->keyval == GDK_KEY_Escape) { gtk_flow_box_stop_rubberband (box); return TRUE; } } return GTK_WIDGET_CLASS (gtk_flow_box_parent_class)->key_press_event (widget, event); } /* Realize and map {{{3 */ static void gtk_flow_box_realize (GtkWidget *widget) { GtkFlowBox *box = GTK_FLOW_BOX (widget); GtkAllocation allocation; GdkWindowAttr attributes = {0}; GdkWindow *window; gtk_widget_get_allocation (GTK_WIDGET (box), &allocation); gtk_widget_set_realized (GTK_WIDGET (box), TRUE); attributes.x = allocation.x; attributes.y = allocation.y; attributes.width = allocation.width; attributes.height = allocation.height; attributes.window_type = GDK_WINDOW_CHILD; attributes.event_mask = gtk_widget_get_events (GTK_WIDGET (box)) | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_POINTER_MOTION_MASK | GDK_KEY_PRESS_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK; attributes.wclass = GDK_INPUT_OUTPUT; window = gdk_window_new (gtk_widget_get_parent_window (GTK_WIDGET (box)), &attributes, GDK_WA_X | GDK_WA_Y); gtk_widget_register_window (GTK_WIDGET (box), window); gtk_widget_set_window (GTK_WIDGET (box), window); } static void gtk_flow_box_unmap (GtkWidget *widget) { GtkFlowBox *box = GTK_FLOW_BOX (widget); remove_autoscroll (box); GTK_WIDGET_CLASS (gtk_flow_box_parent_class)->unmap (widget); } /* GtkContainer implementation {{{2 */ static void gtk_flow_box_add (GtkContainer *container, GtkWidget *child) { gtk_flow_box_insert (GTK_FLOW_BOX (container), child, -1); } static void gtk_flow_box_remove (GtkContainer *container, GtkWidget *widget) { GtkFlowBox *box = GTK_FLOW_BOX (container); GtkFlowBoxPrivate *priv = BOX_PRIV (box); gboolean was_visible; gboolean was_selected; GtkFlowBoxChild *child; if (GTK_IS_FLOW_BOX_CHILD (widget)) child = GTK_FLOW_BOX_CHILD (widget); else { child = (GtkFlowBoxChild*)gtk_widget_get_parent (widget); if (!GTK_IS_FLOW_BOX_CHILD (child)) { g_warning ("Tried to remove non-child %p", widget); return; } } was_visible = child_is_visible (GTK_WIDGET (child)); was_selected = CHILD_PRIV (child)->selected; if (child == priv->active_child) priv->active_child = NULL; if (child == priv->selected_child) priv->selected_child = NULL; gtk_widget_unparent (GTK_WIDGET (child)); g_sequence_remove (CHILD_PRIV (child)->iter); if (was_visible && gtk_widget_get_visible (GTK_WIDGET (box))) gtk_widget_queue_resize (GTK_WIDGET (box)); if (was_selected && !gtk_widget_in_destruction (GTK_WIDGET (box))) g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); } static void gtk_flow_box_forall (GtkContainer *container, gboolean include_internals, GtkCallback callback, gpointer callback_target) { GSequenceIter *iter; GtkWidget *child; iter = g_sequence_get_begin_iter (BOX_PRIV (container)->children); while (!g_sequence_iter_is_end (iter)) { child = g_sequence_get (iter); iter = g_sequence_iter_next (iter); callback (child, callback_target); } } static GType gtk_flow_box_child_type (GtkContainer *container) { return GTK_TYPE_FLOW_BOX_CHILD; } /* Keynav {{{2 */ static gboolean gtk_flow_box_focus (GtkWidget *widget, GtkDirectionType direction) { GtkFlowBox *box = GTK_FLOW_BOX (widget); GtkWidget *focus_child; GSequenceIter *iter; GtkFlowBoxChild *next_focus_child; /* Without "can-focus" flag fall back to the default behavior immediately */ if (!gtk_widget_get_can_focus (widget)) { return GTK_WIDGET_CLASS (gtk_flow_box_parent_class)->focus (widget, direction); } focus_child = gtk_container_get_focus_child (GTK_CONTAINER (box)); next_focus_child = NULL; if (focus_child != NULL) { if (gtk_widget_child_focus (focus_child, direction)) return TRUE; iter = CHILD_PRIV (focus_child)->iter; if (direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD) iter = gtk_flow_box_get_previous_focusable (box, iter); else if (direction == GTK_DIR_RIGHT || direction == GTK_DIR_TAB_FORWARD) iter = gtk_flow_box_get_next_focusable (box, iter); else if (direction == GTK_DIR_UP) iter = gtk_flow_box_get_above_focusable (box, iter); else if (direction == GTK_DIR_DOWN) iter = gtk_flow_box_get_below_focusable (box, iter); if (iter != NULL) next_focus_child = g_sequence_get (iter); } else { if (BOX_PRIV (box)->selected_child) next_focus_child = BOX_PRIV (box)->selected_child; else { if (direction == GTK_DIR_UP || direction == GTK_DIR_TAB_BACKWARD) iter = gtk_flow_box_get_last_focusable (box); else iter = gtk_flow_box_get_first_focusable (box); if (iter != NULL) next_focus_child = g_sequence_get (iter); } } if (next_focus_child == NULL) { if (direction == GTK_DIR_UP || direction == GTK_DIR_DOWN || direction == GTK_DIR_LEFT || direction == GTK_DIR_RIGHT) { if (gtk_widget_keynav_failed (GTK_WIDGET (box), direction)) return TRUE; } return FALSE; } if (gtk_widget_child_focus (GTK_WIDGET (next_focus_child), direction)) return TRUE; return TRUE; } static void gtk_flow_box_add_move_binding (GtkBindingSet *binding_set, guint keyval, GdkModifierType modmask, GtkMovementStep step, gint count) { GdkDisplay *display; GdkModifierType extend_mod_mask = GDK_SHIFT_MASK; GdkModifierType modify_mod_mask = GDK_CONTROL_MASK; display = gdk_display_get_default (); if (display) { extend_mod_mask = gdk_keymap_get_modifier_mask (gdk_keymap_get_for_display (display), GDK_MODIFIER_INTENT_EXTEND_SELECTION); modify_mod_mask = gdk_keymap_get_modifier_mask (gdk_keymap_get_for_display (display), GDK_MODIFIER_INTENT_MODIFY_SELECTION); } gtk_binding_entry_add_signal (binding_set, keyval, modmask, "move-cursor", 2, GTK_TYPE_MOVEMENT_STEP, step, G_TYPE_INT, count, NULL); gtk_binding_entry_add_signal (binding_set, keyval, modmask | extend_mod_mask, "move-cursor", 2, GTK_TYPE_MOVEMENT_STEP, step, G_TYPE_INT, count, NULL); gtk_binding_entry_add_signal (binding_set, keyval, modmask | modify_mod_mask, "move-cursor", 2, GTK_TYPE_MOVEMENT_STEP, step, G_TYPE_INT, count, NULL); gtk_binding_entry_add_signal (binding_set, keyval, modmask | extend_mod_mask | modify_mod_mask, "move-cursor", 2, GTK_TYPE_MOVEMENT_STEP, step, G_TYPE_INT, count, NULL); } static void gtk_flow_box_activate_cursor_child (GtkFlowBox *box) { gtk_flow_box_select_and_activate (box, BOX_PRIV (box)->cursor_child); } static void gtk_flow_box_toggle_cursor_child (GtkFlowBox *box) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); if (priv->cursor_child == NULL) return; if ((priv->selection_mode == GTK_SELECTION_SINGLE || priv->selection_mode == GTK_SELECTION_MULTIPLE) && CHILD_PRIV (priv->cursor_child)->selected) gtk_flow_box_unselect_child_internal (box, priv->cursor_child); else gtk_flow_box_select_and_activate (box, priv->cursor_child); } static gboolean gtk_flow_box_move_cursor (GtkFlowBox *box, GtkMovementStep step, gint count) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); gboolean modify; gboolean extend; GtkFlowBoxChild *child; GtkFlowBoxChild *prev; GtkFlowBoxChild *next; GtkAllocation allocation; gint page_size; GSequenceIter *iter; gint start; GtkAdjustment *adjustment; gboolean vertical; /* Without "can-focus" flag fall back to the default behavior immediately */ if (!gtk_widget_get_can_focus (GTK_WIDGET (box))) return FALSE; vertical = priv->orientation == GTK_ORIENTATION_VERTICAL; if (vertical) { switch (step) { case GTK_MOVEMENT_VISUAL_POSITIONS: step = GTK_MOVEMENT_DISPLAY_LINES; break; case GTK_MOVEMENT_DISPLAY_LINES: step = GTK_MOVEMENT_VISUAL_POSITIONS; break; default: ; } } child = NULL; switch (step) { case GTK_MOVEMENT_VISUAL_POSITIONS: if (priv->cursor_child != NULL) { iter = CHILD_PRIV (priv->cursor_child)->iter; if (gtk_widget_get_direction (GTK_WIDGET (box)) == GTK_TEXT_DIR_RTL) count = - count; while (count < 0 && iter != NULL) { iter = gtk_flow_box_get_previous_focusable (box, iter); count = count + 1; } while (count > 0 && iter != NULL) { iter = gtk_flow_box_get_next_focusable (box, iter); count = count - 1; } if (iter != NULL && !g_sequence_iter_is_end (iter)) child = g_sequence_get (iter); } break; case GTK_MOVEMENT_BUFFER_ENDS: if (count < 0) iter = gtk_flow_box_get_first_focusable (box); else iter = gtk_flow_box_get_last_focusable (box); if (iter != NULL) child = g_sequence_get (iter); break; case GTK_MOVEMENT_DISPLAY_LINES: if (priv->cursor_child != NULL) { iter = CHILD_PRIV (priv->cursor_child)->iter; while (count < 0 && iter != NULL) { iter = gtk_flow_box_get_above_focusable (box, iter); count = count + 1; } while (count > 0 && iter != NULL) { iter = gtk_flow_box_get_below_focusable (box, iter); count = count - 1; } if (iter != NULL) child = g_sequence_get (iter); } break; case GTK_MOVEMENT_PAGES: page_size = 100; adjustment = vertical ? priv->hadjustment : priv->vadjustment; if (adjustment) page_size = gtk_adjustment_get_page_increment (adjustment); if (priv->cursor_child != NULL) { child = priv->cursor_child; iter = CHILD_PRIV (child)->iter; gtk_widget_get_allocation (GTK_WIDGET (child), &allocation); start = vertical ? allocation.x : allocation.y; if (count < 0) { gint i = 0; /* Up */ while (iter != NULL) { iter = gtk_flow_box_get_previous_focusable (box, iter); if (iter == NULL) break; prev = g_sequence_get (iter); /* go up an even number of rows */ if (i % priv->cur_children_per_line == 0) { gtk_widget_get_allocation (GTK_WIDGET (prev), &allocation); if ((vertical ? allocation.x : allocation.y) < start - page_size) break; } child = prev; i++; } } else { gint i = 0; /* Down */ while (!g_sequence_iter_is_end (iter)) { iter = gtk_flow_box_get_next_focusable (box, iter); if (iter == NULL || g_sequence_iter_is_end (iter)) break; next = g_sequence_get (iter); if (i % priv->cur_children_per_line == 0) { gtk_widget_get_allocation (GTK_WIDGET (next), &allocation); if ((vertical ? allocation.x : allocation.y) > start + page_size) break; } child = next; i++; } } gtk_widget_get_allocation (GTK_WIDGET (child), &allocation); } break; default: g_assert_not_reached (); } if (child == NULL || child == priv->cursor_child) { GtkDirectionType direction = count < 0 ? GTK_DIR_UP : GTK_DIR_DOWN; if (!gtk_widget_keynav_failed (GTK_WIDGET (box), direction)) { return FALSE; } return TRUE; } /* If the child has its "can-focus" property set to FALSE then it will * not grab the focus. We must pass the focus to its child directly. */ if (!gtk_widget_get_can_focus (GTK_WIDGET (child))) { GtkWidget *subchild; subchild = gtk_bin_get_child (GTK_BIN (child)); if (subchild) { GtkDirectionType direction = count < 0 ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD; gtk_widget_child_focus (subchild, direction); } } get_current_selection_modifiers (GTK_WIDGET (box), &modify, &extend); gtk_flow_box_update_cursor (box, child); if (!modify) gtk_flow_box_update_selection (box, child, FALSE, extend); return TRUE; } /* Selection {{{2 */ static void gtk_flow_box_selected_children_changed (GtkFlowBox *box) { _gtk_flow_box_accessible_selection_changed (GTK_WIDGET (box)); } /* GObject implementation {{{2 */ static void gtk_flow_box_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GtkFlowBox *box = GTK_FLOW_BOX (object); GtkFlowBoxPrivate *priv = BOX_PRIV (box); switch (prop_id) { case PROP_ORIENTATION: g_value_set_enum (value, priv->orientation); break; case PROP_HOMOGENEOUS: g_value_set_boolean (value, priv->homogeneous); break; case PROP_COLUMN_SPACING: g_value_set_uint (value, priv->column_spacing); break; case PROP_ROW_SPACING: g_value_set_uint (value, priv->row_spacing); break; case PROP_MIN_CHILDREN_PER_LINE: g_value_set_uint (value, priv->min_children_per_line); break; case PROP_MAX_CHILDREN_PER_LINE: g_value_set_uint (value, priv->max_children_per_line); break; case PROP_SELECTION_MODE: g_value_set_enum (value, priv->selection_mode); break; case PROP_ACTIVATE_ON_SINGLE_CLICK: g_value_set_boolean (value, priv->activate_on_single_click); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gtk_flow_box_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GtkFlowBox *box = GTK_FLOW_BOX (object); GtkFlowBoxPrivate *priv = BOX_PRIV (box); switch (prop_id) { case PROP_ORIENTATION: if (priv->orientation != g_value_get_enum (value)) { priv->orientation = g_value_get_enum (value); _gtk_orientable_set_style_classes (GTK_ORIENTABLE (box)); /* Re-box the children in the new orientation */ gtk_widget_queue_resize (GTK_WIDGET (box)); g_object_notify_by_pspec (object, pspec); } break; case PROP_HOMOGENEOUS: gtk_flow_box_set_homogeneous (box, g_value_get_boolean (value)); break; case PROP_COLUMN_SPACING: gtk_flow_box_set_column_spacing (box, g_value_get_uint (value)); break; case PROP_ROW_SPACING: gtk_flow_box_set_row_spacing (box, g_value_get_uint (value)); break; case PROP_MIN_CHILDREN_PER_LINE: gtk_flow_box_set_min_children_per_line (box, g_value_get_uint (value)); break; case PROP_MAX_CHILDREN_PER_LINE: gtk_flow_box_set_max_children_per_line (box, g_value_get_uint (value)); break; case PROP_SELECTION_MODE: gtk_flow_box_set_selection_mode (box, g_value_get_enum (value)); break; case PROP_ACTIVATE_ON_SINGLE_CLICK: gtk_flow_box_set_activate_on_single_click (box, g_value_get_boolean (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gtk_flow_box_finalize (GObject *obj) { GtkFlowBoxPrivate *priv = BOX_PRIV (obj); if (priv->filter_destroy != NULL) priv->filter_destroy (priv->filter_data); if (priv->sort_destroy != NULL) priv->sort_destroy (priv->sort_data); g_sequence_free (priv->children); g_clear_object (&priv->hadjustment); g_clear_object (&priv->vadjustment); g_object_unref (priv->drag_gesture); g_object_unref (priv->multipress_gesture); if (priv->bound_model) { if (priv->create_widget_func_data_destroy) priv->create_widget_func_data_destroy (priv->create_widget_func_data); g_signal_handlers_disconnect_by_func (priv->bound_model, gtk_flow_box_bound_model_changed, obj); g_clear_object (&priv->bound_model); } g_clear_object (&priv->gadget); G_OBJECT_CLASS (gtk_flow_box_parent_class)->finalize (obj); } static void gtk_flow_box_class_init (GtkFlowBoxClass *class) { GObjectClass *object_class = G_OBJECT_CLASS (class); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); GtkContainerClass *container_class = GTK_CONTAINER_CLASS (class); GtkBindingSet *binding_set; object_class->finalize = gtk_flow_box_finalize; object_class->get_property = gtk_flow_box_get_property; object_class->set_property = gtk_flow_box_set_property; widget_class->enter_notify_event = gtk_flow_box_enter_notify_event; widget_class->leave_notify_event = gtk_flow_box_leave_notify_event; widget_class->motion_notify_event = gtk_flow_box_motion_notify_event; widget_class->size_allocate = gtk_flow_box_size_allocate; widget_class->realize = gtk_flow_box_realize; widget_class->unmap = gtk_flow_box_unmap; widget_class->focus = gtk_flow_box_focus; widget_class->draw = gtk_flow_box_draw; widget_class->key_press_event = gtk_flow_box_key_press_event; widget_class->get_request_mode = gtk_flow_box_get_request_mode; widget_class->get_preferred_width = gtk_flow_box_get_preferred_width; widget_class->get_preferred_height = gtk_flow_box_get_preferred_height; widget_class->get_preferred_height_for_width = gtk_flow_box_get_preferred_height_for_width; widget_class->get_preferred_width_for_height = gtk_flow_box_get_preferred_width_for_height; container_class->add = gtk_flow_box_add; container_class->remove = gtk_flow_box_remove; container_class->forall = gtk_flow_box_forall; container_class->child_type = gtk_flow_box_child_type; gtk_container_class_handle_border_width (container_class); class->activate_cursor_child = gtk_flow_box_activate_cursor_child; class->toggle_cursor_child = gtk_flow_box_toggle_cursor_child; class->move_cursor = gtk_flow_box_move_cursor; class->select_all = gtk_flow_box_select_all; class->unselect_all = gtk_flow_box_unselect_all; class->selected_children_changed = gtk_flow_box_selected_children_changed; g_object_class_override_property (object_class, PROP_ORIENTATION, "orientation"); /** * GtkFlowBox:selection-mode: * * The selection mode used by the flow box. */ props[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); /** * GtkFlowBox:activate-on-single-click: * * Determines whether children can be activated with a single * click, or require a double-click. */ props[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); /** * GtkFlowBox:homogeneous: * * Determines whether all children should be allocated the * same size. */ props[PROP_HOMOGENEOUS] = g_param_spec_boolean ("homogeneous", P_("Homogeneous"), P_("Whether the children should all be the same size"), FALSE, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); /** * GtkFlowBox:min-children-per-line: * * The minimum number of children to allocate consecutively * in the given orientation. * * Setting the minimum children per line ensures * that a reasonably small height will be requested * for the overall minimum width of the box. */ props[PROP_MIN_CHILDREN_PER_LINE] = g_param_spec_uint ("min-children-per-line", P_("Minimum Children Per Line"), P_("The minimum number of children to allocate " "consecutively in the given orientation."), 0, G_MAXUINT, 0, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); /** * GtkFlowBox:max-children-per-line: * * The maximum amount of children to request space for consecutively * in the given orientation. */ props[PROP_MAX_CHILDREN_PER_LINE] = g_param_spec_uint ("max-children-per-line", P_("Maximum Children Per Line"), P_("The maximum amount of children to request space for " "consecutively in the given orientation."), 0, G_MAXUINT, DEFAULT_MAX_CHILDREN_PER_LINE, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); /** * GtkFlowBox:row-spacing: * * The amount of vertical space between two children. */ props[PROP_ROW_SPACING] = g_param_spec_uint ("row-spacing", P_("Vertical spacing"), P_("The amount of vertical space between two children"), 0, G_MAXUINT, 0, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); /** * GtkFlowBox:column-spacing: * * The amount of horizontal space between two children. */ props[PROP_COLUMN_SPACING] = g_param_spec_uint ("column-spacing", P_("Horizontal spacing"), P_("The amount of horizontal space between two children"), 0, G_MAXUINT, 0, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); g_object_class_install_properties (object_class, LAST_PROP, props); /** * GtkFlowBox::child-activated: * @box: the #GtkFlowBox on which the signal is emitted * @child: the child that is activated * * The ::child-activated signal is emitted when a child has been * activated by the user. */ signals[CHILD_ACTIVATED] = g_signal_new (I_("child-activated"), GTK_TYPE_FLOW_BOX, G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GtkFlowBoxClass, child_activated), NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, GTK_TYPE_FLOW_BOX_CHILD); /** * GtkFlowBox::selected-children-changed: * @box: the #GtkFlowBox on wich the signal is emitted * * The ::selected-children-changed signal is emitted when the * set of selected children changes. * * Use gtk_flow_box_selected_foreach() or * gtk_flow_box_get_selected_children() to obtain the * selected children. */ signals[SELECTED_CHILDREN_CHANGED] = g_signal_new (I_("selected-children-changed"), GTK_TYPE_FLOW_BOX, G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (GtkFlowBoxClass, selected_children_changed), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); /** * GtkFlowBox::activate-cursor-child: * @box: the #GtkFlowBox on which the signal is emitted * * The ::activate-cursor-child signal is a * [keybinding signal][GtkBindingSignal] * which gets emitted when the user activates the @box. */ signals[ACTIVATE_CURSOR_CHILD] = g_signal_new (I_("activate-cursor-child"), GTK_TYPE_FLOW_BOX, G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GtkFlowBoxClass, activate_cursor_child), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); /** * GtkFlowBox::toggle-cursor-child: * @box: the #GtkFlowBox on which the signal is emitted * * The ::toggle-cursor-child signal is a * [keybinding signal][GtkBindingSignal] * which toggles the selection of the child that has the focus. * * The default binding for this signal is Ctrl-Space. */ signals[TOGGLE_CURSOR_CHILD] = g_signal_new (I_("toggle-cursor-child"), GTK_TYPE_FLOW_BOX, G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GtkFlowBoxClass, toggle_cursor_child), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); /** * GtkFlowBox::move-cursor: * @box: the #GtkFlowBox on which the signal is emitted * @step: the granularity fo the move, as a #GtkMovementStep * @count: the number of @step units to move * * The ::move-cursor signal is a * [keybinding signal][GtkBindingSignal] * which gets emitted when the user initiates a cursor movement. * * Applications should not connect to it, but may emit it with * g_signal_emit_by_name() if they need to control the cursor * programmatically. * * The default bindings for this signal come in two variants, * the variant with the Shift modifier extends the selection, * the variant without the Shift modifer does not. * There are too many key combinations to list them all here. * - Arrow keys move by individual children * - Home/End keys move to the ends of the box * - PageUp/PageDown keys move vertically by pages * * Returns: %TRUE to stop other handlers from being invoked for the event. * %FALSE to propagate the event further. */ signals[MOVE_CURSOR] = g_signal_new (I_("move-cursor"), GTK_TYPE_FLOW_BOX, G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GtkFlowBoxClass, move_cursor), NULL, NULL, _gtk_marshal_BOOLEAN__ENUM_INT, G_TYPE_BOOLEAN, 2, GTK_TYPE_MOVEMENT_STEP, G_TYPE_INT); /** * GtkFlowBox::select-all: * @box: the #GtkFlowBox on which the signal is emitted * * The ::select-all signal is a * [keybinding signal][GtkBindingSignal] * which gets emitted to select all children of the box, if * the selection mode permits it. * * The default bindings for this signal is Ctrl-a. */ signals[SELECT_ALL] = g_signal_new (I_("select-all"), GTK_TYPE_FLOW_BOX, G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GtkFlowBoxClass, select_all), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); /** * GtkFlowBox::unselect-all: * @box: the #GtkFlowBox on which the signal is emitted * * The ::unselect-all signal is a * [keybinding signal][GtkBindingSignal] * which gets emitted to unselect all children of the box, if * the selection mode permits it. * * The default bindings for this signal is Ctrl-Shift-a. */ signals[UNSELECT_ALL] = g_signal_new (I_("unselect-all"), GTK_TYPE_FLOW_BOX, G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GtkFlowBoxClass, unselect_all), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); widget_class->activate_signal = signals[ACTIVATE_CURSOR_CHILD]; binding_set = gtk_binding_set_by_class (class); gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Home, 0, GTK_MOVEMENT_BUFFER_ENDS, -1); gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Home, 0, GTK_MOVEMENT_BUFFER_ENDS, -1); gtk_flow_box_add_move_binding (binding_set, GDK_KEY_End, 0, GTK_MOVEMENT_BUFFER_ENDS, 1); gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_End, 0, GTK_MOVEMENT_BUFFER_ENDS, 1); gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Up, 0, GTK_MOVEMENT_DISPLAY_LINES, -1); gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Up, 0, GTK_MOVEMENT_DISPLAY_LINES, -1); gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Down, 0, GTK_MOVEMENT_DISPLAY_LINES, 1); gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Down, 0, GTK_MOVEMENT_DISPLAY_LINES, 1); gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Page_Up, 0, GTK_MOVEMENT_PAGES, -1); gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Page_Up, 0, GTK_MOVEMENT_PAGES, -1); gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Page_Down, 0, GTK_MOVEMENT_PAGES, 1); gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Page_Down, 0, GTK_MOVEMENT_PAGES, 1); gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Right, 0, GTK_MOVEMENT_VISUAL_POSITIONS, 1); gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Right, 0, GTK_MOVEMENT_VISUAL_POSITIONS, 1); gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Left, 0, GTK_MOVEMENT_VISUAL_POSITIONS, -1); gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Left, 0, GTK_MOVEMENT_VISUAL_POSITIONS, -1); gtk_binding_entry_add_signal (binding_set, GDK_KEY_space, GDK_CONTROL_MASK, "toggle-cursor-child", 0, NULL); gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_Space, GDK_CONTROL_MASK, "toggle-cursor-child", 0, NULL); gtk_binding_entry_add_signal (binding_set, GDK_KEY_a, GDK_CONTROL_MASK, "select-all", 0); gtk_binding_entry_add_signal (binding_set, GDK_KEY_a, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "unselect-all", 0); gtk_widget_class_set_accessible_type (widget_class, GTK_TYPE_FLOW_BOX_ACCESSIBLE); gtk_widget_class_set_css_name (widget_class, "flowbox"); } static void gtk_flow_box_init (GtkFlowBox *box) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); GtkCssNode *widget_node; gtk_widget_set_has_window (GTK_WIDGET (box), TRUE); gtk_widget_set_redraw_on_allocate (GTK_WIDGET (box), TRUE); priv->orientation = GTK_ORIENTATION_HORIZONTAL; priv->selection_mode = GTK_SELECTION_SINGLE; priv->max_children_per_line = DEFAULT_MAX_CHILDREN_PER_LINE; priv->column_spacing = 0; priv->row_spacing = 0; priv->activate_on_single_click = TRUE; _gtk_orientable_set_style_classes (GTK_ORIENTABLE (box)); priv->children = g_sequence_new (NULL); priv->multipress_gesture = gtk_gesture_multi_press_new (GTK_WIDGET (box)); gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (priv->multipress_gesture), FALSE); gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (priv->multipress_gesture), GDK_BUTTON_PRIMARY); gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (priv->multipress_gesture), GTK_PHASE_BUBBLE); g_signal_connect (priv->multipress_gesture, "pressed", G_CALLBACK (gtk_flow_box_multipress_gesture_pressed), box); g_signal_connect (priv->multipress_gesture, "released", G_CALLBACK (gtk_flow_box_multipress_gesture_released), box); g_signal_connect (priv->multipress_gesture, "stopped", G_CALLBACK (gtk_flow_box_multipress_gesture_stopped), box); priv->drag_gesture = gtk_gesture_drag_new (GTK_WIDGET (box)); gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (priv->drag_gesture), FALSE); gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (priv->drag_gesture), GDK_BUTTON_PRIMARY); gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (priv->drag_gesture), GTK_PHASE_CAPTURE); g_signal_connect (priv->drag_gesture, "drag-begin", G_CALLBACK (gtk_flow_box_drag_gesture_begin), box); g_signal_connect (priv->drag_gesture, "drag-update", G_CALLBACK (gtk_flow_box_drag_gesture_update), box); g_signal_connect (priv->drag_gesture, "drag-end", G_CALLBACK (gtk_flow_box_drag_gesture_end), box); widget_node = gtk_widget_get_css_node (GTK_WIDGET (box)); priv->gadget = gtk_css_custom_gadget_new_for_node (widget_node, GTK_WIDGET (box), gtk_flow_box_measure, gtk_flow_box_allocate, gtk_flow_box_render, NULL, NULL); } static void gtk_flow_box_bound_model_changed (GListModel *list, guint position, guint removed, guint added, gpointer user_data) { GtkFlowBox *box = user_data; GtkFlowBoxPrivate *priv = BOX_PRIV (box); gint i; while (removed--) { GtkFlowBoxChild *child; child = gtk_flow_box_get_child_at_index (box, position); gtk_widget_destroy (GTK_WIDGET (child)); } for (i = 0; i < added; i++) { GObject *item; GtkWidget *widget; item = g_list_model_get_item (list, position + i); widget = priv->create_widget_func (item, priv->create_widget_func_data); /* We need to sink the floating reference here, so that we can accept * both instances created with a floating reference (e.g. C functions * that just return the result of g_object_new()) and without (e.g. * from language bindings which will automatically sink the floating * reference). * * See the similar code in gtklistbox.c:gtk_list_box_bound_model_changed. */ if (g_object_is_floating (widget)) g_object_ref_sink (widget); gtk_widget_show (widget); gtk_flow_box_insert (box, widget, position + i); g_object_unref (widget); g_object_unref (item); } } /* Public API {{{2 */ /** * gtk_flow_box_new: * * Creates a GtkFlowBox. * * Returns: a new #GtkFlowBox container * * Since: 3.12 */ GtkWidget * gtk_flow_box_new (void) { return (GtkWidget *)g_object_new (GTK_TYPE_FLOW_BOX, NULL); } static void gtk_flow_box_insert_css_node (GtkFlowBox *box, GtkWidget *child, GSequenceIter *iter) { GSequenceIter *prev_iter; GtkWidget *sibling; prev_iter = g_sequence_iter_prev (iter); if (prev_iter != iter) { sibling = g_sequence_get (prev_iter); gtk_css_node_insert_after (gtk_widget_get_css_node (GTK_WIDGET (box)), gtk_widget_get_css_node (child), gtk_widget_get_css_node (sibling)); } } /** * gtk_flow_box_insert: * @box: a #GtkFlowBox * @widget: the #GtkWidget to add * @position: the position to insert @child in * * Inserts the @widget into @box at @position. * * If a sort function is set, the widget will actually be inserted * at the calculated position and this function has the same effect * as gtk_container_add(). * * If @position is -1, or larger than the total number of children * in the @box, then the @widget will be appended to the end. * * Since: 3.12 */ void gtk_flow_box_insert (GtkFlowBox *box, GtkWidget *widget, gint position) { GtkFlowBoxPrivate *priv; GtkFlowBoxChild *child; GSequenceIter *iter; g_return_if_fail (GTK_IS_FLOW_BOX (box)); g_return_if_fail (GTK_IS_WIDGET (widget)); priv = BOX_PRIV (box); if (GTK_IS_FLOW_BOX_CHILD (widget)) child = GTK_FLOW_BOX_CHILD (widget); else { child = GTK_FLOW_BOX_CHILD (gtk_flow_box_child_new ()); gtk_widget_show (GTK_WIDGET (child)); gtk_container_add (GTK_CONTAINER (child), widget); } if (priv->sort_func != NULL) iter = g_sequence_insert_sorted (priv->children, child, (GCompareDataFunc)gtk_flow_box_sort, box); else if (position == 0) iter = g_sequence_prepend (priv->children, child); else if (position == -1) iter = g_sequence_append (priv->children, child); else { GSequenceIter *pos; pos = g_sequence_get_iter_at_pos (priv->children, position); iter = g_sequence_insert_before (pos, child); } gtk_flow_box_insert_css_node (box, GTK_WIDGET (child), iter); CHILD_PRIV (child)->iter = iter; gtk_widget_set_parent (GTK_WIDGET (child), GTK_WIDGET (box)); gtk_flow_box_apply_filter (box, child); } /** * gtk_flow_box_get_child_at_index: * @box: a #GtkFlowBox * @idx: the position of the child * * Gets the nth child in the @box. * * Returns: (transfer none) (nullable): the child widget, which will * always be a #GtkFlowBoxChild or %NULL in case no child widget * with the given index exists. * * Since: 3.12 */ GtkFlowBoxChild * gtk_flow_box_get_child_at_index (GtkFlowBox *box, gint idx) { GSequenceIter *iter; g_return_val_if_fail (GTK_IS_FLOW_BOX (box), NULL); iter = g_sequence_get_iter_at_pos (BOX_PRIV (box)->children, idx); if (!g_sequence_iter_is_end (iter)) return g_sequence_get (iter); return NULL; } /** * gtk_flow_box_get_child_at_pos: * @box: a #GtkFlowBox * @x: the x coordinate of the child * @y: the y coordinate of the child * * Gets the child in the (@x, @y) position. * * Returns: (transfer none) (nullable): the child widget, which will * always be a #GtkFlowBoxChild or %NULL in case no child widget * exists for the given x and y coordinates. * * Since: 3.22.6 */ GtkFlowBoxChild * gtk_flow_box_get_child_at_pos (GtkFlowBox *box, gint x, gint y) { GtkWidget *child; GSequenceIter *iter; GtkAllocation allocation; for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { child = g_sequence_get (iter); if (!child_is_visible (child)) continue; gtk_widget_get_allocation (child, &allocation); if (x >= allocation.x && x < (allocation.x + allocation.width) && y >= allocation.y && y < (allocation.y + allocation.height)) return GTK_FLOW_BOX_CHILD (child); } return NULL; } /** * gtk_flow_box_set_hadjustment: * @box: a #GtkFlowBox * @adjustment: an adjustment which should be adjusted * when the focus is moved among the descendents of @container * * Hooks up an adjustment to focus handling in @box. * The adjustment is also used for autoscrolling during * rubberband selection. See gtk_scrolled_window_get_hadjustment() * for a typical way of obtaining the adjustment, and * gtk_flow_box_set_vadjustment()for setting the vertical * adjustment. * * The adjustments have to be in pixel units and in the same * coordinate system as the allocation for immediate children * of the box. * * Since: 3.12 */ void gtk_flow_box_set_hadjustment (GtkFlowBox *box, GtkAdjustment *adjustment) { GtkFlowBoxPrivate *priv; g_return_if_fail (GTK_IS_FLOW_BOX (box)); g_return_if_fail (GTK_IS_ADJUSTMENT (adjustment)); priv = BOX_PRIV (box); g_object_ref (adjustment); if (priv->hadjustment) g_object_unref (priv->hadjustment); priv->hadjustment = adjustment; gtk_container_set_focus_hadjustment (GTK_CONTAINER (box), adjustment); } /** * gtk_flow_box_set_vadjustment: * @box: a #GtkFlowBox * @adjustment: an adjustment which should be adjusted * when the focus is moved among the descendents of @container * * Hooks up an adjustment to focus handling in @box. * The adjustment is also used for autoscrolling during * rubberband selection. See gtk_scrolled_window_get_vadjustment() * for a typical way of obtaining the adjustment, and * gtk_flow_box_set_hadjustment()for setting the horizontal * adjustment. * * The adjustments have to be in pixel units and in the same * coordinate system as the allocation for immediate children * of the box. * * Since: 3.12 */ void gtk_flow_box_set_vadjustment (GtkFlowBox *box, GtkAdjustment *adjustment) { GtkFlowBoxPrivate *priv; g_return_if_fail (GTK_IS_FLOW_BOX (box)); g_return_if_fail (GTK_IS_ADJUSTMENT (adjustment)); priv = BOX_PRIV (box); g_object_ref (adjustment); if (priv->vadjustment) g_object_unref (priv->vadjustment); priv->vadjustment = adjustment; gtk_container_set_focus_vadjustment (GTK_CONTAINER (box), adjustment); } static void gtk_flow_box_check_model_compat (GtkFlowBox *box) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); if (priv->bound_model && (priv->sort_func || priv->filter_func)) g_warning ("GtkFlowBox with a model will ignore sort and filter functions"); } /** * gtk_flow_box_bind_model: * @box: a #GtkFlowBox * @model: (allow-none): the #GListModel to be bound to @box * @create_widget_func: a function that creates widgets for items * @user_data: 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 * gtk_flow_box_insert() or gtk_container_add()) while @box is bound to a * model. * * Note that using a model is incompatible with the filtering and sorting * functionality in GtkFlowBox. When using a model, filtering and sorting * should be implemented by the model. * * Since: 3.18 */ void gtk_flow_box_bind_model (GtkFlowBox *box, GListModel *model, GtkFlowBoxCreateWidgetFunc create_widget_func, gpointer user_data, GDestroyNotify user_data_free_func) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); g_return_if_fail (GTK_IS_FLOW_BOX (box)); g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model)); g_return_if_fail (model == NULL || create_widget_func != NULL); if (priv->bound_model) { if (priv->create_widget_func_data_destroy) priv->create_widget_func_data_destroy (priv->create_widget_func_data); g_signal_handlers_disconnect_by_func (priv->bound_model, gtk_flow_box_bound_model_changed, box); g_clear_object (&priv->bound_model); } gtk_flow_box_forall (GTK_CONTAINER (box), FALSE, (GtkCallback) gtk_widget_destroy, NULL); if (model == NULL) return; priv->bound_model = g_object_ref (model); priv->create_widget_func = create_widget_func; priv->create_widget_func_data = user_data; priv->create_widget_func_data_destroy = user_data_free_func; gtk_flow_box_check_model_compat (box); g_signal_connect (priv->bound_model, "items-changed", G_CALLBACK (gtk_flow_box_bound_model_changed), box); gtk_flow_box_bound_model_changed (model, 0, 0, g_list_model_get_n_items (model), box); } /* Setters and getters {{{2 */ /** * gtk_flow_box_get_homogeneous: * @box: a #GtkFlowBox * * Returns whether the box is homogeneous (all children are the * same size). See gtk_box_set_homogeneous(). * * Returns: %TRUE if the box is homogeneous. * * Since: 3.12 */ gboolean gtk_flow_box_get_homogeneous (GtkFlowBox *box) { g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE); return BOX_PRIV (box)->homogeneous; } /** * gtk_flow_box_set_homogeneous: * @box: a #GtkFlowBox * @homogeneous: %TRUE to create equal allotments, * %FALSE for variable allotments * * Sets the #GtkFlowBox:homogeneous property of @box, controlling * whether or not all children of @box are given equal space * in the box. * * Since: 3.12 */ void gtk_flow_box_set_homogeneous (GtkFlowBox *box, gboolean homogeneous) { g_return_if_fail (GTK_IS_FLOW_BOX (box)); homogeneous = homogeneous != FALSE; if (BOX_PRIV (box)->homogeneous != homogeneous) { BOX_PRIV (box)->homogeneous = homogeneous; g_object_notify_by_pspec (G_OBJECT (box), props[PROP_HOMOGENEOUS]); gtk_widget_queue_resize (GTK_WIDGET (box)); } } /** * gtk_flow_box_set_row_spacing: * @box: a #GtkFlowBox * @spacing: the spacing to use * * Sets the vertical space to add between children. * See the #GtkFlowBox:row-spacing property. * * Since: 3.12 */ void gtk_flow_box_set_row_spacing (GtkFlowBox *box, guint spacing) { g_return_if_fail (GTK_IS_FLOW_BOX (box)); if (BOX_PRIV (box)->row_spacing != spacing) { BOX_PRIV (box)->row_spacing = spacing; gtk_widget_queue_resize (GTK_WIDGET (box)); g_object_notify_by_pspec (G_OBJECT (box), props[PROP_ROW_SPACING]); } } /** * gtk_flow_box_get_row_spacing: * @box: a #GtkFlowBox * * Gets the vertical spacing. * * Returns: the vertical spacing * * Since: 3.12 */ guint gtk_flow_box_get_row_spacing (GtkFlowBox *box) { g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE); return BOX_PRIV (box)->row_spacing; } /** * gtk_flow_box_set_column_spacing: * @box: a #GtkFlowBox * @spacing: the spacing to use * * Sets the horizontal space to add between children. * See the #GtkFlowBox:column-spacing property. * * Since: 3.12 */ void gtk_flow_box_set_column_spacing (GtkFlowBox *box, guint spacing) { g_return_if_fail (GTK_IS_FLOW_BOX (box)); if (BOX_PRIV (box)->column_spacing != spacing) { BOX_PRIV (box)->column_spacing = spacing; gtk_widget_queue_resize (GTK_WIDGET (box)); g_object_notify_by_pspec (G_OBJECT (box), props[PROP_COLUMN_SPACING]); } } /** * gtk_flow_box_get_column_spacing: * @box: a #GtkFlowBox * * Gets the horizontal spacing. * * Returns: the horizontal spacing * * Since: 3.12 */ guint gtk_flow_box_get_column_spacing (GtkFlowBox *box) { g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE); return BOX_PRIV (box)->column_spacing; } /** * gtk_flow_box_set_min_children_per_line: * @box: a #GtkFlowBox * @n_children: the minimum number of children per line * * Sets the minimum number of children to line up * in @box’s orientation before flowing. * * Since: 3.12 */ void gtk_flow_box_set_min_children_per_line (GtkFlowBox *box, guint n_children) { g_return_if_fail (GTK_IS_FLOW_BOX (box)); if (BOX_PRIV (box)->min_children_per_line != n_children) { BOX_PRIV (box)->min_children_per_line = n_children; gtk_widget_queue_resize (GTK_WIDGET (box)); g_object_notify_by_pspec (G_OBJECT (box), props[PROP_MIN_CHILDREN_PER_LINE]); } } /** * gtk_flow_box_get_min_children_per_line: * @box: a #GtkFlowBox * * Gets the minimum number of children per line. * * Returns: the minimum number of children per line * * Since: 3.12 */ guint gtk_flow_box_get_min_children_per_line (GtkFlowBox *box) { g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE); return BOX_PRIV (box)->min_children_per_line; } /** * gtk_flow_box_set_max_children_per_line: * @box: a #GtkFlowBox * @n_children: the maximum number of children per line * * Sets the maximum number of children to request and * allocate space for in @box’s orientation. * * Setting the maximum number of children per line * limits the overall natural size request to be no more * than @n_children children long in the given orientation. * * Since: 3.12 */ void gtk_flow_box_set_max_children_per_line (GtkFlowBox *box, guint n_children) { g_return_if_fail (GTK_IS_FLOW_BOX (box)); if (BOX_PRIV (box)->max_children_per_line != n_children) { BOX_PRIV (box)->max_children_per_line = n_children; gtk_widget_queue_resize (GTK_WIDGET (box)); g_object_notify_by_pspec (G_OBJECT (box), props[PROP_MAX_CHILDREN_PER_LINE]); } } /** * gtk_flow_box_get_max_children_per_line: * @box: a #GtkFlowBox * * Gets the maximum number of children per line. * * Returns: the maximum number of children per line * * Since: 3.12 */ guint gtk_flow_box_get_max_children_per_line (GtkFlowBox *box) { g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE); return BOX_PRIV (box)->max_children_per_line; } /** * gtk_flow_box_set_activate_on_single_click: * @box: a #GtkFlowBox * @single: %TRUE to emit child-activated on a single click * * If @single is %TRUE, children will be activated when you click * on them, otherwise you need to double-click. * * Since: 3.12 */ void gtk_flow_box_set_activate_on_single_click (GtkFlowBox *box, gboolean single) { g_return_if_fail (GTK_IS_FLOW_BOX (box)); single = single != FALSE; if (BOX_PRIV (box)->activate_on_single_click != single) { BOX_PRIV (box)->activate_on_single_click = single; g_object_notify_by_pspec (G_OBJECT (box), props[PROP_ACTIVATE_ON_SINGLE_CLICK]); } } /** * gtk_flow_box_get_activate_on_single_click: * @box: a #GtkFlowBox * * Returns whether children activate on single clicks. * * Returns: %TRUE if children are activated on single click, * %FALSE otherwise * * Since: 3.12 */ gboolean gtk_flow_box_get_activate_on_single_click (GtkFlowBox *box) { g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE); return BOX_PRIV (box)->activate_on_single_click; } /* Selection handling {{{2 */ /** * gtk_flow_box_get_selected_children: * @box: a #GtkFlowBox * * Creates a list of all selected children. * * Returns: (element-type GtkFlowBoxChild) (transfer container): * A #GList containing the #GtkWidget for each selected child. * Free with g_list_free() when done. * * Since: 3.12 */ GList * gtk_flow_box_get_selected_children (GtkFlowBox *box) { GtkFlowBoxChild *child; GSequenceIter *iter; GList *selected = NULL; g_return_val_if_fail (GTK_IS_FLOW_BOX (box), NULL); for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { child = g_sequence_get (iter); if (CHILD_PRIV (child)->selected) selected = g_list_prepend (selected, child); } return g_list_reverse (selected); } /** * gtk_flow_box_select_child: * @box: a #GtkFlowBox * @child: a child of @box * * Selects a single child of @box, if the selection * mode allows it. * * Since: 3.12 */ void gtk_flow_box_select_child (GtkFlowBox *box, GtkFlowBoxChild *child) { g_return_if_fail (GTK_IS_FLOW_BOX (box)); g_return_if_fail (GTK_IS_FLOW_BOX_CHILD (child)); gtk_flow_box_select_child_internal (box, child); } /** * gtk_flow_box_unselect_child: * @box: a #GtkFlowBox * @child: a child of @box * * Unselects a single child of @box, if the selection * mode allows it. * * Since: 3.12 */ void gtk_flow_box_unselect_child (GtkFlowBox *box, GtkFlowBoxChild *child) { g_return_if_fail (GTK_IS_FLOW_BOX (box)); g_return_if_fail (GTK_IS_FLOW_BOX_CHILD (child)); gtk_flow_box_unselect_child_internal (box, child); } /** * gtk_flow_box_select_all: * @box: a #GtkFlowBox * * Select all children of @box, if the selection * mode allows it. * * Since: 3.12 */ void gtk_flow_box_select_all (GtkFlowBox *box) { g_return_if_fail (GTK_IS_FLOW_BOX (box)); if (BOX_PRIV (box)->selection_mode != GTK_SELECTION_MULTIPLE) return; if (g_sequence_get_length (BOX_PRIV (box)->children) > 0) { gtk_flow_box_select_all_between (box, NULL, NULL, FALSE); g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); } } /** * gtk_flow_box_unselect_all: * @box: a #GtkFlowBox * * Unselect all children of @box, if the selection * mode allows it. * * Since: 3.12 */ void gtk_flow_box_unselect_all (GtkFlowBox *box) { gboolean dirty = FALSE; g_return_if_fail (GTK_IS_FLOW_BOX (box)); if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_BROWSE) return; dirty = gtk_flow_box_unselect_all_internal (box); if (dirty) g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); } /** * GtkFlowBoxForeachFunc: * @box: a #GtkFlowBox * @child: a #GtkFlowBoxChild * @user_data: (closure): user data * * A function used by gtk_flow_box_selected_foreach(). * It will be called on every selected child of the @box. * * Since: 3.12 */ /** * gtk_flow_box_selected_foreach: * @box: a #GtkFlowBox * @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. * * Since: 3.12 */ void gtk_flow_box_selected_foreach (GtkFlowBox *box, GtkFlowBoxForeachFunc func, gpointer data) { GtkFlowBoxChild *child; GSequenceIter *iter; g_return_if_fail (GTK_IS_FLOW_BOX (box)); for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { child = g_sequence_get (iter); if (CHILD_PRIV (child)->selected) (*func) (box, child, data); } } /** * gtk_flow_box_set_selection_mode: * @box: a #GtkFlowBox * @mode: the new selection mode * * Sets how selection works in @box. * See #GtkSelectionMode for details. * * Since: 3.12 */ void gtk_flow_box_set_selection_mode (GtkFlowBox *box, GtkSelectionMode mode) { gboolean dirty = FALSE; g_return_if_fail (GTK_IS_FLOW_BOX (box)); if (mode == BOX_PRIV (box)->selection_mode) return; if (mode == GTK_SELECTION_NONE || BOX_PRIV (box)->selection_mode == GTK_SELECTION_MULTIPLE) { dirty = gtk_flow_box_unselect_all_internal (box); BOX_PRIV (box)->selected_child = NULL; } BOX_PRIV (box)->selection_mode = mode; g_object_notify_by_pspec (G_OBJECT (box), props[PROP_SELECTION_MODE]); if (dirty) g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); } /** * gtk_flow_box_get_selection_mode: * @box: a #GtkFlowBox * * Gets the selection mode of @box. * * Returns: the #GtkSelectionMode * * Since: 3.12 */ GtkSelectionMode gtk_flow_box_get_selection_mode (GtkFlowBox *box) { g_return_val_if_fail (GTK_IS_FLOW_BOX (box), GTK_SELECTION_SINGLE); return BOX_PRIV (box)->selection_mode; } /* Filtering {{{2 */ /** * GtkFlowBoxFilterFunc: * @child: a #GtkFlowBoxChild that may be filtered * @user_data: (closure): user data * * A function that will be called whenrever a child changes * or is added. It lets you control if the child should be * visible or not. * * Returns: %TRUE if the row should be visible, %FALSE otherwise * * Since: 3.12 */ /** * gtk_flow_box_set_filter_func: * @box: a #GtkFlowBox * @filter_func: (closure user_data) (allow-none): callback that * lets you filter which children to show * @user_data: 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 children to show. For instance, to implement a search * function that only shows the children matching the search terms. * * The @filter_func will be called for each child after the call, and * it will continue to be called each time a child changes (via * gtk_flow_box_child_changed()) or when gtk_flow_box_invalidate_filter() * is called. * * Note that using a filter function is incompatible with using a model * (see gtk_flow_box_bind_model()). * * Since: 3.12 */ void gtk_flow_box_set_filter_func (GtkFlowBox *box, GtkFlowBoxFilterFunc filter_func, gpointer user_data, GDestroyNotify destroy) { GtkFlowBoxPrivate *priv; g_return_if_fail (GTK_IS_FLOW_BOX (box)); priv = BOX_PRIV (box); if (priv->filter_destroy != NULL) priv->filter_destroy (priv->filter_data); priv->filter_func = filter_func; priv->filter_data = user_data; priv->filter_destroy = destroy; gtk_flow_box_check_model_compat (box); gtk_flow_box_apply_filter_all (box); } /** * gtk_flow_box_invalidate_filter: * @box: a #GtkFlowBox * * Updates the filtering for all children. * * Call this function when the result of the filter * function on the @box is changed due ot an external * factor. For instance, this would be used if the * filter function just looked for a specific search * term, and the entry with the string has changed. * * Since: 3.12 */ void gtk_flow_box_invalidate_filter (GtkFlowBox *box) { g_return_if_fail (GTK_IS_FLOW_BOX (box)); if (BOX_PRIV (box)->filter_func != NULL) gtk_flow_box_apply_filter_all (box); } /* Sorting {{{2 */ /** * GtkFlowBoxSortFunc: * @child1: the first child * @child2: the second child * @user_data: (closure): user data * * A function to compare two children to determine which * should come first. * * Returns: < 0 if @child1 should be before @child2, 0 if * the are equal, and > 0 otherwise * * Since: 3.12 */ /** * gtk_flow_box_set_sort_func: * @box: a #GtkFlowBox * @sort_func: (closure user_data) (allow-none): the sort function * @user_data: user data passed to @sort_func * @destroy: destroy notifier for @user_data * * By setting a sort function on the @box, one can dynamically * reorder the children of the box, based on the contents of * the children. * * The @sort_func will be called for each child after the call, * and will continue to be called each time a child changes (via * gtk_flow_box_child_changed()) and when gtk_flow_box_invalidate_sort() * is called. * * Note that using a sort function is incompatible with using a model * (see gtk_flow_box_bind_model()). * * Since: 3.12 */ void gtk_flow_box_set_sort_func (GtkFlowBox *box, GtkFlowBoxSortFunc sort_func, gpointer user_data, GDestroyNotify destroy) { GtkFlowBoxPrivate *priv; g_return_if_fail (GTK_IS_FLOW_BOX (box)); priv = BOX_PRIV (box); if (priv->sort_destroy != NULL) priv->sort_destroy (priv->sort_data); priv->sort_func = sort_func; priv->sort_data = user_data; priv->sort_destroy = destroy; gtk_flow_box_check_model_compat (box); gtk_flow_box_invalidate_sort (box); } static gint gtk_flow_box_sort (GtkFlowBoxChild *a, GtkFlowBoxChild *b, GtkFlowBox *box) { GtkFlowBoxPrivate *priv = BOX_PRIV (box); return priv->sort_func (a, b, priv->sort_data); } static void gtk_flow_box_css_node_foreach (gpointer data, gpointer user_data) { GtkWidget **previous = user_data; GtkWidget *row = data; GtkCssNode *row_node; GtkCssNode *prev_node; if (*previous) { prev_node = gtk_widget_get_css_node (*previous); row_node = gtk_widget_get_css_node (row); gtk_css_node_insert_after (gtk_css_node_get_parent (row_node), row_node, prev_node); } *previous = row; } /** * gtk_flow_box_invalidate_sort: * @box: a #GtkFlowBox * * Updates the sorting for all children. * * Call this when the result of the sort function on * @box is changed due to an external factor. * * Since: 3.12 */ void gtk_flow_box_invalidate_sort (GtkFlowBox *box) { GtkFlowBoxPrivate *priv; GtkWidget *previous = NULL; g_return_if_fail (GTK_IS_FLOW_BOX (box)); priv = BOX_PRIV (box); if (priv->sort_func != NULL) { g_sequence_sort (priv->children, (GCompareDataFunc)gtk_flow_box_sort, box); g_sequence_foreach (priv->children, gtk_flow_box_css_node_foreach, &previous); gtk_widget_queue_resize (GTK_WIDGET (box)); } } /* vim:set foldmethod=marker expandtab: */