File Coverage

blib/lib/Glib/Ex/ConnectProperties.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             # Copyright 2007, 2008, 2009, 2010, 2011, 2012 Kevin Ryde
2              
3             # This file is part of Glib-Ex-ConnectProperties.
4             #
5             # Glib-Ex-ConnectProperties is free software; you can redistribute it and/or
6             # modify it under the terms of the GNU General Public License as published
7             # by the Free Software Foundation; either version 3, or (at your option) any
8             # later version.
9             #
10             # Glib-Ex-ConnectProperties is distributed in the hope that it will be
11             # useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
12             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
13             # Public License for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with Glib-Ex-ConnectProperties. If not, see .
17              
18              
19             # maybe:
20             # multiplier factor [$obj,'prop',mul=>2]
21             # get_method => name or subr
22             # set_method => name or subr
23             # paramspec => $pspec
24              
25             package Glib::Ex::ConnectProperties;
26 10     10   12414 use 5.008;
  10         35  
  10         457  
27 10     10   55 use strict;
  10         15  
  10         322  
28 10     10   50 use warnings;
  10         22  
  10         287  
29 10     10   52 use Carp;
  10         16  
  10         1010  
30 10     10   35340 use Glib;
  0            
  0            
31             use Scalar::Util;
32             use Module::Load;
33             use Glib::Ex::SignalIds 5; # version 5 for add()
34              
35             our $VERSION = 19;
36              
37             # uncomment this to run the ### lines
38             #use Smart::Comments;
39              
40              
41             # Hard/weak refs are as follows.
42             #
43             # * Readable property in new() permanent linkage -- the $object signal
44             # connection has a hard ref to $elem, and $elem->{'self'} has a hard ref
45             # to $self, so $elem is kept alive while $object lives. The entry for
46             # $elem within connp $self->{'array'} is weak so that $elem goes away when
47             # $object is destroyed.
48             #
49             # * Readable property in dynamic() linkage -- $elem->{'self'} is weak, which
50             # means $self can be garbage collected. Each $elem is still kept alive by
51             # the signal connection, but $self->DESTROY drops those connections.
52             #
53             # * Write-only property -- there's no signal connection, and $self has a
54             # hard ref to $elem, with nothing from $elem back to $self. The
55             # write-onlys don't keep $self alive, only the readables. Once the last
56             # readable object is destroyed the $self and write-onlys are destroyed.
57             #
58             # In all cases $elem->{'object'} is only a weak ref to the target $object so
59             # a ConnectProperties never keeps a target object alive.
60             #
61             # When $self->{'array'} gets down to just one element (one readable one)
62             # it'd be possible to discard it as there's nowhere for its "notify" to
63             # propagate values to. But maybe an add() could be made to extend an
64             # existing linkage, in which case would still want that last element. Maybe
65             # could go dynamic() style when down to one element, so if nothing else
66             # cares about the linkage then destroy the lot.
67             #
68              
69             sub new {
70             my ($class, @array) = @_;
71             ### ConnectProperties new()
72              
73             if (@array < 2) {
74             croak 'ConnectProperties: new() must have two or more object/property pairs';
75             }
76              
77             # validate property names before making signal connections
78             foreach my $elem (@array) {
79             my ($object, $pname, @params) = @$elem;
80              
81             # for reference ParamSpec demands pname first char [A-Za-z] and then any
82             # non [A-Za-z0-9-] crunched by canonical_key() to "-"s
83             my $flavour;
84             if ($pname =~ /(.*?)#(.*)/) {
85             $pname = $2;
86             ($flavour = $1) =~ tr/-/_/;
87             } else {
88             $flavour = 'object';
89             }
90             my $elem_class = "Glib::Ex::ConnectProperties::Element::$flavour";
91             ### $elem_class
92             Module::Load::load ($elem_class);
93              
94             # replacing element in @array
95             $elem = $elem_class->new (object => $object,
96             pname => $pname,
97             @params);
98             $elem->check_property;
99             }
100             my $self = bless { array => \@array }, $class;
101             my $first_readable_elem;
102              
103             foreach my $elem (@array) {
104             if (my $h = delete $elem->{'hash_in'}) {
105             ### hash_in func: "@{[keys %$h]}"
106             $elem->{'func_in'} = _make_hash_func ($h);
107             }
108             if (my $h = delete $elem->{'hash_out'}) {
109             ### hash_out func: "@{[keys %$h]}"
110             $elem->{'func_out'} = _make_hash_func ($h);
111             }
112              
113             if (delete $elem->{'bool_not'}) {
114             $elem->{'func_in'} = $elem->{'func_out'} = \&_bool_not;
115             }
116              
117             Scalar::Util::weaken ($elem->{'object'});
118              
119             if (! delete $elem->{'write_only'} && $elem->is_readable) {
120             $first_readable_elem ||= $elem;
121             $elem->{'self'} = $self;
122             $elem->connect_signals;
123             Scalar::Util::weaken ($elem); # the element of $self->{'array'}
124             }
125             }
126              
127             # set initially from first readable, in case not already the same
128             if ($first_readable_elem) {
129             ### initial propagate
130             _do_read_handler ($first_readable_elem->{'object'}, $first_readable_elem);
131             }
132             return $self;
133             }
134              
135             sub dynamic {
136             my $self = shift->new(@_);
137             foreach my $elem (@{$self->{'array'}}) {
138             Scalar::Util::weaken ($elem->{'self'});
139             }
140             return $self;
141             }
142              
143             # For a permanent new() style connection DESTROY is only reached when all
144             # readable objects are gone already, so there's nothing to disconnect. But
145             # a dynamic() is garbage collected with signal connections still present,
146             # hence an explicit disconnect() here.
147             #
148             sub DESTROY {
149             my ($self) = @_;
150             $self->disconnect;
151             }
152              
153             sub disconnect {
154             my ($self) = @_;
155             my $array = $self->{'array'};
156             ### ConnectProperties disconnect: "$self ".scalar(@$array)." elems"
157             while (my $elem = pop @$array) {
158             $elem->disconnect;
159             }
160             }
161              
162             my $value_validate_method
163             = (
164             # Perl-Glib 1.200, value_validate() not wrapped
165             ! Glib::ParamSpec->can('value_validate')
166             ? sub {
167             my ($pspec, $value) = @_;
168             return (0,$value); # unmodified, original value, always wantarray
169             }
170              
171             # Perl-Glib 1.220, value_validate() buggy on non ref counted boxed types
172             : ! eval{Glib->VERSION(1.240);1}
173             ? sub {
174             my ($pspec, $value) = @_;
175             my $type = $pspec->get_value_type;
176             if ($type->isa('Glib::Boxed') && ! $type->isa('Glib::Scalar')) {
177             return (0,$value); # unmodified, original value, always wantarray
178             }
179             return $pspec->value_validate ($value);
180             }
181              
182             # Perl-Glib 1.240, value_validate() good
183             : 'value_validate');
184              
185             # 'notify' or read_signal handler from a connected object
186             sub _do_read_handler {
187             my $from_elem = $_[-1];
188             my $self = $from_elem->{'self'};
189              
190             ### ConnectProperties _do_read_handler: "$self $_[0]/" . ($from_elem->{'pname'} || '[false]')
191             ### notify_in_progress: $self->{'notify_in_progress'}
192              
193             if ($self->{'notify_in_progress'}) { return; }
194             local $self->{'notify_in_progress'} = 1;
195              
196             my $from_val = $from_elem->get_value;
197             ### from_value to propagate: $from_val
198             if (my $func = $from_elem->{'func_out'}) {
199             $from_val = $func->($from_val);
200             ### func_out becomes: $from_val
201             }
202              
203             my $array = $self->{'array'};
204             for (my $i = 0; $i < @$array; $i++) {
205             my ($to_elem, $to_object);
206              
207             unless (($to_elem = $array->[$i])
208             && ($to_object = $to_elem->{'object'})) {
209             ### elem gone, dropping: $i
210             splice @$array, $i--, 1;
211             next;
212             }
213             if ($to_elem == $from_elem # not ourselves
214             || $to_elem->{'read_only'}) { # forced not write
215             next;
216             }
217              
218             my $to_pspec = $to_elem->find_property
219             || do {
220             ### no to_pspec (such as no container child property yet, etc)
221             next;
222             };
223             my $to_flags = $to_pspec->get_flags;
224              
225             # skip non-writable targets
226             ($to_flags & 'writable') || next;
227              
228             my $to_val = $from_val;
229             if (my $func = $to_elem->{'func_in'}) {
230             $to_val = $func->($to_val);
231             ### func_in becomes: $to_val
232             }
233              
234             # value_validate() to clamp $to_val for $to_pspec
235             # value_validate() is wrapped in Glib 1.220, remove the check when ready
236             # to demand that version
237             # In 1.240 may have to keep a new non ref counted boxed return from
238             # func_in() alive if value_validate() makes an alias, hence
239             # $to_val_keepalive.
240             #
241             my $to_val_keepalive = $to_val;
242             (undef, $to_val) = $to_pspec->$value_validate_method($to_val);
243              
244             # skip if target already contains $to_val, to avoid extra 'notify's
245             if ($to_flags & 'readable') {
246             if (_pspec_equal ($to_pspec, $to_elem->get_value, $to_val)) {
247             ### suppress already equal: "$to_object/".($to_elem->{'pname'} || '[false]')
248             next;
249             }
250             }
251              
252             ### store to: "$to_object/". ($to_elem->{'pname'} || '[false]')
253             $to_elem->set_value ($to_val);
254             }
255              
256             return $from_elem->{'read_signal_return'};
257             }
258              
259             sub _pspec_equal {
260             my ($pspec, $x, $y) = @_;
261              
262             # Glib::Param::Boxed values_cmp() is only by pointer value, so try to do
263             # better by looking for an equal() or compare() method on the value type.
264             # This is only for the exact pspec 'Glib::Param::Boxed'. If you make a
265             # subclass for a flavour of boxed object you should implement a values_cmp
266             # for everyone to use.
267             #
268             if (ref $pspec eq 'Glib::Param::Boxed') {
269             my $value_type = $pspec->get_value_type; # string class name
270              
271             if (my $func = $value_type->can('Glib_Ex_ConnectProperties_equal')) {
272             return $func->($x, $y);
273             }
274              
275             # Gtk2::Gdk::Region and Gtk2::Gdk::Color have 'equal' (and GdkFont would
276             # too but it's not wrapped as of Gtk2 1.221). Gtk2::TreePath has a
277             # 'compare' method. Those methods don't much like undef (NULL), and
278             # presume that other similar methods won't either, so guard against
279             # that.
280             #
281             if (my $func = $value_type->can('equal')) {
282             if (! defined $x || ! defined $y) {
283             return ((defined $x) == (defined $y)); # undef==undef, else not equal
284             }
285             return $func->($x, $y);
286             }
287             if (my $func = $value_type->can('compare')) {
288             if (! defined $x || ! defined $y) {
289             return ((defined $x) == (defined $y)); # undef==undef, else not equal
290             }
291             return ($func->($x, $y) == 0);
292             }
293             }
294              
295             # values_cmp() wrapped in Glib 1.220, will remove the fallback when ready
296             # to demand that version
297             my $func = ($pspec->can('values_cmp')
298             || $pspec->can('Glib_Ex_ConnectProperties_values_cmp')
299             || croak 'ConnectProperties: oops, where\'s the values_cmp fallback?');
300             return ($func->($pspec, $x, $y) == 0);
301             }
302              
303             sub _make_hash_func {
304             my ($h) = @_;
305             ### _make_hash_func()
306             ### $h
307             if (defined(tied($h))) {
308             return sub { $h->{$_[0]} };
309             } else {
310             return sub { defined $_[0] ? $h->{$_[0]} : undef };
311             }
312             }
313             sub _bool_not {
314             return ! $_[0];
315             }
316              
317             #------------------------------------------------------------------------------
318             # equality refinements for Glib::Param::Boxed
319             #
320             # This is just a Glib_Ex_ConnectProperties_equal() func added into the
321             # package of the applicable type. Not a documented feature yet. Might
322             # prefer paramspec subclasses offering a suitable values_cmp() which
323             # everyone could use, rather than special stuff here.
324              
325             # Glib::ParamSpec->scalar just makes a Glib::Param::Boxed so values_cmp is
326             # by the SV address, which will be almost always different. Try instead a
327             # compare by 'eq'. It won't look into arrays etc, but you probably should
328             # setup a new ParamSpec type to make that happen properly.
329             #
330             sub Glib::Scalar::Glib_Ex_ConnectProperties_equal {
331             my ($x, $y) = @_;
332             if (! defined $x || ! defined $y) { return ((defined $x) == (defined $y)) }
333             return ($x eq $y);
334             }
335              
336             # Glib::Strv at the perl level as arrayref of strings, or undef.
337             # In Gtk2::AboutDialog it's just a Glib::Param::Boxed, compare by value.
338             #
339             # undef is not equal to an empty array, the same as GParamSpecValueArray has
340             # NULL not equal to a zero length array in param_value_array_values_cmp().
341             # There's probably no difference in actual use though ...
342             #
343             sub Glib::Strv::Glib_Ex_ConnectProperties_equal {
344             my ($x, $y) = @_;
345             if (! defined $x || ! defined $y) { return ((defined $x) == (defined $y)); }
346             if (@$x != @$y) { return 0; }
347             foreach my $i (0 .. $#$x) {
348             if ($x->[$i] ne $y->[$i]) { return 0; }
349             }
350             return 1;
351             }
352              
353             # Gtk2::Gdk::Cursor, by type, with possibly undef
354             #
355             sub Gtk2::Gdk::Cursor::Glib_Ex_ConnectProperties_equal {
356             my ($x, $y) = @_;
357             if (! defined $x || ! defined $y) { return ((defined $x) == (defined $y)); }
358              
359             my $xtype = $x->type;
360             if ($xtype eq 'cursor-is-pixmap') {
361             return $x == $y; # can't look into pixmap contents
362             } else {
363             return $xtype eq $y->type; # standard cursors by type
364             }
365             }
366              
367             # Gtk2::Border at the perl level is a hashref of fields, or undef.
368             # In Gtk2::Entry it's just a Glib::Param::Boxed, compare here by values.
369             # Apart from Gtk2::Entry it's thankfully rare.
370             #
371             sub Gtk2::Border::Glib_Ex_ConnectProperties_equal {
372             my ($x, $y) = @_;
373             if (! defined $x || ! defined $y) { return ((defined $x) == (defined $y)); }
374              
375             return ($x->{'left'} == $y->{'left'}
376             && $x->{'right'} == $y->{'right'}
377             && $x->{'top'} == $y->{'top'}
378             && $x->{'bottom'} == $y->{'bottom'});
379             }
380              
381             #------------------------------------------------------------------------------
382             # values_cmp fallback
383              
384             BEGIN {
385             if (! Glib::ParamSpec->can('values_cmp')) {
386             no warnings 'once';
387              
388             # overall fallback: integers, characters by number; Glib::Object's by ref;
389             # Glib::Boxed by value (fairly useless most of the time)
390             *Glib::ParamSpec::Glib_Ex_ConnectProperties_values_cmp = sub {
391             my ($pspec, $x, $y) = @_;
392             if (! defined $x || ! defined $y) { return ((defined $x) <=> (defined $y))}
393             return ($x <=> $y);
394             };
395              
396             # string and enum by alphabetical
397             # no Glib::Param::GType since values_cmp() exists whenever that one will ...
398             *Glib::Param::String::Glib_Ex_ConnectProperties_values_cmp
399             = *Glib::Param::Enum::Glib_Ex_ConnectProperties_values_cmp
400             = sub {
401             my ($pspec, $x, $y) = @_;
402             if (! defined $x || ! defined $y) {
403             return ((defined $x) <=> (defined $y));
404             }
405             return ($x cmp $y);
406             };
407              
408             # bools allowing any 0, '', undef
409             *Glib::Param::Boolean::Glib_Ex_ConnectProperties_values_cmp = sub {
410             my ($pspec, $x, $y) = @_;
411             return ((! $x) <=> (! $y));
412             };
413              
414             # double following epsilon
415             *Glib::Param::Double::Glib_Ex_ConnectProperties_values_cmp = sub {
416             my ($pspec, $x, $y) = @_;
417             my $epsilon = $pspec->get_epsilon;
418             if ($x < $y) {
419             return -($y-$x > $epsilon);
420             } else {
421             return ($x-$y > $epsilon);
422             }
423             };
424              
425             # float truncated to single precision before comparing, and following epsilon
426             *Glib::Param::Float::Glib_Ex_ConnectProperties_values_cmp = sub {
427             my ($pspec, $x, $y) = @_;
428             ($x, $y) = unpack 'f2', pack ('f2', $x, $y);
429             my $epsilon = $pspec->get_epsilon;
430             if ($x < $y) {
431             return -($y-$x > $epsilon);
432             } else {
433             return ($x-$y > $epsilon);
434             }
435             };
436             }
437             }
438              
439             1;
440             __END__