1 /* GObject - GLib Type, Object, Parameter and Signal Library
2 *
3 * Copyright (C) 2015-2022 Christian Hergert <christian@hergert.me>
4 * Copyright (C) 2015 Garrett Regier <garrettregier@gmail.com>
5 *
6 * SPDX-License-Identifier: LGPL-2.1-or-later
7 *
8 * This library is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Lesser General Public
10 * License as published by the Free Software Foundation; either
11 * version 2.1 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 * Lesser General Public License for more details.
17 *
18 * You should have received a copy of the GNU Lesser General
19 * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
20 *
21 * SPDX-License-Identifier: LGPL-2.1-or-later
22 */
23
24 #include <glib-object.h>
25
26 /* Copied from glib */
27 typedef struct _BindingSource
28 {
29 GObject parent_instance;
30
31 gint foo;
32 gint bar;
33 gdouble value;
34 gboolean toggle;
35 } BindingSource;
36
37 typedef struct _BindingSourceClass
38 {
39 GObjectClass parent_class;
40 } BindingSourceClass;
41
42 enum
43 {
44 PROP_SOURCE_FOO = 1,
45 PROP_SOURCE_BAR,
46 PROP_SOURCE_VALUE,
47 PROP_SOURCE_TOGGLE
48 };
49
50 static GType binding_source_get_type (void);
51 G_DEFINE_TYPE (BindingSource, binding_source, G_TYPE_OBJECT);
52
53 static void
54 binding_source_set_property (GObject *gobject,
55 guint prop_id,
56 const GValue *value,
57 GParamSpec *pspec)
58 {
59 BindingSource *source = (BindingSource *) gobject;
60
61 switch (prop_id)
62 {
63 case PROP_SOURCE_FOO:
64 source->foo = g_value_get_int (value);
65 break;
66
67 case PROP_SOURCE_BAR:
68 source->bar = g_value_get_int (value);
69 break;
70
71 case PROP_SOURCE_VALUE:
72 source->value = g_value_get_double (value);
73 break;
74
75 case PROP_SOURCE_TOGGLE:
76 source->toggle = g_value_get_boolean (value);
77 break;
78
79 default:
80 G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
81 }
82 }
83
84 static void
85 binding_source_get_property (GObject *gobject,
86 guint prop_id,
87 GValue *value,
88 GParamSpec *pspec)
89 {
90 BindingSource *source = (BindingSource *) gobject;
91
92 switch (prop_id)
93 {
94 case PROP_SOURCE_FOO:
95 g_value_set_int (value, source->foo);
96 break;
97
98 case PROP_SOURCE_BAR:
99 g_value_set_int (value, source->bar);
100 break;
101
102 case PROP_SOURCE_VALUE:
103 g_value_set_double (value, source->value);
104 break;
105
106 case PROP_SOURCE_TOGGLE:
107 g_value_set_boolean (value, source->toggle);
108 break;
109
110 default:
111 G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
112 }
113 }
114
115 static void
116 binding_source_class_init (BindingSourceClass *klass)
117 {
118 GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
119
120 gobject_class->set_property = binding_source_set_property;
121 gobject_class->get_property = binding_source_get_property;
122
123 g_object_class_install_property (gobject_class, PROP_SOURCE_FOO,
124 g_param_spec_int ("foo", "Foo", "Foo",
125 -1, 100,
126 0,
127 G_PARAM_READWRITE));
128 g_object_class_install_property (gobject_class, PROP_SOURCE_BAR,
129 g_param_spec_int ("bar", "Bar", "Bar",
130 -1, 100,
131 0,
132 G_PARAM_READWRITE));
133 g_object_class_install_property (gobject_class, PROP_SOURCE_VALUE,
134 g_param_spec_double ("value", "Value", "Value",
135 -100.0, 200.0,
136 0.0,
137 G_PARAM_READWRITE));
138 g_object_class_install_property (gobject_class, PROP_SOURCE_TOGGLE,
139 g_param_spec_boolean ("toggle", "Toggle", "Toggle",
140 FALSE,
141 G_PARAM_READWRITE));
142 }
143
144 static void
145 binding_source_init (BindingSource *self)
146 {
147 }
148
149 typedef struct _BindingTarget
150 {
151 GObject parent_instance;
152
153 gint bar;
154 gdouble value;
155 gboolean toggle;
156 } BindingTarget;
157
158 typedef struct _BindingTargetClass
159 {
160 GObjectClass parent_class;
161 } BindingTargetClass;
162
163 enum
164 {
165 PROP_TARGET_BAR = 1,
166 PROP_TARGET_VALUE,
167 PROP_TARGET_TOGGLE
168 };
169
170 static GType binding_target_get_type (void);
171 G_DEFINE_TYPE (BindingTarget, binding_target, G_TYPE_OBJECT);
172
173 static void
174 binding_target_set_property (GObject *gobject,
175 guint prop_id,
176 const GValue *value,
177 GParamSpec *pspec)
178 {
179 BindingTarget *target = (BindingTarget *) gobject;
180
181 switch (prop_id)
182 {
183 case PROP_TARGET_BAR:
184 target->bar = g_value_get_int (value);
185 break;
186
187 case PROP_TARGET_VALUE:
188 target->value = g_value_get_double (value);
189 break;
190
191 case PROP_TARGET_TOGGLE:
192 target->toggle = g_value_get_boolean (value);
193 break;
194
195 default:
196 G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
197 }
198 }
199
200 static void
201 binding_target_get_property (GObject *gobject,
202 guint prop_id,
203 GValue *value,
204 GParamSpec *pspec)
205 {
206 BindingTarget *target = (BindingTarget *) gobject;
207
208 switch (prop_id)
209 {
210 case PROP_TARGET_BAR:
211 g_value_set_int (value, target->bar);
212 break;
213
214 case PROP_TARGET_VALUE:
215 g_value_set_double (value, target->value);
216 break;
217
218 case PROP_TARGET_TOGGLE:
219 g_value_set_boolean (value, target->toggle);
220 break;
221
222 default:
223 G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
224 }
225 }
226
227 static void
228 binding_target_class_init (BindingTargetClass *klass)
229 {
230 GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
231
232 gobject_class->set_property = binding_target_set_property;
233 gobject_class->get_property = binding_target_get_property;
234
235 g_object_class_install_property (gobject_class, PROP_TARGET_BAR,
236 g_param_spec_int ("bar", "Bar", "Bar",
237 -1, 100,
238 0,
239 G_PARAM_READWRITE));
240 g_object_class_install_property (gobject_class, PROP_TARGET_VALUE,
241 g_param_spec_double ("value", "Value", "Value",
242 -100.0, 200.0,
243 0.0,
244 G_PARAM_READWRITE));
245 g_object_class_install_property (gobject_class, PROP_TARGET_TOGGLE,
246 g_param_spec_boolean ("toggle", "Toggle", "Toggle",
247 FALSE,
248 G_PARAM_READWRITE));
249 }
250
251 static void
252 binding_target_init (BindingTarget *self)
253 {
254 }
255
256 static gboolean
257 celsius_to_fahrenheit (GBinding *binding,
258 const GValue *from_value,
259 GValue *to_value,
260 gpointer user_data G_GNUC_UNUSED)
261 {
262 gdouble celsius, fahrenheit;
263
264 g_assert_true (G_VALUE_HOLDS (from_value, G_TYPE_DOUBLE));
265 g_assert_true (G_VALUE_HOLDS (to_value, G_TYPE_DOUBLE));
266
267 celsius = g_value_get_double (from_value);
268 fahrenheit = (9 * celsius / 5) + 32.0;
269
270 if (g_test_verbose ())
271 g_printerr ("Converting %.2fC to %.2fF\n", celsius, fahrenheit);
272
273 g_value_set_double (to_value, fahrenheit);
274
275 return TRUE;
276 }
277
278 static gboolean
279 fahrenheit_to_celsius (GBinding *binding,
280 const GValue *from_value,
281 GValue *to_value,
282 gpointer user_data G_GNUC_UNUSED)
283 {
284 gdouble celsius, fahrenheit;
285
286 g_assert_true (G_VALUE_HOLDS (from_value, G_TYPE_DOUBLE));
287 g_assert_true (G_VALUE_HOLDS (to_value, G_TYPE_DOUBLE));
288
289 fahrenheit = g_value_get_double (from_value);
290 celsius = 5 * (fahrenheit - 32.0) / 9;
291
292 if (g_test_verbose ())
293 g_printerr ("Converting %.2fF to %.2fC\n", fahrenheit, celsius);
294
295 g_value_set_double (to_value, celsius);
296
297 return TRUE;
298 }
299
300 static void
301 test_binding_group_invalid (void)
302 {
303 GBindingGroup *group = g_binding_group_new ();
304 BindingSource *source = g_object_new (binding_source_get_type (), NULL);
305 BindingTarget *target = g_object_new (binding_target_get_type (), NULL);
306
307 /* Invalid Target Property */
308 g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL,
309 "*find_property*target_property*!=*NULL*");
310 g_binding_group_bind (group, "value",
311 target, "does-not-exist",
312 G_BINDING_DEFAULT);
313 g_test_assert_expected_messages ();
314
315 g_binding_group_set_source (group, NULL);
316
317 /* Invalid Source Property */
318 g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL,
319 "*find_property*source_property*!=*NULL*");
320 g_binding_group_set_source (group, source);
321 g_binding_group_bind (group, "does-not-exist",
322 target, "value",
323 G_BINDING_DEFAULT);
324 g_test_assert_expected_messages ();
325
326 g_binding_group_set_source (group, NULL);
327
328 /* Invalid Source */
329 g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL,
330 "*find_property*->source_property*!=*NULL*");
331 g_binding_group_bind (group, "does-not-exist",
332 target, "value",
333 G_BINDING_DEFAULT);
334 g_binding_group_set_source (group, source);
335 g_test_assert_expected_messages ();
336
337 g_object_unref (target);
338 g_object_unref (source);
339 g_object_unref (group);
340 }
341
342 static void
343 test_binding_group_default (void)
344 {
345 gsize i, j;
346 GBindingGroup *group = g_binding_group_new ();
347 BindingSource *source = g_object_new (binding_source_get_type (), NULL);
348 BindingTarget *targets[5];
349 BindingSource *readback;
350
351 for (i = 0; i < G_N_ELEMENTS (targets); ++i)
352 {
353 targets[i] = g_object_new (binding_target_get_type (), NULL);
354 g_binding_group_bind (group, "foo",
355 targets[i], "bar",
356 G_BINDING_DEFAULT);
357 }
358
359 g_assert_null (g_binding_group_dup_source (group));
360 g_binding_group_set_source (group, source);
361 readback = g_binding_group_dup_source (group);
362 g_assert_true (readback == source);
363 g_object_unref (readback);
364
365 for (i = 0; i < 2; ++i)
366 {
367 g_object_set (source, "foo", 42, NULL);
368 for (j = 0; j < G_N_ELEMENTS (targets); ++j)
369 g_assert_cmpint (source->foo, ==, targets[j]->bar);
370
371 g_object_set (targets[0], "bar", 47, NULL);
372 g_assert_cmpint (source->foo, !=, targets[0]->bar);
373
374 /* Check that we transition the source correctly */
375 g_binding_group_set_source (group, NULL);
376 g_assert_null (g_binding_group_dup_source (group));
377 g_binding_group_set_source (group, source);
378 readback = g_binding_group_dup_source (group);
379 g_assert_true (readback == source);
380 g_object_unref (readback);
381 }
382
383 g_object_unref (group);
384
385 g_object_set (source, "foo", 0, NULL);
386 for (i = 0; i < G_N_ELEMENTS (targets); ++i)
387 g_assert_cmpint (source->foo, !=, targets[i]->bar);
388
389 g_object_unref (source);
390 for (i = 0; i < G_N_ELEMENTS (targets); ++i)
391 g_object_unref (targets[i]);
392 }
393
394 static void
395 test_binding_group_bidirectional (void)
396 {
397 gsize i, j;
398 GBindingGroup *group = g_binding_group_new ();
399 BindingSource *source = g_object_new (binding_source_get_type (), NULL);
400 BindingTarget *targets[5];
401 BindingSource *readback;
402
403 for (i = 0; i < G_N_ELEMENTS (targets); ++i)
404 {
405 targets[i] = g_object_new (binding_target_get_type (), NULL);
406 g_binding_group_bind (group, "value",
407 targets[i], "value",
408 G_BINDING_BIDIRECTIONAL);
409 }
410
411 g_assert_null (g_binding_group_dup_source (group));
412 g_binding_group_set_source (group, source);
413 readback = g_binding_group_dup_source (group);
414 g_assert_true (readback == source);
415 g_object_unref (readback);
416
417 for (i = 0; i < 2; ++i)
418 {
419 g_object_set (source, "value", 42.0, NULL);
420 for (j = 0; j < G_N_ELEMENTS (targets); ++j)
421 g_assert_cmpfloat (source->value, ==, targets[j]->value);
422
423 g_object_set (targets[0], "value", 47.0, NULL);
424 g_assert_cmpfloat (source->value, ==, targets[0]->value);
425
426 /* Check that we transition the source correctly */
427 g_binding_group_set_source (group, NULL);
428 g_assert_null (g_binding_group_dup_source (group));
429 g_binding_group_set_source (group, source);
430 readback = g_binding_group_dup_source (group);
431 g_assert_true (readback == source);
432 g_object_unref (readback);
433 }
434
435 g_object_unref (group);
436
437 g_object_set (targets[0], "value", 0.0, NULL);
438 g_assert_cmpfloat (source->value, !=, targets[0]->value);
439
440 g_object_unref (source);
441 for (i = 0; i < G_N_ELEMENTS (targets); ++i)
442 g_object_unref (targets[i]);
443 }
444
445 static void
446 transform_destroy_notify (gpointer data)
447 {
448 gboolean *transform_destroy_called = data;
449
450 *transform_destroy_called = TRUE;
451 }
452
453 static void
454 test_binding_group_transform (void)
455 {
456 gboolean transform_destroy_called = FALSE;
457 GBindingGroup *group = g_binding_group_new ();
458 BindingSource *source = g_object_new (binding_source_get_type (), NULL);
459 BindingTarget *target = g_object_new (binding_target_get_type (), NULL);
460
461 g_binding_group_set_source (group, source);
462 g_binding_group_bind_full (group, "value",
463 target, "value",
464 G_BINDING_BIDIRECTIONAL,
465 celsius_to_fahrenheit,
466 fahrenheit_to_celsius,
467 &transform_destroy_called,
468 transform_destroy_notify);
469
470 g_object_set (source, "value", 24.0, NULL);
471 g_assert_cmpfloat (target->value, ==, ((9 * 24.0 / 5) + 32.0));
472
473 g_object_set (target, "value", 69.0, NULL);
474 g_assert_cmpfloat (source->value, ==, (5 * (69.0 - 32.0) / 9));
475
476 /* The GDestroyNotify should only be called when the
477 * set is freed, not when the various GBindings are freed
478 */
479 g_binding_group_set_source (group, NULL);
480 g_assert_false (transform_destroy_called);
481
482 g_object_unref (group);
483 g_assert_true (transform_destroy_called);
484
485 g_object_unref (source);
486 g_object_unref (target);
487 }
488
489 static void
490 test_binding_group_transform_closures (void)
491 {
492 gboolean transform_destroy_called_1 = FALSE;
493 gboolean transform_destroy_called_2 = FALSE;
494 GBindingGroup *group = g_binding_group_new ();
495 BindingSource *source = g_object_new (binding_source_get_type (), NULL);
496 BindingTarget *target = g_object_new (binding_target_get_type (), NULL);
497 GClosure *c2f_closure, *f2c_closure;
498
499 c2f_closure = g_cclosure_new (G_CALLBACK (celsius_to_fahrenheit),
500 &transform_destroy_called_1,
501 (GClosureNotify) transform_destroy_notify);
502 f2c_closure = g_cclosure_new (G_CALLBACK (fahrenheit_to_celsius),
503 &transform_destroy_called_2,
504 (GClosureNotify) transform_destroy_notify);
505
506 g_binding_group_set_source (group, source);
507 g_binding_group_bind_with_closures (group, "value",
508 target, "value",
509 G_BINDING_BIDIRECTIONAL,
510 c2f_closure,
511 f2c_closure);
512
513 g_object_set (source, "value", 24.0, NULL);
514 g_assert_cmpfloat (target->value, ==, ((9 * 24.0 / 5) + 32.0));
515
516 g_object_set (target, "value", 69.0, NULL);
517 g_assert_cmpfloat (source->value, ==, (5 * (69.0 - 32.0) / 9));
518
519 /* The GClsoureNotify should only be called when the
520 * set is freed, not when the various GBindings are freed
521 */
522 g_binding_group_set_source (group, NULL);
523 g_assert_false (transform_destroy_called_1);
524 g_assert_false (transform_destroy_called_2);
525
526 g_object_unref (group);
527 g_assert_true (transform_destroy_called_1);
528 g_assert_true (transform_destroy_called_2);
529
530 g_object_unref (source);
531 g_object_unref (target);
532 }
533
534 static void
535 test_binding_group_same_object (void)
536 {
537 gsize i;
538 GBindingGroup *group = g_binding_group_new ();
539 BindingSource *source = g_object_new (binding_source_get_type (),
540 "foo", 100,
541 "bar", 50,
542 NULL);
543
544 g_binding_group_set_source (group, source);
545 g_binding_group_bind (group, "foo",
546 source, "bar",
547 G_BINDING_BIDIRECTIONAL);
548
549 for (i = 0; i < 2; ++i)
550 {
551 g_object_set (source, "foo", 10, NULL);
552 g_assert_cmpint (source->foo, ==, 10);
553 g_assert_cmpint (source->bar, ==, 10);
554
555 g_object_set (source, "bar", 30, NULL);
556 g_assert_cmpint (source->foo, ==, 30);
557 g_assert_cmpint (source->bar, ==, 30);
558
559 /* Check that it is possible both when initially
560 * adding the binding and when changing the source
561 */
562 g_binding_group_set_source (group, NULL);
563 g_binding_group_set_source (group, source);
564 }
565
566 g_object_unref (source);
567 g_object_unref (group);
568 }
569
570 static void
571 test_binding_group_weak_ref_source (void)
572 {
573 GBindingGroup *group = g_binding_group_new ();
574 BindingSource *source = g_object_new (binding_source_get_type (), NULL);
575 BindingTarget *target = g_object_new (binding_target_get_type (), NULL);
576 BindingSource *readback;
577
578 g_binding_group_set_source (group, source);
579 g_binding_group_bind (group, "value",
580 target, "value",
581 G_BINDING_BIDIRECTIONAL);
582
583 g_object_add_weak_pointer (G_OBJECT (source), (gpointer)&source);
584 readback = g_binding_group_dup_source (group);
585 g_assert_true (readback == source);
586 g_object_unref (readback);
587 g_object_unref (source);
588 g_assert_null (source);
589 g_assert_null (g_binding_group_dup_source (group));
590
591 /* Hopefully this would explode if the binding was still alive */
592 g_object_set (target, "value", 0.0, NULL);
593
594 g_object_unref (target);
595 g_object_unref (group);
596 }
597
598 static void
599 test_binding_group_weak_ref_target (void)
600 {
601 GBindingGroup *group = g_binding_group_new ();
602 BindingSource *source = g_object_new (binding_source_get_type (), NULL);
603 BindingTarget *target = g_object_new (binding_target_get_type (), NULL);
604
605 g_binding_group_set_source (group, source);
606 g_binding_group_bind (group, "value",
607 target, "value",
608 G_BINDING_BIDIRECTIONAL);
609
610 g_object_set (source, "value", 47.0, NULL);
611 g_assert_cmpfloat (target->value, ==, 47.0);
612
613 g_object_add_weak_pointer (G_OBJECT (target), (gpointer)&target);
614 g_object_unref (target);
615 g_assert_null (target);
616
617 /* Hopefully this would explode if the binding was still alive */
618 g_object_set (source, "value", 0.0, NULL);
619
620 g_object_unref (source);
621 g_object_unref (group);
622 }
623
624 static void
625 test_binding_group_properties (void)
626 {
627 GBindingGroup *group = g_binding_group_new ();
628 BindingSource *source = g_object_new (binding_source_get_type (), NULL);
629 BindingTarget *target = g_object_new (binding_target_get_type (), NULL);
630 BindingSource *other;
631
632 g_binding_group_set_source (group, source);
633 g_binding_group_bind (group, "value",
634 target, "value",
635 G_BINDING_BIDIRECTIONAL);
636
637 g_object_get (group, "source", &other, NULL);
638 g_assert_true (other == source);
639 g_object_unref (other);
640
641 g_object_set (group, "source", NULL, NULL);
642 g_object_get (group, "source", &other, NULL);
643 g_assert_null (other);
644
645 g_object_add_weak_pointer (G_OBJECT (target), (gpointer)&target);
646 g_object_unref (target);
647 g_assert_null (target);
648
649 g_object_unref (source);
650 g_object_unref (group);
651 }
652
653 static void
654 test_binding_group_weak_notify_no_bindings (void)
655 {
656 GBindingGroup *group = g_binding_group_new ();
657 BindingSource *source = g_object_new (binding_source_get_type (), NULL);
658
659 g_binding_group_set_source (group, source);
660 g_assert_finalize_object (source);
661 g_assert_finalize_object (group);
662 }
663
664 static void
665 test_binding_group_empty_closures (void)
666 {
667 GBindingGroup *group = g_binding_group_new ();
668 BindingSource *source = g_object_new (binding_source_get_type (), NULL);
669 BindingTarget *target = g_object_new (binding_target_get_type (), NULL);
670
671 g_binding_group_bind_full (group, "value", target, "value", 0,
672 NULL, NULL, NULL, NULL);
673
674 g_assert_finalize_object (group);
675 g_assert_finalize_object (target);
676 g_assert_finalize_object (source);
677 }
678
679 gint
680 main (gint argc,
681 gchar *argv[])
682 {
683 g_test_init (&argc, &argv, NULL);
684 g_test_add_func ("/GObject/BindingGroup/invalid", test_binding_group_invalid);
685 g_test_add_func ("/GObject/BindingGroup/default", test_binding_group_default);
686 g_test_add_func ("/GObject/BindingGroup/bidirectional", test_binding_group_bidirectional);
687 g_test_add_func ("/GObject/BindingGroup/transform", test_binding_group_transform);
688 g_test_add_func ("/GObject/BindingGroup/transform-closures", test_binding_group_transform_closures);
689 g_test_add_func ("/GObject/BindingGroup/same-object", test_binding_group_same_object);
690 g_test_add_func ("/GObject/BindingGroup/weak-ref-source", test_binding_group_weak_ref_source);
691 g_test_add_func ("/GObject/BindingGroup/weak-ref-target", test_binding_group_weak_ref_target);
692 g_test_add_func ("/GObject/BindingGroup/properties", test_binding_group_properties);
693 g_test_add_func ("/GObject/BindingGroup/weak-notify-no-bindings", test_binding_group_weak_notify_no_bindings);
694 g_test_add_func ("/GObject/BindingGroup/empty-closures", test_binding_group_empty_closures);
695 return g_test_run ();
696 }