]> Pileus Git - ~andy/gtk/blob - gtk/gtksizerequest.c
Fixed problems with combination of height-for-width apis and
[~andy/gtk] / gtk / gtksizerequest.c
1 /* gtksizerequest.c
2  * Copyright (C) 2007-2010 Openismus GmbH
3  *
4  * Authors:
5  *      Mathias Hasselmann <mathias@openismus.com>
6  *      Tristan Van Berkom <tristan.van.berkom@gmail.com>
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Library General Public
10  * License as published by the Free Software Foundation; either
11  * version 2 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * Library General Public License for more details.
17  *
18  * You should have received a copy of the GNU Library General Public
19  * License along with this library; if not, write to the
20  * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
21  * Boston, MA 02111-1307, USA.
22  */
23
24 #include <config.h>
25
26 #include "gtksizerequest.h"
27
28 #include "gtkdebug.h"
29 #include "gtkintl.h"
30 #include "gtkprivate.h"
31 #include "gtksizegroup-private.h"
32 #include "gtkwidgetprivate.h"
33
34 /* looks for a cached size request for this for_size. If not
35  * found, returns the oldest entry so it can be overwritten
36  *
37  * Note that this caching code was directly derived from
38  * the Clutter toolkit.
39  */
40 static gboolean
41 get_cached_size (SizeRequestCache  *cache,
42                  GtkSizeGroupMode   orientation,
43                  gint               for_size,
44                  SizeRequest      **result)
45 {
46   guint i, n_sizes;
47   SizeRequest  *cached_sizes;
48
49   if (orientation == GTK_SIZE_GROUP_HORIZONTAL)
50     {
51       cached_sizes = cache->widths;
52       n_sizes = cache->cached_widths;
53     }
54   else
55     {
56       cached_sizes = cache->heights;
57       n_sizes = cache->cached_widths;
58     }
59
60   /* Search for an already cached size */
61   for (i = 0; i < n_sizes; i++)
62     {
63       if (cached_sizes[i].for_size == for_size)
64         {
65           *result = &cached_sizes[i];
66           return TRUE;
67         }
68     }
69
70   /* If not found, pull a new size from the cache, the returned size cache
71    * will immediately be used to cache the new computed size so we go ahead
72    * and increment the last_cached_width/height right away */
73   if (orientation == GTK_SIZE_GROUP_HORIZONTAL)
74     {
75       if (cache->cached_widths < GTK_SIZE_REQUEST_CACHED_SIZES)
76         {
77           cache->cached_widths++;
78           cache->last_cached_width = cache->cached_widths - 1;
79         }
80       else
81         {
82           if (++cache->last_cached_width == GTK_SIZE_REQUEST_CACHED_SIZES)
83             cache->last_cached_width = 0;
84         }
85
86       *result = &cache->widths[cache->last_cached_width];
87     }
88   else /* GTK_SIZE_GROUP_VERTICAL */
89     {
90       if (cache->cached_heights < GTK_SIZE_REQUEST_CACHED_SIZES)
91         {
92           cache->cached_heights++;
93           cache->last_cached_height = cache->cached_heights - 1;
94         }
95       else
96         {
97           if (++cache->last_cached_height == GTK_SIZE_REQUEST_CACHED_SIZES)
98             cache->last_cached_height = 0;
99         }
100
101       *result = &cache->heights[cache->last_cached_height];
102     }
103
104   return FALSE;
105 }
106
107 static void
108 do_size_request (GtkWidget      *widget,
109                  GtkRequisition *requisition)
110 {
111   /* Now we dont bother caching the deprecated "size-request" returns,
112    * just unconditionally invoke here just in case we run into legacy stuff */
113   gtk_widget_ensure_style (widget);
114   g_signal_emit_by_name (widget, "size-request", requisition);
115 }
116
117 #ifndef G_DISABLE_CHECKS
118 static GQuark recursion_check_quark = 0;
119 #endif /* G_DISABLE_CHECKS */
120
121 static void
122 push_recursion_check (GtkWidget       *request,
123                       GtkSizeGroupMode orientation,
124                       gint             for_size)
125 {
126 #ifndef G_DISABLE_CHECKS
127   const char *previous_method;
128   const char *method;
129
130   if (recursion_check_quark == 0)
131     recursion_check_quark = g_quark_from_static_string ("gtk-size-request-in-progress");
132
133   previous_method = g_object_get_qdata (G_OBJECT (request), recursion_check_quark);
134
135   if (orientation == GTK_SIZE_GROUP_HORIZONTAL)
136     {
137       method = for_size < 0 ? "get_width" : "get_width_for_height";
138     }
139   else
140     {
141       method = for_size < 0 ? "get_height" : "get_height_for_width";
142     }
143
144   if (previous_method != NULL)
145     {
146       g_warning ("%s %p: widget tried to gtk_widget_%s inside "
147                  " GtkWidget     ::%s implementation. "
148                  "Should just invoke GTK_WIDGET_GET_CLASS(widget)->%s "
149                  "directly rather than using gtk_widget_%s",
150                  G_OBJECT_TYPE_NAME (request), request,
151                  method, previous_method,
152                  method, method);
153     }
154
155   g_object_set_qdata (G_OBJECT (request), recursion_check_quark, (char*) method);
156 #endif /* G_DISABLE_CHECKS */
157 }
158
159 static void
160 pop_recursion_check (GtkWidget       *request,
161                      GtkSizeGroupMode orientation)
162 {
163 #ifndef G_DISABLE_CHECKS
164   g_object_set_qdata (G_OBJECT (request), recursion_check_quark, NULL);
165 #endif
166 }
167
168 static void
169 compute_size_for_orientation (GtkWidget         *request,
170                               GtkSizeGroupMode   orientation,
171                               gint               for_size,
172                               gint              *minimum_size,
173                               gint              *natural_size)
174 {
175   SizeRequestCache *cache;
176   SizeRequest      *cached_size;
177   GtkWidget        *widget;
178   gboolean          found_in_cache = FALSE;
179   int adjusted_min, adjusted_natural;
180
181   g_return_if_fail (GTK_IS_WIDGET (request));
182   g_return_if_fail (minimum_size != NULL || natural_size != NULL);
183
184   widget = GTK_WIDGET (request);
185   cache  = _gtk_widget_peek_request_cache (widget);
186
187   if (orientation == GTK_SIZE_GROUP_HORIZONTAL)
188     {
189       cached_size = &cache->widths[0];
190
191       if (!_gtk_widget_get_width_request_needed (widget))
192         found_in_cache = get_cached_size (cache, orientation, for_size, &cached_size);
193       else
194         {
195           memset (cache->widths, 0, GTK_SIZE_REQUEST_CACHED_SIZES * sizeof (SizeRequest));
196           cache->cached_widths = 1;
197           cache->last_cached_width = 0;
198         }
199     }
200   else
201     {
202       cached_size = &cache->heights[0];
203
204       if (!_gtk_widget_get_height_request_needed (widget))
205         found_in_cache = get_cached_size (cache, orientation, for_size, &cached_size);
206       else
207         {
208           memset (cache->heights, 0, GTK_SIZE_REQUEST_CACHED_SIZES * sizeof (SizeRequest));
209           cache->cached_heights = 1;
210           cache->last_cached_height = 0;
211         }
212     }
213
214   if (!found_in_cache)
215     {
216       GtkRequisition requisition = { 0, 0 };
217       gint min_size = 0, nat_size = 0;
218       gint requisition_size;
219
220       /* Unconditional size request runs but is often unhandled. */
221       do_size_request (widget, &requisition);
222
223       push_recursion_check (request, orientation, for_size);
224       if (orientation == GTK_SIZE_GROUP_HORIZONTAL)
225         {
226           requisition_size = requisition.width;
227
228           if (for_size < 0)
229             {
230               GTK_WIDGET_GET_CLASS (request)->get_preferred_width (request, &min_size, &nat_size);
231             }
232           else
233             {
234               int ignored_position = 0;
235               int natural_height;
236
237               /* Pull the base natural height from the cache as it's needed to adjust 
238                * the proposed 'for_size' */
239               gtk_widget_get_preferred_height (widget, NULL, &natural_height);
240
241               /* convert for_size to unadjusted height (for_size is a proposed allocation) */
242               GTK_WIDGET_GET_CLASS (request)->adjust_size_allocation (widget,
243                                                                       GTK_ORIENTATION_VERTICAL,
244                                                                       &natural_height,
245                                                                       &ignored_position,
246                                                                       &for_size);
247
248               GTK_WIDGET_GET_CLASS (request)->get_preferred_width_for_height (request, for_size,
249                                                                               &min_size, &nat_size);
250             }
251         }
252       else
253         {
254           requisition_size = requisition.height;
255
256           if (for_size < 0)
257             {
258               GTK_WIDGET_GET_CLASS (request)->get_preferred_height (request, &min_size, &nat_size);
259             }
260           else
261             {
262               int ignored_position = 0;
263               int natural_width;
264
265               /* Pull the base natural width from the cache as it's needed to adjust 
266                * the proposed 'for_size' */
267               gtk_widget_get_preferred_width (widget, NULL, &natural_width);
268
269               /* convert for_size to unadjusted width (for_size is a proposed allocation) */
270               GTK_WIDGET_GET_CLASS (request)->adjust_size_allocation (widget,
271                                                                       GTK_ORIENTATION_HORIZONTAL,
272                                                                       &natural_width,
273                                                                       &ignored_position,
274                                                                       &for_size);
275
276               GTK_WIDGET_GET_CLASS (request)->get_preferred_height_for_width (request, for_size,
277                                                                               &min_size, &nat_size);
278             }
279         }
280       pop_recursion_check (request, orientation);
281
282       if (min_size > nat_size)
283         {
284           g_warning ("%s %p reported min size %d and natural size %d; natural size must be >= min size",
285                      G_OBJECT_TYPE_NAME (request), request, min_size, nat_size);
286         }
287
288       /* Support for dangling "size-request" signal implementations on
289        * legacy widgets
290        */
291       min_size = MAX (min_size, requisition_size);
292       nat_size = MAX (nat_size, requisition_size);
293
294       cached_size->minimum_size = min_size;
295       cached_size->natural_size = nat_size;
296       cached_size->for_size     = for_size;
297
298       if (orientation == GTK_SIZE_GROUP_HORIZONTAL)
299           _gtk_widget_set_width_request_needed (widget, FALSE);
300       else
301           _gtk_widget_set_height_request_needed (widget, FALSE);
302
303       adjusted_min = cached_size->minimum_size;
304       adjusted_natural = cached_size->natural_size;
305       GTK_WIDGET_GET_CLASS (request)->adjust_size_request (GTK_WIDGET (request),
306                                                            orientation == GTK_SIZE_GROUP_HORIZONTAL ?
307                                                            GTK_ORIENTATION_HORIZONTAL :
308                                                            GTK_ORIENTATION_VERTICAL,
309                                                            &adjusted_min,
310                                                            &adjusted_natural);
311
312       if (adjusted_min < cached_size->minimum_size ||
313           adjusted_natural < cached_size->natural_size)
314         {
315           g_warning ("%s %p adjusted size %s min %d natural %d must not decrease below min %d natural %d",
316                      G_OBJECT_TYPE_NAME (request), request,
317                      orientation == GTK_SIZE_GROUP_VERTICAL ? "vertical" : "horizontal",
318                      adjusted_min, adjusted_natural,
319                      cached_size->minimum_size, cached_size->natural_size);
320           /* don't use the adjustment */
321         }
322       else if (adjusted_min > adjusted_natural)
323         {
324           g_warning ("%s %p adjusted size %s min %d natural %d original min %d natural %d has min greater than natural",
325                      G_OBJECT_TYPE_NAME (request), request,
326                      orientation == GTK_SIZE_GROUP_VERTICAL ? "vertical" : "horizontal",
327                      adjusted_min, adjusted_natural,
328                      cached_size->minimum_size, cached_size->natural_size);
329           /* don't use the adjustment */
330         }
331       else
332         {
333           /* adjustment looks good */
334           cached_size->minimum_size = adjusted_min;
335           cached_size->natural_size = adjusted_natural;
336         }
337
338       /* Update size-groups with our request and update our cached requests 
339        * with the size-group values in a single pass.
340        */
341       _gtk_size_group_bump_requisition (GTK_WIDGET (request),
342                                         orientation, 
343                                         &cached_size->minimum_size,
344                                         &cached_size->natural_size);
345     }
346
347   if (minimum_size)
348     *minimum_size = cached_size->minimum_size;
349
350   if (natural_size)
351     *natural_size = cached_size->natural_size;
352
353   g_assert (cached_size->minimum_size <= cached_size->natural_size);
354
355   GTK_NOTE (SIZE_REQUEST,
356             g_print ("[%p] %s\t%s: %d is minimum %d and natural: %d (hit cache: %s)\n",
357                      request, G_OBJECT_TYPE_NAME (request),
358                      orientation == GTK_SIZE_GROUP_HORIZONTAL ?
359                      "width for height" : "height for width" ,
360                      for_size,
361                      cached_size->minimum_size,
362                      cached_size->natural_size,
363                      found_in_cache ? "yes" : "no"));
364
365 }
366
367 /**
368  * gtk_widget_get_request_mode:
369  * @widget: a #GtkWidget instance
370  *
371  * Gets whether the widget prefers a height-for-width layout
372  * or a width-for-height layout.
373  *
374  * <note><para>#GtkBin widgets generally propagate the preference of
375  * their child, container widgets need to request something either in
376  * context of their children or in context of their allocation
377  * capabilities.</para></note>
378  *
379  * Returns: The #GtkSizeRequestMode preferred by @widget.
380  *
381  * Since: 3.0
382  */
383 GtkSizeRequestMode
384 gtk_widget_get_request_mode (GtkWidget *widget)
385 {
386   GtkWidgetClass *klass;
387
388   g_return_val_if_fail (GTK_IS_WIDGET (widget), GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH);
389
390   klass = GTK_WIDGET_GET_CLASS (widget);
391   if (klass->get_request_mode)
392     return klass->get_request_mode (widget);
393
394   /* By default widgets are height-for-width. */
395   return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
396 }
397
398 /**
399  * gtk_widget_get_preferred_width:
400  * @widget: a #GtkWidget instance
401  * @minimum_width: (out) (allow-none): location to store the minimum width, or %NULL
402  * @natural_width: (out) (allow-none): location to store the natural width, or %NULL
403  *
404  * Retrieves a widget's initial minimum and natural width.
405  *
406  * <note><para>This call is specific to height-for-width
407  * requests.</para></note>
408  *
409  * The returned request will be modified by the
410  * GtkWidgetClass::adjust_size_request virtual method and by any
411  * #GtkSizeGroup that have been applied. That is, the returned request
412  * is the one that should be used for layout, not necessarily the one
413  * returned by the widget itself.
414  *
415  * Since: 3.0
416  */
417 void
418 gtk_widget_get_preferred_width (GtkWidget *widget,
419                                 gint      *minimum_width,
420                                 gint      *natural_width)
421 {
422   compute_size_for_orientation (widget, GTK_SIZE_GROUP_HORIZONTAL,
423                                 -1, minimum_width, natural_width);
424 }
425
426
427 /**
428  * gtk_widget_get_preferred_height:
429  * @widget: a #GtkWidget instance
430  * @minimum_height: (out) (allow-none): location to store the minimum height, or %NULL
431  * @natural_height: (out) (allow-none): location to store the natural height, or %NULL
432  *
433  * Retrieves a widget's initial minimum and natural height.
434  *
435  * <note><para>This call is specific to width-for-height requests.</para></note>
436  *
437  * The returned request will be modified by the
438  * GtkWidgetClass::adjust_size_request virtual method and by any
439  * #GtkSizeGroup that have been applied. That is, the returned request
440  * is the one that should be used for layout, not necessarily the one
441  * returned by the widget itself.
442  *
443  * Since: 3.0
444  */
445 void
446 gtk_widget_get_preferred_height (GtkWidget *widget,
447                                  gint      *minimum_height,
448                                  gint      *natural_height)
449 {
450   compute_size_for_orientation (widget, GTK_SIZE_GROUP_VERTICAL,
451                                 -1, minimum_height, natural_height);
452 }
453
454
455
456 /**
457  * gtk_widget_get_preferred_width_for_height:
458  * @widget: a #GtkWidget instance
459  * @height: the height which is available for allocation
460  * @minimum_width: (out) (allow-none): location for storing the minimum width, or %NULL
461  * @natural_width: (out) (allow-none): location for storing the natural width, or %NULL
462  *
463  * Retrieves a widget's minimum and natural width if it would be given
464  * the specified @height.
465  *
466  * The returned request will be modified by the
467  * GtkWidgetClass::adjust_size_request virtual method and by any
468  * #GtkSizeGroup that have been applied. That is, the returned request
469  * is the one that should be used for layout, not necessarily the one
470  * returned by the widget itself.
471  *
472  * Since: 3.0
473  */
474 void
475 gtk_widget_get_preferred_width_for_height (GtkWidget *widget,
476                                            gint       height,
477                                            gint      *minimum_width,
478                                            gint      *natural_width)
479 {
480   compute_size_for_orientation (widget, GTK_SIZE_GROUP_HORIZONTAL,
481                                 height, minimum_width, natural_width);
482 }
483
484 /**
485  * gtk_widget_get_preferred_height_for_width:
486  * @widget: a #GtkWidget instance
487  * @width: the width which is available for allocation
488  * @minimum_height: (out) (allow-none): location for storing the minimum height, or %NULL
489  * @natural_height: (out) (allow-none): location for storing the natural height, or %NULL
490  *
491  * Retrieves a widget's minimum and natural height if it would be given
492  * the specified @width.
493  *
494  * The returned request will be modified by the
495  * GtkWidgetClass::adjust_size_request virtual method and by any
496  * #GtkSizeGroup that have been applied. That is, the returned request
497  * is the one that should be used for layout, not necessarily the one
498  * returned by the widget itself.
499  *
500  * Since: 3.0
501  */
502 void
503 gtk_widget_get_preferred_height_for_width (GtkWidget *widget,
504                                            gint       width,
505                                            gint      *minimum_height,
506                                            gint      *natural_height)
507 {
508   compute_size_for_orientation (widget, GTK_SIZE_GROUP_VERTICAL,
509                                 width, minimum_height, natural_height);
510 }
511
512 /**
513  * gtk_widget_get_preferred_size:
514  * @widget: a #GtkWidget instance
515  * @minimum_size: (out) (allow-none): location for storing the minimum size, or %NULL
516  * @natural_size: (out) (allow-none): location for storing the natural size, or %NULL
517  *
518  * Retrieves the minimum and natural size of a widget taking
519  * into account the widget's preference for height-for-width management.
520  *
521  * This is used to retrieve a suitable size by container widgets which do
522  * not impose any restrictions on the child placement. It can be used
523  * to deduce toplevel window and menu sizes as well as child widgets in
524  * free form containers such as GtkLayout.
525  *
526  * <note><para>Handle with care, note that the natural height of a height-for-width
527  * widget will generally be a smaller size than the minimum height, since the required
528  * height for the natural width is generally smaller than the required height for
529  * the minimum width.</para></note>
530  *
531  * Since: 3.0
532  */
533 void
534 gtk_widget_get_preferred_size (GtkWidget      *widget,
535                                GtkRequisition *minimum_size,
536                                GtkRequisition *natural_size)
537 {
538   gint min_width, nat_width;
539   gint min_height, nat_height;
540
541   g_return_if_fail (GTK_IS_WIDGET (widget));
542
543   if (gtk_widget_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH)
544     {
545       gtk_widget_get_preferred_width (widget, &min_width, &nat_width);
546
547       if (minimum_size)
548         {
549           minimum_size->width = min_width;
550           gtk_widget_get_preferred_height_for_width (widget, min_width,
551                                                      &minimum_size->height, NULL);
552         }
553
554       if (natural_size)
555         {
556           natural_size->width = nat_width;
557           gtk_widget_get_preferred_height_for_width (widget, nat_width,
558                                                      NULL, &natural_size->height);
559         }
560     }
561   else /* GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT */
562     {
563       gtk_widget_get_preferred_height (widget, &min_height, &nat_height);
564
565       if (minimum_size)
566         {
567           minimum_size->height = min_height;
568           gtk_widget_get_preferred_width_for_height (widget, min_height,
569                                                      &minimum_size->width, NULL);
570         }
571
572       if (natural_size)
573         {
574           natural_size->height = nat_height;
575           gtk_widget_get_preferred_width_for_height (widget, nat_height,
576                                                      NULL, &natural_size->width);
577         }
578     }
579 }
580
581
582 static gint
583 compare_gap (gconstpointer p1,
584              gconstpointer p2,
585              gpointer      data)
586 {
587   GtkRequestedSize *sizes = data;
588   const guint *c1 = p1;
589   const guint *c2 = p2;
590
591   const gint d1 = MAX (sizes[*c1].natural_size -
592                        sizes[*c1].minimum_size,
593                        0);
594   const gint d2 = MAX (sizes[*c2].natural_size -
595                        sizes[*c2].minimum_size,
596                        0);
597
598   gint delta = (d2 - d1);
599
600   if (0 == delta)
601     delta = (*c2 - *c1);
602
603   return delta;
604 }
605
606 /**
607  * gtk_distribute_natural_allocation: 
608  * @extra_space: Extra space to redistribute among children after subtracting
609  *               minimum sizes and any child padding from the overall allocation
610  * @n_requested_sizes: Number of requests to fit into the allocation
611  * @sizes: An array of structs with a client pointer and a minimum/natural size
612  *         in the orientation of the allocation.
613  *
614  * Distributes @extra_space to child @sizes by bringing up smaller
615  * children up to natural size first.
616  *
617  * The remaining space will be added to the @minimum_size member of the
618  * GtkRequestedSize struct. If all sizes reach their natural size then
619  * the remaining space is returned.
620  *
621  * Returns: The remainder of @extra_space after redistributing space
622  * to @sizes.
623  */
624 gint 
625 gtk_distribute_natural_allocation (gint              extra_space,
626                                    guint             n_requested_sizes,
627                                    GtkRequestedSize *sizes)
628 {
629   guint *spreading;
630   gint   i;
631
632   g_return_val_if_fail (extra_space >= 0, 0);
633
634   spreading = g_newa (guint, n_requested_sizes);
635
636   for (i = 0; i < n_requested_sizes; i++)
637     spreading[i] = i;
638
639   /* Distribute the container's extra space c_gap. We want to assign
640    * this space such that the sum of extra space assigned to children
641    * (c^i_gap) is equal to c_cap. The case that there's not enough
642    * space for all children to take their natural size needs some
643    * attention. The goals we want to achieve are:
644    *
645    *   a) Maximize number of children taking their natural size.
646    *   b) The allocated size of children should be a continuous
647    *   function of c_gap.  That is, increasing the container size by
648    *   one pixel should never make drastic changes in the distribution.
649    *   c) If child i takes its natural size and child j doesn't,
650    *   child j should have received at least as much gap as child i.
651    *
652    * The following code distributes the additional space by following
653    * these rules.
654    */
655   
656   /* Sort descending by gap and position. */
657   g_qsort_with_data (spreading,
658                      n_requested_sizes, sizeof (guint),
659                      compare_gap, sizes);
660   
661   /* Distribute available space.
662    * This master piece of a loop was conceived by Behdad Esfahbod.
663    */
664   for (i = n_requested_sizes - 1; extra_space > 0 && i >= 0; --i)
665     {
666       /* Divide remaining space by number of remaining children.
667        * Sort order and reducing remaining space by assigned space
668        * ensures that space is distributed equally.
669        */
670       gint glue = (extra_space + i) / (i + 1);
671       gint gap = sizes[(spreading[i])].natural_size
672         - sizes[(spreading[i])].minimum_size;
673       
674       gint extra = MIN (glue, gap);
675       
676       sizes[spreading[i]].minimum_size += extra;
677       
678       extra_space -= extra;
679     }
680
681   return extra_space;
682 }
683