/* * 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, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ /** * SECTION:eggflowbox * @Short_Description: A container that allows reflowing its children * @Title: EggFlowBox * * #EggFlowBox 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. * */ #ifdef HAVE_CONFIG_H #include #endif #include #include "egg-flow-box.h" #define P_(msgid) (msgid) enum { CHILD_ACTIVATED, SELECTED_CHILDREN_CHANGED, LAST_SIGNAL }; enum { PROP_0, PROP_ORIENTATION, PROP_HOMOGENEOUS, PROP_HALIGN_POLICY, PROP_VALIGN_POLICY, PROP_COLUMN_SPACING, PROP_ROW_SPACING, PROP_MIN_CHILDREN_PER_LINE, PROP_MAX_CHILDREN_PER_LINE, PROP_SELECTION_MODE, PROP_ACTIVATE_ON_SINGLE_CLICK }; typedef struct _EggFlowBoxChildInfo EggFlowBoxChildInfo; struct _EggFlowBoxPrivate { GtkOrientation orientation; GtkAlign halign_policy; GtkAlign valign_policy; guint homogeneous : 1; guint activate_on_single_click : 1; GtkSelectionMode selection_mode; guint row_spacing; guint column_spacing; gboolean active_child_active; EggFlowBoxChildInfo *active_child; guint16 min_children_per_line; guint16 max_children_per_line; GSequence *children; GHashTable *child_hash; }; struct _EggFlowBoxChildInfo { GSequenceIter *iter; GtkWidget *widget; guint selected : 1; GdkRectangle area; }; static guint signals[LAST_SIGNAL] = { 0 }; G_DEFINE_TYPE_WITH_CODE (EggFlowBox, egg_flow_box, GTK_TYPE_CONTAINER, G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)) #define ORIENTATION_ALIGN_POLICY(box) \ (((EggFlowBox *)(box))->priv->orientation == GTK_ORIENTATION_HORIZONTAL ? \ ((EggFlowBox *)(box))->priv->halign_policy : \ ((EggFlowBox *)(box))->priv->valign_policy) #define OPPOSING_ORIENTATION_ALIGN_POLICY(box) \ (((EggFlowBox *)(box))->priv->orientation == GTK_ORIENTATION_HORIZONTAL ? \ ((EggFlowBox *)(box))->priv->valign_policy : \ ((EggFlowBox *)(box))->priv->halign_policy) static EggFlowBoxChildInfo* egg_flow_box_child_info_new (GtkWidget *widget) { EggFlowBoxChildInfo *info; info = g_new0 (EggFlowBoxChildInfo, 1); info->widget = g_object_ref (widget); return info; } static void egg_flow_box_child_info_free (EggFlowBoxChildInfo *info) { g_clear_object (&info->widget); g_free (info); } static EggFlowBoxChildInfo* egg_flow_box_lookup_info (EggFlowBox *flow_box, GtkWidget* child) { EggFlowBoxPrivate *priv = flow_box->priv; return g_hash_table_lookup (priv->child_hash, child); } /** * egg_flow_box_get_homogeneous: * @box: a #EggFlowBox * * Returns whether the box is homogeneous (all children are the * same size). See gtk_box_set_homogeneous(). * * Return value: %TRUE if the box is homogeneous. **/ gboolean egg_flow_box_get_homogeneous (EggFlowBox *box) { g_return_val_if_fail (GTK_IS_BOX (box), FALSE); return box->priv->homogeneous; } /** * egg_flow_box_set_homogeneous: * @box: a #EggFlowBox * @homogeneous: a boolean value, %TRUE to create equal allotments, * %FALSE for variable allotments * * Sets the #EggFlowBox:homogeneous property of @box, controlling * whether or not all children of @box are given equal space * in the box. */ void egg_flow_box_set_homogeneous (EggFlowBox *box, gboolean homogeneous) { EggFlowBoxPrivate *priv; g_return_if_fail (EGG_IS_FLOW_BOX (box)); priv = box->priv; if ((homogeneous ? TRUE : FALSE) != priv->homogeneous) { priv->homogeneous = homogeneous ? TRUE : FALSE; g_object_notify (G_OBJECT (box), "homogeneous"); gtk_widget_queue_resize (GTK_WIDGET (box)); } } static gint get_visible_children (EggFlowBox *box) { EggFlowBoxPrivate *priv = box->priv; GSequenceIter *iter; gint i = 0; for (iter = g_sequence_get_begin_iter (priv->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { EggFlowBoxChildInfo *child_info; GtkWidget *child; child_info = g_sequence_get (iter); child = child_info->widget; if (!gtk_widget_get_visible (child)) continue; i++; } return i; } /* Used in columned modes where all items share at least their * equal widths or heights */ static void get_average_item_size (EggFlowBox *box, GtkOrientation orientation, gint *min_size, gint *nat_size) { EggFlowBoxPrivate *priv = box->priv; GSequenceIter *iter; gint max_min_size = 0; gint max_nat_size = 0; for (iter = g_sequence_get_begin_iter (priv->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { EggFlowBoxChildInfo *child_info; GtkWidget *child; gint child_min, child_nat; child_info = g_sequence_get (iter); child = child_info->widget; if (!gtk_widget_get_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 (EggFlowBox *box, GtkOrientation orientation, gint item_size, gint *min_item_size, gint *nat_item_size) { EggFlowBoxPrivate *priv = box->priv; GSequenceIter *iter; gint max_min_size = 0; gint max_nat_size = 0; for (iter = g_sequence_get_begin_iter (priv->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { EggFlowBoxChildInfo *child_info; GtkWidget *child; gint child_min, child_nat; child_info = g_sequence_get (iter); child = child_info->widget; if (!gtk_widget_get_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 itterating over a list of children, note the new index is returned) */ static GSequenceIter * get_largest_size_for_line_in_opposing_orientation (EggFlowBox *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; EggFlowBoxChildInfo *child_info; gint child_min, child_nat, this_item_size; child_info = g_sequence_get (iter); child = child_info->widget; if (!gtk_widget_get_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_POLICY (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 (EggFlowBox *box, GtkOrientation orientation, gint line_length, gint item_spacing, gint n_children, GtkRequestedSize *item_sizes) { EggFlowBoxPrivate *priv = box->priv; 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 (priv->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter), i++) { EggFlowBoxChildInfo *child_info; GtkWidget *child; GtkAlign item_align; gint child_min, child_nat; gint position; child_info = g_sequence_get (iter); child = child_info->widget; if (!gtk_widget_get_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_POLICY (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); } 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 (EggFlowBox *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 egg_flow_box_real_size_allocate (GtkWidget *widget, GtkAllocation *allocation) { EggFlowBox *box = EGG_FLOW_BOX (widget); EggFlowBoxPrivate *priv = box->priv; GtkAllocation child_allocation; gint avail_size, avail_other_size, min_items, item_spacing, line_spacing; GtkAlign item_align; GtkAlign line_align; GdkWindow *window; 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; GtkStyleContext *context; gint focus_width; gint focus_pad; child_allocation.x = 0; child_allocation.y = 0; child_allocation.width = 0; child_allocation.height = 0; 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); context = gtk_widget_get_style_context (GTK_WIDGET (box)); gtk_style_context_get_style (context, "focus-line-width", &focus_width, "focus-padding", &focus_pad, NULL); child_allocation.x = 0 + focus_width + focus_pad; child_allocation.y = 0; child_allocation.width = allocation->width - 2 * (focus_width + focus_pad); 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_POLICY (box); line_align = OPPOSING_ORIENTATION_ALIGN_POLICY (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_average_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++) { 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. */ /* 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 = item_offset = 0; /* 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)) { EggFlowBoxChildInfo *child_info; GtkWidget *child; gint position; gint this_item_size; child_info = g_sequence_get (iter); child = child_info->widget; if (!gtk_widget_get_visible (child)) { child_info->area.x = child_allocation.x; child_info->area.y = child_allocation.y; child_info->area.width = 0; child_info->area.height = 0; 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 = 0; 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 = allocation->x + item_offset; child_allocation.y = allocation->y + line_offset; child_allocation.width = this_item_size; child_allocation.height = this_line_size; } else /* GTK_ORIENTATION_VERTICAL */ { child_allocation.x = allocation->x + line_offset; child_allocation.y = allocation->y + item_offset; child_allocation.width = this_line_size; child_allocation.height = this_item_size; } child_info->area.x = child_allocation.x; child_info->area.y = child_allocation.y; child_info->area.width = child_allocation.width; child_info->area.height = child_allocation.height; 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); } static void egg_flow_box_real_add (GtkContainer *container, GtkWidget *child) { EggFlowBox *box = EGG_FLOW_BOX (container); EggFlowBoxPrivate *priv = box->priv; EggFlowBoxChildInfo *info; GSequenceIter *iter = NULL; g_return_if_fail (EGG_IS_FLOW_BOX (box)); g_return_if_fail (GTK_IS_WIDGET (child)); info = egg_flow_box_child_info_new (child); g_hash_table_insert (priv->child_hash, child, info); iter = g_sequence_append (priv->children, info); info->iter = iter; gtk_widget_set_parent (child, GTK_WIDGET (box)); } static void egg_flow_box_real_remove (GtkContainer *container, GtkWidget *child) { EggFlowBox *box = EGG_FLOW_BOX (container); EggFlowBoxPrivate *priv = box->priv; gboolean was_visible; gboolean was_selected; EggFlowBoxChildInfo *child_info; g_return_if_fail (child != NULL); was_visible = gtk_widget_get_visible (child); child_info = egg_flow_box_lookup_info (box, child); if (child_info == NULL) { g_warning ("Tried to remove non-child %p\n", child); return; } was_selected = child_info->selected; gtk_widget_unparent (child); g_hash_table_remove (priv->child_hash, child); g_sequence_remove (child_info->iter); if (was_visible && gtk_widget_get_visible (GTK_WIDGET (box))) gtk_widget_queue_resize (GTK_WIDGET (box)); if (was_selected) g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); } static void egg_flow_box_real_forall (GtkContainer *container, gboolean include_internals, GtkCallback callback, gpointer callback_target) { EggFlowBox *box = EGG_FLOW_BOX (container); EggFlowBoxPrivate *priv = box->priv; GSequenceIter *iter; EggFlowBoxChildInfo *child_info; iter = g_sequence_get_begin_iter (priv->children); while (!g_sequence_iter_is_end (iter)) { child_info = g_sequence_get (iter); iter = g_sequence_iter_next (iter); callback (child_info->widget, callback_target); } } static GType egg_flow_box_real_child_type (GtkContainer *container) { return GTK_TYPE_WIDGET; } static GtkSizeRequestMode egg_flow_box_real_get_request_mode (GtkWidget *widget) { EggFlowBox *box = EGG_FLOW_BOX (widget); EggFlowBoxPrivate *priv = box->priv; return (priv->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 (EggFlowBox *box, GtkOrientation orientation, gint line_length, gint *min_size, gint *nat_size) { EggFlowBoxPrivate *priv = box->priv; 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 = priv->column_spacing; else spacing = priv->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 (priv->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { EggFlowBoxChildInfo *child_info; GtkWidget *child; gint child_min, child_nat; child_info = g_sequence_get (iter); child = child_info->widget; if (!gtk_widget_get_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 egg_flow_box_real_get_preferred_width (GtkWidget *widget, gint *minimum_size, gint *natural_size) { EggFlowBox *box = EGG_FLOW_BOX (widget); EggFlowBoxPrivate *priv = box->priv; 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_average_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_average_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_WIDGET_GET_CLASS (widget)->get_preferred_height (widget, &min_height, NULL); GTK_WIDGET_GET_CLASS (widget)->get_preferred_width_for_height (widget, min_height, &min_width, &nat_width); } if (minimum_size) *minimum_size = min_width; if (natural_size) *natural_size = nat_width; } static void egg_flow_box_real_get_preferred_height (GtkWidget *widget, gint *minimum_size, gint *natural_size) { EggFlowBox *box = EGG_FLOW_BOX (widget); EggFlowBoxPrivate *priv = box->priv; 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_WIDGET_GET_CLASS (widget)->get_preferred_width (widget, &min_width, NULL); GTK_WIDGET_GET_CLASS (widget)->get_preferred_height_for_width (widget, min_width, &min_height, &nat_height); } 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_average_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 boxs * give the same height to all children */ { get_average_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; } } if (minimum_size) *minimum_size = min_height; if (natural_size) *natural_size = nat_height; } static void egg_flow_box_real_get_preferred_height_for_width (GtkWidget *widget, gint width, gint *minimum_height, gint *natural_height) { EggFlowBox *box = EGG_FLOW_BOX (widget); EggFlowBoxPrivate *priv = box->priv; 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; /* Make sure its no smaller than the minimum */ GTK_WIDGET_GET_CLASS (widget)->get_preferred_width (widget, &min_width, NULL); avail_size = MAX (width, min_width); if (avail_size <= 0) goto out; get_average_item_size (box, GTK_ORIENTATION_HORIZONTAL, &min_item_width, &nat_item_width); if (nat_item_width <= 0) goto out; /* 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 (priv->halign_policy != 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 = g_sequence_iter_next (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_WIDGET_GET_CLASS (widget)->get_preferred_height (widget, &min_height, &nat_height); } out: if (minimum_height) *minimum_height = min_height; if (natural_height) *natural_height = nat_height; } static void egg_flow_box_real_get_preferred_width_for_height (GtkWidget *widget, gint height, gint *minimum_width, gint *natural_width) { EggFlowBox *box = EGG_FLOW_BOX (widget); EggFlowBoxPrivate *priv = box->priv; 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_WIDGET_GET_CLASS (widget)->get_preferred_width (widget, &min_width, &nat_width); } 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; /* Make sure its no smaller than the minimum */ GTK_WIDGET_GET_CLASS (widget)->get_preferred_height (widget, &min_height, NULL); avail_size = MAX (height, min_height); if (avail_size <= 0) goto out; get_average_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 (priv->valign_policy != 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 = g_sequence_iter_next (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: if (minimum_width) *minimum_width = min_width; if (natural_width) *natural_width = nat_width; } /** * egg_flow_box_set_halign_policy: * @box: An #EggFlowBox * @align: The #GtkAlign to use. * * Sets the horizontal align policy for @box's children. */ void egg_flow_box_set_halign_policy (EggFlowBox *box, GtkAlign align) { EggFlowBoxPrivate *priv; g_return_if_fail (EGG_IS_FLOW_BOX (box)); priv = box->priv; if (priv->halign_policy != align) { priv->halign_policy = align; gtk_widget_queue_resize (GTK_WIDGET (box)); g_object_notify (G_OBJECT (box), "halign-policy"); } } /** * egg_flow_box_get_halign_policy: * @box: An #EggFlowBox * * Gets the horizontal alignment policy. * * Returns: The horizontal #GtkAlign for @box. */ GtkAlign egg_flow_box_get_halign_policy (EggFlowBox *box) { g_return_val_if_fail (EGG_IS_FLOW_BOX (box), FALSE); return box->priv->halign_policy; } /** * egg_flow_box_set_valign_policy: * @box: An #EggFlowBox * @align: The #GtkAlign to use. * * Sets the vertical alignment policy for @box's children. */ void egg_flow_box_set_valign_policy (EggFlowBox *box, GtkAlign align) { EggFlowBoxPrivate *priv; g_return_if_fail (EGG_IS_FLOW_BOX (box)); priv = box->priv; if (priv->valign_policy != align) { priv->valign_policy = align; gtk_widget_queue_resize (GTK_WIDGET (box)); g_object_notify (G_OBJECT (box), "valign-policy"); } } /** * egg_flow_box_get_valign_policy: * @box: An #EggFlowBox * * Gets the vertical alignment policy. * * Returns: The vertical #GtkAlign for @box. */ GtkAlign egg_flow_box_get_valign_policy (EggFlowBox *box) { g_return_val_if_fail (EGG_IS_FLOW_BOX (box), FALSE); return box->priv->valign_policy; } /** * egg_flow_box_set_row_spacing: * @box: An #EggFlowBox * @spacing: The spacing to use. * * Sets the vertical space to add between children. */ void egg_flow_box_set_row_spacing (EggFlowBox *box, guint spacing) { EggFlowBoxPrivate *priv; g_return_if_fail (EGG_IS_FLOW_BOX (box)); priv = box->priv; if (priv->row_spacing != spacing) { priv->row_spacing = spacing; gtk_widget_queue_resize (GTK_WIDGET (box)); g_object_notify (G_OBJECT (box), "vertical-spacing"); } } /** * egg_flow_box_get_row_spacing: * @box: An #EggFlowBox * * Gets the vertical spacing. * * Returns: The vertical spacing. */ guint egg_flow_box_get_row_spacing (EggFlowBox *box) { g_return_val_if_fail (EGG_IS_FLOW_BOX (box), FALSE); return box->priv->row_spacing; } /** * egg_flow_box_set_column_spacing: * @box: An #EggFlowBox * @spacing: The spacing to use. * * Sets the horizontal space to add between children. */ void egg_flow_box_set_column_spacing (EggFlowBox *box, guint spacing) { EggFlowBoxPrivate *priv; g_return_if_fail (EGG_IS_FLOW_BOX (box)); priv = box->priv; if (priv->column_spacing != spacing) { priv->column_spacing = spacing; gtk_widget_queue_resize (GTK_WIDGET (box)); g_object_notify (G_OBJECT (box), "horizontal-spacing"); } } /** * egg_flow_box_get_column_spacing: * @box: An #EggFlowBox * * Gets the horizontal spacing. * * Returns: The horizontal spacing. */ guint egg_flow_box_get_column_spacing (EggFlowBox *box) { g_return_val_if_fail (EGG_IS_FLOW_BOX (box), FALSE); return box->priv->column_spacing; } /** * egg_flow_box_set_min_children_per_line: * @box: An #EggFlowBox * @n_children: The minimum amount of children per line. * * Sets the minimum amount of children to line up * in @box's orientation before flowping. */ void egg_flow_box_set_min_children_per_line (EggFlowBox *box, guint n_children) { EggFlowBoxPrivate *priv; g_return_if_fail (EGG_IS_FLOW_BOX (box)); priv = box->priv; if (priv->min_children_per_line != n_children) { priv->min_children_per_line = n_children; gtk_widget_queue_resize (GTK_WIDGET (box)); g_object_notify (G_OBJECT (box), "min-children-per-line"); } } /** * egg_flow_box_get_min_children_per_line: * @box: An #EggFlowBox * * Gets the minimum amount of children per line. * * Returns: The minimum amount of children per line. */ guint egg_flow_box_get_min_children_per_line (EggFlowBox *box) { g_return_val_if_fail (EGG_IS_FLOW_BOX (box), FALSE); return box->priv->min_children_per_line; } /** * egg_flow_box_set_max_children_per_line: * @box: An #EggFlowBox * @n_children: The natural amount of children per line. * * Sets the natural length of items to request and * allocate space for in @box's orientation. * * Setting the natural amount of children per line * limits the overall natural size request to be no more * than @n_children items long in the given orientation. */ void egg_flow_box_set_max_children_per_line (EggFlowBox *box, guint n_children) { EggFlowBoxPrivate *priv; g_return_if_fail (EGG_IS_FLOW_BOX (box)); priv = box->priv; if (priv->max_children_per_line != n_children) { priv->max_children_per_line = n_children; gtk_widget_queue_resize (GTK_WIDGET (box)); g_object_notify (G_OBJECT (box), "max-children-per-line"); } } /** * egg_flow_box_get_max_children_per_line: * @box: An #EggFlowBox * * Gets the natural amount of children per line. * * Returns: The natural amount of children per line. */ guint egg_flow_box_get_max_children_per_line (EggFlowBox *box) { g_return_val_if_fail (EGG_IS_FLOW_BOX (box), FALSE); return box->priv->max_children_per_line; } /** * egg_flow_box_set_activate_on_single_click: * @box: An #EggFlowBox * @single: %TRUE to emit child-activated on a single click * * Causes the #EggFlowBox::child-activated signal to be emitted on * a single click instead of a double click. **/ void egg_flow_box_set_activate_on_single_click (EggFlowBox *box, gboolean single) { g_return_if_fail (EGG_IS_FLOW_BOX (box)); single = single != FALSE; if (box->priv->activate_on_single_click == single) return; box->priv->activate_on_single_click = single; g_object_notify (G_OBJECT (box), "activate-on-single-click"); } /** * egg_flow_box_get_activate_on_single_click: * @box: An #EggFlowBox * * Gets the setting set by egg_flow_box_set_activate_on_single_click(). * * Return value: %TRUE if child-activated will be emitted on a single click **/ gboolean egg_flow_box_get_activate_on_single_click (EggFlowBox *box) { g_return_val_if_fail (EGG_IS_FLOW_BOX (box), FALSE); return box->priv->activate_on_single_click; } static void egg_flow_box_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { EggFlowBox *box = EGG_FLOW_BOX (object); EggFlowBoxPrivate *priv = box->priv; 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_HALIGN_POLICY: g_value_set_enum (value, priv->halign_policy); break; case PROP_VALIGN_POLICY: g_value_set_enum (value, priv->valign_policy); 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 egg_flow_box_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { EggFlowBox *box = EGG_FLOW_BOX (object); EggFlowBoxPrivate *priv = box->priv; switch (prop_id) { case PROP_ORIENTATION: priv->orientation = g_value_get_enum (value); /* Re-box the children in the new orientation */ gtk_widget_queue_resize (GTK_WIDGET (box)); break; case PROP_HOMOGENEOUS: egg_flow_box_set_homogeneous (box, g_value_get_boolean (value)); break; case PROP_HALIGN_POLICY: egg_flow_box_set_halign_policy (box, g_value_get_enum (value)); break; case PROP_VALIGN_POLICY: egg_flow_box_set_valign_policy (box, g_value_get_enum (value)); break; case PROP_COLUMN_SPACING: egg_flow_box_set_column_spacing (box, g_value_get_uint (value)); break; case PROP_ROW_SPACING: egg_flow_box_set_row_spacing (box, g_value_get_uint (value)); break; case PROP_MIN_CHILDREN_PER_LINE: egg_flow_box_set_min_children_per_line (box, g_value_get_uint (value)); break; case PROP_MAX_CHILDREN_PER_LINE: egg_flow_box_set_max_children_per_line (box, g_value_get_uint (value)); break; case PROP_SELECTION_MODE: egg_flow_box_set_selection_mode (box, g_value_get_enum (value)); break; case PROP_ACTIVATE_ON_SINGLE_CLICK: egg_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 EggFlowBoxChildInfo * egg_flow_box_find_child_at_pos (EggFlowBox *box, gint x, gint y) { EggFlowBoxPrivate *priv = box->priv; EggFlowBoxChildInfo *child_info; GSequenceIter *iter; EggFlowBoxChildInfo *info; child_info = NULL; for (iter = g_sequence_get_begin_iter (priv->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { info = (EggFlowBoxChildInfo *) g_sequence_get (iter); if (x >= info->area.x && x < (info->area.x + info->area.width) && y >= info->area.y && y < (info->area.y + info->area.height)) { child_info = info; break; } } return child_info; } static gboolean egg_flow_box_real_button_press_event (GtkWidget *widget, GdkEventButton *event) { EggFlowBox *box = EGG_FLOW_BOX (widget); EggFlowBoxPrivate *priv = box->priv; if (event->button == 1) { EggFlowBoxChildInfo *child_info; child_info = egg_flow_box_find_child_at_pos (box, event->x, event->y); if (child_info != NULL) { priv->active_child = child_info; priv->active_child_active = TRUE; if (event->type == GDK_2BUTTON_PRESS && !priv->activate_on_single_click) g_signal_emit (box, signals[CHILD_ACTIVATED], 0, child_info->widget); } } return FALSE; } static void egg_flow_box_queue_draw_child (EggFlowBox *box, EggFlowBoxChildInfo *child_info) { GdkRectangle rect; GdkWindow *window; rect = child_info->area; window = gtk_widget_get_window (GTK_WIDGET (box)); gdk_window_invalidate_rect (window, &rect, TRUE); } static gboolean egg_flow_box_unselect_all_internal (EggFlowBox *box) { EggFlowBoxChildInfo *child_info; GSequenceIter *iter; gboolean dirty = FALSE; if (box->priv->selection_mode == GTK_SELECTION_NONE) return FALSE; for (iter = g_sequence_get_begin_iter (box->priv->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { child_info = g_sequence_get (iter); if (child_info->selected) { child_info->selected = FALSE; egg_flow_box_queue_draw_child (box, child_info); dirty = TRUE; } } } static void egg_flow_box_select_child_info (EggFlowBox *box, EggFlowBoxChildInfo *child_info) { if (child_info->selected) return; if (box->priv->selection_mode == GTK_SELECTION_NONE) return; else if (box->priv->selection_mode != GTK_SELECTION_MULTIPLE) egg_flow_box_unselect_all_internal (box); child_info->selected = TRUE; g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); egg_flow_box_queue_draw_child (box, child_info); } static void egg_flow_box_select_and_activate (EggFlowBox *box, EggFlowBoxChildInfo *child_info) { GtkWidget *w = NULL; if (child_info != NULL) w = child_info->widget; egg_flow_box_select_child_info (box, child_info); if (w != NULL) g_signal_emit (box, signals[CHILD_ACTIVATED], 0, w); } static gboolean egg_flow_box_real_button_release_event (GtkWidget *widget, GdkEventButton *event) { EggFlowBox *box = EGG_FLOW_BOX (widget); EggFlowBoxPrivate *priv = box->priv; if (event->button == 1) { if (priv->active_child != NULL && priv->active_child_active) { if (priv->activate_on_single_click) egg_flow_box_select_and_activate (box, priv->active_child); } priv->active_child = NULL; priv->active_child_active = FALSE; gtk_widget_queue_draw (GTK_WIDGET (box)); } return FALSE; } typedef struct { EggFlowBoxChildInfo *child; GtkStateFlags state; } ChildFlags; static ChildFlags * child_flags_find_or_add (ChildFlags *array, int *array_length, EggFlowBoxChildInfo *to_find) { gint i; for (i = 0; i < *array_length; i++) { if (array[i].child == to_find) return &array[i]; } *array_length = *array_length + 1; array[*array_length - 1].child = to_find; array[*array_length - 1].state = 0; return &array[*array_length - 1]; } static gboolean egg_flow_box_real_draw (GtkWidget *widget, cairo_t *cr) { EggFlowBox *box = EGG_FLOW_BOX (widget); EggFlowBoxPrivate *priv = box->priv; GtkAllocation allocation = {0}; GtkStyleContext* context; GtkStateFlags state; gint focus_pad; int i; GSequenceIter *iter; gtk_widget_get_allocation (GTK_WIDGET (box), &allocation); context = gtk_widget_get_style_context (GTK_WIDGET (box)); state = gtk_widget_get_state_flags (widget); gtk_render_background (context, cr, (gdouble) 0, (gdouble) 0, (gdouble) allocation.width, (gdouble) allocation.height); for (iter = g_sequence_get_begin_iter (box->priv->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { EggFlowBoxChildInfo *child_info; ChildFlags flags[3], *found; gint flags_length; child_info = g_sequence_get (iter); if (child_info->selected) { flags_length = 0; found = child_flags_find_or_add (flags, &flags_length, child_info); found->state |= (state | GTK_STATE_FLAG_SELECTED); for (i = 0; i < flags_length; i++) { ChildFlags *flag = &flags[i]; gtk_style_context_save (context); gtk_style_context_set_state (context, flag->state); gtk_render_background (context, cr, flag->child->area.x, flag->child->area.y, flag->child->area.width, flag->child->area.height); gtk_style_context_restore (context); } } } GTK_WIDGET_CLASS (egg_flow_box_parent_class)->draw ((GtkWidget *) G_TYPE_CHECK_INSTANCE_CAST (box, GTK_TYPE_CONTAINER, GtkContainer), cr); return TRUE; } static void egg_flow_box_real_realize (GtkWidget *widget) { EggFlowBox *box = EGG_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_EXPOSURE_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_style_context_set_background (gtk_widget_get_style_context (GTK_WIDGET (box)), window); gdk_window_set_user_data (window, (GObject*) box); gtk_widget_set_window (GTK_WIDGET (box), window); /* Passes ownership */ } static void egg_flow_box_finalize (GObject *obj) { EggFlowBox *flow_box = EGG_FLOW_BOX (obj); EggFlowBoxPrivate *priv = flow_box->priv; g_sequence_free (priv->children); g_hash_table_unref (priv->child_hash); G_OBJECT_CLASS (egg_flow_box_parent_class)->finalize (obj); } static void egg_flow_box_class_init (EggFlowBoxClass *class) { GObjectClass *object_class = G_OBJECT_CLASS (class); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); GtkContainerClass *container_class = GTK_CONTAINER_CLASS (class); object_class->finalize = egg_flow_box_finalize; object_class->get_property = egg_flow_box_get_property; object_class->set_property = egg_flow_box_set_property; widget_class->size_allocate = egg_flow_box_real_size_allocate; widget_class->realize = egg_flow_box_real_realize; widget_class->draw = egg_flow_box_real_draw; widget_class->button_press_event = egg_flow_box_real_button_press_event; widget_class->button_release_event = egg_flow_box_real_button_release_event; widget_class->get_request_mode = egg_flow_box_real_get_request_mode; widget_class->get_preferred_width = egg_flow_box_real_get_preferred_width; widget_class->get_preferred_height = egg_flow_box_real_get_preferred_height; widget_class->get_preferred_height_for_width = egg_flow_box_real_get_preferred_height_for_width; widget_class->get_preferred_width_for_height = egg_flow_box_real_get_preferred_width_for_height; container_class->add = egg_flow_box_real_add; container_class->remove = egg_flow_box_real_remove; container_class->forall = egg_flow_box_real_forall; container_class->child_type = egg_flow_box_real_child_type; gtk_container_class_handle_border_width (container_class); g_object_class_override_property (object_class, PROP_ORIENTATION, "orientation"); g_object_class_install_property (object_class, 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_object_class_install_property (object_class, 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_object_class_install_property (object_class, PROP_HOMOGENEOUS, g_param_spec_boolean ("homogeneous", P_("Homogeneous"), P_("Whether the children should all be the same size"), FALSE, G_PARAM_READWRITE)); /** * EggFlowBox:halign-policy: * * The #GtkAlign to used to define what is done with extra * space in a given orientation. */ g_object_class_install_property (object_class, PROP_HALIGN_POLICY, g_param_spec_enum ("halign-policy", P_("Horizontal align policy"), P_("The align policy horizontally"), GTK_TYPE_ALIGN, GTK_ALIGN_FILL, G_PARAM_READWRITE)); /** * EggFlowBox:valign-policy: * * The #GtkAlign to used to define what is done with extra * space in a given orientation. */ g_object_class_install_property (object_class, PROP_VALIGN_POLICY, g_param_spec_enum ("valign-policy", P_("Vertical align policy"), P_("The align policy vertically"), GTK_TYPE_ALIGN, GTK_ALIGN_START, G_PARAM_READWRITE)); /** * EggFlowBox: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. * */ g_object_class_install_property (object_class, 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, 65535, 0, G_PARAM_READWRITE)); /** * EggFlowBox:max-children-per-line: * * The maximum amount of children to request space for consecutively in the given orientation. * */ g_object_class_install_property (object_class, 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, 65535, 0, G_PARAM_READWRITE)); /** * EggFlowBox:vertical-spacing: * * The amount of vertical space between two children. * */ g_object_class_install_property (object_class, PROP_ROW_SPACING, g_param_spec_uint ("vertical-spacing", P_("Vertical spacing"), P_("The amount of vertical space between two children"), 0, 65535, 0, G_PARAM_READWRITE)); /** * EggFlowBox:horizontal-spacing: * * The amount of horizontal space between two children. * */ g_object_class_install_property (object_class, PROP_COLUMN_SPACING, g_param_spec_uint ("horizontal-spacing", P_("Horizontal spacing"), P_("The amount of horizontal space between two children"), 0, 65535, 0, G_PARAM_READWRITE)); signals[CHILD_ACTIVATED] = g_signal_new ("child-activated", EGG_TYPE_FLOW_BOX, G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (EggFlowBoxClass, child_activated), NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, GTK_TYPE_WIDGET); signals[SELECTED_CHILDREN_CHANGED] = g_signal_new ("selected-children-changed", EGG_TYPE_FLOW_BOX, G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (EggFlowBoxClass, selected_children_changed), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); g_type_class_add_private (class, sizeof (EggFlowBoxPrivate)); } static void egg_flow_box_init (EggFlowBox *box) { EggFlowBoxPrivate *priv; box->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE (box, EGG_TYPE_FLOW_BOX, EggFlowBoxPrivate); priv->orientation = GTK_ORIENTATION_HORIZONTAL; priv->selection_mode = GTK_SELECTION_SINGLE; priv->halign_policy = GTK_ALIGN_FILL; priv->valign_policy = GTK_ALIGN_START; priv->column_spacing = 0; priv->row_spacing = 0; priv->children = g_sequence_new ((GDestroyNotify)egg_flow_box_child_info_free); priv->child_hash = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, NULL); priv->activate_on_single_click = TRUE; gtk_widget_set_has_window (GTK_WIDGET (box), TRUE); } /** * egg_flow_box_new: * * Creates an #EggFlowBox. * * Returns: A new #EggFlowBox container */ GtkWidget * egg_flow_box_new (void) { return (GtkWidget *)g_object_new (EGG_TYPE_FLOW_BOX, NULL); } static void egg_flow_box_unselect_all (EggFlowBox *box) { gboolean dirty = FALSE; g_return_if_fail (EGG_IS_FLOW_BOX (box)); if (box->priv->selection_mode == GTK_SELECTION_BROWSE) return; dirty = egg_flow_box_unselect_all_internal (box); if (dirty) g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); } /** * egg_flow_box_get_selected_children: * @box: An #EggFlowBox. * * Creates a list of all selected children. * * Return value: (element-type GtkWidget) (transfer container): A #GList containing the #GtkWidget for each selected child. **/ GList * egg_flow_box_get_selected_children (EggFlowBox *box) { EggFlowBoxChildInfo *child_info; GSequenceIter *iter; GList *selected = NULL; g_return_if_fail (box != NULL); for (iter = g_sequence_get_begin_iter (box->priv->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { child_info = g_sequence_get (iter); if (child_info->selected) selected = g_list_prepend (selected, child_info->widget); } return g_list_reverse (selected); } void egg_flow_box_select_child (EggFlowBox *box, GtkWidget *child) { EggFlowBoxChildInfo *child_info; g_return_if_fail (EGG_IS_FLOW_BOX (box)); g_return_if_fail (child != NULL); child_info = egg_flow_box_lookup_info (box, child); if (child_info == NULL) { g_warning ("Tried to select non-child %p\n", child); return; } egg_flow_box_select_child_info (box, child_info); } /** * egg_flow_box_selected_foreach: * @flow_box: An #EggFlowBox. * @func: (scope call): The function to call for each selected child. * @data: User data to pass to the function. * * Calls a function for each selected child. Note that the * selection cannot be modified from within this function. */ void egg_flow_box_selected_foreach (EggFlowBox *box, EggFlowBoxForeachFunc func, gpointer data) { EggFlowBoxChildInfo *child_info; GSequenceIter *iter; g_return_if_fail (EGG_IS_FLOW_BOX (box)); for (iter = g_sequence_get_begin_iter (box->priv->children); !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) { child_info = g_sequence_get (iter); if (child_info->selected) (* func) (box, child_info->widget, data); } } void egg_flow_box_set_selection_mode (EggFlowBox *box, GtkSelectionMode mode) { g_return_if_fail (EGG_IS_FLOW_BOX (box)); if (mode == box->priv->selection_mode) return; if (mode == GTK_SELECTION_NONE || box->priv->selection_mode == GTK_SELECTION_MULTIPLE) egg_flow_box_unselect_all (box); box->priv->selection_mode = mode; g_object_notify (G_OBJECT (box), "selection-mode"); } GtkSelectionMode egg_flow_box_get_selection_mode (EggFlowBox *box) { g_return_val_if_fail (EGG_IS_FLOW_BOX (box), GTK_SELECTION_SINGLE); return box->priv->selection_mode; }