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 ()) |
19 | G_DECLARE_FINAL_TYPE (GtkClock, gtk_clock, GTK, CLOCK, GObject) |
20 | |
21 | /* This is our object. It's just a timezone */ |
22 | typedef struct _GtkClock GtkClock; |
23 | struct _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 | |
33 | enum { |
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 | */ |
46 | static GDateTime * |
47 | gtk_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 | */ |
60 | static void |
61 | gtk_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 */ |
147 | static int |
148 | gtk_clock_get_intrinsic_width (GdkPaintable *paintable) |
149 | { |
150 | return 100; |
151 | } |
152 | |
153 | static int |
154 | gtk_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 | */ |
163 | static void |
164 | gtk_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 | */ |
175 | G_DEFINE_TYPE_WITH_CODE (GtkClock, gtk_clock, G_TYPE_OBJECT, |
176 | G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE, |
177 | gtk_clock_paintable_init)) |
178 | |
179 | static GParamSpec *properties[N_PROPS] = { NULL, }; |
180 | |
181 | static void |
182 | gtk_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 | |
209 | static void |
210 | gtk_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 */ |
234 | static GSList *ticking_clocks = NULL; |
235 | |
236 | /* This is the ID of the timeout source that is updating all |
237 | * ticking clocks. |
238 | */ |
239 | static guint ticking_clock_id = 0; |
240 | |
241 | /* Every second, this function is called to tell everybody that |
242 | * the clocks are ticking. |
243 | */ |
244 | static gboolean |
245 | gtk_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 | |
267 | static void |
268 | gtk_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 | |
277 | static void |
278 | gtk_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 | |
287 | static void |
288 | gtk_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 | |
300 | static void |
301 | gtk_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 | |
319 | static void |
320 | gtk_clock_init (GtkClock *self) |
321 | { |
322 | gtk_clock_start_ticking (self); |
323 | } |
324 | |
325 | static GtkClock * |
326 | gtk_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 | |
341 | static GListModel * |
342 | create_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 | |
386 | static char * |
387 | convert_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 | */ |
397 | static void |
398 | setup_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 | |
457 | static GtkWidget *window = NULL; |
458 | |
459 | GtkWidget * |
460 | do_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 | |