File Coverage

blib/lib/App/Chart/Gtk2/IntradayImage.pm
Criterion Covered Total %
statement 12 14 85.7
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 17 19 89.4


line stmt bran cond sub pod time code
1             # Copyright 2007, 2008, 2009, 2010, 2011, 2012, 2014, 2016 Kevin Ryde
2              
3             # This file is part of Chart.
4             #
5             # Chart is free software; you can redistribute it and/or modify it under the
6             # terms of the GNU General Public License as published by the Free Software
7             # Foundation; either version 3, or (at your option) any later version.
8             #
9             # Chart is distributed in the hope that it will be useful, but WITHOUT ANY
10             # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11             # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12             # details.
13             #
14             # You should have received a copy of the GNU General Public License along
15             # with Chart. If not, see <http://www.gnu.org/licenses/>.
16              
17             package App::Chart::Gtk2::IntradayImage;
18 1     1   434 use 5.010;
  1         3  
19 1     1   5 use strict;
  1         1  
  1         17  
20 1     1   5 use warnings;
  1         1  
  1         27  
21 1     1   3 use Carp;
  1         2  
  1         50  
22 1     1   224 use Gtk2 1.220;
  0            
  0            
23             use Encode;
24             use Encode::Locale; # for coding system "locale"
25             use List::Util qw(min max);
26             use Locale::TextDomain ('App-Chart');
27              
28             use Gtk2::Ex::Units;
29             use Gtk2::Ex::PixbufBits;
30             use App::Chart::Database;
31             use App::Chart::Gtk2::GUI;
32              
33             # uncomment this to run the ### lines
34             #use Smart::Comments;
35              
36              
37             use Glib::Object::Subclass
38             'Gtk2::DrawingArea',
39             signals => { expose_event => \&_do_expose_event,
40             size_request => \&_do_size_request },
41             properties => [Glib::ParamSpec->string
42             ('symbol',
43             __('Symbol'),
44             'The symbol of the stock or commodity to be shown.',
45             '', # default
46             Glib::G_PARAM_READWRITE),
47              
48             Glib::ParamSpec->string
49             ('mode',
50             'Mode',
51             'The intraday mode, such as "1d" for one day. The possible values here depend on the symbol\'s data source code.',
52             '', # default
53             Glib::G_PARAM_READWRITE)];
54              
55             sub INIT_INSTANCE {
56             my ($self) = @_;
57             $self->{'symbol'} = '';
58             $self->{'mode'} = '';
59              
60             # single pixbuf draw operation doesn't need double buffering
61             $self->set_double_buffered (0);
62              
63             App::Chart::chart_dirbroadcast()->connect_for_object
64             ('intraday-changed', \&_do_intraday_changed, $self);
65             }
66              
67             sub SET_PROPERTY {
68             my ($self, $pspec, $newval) = @_;
69             my $pname = $pspec->get_name;
70             ### IntradayImage SET_PROPERTY(): "$pname $newval"
71              
72             my $oldval = $self->{$pname};
73             $self->{$pname} = $newval; # per default GET_PROPERTY
74            
75             ### stored to: ''.\$self->{$pname}
76              
77             if ($oldval eq $newval) {
78             return;
79             }
80              
81             if ($pname eq 'symbol' || $pname eq 'mode') {
82             # new image (or new no image)
83             delete $self->{'xor_background'}; # new colour scheme
84             $self->queue_resize;
85             $self->queue_draw;
86             }
87             }
88              
89             # 'size-request' class closure
90             sub _do_size_request {
91             my ($self, $req) = @_;
92             ### IntradayImage _do_size_request()
93             my $pixbuf = _load_pixbuf ($self);
94             if (ref $pixbuf) {
95             $req->width ($pixbuf->get_width);
96             $req->height ($pixbuf->get_height);
97             } else {
98             $req->width (35 * Gtk2::Ex::Units::em($self));
99             $req->height (6 * Gtk2::Ex::Units::line_height($self));
100             }
101             ### _do_size_request() decide: $req->width."x".$req->height
102             }
103              
104             # 'expose-event' class closure
105             sub _do_expose_event {
106             my ($self, $event) = @_;
107             ### IntradayImage _do_expose_event(): $self->get_name.' "'.($self->{'symbol'}||'[nosymbol]').'" "'.($self->{'mode'}||'[nomode]').'"'
108             my $win = $self->window;
109              
110             # Reading the database on every expose probably isn't fast, but we're not
111             # expecting to scroll or anything much, so leaving the data on disk might
112             # save a little memory.
113             #
114             my $pixbuf = _load_pixbuf ($self);
115             if (! ref $pixbuf) {
116             my $errmsg = $pixbuf;
117             ### pixbuf load error: $errmsg
118             $win->clear;
119             App::Chart::Gtk2::GUI::draw_text_centred ($self, undef, $errmsg);
120             return Gtk2::EVENT_PROPAGATE;
121             }
122              
123             my $pix_width = $pixbuf->get_width;
124             my $pix_height = $pixbuf->get_height;
125             ### pixbuf: "${pix_width}x${pix_height}"
126              
127             my ($x, $y, $alloc_width, $alloc_height) = $self->allocation->values;
128             ### alloc size: "${alloc_width}x${alloc_height} at $x,$y"
129              
130             # windowed
131             $x = 0; $y = 0;
132              
133             # align in allocated space, if alloc bigger than pixbuf
134             my $x_offset = max(0, ($alloc_width - $pix_width) / 2);
135             my $y_offset = max(0, ($alloc_height - $pix_height) / 2);
136              
137             # restrict to alloc width/height, in case pixbuf+pad is bigger than alloc
138             my $width = min ($alloc_width - $x_offset, $pix_width);
139             my $height = min ($alloc_height - $y_offset, $pix_height);
140              
141             my $gc = $self->get_style->bg_gc($self->state);
142             $gc->set_clip_region ($event->region);
143             $win->draw_pixbuf ($gc, $pixbuf,
144             0, 0, # source x,y
145             $x+$x_offset, $y+$y_offset, # dest x,y
146             $width, $height,
147             'normal', # dither
148             0, 0); # dither x,y
149              
150             my $region = $event->region->copy;
151             $region->subtract (Gtk2::Gdk::Region->rectangle
152             (Gtk2::Gdk::Rectangle->new
153             ($x+$x_offset, $y+$y_offset, $width, $height)));
154             $gc->set_clip_region ($region);
155             $win->draw_rectangle ($gc, 1, $event->area->values);
156              
157             $gc->set_clip_region (undef);
158             return Gtk2::EVENT_PROPAGATE;
159             }
160              
161             sub _load_pixbuf {
162             my ($self) = @_;
163             ### _load_pixbuf() ...
164              
165             my $symbol = $self->{'symbol'};
166             my $mode = $self->{'mode'};
167             if (! $symbol || ! $mode) { return __('(No data)'); }
168              
169             my $dbh = App::Chart::DBI->instance;
170             my $sth = $dbh->prepare_cached
171             ('SELECT image, error FROM intraday_image WHERE symbol=? AND mode=?');
172              
173             # Crib note: Some DBI 1.618 SQLite3 1.35 seems to hold a ref to the
174             # scalars passed to selectrow_array() until the next call. So use the
175             # local variables since holding onto $self->{'symbol'} looks like a leak.
176             #
177             my ($image, $error) = $dbh->selectrow_array ($sth, undef,
178             $symbol,
179             $mode);
180             $sth->finish();
181             if (! defined $image) { # error message in database
182             return $error || __('(No data)');
183             }
184              
185             my $loader = Gtk2::Gdk::PixbufLoader->new();
186             my $pixbuf;
187             if (eval {
188             $loader->write ($image);
189             $loader->close ();
190             $pixbuf = $loader->get_pixbuf;
191             1 }) {
192             return $pixbuf;
193             } else {
194             # Should be Glib::Error in $@ thrown by $loader, but allow for plain
195             # string too.
196             my $err = "$@";
197             unless (utf8::is_utf8($err)) { $err = Encode::decode('locale',$err); }
198             return $err;
199             }
200             }
201              
202             sub _do_intraday_changed {
203             my ($self, $symbol, $mode) = @_;
204             ### IntradayImage _do_intraday_changed(): "\"$symbol\" \"$mode\"\n"
205             if ($self->{'symbol'} eq $symbol && $self->{'mode'} eq $mode) {
206             # new image (or new no image)
207             delete $self->{'xor_background'}; # new colour scheme
208             $self->queue_resize;
209             $self->queue_draw;
210             }
211             }
212              
213             #-----------------------------------------------------------------------------
214             # pixbuf background
215              
216             sub Gtk2_Ex_Xor_background {
217             my ($self) = @_;
218             return ($self->{'xor_background'} ||= do {
219             require Gtk2::Ex::PixbufBits;
220             my $pixbuf = _load_pixbuf ($self);
221             if (! ref $pixbuf) {
222             # error loading, treat as widget background
223             return $self->get_style->bg($self->state);
224             }
225             my $rgbstr = Gtk2::Ex::PixbufBits::sampled_majority_color ($pixbuf);
226             ### IntradayImage xor background: $rgbstr
227             require App::Chart::Gtk2::Ex::GdkColorAlloc;
228             App::Chart::Gtk2::Ex::GdkColorAlloc->new (widget => $self,
229             color => $rgbstr);
230             });
231             }
232              
233             1;
234             __END__
235              
236             =for stopwords intraday PNG JPEG GIF
237              
238             =head1 NAME
239              
240             App::Chart::Gtk2::IntradayImage -- intraday image display widget
241              
242             =head1 SYNOPSIS
243              
244             my $image = App::Chart::Gtk2::IntradayImage->new;
245             $image->set (symbol => 'BHP.AX',
246             mode => '1d');
247              
248             =head1 WIDGET HIERARCHY
249              
250             C<App::Chart::Gtk2::IntradayImage> is a subclass of C<Gtk2::DrawingArea>.
251              
252             Gtk2::Widget
253             Gtk2::DrawingArea
254             App::Chart::Gtk2::IntradayImage
255              
256             =head1 DESCRIPTION
257              
258             C<App::Chart::Gtk2::IntradayImage> displays an intraday graph image (a PNG, JPEG,
259             GIF etc) from the database for the given symbol and mode. The display
260             updates when a new image download is notified through the
261             C<App::Chart::Glib::Ex::DirBroadcast> mechanism. The database can have a
262             partial image during downloading, in that case as much as available is
263             displayed, with the effect being to progressively draw more as it downloads.
264             If there's no image at all then either an error message (from the database)
265             or simply "No data" is shown.
266              
267             This widget is just the image part. See L<App::Chart::Gtk2::IntradayDialog>
268             for the full interactive display.
269              
270             =head1 PROPERTIES
271              
272             =over 4
273              
274             =item C<symbol> (string)
275              
276             The stock or commodity symbol to display.
277              
278             =item C<mode> (string)
279              
280             The display mode, such as "1d" for a 1-day intraday image.
281              
282             =back
283              
284             =head1 SEE ALSO
285              
286             L<App::Chart::Gtk2::IntradayDialog>, L<Gtk2::Widget>