Fix size negotiation for StScrollView

StScrollable: Document how size negotation now works between the
  parent and scrollable child.

StBoxLayout: Adapt to the new contract for how size negotiation
  works; in particular, handle being allocated less than the
  minimum size when scrolled and treat the minimum size as the
  size of the scrolled area in instead of the natural size.

StScrollView: Substantially rewrite with fixes including:
 - Implement new size negotation contract; this allows us
   to determine scrollbar visibility without having to
   connect to the adjustment.
 - Implement all ALWAYS along with the existing NEVER/AUTO
 - When hiding and showing scrollbars and shadows, don't
   hide and show widgets, just turn on and off including them
   in pick and paint. This avoids queueing relayouts.
 - Cleanups for the code for connecting to adjustments,
   for changing policy, and for turning on and off shadows.

scroll-view-sizing.js: New test case for StScrollView, allowing
  resizing the scroll view interactively, changing the scrollbar
  policies and turning shadows on and off.

https://bugzilla.gnome.org/show_bug.cgi?id=611740
This commit is contained in:
Owen W. Taylor 2010-03-05 17:20:56 -05:00
parent 524e2ca8e2
commit ffd25fe9e4
4 changed files with 802 additions and 225 deletions

View File

@ -569,11 +569,25 @@ st_box_layout_get_preferred_height (ClutterActor *actor,
gfloat *min_height_p,
gfloat *natural_height_p)
{
StBoxLayout *self = ST_BOX_LAYOUT (actor);
StBoxLayoutPrivate *priv = self->priv;
StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor));
st_theme_node_adjust_for_width (theme_node, &for_width);
get_content_preferred_height (ST_BOX_LAYOUT (actor), for_width,
if (priv->hadjustment)
{
/* If we're scrolled, the parent calls us with the width that
* we'll actually get, which can be smaller than the minimum
* width that we give our contents.
*/
gfloat min_width;
get_content_preferred_width (self, -1, &min_width, NULL);
for_width = MAX (for_width, min_width);
}
get_content_preferred_height (self, for_width,
min_height_p, natural_height_p);
st_theme_node_adjust_preferred_height (theme_node,
@ -743,10 +757,11 @@ st_box_layout_allocate (ClutterActor *actor,
avail_width = content_box.x2 - content_box.x1;
avail_height = content_box.y2 - content_box.y1;
get_content_preferred_height (ST_BOX_LAYOUT (actor), avail_width,
&min_height, &natural_height);
get_content_preferred_width (ST_BOX_LAYOUT (actor), avail_height,
&min_width, &natural_width);
get_content_preferred_height (ST_BOX_LAYOUT (actor), MAX (avail_width, min_width),
&min_height, &natural_height);
/* update adjustments for scrolling */
if (priv->vadjustment)
@ -755,7 +770,7 @@ st_box_layout_allocate (ClutterActor *actor,
g_object_set (G_OBJECT (priv->vadjustment),
"lower", 0.0,
"upper", natural_height,
"upper", min_height,
"page-size", avail_height,
"step-increment", avail_height / 6,
"page-increment", avail_height,
@ -771,7 +786,7 @@ st_box_layout_allocate (ClutterActor *actor,
g_object_set (G_OBJECT (priv->hadjustment),
"lower", 0.0,
"upper", natural_width,
"upper", min_width,
"page-size", avail_width,
"step-increment", avail_width / 6,
"page-increment", avail_width,
@ -781,6 +796,18 @@ st_box_layout_allocate (ClutterActor *actor,
st_adjustment_set_value (priv->hadjustment, prev_value);
}
if (avail_height < min_height)
{
avail_height = min_height;
content_box.y2 = content_box.y1 + avail_height;
}
if (avail_width < min_width)
{
avail_width = min_width;
content_box.x2 = content_box.x1 + avail_width;
}
if (priv->is_vertical)
{
expand_amount = MAX (0, avail_height - natural_height);
@ -894,9 +921,6 @@ st_box_layout_allocate (ClutterActor *actor,
child_box.y1 = (int)(0.5 + position);
child_box.y2 = (int)(0.5 + next_position);
child_box.x1 = content_box.x1;
if (priv->hadjustment)
child_box.x2 = content_box.x1 + MAX (avail_width, natural_width);
else
child_box.x2 = content_box.x2;
_st_allocate_fill (ST_WIDGET (actor), child, &child_box,
@ -918,9 +942,6 @@ st_box_layout_allocate (ClutterActor *actor,
}
child_box.y1 = content_box.y1;
if (priv->vadjustment)
child_box.y2 = content_box.y1 + MAX (avail_height, natural_height);
else
child_box.y2 = content_box.y2;
_st_allocate_fill (ST_WIDGET (actor), child, &child_box,

View File

@ -32,6 +32,35 @@
* allow the user to move around the scrollable area.
*/
/* TODO: The code here currently only deals with height-for-width
* allocation; width-for-height allocation would need a second set of
* code paths through get_preferred_height()/get_preferred_width()/allocate()
* that reverse the roles of the horizontal and vertical scrollbars.
*
* TODO: The multiple layout passes with and without scrollbars when
* using the automatic policy causes considerable inefficiency because
* it breaks request caching; we should saved the last size passed
* into allocate() and if it's the same as previous size not repeat
* the determination of scrollbar visibility. This requires overriding
* queue_relayout() so we know when to discard the saved value.
*
* The size negotiation between the #StScrollView and the child is
* described in the documentation for #StScrollable; the significant
* part to note there is that reported minimum sizes for a scrolled
* child are the minimum sizes when no scrollbar is needed. This allows
* us to determine what scrollbars are visible without a need to look
* inside the #StAdjustment.
*
* The second simplification that we make that allows us to implement
* a straighforward height-for-width negotiation without multiple
* allocate passes is that when the vertical scrollbar policy is
* AUTO, we always reserve space for the vertical scrollbar in the
* reported minimum and natural size.
*
* See https://bugzilla.gnome.org/show_bug.cgi?id=611740 for a more
* detailed description of the considerations involved.
*/
#include "st-scroll-view.h"
#include "st-marshal.h"
#include "st-scroll-bar.h"
@ -65,8 +94,11 @@ struct _StScrollViewPrivate
ClutterActor *hscroll;
ClutterActor *vscroll;
GtkPolicyType hscroll_policy;
GtkPolicyType vscroll_policy;
StAdjustment *hadjustment;
StAdjustment *vadjustment;
GtkPolicyType hscrollbar_policy;
GtkPolicyType vscrollbar_policy;
ClutterActor *top_shadow;
ClutterActor *bottom_shadow;
@ -78,6 +110,10 @@ struct _StScrollViewPrivate
gboolean row_size_set : 1;
gboolean column_size_set : 1;
guint mouse_scroll : 1;
guint hscrollbar_visible : 1;
guint vscrollbar_visible : 1;
guint top_shadow_visible : 1;
guint bottom_shadow_visible : 1;
};
enum {
@ -108,10 +144,10 @@ st_scroll_view_get_property (GObject *object,
g_value_set_object (value, priv->vscroll);
break;
case PROP_HSCROLLBAR_POLICY:
g_value_set_enum (value, priv->hscroll_policy);
g_value_set_enum (value, priv->hscrollbar_policy);
break;
case PROP_VSCROLLBAR_POLICY:
g_value_set_enum (value, priv->vscroll_policy);
g_value_set_enum (value, priv->vscrollbar_policy);
break;
case PROP_MOUSE_SCROLL:
g_value_set_boolean (value, priv->mouse_scroll);
@ -125,26 +161,24 @@ st_scroll_view_get_property (GObject *object,
}
static void
update_shadow_visibility (GObject *gobject,
GParamSpec *arg1,
gpointer user_data)
update_shadow_visibility (StScrollView *scroll)
{
StAdjustment *vadjust = ST_ADJUSTMENT (gobject);
StScrollViewPrivate *priv = ST_SCROLL_VIEW (user_data)->priv;
StScrollViewPrivate *priv = scroll->priv;
if (priv->vshadows && priv->vadjustment)
{
gdouble value, lower, upper, page_size;
st_adjustment_get_values (vadjust, &value, &lower, &upper, NULL, NULL, &page_size);
st_adjustment_get_values (priv->vadjustment, &value, &lower, &upper, NULL, NULL, &page_size);
if (fabs (value - lower) > 0.1 && priv->vshadows)
clutter_actor_show (priv->top_shadow);
priv->top_shadow_visible = fabs (value - lower) > 0.1;
priv->bottom_shadow_visible = fabs (upper - value - page_size) > 0.1;
}
else
clutter_actor_hide (priv->top_shadow);
if (fabs (upper - value - page_size) > 0.1 && priv->vshadows)
clutter_actor_show (priv->bottom_shadow);
else
clutter_actor_hide (priv->bottom_shadow);
{
priv->top_shadow_visible = FALSE;
priv->bottom_shadow_visible = FALSE;
}
}
/**
@ -159,7 +193,6 @@ void
st_scroll_view_set_vshadows (StScrollView *self,
gboolean vshadows)
{
StAdjustment *vadjust;
StScrollViewPrivate *priv = ST_SCROLL_VIEW (self)->priv;
vshadows = vshadows != FALSE;
@ -168,12 +201,30 @@ st_scroll_view_set_vshadows (StScrollView *self,
priv->vshadows = vshadows;
vadjust = st_scroll_bar_get_adjustment (ST_SCROLL_BAR(priv->vscroll));
if (vshadows)
{
if (priv->top_shadow)
{
clutter_actor_show (priv->top_shadow);
clutter_actor_show (priv->bottom_shadow);
}
else
{
priv->top_shadow = g_object_new (ST_TYPE_BIN, "style-class", "top-shadow", NULL);
priv->bottom_shadow = g_object_new (ST_TYPE_BIN, "style-class", "bottom-shadow", NULL);
if (!vadjust)
return;
clutter_actor_set_parent (priv->bottom_shadow, CLUTTER_ACTOR (self));
clutter_actor_set_parent (priv->top_shadow, CLUTTER_ACTOR (self));
}
}
else
{
clutter_actor_hide (priv->top_shadow);
clutter_actor_hide (priv->bottom_shadow);
}
update_shadow_visibility (self);
update_shadow_visibility (G_OBJECT (vadjust), NULL, self);
g_object_notify (G_OBJECT (self), "vshadows");
}
@ -198,11 +249,11 @@ st_scroll_view_set_property (GObject *object,
case PROP_HSCROLLBAR_POLICY:
st_scroll_view_set_policy (self,
g_value_get_enum (value),
priv->vscroll_policy);
priv->vscrollbar_policy);
break;
case PROP_VSCROLLBAR_POLICY:
st_scroll_view_set_policy (self,
priv->hscroll_policy,
priv->hscrollbar_policy,
g_value_get_enum (value));
break;
default:
@ -247,14 +298,14 @@ st_scroll_view_paint (ClutterActor *actor)
CLUTTER_ACTOR_CLASS (st_scroll_view_parent_class)->paint (actor);
/* paint our custom children */
if (CLUTTER_ACTOR_IS_VISIBLE (priv->hscroll))
if (priv->hscrollbar_visible && CLUTTER_ACTOR_IS_VISIBLE (priv->hscroll))
clutter_actor_paint (priv->hscroll);
if (CLUTTER_ACTOR_IS_VISIBLE (priv->vscroll))
if (priv->vscrollbar_visible && CLUTTER_ACTOR_IS_VISIBLE (priv->vscroll))
clutter_actor_paint (priv->vscroll);
if (CLUTTER_ACTOR_IS_VISIBLE (priv->top_shadow))
if (priv->top_shadow_visible)
clutter_actor_paint (priv->top_shadow);
if (CLUTTER_ACTOR_IS_VISIBLE (priv->bottom_shadow))
if (priv->bottom_shadow_visible)
clutter_actor_paint (priv->bottom_shadow);
}
@ -268,37 +319,46 @@ st_scroll_view_pick (ClutterActor *actor,
CLUTTER_ACTOR_CLASS (st_scroll_view_parent_class)->pick (actor, color);
/* paint our custom children */
if (CLUTTER_ACTOR_IS_VISIBLE (priv->hscroll))
if (priv->hscrollbar_visible && CLUTTER_ACTOR_IS_VISIBLE (priv->hscroll))
clutter_actor_paint (priv->hscroll);
if (CLUTTER_ACTOR_IS_VISIBLE (priv->vscroll))
if (priv->vscrollbar_visible && CLUTTER_ACTOR_IS_VISIBLE (priv->vscroll))
clutter_actor_paint (priv->vscroll);
if (CLUTTER_ACTOR_IS_VISIBLE (priv->top_shadow))
clutter_actor_paint (priv->top_shadow);
if (CLUTTER_ACTOR_IS_VISIBLE (priv->bottom_shadow))
clutter_actor_paint (priv->bottom_shadow);
}
static double
get_scrollbar_width (StScrollView *scroll_view)
get_scrollbar_width (StScrollView *scroll)
{
StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (scroll_view));
StScrollViewPrivate *priv = scroll->priv;
if (CLUTTER_ACTOR_IS_VISIBLE (priv->vscroll))
{
StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (scroll));
double result = DEFAULT_SCROLLBAR_WIDTH;
st_theme_node_get_length (theme_node, "scrollbar-width", FALSE, &result);
return result;
}
else
return 0;
}
static double
get_scrollbar_height (StScrollView *scroll_view)
get_scrollbar_height (StScrollView *scroll)
{
StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (scroll_view));
StScrollViewPrivate *priv = scroll->priv;
if (CLUTTER_ACTOR_IS_VISIBLE (priv->hscroll))
{
StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (scroll));
double result = DEFAULT_SCROLLBAR_WIDTH;
st_theme_node_get_length (theme_node, "scrollbar-height", FALSE, &result);
return result;
}
else
return 0;
}
static void
@ -309,32 +369,64 @@ st_scroll_view_get_preferred_width (ClutterActor *actor,
{
StScrollViewPrivate *priv = ST_SCROLL_VIEW (actor)->priv;
StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor));
gboolean account_for_vscrollbar = FALSE;
gfloat min_width = 0, natural_width;
gfloat child_min_width, child_natural_width;
if (!priv->child)
return;
st_theme_node_adjust_for_height (theme_node, &for_height);
/* Our natural width is the natural width of the child */
clutter_actor_get_preferred_width (priv->child,
for_height,
NULL,
natural_width_p);
clutter_actor_get_preferred_width (priv->child, -1,
&child_min_width, &child_natural_width);
/* Add space for the scroll-bar if we can determine it will be necessary */
if ((for_height >= 0) && natural_width_p)
natural_width = child_natural_width;
switch (priv->hscrollbar_policy)
{
gfloat natural_height;
case GTK_POLICY_NEVER:
min_width = child_min_width;
break;
case GTK_POLICY_ALWAYS:
case GTK_POLICY_AUTOMATIC:
/* Should theoretically use the min width of the hscrollbar,
* but that's not cleanly defined at the moment */
min_width = 0;
break;
}
clutter_actor_get_preferred_height (priv->child, -1.0,
NULL,
&natural_height);
if (for_height < natural_height)
*natural_width_p += get_scrollbar_width (ST_SCROLL_VIEW (actor));
switch (priv->vscrollbar_policy)
{
case GTK_POLICY_NEVER:
account_for_vscrollbar = FALSE;
break;
case GTK_POLICY_ALWAYS:
account_for_vscrollbar = TRUE;
break;
case GTK_POLICY_AUTOMATIC:
/* For automatic scrollbars, we always request space for the vertical
* scrollbar; we won't know whether we actually need one until our
* height is assigned in allocate().
*/
account_for_vscrollbar = TRUE;
break;
}
account_for_vscrollbar = priv->vscrollbar_policy != GTK_POLICY_NEVER;
if (account_for_vscrollbar)
{
float sb_width = get_scrollbar_width (ST_SCROLL_VIEW (actor));
min_width += sb_width;
natural_width += sb_width;
}
if (min_width_p)
*min_width_p = 0;
*min_width_p = min_width;
if (natural_width_p)
*natural_width_p = natural_width;
st_theme_node_adjust_preferred_width (theme_node, min_width_p, natural_width_p);
}
@ -347,33 +439,83 @@ st_scroll_view_get_preferred_height (ClutterActor *actor,
{
StScrollViewPrivate *priv = ST_SCROLL_VIEW (actor)->priv;
StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor));
gboolean account_for_hscrollbar = FALSE;
gfloat min_height = 0, natural_height;
gfloat child_min_height, child_natural_height;
gfloat child_min_width;
gfloat sb_width;
if (!priv->child)
return;
st_theme_node_adjust_for_width (theme_node, &for_width);
/* Our natural height is the natural height of the child */
clutter_actor_get_preferred_height (priv->child,
for_width,
NULL,
natural_height_p);
clutter_actor_get_preferred_width (priv->child, -1,
&child_min_width, NULL);
/* Add space for the scroll-bar if we can determine it will be necessary */
if ((for_width >= 0) && natural_height_p)
{
gfloat natural_width;
clutter_actor_get_preferred_width (priv->child, -1.0,
NULL,
&natural_width);
if (for_width < natural_width)
*natural_height_p += get_scrollbar_height (ST_SCROLL_VIEW (actor));
}
natural_height = child_natural_height;
if (min_height_p)
*min_height_p = 0;
sb_width = get_scrollbar_width (ST_SCROLL_VIEW (actor));
switch (priv->vscrollbar_policy)
{
case GTK_POLICY_NEVER:
break;
case GTK_POLICY_ALWAYS:
case GTK_POLICY_AUTOMATIC:
/* We've requested space for the scrollbar, subtract it back out */
for_width -= sb_width;
break;
}
switch (priv->hscrollbar_policy)
{
case GTK_POLICY_NEVER:
account_for_hscrollbar = FALSE;
break;
case GTK_POLICY_ALWAYS:
account_for_hscrollbar = TRUE;
break;
case GTK_POLICY_AUTOMATIC:
account_for_hscrollbar = for_width < child_min_width;
break;
}
clutter_actor_get_preferred_height (priv->child, for_width,
&child_min_height, &child_natural_height);
natural_height = child_natural_height;
switch (priv->vscrollbar_policy)
{
case GTK_POLICY_NEVER:
min_height = child_min_height;
break;
case GTK_POLICY_ALWAYS:
case GTK_POLICY_AUTOMATIC:
/* Should theoretically use the min height of the hscrollbar,
* but that's not cleanly defined at the moment */
min_height = 0;
break;
}
if (account_for_hscrollbar)
{
float sb_height = get_scrollbar_height (ST_SCROLL_VIEW (actor));
min_height += sb_height;
natural_height += sb_height;
}
if (min_height_p)
*min_height_p = min_height;
if (natural_height_p)
*natural_height_p = natural_height;
st_theme_node_adjust_preferred_height (theme_node, min_height_p, natural_height_p);
}
@ -385,6 +527,7 @@ st_scroll_view_allocate (ClutterActor *actor,
ClutterActorBox content_box, child_box;
ClutterActorClass *parent_parent_class;
gfloat avail_width, avail_height, sb_width, sb_height;
gboolean hscrollbar_visible, vscrollbar_visible;
StScrollViewPrivate *priv = ST_SCROLL_VIEW (actor)->priv;
StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor));
@ -408,13 +551,73 @@ st_scroll_view_allocate (ClutterActor *actor,
avail_height = content_box.y2 - content_box.y1;
sb_width = get_scrollbar_width (ST_SCROLL_VIEW (actor));
sb_height = get_scrollbar_width (ST_SCROLL_VIEW (actor));
sb_height = get_scrollbar_height (ST_SCROLL_VIEW (actor));
if (!CLUTTER_ACTOR_IS_VISIBLE (priv->vscroll))
sb_width = 0;
/* Determine what scrollbars are visible. The basic idea of the handling
* of an automatic scrollbars is that we start off with the assumption
* that that we don't need any scrollbars, see if that works, and if
* not add horizontal and vertical scrollbars until we are no longer
* overflowing.
*/
if (priv->child)
{
gfloat child_min_width;
gfloat child_min_height;
if (!CLUTTER_ACTOR_IS_VISIBLE (priv->hscroll))
sb_height = 0;
clutter_actor_get_preferred_width (priv->child, -1,
&child_min_width, NULL);
if (priv->vscrollbar_policy == GTK_POLICY_AUTOMATIC)
{
if (priv->hscrollbar_policy == GTK_POLICY_AUTOMATIC)
{
/* Pass one, try without a vertical scrollbar */
clutter_actor_get_preferred_height (priv->child, MAX (avail_width, child_min_width),
&child_min_height, NULL);
vscrollbar_visible = child_min_height > avail_height;
hscrollbar_visible = child_min_width > avail_width - (vscrollbar_visible ? sb_width : 0);
vscrollbar_visible = child_min_height > avail_height - (hscrollbar_visible ? sb_height : 0);
/* Pass two - if we needed a vertical scrollbar, get a new preferred height */
if (vscrollbar_visible)
{
clutter_actor_get_preferred_height (priv->child, MAX (avail_width - sb_width, child_min_width),
&child_min_height, NULL);
hscrollbar_visible = child_min_width > avail_width - sb_width;
}
}
else
{
hscrollbar_visible = priv->hscrollbar_policy != GTK_POLICY_NEVER;
/* try without a vertical scrollbar */
clutter_actor_get_preferred_height (priv->child, MAX (avail_width, child_min_width),
&child_min_height, NULL);
vscrollbar_visible = child_min_height > avail_height - (hscrollbar_visible ? sb_height : 0);
}
}
else
{
vscrollbar_visible = priv->vscrollbar_policy != GTK_POLICY_NEVER;
if (priv->hscrollbar_policy == GTK_POLICY_AUTOMATIC)
hscrollbar_visible = child_min_width > avail_height - (vscrollbar_visible ? 0 : sb_width);
else
hscrollbar_visible = priv->hscrollbar_policy != GTK_POLICY_NEVER;
}
}
else
{
hscrollbar_visible = FALSE;
vscrollbar_visible = FALSE;
}
/* Whether or not we show the scrollbars, if the scrollbars are visible
* actors, we need to give them some allocation, so we unconditionally
* give them the "right" allocation; that might overlap the child when
* the scrollbars are not visible, but it doesn't matter because we
* don't include them in pick or paint.
*/
/* Vertical scrollbar */
if (CLUTTER_ACTOR_IS_VISIBLE (priv->vscroll))
@ -438,6 +641,11 @@ st_scroll_view_allocate (ClutterActor *actor,
clutter_actor_allocate (priv->hscroll, &child_box, flags);
}
/* Fold visibility into the scrollbar sizes to simplify the rest of the computations */
if (!hscrollbar_visible)
sb_height = 0;
if (!vscrollbar_visible)
sb_width = 0;
/* Child */
child_box.x1 = content_box.x1;
@ -449,23 +657,26 @@ st_scroll_view_allocate (ClutterActor *actor,
clutter_actor_allocate (priv->child, &child_box, flags);
/*Shadows*/
if (CLUTTER_ACTOR_IS_VISIBLE (priv->top_shadow))
if (priv->top_shadow && CLUTTER_ACTOR_IS_VISIBLE (priv->top_shadow))
{
child_box.x1 = content_box.x1;
child_box.y1 = content_box.y1;
child_box.x2 = MAX (child_box.x1, content_box.x2 - sb_width);
child_box.y2 = MAX (child_box.y1, content_box.y1 + clutter_actor_get_height (priv->top_shadow));
child_box.y2 = content_box.y1 + clutter_actor_get_height (priv->top_shadow);
clutter_actor_allocate (priv->top_shadow, &child_box, flags);
}
if (CLUTTER_ACTOR_IS_VISIBLE (priv->bottom_shadow))
if (priv->bottom_shadow && CLUTTER_ACTOR_IS_VISIBLE (priv->bottom_shadow))
{
child_box.x1 = content_box.x1;
child_box.y1 = content_box.y2 - clutter_actor_get_height (priv->bottom_shadow);
child_box.x2 = MAX (child_box.x1, content_box.x2 - sb_width);
child_box.y2 = content_box.y2 + 1;
child_box.y1 = MAX (content_box.y1, content_box.y2 - sb_height - clutter_actor_get_height (priv->bottom_shadow));
child_box.x2 = MAX (content_box.x1, content_box.x2 - sb_width);
child_box.y2 = MAX (content_box.y1, content_box.y2 - sb_height);
clutter_actor_allocate (priv->bottom_shadow, &child_box, flags);
}
priv->hscrollbar_visible = hscrollbar_visible;
priv->vscrollbar_visible = vscrollbar_visible;
}
static void
@ -476,8 +687,11 @@ st_scroll_view_style_changed (StWidget *widget)
st_widget_style_changed (ST_WIDGET (priv->hscroll));
st_widget_style_changed (ST_WIDGET (priv->vscroll));
if (priv->top_shadow)
{
st_widget_style_changed (ST_WIDGET (priv->top_shadow));
st_widget_style_changed (ST_WIDGET (priv->bottom_shadow));
}
ST_WIDGET_CLASS (st_scroll_view_parent_class)->style_changed (widget);
}
@ -632,70 +846,67 @@ st_scroll_view_class_init (StScrollViewClass *klass)
}
static void
child_adjustment_changed_cb (StAdjustment *adjustment,
ClutterActor *bar)
disconnect_hadjustment (StScrollView *scroll)
{
StScrollView *scroll;
gdouble lower, upper, page_size;
StScrollViewPrivate *priv = scroll->priv;
scroll = ST_SCROLL_VIEW (clutter_actor_get_parent (bar));
priv->hadjustment = NULL;
}
if (bar == scroll->priv->vscroll)
update_shadow_visibility (G_OBJECT (adjustment), NULL, scroll);
static void
child_adjustment_changed_cb (StAdjustment *adjustment,
StScrollView *scroll)
{
update_shadow_visibility (scroll);
}
/* Determine if this scroll-bar should be visible */
st_adjustment_get_values (adjustment, NULL,
&lower, &upper,
NULL, NULL,
&page_size);
static void
child_adjustment_notify_value (GObject *gobject,
GParamSpec *pspec,
StScrollView *scroll)
{
update_shadow_visibility (scroll);
}
if ((upper - lower) > page_size)
clutter_actor_show (bar);
else
clutter_actor_hide (bar);
static void
disconnect_vadjustment (StScrollView *scroll)
{
StScrollViewPrivate *priv = scroll->priv;
/* Request a resize */
clutter_actor_queue_relayout (CLUTTER_ACTOR (scroll));
if (priv->vadjustment)
{
g_signal_handlers_disconnect_by_func (priv->vadjustment,
child_adjustment_notify_value,
scroll);
g_signal_handlers_disconnect_by_func (priv->vadjustment,
child_adjustment_changed_cb,
scroll);
priv->vadjustment = NULL;
}
}
static void
child_hadjustment_notify_cb (GObject *gobject,
GParamSpec *arg1,
gpointer user_data)
StScrollView *scroll)
{
StAdjustment *hadjust;
ClutterActor *actor = CLUTTER_ACTOR (gobject);
StScrollViewPrivate *priv = ST_SCROLL_VIEW (user_data)->priv;
StScrollViewPrivate *priv = scroll->priv;
hadjust = st_scroll_bar_get_adjustment (ST_SCROLL_BAR(priv->hscroll));
if (hadjust)
g_signal_handlers_disconnect_by_func (hadjust,
child_adjustment_changed_cb,
priv->hscroll);
disconnect_hadjustment (scroll);
if (priv->hscroll_policy == GTK_POLICY_NEVER)
return;
st_scrollable_get_adjustments (ST_SCROLLABLE (actor), &priv->hadjustment, NULL);
st_scroll_bar_set_adjustment (ST_SCROLL_BAR(priv->hscroll), priv->hadjustment);
st_scrollable_get_adjustments (ST_SCROLLABLE (actor), &hadjust, NULL);
if (hadjust)
if (priv->hadjustment)
{
/* Force scroll step if neede. */
if (priv->column_size_set)
{
g_object_set (hadjust,
g_object_set (priv->hadjustment,
"step-increment", priv->column_size,
NULL);
}
st_scroll_bar_set_adjustment (ST_SCROLL_BAR(priv->hscroll), hadjust);
if (priv->hscroll_policy == GTK_POLICY_AUTOMATIC)
{
g_signal_connect (hadjust, "changed", G_CALLBACK (
child_adjustment_changed_cb), priv->hscroll);
child_adjustment_changed_cb (hadjust, priv->hscroll);
}
}
}
@ -704,44 +915,29 @@ child_vadjustment_notify_cb (GObject *gobject,
GParamSpec *arg1,
gpointer user_data)
{
StAdjustment *vadjust;
StScrollView *scroll = ST_SCROLL_VIEW (user_data);
StScrollViewPrivate *priv = scroll->priv;
ClutterActor *actor = CLUTTER_ACTOR (gobject);
StScrollViewPrivate *priv = ST_SCROLL_VIEW (user_data)->priv;
disconnect_vadjustment (scroll);
vadjust = st_scroll_bar_get_adjustment (ST_SCROLL_BAR(priv->vscroll));
if (vadjust)
{
g_signal_handlers_disconnect_by_func (vadjust,
update_shadow_visibility,
user_data);
g_signal_handlers_disconnect_by_func (vadjust,
child_adjustment_changed_cb,
priv->vscroll);
}
if (priv->vscroll_policy == GTK_POLICY_NEVER)
return;
st_scrollable_get_adjustments (ST_SCROLLABLE(actor), NULL, &vadjust);
if (vadjust)
st_scrollable_get_adjustments (ST_SCROLLABLE(priv->child), NULL, &priv->vadjustment);
st_scroll_bar_set_adjustment (ST_SCROLL_BAR(priv->vscroll), priv->vadjustment);
if (priv->vadjustment)
{
/* Force scroll step if neede. */
if (priv->row_size_set)
{
g_object_set (vadjust,
g_object_set (priv->vadjustment,
"step-increment", priv->row_size,
NULL);
}
st_scroll_bar_set_adjustment (ST_SCROLL_BAR(priv->vscroll), vadjust);
if (priv->vscroll_policy == GTK_POLICY_AUTOMATIC)
{
g_signal_connect (vadjust, "changed", G_CALLBACK (
child_adjustment_changed_cb), priv->vscroll);
g_signal_connect (vadjust, "notify::value", G_CALLBACK (
update_shadow_visibility), user_data);
child_adjustment_changed_cb (vadjust, priv->vscroll);
update_shadow_visibility (G_OBJECT (vadjust), NULL, user_data);
}
g_signal_connect (priv->vadjustment, "changed",
G_CALLBACK (child_adjustment_changed_cb), scroll);
g_signal_connect (priv->vadjustment, "notify::value",
G_CALLBACK (child_adjustment_notify_value), scroll);
update_shadow_visibility (scroll);
}
}
@ -750,8 +946,8 @@ st_scroll_view_init (StScrollView *self)
{
StScrollViewPrivate *priv = self->priv = SCROLL_VIEW_PRIVATE (self);
priv->hscroll_policy = GTK_POLICY_AUTOMATIC;
priv->vscroll_policy = GTK_POLICY_AUTOMATIC;
priv->hscrollbar_policy = GTK_POLICY_AUTOMATIC;
priv->vscrollbar_policy = GTK_POLICY_AUTOMATIC;
priv->hscroll = CLUTTER_ACTOR (st_scroll_bar_new (NULL));
priv->vscroll = g_object_new (ST_TYPE_SCROLL_BAR, "vertical", TRUE, NULL);
@ -759,12 +955,6 @@ st_scroll_view_init (StScrollView *self)
clutter_actor_set_parent (priv->hscroll, CLUTTER_ACTOR (self));
clutter_actor_set_parent (priv->vscroll, CLUTTER_ACTOR (self));
priv->top_shadow = g_object_new (ST_TYPE_BIN, "style-class", "top-shadow", NULL);
priv->bottom_shadow = g_object_new (ST_TYPE_BIN, "style-class", "bottom-shadow", NULL);
clutter_actor_set_parent (priv->bottom_shadow, CLUTTER_ACTOR (self));
clutter_actor_set_parent (priv->top_shadow, CLUTTER_ACTOR (self));
/* mouse scroll is enabled by default, so we also need to be reactive */
priv->mouse_scroll = TRUE;
g_object_set (G_OBJECT (self), "reactive", TRUE, NULL);
@ -791,8 +981,8 @@ st_scroll_view_add (ClutterContainer *container,
g_signal_connect (actor, "notify::vadjustment",
G_CALLBACK (child_vadjustment_notify_cb),
container);
child_hadjustment_notify_cb (G_OBJECT (actor), NULL, container);
child_vadjustment_notify_cb (G_OBJECT (actor), NULL, container);
child_hadjustment_notify_cb (G_OBJECT (actor), NULL, self);
child_vadjustment_notify_cb (G_OBJECT (actor), NULL, self);
}
else
{
@ -856,8 +1046,11 @@ st_scroll_view_foreach_with_internals (ClutterContainer *container,
if (priv->vscroll != NULL)
callback (priv->vscroll, user_data);
if (priv->top_shadow)
{
callback (priv->top_shadow, user_data);
callback (priv->bottom_shadow, user_data);
}
}
static void
@ -1048,58 +1241,29 @@ st_scroll_view_set_policy (StScrollView *scroll,
GtkPolicyType vscroll)
{
StScrollViewPrivate *priv;
StAdjustment *hadjust, *vadjust;
g_return_if_fail (ST_IS_SCROLL_VIEW (scroll));
g_return_if_fail (hscroll != GTK_POLICY_ALWAYS && vscroll != GTK_POLICY_ALWAYS);
priv = ST_SCROLL_VIEW (scroll)->priv;
if (priv->hscroll_policy == hscroll && priv->vscroll_policy == vscroll)
if (priv->hscrollbar_policy == hscroll && priv->vscrollbar_policy == vscroll)
return;
g_object_freeze_notify ((GObject *) scroll);
if (priv->hscroll_policy != hscroll)
if (priv->hscrollbar_policy != hscroll)
{
priv->hscroll_policy = hscroll;
priv->hscrollbar_policy = hscroll;
g_object_notify ((GObject *) scroll, "hscrollbar-policy");
}
if (priv->vscroll_policy != vscroll)
if (priv->vscrollbar_policy != vscroll)
{
priv->vscroll_policy = vscroll;
priv->vscrollbar_policy = vscroll;
g_object_notify ((GObject *) scroll, "vscrollbar-policy");
}
st_scrollable_get_adjustments (ST_SCROLLABLE (priv->child), &hadjust, &vadjust);
if (priv->hscroll_policy == GTK_POLICY_NEVER)
{
hadjust = NULL;
clutter_actor_hide (priv->hscroll);
}
else if (priv->hscroll_policy == GTK_POLICY_AUTOMATIC)
{
/* We call this function because we need to set up the adjustment
* notification hooks, and the _show will queue a reallocate, from which we'll figure
* out if we need to really show or hide the scrollbar.
*/
child_hadjustment_notify_cb (G_OBJECT (priv->child), NULL, scroll);
clutter_actor_show (priv->hscroll);
}
if (priv->vscroll_policy == GTK_POLICY_NEVER)
{
vadjust = NULL;
clutter_actor_hide (priv->vscroll);
}
else
{
child_vadjustment_notify_cb (G_OBJECT (priv->child), NULL, scroll);
clutter_actor_show (priv->vscroll);
}
st_scrollable_set_adjustments (ST_SCROLLABLE (priv->child), hadjust, vadjust);
clutter_actor_queue_relayout (CLUTTER_ACTOR (scroll));
g_object_thaw_notify ((GObject *) scroll);
}

View File

@ -25,6 +25,50 @@
#include "st-scrollable.h"
/**
* SECTION:st-scrollable
* @short_description: A #ClutterActor that can be scrolled
*
* The #StScrollable interface is exposed by actors that support scrolling.
*
* The interface contains methods for getting and setting the adjustments
* for scrolling; these adjustments will be used to hook the scrolled
* position up to scrollbars or other external controls.
*
* For #StScrollable supporting height-for-width size negotation, size
* negotation works as follows:
*
* In response to get_preferred_width(), the scrollable should report
* the minimum width at which horizontal scrolling is needed for the
* preferred width, and natural width of the actor when not
* horizontally scrolled as the natural width.
*
* The for_width passed into get_preferred_height() is the width at which
* the scrollable will be allocated; this will be smaller than the minimum
* width when scrolling horizontally, so the scrollable may want to adjust
* it up to the minimum width before computing a preferred height. (Other
* scrollables may want to fit as much content into the allocated area
* as possible and only scroll what absolutely needs to scroll - consider,
* for example, the line-wrapping behavior of a text editor where there
* is a long line without any spaces.) As for width, get_preferred_height()
* should return the minimum size at which no scrolling is needed for the
* minimum height, and the natural size of the actor when not vertically scrolled
* as the natural height.
*
* In allocate() the allocation box passed in will be actual allocated
* size of the actor so will be smaller than the reported minimum
* width and/or height when scrolling is present. Any scrollable actor
* must support being allocated at any size down to 0x0 without
* crashing, however if the actor has content around the scrolled area
* and has an absolute minimum size that's bigger than 0x0 its
* acceptable for it to misdraw between 0x0 and the absolute minimum
* size. It's up to the application author to avoid letting the user
* resize the scroll view small enough so that the scrolled area
* vanishes.
*
* width-for-height size negotation is similar, with the roles of width
* and height reversed.
*/
static void
st_scrollable_base_init (gpointer g_iface)
{

View File

@ -0,0 +1,348 @@
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
const Clutter = imports.gi.Clutter;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const Shell = imports.gi.Shell;
const St = imports.gi.St;
const UI = imports.testcommon.ui;
// This is an interactive test of the sizing behavior of StScrollView. It
// may be interesting in the future to split out the two classes at the
// top into utility classes for testing the sizing behavior of other
// containers and actors.
/****************************************************************************/
// FlowedBoxes: This is a simple actor that demonstrates an interesting
// height-for-width behavior. A set of boxes of different sizes are line-wrapped
// horizontally with the minimum horizontal size being determined by the
// largest box. It would be easy to extend this to allow doing vertical
// wrapping instead, if you wanted to see just how badly our width-for-height
// implementation is or work on fixing it.
const BOX_HEIGHT = 20;
const BOX_WIDTHS = [
10, 40, 100, 20, 60, 30, 70, 10, 20, 200, 50, 70, 90, 20, 40
];
const SPACING = 10;
function FlowedBoxes() {
this._init();
}
FlowedBoxes.prototype = {
_init: function() {
this.actor = new Shell.GenericContainer();
this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
this.actor.connect('allocate', Lang.bind(this, this._allocate));
for (let i = 0; i < BOX_WIDTHS.length; i++) {
let child = new St.Bin({ width: BOX_WIDTHS[i], height: BOX_HEIGHT,
style: 'border: 1px solid #444444; background: #00aa44' })
this.actor.add_actor(child);
}
},
_getPreferredWidth: function (actor, forHeight, alloc) {
let children = this.actor.get_children();
let maxMinWidth = 0;
let totalNaturalWidth = 0;
for (let i = 0; i < children.length; i++) {
let child = children[i];
let [minWidth, naturalWidth] = child.get_preferred_width(-1);
maxMinWidth = Math.max(maxMinWidth, minWidth);
if (i != 0)
totalNaturalWidth += SPACING;
totalNaturalWidth += naturalWidth;
}
alloc.min_size = maxMinWidth;
alloc.natural_size = totalNaturalWidth;
},
_layoutChildren: function(forWidth, callback) {
let children = this.actor.get_children();
let x = 0;
let y = 0;
for (let i = 0; i < children.length; i++) {
let child = children[i];
let [minWidth, naturalWidth] = child.get_preferred_width(-1);
let [minHeight, naturalHeight] = child.get_preferred_height(naturalWidth);
let x1 = x;
if (x != 0)
x1 += SPACING;
let x2 = x1 + naturalWidth;
if (x2 > forWidth) {
if (x > 0) {
x1 = 0;
y += BOX_HEIGHT + SPACING;
}
x2 = naturalWidth;
}
callback(child, x1, y, x2, y + naturalHeight);
x = x2;
}
},
_getPreferredHeight: function (actor, forWidth, alloc) {
let height = 0;
this._layoutChildren(forWidth,
function(child, x1, y1, x2, y2) {
height = Math.max(height, y2);
});
alloc.min_size = alloc.natural_size = height;
},
_allocate: function (actor, box, flags) {
this._layoutChildren(box.x2 - box.x1,
function(child, x1, y1, x2, y2) {
child.allocate(new Clutter.ActorBox({ x1: x1, y1: y1, x2: x2, y2: y2 }),
flags);
});
}
};
/****************************************************************************/
// SizingIllustrator: this is a container that allows interactively exploring
// the sizing behavior of the child. Lines are drawn to indicate the minimum
// and natural size of the child, and a drag handle allows the user to resize
// the child interactively and see how that affects it.
//
// This is currently only written for the case where the child is height-for-width
function SizingIllustrator() {
this._init();
}
SizingIllustrator.prototype = {
_init: function() {
this.actor = new Shell.GenericContainer();
this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
this.actor.connect('allocate', Lang.bind(this, this._allocate));
this.minWidthLine = new St.Bin({ style: 'background: red' });
this.actor.add_actor(this.minWidthLine);
this.minHeightLine = new St.Bin({ style: 'background: red' });
this.actor.add_actor(this.minHeightLine);
this.naturalWidthLine = new St.Bin({ style: 'background: #4444ff' });
this.actor.add_actor(this.naturalWidthLine);
this.naturalHeightLine = new St.Bin({ style: 'background: #4444ff' });
this.actor.add_actor(this.naturalHeightLine);
this.currentWidthLine = new St.Bin({ style: 'background: #aaaaaa' });
this.actor.add_actor(this.currentWidthLine);
this.currentHeightLine = new St.Bin({ style: 'background: #aaaaaa' });
this.actor.add_actor(this.currentHeightLine);
this.handle = new St.Bin({ style: 'background: yellow; border: 1px solid black;',
reactive: true });
this.handle.connect('button-press-event', Lang.bind(this, this._handlePressed));
this.handle.connect('button-release-event', Lang.bind(this, this._handleReleased));
this.handle.connect('motion-event', Lang.bind(this, this._handleMotion));
this.actor.add_actor(this.handle);
this._inDrag = false;
this.width = 300;
this.height = 300;
},
add: function(child) {
this.child = child;
this.actor.add_actor(this.child);
this.child.lower_bottom();
},
_getPreferredWidth: function (actor, forHeight, alloc) {
let children = this.actor.get_children();
for (let i = 0; i < children.length; i++) {
let child = children[i];
let [minWidth, naturalWidth] = child.get_preferred_width(-1);
if (child == this.child) {
this.minWidth = minWidth;
this.naturalWidth = naturalWidth;
}
}
alloc.min_size = 0;
alloc.natural_size = 400;
},
_getPreferredHeight: function (actor, forWidth, alloc) {
let children = this.actor.get_children();
for (let i = 0; i < children.length; i++) {
let child = children[i];
if (child == this.child) {
[this.minHeight, this.naturalHeight] = child.get_preferred_height(this.width);
} else {
let [minWidth, naturalWidth] = child.get_preferred_width(-1);
child.get_preferred_height(naturalWidth);
}
}
alloc.min_size = 0;
alloc.natural_size = 400;
},
_allocate: function (actor, box, flags) {
let allocWidth = box.x2 - box.x1;
let allocHeight = box.y2 - box.y1;
function alloc(child, x1, y1, x2, y2) {
child.allocate(new Clutter.ActorBox({ x1: x1, y1: y1, x2: x2, y2: y2 }),
flags);
}
alloc(this.child, 0, 0, this.width, this.height);
alloc(this.minWidthLine, this.minWidth, 0, this.minWidth + 1, allocHeight);
alloc(this.naturalWidthLine, this.naturalWidth, 0, this.naturalWidth + 1, allocHeight);
alloc(this.currentWidthLine, this.width, 0, this.width + 1, allocHeight);
alloc(this.minHeightLine, 0, this.minHeight, allocWidth, this.minHeight + 1);
alloc(this.naturalHeightLine, 0, this.naturalHeight, allocWidth, this.naturalHeight + 1);
alloc(this.currentHeightLine, 0, this.height, allocWidth, this.height + 1);
alloc(this.handle, this.width, this.height, this.width + 10, this.height + 10);
},
_handlePressed: function(handle, event) {
if (event.get_button() == 1) {
this._inDrag = true;
let [handleX, handleY] = handle.get_transformed_position();
let [x, y] = event.get_coords();
this._dragX = x - handleX;
this._dragY = y - handleY;
Clutter.grab_pointer(handle);
}
},
_handleReleased: function(handle, event) {
if (event.get_button() == 1) {
this._inDrag = false;
Clutter.ungrab_pointer(handle);
}
},
_handleMotion: function(handle, event) {
if (this._inDrag) {
let [x, y] = event.get_coords();
let [actorX, actorY] = this.actor.get_transformed_position();
this.width = x - this._dragX - actorX;
this.height = y - this._dragY - actorY;
this.actor.queue_relayout();
}
}
};
/****************************************************************************/
UI.init();
let stage = Clutter.Stage.get_default();
stage.width = 600;
stage.height = 600;
let mainBox = new St.BoxLayout({ width: stage.width,
height: stage.height,
vertical: true,
style: 'padding: 10px;'
+ 'spacing: 5px;'
+ 'font: 16px sans-serif;'
+ 'background: black;'
+ 'color: white;' });
stage.add_actor(mainBox);
const DOCS = 'Red lines represent minimum size, blue lines natural size. Drag yellow handle to resize ScrollView. Click on options to change.';
let docsLabel = new St.Label({ text: DOCS });
docsLabel.clutter_text.line_wrap = true;
mainBox.add(docsLabel);
let bin = new St.Bin({ x_fill: true, y_fill: true, style: 'border: 2px solid #666666;' });
mainBox.add(bin, { x_fill: true, y_fill: true, expand: true });
let illustrator = new SizingIllustrator();
bin.add_actor(illustrator.actor);
let scrollView = new St.ScrollView();
illustrator.add(scrollView);
let box = new St.BoxLayout({ vertical: true });
scrollView.add_actor(box);
let flowedBoxes = new FlowedBoxes();
box.add(flowedBoxes.actor, { expand: false, x_fill: true, y_fill: true });
let policyBox = new St.BoxLayout({ vertical: false });
mainBox.add(policyBox);
policyBox.add(new St.Label({ text: 'Horizontal Policy: ' }));
let hpolicy = new St.Button({ label: 'AUTOMATIC', style: 'text-decoration: underline; color: #4444ff;' });
policyBox.add(hpolicy);
let spacer = new St.Bin();
policyBox.add(spacer, { expand: true });
policyBox.add(new St.Label({ text: 'Vertical Policy: '}));
let vpolicy = new St.Button({ label: 'AUTOMATIC', style: 'text-decoration: underline; color: #4444ff;' });
policyBox.add(vpolicy);
function togglePolicy(button) {
switch(button.label) {
case 'AUTOMATIC':
button.label = 'ALWAYS';
break;
case 'ALWAYS':
button.label = 'NEVER';
break;
case 'NEVER':
button.label = 'AUTOMATIC';
break;
}
scrollView.set_policy(Gtk.PolicyType[hpolicy.label], Gtk.PolicyType[vpolicy.label]);
}
hpolicy.connect('clicked', function() { togglePolicy(hpolicy); });
vpolicy.connect('clicked', function() { togglePolicy(vpolicy); });
let shadowsBox = new St.BoxLayout({ vertical: false });
mainBox.add(shadowsBox);
spacer = new St.Bin();
shadowsBox.add(spacer, { expand: true });
shadowsBox.add(new St.Label({ text: 'Vertical Shadows: '}));
let vshadows = new St.Button({ label: 'No', style: 'text-decoration: underline; color: #4444ff;' });
shadowsBox.add(vshadows);
function toggleShadows(button) {
switch(button.label) {
case 'No':
button.label = 'Yes';
break;
case 'Yes':
button.label = 'No';
break;
}
scrollView.set_vshadows(vshadows.label == 'Yes');
}
vshadows.connect('clicked', function() { toggleShadows(vshadows); });
stage.show();
Clutter.main();
stage.destroy();