File Coverage

blib/lib/Gtk2/Ex/Clock.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             # Copyright 2007, 2008, 2009, 2010, 2011 Kevin Ryde
2              
3             # This file is part of Gtk2-Ex-Clock.
4             #
5             # Gtk2-Ex-Clock is free software; you can redistribute it and/or modify it
6             # under the terms of the GNU General Public License as published by the Free
7             # Software Foundation; either version 3, or (at your option) any later
8             # version.
9             #
10             # Gtk2-Ex-Clock is distributed in the hope that it will be useful, but
11             # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12             # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13             # for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with Gtk2-Ex-Clock. If not, see .
17              
18             package Gtk2::Ex::Clock;
19 4     4   403836 use 5.008;
  4         89  
  4         165  
20 4     4   30 use strict;
  4         7  
  4         158  
21 4     4   30 use warnings;
  4         7  
  4         155  
22 4     4   4355 use Gtk2 1.200; # version 1.200 for GDK_PRIORITY_REDRAW
  0            
  0            
23             use POSIX ();
24             use POSIX::Wide 2; # version 2 for netbsd 646 alias
25             use List::Util qw(min);
26             use Scalar::Util;
27             use Time::HiRes;
28             use Glib::Ex::SourceIds;
29              
30             # uncomment this to run the ### lines
31             #use Smart::Comments;
32              
33             our $VERSION = 15;
34              
35             use constant _DEFAULT_FORMAT => '%H:%M';
36              
37             use Glib::Object::Subclass
38             'Gtk2::Label',
39             properties => [Glib::ParamSpec->string
40             ('format',
41             'Format string',
42             'An strftime() format string to display the time.',
43             _DEFAULT_FORMAT,
44             Glib::G_PARAM_READWRITE),
45              
46             Glib::ParamSpec->scalar
47             ('timezone',
48             'Timezone',
49             'The timezone to use in the display, either a string for the TZ environment variable, or a DateTime::TimeZone object. An empty string or undef means the local timezone.',
50             Glib::G_PARAM_READWRITE),
51              
52             Glib::ParamSpec->string
53             ('timezone-string',
54             'Timezone string',
55             'The timezone to use in the display, as a string for the TZ environment variable. An empty string or undef means the local timezone.',
56             (eval {Glib->VERSION(1.240);1}
57             ? undef # default
58             : ''), # no undef/NULL before Perl-Glib 1.240
59             Glib::G_PARAM_READWRITE),
60              
61             Glib::ParamSpec->int
62             ('resolution',
63             'Resolution',
64             'The resolution of the clock, in seconds, or 0 to decide this from the format string.',
65             0, # min
66             3600, # max
67             0, # default
68             Glib::G_PARAM_READWRITE),
69             ];
70              
71              
72             # _TIMER_MARGIN_MILLISECONDS is an extra period in milliseconds to add to
73             # the timer period requested. It's designed to ensure the timer doesn't
74             # fire before the target time boundary of 1 second or 1 minute, in case
75             # g_timeout_add() or the select() within it ends up rounding down to a clock
76             # tick boundary.
77             #
78             # In the unlikely event there's no sysconf() value for CLK_TCK, or no
79             # sysconf() func at all, assume the traditional 100 ticks/second, ie. a
80             # resolution of 10 milliseconds, giving a 20 ms margin.
81             #
82             use constant _TIMER_MARGIN_MILLISECONDS => do {
83             my $clk_tck = -1; # default -1 like the error return from sysconf()
84             ## no critic (RequireCheckingReturnValueOfEval)
85             eval { $clk_tck = POSIX::sysconf (POSIX::_SC_CLK_TCK()); };
86             ### $clk_tck
87             if ($clk_tck <= 0) { $clk_tck = 100; } # default assume 100 Hz, 10ms tick
88             (2 * 1000.0 / $clk_tck)
89             };
90             ### _TIMER_MARGIN_MILLISECONDS: _TIMER_MARGIN_MILLISECONDS()
91              
92              
93             sub INIT_INSTANCE {
94             my ($self) = @_;
95             $self->{'format'} = _DEFAULT_FORMAT;
96             $self->{'resolution'} = 0; # per pspec default
97             $self->{'decided_resolution'} = 60; # of _DEFAULT_FORMAT
98             $self->set_use_markup (1);
99             _update($self); # initial string for initial size
100             }
101              
102             sub GET_PROPERTY {
103             my ($self, $pspec) = @_;
104             my $pname = $pspec->get_name;
105              
106             if ($pname eq 'timezone_string') {
107             my $timezone = $self->{'timezone'};
108             # For DateTime::TimeZone read back the ->name() string.
109             # Not yet documented. Is this a good idea?
110             if (Scalar::Util::blessed ($timezone)) {
111             $timezone = $timezone->name;
112             }
113             return $timezone;
114             }
115              
116             return $self->{$pname};
117             }
118              
119             sub SET_PROPERTY {
120             my ($self, $pspec, $newval) = @_;
121             ### Clock SET_PROPERTY: $pspec->get_name
122              
123             my $pname = $pspec->get_name;
124             if ($pname eq 'timezone_string') { # alias
125             $pname = 'timezone';
126             }
127             $self->{$pname} = $newval; # per default GET_PROPERTY
128              
129             if ($pname eq 'timezone') {
130             if (Scalar::Util::blessed($newval)
131             && $newval->isa('DateTime::TimeZone')) {
132             require DateTime;
133             } elsif (defined $newval && $newval ne '') {
134             require Tie::TZ;
135             }
136             }
137              
138             if ($pname eq 'resolution' || $pname eq 'format') {
139             $self->{'decided_resolution'}
140             = $self->get('resolution')
141             || ($self->strftime_is_seconds($self->{'format'}) ? 1 : 60);
142             ### decided resolution: $self->{'decided_resolution'}
143             }
144             if ($pname eq 'timezone' || $pname eq 'format') {
145             _update ($self);
146             }
147             }
148              
149             sub _timer_callback {
150             my ($ref_weak_self) = @_;
151             ### _timer_callback()
152             my $self = $$ref_weak_self || return 0; # Glib::SOURCE_REMOVE
153              
154             _update ($self);
155              
156             # this timer should be removed by SourceIds anyway
157             return 0; # Glib::SOURCE_REMOVE
158             }
159              
160             # set the label string and start or restart timer
161             sub _update {
162             my ($self) = @_;
163             ### Clock _update()
164              
165             my $tod = Time::HiRes::time();
166             my $format = $self->{'format'};
167             my $timezone = $self->{'timezone'};
168              
169             my ($str, $minute, $second);
170             if (Scalar::Util::blessed($timezone)
171             && $timezone->isa('DateTime::TimeZone')) {
172             my $t = DateTime->from_epoch (epoch => $tod, time_zone => $timezone);
173             $str = $t->strftime ($format);
174             $minute = $t->minute;
175             $second = $t->second;
176             } else {
177             my @tm;
178             if (defined $timezone && $timezone ne '') {
179             ### using TZ: $timezone
180             no warnings 'once';
181             local $Tie::TZ::TZ = $timezone;
182             @tm = localtime ($tod);
183             $str = POSIX::Wide::strftime ($format, @tm);
184             } else {
185             ### using current timezone
186             @tm = localtime ($tod);
187             $str = POSIX::Wide::strftime ($format, @tm);
188             }
189             $minute = $tm[1];
190             $second = $tm[0];
191             }
192             $self->set_label ($str);
193              
194             # Decide how long in milliseconds until the next update. This is from the
195             # current $minute,$second,frac($tod) to the next multiple of
196             # $self->{'decided_resolution'} seconds, plus _TIMER_MARGIN_MILLISECONDS
197             # described above.
198             #
199             # If $self->{'decided_resolution'} is 1 second then $minute,$second have
200             # no effect and it's just from the fractional part of $tod to the next 1
201             # second. Similarly if $self->{'decided_resolution'} is 60 seconds then
202             # $minute has no effect.
203             #
204             # Rumour has it $second can be 60 for some oddity like a TAI system clock
205             # displaying UTC. Dunno if it really happens, but cap at 59 just in case.
206             #
207             # In theory an mktime of $second+1, or $minute+1,$second=0, would be the
208             # $tod value to target. Not absolutely certain that would come out right
209             # if crossing a daylight savings boundary, though capping it modulo the
210             # resolution like ($newtod - $tod) % $self->{'decided_resolution'} would
211             # ensure a sensible range. Would an mktime be worthwhile? Taking just
212             # 60*$minute+$second is a little less work.
213             #
214             my $milliseconds = POSIX::ceil
215             (_TIMER_MARGIN_MILLISECONDS
216             + (1000
217             * ($self->{'decided_resolution'}
218             - ((60*$minute + min(59,$second)) % $self->{'decided_resolution'})
219             - ($tod - POSIX::floor($tod))))); # fraction part
220              
221             ### timer: "$tod is $minute,$second wait $milliseconds to give ".($tod + $milliseconds / 1000.0)
222             Scalar::Util::weaken (my $weak_self = $self);
223             $self->{'timer'} = Glib::Ex::SourceIds->new
224             (Glib::Timeout->add ($milliseconds,
225             \&_timer_callback, \$weak_self,
226             Gtk2::GDK_PRIORITY_REDRAW() - 1)); # before redraws
227              
228             }
229              
230              
231             #------------------------------------------------------------------------------
232              
233             # $format is an strftime() format string. Return true if it has 1 second
234             # resolution.
235             #
236             sub strftime_is_seconds {
237             my ($self, $format) = @_;
238              
239             # %c is ctime() style, includes seconds
240             # %r is "%I:%M:%S %p"
241             # %s is seconds since 1970 (a GNU extension)
242             # %S is seconds 0 to 59
243             # %T is "%H:%M:%S"
244             # %X is locale preferred time, probably "%H:%M:%S"
245             # modifiers standard E and O, plus GNU "-_0^"
246             #
247             # DateTime extras:
248             # %N is nanoseconds, which really can't work, so ignore
249             #
250             # DateTime methods:
251             # second()
252             # sec()
253             # hms(), time()
254             # datetime(), is8601()
255             # epoch()
256             # utc_rd_as_seconds()
257             #
258             # jd(), mjd() fractional part represents the time, but the decimals
259             # aren't a whole second so won't really display properly, ignore for now
260             #
261             $format =~ s/%%//g; # literal "%"s, so eg. "%%Something" is not "%S"
262             return ($format =~ /%[-_^0-9EO]*
263             ([crsSTX]
264             |\{(sec(ond)?|hms|(date)?time|iso8601|epoch|utc_rd_as_seconds)})/x);
265             }
266              
267             1;
268             __END__