* examples/hildon-bread-crumb-trail-example.c: * src/Makefile.am: * src/hildon-bread...
[hildon] / src / hildon-bread-crumb-trail.c
1 /*
2  * This file is a part of hildon
3  *
4  * Copyright (C) 2007 Nokia Corporation, all rights reserved.
5  *
6  * Contact: Michael Dominic Kostrzewa <michael.kostrzewa@nokia.com>
7  * Author: Xan Lopez <xan.lopez@nokia.com>
8  *
9  * This library is free software; you can redistribute it and/or
10  * modify it under the terms of the GNU Lesser General Public License
11  * as published by the Free Software Foundation; version 2.1 of
12  * the License, or (at your option) any later version.
13  *
14  * This library is distributed in the hope that it will be useful, but
15  * WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17  * Lesser General Public License for more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public
20  * License along with this library; if not, write to the Free Software
21  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22  * 02110-1301 USA
23  *
24  */
25
26
27 /**
28  * SECTION:hildon-bread-crumb-trail
29  * @short_description: Widget used to represent a specific path in a hierarchical tree.
30  * Stability: Unstable
31  *
32  * HildonBreadCrumbTrail is a GTK widget used to represent the currently active path in
33  * some kind of hierarchical structure (file system, media library, structured document, etc).
34  *
35  * It has built-in support for text and icon bread crumbs, but the trail only requires a very
36  * simple interface to be implemented for its children and thus new types of items can be
37  * implemented if needed. See #HildonBreadCrumb for more details.
38  */
39
40 #include "hildon-marshalers.h"
41 #include "hildon-bread-crumb-trail.h"
42
43 #define HILDON_BREAD_CRUMB_TRAIL_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), HILDON_TYPE_BREAD_CRUMB_TRAIL, HildonBreadCrumbTrailPrivate))
44
45 struct _HildonBreadCrumbTrailPrivate
46 {
47   GtkWidget *back_button;
48   GList *item_list;
49 };
50
51 /* Signals */
52
53 enum {
54   CRUMB_CLICKED,
55   LAST_SIGNAL
56 };
57
58 /* Properties */
59
60 enum {
61   PROP_0
62 };
63
64 static void hildon_bread_crumb_trail_size_request (GtkWidget *widget,
65                                                    GtkRequisition *requisition);
66 static void hildon_bread_crumb_trail_size_allocate (GtkWidget *widget,
67                                                     GtkAllocation *allocation);
68 static void hildon_bread_crumb_trail_add (GtkContainer *container,
69                                           GtkWidget *widget);
70 static void hildon_bread_crumb_trail_forall (GtkContainer *container,
71                                              gboolean include_internals,
72                                              GtkCallback callback,
73                                              gpointer callback_data);
74 static void hildon_bread_crumb_trail_remove (GtkContainer *container,
75                                              GtkWidget *widget);
76 static void hildon_bread_crumb_trail_finalize (GObject *object);
77 static void hildon_bread_crumb_trail_scroll_back (GtkWidget *button,
78                                                   HildonBreadCrumbTrail *bct);
79 static void hildon_bread_crumb_trail_update_back_button_sensitivity (HildonBreadCrumbTrail *bct);
80
81 static guint bread_crumb_trail_signals[LAST_SIGNAL] = { 0 };
82
83 /* GType methods */
84
85 G_DEFINE_TYPE (HildonBreadCrumbTrail, hildon_bread_crumb_trail, GTK_TYPE_CONTAINER)
86
87 static void
88 hildon_bread_crumb_trail_class_init (HildonBreadCrumbTrailClass *klass)
89 {
90   GObjectClass *gobject_class = (GObjectClass*)klass;
91   GtkObjectClass *object_class = (GtkObjectClass*)klass;
92   GtkWidgetClass *widget_class = (GtkWidgetClass*)klass;
93   GtkContainerClass *container_class = (GtkContainerClass*)klass;
94
95   /* GObject signals */
96   gobject_class->finalize = hildon_bread_crumb_trail_finalize;
97
98   /* GtkWidget signals */
99   widget_class->size_request = hildon_bread_crumb_trail_size_request;
100   widget_class->size_allocate = hildon_bread_crumb_trail_size_allocate;
101
102   /* GtkContainer signals */
103   container_class->add = hildon_bread_crumb_trail_add;
104   container_class->forall = hildon_bread_crumb_trail_forall;
105   container_class->remove = hildon_bread_crumb_trail_remove;
106
107   /* Style properties */
108
109 #define _BREAD_CRUMB_TRAIL_MINIMUM_WIDTH 10
110
111   /* FIXME: is this the best way to do it? */
112   gtk_widget_class_install_style_property (widget_class,
113                                            g_param_spec_int ("minimum-width",
114                                                              "Minimum width",
115                                                              "The minimum width in characters the children widgets will request",
116                                                              0,
117                                                              G_MAXINT,
118                                                              _BREAD_CRUMB_TRAIL_MINIMUM_WIDTH,
119                                                              G_PARAM_READABLE));
120
121 #define _BREAD_CRUMB_TRAIL_ARROW_SIZE 34
122
123   gtk_widget_class_install_style_property (widget_class,
124                                            g_param_spec_int ("arrow-size",
125                                                              "Arrow size",
126                                                              "Size of the back button arrow",
127                                                              0,
128                                                              G_MAXINT,
129                                                              _BREAD_CRUMB_TRAIL_ARROW_SIZE,
130                                                              G_PARAM_READABLE));
131   /* Signals */
132   bread_crumb_trail_signals[CRUMB_CLICKED] =
133     g_signal_new ("crumb-clicked",
134                   G_OBJECT_CLASS_TYPE (object_class),
135                   G_SIGNAL_RUN_LAST,
136                   G_STRUCT_OFFSET (HildonBreadCrumbTrailClass, crumb_clicked),
137                   g_signal_accumulator_true_handled, NULL,
138                   _hildon_marshal_BOOLEAN__POINTER,
139                   G_TYPE_BOOLEAN, 1,
140                   G_TYPE_POINTER);
141                   
142   /* Private data */
143   g_type_class_add_private (gobject_class, sizeof (HildonBreadCrumbTrailPrivate));
144 }
145
146 static void
147 hildon_bread_crumb_trail_finalize (GObject *object)
148 {
149   HildonBreadCrumbTrailPrivate *priv = HILDON_BREAD_CRUMB_TRAIL (object)->priv;
150
151   g_list_free (priv->item_list);
152
153   G_OBJECT_CLASS (hildon_bread_crumb_trail_parent_class)->finalize (object);
154 }
155
156 static void
157 hildon_bread_crumb_trail_size_request (GtkWidget *widget,
158                                        GtkRequisition *requisition)
159 {
160   GList *p;
161   GtkRequisition child_requisition;
162   HildonBreadCrumbTrail *bct;
163   HildonBreadCrumbTrailPrivate *priv;
164   gint minimum_width, width = 0;
165   PangoLayout *layout;
166   gchar *tmp = NULL;
167
168   bct= HILDON_BREAD_CRUMB_TRAIL (widget);
169   priv = bct->priv;
170
171   requisition->height = 0;
172   requisition->width = 0;
173
174   gtk_widget_size_request (priv->back_button, &child_requisition);
175   requisition->width = child_requisition.width;
176   requisition->height = child_requisition.height;
177
178   if (priv->item_list)
179     {
180       /* Add minimum width for one item */
181       /* TODO: this can be probably cached */
182       gtk_widget_style_get (widget,
183                             "minimum-width", &minimum_width,
184                             NULL);
185
186       tmp = g_strnfill ((gsize)minimum_width, 'm');
187       layout = gtk_widget_create_pango_layout (widget, tmp);
188       g_free (tmp);
189       pango_layout_get_size (layout, &width, NULL);
190       requisition->width += PANGO_PIXELS (width);
191       g_object_unref (layout);
192     }
193
194   /* Button requisitions */
195   for (p = priv->item_list; p; p = p->next)
196     {
197       GtkWidget *child = GTK_WIDGET (p->data);
198
199       if (GTK_WIDGET_VISIBLE (child))
200         gtk_widget_size_request (child, &child_requisition);
201     }
202
203   /* Border width */
204   requisition->width += GTK_CONTAINER (widget)->border_width * 2;
205   requisition->height += GTK_CONTAINER (widget)->border_width * 2;
206
207   widget->requisition = *requisition;
208 }
209
210 /* Document me please */
211
212 static void
213 hildon_bread_crumb_trail_size_allocate (GtkWidget *widget,
214                                         GtkAllocation *allocation)
215 {
216   GtkRequisition req;
217   gint natural_width, natural_height;
218   HildonBreadCrumb *item;
219   GtkAllocation child_allocation;
220   GtkRequisition child_requisition;
221   GtkWidget *child;
222   gint allocation_width;
223   gint border_width, width;
224   gint extra_space;
225   GList *p, *first_show, *first_hide;
226   HildonBreadCrumbTrailPrivate *priv = HILDON_BREAD_CRUMB_TRAIL (widget)->priv;
227
228   widget->allocation = *allocation;
229
230   border_width = (gint) GTK_CONTAINER (widget)->border_width;
231   allocation_width = allocation->width - 2 * border_width;
232
233   /* Allocate the back button */
234   child_allocation.x = allocation->x + border_width;
235   child_allocation.y = allocation->y + border_width;
236   gtk_widget_get_child_requisition (priv->back_button, &child_requisition);
237   child_allocation.width = child_requisition.width;
238   child_allocation.height = child_requisition.height;
239   gtk_widget_size_allocate (priv->back_button, &child_allocation);
240   child_allocation.x += child_allocation.width;
241
242   /* If there are no buttons there's nothing else to do */
243   if (priv->item_list == NULL)
244     return;
245
246   /* We find out how many buttons can we show, starting from the
247      the last one in the logical path (the first item in the list) */
248
249   width = child_allocation.width;
250   p = priv->item_list;
251   first_show = NULL;
252   first_hide = NULL; 
253   extra_space = 0;
254
255   for (p = priv->item_list; p; p = p->next)
256     {
257       item = HILDON_BREAD_CRUMB (p->data);
258       child = GTK_WIDGET (item);
259
260       /* Does the widget fit with its natural size? */
261       hildon_bread_crumb_get_natural_size (item,
262                                            &natural_width,
263                                            &natural_height);
264
265       if (width + natural_width <= allocation_width)
266         {
267           /* Yes, it does */
268           first_show = p;
269           first_hide = p->next;
270           width += natural_width;
271         }
272       else
273         {
274           /* No, it doesn't. Allocate as much as possible
275              and stop */
276           child_allocation.width = allocation_width - width;
277
278           gtk_widget_size_request (child, &req);
279
280           if (child_allocation.width > req.width)
281             {
282               first_hide = p->next;
283               gtk_widget_size_allocate (child, &child_allocation);
284               gtk_widget_show (child);
285               gtk_widget_set_child_visible (child, TRUE);
286               child_allocation.x += child_allocation.width;
287             }
288           else
289             {
290               extra_space = child_allocation.width;
291             }
292
293           break;
294         }
295     }
296
297   /* Not enough items to fill the breadcrumb? */
298   if (p == NULL && width < allocation_width)
299     {
300       extra_space = allocation_width - width;
301     }
302
303   /* Allocate the other buttons */
304   for (p = first_show; p; p = p->prev)
305     {
306       item = HILDON_BREAD_CRUMB (p->data);
307       child = GTK_WIDGET (item);
308
309       /* Does the widget fit with its natural size? */
310       hildon_bread_crumb_get_natural_size (item,
311                                            &natural_width,
312                                            &natural_height);
313
314       /* If I'm the last and there's extra space, use it */
315       if (p->prev == NULL && extra_space != 0)
316         {
317           natural_width += extra_space;
318         }
319
320       child_allocation.width = natural_width;
321       gtk_widget_size_allocate (child, &child_allocation);
322       gtk_widget_show (child);
323       gtk_widget_set_child_visible (child, TRUE);
324       child_allocation.x += child_allocation.width;
325     }
326
327   for (p = first_hide; p; p = p->next)
328     {
329       item = HILDON_BREAD_CRUMB (p->data);
330       child = GTK_WIDGET (item);
331
332       gtk_widget_hide (GTK_WIDGET (item));
333       gtk_widget_set_child_visible (GTK_WIDGET (item), FALSE);
334     }
335 }
336
337 static gpointer
338 get_bread_crumb_id (HildonBreadCrumb *item)
339 {
340   return g_object_get_data (G_OBJECT (item), "bread-crumb-id");
341 }
342
343 static void
344 crumb_activated_cb (GtkWidget *button,
345                     HildonBreadCrumbTrail *bct)
346 {
347   gboolean signal_handled = FALSE;
348
349   g_signal_emit (bct, bread_crumb_trail_signals[CRUMB_CLICKED], 0,
350                  get_bread_crumb_id (HILDON_BREAD_CRUMB (button)),
351                  &signal_handled);
352
353   if (signal_handled == FALSE)
354     {
355       GtkWidget *child;
356       HildonBreadCrumbTrailPrivate *priv;
357
358       priv = bct->priv;
359
360       child = GTK_WIDGET (priv->item_list->data);
361
362       /* We remove the tip of the list until we hit the clicked button */
363       while (child != button)
364         {
365           gtk_container_remove (GTK_CONTAINER (bct), child);
366
367           child = GTK_WIDGET (priv->item_list->data);
368         }
369     }
370 }
371
372 static void
373 hildon_bread_crumb_trail_add (GtkContainer *container,
374                               GtkWidget *widget)
375 {
376   gtk_widget_set_parent (widget, GTK_WIDGET (container));
377 }
378
379 static void
380 hildon_bread_crumb_trail_forall (GtkContainer *container,
381                                  gboolean include_internals,
382                                  GtkCallback callback,
383                                  gpointer callback_data)
384 {
385   g_return_if_fail (callback != NULL);
386   g_return_if_fail (HILDON_IS_BREAD_CRUMB_TRAIL (container));
387
388   GList *children;
389   HildonBreadCrumbTrailPrivate *priv = HILDON_BREAD_CRUMB_TRAIL (container)->priv;
390
391   children = priv->item_list;
392
393   while (children)
394     {
395       GtkWidget *child;
396       child = GTK_WIDGET (children->data);
397       children = children->next;
398
399       (*callback) (child, callback_data);
400     }
401
402   if (include_internals && priv->back_button)
403     {
404       (*callback) (priv->back_button, callback_data);
405     }
406 }
407
408 static void
409 hildon_bread_crumb_trail_remove (GtkContainer *container,
410                                  GtkWidget *widget)
411 {
412   GList *p;
413   HildonBreadCrumbTrailPrivate *priv;
414   gboolean was_visible = GTK_WIDGET_VISIBLE (widget);
415
416   priv = HILDON_BREAD_CRUMB_TRAIL (container)->priv;
417
418   p = priv->item_list;
419
420   while (p)
421     {
422       if (widget == GTK_WIDGET (p->data))
423         {
424           g_signal_handlers_disconnect_by_func (widget, G_CALLBACK (crumb_activated_cb),
425                                                 HILDON_BREAD_CRUMB_TRAIL (container));
426           gtk_widget_unparent (widget);
427
428           priv->item_list = g_list_remove_link (priv->item_list, p);
429           g_list_free (p);
430
431           hildon_bread_crumb_trail_update_back_button_sensitivity (HILDON_BREAD_CRUMB_TRAIL (container));
432
433           if (was_visible)
434             {
435               gtk_widget_queue_resize (GTK_WIDGET (container));
436             }
437         }
438
439       p = p->next;
440     }
441 }
442
443 static void
444 hildon_bread_crumb_trail_update_back_button_sensitivity (HildonBreadCrumbTrail *bct)
445 {
446   guint list_length;
447   HildonBreadCrumbTrailPrivate *priv = bct->priv;
448
449   list_length = g_list_length (priv->item_list);
450
451   if (list_length == 0 || list_length == 1)
452     {
453       gtk_widget_set_sensitive (priv->back_button, FALSE);
454     }
455   else
456     {
457       gtk_widget_set_sensitive (priv->back_button, TRUE);
458     }
459 }
460
461 static GtkWidget*
462 create_back_button (HildonBreadCrumbTrail *bct)
463 {
464   GtkWidget *button;
465   GtkWidget *arrow;
466   gint arrow_size;
467
468   gtk_widget_push_composite_child ();
469
470   button = gtk_button_new ();
471   gtk_widget_set_name (button, "hildon-bread-crumb-back-button");
472   gtk_widget_set_sensitive (button, FALSE);
473
474   arrow = gtk_arrow_new (GTK_ARROW_LEFT, GTK_SHADOW_NONE);
475   gtk_widget_style_get (GTK_WIDGET (bct),
476                         "arrow-size", &arrow_size,
477                         NULL);
478   gtk_widget_set_size_request (arrow, arrow_size, arrow_size);
479
480   gtk_container_add (GTK_CONTAINER (button), arrow);
481   gtk_container_add (GTK_CONTAINER (bct), button);
482   gtk_widget_show_all (button);
483
484   gtk_widget_pop_composite_child ();
485
486   return button;
487 }
488
489 static void
490 hildon_bread_crumb_trail_init (HildonBreadCrumbTrail *bct)
491 {
492   HildonBreadCrumbTrailPrivate *priv = HILDON_BREAD_CRUMB_TRAIL_GET_PRIVATE (bct);
493
494   GTK_WIDGET_SET_FLAGS (bct, GTK_NO_WINDOW);
495   gtk_widget_set_redraw_on_allocate (GTK_WIDGET (bct), FALSE);
496
497   bct->priv = priv;
498   priv->item_list = NULL;
499
500   priv->back_button = create_back_button (bct);
501   g_signal_connect (priv->back_button, "clicked",
502                     G_CALLBACK (hildon_bread_crumb_trail_scroll_back),
503                     bct);
504 }
505
506 static void
507 hildon_bread_crumb_trail_scroll_back (GtkWidget *button,
508                                       HildonBreadCrumbTrail *bct)
509 {
510   HildonBreadCrumb *item;
511
512   hildon_bread_crumb_trail_pop (bct);
513
514   item = HILDON_BREAD_CRUMB (bct->priv->item_list->data);
515
516   g_signal_emit (bct, bread_crumb_trail_signals[CRUMB_CLICKED], 0,
517                  get_bread_crumb_id (item));
518 }
519
520 /*
521  * Helper function to add a bread crumb to the trail. This should go in the
522  * container add method, but there's no way to plug there the id/destroy
523  * information (FIXME)
524  */
525
526 static void
527 attach_bread_crumb (HildonBreadCrumbTrail *bct,
528                     GtkWidget *bread_crumb,
529                     gpointer id,
530                     GDestroyNotify destroy)
531 {
532   g_object_set_data_full (G_OBJECT (bread_crumb), "bread-crumb-id", id, destroy);
533
534   g_signal_connect (G_OBJECT (bread_crumb), "crumb-activated",
535                     G_CALLBACK (crumb_activated_cb), bct);
536
537   bct->priv->item_list = g_list_prepend (bct->priv->item_list, bread_crumb);
538
539   gtk_container_add (GTK_CONTAINER (bct), bread_crumb);
540   gtk_widget_show (bread_crumb);
541
542   hildon_bread_crumb_trail_update_back_button_sensitivity (bct);
543 }
544
545 /* PUBLIC API */
546
547 /**
548  * hildon_bread_crumb_trail_new:
549  * 
550  * Creates a new #HildonBreadCrumbTrail widget.
551  *
552  * Returns: a #GtkWidget pointer of newly created bread crumb trail
553  * widget
554  *
555  * Stability: Unstable
556  */
557
558 GtkWidget*
559 hildon_bread_crumb_trail_new (void)
560 {
561   return GTK_WIDGET (g_object_new (HILDON_TYPE_BREAD_CRUMB_TRAIL, NULL));
562 }
563
564 /**
565  * hildon_bread_crumb_trail_push:
566  * @bct: pointer to #HildonBreadCrumbTrail
567  * @item: the #HildonBreadCrumb to be added to the trail
568  * @id: optional id for the bread crumb
569  * @destroy: GDestroyNotify callback to be called when the bread crumb is destroyed
570  *
571  * Adds a new bread crumb to the end of the trail.
572  *
573  * Stability: Unstable
574  */
575
576 void
577 hildon_bread_crumb_trail_push (HildonBreadCrumbTrail *bct,
578                                HildonBreadCrumb *item,
579                                gpointer id,
580                                GDestroyNotify destroy)
581 {
582   GtkWidget *widget;
583
584   g_return_if_fail (HILDON_IS_BREAD_CRUMB_TRAIL (bct));
585   g_return_if_fail (HILDON_IS_BREAD_CRUMB (item));
586
587   widget = GTK_WIDGET (item);
588
589   attach_bread_crumb (bct, widget, id, destroy);
590 }
591
592 /**
593  * hildon_bread_crumb_trail_push_text:
594  * @bct: pointer to #HildonBreadCrumbTrail
595  * @text: content of the new bread crumb
596  * @id: optional id for the bread crumb
597  * @destroy: GDestroyNotify callback to be called when the bread crumb is destroyed
598  *
599  * Adds a new bread crumb to the end of the trail containing the specified text.
600  *
601  * Stability: Unstable
602  */
603
604 void
605 hildon_bread_crumb_trail_push_text (HildonBreadCrumbTrail *bct,
606                                     const gchar *text,
607                                     gpointer id,
608                                     GDestroyNotify destroy)
609 {
610   GtkWidget *widget;
611
612   g_return_if_fail (HILDON_IS_BREAD_CRUMB_TRAIL (bct));
613   g_return_if_fail (text != NULL);
614
615   widget = _hildon_bread_crumb_widget_new_with_text (text);
616   attach_bread_crumb (bct, widget, id, destroy);
617 }
618
619 /**
620  * hildon_bread_crumb_trail_push_text:
621  * @bct: pointer to #HildonBreadCrumbTrail
622  * @text: content of the new bread crumb
623  * @icon: a widget to set as the icon in the bread crumb
624  * @id: optional id for the bread crumb
625  * @destroy: GDestroyNotify callback to be called when the bread crumb is destroyed
626  *
627  * Adds a new bread crumb to the end of the trail containing the specified text and
628  * icon.
629  *
630  * Stability: Unstable
631  */
632
633 void
634 hildon_bread_crumb_trail_push_icon (HildonBreadCrumbTrail *bct,
635                                     const gchar *text,
636                                     GtkWidget *icon,
637                                     gpointer id,
638                                     GDestroyNotify destroy)
639 {
640   GtkWidget *widget;
641
642   g_return_if_fail (HILDON_IS_BREAD_CRUMB_TRAIL (bct));
643   g_return_if_fail (text != NULL);
644   g_return_if_fail (GTK_IS_WIDGET (icon));
645
646   widget = _hildon_bread_crumb_widget_new_with_icon (icon, text);
647   attach_bread_crumb (bct, widget, id, destroy);
648 }
649
650 /**
651  * hildon_bread_crumb_trail_pop:
652  * @bct: pointer to #HildonBreadCrumbTrail
653  *
654  * Removes the last bread crumb from the trail.
655  *
656  * Stability: Unstable
657  */
658
659 void
660 hildon_bread_crumb_trail_pop (HildonBreadCrumbTrail *bct)
661 {
662   GtkWidget *child;
663   HildonBreadCrumbTrailPrivate *priv;
664
665   g_return_if_fail (HILDON_IS_BREAD_CRUMB_TRAIL (bct));
666
667   priv = bct->priv;
668
669   if (priv->item_list == NULL)
670     return;
671
672   if (priv->item_list)
673     {
674       child = GTK_WIDGET (priv->item_list->data);
675       gtk_container_remove (GTK_CONTAINER (bct), child);
676     }
677
678   hildon_bread_crumb_trail_update_back_button_sensitivity (bct);
679 }
680
681 /**
682  * hildon_bread_crumb_trail_clear:
683  * @bct: pointer to #HildonBreadCrumbTrail
684  *
685  * Removes all the bread crumbs from the bread crumb trail.
686  *
687  * Stability: Unstable
688  */
689
690 void
691 hildon_bread_crumb_trail_clear (HildonBreadCrumbTrail *bct)
692 {
693   HildonBreadCrumbTrailPrivate *priv;
694
695   g_return_if_fail (HILDON_IS_BREAD_CRUMB_TRAIL (bct));
696
697   priv = bct->priv;
698
699   while (priv->item_list)
700     {
701       hildon_bread_crumb_trail_pop (bct);
702     }
703 }