1/* Lists/Clocks
2 * #Keywords: GtkGridView, GtkListItemFactory, GListModel
3 *
4 * This demo displays the time in different timezones.
5 *
6 * The goal is to show how to set up expressions that track changes
7 * in objects and make them update widgets. For that, we create a
8 * clock object that updates its time every second and then use
9 * various ways to display that time.
10 *
11 * Typically, this will be done using GtkBuilder .ui files with the
12 * help of the <binding> tag, but this demo shows the code that runs
13 * behind that.
14 */
15
16#include <gtk/gtk.h>
17
18#define GTK_TYPE_CLOCK (gtk_clock_get_type ())
19G_DECLARE_FINAL_TYPE (GtkClock, gtk_clock, GTK, CLOCK, GObject)
20
21/* This is our object. It's just a timezone */
22typedef struct _GtkClock GtkClock;
23struct _GtkClock
24{
25 GObject parent_instance;
26
27 /* We allow this to be NULL for the local timezone */
28 GTimeZone *timezone;
29 /* Name of the location we're displaying time for */
30 char *location;
31};
32
33enum {
34 PROP_0,
35 PROP_LOCATION,
36 PROP_TIME,
37 PROP_TIMEZONE,
38
39 N_PROPS
40};
41
42/* This function returns the current time in the clock's timezone.
43 * Note that this returns a new object every time, so we need to
44 * remember to unref it after use.
45 */
46static GDateTime *
47gtk_clock_get_time (GtkClock *clock)
48{
49 if (clock->timezone)
50 return g_date_time_new_now (tz: clock->timezone);
51 else
52 return g_date_time_new_now_local ();
53}
54
55/* Here, we implement the functionality required by the GdkPaintable
56 * interface. This way we have a trivial way to display an analog clock.
57 * It also allows demonstrating how to directly use objects in the
58 * listview later by making this object do something interesting.
59 */
60static void
61gtk_clock_snapshot (GdkPaintable *paintable,
62 GdkSnapshot *snapshot,
63 double width,
64 double height)
65{
66 GtkClock *self = GTK_CLOCK (ptr: paintable);
67 GDateTime *time;
68 GskRoundedRect outline;
69
70#define BLACK ((GdkRGBA) { 0, 0, 0, 1 })
71
72 /* save/restore() is necessary so we can undo the transforms we start
73 * out with.
74 */
75 gtk_snapshot_save (snapshot);
76
77 /* First, we move the (0, 0) point to the center of the area so
78 * we can draw everything relative to it.
79 */
80 gtk_snapshot_translate (snapshot, point: &GRAPHENE_POINT_INIT (width / 2, height / 2));
81
82 /* Next we scale it, so that we can pretend that the clock is
83 * 100px in size. That way, we don't need to do any complicated
84 * math later. We use MIN() here so that we use the smaller
85 * dimension for sizing. That way we don't overdraw but keep
86 * the aspect ratio.
87 */
88 gtk_snapshot_scale (snapshot, MIN (width, height) / 100.0, MIN (width, height) / 100.0);
89
90 /* Now we have a circle with diameter 100px (and radius 50px) that
91 * has its (0, 0) point at the center. Let's draw a simple clock into it.
92 */
93 time = gtk_clock_get_time (clock: self);
94
95 /* First, draw a circle. This is a neat little trick to draw a circle
96 * without requiring Cairo.
97 */
98 gsk_rounded_rect_init_from_rect (self: &outline, bounds: &GRAPHENE_RECT_INIT(-50, -50, 100, 100), radius: 50);
99 gtk_snapshot_append_border (snapshot,
100 outline: &outline,
101 border_width: (float[4]) { 4, 4, 4, 4 },
102 border_color: (GdkRGBA [4]) { BLACK, BLACK, BLACK, BLACK });
103
104 /* Next, draw the hour hand.
105 * We do this using transforms again: Instead of computing where the angle
106 * points to, we just rotate everything and then draw the hand as if it
107 * was :00. We don't even need to care about am/pm here because rotations
108 * just work.
109 */
110 gtk_snapshot_save (snapshot);
111 gtk_snapshot_rotate (snapshot, angle: 30 * g_date_time_get_hour (datetime: time) + 0.5 * g_date_time_get_minute (datetime: time));
112 gsk_rounded_rect_init_from_rect (self: &outline, bounds: &GRAPHENE_RECT_INIT(-2, -23, 4, 25), radius: 2);
113 gtk_snapshot_push_rounded_clip (snapshot, bounds: &outline);
114 gtk_snapshot_append_color (snapshot, color: &BLACK, bounds: &outline.bounds);
115 gtk_snapshot_pop (snapshot);
116 gtk_snapshot_restore (snapshot);
117
118 /* And the same as above for the minute hand. Just make this one longer
119 * so people can tell the hands apart.
120 */
121 gtk_snapshot_save (snapshot);
122 gtk_snapshot_rotate (snapshot, angle: 6 * g_date_time_get_minute (datetime: time));
123 gsk_rounded_rect_init_from_rect (self: &outline, bounds: &GRAPHENE_RECT_INIT(-2, -43, 4, 45), radius: 2);
124 gtk_snapshot_push_rounded_clip (snapshot, bounds: &outline);
125 gtk_snapshot_append_color (snapshot, color: &BLACK, bounds: &outline.bounds);
126 gtk_snapshot_pop (snapshot);
127 gtk_snapshot_restore (snapshot);
128
129 /* and finally, the second indicator. */
130 gtk_snapshot_save (snapshot);
131 gtk_snapshot_rotate (snapshot, angle: 6 * g_date_time_get_second (datetime: time));
132 gsk_rounded_rect_init_from_rect (self: &outline, bounds: &GRAPHENE_RECT_INIT(-2, -43, 4, 10), radius: 2);
133 gtk_snapshot_push_rounded_clip (snapshot, bounds: &outline);
134 gtk_snapshot_append_color (snapshot, color: &BLACK, bounds: &outline.bounds);
135 gtk_snapshot_pop (snapshot);
136 gtk_snapshot_restore (snapshot);
137
138 /* And finally, don't forget to restore the initial save() that
139 * we did for the initial transformations.
140 */
141 gtk_snapshot_restore (snapshot);
142
143 g_date_time_unref (datetime: time);
144}
145
146/* Our desired size is 100px. That sounds okay for an analog clock */
147static int
148gtk_clock_get_intrinsic_width (GdkPaintable *paintable)
149{
150 return 100;
151}
152
153static int
154gtk_clock_get_intrinsic_height (GdkPaintable *paintable)
155{
156 return 100;
157}
158
159/* Initialize the paintable interface. This way we turn our clocks
160 * into objects that can be drawn. There are more functions to this
161 * interface to define desired size, but this is enough.
162 */
163static void
164gtk_clock_paintable_init (GdkPaintableInterface *iface)
165{
166 iface->snapshot = gtk_clock_snapshot;
167 iface->get_intrinsic_width = gtk_clock_get_intrinsic_width;
168 iface->get_intrinsic_height = gtk_clock_get_intrinsic_height;
169}
170
171/* Finally, we define the type. The important part is adding the
172 * paintable interface, so GTK knows that this object can indeed
173 * be drawn.
174 */
175G_DEFINE_TYPE_WITH_CODE (GtkClock, gtk_clock, G_TYPE_OBJECT,
176 G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE,
177 gtk_clock_paintable_init))
178
179static GParamSpec *properties[N_PROPS] = { NULL, };
180
181static void
182gtk_clock_get_property (GObject *object,
183 guint property_id,
184 GValue *value,
185 GParamSpec *pspec)
186{
187 GtkClock *self = GTK_CLOCK (ptr: object);
188
189 switch (property_id)
190 {
191 case PROP_LOCATION:
192 g_value_set_string (value, v_string: self->location);
193 break;
194
195 case PROP_TIME:
196 g_value_take_boxed (value, v_boxed: gtk_clock_get_time (clock: self));
197 break;
198
199 case PROP_TIMEZONE:
200 g_value_set_boxed (value, v_boxed: self->timezone);
201 break;
202
203 default:
204 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
205 break;
206 }
207}
208
209static void
210gtk_clock_set_property (GObject *object,
211 guint property_id,
212 const GValue *value,
213 GParamSpec *pspec)
214{
215 GtkClock *self = GTK_CLOCK (ptr: object);
216
217 switch (property_id)
218 {
219 case PROP_LOCATION:
220 self->location = g_value_dup_string (value);
221 break;
222
223 case PROP_TIMEZONE:
224 self->timezone = g_value_dup_boxed (value);
225 break;
226
227 default:
228 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
229 break;
230 }
231}
232
233/* This is the list of all the ticking clocks */
234static GSList *ticking_clocks = NULL;
235
236/* This is the ID of the timeout source that is updating all
237 * ticking clocks.
238 */
239static guint ticking_clock_id = 0;
240
241/* Every second, this function is called to tell everybody that
242 * the clocks are ticking.
243 */
244static gboolean
245gtk_clock_tick (gpointer unused)
246{
247 GSList *l;
248
249 for (l = ticking_clocks; l; l = l->next)
250 {
251 GtkClock *clock = l->data;
252
253 /* We will now return a different value for the time property,
254 * so notify about that.
255 */
256 g_object_notify_by_pspec (G_OBJECT (clock), pspec: properties[PROP_TIME]);
257
258 /* We will also draw the hands of the clock differently.
259 * So notify about that, too.
260 */
261 gdk_paintable_invalidate_contents (paintable: GDK_PAINTABLE (ptr: clock));
262 }
263
264 return G_SOURCE_CONTINUE;
265}
266
267static void
268gtk_clock_stop_ticking (GtkClock *self)
269{
270 ticking_clocks = g_slist_remove (list: ticking_clocks, data: self);
271
272 /* If no clock is remaining, stop running the tick updates */
273 if (ticking_clocks == NULL && ticking_clock_id != 0)
274 g_clear_handle_id (&ticking_clock_id, g_source_remove);
275}
276
277static void
278gtk_clock_start_ticking (GtkClock *self)
279{
280 /* if no clock is ticking yet, start */
281 if (ticking_clock_id == 0)
282 ticking_clock_id = g_timeout_add_seconds (interval: 1, function: gtk_clock_tick, NULL);
283
284 ticking_clocks = g_slist_prepend (list: ticking_clocks, data: self);
285}
286
287static void
288gtk_clock_finalize (GObject *object)
289{
290 GtkClock *self = GTK_CLOCK (ptr: object);
291
292 gtk_clock_stop_ticking (self);
293
294 g_free (mem: self->location);
295 g_clear_pointer (&self->timezone, g_time_zone_unref);
296
297 G_OBJECT_CLASS (gtk_clock_parent_class)->finalize (object);
298}
299
300static void
301gtk_clock_class_init (GtkClockClass *klass)
302{
303 GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
304
305 gobject_class->get_property = gtk_clock_get_property;
306 gobject_class->set_property = gtk_clock_set_property;
307 gobject_class->finalize = gtk_clock_finalize;
308
309 properties[PROP_LOCATION] =
310 g_param_spec_string (name: "location", NULL, NULL, NULL, flags: G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
311 properties[PROP_TIME] =
312 g_param_spec_boxed (name: "time", NULL, NULL, G_TYPE_DATE_TIME, flags: G_PARAM_READABLE);
313 properties[PROP_TIMEZONE] =
314 g_param_spec_boxed (name: "timezone", NULL, NULL, G_TYPE_TIME_ZONE, flags: G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
315
316 g_object_class_install_properties (oclass: gobject_class, n_pspecs: N_PROPS, pspecs: properties);
317}
318
319static void
320gtk_clock_init (GtkClock *self)
321{
322 gtk_clock_start_ticking (self);
323}
324
325static GtkClock *
326gtk_clock_new (const char *location,
327 GTimeZone *_tz)
328{
329 GtkClock *result;
330
331 result = g_object_new (GTK_TYPE_CLOCK,
332 first_property_name: "location", location,
333 "timezone", _tz,
334 NULL);
335
336 g_clear_pointer (&_tz, g_time_zone_unref);
337
338 return result;
339}
340
341static GListModel *
342create_clocks_model (void)
343{
344 GListStore *result;
345 GtkClock *clock;
346
347 result = g_list_store_new (GTK_TYPE_CLOCK);
348
349 /* local time */
350 clock = gtk_clock_new (location: "local", NULL);
351 g_list_store_append (store: result, item: clock);
352 g_object_unref (object: clock);
353 /* UTC time */
354 clock = gtk_clock_new (location: "UTC", tz: g_time_zone_new_utc ());
355 g_list_store_append (store: result, item: clock);
356 g_object_unref (object: clock);
357 /* A bunch of timezones with GTK hackers */
358 clock = gtk_clock_new (location: "San Francisco", tz: g_time_zone_new (identifier: "America/Los_Angeles"));
359 g_list_store_append (store: result, item: clock);
360 g_object_unref (object: clock);
361 clock = gtk_clock_new (location: "Xalapa", tz: g_time_zone_new (identifier: "America/Mexico_City"));
362 g_list_store_append (store: result, item: clock);
363 g_object_unref (object: clock);
364 clock = gtk_clock_new (location: "Boston", tz: g_time_zone_new (identifier: "America/New_York"));
365 g_list_store_append (store: result, item: clock);
366 g_object_unref (object: clock);
367 clock = gtk_clock_new (location: "London", tz: g_time_zone_new (identifier: "Europe/London"));
368 g_list_store_append (store: result, item: clock);
369 g_object_unref (object: clock);
370 clock = gtk_clock_new (location: "Berlin", tz: g_time_zone_new (identifier: "Europe/Berlin"));
371 g_list_store_append (store: result, item: clock);
372 g_object_unref (object: clock);
373 clock = gtk_clock_new (location: "Moscow", tz: g_time_zone_new (identifier: "Europe/Moscow"));
374 g_list_store_append (store: result, item: clock);
375 g_object_unref (object: clock);
376 clock = gtk_clock_new (location: "New Delhi", tz: g_time_zone_new (identifier: "Asia/Kolkata"));
377 g_list_store_append (store: result, item: clock);
378 g_object_unref (object: clock);
379 clock = gtk_clock_new (location: "Shanghai", tz: g_time_zone_new (identifier: "Asia/Shanghai"));
380 g_list_store_append (store: result, item: clock);
381 g_object_unref (object: clock);
382
383 return G_LIST_MODEL (ptr: result);
384}
385
386static char *
387convert_time_to_string (GObject *image,
388 GDateTime *time,
389 gpointer unused)
390{
391 return g_date_time_format (datetime: time, format: "%x\n%X");
392}
393
394/* And this function is the crux for this whole demo.
395 * It shows how to use expressions to set up bindings.
396 */
397static void
398setup_listitem_cb (GtkListItemFactory *factory,
399 GtkListItem *list_item)
400{
401 GtkWidget *box, *picture, *location_label, *time_label;
402 GtkExpression *clock_expression, *expression;
403
404 box = gtk_box_new (orientation: GTK_ORIENTATION_VERTICAL, spacing: 0);
405 gtk_list_item_set_child (self: list_item, child: box);
406
407 /* First, we create an expression that gets us the clock from the listitem:
408 * 1. Create an expression that gets the list item.
409 * 2. Use that expression's "item" property to get the clock
410 */
411 expression = gtk_constant_expression_new (GTK_TYPE_LIST_ITEM, list_item);
412 clock_expression = gtk_property_expression_new (GTK_TYPE_LIST_ITEM, expression, property_name: "item");
413
414 /* Bind the clock's location to a label.
415 * This is easy: We just get the "location" property of the clock.
416 */
417 expression = gtk_property_expression_new (GTK_TYPE_CLOCK,
418 expression: gtk_expression_ref (self: clock_expression),
419 property_name: "location");
420 /* Now create the label and bind the expression to it. */
421 location_label = gtk_label_new (NULL);
422 gtk_expression_bind (self: expression, target: location_label, property: "label", this_: location_label);
423 gtk_box_append (GTK_BOX (box), child: location_label);
424
425
426 /* Here we bind the item itself to a GdkPicture.
427 * This is simply done by using the clock expression itself.
428 */
429 expression = gtk_expression_ref (self: clock_expression);
430 /* Now create the widget and bind the expression to it. */
431 picture = gtk_picture_new ();
432 gtk_expression_bind (self: expression, target: picture, property: "paintable", this_: picture);
433 gtk_box_append (GTK_BOX (box), child: picture);
434
435
436 /* And finally, everything comes together.
437 * We create a label for displaying the time as text.
438 * For that, we need to transform the "GDateTime" of the
439 * time property into a string so that the label can display it.
440 */
441 expression = gtk_property_expression_new (GTK_TYPE_CLOCK,
442 expression: gtk_expression_ref (self: clock_expression),
443 property_name: "time");
444 expression = gtk_cclosure_expression_new (G_TYPE_STRING,
445 NULL,
446 n_params: 1, params: (GtkExpression *[1]) { expression },
447 G_CALLBACK (convert_time_to_string),
448 NULL, NULL);
449 /* Now create the label and bind the expression to it. */
450 time_label = gtk_label_new (NULL);
451 gtk_expression_bind (self: expression, target: time_label, property: "label", this_: time_label);
452 gtk_box_append (GTK_BOX (box), child: time_label);
453
454 gtk_expression_unref (self: clock_expression);
455}
456
457static GtkWidget *window = NULL;
458
459GtkWidget *
460do_listview_clocks (GtkWidget *do_widget)
461{
462 if (window == NULL)
463 {
464 GtkWidget *gridview, *sw;
465 GtkListItemFactory *factory;
466 GtkSelectionModel *model;
467
468 /* This is the normal window setup code every demo does */
469 window = gtk_window_new ();
470 gtk_window_set_title (GTK_WINDOW (window), title: "Clocks");
471 gtk_window_set_default_size (GTK_WINDOW (window), width: 600, height: 400);
472 gtk_window_set_display (GTK_WINDOW (window),
473 display: gtk_widget_get_display (widget: do_widget));
474 g_object_add_weak_pointer (G_OBJECT (window), weak_pointer_location: (gpointer *) &window);
475
476 /* List widgets go into a scrolled window. Always. */
477 sw = gtk_scrolled_window_new ();
478 gtk_window_set_child (GTK_WINDOW (window), child: sw);
479
480 /* Create the factory that creates the listitems. Because we
481 * used bindings above during setup, we only need to connect
482 * to the setup signal.
483 * The bindings take care of the bind step.
484 */
485 factory = gtk_signal_list_item_factory_new ();
486 g_signal_connect (factory, "setup", G_CALLBACK (setup_listitem_cb), NULL);
487
488 model = GTK_SELECTION_MODEL (ptr: gtk_no_selection_new (model: create_clocks_model ()));
489 gridview = gtk_grid_view_new (model, factory);
490 gtk_scrollable_set_hscroll_policy (GTK_SCROLLABLE (gridview), policy: GTK_SCROLL_NATURAL);
491 gtk_scrollable_set_vscroll_policy (GTK_SCROLLABLE (gridview), policy: GTK_SCROLL_NATURAL);
492
493 gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), child: gridview);
494 }
495
496 if (!gtk_widget_get_visible (widget: window))
497 gtk_widget_show (widget: window);
498 else
499 gtk_window_destroy (GTK_WINDOW (window));
500
501 return window;
502}
503

source code of gtk/demos/gtk-demo/listview_clocks.c