| 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 |  | 524 | use 5.010; | 
|  | 1 |  |  |  |  | 3 |  | 
| 19 | 1 |  |  | 1 |  | 4 | use strict; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 17 |  | 
| 20 | 1 |  |  | 1 |  | 4 | use warnings; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 29 |  | 
| 21 | 1 |  |  | 1 |  | 159 | 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 |