/* sp-multi-paned.c * * Copyright (C) 2016 Christian Hergert * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "sp-multi-paned.h" #define HANDLE_WIDTH 10 #define HANDLE_HEIGHT 10 #define IS_HORIZONTAL(o) (o == GTK_ORIENTATION_HORIZONTAL) /** * SECTION:sp-multi-paned * @title: SpMultiPaned * @short_description: A widget with multiple adjustable panes * * This widget is similar to #GtkPaned except that it allows adding more than * two children to the widget. For each additional child added to the * #SpMultiPaned, an additional resize grip is added. */ typedef struct { /* * The child widget in question. */ GtkWidget *widget; /* * The input only window for resize grip. * Has a cursor associated with it. */ GdkWindow *handle; /* * The position the handle has been dragged to. * This is used to adjust size requests. */ gint position; /* * Cached size requests to avoid extra sizing calls during * the layout procedure. */ GtkRequisition min_req; GtkRequisition nat_req; /* * A cached size allocation used during the size_allocate() * cycle. This allows us to do a first pass to allocate * natural sizes, and then followup when dealing with * expanding children. */ GtkAllocation alloc; /* * If the position field has been set. */ guint position_set : 1; } SpMultiPanedChild; typedef struct { /* * A GArray of SpMultiPanedChild containing everything we need to * do size requests, drag operations, resize handles, and temporary * space needed in such operations. */ GArray *children; /* * The gesture used for dragging resize handles. * * TODO: GtkPaned now uses two gestures, one for mouse and one for touch. * We should do the same as it improved things quite a bit. */ GtkGesturePan *gesture; /* * For GtkOrientable:orientation. */ GtkOrientation orientation; /* * This is the child that is currently being dragged. Keep in mind that * the drag handle is immediately after the child. So the final visible * child has the handle input-only window hidden. */ SpMultiPanedChild *drag_begin; /* * The position (width or height) of the child when the drag began. * We use the pan delta offset to determine what the size should be * by adding (or subtracting) to this value. */ gint drag_begin_position; /* * If we are dragging a handle in a fashion that would shrink the * previous widgets, we need to track how much to subtract from their * target allocations. This is set during the drag operation and used * in allocation_stage_drag_overflow() to adjust the neighbors. */ gint drag_extra_offset; } SpMultiPanedPrivate; typedef struct { SpMultiPanedChild **children; guint n_children; GtkOrientation orientation; GtkAllocation top_alloc; gint avail_width; gint avail_height; gint handle_size; } AllocationState; typedef void (*AllocationStage) (SpMultiPaned *self, AllocationState *state); static void allocation_stage_allocate (SpMultiPaned *self, AllocationState *state); static void allocation_stage_borders (SpMultiPaned *self, AllocationState *state); static void allocation_stage_cache_request (SpMultiPaned *self, AllocationState *state); static void allocation_stage_drag_overflow (SpMultiPaned *self, AllocationState *state); static void allocation_stage_expand (SpMultiPaned *self, AllocationState *state); static void allocation_stage_handles (SpMultiPaned *self, AllocationState *state); static void allocation_stage_minimums (SpMultiPaned *self, AllocationState *state); static void allocation_stage_naturals (SpMultiPaned *self, AllocationState *state); static void allocation_stage_positions (SpMultiPaned *self, AllocationState *state); G_DEFINE_TYPE_EXTENDED (SpMultiPaned, sp_multi_paned, GTK_TYPE_CONTAINER, 0, G_ADD_PRIVATE (SpMultiPaned) G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)) enum { PROP_0, PROP_ORIENTATION, N_PROPS }; enum { CHILD_PROP_0, CHILD_PROP_POSITION, N_CHILD_PROPS }; enum { STYLE_PROP_0, STYLE_PROP_HANDLE_SIZE, N_STYLE_PROPS }; enum { RESIZE_DRAG_BEGIN, RESIZE_DRAG_END, N_SIGNALS }; /* * TODO: An obvious optimization here would be to move the constant * branches outside the loops. */ static GParamSpec *properties [N_PROPS]; static GParamSpec *child_properties [N_CHILD_PROPS]; static GParamSpec *style_properties [N_STYLE_PROPS]; static guint signals [N_SIGNALS]; static AllocationStage allocation_stages[] = { allocation_stage_borders, allocation_stage_cache_request, allocation_stage_minimums, allocation_stage_handles, allocation_stage_positions, allocation_stage_drag_overflow, allocation_stage_naturals, allocation_stage_expand, allocation_stage_allocate, }; static void sp_multi_paned_reset_positions (SpMultiPaned *self) { SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); guint i; g_assert (SP_IS_MULTI_PANED (self)); for (i = 0; i < priv->children->len; i++) { SpMultiPanedChild *child = &g_array_index (priv->children, SpMultiPanedChild, i); child->position = -1; child->position_set = FALSE; gtk_container_child_notify_by_pspec (GTK_CONTAINER (self), child->widget, child_properties [CHILD_PROP_POSITION]); } gtk_widget_queue_resize (GTK_WIDGET (self)); } static SpMultiPanedChild * sp_multi_paned_get_next_visible_child (SpMultiPaned *self, SpMultiPanedChild *child) { SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); guint i; g_assert (SP_IS_MULTI_PANED (self)); g_assert (child != NULL); g_assert (priv->children != NULL); g_assert (priv->children->len > 0); i = child - ((SpMultiPanedChild *)(gpointer)priv->children->data); for (++i; i < priv->children->len; i++) { SpMultiPanedChild *next = &g_array_index (priv->children, SpMultiPanedChild, i); if (gtk_widget_get_visible (next->widget)) return next; } return NULL; } static gboolean sp_multi_paned_is_last_visible_child (SpMultiPaned *self, SpMultiPanedChild *child) { g_assert (SP_IS_MULTI_PANED (self)); g_assert (child != NULL); return !sp_multi_paned_get_next_visible_child (self, child); } static void sp_multi_paned_get_handle_rect (SpMultiPaned *self, SpMultiPanedChild *child, GdkRectangle *handle_rect) { SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); GtkAllocation alloc; g_assert (SP_IS_MULTI_PANED (self)); g_assert (child != NULL); g_assert (handle_rect != NULL); handle_rect->x = -1; handle_rect->y = -1; handle_rect->width = 0; handle_rect->height = 0; if (!gtk_widget_get_visible (child->widget) || !gtk_widget_get_realized (child->widget)) return; if (sp_multi_paned_is_last_visible_child (self, child)) return; gtk_widget_get_allocation (child->widget, &alloc); if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) { handle_rect->x = alloc.x + alloc.width - (HANDLE_WIDTH / 2); handle_rect->width = HANDLE_WIDTH; handle_rect->y = alloc.y; handle_rect->height = alloc.height; } else { handle_rect->x = alloc.x; handle_rect->width = alloc.width; handle_rect->y = alloc.y + alloc.height - (HANDLE_HEIGHT / 2); handle_rect->height = HANDLE_HEIGHT; } } static void sp_multi_paned_create_child_handle (SpMultiPaned *self, SpMultiPanedChild *child) { SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); GdkWindowAttr attributes = { 0 }; GdkDisplay *display; GdkWindow *parent; const char *cursor_name; GdkRectangle handle_rect; g_assert (SP_IS_MULTI_PANED (self)); g_assert (child != NULL); g_assert (child->handle == NULL); display = gtk_widget_get_display (GTK_WIDGET (self)); parent = gtk_widget_get_window (GTK_WIDGET (self)); cursor_name = (priv->orientation == GTK_ORIENTATION_HORIZONTAL) ? "col-resize" : "row-resize"; sp_multi_paned_get_handle_rect (self, child, &handle_rect); attributes.window_type = GDK_WINDOW_CHILD; attributes.wclass = GDK_INPUT_ONLY; attributes.x = handle_rect.x; attributes.y = -handle_rect.y; attributes.width = handle_rect.width; attributes.height = handle_rect.height; attributes.visual = gtk_widget_get_visual (GTK_WIDGET (self)); attributes.event_mask = (GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_POINTER_MOTION_MASK); attributes.cursor = gdk_cursor_new_from_name (display, cursor_name); child->handle = gdk_window_new (parent, &attributes, GDK_WA_CURSOR); gtk_widget_register_window (GTK_WIDGET (self), child->handle); g_clear_object (&attributes.cursor); } static gint sp_multi_paned_calc_handle_size (SpMultiPaned *self) { SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); gint visible_children = 0; gint handle_size = 1; guint i; g_assert (SP_IS_MULTI_PANED (self)); gtk_widget_style_get (GTK_WIDGET (self), "handle-size", &handle_size, NULL); for (i = 0; i < priv->children->len; i++) { SpMultiPanedChild *child = &g_array_index (priv->children, SpMultiPanedChild, i); if (gtk_widget_get_visible (child->widget)) visible_children++; } return MAX (0, (visible_children - 1) * handle_size); } static void sp_multi_paned_destroy_child_handle (SpMultiPaned *self, SpMultiPanedChild *child) { g_assert (SP_IS_MULTI_PANED (self)); g_assert (child != NULL); if (child->handle != NULL) { gdk_window_destroy (child->handle); child->handle = NULL; } } static void sp_multi_paned_update_child_handles (SpMultiPaned *self) { SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); GtkWidget *widget = GTK_WIDGET (self); if (gtk_widget_get_realized (widget)) { GdkCursor *cursor; guint i; if (gtk_widget_is_sensitive (widget)) cursor = gdk_cursor_new_from_name (gtk_widget_get_display (widget), priv->orientation == GTK_ORIENTATION_HORIZONTAL ? "col-resize" : "row-resize"); else cursor = NULL; for (i = 0; i < priv->children->len; i++) { SpMultiPanedChild *child = &g_array_index (priv->children, SpMultiPanedChild, i); gdk_window_set_cursor (child->handle, cursor); } if (cursor) g_object_unref (cursor); } } static SpMultiPanedChild * sp_multi_paned_get_child (SpMultiPaned *self, GtkWidget *widget) { SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); guint i; g_assert (SP_IS_MULTI_PANED (self)); g_assert (GTK_IS_WIDGET (widget)); for (i = 0; i < priv->children->len; i++) { SpMultiPanedChild *child = &g_array_index (priv->children, SpMultiPanedChild, i); if (child->widget == widget) return child; } g_assert_not_reached (); return NULL; } static gint sp_multi_paned_get_child_position (SpMultiPaned *self, GtkWidget *widget) { SpMultiPanedChild *child; g_assert (SP_IS_MULTI_PANED (self)); g_assert (GTK_IS_WIDGET (widget)); child = sp_multi_paned_get_child (self, widget); return child->position; } static void sp_multi_paned_set_child_position (SpMultiPaned *self, GtkWidget *widget, gint position) { SpMultiPanedChild *child; g_assert (SP_IS_MULTI_PANED (self)); g_assert (GTK_IS_WIDGET (widget)); g_assert (position >= -1); child = sp_multi_paned_get_child (self, widget); if (child->position != position) { child->position = position; child->position_set = (position != -1); gtk_container_child_notify_by_pspec (GTK_CONTAINER (self), widget, child_properties [CHILD_PROP_POSITION]); gtk_widget_queue_resize (GTK_WIDGET (self)); } } static void sp_multi_paned_add (GtkContainer *container, GtkWidget *widget) { SpMultiPaned *self = (SpMultiPaned *)container; SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); SpMultiPanedChild child = { 0 }; g_assert (SP_IS_MULTI_PANED (self)); g_assert (GTK_IS_WIDGET (widget)); child.widget = g_object_ref_sink (widget); child.position = -1; if (gtk_widget_get_realized (GTK_WIDGET (self))) sp_multi_paned_create_child_handle (self, &child); gtk_widget_set_parent (widget, GTK_WIDGET (self)); g_array_append_val (priv->children, child); sp_multi_paned_reset_positions (self); gtk_gesture_set_state (GTK_GESTURE (priv->gesture), GTK_EVENT_SEQUENCE_DENIED); } static void sp_multi_paned_remove (GtkContainer *container, GtkWidget *widget) { SpMultiPaned *self = (SpMultiPaned *)container; SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); guint i; g_assert (SP_IS_MULTI_PANED (self)); g_assert (GTK_IS_WIDGET (widget)); for (i = 0; i < priv->children->len; i++) { SpMultiPanedChild *child = &g_array_index (priv->children, SpMultiPanedChild, i); if (child->widget == widget) { sp_multi_paned_destroy_child_handle (self, child); g_array_remove_index (priv->children, i); child = NULL; gtk_widget_unparent (widget); g_object_unref (widget); break; } } sp_multi_paned_reset_positions (self); gtk_gesture_set_state (GTK_GESTURE (priv->gesture), GTK_EVENT_SEQUENCE_DENIED); } static void sp_multi_paned_forall (GtkContainer *container, gboolean include_internals, GtkCallback callback, gpointer user_data) { SpMultiPaned *self = (SpMultiPaned *)container; SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); gint i; g_assert (SP_IS_MULTI_PANED (self)); g_assert (callback != NULL); for (i = priv->children->len; i > 0; i--) { SpMultiPanedChild *child = &g_array_index (priv->children, SpMultiPanedChild, i - 1); callback (child->widget, user_data); } } static GtkSizeRequestMode sp_multi_paned_get_request_mode (GtkWidget *widget) { SpMultiPaned *self = (SpMultiPaned *)widget; SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); g_assert (SP_IS_MULTI_PANED (self)); return (priv->orientation == GTK_ORIENTATION_HORIZONTAL) ? GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT : GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; } static void sp_multi_paned_get_preferred_height (GtkWidget *widget, gint *min_height, gint *nat_height) { SpMultiPaned *self = (SpMultiPaned *)widget; SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); guint i; gint real_min_height = 0; gint real_nat_height = 0; g_assert (SP_IS_MULTI_PANED (self)); g_assert (min_height != NULL); g_assert (nat_height != NULL); for (i = 0; i < priv->children->len; i++) { SpMultiPanedChild *child = &g_array_index (priv->children, SpMultiPanedChild, i); gint child_min_height = 0; gint child_nat_height = 0; if (gtk_widget_get_visible (child->widget)) { gtk_widget_get_preferred_height (child->widget, &child_min_height, &child_nat_height); if (priv->orientation == GTK_ORIENTATION_VERTICAL) { real_min_height += child_min_height; real_nat_height += child_nat_height; } else { real_min_height = MAX (real_min_height, child_min_height); real_nat_height = MAX (real_nat_height, child_nat_height); } } } if (priv->orientation == GTK_ORIENTATION_VERTICAL) { gint handle_size = sp_multi_paned_calc_handle_size (self); real_min_height += handle_size; real_nat_height += handle_size; } *min_height = real_min_height; *nat_height = real_nat_height; } static void sp_multi_paned_get_child_preferred_height_for_width (SpMultiPaned *self, SpMultiPanedChild *children, gint n_children, gint width, gint *min_height, gint *nat_height) { SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); SpMultiPanedChild *child = children; gint child_min_height = 0; gint child_nat_height = 0; gint neighbor_min_height = 0; gint neighbor_nat_height = 0; g_assert (SP_IS_MULTI_PANED (self)); g_assert (n_children == 0 || children != NULL); g_assert (min_height != NULL); g_assert (nat_height != NULL); *min_height = 0; *nat_height = 0; if (n_children == 0) return; if (gtk_widget_get_visible (child->widget)) gtk_widget_get_preferred_height_for_width (child->widget, width, &child_min_height, &child_nat_height); sp_multi_paned_get_child_preferred_height_for_width (self, children + 1, n_children - 1, width, &neighbor_min_height, &neighbor_nat_height); if (priv->orientation == GTK_ORIENTATION_VERTICAL) { *min_height = child_min_height + neighbor_min_height; *nat_height = child_nat_height + neighbor_nat_height; } else { *min_height = MAX (child_min_height, neighbor_min_height); *nat_height = MAX (child_nat_height, neighbor_nat_height); } } static void sp_multi_paned_get_preferred_height_for_width (GtkWidget *widget, gint width, gint *min_height, gint *nat_height) { SpMultiPaned *self = (SpMultiPaned *)widget; SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); g_assert (SP_IS_MULTI_PANED (self)); g_assert (min_height != NULL); g_assert (nat_height != NULL); *min_height = 0; *nat_height = 0; sp_multi_paned_get_child_preferred_height_for_width (self, (SpMultiPanedChild *)(gpointer)priv->children->data, priv->children->len, width, min_height, nat_height); if (priv->orientation == GTK_ORIENTATION_VERTICAL) { gint handle_size = sp_multi_paned_calc_handle_size (self); *min_height += handle_size; *nat_height += handle_size; } } static void sp_multi_paned_get_preferred_width (GtkWidget *widget, gint *min_width, gint *nat_width) { SpMultiPaned *self = (SpMultiPaned *)widget; SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); guint i; gint real_min_width = 0; gint real_nat_width = 0; g_assert (SP_IS_MULTI_PANED (self)); g_assert (min_width != NULL); g_assert (nat_width != NULL); for (i = 0; i < priv->children->len; i++) { SpMultiPanedChild *child = &g_array_index (priv->children, SpMultiPanedChild, i); gint child_min_width = 0; gint child_nat_width = 0; if (gtk_widget_get_visible (child->widget)) { gtk_widget_get_preferred_width (child->widget, &child_min_width, &child_nat_width); if (priv->orientation == GTK_ORIENTATION_VERTICAL) { real_min_width = MAX (real_min_width, child_min_width); real_nat_width = MAX (real_nat_width, child_nat_width); } else { real_min_width += child_min_width; real_nat_width += child_nat_width; } } } if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) { gint handle_size = sp_multi_paned_calc_handle_size (self); real_min_width += handle_size; real_nat_width += handle_size; } *min_width = real_min_width; *nat_width = real_nat_width; } static void sp_multi_paned_get_child_preferred_width_for_height (SpMultiPaned *self, SpMultiPanedChild *children, gint n_children, gint height, gint *min_width, gint *nat_width) { SpMultiPanedChild *child = children; SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); gint child_min_width = 0; gint child_nat_width = 0; gint neighbor_min_width = 0; gint neighbor_nat_width = 0; g_assert (SP_IS_MULTI_PANED (self)); g_assert (n_children == 0 || children != NULL); g_assert (min_width != NULL); g_assert (nat_width != NULL); *min_width = 0; *nat_width = 0; if (n_children == 0) return; if (gtk_widget_get_visible (child->widget)) gtk_widget_get_preferred_width_for_height (child->widget, height, &child_min_width, &child_nat_width); sp_multi_paned_get_child_preferred_width_for_height (self, children + 1, n_children - 1, height, &neighbor_min_width, &neighbor_nat_width); if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) { *min_width = child_min_width + neighbor_min_width; *nat_width = child_nat_width + neighbor_nat_width; } else { *min_width = MAX (child_min_width, neighbor_min_width); *nat_width = MAX (child_nat_width, neighbor_nat_width); } } static void sp_multi_paned_get_preferred_width_for_height (GtkWidget *widget, gint height, gint *min_width, gint *nat_width) { SpMultiPaned *self = (SpMultiPaned *)widget; SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); g_assert (SP_IS_MULTI_PANED (self)); g_assert (min_width != NULL); g_assert (nat_width != NULL); sp_multi_paned_get_child_preferred_width_for_height (self, (SpMultiPanedChild *)(gpointer)priv->children->data, priv->children->len, height, min_width, nat_width); if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) { gint handle_size = sp_multi_paned_calc_handle_size (self); *min_width += handle_size; *nat_width += handle_size; } } static void allocation_stage_handles (SpMultiPaned *self, AllocationState *state) { guint i; g_assert (SP_IS_MULTI_PANED (self)); g_assert (state != NULL); g_assert (state->children != NULL); g_assert (state->n_children > 0); /* * Push each child allocation forward by the sum handle widths up to * their position in the paned. */ for (i = 1; i < state->n_children; i++) { SpMultiPanedChild *child = state->children [i]; if (IS_HORIZONTAL (state->orientation)) child->alloc.x += (i * state->handle_size); else child->alloc.y += (i * state->handle_size); } if (IS_HORIZONTAL (state->orientation)) state->avail_width -= (state->n_children - 1) * state->handle_size; else state->avail_height -= (state->n_children - 1) * state->handle_size; } static void allocation_stage_minimums (SpMultiPaned *self, AllocationState *state) { gint next_x; gint next_y; guint i; g_assert (SP_IS_MULTI_PANED (self)); g_assert (state != NULL); g_assert (state->children != NULL); g_assert (state->n_children > 0); next_x = state->top_alloc.x; next_y = state->top_alloc.y; for (i = 0; i < state->n_children; i++) { SpMultiPanedChild *child = state->children [i]; if (IS_HORIZONTAL (state->orientation)) { child->alloc.x = next_x; child->alloc.y = state->top_alloc.y; child->alloc.width = child->min_req.width; child->alloc.height = state->top_alloc.height; next_x = child->alloc.x + child->alloc.width; state->avail_width -= child->alloc.width; } else { child->alloc.x = state->top_alloc.x; child->alloc.y = next_y; child->alloc.width = state->top_alloc.width; child->alloc.height = child->min_req.height; next_y = child->alloc.y + child->alloc.height; state->avail_height -= child->alloc.height; } } } static void allocation_stage_naturals (SpMultiPaned *self, AllocationState *state) { gint x_adjust = 0; gint y_adjust = 0; guint i; g_assert (SP_IS_MULTI_PANED (self)); g_assert (state != NULL); g_assert (state->children != NULL); g_assert (state->n_children > 0); for (i = 0; i < state->n_children; i++) { SpMultiPanedChild *child = state->children [i]; child->alloc.x += x_adjust; child->alloc.y += y_adjust; if (!child->position_set) { if (IS_HORIZONTAL (state->orientation)) { if (child->nat_req.width > child->alloc.width) { gint adjust = MIN (state->avail_width, child->nat_req.width - child->alloc.width); child->alloc.width += adjust; state->avail_width -= adjust; x_adjust += adjust; } } else { if (child->nat_req.height > child->alloc.height) { gint adjust = MIN (state->avail_height, child->nat_req.height - child->alloc.height); child->alloc.height += adjust; state->avail_height -= adjust; y_adjust += adjust; } } } } } static void allocation_stage_borders (SpMultiPaned *self, AllocationState *state) { gint border_width; g_assert (SP_IS_MULTI_PANED (self)); g_assert (state != NULL); g_assert (state->children != NULL); g_assert (state->n_children > 0); border_width = gtk_container_get_border_width (GTK_CONTAINER (self)); state->top_alloc.x += border_width; state->top_alloc.y += border_width; state->top_alloc.width -= border_width * 2; state->top_alloc.height -= border_width * 2; if (state->top_alloc.width < 0) state->top_alloc.width = 0; if (state->top_alloc.height < 0) state->top_alloc.height = 0; state->avail_width = state->top_alloc.width; state->avail_height = state->top_alloc.height; } static void allocation_stage_cache_request (SpMultiPaned *self, AllocationState *state) { guint i; g_assert (SP_IS_MULTI_PANED (self)); g_assert (state != NULL); g_assert (state->children != NULL); g_assert (state->n_children > 0); for (i = 0; i < state->n_children; i++) { SpMultiPanedChild *child = state->children [i]; if (IS_HORIZONTAL (state->orientation)) gtk_widget_get_preferred_width_for_height (child->widget, state->avail_height, &child->min_req.width, &child->nat_req.width); else gtk_widget_get_preferred_height_for_width (child->widget, state->avail_width, &child->min_req.height, &child->nat_req.height); } } static void allocation_stage_positions (SpMultiPaned *self, AllocationState *state) { gint x_adjust = 0; gint y_adjust = 0; guint i; g_assert (SP_IS_MULTI_PANED (self)); g_assert (state != NULL); g_assert (state->children != NULL); g_assert (state->n_children > 0); /* * Child may have a position set, which happens when dragging the input * window (handle) to resize the child. If so, we want to try to allocate * extra space above the minimum size. */ for (i = 0; i < state->n_children; i++) { SpMultiPanedChild *child = state->children [i]; child->alloc.x += x_adjust; child->alloc.y += y_adjust; if (child->position_set) { if (IS_HORIZONTAL (state->orientation)) { if (child->position > child->alloc.width) { gint adjust = MIN (state->avail_width, child->position - child->alloc.width); child->alloc.width += adjust; state->avail_width -= adjust; x_adjust += adjust; } } else { if (child->position > child->alloc.height) { gint adjust = MIN (state->avail_height, child->position - child->alloc.height); child->alloc.height += adjust; state->avail_height -= adjust; y_adjust += adjust; } } } } } static void allocation_stage_drag_overflow (SpMultiPaned *self, AllocationState *state) { SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); guint drag_index; gint j; gint drag_overflow; g_assert (SP_IS_MULTI_PANED (self)); g_assert (state != NULL); g_assert (state->children != NULL); g_assert (state->n_children > 0); if (priv->drag_begin == NULL) return; drag_overflow = ABS (priv->drag_extra_offset); for (drag_index = 0; drag_index < state->n_children; drag_index++) if (state->children [drag_index] == priv->drag_begin) break; if (drag_index == 0 || drag_index >= state->n_children || state->children [drag_index] != priv->drag_begin) return; /* * If the user is dragging and we have run out of room in the drag * child, then we need to start stealing space from the previous * items. * * This works our way back to the beginning from the drag child * stealing available space and giving it to the child *AFTER* the * drag item. This is because the drag handle is after the drag * child, so logically to the user, its drag_index+1. */ for (j = (int)drag_index; j >= 0 && drag_overflow > 0; j--) { SpMultiPanedChild *child = state->children [j]; guint k; gint adjust = 0; if (IS_HORIZONTAL (state->orientation)) { if (child->alloc.width > child->min_req.width) { if (drag_overflow > (child->alloc.width - child->min_req.width)) adjust = child->alloc.width - child->min_req.width; else adjust = drag_overflow; drag_overflow -= adjust; child->alloc.width -= adjust; state->children [drag_index + 1]->alloc.width += adjust; } } else { if (child->alloc.height > child->min_req.height) { if (drag_overflow > (child->alloc.height - child->min_req.height)) adjust = child->alloc.height - child->min_req.height; else adjust = drag_overflow; drag_overflow -= adjust; child->alloc.height -= adjust; state->children [drag_index + 1]->alloc.height += adjust; } } /* * Now walk back forward and adjust x/y offsets for all of the * children that will have just shifted. */ for (k = j + 1; k <= drag_index + 1; k++) { SpMultiPanedChild *neighbor = state->children [k]; if (IS_HORIZONTAL (state->orientation)) neighbor->alloc.x -= adjust; else neighbor->alloc.y -= adjust; } } } static void allocation_stage_expand (SpMultiPaned *self, AllocationState *state) { gint x_adjust = 0; gint y_adjust = 0; gint n_expand = 0; gint adjust; guint i; g_assert (SP_IS_MULTI_PANED (self)); g_assert (state != NULL); g_assert (state->children != NULL); g_assert (state->n_children > 0); if (state->n_children == 1) { SpMultiPanedChild *child = state->children [0]; /* * Special case for single child, just expand to the * available space. Ideally we would have much shorter * allocation stages in this case. */ if (IS_HORIZONTAL (state->orientation)) { if (gtk_widget_get_hexpand (child->widget)) child->alloc.width = state->top_alloc.width; } else { if (gtk_widget_get_vexpand (child->widget)) child->alloc.height = state->top_alloc.height; } return; } for (i = 0; i < state->n_children; i++) { SpMultiPanedChild *child = state->children [i]; if (!child->position_set) { if (IS_HORIZONTAL (state->orientation)) { if (gtk_widget_get_hexpand (child->widget)) n_expand++; } else { if (gtk_widget_get_vexpand (child->widget)) n_expand++; } } } if (n_expand == 0) return; if (IS_HORIZONTAL (state->orientation)) adjust = state->avail_width / n_expand; else adjust = state->avail_height / n_expand; for (i = 0; i < state->n_children; i++) { SpMultiPanedChild *child = state->children [i]; child->alloc.x += x_adjust; child->alloc.y += y_adjust; if (!child->position_set) { if (IS_HORIZONTAL (state->orientation)) { if (gtk_widget_get_hexpand (child->widget)) { child->alloc.width += adjust; state->avail_height -= adjust; x_adjust += adjust; } } else { if (gtk_widget_get_vexpand (child->widget)) { child->alloc.height += adjust; state->avail_height -= adjust; y_adjust += adjust; } } } } if (IS_HORIZONTAL (state->orientation)) { if (state->avail_width > 0) { state->children [state->n_children - 1]->alloc.width += state->avail_width; state->avail_width = 0; } } else { if (state->avail_height > 0) { state->children [state->n_children - 1]->alloc.height += state->avail_height; state->avail_height = 0; } } } static void allocation_stage_allocate (SpMultiPaned *self, AllocationState *state) { guint i; g_assert (SP_IS_MULTI_PANED (self)); g_assert (state != NULL); g_assert (state->children != NULL); g_assert (state->n_children > 0); for (i = 0; i < state->n_children; i++) { SpMultiPanedChild *child = state->children [i]; gtk_widget_size_allocate (child->widget, &child->alloc); if ((child->handle != NULL) && (state->n_children != (i + 1))) { if (state->orientation == GTK_ORIENTATION_HORIZONTAL) { gdk_window_move_resize (child->handle, child->alloc.x + child->alloc.width - (HANDLE_WIDTH / 2), child->alloc.y, HANDLE_WIDTH, child->alloc.height); } else { gdk_window_move_resize (child->handle, child->alloc.x, child->alloc.y + child->alloc.height - (HANDLE_HEIGHT / 2), child->alloc.width, HANDLE_HEIGHT); } gdk_window_show (child->handle); } } } static void sp_multi_paned_size_allocate (GtkWidget *widget, GtkAllocation *allocation) { SpMultiPaned *self = (SpMultiPaned *)widget; SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); AllocationState state = { 0 }; GPtrArray *children; guint i; g_assert (SP_IS_MULTI_PANED (self)); g_assert (allocation != NULL); GTK_WIDGET_CLASS (sp_multi_paned_parent_class)->size_allocate (widget, allocation); if (priv->children->len == 0) return; children = g_ptr_array_new (); for (i = 0; i < priv->children->len; i++) { SpMultiPanedChild *child = &g_array_index (priv->children, SpMultiPanedChild, i); child->alloc.x = 0; child->alloc.y = 0; child->alloc.width = 0; child->alloc.height = 0; if (child->widget != NULL && gtk_widget_get_child_visible (child->widget) && gtk_widget_get_visible (child->widget)) g_ptr_array_add (children, child); else if (child->handle) gdk_window_hide (child->handle); } state.children = (SpMultiPanedChild **)children->pdata; state.n_children = children->len; if (state.n_children == 0) { g_ptr_array_free (children, TRUE); return; } gtk_widget_style_get (GTK_WIDGET (self), "handle-size", &state.handle_size, NULL); state.orientation = priv->orientation; state.top_alloc = *allocation; state.avail_width = allocation->width; state.avail_height = allocation->height; for (i = 0; i < G_N_ELEMENTS (allocation_stages); i++) allocation_stages [i] (self, &state); g_ptr_array_free (children, TRUE); } static void sp_multi_paned_realize (GtkWidget *widget) { SpMultiPaned *self = (SpMultiPaned *)widget; SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); guint i; g_assert (SP_IS_MULTI_PANED (self)); GTK_WIDGET_CLASS (sp_multi_paned_parent_class)->realize (widget); for (i = 0; i < priv->children->len; i++) { SpMultiPanedChild *child = &g_array_index (priv->children, SpMultiPanedChild, i); sp_multi_paned_create_child_handle (self, child); } } static void sp_multi_paned_unrealize (GtkWidget *widget) { SpMultiPaned *self = (SpMultiPaned *)widget; SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); guint i; g_assert (SP_IS_MULTI_PANED (self)); for (i = 0; i < priv->children->len; i++) { SpMultiPanedChild *child = &g_array_index (priv->children, SpMultiPanedChild, i); sp_multi_paned_destroy_child_handle (self, child); } GTK_WIDGET_CLASS (sp_multi_paned_parent_class)->unrealize (widget); } static void sp_multi_paned_map (GtkWidget *widget) { SpMultiPaned *self = (SpMultiPaned *)widget; SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); guint i; g_assert (SP_IS_MULTI_PANED (self)); GTK_WIDGET_CLASS (sp_multi_paned_parent_class)->map (widget); for (i = 0; i < priv->children->len; i++) { SpMultiPanedChild *child = &g_array_index (priv->children, SpMultiPanedChild, i); gdk_window_show (child->handle); } } static void sp_multi_paned_unmap (GtkWidget *widget) { SpMultiPaned *self = (SpMultiPaned *)widget; SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); guint i; g_assert (SP_IS_MULTI_PANED (self)); for (i = 0; i < priv->children->len; i++) { SpMultiPanedChild *child = &g_array_index (priv->children, SpMultiPanedChild, i); gdk_window_hide (child->handle); } GTK_WIDGET_CLASS (sp_multi_paned_parent_class)->unmap (widget); } static gboolean sp_multi_paned_draw (GtkWidget *widget, cairo_t *cr) { SpMultiPaned *self = (SpMultiPaned *)widget; SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); gboolean ret; g_assert (SP_IS_MULTI_PANED (self)); g_assert (cr != NULL); ret = GTK_WIDGET_CLASS (sp_multi_paned_parent_class)->draw (widget, cr); if (ret != GDK_EVENT_STOP) { GtkStyleContext *style_context; gint handle_size = 1; guint i; style_context = gtk_widget_get_style_context (GTK_WIDGET (self)); gtk_widget_style_get (widget, "handle-size", &handle_size, NULL); for (i = 0; i < priv->children->len; i++) { SpMultiPanedChild *child = &g_array_index (priv->children, SpMultiPanedChild, i); GtkAllocation alloc; if (!gtk_widget_get_realized (child->widget) || !gtk_widget_get_visible (child->widget)) continue; gtk_widget_get_allocation (child->widget, &alloc); if (!sp_multi_paned_is_last_visible_child (self, child)) { if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) gtk_render_handle (style_context, cr, alloc.x + alloc.width, 0, handle_size, alloc.height); else gtk_render_handle (style_context, cr, 0, alloc.y + alloc.height, alloc.width, handle_size); } } } return ret; } static void sp_multi_paned_pan_gesture_drag_begin (SpMultiPaned *self, gdouble x, gdouble y, GtkGesturePan *gesture) { SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); GdkEventSequence *sequence; const GdkEvent *event; guint i; g_assert (SP_IS_MULTI_PANED (self)); g_assert (GTK_IS_GESTURE_PAN (gesture)); g_assert (gesture == priv->gesture); sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture)); event = gtk_gesture_get_last_event (GTK_GESTURE (gesture), sequence); priv->drag_begin = NULL; priv->drag_begin_position = 0; priv->drag_extra_offset = 0; for (i = 0; i < priv->children->len; i++) { SpMultiPanedChild *child = &g_array_index (priv->children, SpMultiPanedChild, i); if (child->handle == event->any.window) { priv->drag_begin = child; break; } /* * We want to make any child before the drag child "sticky" so that it * will no longer have expand adjustments while we perform the drag * operation. */ if (gtk_widget_get_child_visible (child->widget) && gtk_widget_get_visible (child->widget)) { child->position_set = TRUE; child->position = IS_HORIZONTAL (priv->orientation) ? child->alloc.width : child->alloc.height; } } if (priv->drag_begin == NULL) { gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED); return; } if (IS_HORIZONTAL (priv->orientation)) priv->drag_begin_position = priv->drag_begin->alloc.width; else priv->drag_begin_position = priv->drag_begin->alloc.height; gtk_gesture_pan_set_orientation (gesture, priv->orientation); gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED); g_signal_emit (self, signals [RESIZE_DRAG_BEGIN], 0, priv->drag_begin->widget); } static void sp_multi_paned_pan_gesture_drag_end (SpMultiPaned *self, gdouble x, gdouble y, GtkGesturePan *gesture) { SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); GdkEventSequence *sequence; GtkEventSequenceState state; g_assert (SP_IS_MULTI_PANED (self)); g_assert (GTK_IS_GESTURE_PAN (gesture)); g_assert (gesture == priv->gesture); sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture)); state = gtk_gesture_get_sequence_state (GTK_GESTURE (gesture), sequence); if (state != GTK_EVENT_SEQUENCE_CLAIMED) goto cleanup; g_assert (priv->drag_begin != NULL); g_signal_emit (self, signals [RESIZE_DRAG_END], 0, priv->drag_begin->widget); cleanup: priv->drag_begin = NULL; priv->drag_begin_position = 0; priv->drag_extra_offset = 0; } static void sp_multi_paned_pan_gesture_pan (SpMultiPaned *self, GtkPanDirection direction, gdouble offset, GtkGesturePan *gesture) { SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); GtkAllocation alloc; g_assert (SP_IS_MULTI_PANED (self)); g_assert (GTK_IS_GESTURE_PAN (gesture)); g_assert (gesture == priv->gesture); g_assert (priv->drag_begin != NULL); gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) { if (direction == GTK_PAN_DIRECTION_LEFT) offset = -offset; } else { g_assert (priv->orientation == GTK_ORIENTATION_VERTICAL); if (direction == GTK_PAN_DIRECTION_UP) offset = -offset; } if ((priv->drag_begin_position + offset) < 0) priv->drag_extra_offset = (priv->drag_begin_position + offset); else priv->drag_extra_offset = 0; priv->drag_begin->position = MAX (0, priv->drag_begin_position + offset); priv->drag_begin->position_set = TRUE; gtk_widget_queue_allocate (GTK_WIDGET (self)); } static void sp_multi_paned_create_pan_gesture (SpMultiPaned *self) { SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); GtkGesture *gesture; g_assert (SP_IS_MULTI_PANED (self)); g_assert (priv->gesture == NULL); gesture = gtk_gesture_pan_new (GTK_WIDGET (self), GTK_ORIENTATION_HORIZONTAL); gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (gesture), FALSE); gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (gesture), GTK_PHASE_CAPTURE); g_signal_connect_object (gesture, "drag-begin", G_CALLBACK (sp_multi_paned_pan_gesture_drag_begin), self, G_CONNECT_SWAPPED); g_signal_connect_object (gesture, "drag-end", G_CALLBACK (sp_multi_paned_pan_gesture_drag_end), self, G_CONNECT_SWAPPED); g_signal_connect_object (gesture, "pan", G_CALLBACK (sp_multi_paned_pan_gesture_pan), self, G_CONNECT_SWAPPED); priv->gesture = GTK_GESTURE_PAN (gesture); } static void sp_multi_paned_resize_drag_begin (SpMultiPaned *self, GtkWidget *child) { g_assert (SP_IS_MULTI_PANED (self)); g_assert (GTK_IS_WIDGET (child)); } static void sp_multi_paned_resize_drag_end (SpMultiPaned *self, GtkWidget *child) { g_assert (SP_IS_MULTI_PANED (self)); g_assert (GTK_IS_WIDGET (child)); } static void sp_multi_paned_get_child_property (GtkContainer *container, GtkWidget *widget, guint prop_id, GValue *value, GParamSpec *pspec) { SpMultiPaned *self = SP_MULTI_PANED (container); switch (prop_id) { case CHILD_PROP_POSITION: g_value_set_int (value, sp_multi_paned_get_child_position (self, widget)); break; default: GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec); } } static void sp_multi_paned_set_child_property (GtkContainer *container, GtkWidget *widget, guint prop_id, const GValue *value, GParamSpec *pspec) { SpMultiPaned *self = SP_MULTI_PANED (container); switch (prop_id) { case CHILD_PROP_POSITION: sp_multi_paned_set_child_position (self, widget, g_value_get_int (value)); break; default: GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec); } } static void sp_multi_paned_finalize (GObject *object) { SpMultiPaned *self = (SpMultiPaned *)object; SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); g_assert (priv->children->len == 0); g_clear_pointer (&priv->children, g_array_unref); g_clear_object (&priv->gesture); G_OBJECT_CLASS (sp_multi_paned_parent_class)->finalize (object); } static void sp_multi_paned_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { SpMultiPaned *self = SP_MULTI_PANED (object); SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); switch (prop_id) { case PROP_ORIENTATION: g_value_set_enum (value, priv->orientation); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void sp_multi_paned_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { SpMultiPaned *self = SP_MULTI_PANED (object); SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); switch (prop_id) { case PROP_ORIENTATION: priv->orientation = g_value_get_enum (value); sp_multi_paned_update_child_handles (self); gtk_widget_queue_resize (GTK_WIDGET (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void sp_multi_paned_state_flags_changed (GtkWidget *widget, GtkStateFlags previous_state) { sp_multi_paned_update_child_handles (SP_MULTI_PANED (widget)); GTK_WIDGET_CLASS (sp_multi_paned_parent_class)->state_flags_changed (widget, previous_state); } static void sp_multi_paned_class_init (SpMultiPanedClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); object_class->get_property = sp_multi_paned_get_property; object_class->set_property = sp_multi_paned_set_property; object_class->finalize = sp_multi_paned_finalize; widget_class->get_request_mode = sp_multi_paned_get_request_mode; widget_class->get_preferred_width = sp_multi_paned_get_preferred_width; widget_class->get_preferred_height = sp_multi_paned_get_preferred_height; widget_class->get_preferred_width_for_height = sp_multi_paned_get_preferred_width_for_height; widget_class->get_preferred_height_for_width = sp_multi_paned_get_preferred_height_for_width; widget_class->size_allocate = sp_multi_paned_size_allocate; widget_class->realize = sp_multi_paned_realize; widget_class->unrealize = sp_multi_paned_unrealize; widget_class->map = sp_multi_paned_map; widget_class->unmap = sp_multi_paned_unmap; widget_class->draw = sp_multi_paned_draw; widget_class->state_flags_changed = sp_multi_paned_state_flags_changed; container_class->add = sp_multi_paned_add; container_class->remove = sp_multi_paned_remove; container_class->get_child_property = sp_multi_paned_get_child_property; container_class->set_child_property = sp_multi_paned_set_child_property; container_class->forall = sp_multi_paned_forall; klass->resize_drag_begin = sp_multi_paned_resize_drag_begin; klass->resize_drag_end = sp_multi_paned_resize_drag_end; gtk_widget_class_set_css_name (widget_class, "multipaned"); properties [PROP_ORIENTATION] = g_param_spec_enum ("orientation", "Orientation", "Orientation", GTK_TYPE_ORIENTATION, GTK_ORIENTATION_VERTICAL, (G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); g_object_class_install_properties (object_class, N_PROPS, properties); child_properties [CHILD_PROP_POSITION] = g_param_spec_int ("position", "Position", "Position", -1, G_MAXINT, 0, (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); gtk_container_class_install_child_properties (container_class, N_CHILD_PROPS, child_properties); style_properties [STYLE_PROP_HANDLE_SIZE] = g_param_spec_int ("handle-size", "Handle Size", "Width of the resize handle", 0, G_MAXINT, 1, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); gtk_widget_class_install_style_property (widget_class, style_properties [STYLE_PROP_HANDLE_SIZE]); signals [RESIZE_DRAG_BEGIN] = g_signal_new ("resize-drag-begin", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (SpMultiPanedClass, resize_drag_begin), NULL, NULL, NULL, G_TYPE_NONE, 1, GTK_TYPE_WIDGET); signals [RESIZE_DRAG_END] = g_signal_new ("resize-drag-end", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (SpMultiPanedClass, resize_drag_end), NULL, NULL, NULL, G_TYPE_NONE, 1, GTK_TYPE_WIDGET); } static void sp_multi_paned_init (SpMultiPaned *self) { SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); gtk_widget_set_has_window (GTK_WIDGET (self), FALSE); priv->children = g_array_new (FALSE, TRUE, sizeof (SpMultiPanedChild)); sp_multi_paned_create_pan_gesture (self); } GtkWidget * sp_multi_paned_new (void) { return g_object_new (SP_TYPE_MULTI_PANED, NULL); } guint sp_multi_paned_get_n_children (SpMultiPaned *self) { SpMultiPanedPrivate *priv = sp_multi_paned_get_instance_private (self); g_return_val_if_fail (SP_IS_MULTI_PANED (self), 0); return priv->children ? priv->children->len : 0; }