File Coverage

blib/lib/App/Chart/Gtk2/IntradayDialog.pm
Criterion Covered Total %
statement 9 11 81.8
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 13 15 86.6


line stmt bran cond sub pod time code
1             # Copyright 2007, 2008, 2009, 2010, 2011, 2012, 2013 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::IntradayDialog;
18 1     1   509 use 5.010;
  1         5  
19 1     1   5 use strict;
  1         2  
  1         16  
20 1     1   4 use warnings;
  1         1  
  1         30  
21 1     1   152 use Gtk2 1.220;
  0            
  0            
22             use List::Util 'min';
23             use Regexp::Common 'whitespace';
24             use POSIX ();
25             use Glib::Ex::ConnectProperties;
26             use Gtk2::Ex::CrossHair;
27             use Locale::TextDomain ('App-Chart');
28              
29             use Gtk2::Ex::EntryBits;
30             use Gtk2::Ex::Units;
31             use App::Chart::Glib::Ex::MoreUtils;
32             use App::Chart;
33             use App::Chart::Gtk2::GUI;
34             use App::Chart::IntradayHandler;
35             use App::Chart::Gtk2::IntradayImage;
36             use App::Chart::Gtk2::IntradayModeComboBox;
37             use App::Chart::Gtk2::Job;
38             use App::Chart::Gtk2::Job::Intraday;
39              
40             # uncomment this to run the ### lines
41             #use Smart::Comments;
42              
43             use Glib::Object::Subclass
44             'Gtk2::Dialog',
45             properties => [Glib::ParamSpec->string
46             ('symbol',
47             __('Symbol'),
48             'The symbol of the stock or commodity to be shown',
49             '', # default
50             Glib::G_PARAM_READWRITE),
51              
52             Glib::ParamSpec->string
53             ('mode',
54             'mode',
55             'The graph mode, such as 1 day or 5 days',
56             '', # default
57             Glib::G_PARAM_READWRITE)];
58              
59             use constant { RESPONSE_REFRESH => 0,
60             RESPONSE_CROSS => 1,
61             RESPONSE_SAVE => 2,
62             RESPONSE_PRINT => 3 };
63              
64              
65             sub INIT_INSTANCE {
66             my ($self) = @_;
67             $self->{'symbol'} = ''; # defaults
68             $self->{'mode'} = '';
69              
70             my $combobox = $self->{'combobox'}
71             = App::Chart::Gtk2::IntradayModeComboBox->new;
72             $combobox->show;
73             $self->add_accel_group ($combobox->accelgroup);
74              
75             $self->action_area->add ($combobox);
76             Glib::Ex::ConnectProperties->new ([$self,'symbol'],
77             [$combobox,'symbol']);
78             Glib::Ex::ConnectProperties->new ([$combobox,'mode'],
79             [$self,'mode']);
80              
81             my $crossbutton = $self->{'crossbutton'}
82             = Gtk2::CheckButton->new (__('Cr_oss'));
83             $crossbutton->set_active (0);
84             $self->add_action_widget ($crossbutton, RESPONSE_CROSS);
85              
86             $self->set_title (__('Chart: Intraday'));
87             $self->{'refresh_button'}
88             = $self->add_button ('gtk-refresh' => RESPONSE_REFRESH);
89             $self->add_buttons ('gtk-print' => RESPONSE_PRINT,
90             'gtk-save' => RESPONSE_SAVE,
91             'gtk-close' => 'close',
92             'gtk-help' => 'help');
93             # this is an "after" to allow a user's signals to be called first on
94             # 'close' or 'delete-event', since we're going to $self->destroy on those
95             $self->signal_connect_after (response => \&_do_response);
96              
97             my $vbox = $self->vbox;
98              
99             # display symbol and mode in a label, since can't be certain the window
100             # manager will have a good title bar
101             my $title_label = $self->{'title_label'} = Gtk2::Label->new ('');
102             _update_title_label ($self);
103             $vbox->pack_start ($title_label, 0,0,0);
104              
105             # centre in area, don't grow image beyond desired size
106             my $align = Gtk2::Alignment->new (0.5, 0.5, 0, 0);
107             $vbox->pack_start ($align, 1,1,0);
108              
109             my $image = $self->{'image'} = App::Chart::Gtk2::IntradayImage->new;
110             $align->add ($image);
111             Glib::Ex::ConnectProperties->new ([$self,'symbol'],
112             [$image,'symbol']);
113             Glib::Ex::ConnectProperties->new ([$self,'mode'],
114             [$image,'mode']);
115              
116             my $crosshair = $self->{'crosshair'}
117             = Gtk2::Ex::CrossHair->new (widget => $image,
118             foreground => 'orange');
119             Glib::Ex::ConnectProperties->new ([$crossbutton,'active'],
120             [$crosshair,'active']);
121             $image->add_events ('button-press-mask');
122             $image->signal_connect (button_press_event =>\&_do_image_button_press_event);
123              
124             my $progress_label = $self->{'progress_label'} = Gtk2::Label->new ('');
125             $vbox->pack_start ($progress_label, 0,0,0);
126              
127             my $hbox = Gtk2::HBox->new();
128             $vbox->pack_start ($hbox, 0,0,0);
129              
130             $hbox->pack_start (Gtk2::Label->new (__('Symbol')), 0,0,0);
131              
132             my $entry = Gtk2::Entry->new ();
133             $self->{'entry'} = $entry;
134             $hbox->pack_start ($entry, 1, 1, 0.5 * Gtk2::Ex::Units::em($entry));
135             $entry->signal_connect (activate => \&_do_entry_activate);
136              
137             my $button = Gtk2::Button->new_with_label (__('Enter'));
138             $hbox->pack_start ($button, 0,0,0);
139             $button->signal_connect (clicked => \&_do_enter_button);
140              
141             # During perl "global destruction" can have App::Chart::Gtk2::Job already
142             # destroyed enough that it has disconnected the message emission hook
143             # itself, leading to an unsightly Glib warning on attempting
144             # signal_remove_emission_hook() in our 'destroy' class closure.
145             #
146             # As a workaround instead leave it connected, with a weakened ref, and let
147             # it return 0 to disconnect itself on the next emission (if any).
148             #
149             # App::Chart::Gtk2::Job->signal_add_emission_hook
150             # ('status-changed', \&_do_job_status_changed,
151             # App::Chart::Glib::Ex::MoreUtils::ref_weak($self));
152             #
153             require App::Chart::Glib::Ex::EmissionHook;
154             $self->{'hook'} = App::Chart::Glib::Ex::EmissionHook->new
155             ('App::Chart::Gtk2::Job',
156             status_changed => \&_do_job_status_changed,
157             App::Chart::Glib::Ex::MoreUtils::ref_weak($self));
158              
159             $vbox->show_all;
160              
161             # secret Control-L to redraw
162             # ENHANCE-ME: maybe accel_path thing for configurability
163             my $accelgroup = $self->{'accelgroup'} = Gtk2::AccelGroup->new;
164             $self->add_accel_group ($accelgroup);
165             $accelgroup->connect (Gtk2::Gdk->keyval_from_name('l'), ['control-mask'], [],
166             \&_do_accel_redraw);
167              
168             # with a sensible intraday image size
169             Gtk2::Ex::Units::set_default_size_with_subsizes
170             ($self, [$image, 512, 288]);
171             }
172              
173             sub SET_PROPERTY {
174             my ($self, $pspec, $newval) = @_;
175             ### SET_PROPERTY: $pspec->get_name
176             ### newval: ''.\$newval
177              
178             my $pname = $pspec->get_name;
179             $self->{$pname} = $newval; # per default GET_PROPERTY
180              
181             if ($pname eq 'symbol') {
182             my $symbol = $newval;
183             my $entry = $self->{'entry'};
184             $entry->set_text ($symbol);
185             Gtk2::Ex::EntryBits::select_region_noclip ($entry, 0, -1);
186             }
187             _update_title_label ($self);
188             _update_job_status ($self);
189             $self->refresh_old;
190             }
191              
192             # refresh if the image in the database is more than 2 minutes old, or
193             # there's none in the database at all
194             sub refresh_old {
195             my ($self) = @_;
196             my $symbol = $self->{'symbol'} || return;
197             my $mode = $self->{'mode'} || return;
198              
199             require App::Chart::DBI;
200             require App::Chart::Download;
201             my $timestamp = App::Chart::DBI->read_single
202             ('SELECT fetch_timestamp FROM intraday_image WHERE symbol=? AND mode=?',
203             $symbol, $mode);
204             if (! App::Chart::Download::timestamp_within ($timestamp, 120)) {
205             $self->refresh;
206             }
207             }
208              
209             # download a fresh image for the current symbol+mode
210             sub refresh {
211             my ($self) = @_;
212             ### IntradayDialog refresh()
213             my $symbol = $self->{'symbol'} || return;
214             my $mode = $self->{'mode'} || return;
215              
216             require App::Chart::Gtk2::Job::Intraday;
217             App::Chart::Gtk2::Job::Intraday->start ($symbol, $mode);
218             _update_job_status ($self);
219             }
220              
221             # 'activate' signal on the Gtk2::Entry
222             sub _do_entry_activate {
223             my ($entry) = @_;
224             my $self = $entry->get_toplevel;
225             $self->goto_entry;
226             }
227             # 'clicked' signal on the "Enter" button
228             sub _do_enter_button {
229             my ($button) = @_;
230             my $self = $button->get_toplevel;
231             $self->goto_entry;
232             }
233             # set symbol to current contents of the text entry widget
234             sub goto_entry {
235             my ($self) = @_;
236             my $entry = $self->{'entry'};
237             my $symbol = $entry->get_text;
238             $symbol =~ s/$RE{ws}{crop}//go; # leading and trailing whitespace
239             $self->set (symbol => $symbol);
240             $self->refresh_old;
241             }
242              
243             # 'button-press-event' in the IntradayImage widget
244             sub _do_image_button_press_event {
245             my ($image, $event) = @_;
246             if ($event->button == 3) {
247             my $self = $image->get_toplevel;
248             $self->{'crosshair'}->start ($event);
249             }
250             return Gtk2::EVENT_PROPAGATE;
251             }
252              
253             sub _do_response {
254             my ($self, $response) = @_;
255             ### IntradayDialog response: $response
256              
257             if ($response eq RESPONSE_REFRESH) {
258             $self->refresh;
259              
260             } elsif ($response eq RESPONSE_SAVE) {
261             require App::Chart::Gtk2::IntradaySave;
262             App::Chart::Gtk2::IntradaySave->popup ($self);
263              
264             } elsif ($response eq RESPONSE_PRINT) {
265             $self->print_image;
266              
267             } elsif ($response eq 'close') {
268             # as per a keyboard close, defaults to raising 'delete-event', which in
269             # turn defaults to a destroy
270             $self->signal_emit ('close');
271              
272             } elsif ($response eq 'help') {
273             require App::Chart::Manual;
274             App::Chart::Manual->open(__p('manual-node','Intraday'), $self);
275             }
276             }
277              
278             sub _update_title_label {
279             my ($self) = @_;
280             my $title_label = $self->{'title_label'};
281             my $symbol = $self->{'symbol'};
282             my $mode = $self->{'mode'};
283             my $handler
284             = App::Chart::IntradayHandler->handler_for_symbol_and_mode ($symbol, $mode);
285             my $modename = ($handler ? $handler->name_sans_mnemonic : '');
286             $title_label->set_text ($symbol
287             ? __x('Chart: Intraday: {symbol} - {modename}',
288             symbol => $symbol,
289             modename => $modename)
290             : __('Chart: Intraday'));
291             }
292              
293             # 'status-change' signal emission hook
294             sub _do_job_status_changed {
295             my ($invocation_hint, $param_list, $ref_weak_self) = @_;
296             my $self = $$ref_weak_self || return 0; # disconnect
297             _update_job_status ($self);
298             return 1; # stay connected
299             }
300              
301             sub _update_job_status {
302             my ($self) = @_;
303             ### IntradayDialog update job status
304             my $symbol = $self->{'symbol'};
305             my $mode = $self->{'mode'};
306             my $job = App::Chart::Gtk2::Job::Intraday->find ($symbol, $mode);
307             my $job_running = ($job && $job->is_stoppable);
308             ### job: $job
309              
310             my $status_str = ($job ? __('Download: ') . $job->status : '');
311             $self->{'progress_label'}->set_text ($status_str);
312             ### status: $job && $job->status
313              
314             $self->set_response_sensitive (RESPONSE_REFRESH,
315             $symbol && $mode && ! $job_running);
316              
317             if ($job_running) {
318             # created when first needed for a running job
319             $self->{'widgetcursor'} ||= do {
320             require Gtk2::Ex::WidgetCursor;
321             Gtk2::Ex::WidgetCursor->new (widgets => [ $self->{'image'},
322             $self->{'refresh_button'} ],
323             cursor => 'watch',
324             priority => 10);
325             };
326             }
327             if (my $wcursor = $self->{'widgetcursor'}) {
328             $wcursor->active ($job_running);
329             }
330             }
331              
332             sub _do_accel_redraw {
333             my ($accelgroup, $self, $keyval, $modifiers) = @_;
334             $self->queue_draw;
335             }
336              
337             sub popup {
338             my ($class, $symbol, $parent) = @_;
339             if (! defined $symbol) { $symbol = ''; }
340             require App::Chart::Gtk2::Ex::ToplevelBits;
341             return App::Chart::Gtk2::Ex::ToplevelBits::popup
342             ($class,
343             properties => { symbol => $symbol },
344             screen => $parent);
345             }
346              
347             #------------------------------------------------------------------------------
348             # printing
349              
350             sub print_image {
351             my ($self) = @_;
352             my $print = Gtk2::PrintOperation->new;
353             $print->set_n_pages (1);
354             if (my $settings = $self->{'print_settings'}) {
355             $print->set_print_settings ($settings);
356             }
357             $print->signal_connect (draw_page => \&_draw_page,
358             App::Chart::Glib::Ex::MoreUtils::ref_weak($self));
359              
360             my $result = $print->run ('print-dialog', $self);
361             if ($result eq 'apply') {
362             $self->{'print_settings'} = $print->get_print_settings;
363             }
364             }
365              
366             sub _draw_page {
367             my ($print, $pcontext, $pagenum, $ref_weak_self) = @_;
368             ### _draw_page()
369             my $self = $$ref_weak_self || return;
370             my $c = $pcontext->get_cairo_context;
371              
372             my $symbol = $self->{'symbol'};
373             my $mode = $self->{'mode'};
374             my $handler = App::Chart::IntradayHandler->handler_for_symbol_and_mode
375             ($symbol, $mode);
376             my $modename = ($handler ? $handler->name_sans_mnemonic : '');
377             my $str = "$symbol - $mode";
378              
379             my $dbh = App::Chart::DBI->instance;
380             my $sth = $dbh->prepare_cached
381             ('SELECT image, error, fetch_timestamp FROM intraday_image WHERE symbol=? AND mode=?');
382             my ($blob, $error, $timestamp) = $dbh->selectrow_array
383             ($sth, undef, $self->{'symbol'}, $self->{'mode'});
384             $sth->finish();
385             if (defined $timestamp) {
386             my $timet = App::Chart::Download::timestamp_to_timet($timestamp);
387             my $timezone = App::Chart::TZ->for_symbol ($symbol);
388             $str .= ' ' . POSIX::strftime ($App::Chart::option{'d_fmt'} . ' %H:%M',
389             $timezone->localtime($timet));
390             }
391             $str .= "\n\n"; # blank line
392              
393             my $pixbuf = $self->{'image'}->_load_pixbuf;
394             # $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file
395             # ('/usr/share/emacs/23.2/etc/images/splash.png');
396             # $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file
397             # ('/usr/share/games/gav/themes/classic/background_big.png');
398             if (! ref $pixbuf) {
399             $str .= $pixbuf; # error message
400             }
401              
402             my $pwidth = $pcontext->get_width;
403             ### $pwidth
404              
405             my $layout = $pcontext->create_pango_layout;
406             $layout->set_width ($pwidth * Gtk2::Pango::PANGO_SCALE);
407             $layout->set_text ($str);
408             my (undef, $str_height) = $layout->get_pixel_size;
409             ### $str_height
410             $c->move_to (0, 0);
411             Gtk2::Pango::Cairo::show_layout ($c, $layout);
412              
413             if (ref $pixbuf) {
414             my $pixbuf_width = $pixbuf->get_width;
415             my $pixbuf_height = $pixbuf->get_height;
416             ### $pixbuf_width
417             ### $pixbuf_height
418              
419             my $pheight = $pcontext->get_height - $str_height;
420             $c->translate (0, $str_height);
421              
422             if ($pixbuf_width > $pwidth || $pixbuf_height > $pheight) {
423             # shrink if too big
424             my $factor = min ($pwidth / $pixbuf_width,
425             $pheight / $pixbuf_height);
426             $c->scale ($factor, $factor);
427             }
428              
429             Gtk2::Gdk::Cairo::Context::set_source_pixbuf ($c, $pixbuf, 0,0);
430             $c->rectangle (0,0, $pixbuf_width,$pixbuf_height);
431             $c->paint;
432             }
433             }
434              
435             1;
436             __END__
437              
438             =for stopwords intraday
439              
440             =head1 NAME
441              
442             App::Chart::Gtk2::IntradayDialog -- intraday graph dialog widget
443              
444             =head1 SYNOPSIS
445              
446             use App::Chart::Gtk2::IntradayDialog;
447             App::Chart::Gtk2::IntradayDialog->popup; # initially empty
448             App::Chart::Gtk2::IntradayDialog->popup ('BHP.AX'); # or given symbol
449              
450             =head1 WIDGET HIERARCHY
451              
452             C<App::Chart::Gtk2::IntradayDialog> is a subclass of C<Gtk2::Dialog>.
453              
454             Gtk2::Widget
455             Gtk2::Container
456             Gtk2::Bin
457             Gtk2::Window
458             Gtk2::Dialog
459             App::Chart::Gtk2::IntradayDialog
460              
461             =head1 DESCRIPTION
462              
463             A C<App::Chart::Gtk2::IntradayDialog> displays intraday graphs in the form of
464             downloaded graphics images. The various data sources setup available modes
465             such as 1-day or 5-day and the C<IntradayDialog> downloads and shows them.
466              
467             Some data sources don't offer historical data as figures, but only as
468             graphics images. For them "intraday" is pressed into service to show daily
469             data too.
470              
471             =head1 FUNCTIONS
472              
473             =over 4
474              
475             =item C<< App::Chart::Gtk2::IntradayDialog->popup () >>
476              
477             =item C<< App::Chart::Gtk2::IntradayDialog->popup ($symbol) >>
478              
479             Present an intraday dialog for C<$symbol>. C<$symbol> is a string, or
480             empty, C<undef> or omitted to get a dialog showing nothing initially.
481              
482             If a dialog already exists showing C<$symbol> then it's raised rather than
483             creating a new one.
484              
485             =item C<< $dialog->goto_entry() >>
486              
487             Go to the symbol entered in the text entry box by setting it as the
488             C<symbol> property. This is used by the return key in that entry box and
489             the "Enter" button beside it.
490              
491             =item C<< $dialog->refresh() >>
492              
493             Download a new image for the current symbol and mode. This is the "Refresh"
494             button in the action area.
495              
496             =back
497              
498             =head1 PROPERTIES
499              
500             =over 4
501              
502             =item C<symbol>
503              
504             The stock symbol (a string) to display.
505              
506             =item C<mode>
507              
508             The display mode (a string), such as '1 Day'.
509              
510             =back
511              
512             =head1 SEE ALSO
513              
514             L<App::Chart::Gtk2::IntradayImage>
515              
516             =head1 HOME PAGE
517              
518             L<http://user42.tuxfamily.org/chart/index.html>
519              
520             =head1 LICENCE
521              
522             Copyright 2007, 2008, 2009, 2010, 2011, 2012, 2013 Kevin Ryde
523              
524             Chart is free software; you can redistribute it and/or modify it under the
525             terms of the GNU General Public License as published by the Free Software
526             Foundation; either version 3, or (at your option) any later version.
527              
528             Chart is distributed in the hope that it will be useful, but WITHOUT ANY
529             WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
530             FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
531             details.
532              
533             You should have received a copy of the GNU General Public License along with
534             Chart; see the file F<COPYING>. Failing that, see
535             L<http://www.gnu.org/licenses/>.
536              
537             =cut