/* dzl-multi-paned.c
*
* Copyright (C) 2016 Christian Hergert <chergert@redhat.com>
*
* 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.1 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
*/
#define G_LOG_DOMAIN "dzl-multi-paned"
#include "config.h"
#include "widgets/dzl-multi-paned.h"
#include "util/dzl-util-private.h"
#define HANDLE_WIDTH 10
#define HANDLE_HEIGHT 10
#define IS_HORIZONTAL(o) (o == GTK_ORIENTATION_HORIZONTAL)
/**
* SECTION:dzlmultipaned
* @title: DzlMultiPaned
* @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
* #DzlMultiPaned, 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;
} DzlMultiPanedChild;
typedef struct
{
/*
* A GArray of DzlMultiPanedChild 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.
*/
DzlMultiPanedChild *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;
} DzlMultiPanedPrivate;
typedef struct
{
DzlMultiPanedChild **children;
guint n_children;
GtkOrientation orientation;
GtkAllocation top_alloc;
gint avail_width;
gint avail_height;
gint handle_size;
} AllocationState;
typedef void (*AllocationStage) (DzlMultiPaned *self,
AllocationState *state);
static void allocation_stage_allocate (DzlMultiPaned *self,
AllocationState *state);
static void allocation_stage_borders (DzlMultiPaned *self,
AllocationState *state);
static void allocation_stage_cache_request (DzlMultiPaned *self,
AllocationState *state);
static void allocation_stage_drag_overflow (DzlMultiPaned *self,
AllocationState *state);
static void allocation_stage_expand (DzlMultiPaned *self,
AllocationState *state);
static void allocation_stage_handles (DzlMultiPaned *self,
AllocationState *state);
static void allocation_stage_minimums (DzlMultiPaned *self,
AllocationState *state);
static void allocation_stage_naturals (DzlMultiPaned *self,
AllocationState *state);
static void allocation_stage_positions (DzlMultiPaned *self,
AllocationState *state);
G_DEFINE_TYPE_EXTENDED (DzlMultiPaned, dzl_multi_paned, GTK_TYPE_CONTAINER, 0,
G_ADD_PRIVATE (DzlMultiPaned)
G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL))
enum {
PROP_0,
PROP_ORIENTATION,
N_PROPS
};
enum {
CHILD_PROP_0,
CHILD_PROP_INDEX,
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
dzl_multi_paned_reset_positions (DzlMultiPaned *self)
{
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
guint i;
g_assert (DZL_IS_MULTI_PANED (self));
for (i = 0; i < priv->children->len; i++)
{
DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, 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 DzlMultiPanedChild *
dzl_multi_paned_get_next_visible_child (DzlMultiPaned *self,
DzlMultiPanedChild *child)
{
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
guint i;
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (child != NULL);
g_assert (priv->children != NULL);
g_assert (priv->children->len > 0);
i = child - ((DzlMultiPanedChild *)(gpointer)priv->children->data);
for (++i; i < priv->children->len; i++)
{
DzlMultiPanedChild *next = &g_array_index (priv->children, DzlMultiPanedChild, i);
if (gtk_widget_get_visible (next->widget))
return next;
}
return NULL;
}
static gboolean
dzl_multi_paned_is_last_visible_child (DzlMultiPaned *self,
DzlMultiPanedChild *child)
{
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (child != NULL);
return !dzl_multi_paned_get_next_visible_child (self, child);
}
static void
dzl_multi_paned_get_handle_rect (DzlMultiPaned *self,
DzlMultiPanedChild *child,
GdkRectangle *handle_rect)
{
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
GtkAllocation alloc;
g_assert (DZL_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 (dzl_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
dzl_multi_paned_create_child_handle (DzlMultiPaned *self,
DzlMultiPanedChild *child)
{
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
GdkWindowAttr attributes = { 0 };
GdkDisplay *display;
GdkWindow *parent;
const char *cursor_name;
GdkRectangle handle_rect;
g_assert (DZL_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";
dzl_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
dzl_multi_paned_calc_handle_size (DzlMultiPaned *self)
{
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
gint visible_children = 0;
gint handle_size = 1;
guint i;
g_assert (DZL_IS_MULTI_PANED (self));
gtk_widget_style_get (GTK_WIDGET (self), "handle-size", &handle_size, NULL);
for (i = 0; i < priv->children->len; i++)
{
DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
if (gtk_widget_get_visible (child->widget))
visible_children++;
}
return MAX (0, (visible_children - 1) * handle_size);
}
static void
dzl_multi_paned_destroy_child_handle (DzlMultiPaned *self,
DzlMultiPanedChild *child)
{
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (child != NULL);
if (child->handle != NULL)
{
gtk_widget_unregister_window (GTK_WIDGET (self), child->handle);
gdk_window_destroy (child->handle);
child->handle = NULL;
}
}
static void
dzl_multi_paned_update_child_handles (DzlMultiPaned *self)
{
DzlMultiPanedPrivate *priv = dzl_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++)
{
DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
gdk_window_set_cursor (child->handle, cursor);
}
if (cursor)
g_object_unref (cursor);
}
}
static DzlMultiPanedChild *
dzl_multi_paned_get_child (DzlMultiPaned *self,
GtkWidget *widget)
{
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (GTK_IS_WIDGET (widget));
for (guint i = 0; i < priv->children->len; i++)
{
DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
if (child->widget == widget)
return child;
}
g_assert_not_reached ();
return NULL;
}
static gint
dzl_multi_paned_get_child_index (DzlMultiPaned *self,
GtkWidget *widget)
{
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (GTK_IS_WIDGET (widget));
for (guint i = 0; i < priv->children->len; i++)
{
const DzlMultiPanedChild *ele = &g_array_index (priv->children, DzlMultiPanedChild, i);
if (ele->widget == widget)
return i;
}
return -1;
}
static void
dzl_multi_paned_set_child_index (DzlMultiPaned *self,
GtkWidget *widget,
gint index)
{
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (GTK_IS_WIDGET (widget));
g_assert (index >= -1);
g_assert (priv->children->len > 0);
if (index < 0)
index = priv->children->len - 1;
index = CLAMP (index, 0, priv->children->len - 1);
for (guint i = 0; i < priv->children->len; i++)
{
const DzlMultiPanedChild *ele = &g_array_index (priv->children, DzlMultiPanedChild, i);
if (ele->widget == widget)
{
DzlMultiPanedChild copy = { 0 };
copy.widget = ele->widget;
copy.handle = ele->handle;
copy.position = -1;
copy.position_set = FALSE;
g_array_remove_index (priv->children, i);
g_array_insert_val (priv->children, index, copy);
gtk_container_child_notify_by_pspec (GTK_CONTAINER (self), widget,
child_properties [CHILD_PROP_INDEX]);
gtk_widget_queue_resize (GTK_WIDGET (self));
break;
}
}
}
static gint
dzl_multi_paned_get_child_position (DzlMultiPaned *self,
GtkWidget *widget)
{
DzlMultiPanedChild *child;
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (GTK_IS_WIDGET (widget));
child = dzl_multi_paned_get_child (self, widget);
return child->position;
}
static void
dzl_multi_paned_set_child_position (DzlMultiPaned *self,
GtkWidget *widget,
gint position)
{
DzlMultiPanedChild *child;
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (GTK_IS_WIDGET (widget));
g_assert (position >= -1);
child = dzl_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
dzl_multi_paned_add (GtkContainer *container,
GtkWidget *widget)
{
DzlMultiPaned *self = (DzlMultiPaned *)container;
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
DzlMultiPanedChild child = { 0 };
g_assert (DZL_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)))
dzl_multi_paned_create_child_handle (self, &child);
gtk_widget_set_parent (widget, GTK_WIDGET (self));
g_array_append_val (priv->children, child);
dzl_multi_paned_reset_positions (self);
gtk_gesture_set_state (GTK_GESTURE (priv->gesture), GTK_EVENT_SEQUENCE_DENIED);
gtk_widget_queue_resize (GTK_WIDGET (self));
}
static void
dzl_multi_paned_remove (GtkContainer *container,
GtkWidget *widget)
{
DzlMultiPaned *self = (DzlMultiPaned *)container;
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
guint i;
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (GTK_IS_WIDGET (widget));
for (i = 0; i < priv->children->len; i++)
{
DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
if (child->widget == widget)
{
dzl_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;
}
}
dzl_multi_paned_reset_positions (self);
gtk_gesture_set_state (GTK_GESTURE (priv->gesture), GTK_EVENT_SEQUENCE_DENIED);
gtk_widget_queue_resize (GTK_WIDGET (self));
}
static void
dzl_multi_paned_forall (GtkContainer *container,
gboolean include_internals,
GtkCallback callback,
gpointer user_data)
{
DzlMultiPaned *self = (DzlMultiPaned *)container;
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
gint i;
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (callback != NULL);
for (i = priv->children->len; i > 0; i--)
{
DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i - 1);
callback (child->widget, user_data);
}
}
static GtkSizeRequestMode
dzl_multi_paned_get_request_mode (GtkWidget *widget)
{
DzlMultiPaned *self = (DzlMultiPaned *)widget;
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
g_assert (DZL_IS_MULTI_PANED (self));
return (priv->orientation == GTK_ORIENTATION_HORIZONTAL) ? GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT
: GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
}
static void
dzl_multi_paned_get_preferred_height (GtkWidget *widget,
gint *min_height,
gint *nat_height)
{
DzlMultiPaned *self = (DzlMultiPaned *)widget;
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
GtkStyleContext *style_context;
GtkBorder borders;
guint i;
gint real_min_height = 0;
gint real_nat_height = 0;
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (min_height != NULL);
g_assert (nat_height != NULL);
for (i = 0; i < priv->children->len; i++)
{
DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, 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 = dzl_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;
style_context = gtk_widget_get_style_context (widget);
dzl_gtk_style_context_get_borders (style_context, &borders);
*min_height += borders.top + borders.bottom;
*nat_height += borders.top + borders.bottom;
}
static void
dzl_multi_paned_get_child_preferred_height_for_width (DzlMultiPaned *self,
DzlMultiPanedChild *children,
gint n_children,
gint width,
gint *min_height,
gint *nat_height)
{
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
DzlMultiPanedChild *child = children;
gint child_min_height = 0;
gint child_nat_height = 0;
gint neighbor_min_height = 0;
gint neighbor_nat_height = 0;
g_assert (DZL_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);
dzl_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
dzl_multi_paned_get_preferred_height_for_width (GtkWidget *widget,
gint width,
gint *min_height,
gint *nat_height)
{
DzlMultiPaned *self = (DzlMultiPaned *)widget;
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
GtkStyleContext *style_context;
GtkBorder borders;
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (min_height != NULL);
g_assert (nat_height != NULL);
*min_height = 0;
*nat_height = 0;
dzl_multi_paned_get_child_preferred_height_for_width (self,
(DzlMultiPanedChild *)(gpointer)priv->children->data,
priv->children->len,
width,
min_height,
nat_height);
if (priv->orientation == GTK_ORIENTATION_VERTICAL)
{
gint handle_size = dzl_multi_paned_calc_handle_size (self);
*min_height += handle_size;
*nat_height += handle_size;
}
style_context = gtk_widget_get_style_context (widget);
dzl_gtk_style_context_get_borders (style_context, &borders);
*min_height += borders.top + borders.bottom;
*nat_height += borders.top + borders.bottom;
}
static void
dzl_multi_paned_get_preferred_width (GtkWidget *widget,
gint *min_width,
gint *nat_width)
{
DzlMultiPaned *self = (DzlMultiPaned *)widget;
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
GtkStyleContext *style_context;
GtkBorder borders;
guint i;
gint real_min_width = 0;
gint real_nat_width = 0;
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (min_width != NULL);
g_assert (nat_width != NULL);
for (i = 0; i < priv->children->len; i++)
{
DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, 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 = dzl_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;
style_context = gtk_widget_get_style_context (widget);
dzl_gtk_style_context_get_borders (style_context, &borders);
*min_width += borders.left + borders.right;
*nat_width += borders.left + borders.right;
}
static void
dzl_multi_paned_get_child_preferred_width_for_height (DzlMultiPaned *self,
DzlMultiPanedChild *children,
gint n_children,
gint height,
gint *min_width,
gint *nat_width)
{
DzlMultiPanedChild *child = children;
DzlMultiPanedPrivate *priv = dzl_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 (DZL_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);
dzl_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
dzl_multi_paned_get_preferred_width_for_height (GtkWidget *widget,
gint height,
gint *min_width,
gint *nat_width)
{
DzlMultiPaned *self = (DzlMultiPaned *)widget;
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
GtkStyleContext *style_context;
GtkBorder borders;
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (min_width != NULL);
g_assert (nat_width != NULL);
dzl_multi_paned_get_child_preferred_width_for_height (self,
(DzlMultiPanedChild *)(gpointer)priv->children->data,
priv->children->len,
height,
min_width,
nat_width);
if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
{
gint handle_size = dzl_multi_paned_calc_handle_size (self);
*min_width += handle_size;
*nat_width += handle_size;
}
style_context = gtk_widget_get_style_context (widget);
dzl_gtk_style_context_get_borders (style_context, &borders);
*min_width += borders.left + borders.right;
*nat_width += borders.left + borders.right;
}
static void
allocation_stage_handles (DzlMultiPaned *self,
AllocationState *state)
{
guint i;
g_assert (DZL_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++)
{
DzlMultiPanedChild *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 (DzlMultiPaned *self,
AllocationState *state)
{
gint next_x;
gint next_y;
guint i;
g_assert (DZL_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++)
{
DzlMultiPanedChild *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 (DzlMultiPaned *self,
AllocationState *state)
{
gint x_adjust = 0;
gint y_adjust = 0;
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (state != NULL);
g_assert (state->children != NULL);
g_assert (state->n_children > 0);
for (guint i = 0; i < state->n_children; i++)
{
DzlMultiPanedChild *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 (DzlMultiPaned *self,
AllocationState *state)
{
GtkStyleContext *style_context;
GtkBorder borders;
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (state != NULL);
g_assert (state->children != NULL);
g_assert (state->n_children > 0);
/*
* This subtracts the border+padding from the allocation area so the
* children are guaranteed to fall within that area.
*/
style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
dzl_gtk_style_context_get_borders (style_context, &borders);
state->top_alloc.x += borders.left;
state->top_alloc.y += borders.right;
state->top_alloc.width -= (borders.left + borders.right);
state->top_alloc.height -= (borders.top + borders.bottom);
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 (DzlMultiPaned *self,
AllocationState *state)
{
guint i;
g_assert (DZL_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++)
{
DzlMultiPanedChild *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 (DzlMultiPaned *self,
AllocationState *state)
{
gint x_adjust = 0;
gint y_adjust = 0;
guint i;
g_assert (DZL_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++)
{
DzlMultiPanedChild *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 (DzlMultiPaned *self,
AllocationState *state)
{
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
guint drag_index;
gint j;
gint drag_overflow;
g_assert (DZL_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--)
{
DzlMultiPanedChild *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++)
{
DzlMultiPanedChild *neighbor = state->children [k];
if (IS_HORIZONTAL (state->orientation))
neighbor->alloc.x -= adjust;
else
neighbor->alloc.y -= adjust;
}
}
}
static gint
sort_children_horizontal (const DzlMultiPanedChild * const *first,
const DzlMultiPanedChild * const *second)
{
return (*first)->alloc.width - (*second)->alloc.width;
}
static gint
sort_children_vertical (const DzlMultiPanedChild * const *first,
const DzlMultiPanedChild * const *second)
{
return (*first)->alloc.height - (*second)->alloc.height;
}
static void
allocation_stage_expand (DzlMultiPaned *self,
AllocationState *state)
{
g_autoptr(GPtrArray) expanding = NULL;
gint x_adjust = 0;
gint y_adjust = 0;
gint adjust;
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (state != NULL);
g_assert (state->children != NULL);
g_assert (state->n_children > 0);
if (state->n_children == 1)
{
DzlMultiPanedChild *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 (gtk_widget_compute_expand (child->widget, state->orientation))
{
if (IS_HORIZONTAL (state->orientation))
child->alloc.width = state->top_alloc.width;
else
child->alloc.height = state->top_alloc.height;
}
return;
}
/*
* First, we need to collect all of the children who are expanding
* in the direction matching our orientation. After that, we will
* sort them by their allocation size so that we can divy out extra
* allocation starting from the smallest. This will give us an effect
* of homogeneous size when all children are set to expand and enough
* space is available.
*/
expanding = g_ptr_array_new ();
for (guint i = 0; i < state->n_children; i++)
{
DzlMultiPanedChild *child = state->children [i];
if (!child->position_set)
{
if (gtk_widget_compute_expand (child->widget, state->orientation))
g_ptr_array_add (expanding, child);
}
}
if (expanding->len == 0)
goto fill_last;
/* Now sort, smallest first, based on size in given orientation. */
if (IS_HORIZONTAL (state->orientation))
g_ptr_array_sort (expanding, (GCompareFunc) sort_children_horizontal);
else
g_ptr_array_sort (expanding, (GCompareFunc) sort_children_vertical);
/*
* While we have additional space available to hand out, allocate
* as much space as it takes to get to the next item in the array.
* If we run out of additional space, that is okay (as long as we
* dont hand out more than we have).
*/
g_assert (expanding->len > 0);
for (guint i = 0; i < expanding->len - 1; i++)
{
DzlMultiPanedChild *child = g_ptr_array_index (expanding, i);
DzlMultiPanedChild *next = g_ptr_array_index (expanding, i + 1);
if (IS_HORIZONTAL (state->orientation))
{
guint j;
g_assert (next->alloc.width >= child->alloc.width);
adjust = next->alloc.width - child->alloc.width;
if (adjust > state->avail_width)
adjust = state->avail_width;
child->alloc.width += adjust;
state->avail_width -= adjust;
/* Adjust X of children following */
for (j = 0; j < state->n_children; j++)
if (state->children[j] == child)
break;
for (++j; j < state->n_children; j++)
state->children[j]->alloc.x += adjust;
g_assert (state->avail_width >= 0);
if (state->avail_width == 0)
break;
}
else
{
guint j;
g_assert (next->alloc.height >= child->alloc.height);
adjust = next->alloc.height - child->alloc.height;
if (adjust > state->avail_height)
adjust = state->avail_height;
child->alloc.height += adjust;
state->avail_height -= adjust;
/* Adjust Y of children following */
for (j = 0; j < state->n_children; j++)
if (state->children[j] == child)
break;
for (++j; j < state->n_children; j++)
state->children[j]->alloc.y += adjust;
g_assert (state->avail_height >= 0);
if (state->avail_height == 0)
break;
}
}
/*
* If we still have more space we can divy out, then do the normal
* expansion case now that we are starting with evenly sized
* children.
*/
if (IS_HORIZONTAL (state->orientation))
adjust = state->avail_width / expanding->len;
else
adjust = state->avail_height / expanding->len;
for (guint i = 0; i < state->n_children; i++)
{
DzlMultiPanedChild *child = state->children [i];
child->alloc.x += x_adjust;
child->alloc.y += y_adjust;
if (!child->position_set)
{
if (gtk_widget_compute_expand (child->widget, state->orientation))
{
if (IS_HORIZONTAL (state->orientation))
{
child->alloc.width += adjust;
state->avail_width -= adjust;
x_adjust += adjust;
}
else
{
child->alloc.height += adjust;
state->avail_height -= adjust;
y_adjust += adjust;
}
}
}
}
fill_last:
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 (DzlMultiPaned *self,
AllocationState *state)
{
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (state != NULL);
g_assert (state->children != NULL);
g_assert (state->n_children > 0);
for (guint i = 0; i < state->n_children; i++)
{
DzlMultiPanedChild *child = state->children [i];
gtk_widget_size_allocate (child->widget, &child->alloc);
if (child->handle != NULL)
{
if (state->n_children != (i + 1))
{
if (IS_HORIZONTAL (state->orientation))
{
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);
}
else
{
gdk_window_hide (child->handle);
}
}
}
}
static void
dzl_multi_paned_size_allocate (GtkWidget *widget,
GtkAllocation *allocation)
{
DzlMultiPaned *self = (DzlMultiPaned *)widget;
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
AllocationState state = { 0 };
g_autoptr(GPtrArray) children = NULL;
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (allocation != NULL);
GTK_WIDGET_CLASS (dzl_multi_paned_parent_class)->size_allocate (widget, allocation);
if (priv->children->len == 0)
return;
children = g_ptr_array_new ();
for (guint i = 0; i < priv->children->len; i++)
{
DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, 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 = (DzlMultiPanedChild **)children->pdata;
state.n_children = children->len;
if (state.n_children == 0)
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 (guint i = 0; i < G_N_ELEMENTS (allocation_stages); i++)
allocation_stages [i] (self, &state);
gtk_widget_queue_draw (GTK_WIDGET (self));
}
static void
dzl_multi_paned_realize (GtkWidget *widget)
{
DzlMultiPaned *self = (DzlMultiPaned *)widget;
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
guint i;
g_assert (DZL_IS_MULTI_PANED (self));
GTK_WIDGET_CLASS (dzl_multi_paned_parent_class)->realize (widget);
for (i = 0; i < priv->children->len; i++)
{
DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
dzl_multi_paned_create_child_handle (self, child);
}
}
static void
dzl_multi_paned_unrealize (GtkWidget *widget)
{
DzlMultiPaned *self = (DzlMultiPaned *)widget;
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
guint i;
g_assert (DZL_IS_MULTI_PANED (self));
for (i = 0; i < priv->children->len; i++)
{
DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
dzl_multi_paned_destroy_child_handle (self, child);
}
GTK_WIDGET_CLASS (dzl_multi_paned_parent_class)->unrealize (widget);
}
static void
dzl_multi_paned_map (GtkWidget *widget)
{
DzlMultiPaned *self = (DzlMultiPaned *)widget;
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
guint i;
g_assert (DZL_IS_MULTI_PANED (self));
GTK_WIDGET_CLASS (dzl_multi_paned_parent_class)->map (widget);
for (i = 0; i < priv->children->len; i++)
{
DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
gdk_window_show (child->handle);
}
}
static void
dzl_multi_paned_unmap (GtkWidget *widget)
{
DzlMultiPaned *self = (DzlMultiPaned *)widget;
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
guint i;
g_assert (DZL_IS_MULTI_PANED (self));
for (i = 0; i < priv->children->len; i++)
{
DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
gdk_window_hide (child->handle);
}
GTK_WIDGET_CLASS (dzl_multi_paned_parent_class)->unmap (widget);
}
static gboolean
dzl_multi_paned_draw (GtkWidget *widget,
cairo_t *cr)
{
DzlMultiPaned *self = (DzlMultiPaned *)widget;
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
GtkStyleContext *style_context;
GtkAllocation alloc;
GtkBorder margin;
GtkBorder borders;
GtkStateFlags state;
gint handle_size = 1;
guint i;
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (cr != NULL);
gtk_widget_get_allocation (widget, &alloc);
alloc.x = 0;
alloc.y = 0;
style_context = gtk_widget_get_style_context (widget);
state = gtk_style_context_get_state (style_context);
dzl_gtk_style_context_get_borders (style_context, &borders);
gtk_style_context_get_margin (style_context, state, &margin);
dzl_gtk_allocation_subtract_border (&alloc, &margin);
gtk_render_background (style_context, cr, alloc.x, alloc.y, alloc.width, alloc.height);
gtk_widget_style_get (widget, "handle-size", &handle_size, NULL);
for (i = 0; i < priv->children->len; i++)
{
DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
if (!gtk_widget_get_realized (child->widget) ||
!gtk_widget_get_visible (child->widget))
continue;
gtk_container_propagate_draw (GTK_CONTAINER (self), child->widget, cr);
}
if (priv->children->len > 0)
{
gtk_style_context_save (style_context);
gtk_style_context_add_class (style_context, "handle");
for (i = 0; i < priv->children->len; i++)
{
DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
GtkAllocation child_alloc;
if (!gtk_widget_get_visible (child->widget) ||
!gtk_widget_get_child_visible (child->widget))
continue;
if (dzl_multi_paned_is_last_visible_child (self, child))
break;
gtk_widget_get_allocation (child->widget, &child_alloc);
gtk_widget_translate_coordinates (child->widget, widget, 0, 0, &child_alloc.x, &child_alloc.y);
if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
gtk_render_handle (style_context,
cr,
child_alloc.x + child_alloc.width,
borders.top,
handle_size,
child_alloc.height);
else
gtk_render_handle (style_context,
cr,
borders.left,
child_alloc.y + child_alloc.height,
child_alloc.width,
handle_size);
}
gtk_style_context_restore (style_context);
}
gtk_render_frame (style_context, cr, alloc.x, alloc.y, alloc.width, alloc.height);
return FALSE;
}
static void
dzl_multi_paned_pan_gesture_drag_begin (DzlMultiPaned *self,
gdouble x,
gdouble y,
GtkGesturePan *gesture)
{
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
GdkEventSequence *sequence;
const GdkEvent *event;
guint i;
g_assert (DZL_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++)
{
DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
if (child->handle == event->any.window)
{
priv->drag_begin = child;
break;
}
}
if (priv->drag_begin == NULL)
{
gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED);
return;
}
for (i = 0; i < priv->children->len; i++)
{
DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
if (child->handle == event->any.window)
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 (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
dzl_multi_paned_pan_gesture_drag_end (DzlMultiPaned *self,
gdouble x,
gdouble y,
GtkGesturePan *gesture)
{
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
GdkEventSequence *sequence;
GtkEventSequenceState state;
g_assert (DZL_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
dzl_multi_paned_pan_gesture_pan (DzlMultiPaned *self,
GtkPanDirection direction,
gdouble offset,
GtkGesturePan *gesture)
{
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
GtkAllocation alloc;
g_assert (DZL_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
dzl_multi_paned_create_pan_gesture (DzlMultiPaned *self)
{
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
GtkGesture *gesture;
g_assert (DZL_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 (dzl_multi_paned_pan_gesture_drag_begin),
self,
G_CONNECT_SWAPPED);
g_signal_connect_object (gesture,
"drag-end",
G_CALLBACK (dzl_multi_paned_pan_gesture_drag_end),
self,
G_CONNECT_SWAPPED);
g_signal_connect_object (gesture,
"pan",
G_CALLBACK (dzl_multi_paned_pan_gesture_pan),
self,
G_CONNECT_SWAPPED);
priv->gesture = GTK_GESTURE_PAN (gesture);
}
static void
dzl_multi_paned_resize_drag_begin (DzlMultiPaned *self,
GtkWidget *child)
{
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (GTK_IS_WIDGET (child));
}
static void
dzl_multi_paned_resize_drag_end (DzlMultiPaned *self,
GtkWidget *child)
{
g_assert (DZL_IS_MULTI_PANED (self));
g_assert (GTK_IS_WIDGET (child));
}
static void
dzl_multi_paned_get_child_property (GtkContainer *container,
GtkWidget *widget,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
DzlMultiPaned *self = DZL_MULTI_PANED (container);
switch (prop_id)
{
case CHILD_PROP_INDEX:
g_value_set_int (value, dzl_multi_paned_get_child_index (self, widget));
break;
case CHILD_PROP_POSITION:
g_value_set_int (value, dzl_multi_paned_get_child_position (self, widget));
break;
default:
GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec);
}
}
static void
dzl_multi_paned_set_child_property (GtkContainer *container,
GtkWidget *widget,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
DzlMultiPaned *self = DZL_MULTI_PANED (container);
switch (prop_id)
{
case CHILD_PROP_INDEX:
dzl_multi_paned_set_child_index (self, widget, g_value_get_int (value));
break;
case CHILD_PROP_POSITION:
dzl_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
dzl_multi_paned_finalize (GObject *object)
{
DzlMultiPaned *self = (DzlMultiPaned *)object;
DzlMultiPanedPrivate *priv = dzl_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 (dzl_multi_paned_parent_class)->finalize (object);
}
static void
dzl_multi_paned_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
DzlMultiPaned *self = DZL_MULTI_PANED (object);
DzlMultiPanedPrivate *priv = dzl_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
dzl_multi_paned_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
DzlMultiPaned *self = DZL_MULTI_PANED (object);
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
switch (prop_id)
{
case PROP_ORIENTATION:
priv->orientation = g_value_get_enum (value);
for (guint i = 0; i < priv->children->len; i++)
{
DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
child->position_set = FALSE;
}
dzl_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
dzl_multi_paned_state_flags_changed (GtkWidget *widget,
GtkStateFlags previous_state)
{
dzl_multi_paned_update_child_handles (DZL_MULTI_PANED (widget));
GTK_WIDGET_CLASS (dzl_multi_paned_parent_class)->state_flags_changed (widget, previous_state);
}
static void
dzl_multi_paned_class_init (DzlMultiPanedClass *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 = dzl_multi_paned_get_property;
object_class->set_property = dzl_multi_paned_set_property;
object_class->finalize = dzl_multi_paned_finalize;
widget_class->get_request_mode = dzl_multi_paned_get_request_mode;
widget_class->get_preferred_width = dzl_multi_paned_get_preferred_width;
widget_class->get_preferred_height = dzl_multi_paned_get_preferred_height;
widget_class->get_preferred_width_for_height = dzl_multi_paned_get_preferred_width_for_height;
widget_class->get_preferred_height_for_width = dzl_multi_paned_get_preferred_height_for_width;
widget_class->size_allocate = dzl_multi_paned_size_allocate;
widget_class->realize = dzl_multi_paned_realize;
widget_class->unrealize = dzl_multi_paned_unrealize;
widget_class->map = dzl_multi_paned_map;
widget_class->unmap = dzl_multi_paned_unmap;
widget_class->draw = dzl_multi_paned_draw;
widget_class->state_flags_changed = dzl_multi_paned_state_flags_changed;
container_class->add = dzl_multi_paned_add;
container_class->remove = dzl_multi_paned_remove;
container_class->get_child_property = dzl_multi_paned_get_child_property;
container_class->set_child_property = dzl_multi_paned_set_child_property;
container_class->forall = dzl_multi_paned_forall;
klass->resize_drag_begin = dzl_multi_paned_resize_drag_begin;
klass->resize_drag_end = dzl_multi_paned_resize_drag_end;
gtk_widget_class_set_css_name (widget_class, "dzlmultipaned");
properties [PROP_ORIENTATION] =
g_param_spec_enum ("orientation",
"Orientation",
"Orientation",
GTK_TYPE_ORIENTATION,
GTK_ORIENTATION_VERTICAL,
(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_properties (object_class, N_PROPS, properties);
child_properties [CHILD_PROP_INDEX] =
g_param_spec_int ("index",
"Index",
"The index of the child",
-1,
G_MAXINT,
-1,
(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
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 (DzlMultiPanedClass, 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 (DzlMultiPanedClass, resize_drag_end),
NULL, NULL, NULL,
G_TYPE_NONE, 1, GTK_TYPE_WIDGET);
}
static void
dzl_multi_paned_init (DzlMultiPaned *self)
{
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
gtk_widget_set_has_window (GTK_WIDGET (self), FALSE);
priv->orientation = GTK_ORIENTATION_VERTICAL;
priv->children = g_array_new (FALSE, TRUE, sizeof (DzlMultiPanedChild));
dzl_multi_paned_create_pan_gesture (self);
}
GtkWidget *
dzl_multi_paned_new (void)
{
return g_object_new (DZL_TYPE_MULTI_PANED, NULL);
}
guint
dzl_multi_paned_get_n_children (DzlMultiPaned *self)
{
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
g_return_val_if_fail (DZL_IS_MULTI_PANED (self), 0);
return priv->children ? priv->children->len : 0;
}
/**
* dzl_multi_paned_get_nth_child:
* @self: a #DzlMultiPaned
*
* Gets the @nth child of the #DzlMultiPaned.
*
* It is an error to call this function with a value >= the result of
* dzl_multi_paned_get_nth_child().
*
* The index starts from 0.
*
* Returns: (transfer none): A #GtkWidget
*/
GtkWidget *
dzl_multi_paned_get_nth_child (DzlMultiPaned *self,
guint nth)
{
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
g_return_val_if_fail (DZL_IS_MULTI_PANED (self), NULL);
g_return_val_if_fail (nth < priv->children->len, NULL);
return g_array_index (priv->children, DzlMultiPanedChild, nth).widget;
}
/**
* dzl_multi_paned_get_at_point:
* @self: a #DzlMultiPaned
* @x: x coordinate
* @y: y coordinate
*
* Locates the widget at position x,y within widget.
*
* @x and @y should be relative to @self.
*
* Returns: (transfer none) (nullable): a #GtkWidget or %NULL
*
* Since: 3.28
*/
GtkWidget *
dzl_multi_paned_get_at_point (DzlMultiPaned *self,
gint x,
gint y)
{
DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
GtkAllocation alloc;
g_return_val_if_fail (DZL_IS_MULTI_PANED (self), NULL);
gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
if (IS_HORIZONTAL (priv->orientation))
{
for (guint i = 0; i < priv->children->len; i++)
{
const DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
if (x >= child->alloc.x && x < (child->alloc.x + child->alloc.width))
return child->widget;
}
}
else
{
for (guint i = 0; i < priv->children->len; i++)
{
const DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
if (y >= child->alloc.y && y < (child->alloc.y + child->alloc.height))
return child->widget;
}
}
return NULL;
}