File Coverage

blib/lib/App/Chart/Gtk2/WatchlistDialog.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, 2013, 2014 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::WatchlistDialog;
18 1     1   60189 use 5.010;
  1         3  
19 1     1   4 use strict;
  1         2  
  1         16  
20 1     1   4 use warnings;
  1         2  
  1         20  
21 1     1   4 use Carp;
  1         2  
  1         51  
22 1     1   728 use Gtk2 1.220;
  0            
  0            
23             use Locale::TextDomain 1.17; # for __p()
24             use Locale::TextDomain ('App-Chart');
25              
26             use Glib::Ex::ConnectProperties;
27             use Gtk2::Ex::EntryBits;
28             use Gtk2::Ex::TreeViewBits;
29             use Gtk2::Ex::Units;
30             use Gtk2::Ex::WidgetCursor;
31              
32             use App::Chart::Gtk2::Ex::CellRendererTextBits;
33             use App::Chart::Gtk2::Ex::NotebookLazyPages;
34             use App::Chart::Gtk2::Ex::ToplevelBits;
35             use App::Chart::Gtk2::Symlist;
36              
37             # uncomment this to run the ### lines
38             #use Devel::Comments;
39              
40             BEGIN {
41             Gtk2->CHECK_VERSION(2,12,0)
42             or die "Need Gtk 2.12 or higher"; # for ->error_bell
43             }
44              
45             use constant DEFAULT_SYMLIST_KEY => 'favourites';
46              
47             # use App::Chart::Gtk2::Ex::ToplevelSingleton hide_on_delete => 1;
48             # use base 'App::Chart::Gtk2::Ex::ToplevelSingleton';
49             # sub popup {
50             # my ($class, $parent) = @_;
51             # my $self = $class->instance_for_screen ($parent);
52             # $self->present;
53             # return $self;
54             # }
55              
56             use Glib::Object::Subclass
57             'Gtk2::Dialog',
58             properties => [ Glib::ParamSpec->object
59             ('symlist',
60             'symlist',
61             'The symlist to display.',
62             # App::Chart::Gtk2::Symlist::Join isn't a glib derivative
63             'Glib::Object', # 'App::Chart::Gtk2::Symlist',
64             Glib::G_PARAM_READWRITE),
65             ];
66              
67              
68             use constant { NOTEBOOK_PAGENUM_SYMBOLS => 0,
69             NOTEBOOK_PAGENUM_SYMLISTS => 1 };
70              
71             use constant { RESPONSE_REFRESH => 0,
72             RESPONSE_DELETE => 1,
73             RESPONSE_INTRADAY => 2,
74             RESPONSE_EDIT_NAME => 3,
75             };
76              
77             sub INIT_INSTANCE {
78             my ($self) = @_;
79             $self->set_title (__('Chart: Watchlist'));
80             $self->signal_connect (response => \&_do_response);
81              
82             my $vbox = $self->vbox;
83             my $action_area = $self->action_area;
84             my $em = Gtk2::Ex::Units::em($self);
85              
86             my $symlist = $self->{'symlist'}
87             = App::Chart::Gtk2::Symlist->new_from_key (DEFAULT_SYMLIST_KEY);
88              
89             my $notebook = $self->{'notebook'} = Gtk2::Notebook->new;
90             $notebook->set (tab_hborder => 0.5 * $em);
91             $vbox->pack_start ($notebook, 1,1,0);
92              
93             # require App::Chart::Gtk2::SymlistComboBox;
94             # my $combobox = App::Chart::Gtk2::SymlistComboBox->new;
95             # Glib::Ex::ConnectProperties->new ([$self, 'symlist'],
96             # [$combobox,'symlist']);
97             # $combobox->signal_connect (changed => \&_do_combobox_changed);
98             # $action_area->add ($combobox);
99              
100             $self->add_buttons ('gtk-refresh' => RESPONSE_REFRESH,
101             'gtk-delete' => RESPONSE_DELETE);
102             {
103             my $intraday_button = $self->{'intraday_button'}
104             = Gtk2::Button->new_with_mnemonic (__('_Intraday'));
105             $self->add_action_widget ($intraday_button, RESPONSE_INTRADAY);
106             }
107             {
108             my $edit_button = $self->{'edit_button'}
109             = Gtk2::Button->new_with_mnemonic (__('_Edit Name'));
110             $self->add_action_widget ($edit_button, RESPONSE_EDIT_NAME);
111             }
112             $self->add_buttons ('gtk-close' => 'close',
113             'gtk-help' => 'help');
114              
115              
116              
117             require App::Chart::Gtk2::WatchlistModel;
118             my $model = App::Chart::Gtk2::WatchlistModel->new ($symlist);
119              
120             my $symbols_vbox = Gtk2::VBox->new;
121             my $symbols_tab_eventbox = $self->{'symbols_tab_eventbox'}
122             = Gtk2::EventBox->new;
123             $symbols_tab_eventbox->signal_connect
124             (button_press_event => \&_do_symbols_tab_button_press);
125             my $symbols_tab_label = $self->{'symbols_tab_label'} = Gtk2::Label->new;
126             $symbols_tab_label->show;
127             $symbols_tab_eventbox->add ($symbols_tab_label);
128             $notebook->append_page ($symbols_vbox, $symbols_tab_eventbox);
129              
130             my $symlists_vbox = $self->{'symlists_vbox'} = Gtk2::VBox->new;
131             $notebook->append_page ($symlists_vbox, __('Symlists'));
132             App::Chart::Gtk2::Ex::NotebookLazyPages::set_init
133             ($notebook, $symlists_vbox, \&_init_symlists_page);
134              
135             $notebook->signal_connect ('notify::page' => \&_do_notebook_notify_page);
136              
137             my $scrolled = $self->{'symbols_scrolled'} = Gtk2::ScrolledWindow->new;
138             $scrolled->set(hscrollbar_policy => 'never');
139             $symbols_vbox->pack_start ($scrolled, 1,1,0);
140              
141             my $treeview = $self->{'symbols_treeview'}
142             = Gtk2::TreeView->new_with_model ($model);
143             $treeview->set (fixed_height_mode => 1,
144             reorderable => $symlist && $symlist->can_edit);
145              
146             $scrolled->add ($treeview);
147             $treeview->signal_connect (query_tooltip => \&_do_query_tooltip);
148             $treeview->set (has_tooltip => 1);
149              
150             my $selection = $treeview->get_selection;
151             $selection->signal_connect (changed => \&_do_symbol_selection_changed);
152             $selection->set_mode ('single');
153              
154             my $renderer_left = Gtk2::CellRendererText->new;
155             $renderer_left->set (xalign => 0,
156             ypad => 0);
157             $renderer_left->set_fixed_height_from_font (1);
158             my $renderer_right = Gtk2::CellRendererText->new;
159             $renderer_right->set (xalign => 1,
160             ypad => 0);
161             $renderer_right->set_fixed_height_from_font (1);
162              
163             {
164             my $renderer = $self->{'symbol_renderer'} = Gtk2::CellRendererText->new;
165             $renderer->set (xalign => 0, ypad => 0);
166             $renderer->set_fixed_height_from_font (1);
167              
168             my $column = Gtk2::TreeViewColumn->new_with_attributes
169             (__('Symbol'), $renderer,
170             text => $model->COL_SYMBOL,
171             foreground => $model->COL_COLOUR);
172             $column->set (sizing => 'fixed',
173             fixed_width => 8*$em,
174             resizable => 1);
175             App::Chart::Gtk2::Ex::CellRendererTextBits::renderer_edited_set_value
176             ($renderer, $column, 0);
177             $renderer->signal_connect (edited => \&_do_symbol_renderer_edited);
178             $treeview->append_column ($column);
179             }
180             {
181             my $column = Gtk2::TreeViewColumn->new_with_attributes
182             (__('Bid/Offer'), $renderer_right,
183             text => $model->COL_BIDOFFER,
184             foreground => $model->COL_COLOUR);
185             $column->set (sizing => 'fixed',
186             fixed_width => 12*$em,
187             resizable => 1);
188             $treeview->append_column ($column);
189             }
190             {
191             my $column = Gtk2::TreeViewColumn->new_with_attributes
192             (__('Last'), $renderer_right,
193             text => $model->COL_LAST,
194             foreground => $model->COL_COLOUR);
195             $column->set (sizing => 'fixed',
196             fixed_width => 7*$em,
197             resizable => 1);
198             $treeview->append_column ($column);
199             }
200             {
201             my $column = Gtk2::TreeViewColumn->new_with_attributes
202             (__('Change'), $renderer_right,
203             text => $model->COL_CHANGE,
204             foreground => $model->COL_COLOUR);
205             $column->set (sizing => 'fixed',
206             fixed_width => 7*$em,
207             resizable => 1);
208             $treeview->append_column ($column);
209             }
210             {
211             my $column = Gtk2::TreeViewColumn->new_with_attributes
212             (__('High'), $renderer_right,
213             text => $model->COL_HIGH,
214             foreground => $model->COL_COLOUR);
215             $column->set (sizing => 'fixed',
216             fixed_width => 7*$em,
217             resizable => 1);
218             $treeview->append_column ($column);
219             }
220             {
221             my $column = Gtk2::TreeViewColumn->new_with_attributes
222             (__('Low'), $renderer_right,
223             text => $model->COL_LOW,
224             foreground => $model->COL_COLOUR);
225             $column->set (sizing => 'fixed',
226             fixed_width => 7*$em,
227             resizable => 1);
228             $treeview->append_column ($column);
229             }
230             {
231             my $column = Gtk2::TreeViewColumn->new_with_attributes
232             (__('Volume'), $renderer_right,
233             text => $model->COL_VOLUME,
234             foreground => $model->COL_COLOUR);
235             $column->set (sizing => 'fixed',
236             fixed_width => 6*$em,
237             resizable => 1);
238             $treeview->append_column ($column);
239             }
240             {
241             my $column = Gtk2::TreeViewColumn->new_with_attributes
242             (__('When'), $renderer_right,
243             text => $model->COL_WHEN,
244             foreground => $model->COL_COLOUR);
245             $column->set (sizing => 'fixed',
246             fixed_width => 6*$em,
247             resizable => 1);
248             $treeview->append_column ($column);
249             }
250             {
251             my $column = Gtk2::TreeViewColumn->new_with_attributes
252             (__('Notes'), $renderer_left,
253             text => $model->COL_NOTE,
254             foreground => $model->COL_COLOUR);
255             $column->set (sizing => 'fixed',
256             fixed_width => 8*$em,
257             resizable => 1);
258             $treeview->append_column ($column);
259             }
260             $treeview->add_events ('button-press-mask');
261             $treeview->signal_connect (button_press_event => \&_do_symbol_menu_popup);
262             $treeview->signal_connect (row_activated => \&_do_symbol_treeview_activate);
263              
264             my $hbox = Gtk2::HBox->new;
265             $symbols_vbox->pack_start ($hbox, 0,0,0);
266              
267             my $entry_label = Gtk2::Label->new (__('New Symbol'));
268             $hbox->pack_start ($entry_label, 0,0,0);
269              
270             my $entry = $self->{'symbol_entry'} = Gtk2::Entry->new;
271             $hbox->pack_start ($entry, 1,1,0);
272             $entry->signal_connect (activate => \&_do_symbol_entry_activate);
273              
274             { my $button = Gtk2::Button->new_with_label (__('Insert'));
275             $hbox->pack_start ($button, 0,0,0);
276             $button->signal_connect (clicked => \&_do_symbol_entry_activate);
277             }
278              
279             _update_delete_sensitive ($self);
280             _update_intraday_sensitive ($self);
281             _update_edit_sensitive ($self);
282              
283             $vbox->show_all;
284             _do_notebook_notify_page ($notebook); # initial hides
285              
286             # with a sensible rows size for the TreeView
287             Gtk2::Ex::Units::set_default_size_with_subsizes
288             ($self, [$scrolled, -1, '20 lines']);
289              
290             $self->{'symlist'} = undef; # fake to force update
291             $self->set_symlist ($symlist);
292             }
293              
294             # # 'notify:symlist' on the App::Chart::Gtk2::SymlistComboBox
295             # # switch page to the symbol list display when a symlist is selected
296             # sub _do_combobox_changed {
297             # my ($combobox) = @_;
298             # if (DEBUG) {
299             # say "Watchlist symlist combobox changed, switch notebook to symbols";
300             # }
301             # my $self = $combobox->get_toplevel;
302             # my $notebook = $self->{'notebook'};
303             # $notebook->set_current_page(NOTEBOOK_PAGENUM_SYMBOLS);
304             # }
305              
306             # 'edited' signal on the Gtk2::CellRendererText in the symbol column,
307             # initiate a download of the new symbol
308             sub _do_symbol_renderer_edited {
309             my ($renderer, $pathstr, $newstr) = @_;
310             require App::Chart::Gtk2::Job::Latest;
311             App::Chart::Gtk2::Job::Latest->start ([$newstr]);
312             }
313              
314             sub _do_notebook_notify_page {
315             my ($notebook) = @_;
316             my $self = $notebook->get_toplevel;
317             ### Watchlist notebook switch to: $notebook->get_current_page
318              
319             my $pagenum = $notebook->get_current_page;
320             $self->{'intraday_button'}->set
321             (visible => ($pagenum == NOTEBOOK_PAGENUM_SYMBOLS));
322             $self->{'edit_button'}->set
323             (visible => ($pagenum == NOTEBOOK_PAGENUM_SYMLISTS));
324             _update_delete_sensitive ($self);
325             }
326              
327             sub _init_symlists_page {
328             my ($notebook, $vbox, $pagenum) = @_;
329             my $self = $notebook->get_toplevel;
330             ### Watchlist _init_symlists_page()
331              
332             my $scrolled = $self->{'symlists_scrolled'} = Gtk2::ScrolledWindow->new;
333             $scrolled->set (hscrollbar_policy => 'automatic');
334             $vbox->pack_start ($scrolled, 1,1,0);
335              
336             require App::Chart::Gtk2::SymlistListModel;
337             my $model = App::Chart::Gtk2::SymlistListModel->instance;
338              
339             my $treeview = $self->{'symlists_treeview'}
340             = Gtk2::TreeView->new_with_model ($model);
341             $treeview->set (fixed_height_mode => 0,
342             reorderable => 1);
343             $scrolled->add ($treeview);
344             $treeview->signal_connect (row_activated =>\&_do_symlists_treeview_activate);
345             $treeview->add_events ('button-press-mask');
346             $treeview->signal_connect (button_press_event => \&_do_symlist_menu_popup);
347             # $treeview->signal_connect (query_tooltip => \&_do_query_tooltip);
348             # $treeview->set (has_tooltip => 1);
349              
350             my $selection = $treeview->get_selection;
351             $selection->signal_connect (changed => \&_do_symlist_selection_changed);
352             $selection->set_mode ('single');
353              
354             {
355             my $renderer = $self->{'symlists_name_renderer'}
356             = Gtk2::CellRendererText->new;
357             $renderer->set (xalign => 0,
358             ypad => 0);
359             my $column = $self->{'symlists_name_treecolumn'}
360             = Gtk2::TreeViewColumn->new_with_attributes
361             (__('Name'), $renderer, text => $model->COL_NAME);
362             App::Chart::Gtk2::Ex::CellRendererTextBits::renderer_edited_set_value
363             ($renderer, $column, $model->COL_NAME);
364             $treeview->append_column ($column);
365             }
366             {
367             my $renderer = Gtk2::CellRendererText->new;
368             $renderer->set (xalign => 0,
369             ypad => 0);
370             my $column = Gtk2::TreeViewColumn->new_with_attributes
371             (__('Key'), $renderer, text => $model->COL_KEY);
372             $treeview->append_column ($column);
373             }
374             # {
375             # my $renderer = Gtk2::CellRendererText->new;
376             # $renderer->set (xalign => 0,
377             # ypad => 0,
378             # text => __('Edit Name'));
379             # my $column = Gtk2::TreeViewColumn->new_with_attributes
380             # ('', $renderer);
381             # $treeview->append_column ($column);
382             # }
383              
384             my $hbox = Gtk2::HBox->new;
385             $vbox->pack_start ($hbox, 0,0,0);
386              
387             my $entry_label = Gtk2::Label->new (__('New List'));
388             $hbox->pack_start ($entry_label, 0,0,0);
389              
390             my $entry = $self->{'symlist_entry'} = Gtk2::Entry->new;
391             $hbox->pack_start ($entry, 1,1,0);
392             $entry->signal_connect (activate => \&_do_symlist_entry_activate);
393              
394             { my $button = Gtk2::Button->new_with_label (__('Insert'));
395             $hbox->pack_start ($button, 0,0,0);
396             $button->signal_connect (clicked => \&_do_symlist_entry_activate);
397             }
398              
399             $self->{'symlists_setup'} = 1;
400             $vbox->show_all;
401             }
402              
403             sub SET_PROPERTY {
404             my ($self, $pspec, $newval) = @_;
405             my $pname = $pspec->get_name;
406             if ($pspec->get_name eq 'symlist') {
407             $self->set_symlist ($newval);
408             } else {
409             $self->{$pname} = $newval; # per default GET_PROPERTY
410             }
411             }
412              
413             sub get_selected_symbol {
414             my ($self) = @_;
415             my $treeview = $self->{'symbols_treeview'};
416             my $selection = $treeview->get_selection;
417             my ($model, $iter) = $selection->get_selected;
418             if (! defined $iter) { return undef; }
419             my ($symbol) = $model->get ($iter, 0);
420             return $symbol;
421             }
422              
423             sub set_symlist {
424             my ($self, $symlist) = @_;
425             ### Watchlist set_symlist()
426              
427             if (my $conn = delete $self->{'symlist_name_conn'}) {
428             $conn->disconnect;
429             }
430             my $label = $self->{'symbols_tab_label'};
431             if ($symlist) {
432             $self->{'symlist_name_conn'}
433             = Glib::Ex::ConnectProperties->new ([$symlist,'name'],
434             [$label, 'label']);
435             } else {
436             $label->set_text (__('(No list)'));
437             }
438              
439             if (($symlist||0) eq ($self->{'symlist'}||0)) {
440             ### symlist unchanged
441             return;
442             }
443              
444             ### new symlist: "$symlist"
445             Gtk2::Ex::WidgetCursor->busy;
446             my $model = $symlist && App::Chart::Gtk2::WatchlistModel->new ($symlist);
447              
448             {
449             my $reorderable = $symlist && $symlist->can_edit;
450             my $symbols_treeview = $self->{'symbols_treeview'};
451             ### $reorderable
452             $symbols_treeview->set (model => $model,
453             reorderable => $reorderable);
454              
455             # FIXME: what was this? dragging text to add a symbol? doing it turns
456             # off reorderable circa gtk 2.24 -- another incompatible change probably ...
457             #
458             # if ($reorderable) {
459             # $symbols_treeview->enable_model_drag_dest
460             # (['move'], { target => 'text/plain',
461             # flags => []}); # 'other-app'
462             # }
463             }
464              
465             $self->{'symlist'} = $symlist;
466              
467             if ($symlist) {
468             if (my $treeview = $self->{'symlists_treeview'}) {
469             my $model = $treeview->get_model;
470             my $key = $symlist->key;
471             $model->foreach
472             (sub {
473             my ($model, $path, $iter) = @_;
474             my $this_key = $model->get_value ($iter, $model->COL_KEY);
475             if ($this_key ne $key) { return 0; } # keep iterating
476             my $selection = $treeview->get_selection;
477             $selection->select_path ($path);
478             return 1; # stop iterating
479             });
480             }
481             }
482              
483             my $editable = $symlist && $symlist->can_edit;
484             ### symbol column editable: $editable
485             $self->{'symbol_renderer'}->set (editable => $editable);
486              
487             $self->notify ('symlist');
488             }
489              
490             sub get_symlists_selected_key {
491             my ($self) = @_;
492             my $treeview = $self->{'symlists_treeview'} || return; # if created yet
493             my $selection = $treeview->get_selection;
494             my ($model, $iter) = $selection->get_selected;
495             if (! defined $iter) { return; }
496             my ($symbol) = $model->get ($iter, $model->COL_KEY);
497             return $symbol;
498             }
499              
500             sub _update_delete_sensitive {
501             my ($self) = @_;
502             ### Watchlist _update_delete_sensitive()
503             $self->set_response_sensitive (RESPONSE_DELETE,
504             _want_delete_sensitive($self));
505             }
506             sub _want_delete_sensitive {
507             my ($self) = @_;
508             my $notebook = $self->{'notebook'};
509             my $pagenum = $notebook->get_current_page;
510             ### $pagenum
511              
512             if ($pagenum == NOTEBOOK_PAGENUM_SYMLISTS) {
513             my $key = $self->get_symlists_selected_key;
514             if (! defined $key) {
515             ### no selected symlist
516             return 0;
517             }
518             my $symlist = App::Chart::Gtk2::Symlist->new_from_key ($key);
519             if (! $symlist) {
520             ### no such symlist: $key
521             return 0;
522             }
523             ### can_delete_symlist() on: $key
524             return $symlist->can_delete_symlist;
525              
526             } else {
527             my $treeview = $self->{'symbols_treeview'};
528             my $selection = $treeview->get_selection;
529             my ($model, $iter) = $selection->get_selected;
530             if (! $iter) {
531             ### no selected symbol
532             return 0;
533             }
534             my $symlist = $model->get_model;
535             return $symlist->can_edit;
536             }
537             }
538              
539             sub _update_intraday_sensitive {
540             my ($self) = @_;
541             ### Watchlist _update_intraday_sensitive()
542             my $symbol = $self->get_selected_symbol;
543             ### $symbol
544             $self->set_response_sensitive (RESPONSE_INTRADAY,
545             symbol_intraday_sensitive($symbol));
546             }
547             sub symbol_intraday_sensitive {
548             my ($symbol) = @_;
549             if (! $symbol) { return 0; }
550             require App::Chart::IntradayHandler;
551             return scalar (App::Chart::IntradayHandler->handlers_for_symbol ($symbol));
552             }
553              
554             sub _update_edit_sensitive {
555             my ($self) = @_;
556             $self->set_response_sensitive (RESPONSE_EDIT_NAME,
557             defined $self->get_symlists_selected_key);
558             }
559              
560             sub _do_symbols_tab_button_press {
561             my ($symbols_tab_eventbox, $event) = @_;
562             my $self = $symbols_tab_eventbox->get_toplevel;
563              
564             if ($event->button == 3) {
565             require App::Chart::Gtk2::SymlistRadioMenu;
566             my $symlist_menu = App::Chart::Gtk2::SymlistRadioMenu->new;
567             ### menu destroy connection: $symlist_menu->signal_connect (destroy => sub { print "Watchlist symlist menu destroyed\n" })
568             Glib::Ex::ConnectProperties->new ([$self, 'symlist'],
569             [$symlist_menu, 'symlist']);
570             $symlist_menu->set_screen ($self->get_screen);
571             $symlist_menu->popup (undef, # parent menushell
572             undef, # parent menuitem
573             undef, # position func
574             undef, # position userdata
575             $_, # button
576             $event->time);
577             return Gtk2::EVENT_PROPAGATE;
578             }
579              
580             # GtkNotebook button press handler can cope with an event from a child
581             # widget
582             return $self->{'notebook'}->signal_emit ('button_press_event', $event);
583             }
584              
585             sub _do_symbol_treeview_activate {
586             my ($treeview, $path, $column) = @_;
587             my $self = $treeview->get_toplevel;
588             my $symlist = $self->{'symlist'};
589             my $iter = $symlist->get_iter ($path);
590             my $symbol = $symlist->get_value ($iter, 0);
591              
592             require App::Chart::Gtk2::Main;
593             my $main = App::Chart::Gtk2::Main->find_for_dialog ($self);
594             $main->goto_symbol ($symbol, $symlist);
595             $main->present;
596             }
597              
598             sub _do_symlists_treeview_activate {
599             my ($treeview, $path, $column) = @_;
600             my $model = $treeview->get_model;
601             my $iter = $model->get_iter ($path);
602             my $key = $model->get_value ($iter, $model->COL_KEY);
603             my $symlist = App::Chart::Gtk2::Symlist->new_from_key ($key);
604             my $self = $treeview->get_toplevel;
605             $self->set_symlist ($symlist);
606             $self->{'notebook'}->set_current_page (0);
607             }
608              
609             sub _do_symbol_selection_changed {
610             my ($selection) = @_;
611             my $self = $selection->get_tree_view->get_toplevel;
612             _update_intraday_sensitive ($self);
613             _update_delete_sensitive ($self);
614             }
615             sub _do_symlist_selection_changed {
616             my ($selection) = @_;
617             my $self = $selection->get_tree_view->get_toplevel;
618             _update_delete_sensitive ($self);
619             _update_edit_sensitive ($self);
620             }
621              
622             # 'response' signal handler
623             sub _do_response {
624             my ($self, $response) = @_;
625             ### Watchlist _do_response(): $response
626              
627             if ($response eq RESPONSE_REFRESH) {
628             $self->refresh;
629              
630             } elsif ($response eq RESPONSE_DELETE) {
631             my $notebook = $self->{'notebook'};
632             my $pagenum = $notebook->get_current_page;
633             my $treeview;
634             if ($pagenum == NOTEBOOK_PAGENUM_SYMLISTS) {
635             $treeview = $self->{'symlists_treeview'};
636             # supposed to be insensitive when no selection, but check anyway
637             my $key = $self->get_symlists_selected_key || return;
638              
639             # ignore somehow unknown key
640             my $symlist = App::Chart::Gtk2::Symlist->new_from_key ($key) || return;
641              
642             if (! $symlist->is_empty) {
643             # dialog if symlist not empty
644             require App::Chart::Gtk2::DeleteSymlistDialog;
645             App::Chart::Gtk2::DeleteSymlistDialog->popup ($symlist, $self);
646             return;
647             }
648             } else {
649             $treeview = $self->{'symbols_treeview'};
650             }
651             require Gtk2::Ex::TreeViewBits;
652             Gtk2::Ex::TreeViewBits::remove_selected_rows ($treeview);
653              
654             } elsif ($response eq RESPONSE_INTRADAY) {
655             # supposed to be insensitive when no selected symbol, but check anyway
656             my $symbol = $self->get_selected_symbol // return;
657             App::Chart::Gtk2::Ex::ToplevelBits::popup
658             ('App::Chart::Gtk2::IntradayDialog',
659             properties => { symbol => $symbol },
660             screen => $self);
661              
662             } elsif ($response eq RESPONSE_EDIT_NAME) {
663             my $notebook = $self->{'notebook'};
664             my $pagenum = $notebook->get_current_page;
665             # supposed to be visible only when symlists showing, but check anyway
666             ($pagenum == NOTEBOOK_PAGENUM_SYMLISTS) or return;
667              
668             my $treeview = $self->{'symlists_treeview'};
669             my $selection = $treeview->get_selection;
670             my ($symlists_model, $iter) = $selection->get_selected;
671             # supposed to be insensitive if no selection, but check anyway
672             if (! defined $iter) { return; }
673              
674             my $path = $symlists_model->get_path($iter);
675             ### set_cursor to path: $path->to_string
676             $treeview->grab_focus;
677             my $renderer = $self->{'symlists_name_renderer'};
678             $renderer->set (editable => 1);
679             $treeview->set_cursor ($path, $self->{'symlists_name_treecolumn'}, 1);
680             $renderer->set (editable => 0);
681              
682             } elsif ($response eq 'close') {
683             # as per a keyboard close, defaults to raising 'delete-event', which in
684             # turn defaults to a destroy
685             $self->signal_emit ('close');
686              
687             } elsif ($response eq 'help') {
688             require App::Chart::Manual;
689             App::Chart::Manual->open(__p('manual-node','Watchlist'), $self);
690             }
691             }
692              
693             sub refresh {
694             my ($self) = @_;
695             Gtk2::Ex::WidgetCursor->busy;
696             if (my $symlist = $self->{'symlist'}) {
697             require App::Chart::Gtk2::Job::Latest;
698             App::Chart::Gtk2::Job::Latest->start_symlist ($symlist);
699             }
700             }
701              
702             sub _do_symbol_menu_popup {
703             my ($treeview, $event) = @_;
704             if ($event->button == 3) {
705             require App::Chart::Gtk2::WatchlistSymbolMenu;
706             App::Chart::Gtk2::WatchlistSymbolMenu->popup_from_treeview ($event, $treeview);
707             }
708             return Gtk2::EVENT_PROPAGATE;
709             }
710              
711             sub _do_symlist_menu_popup {
712             # nothing yet ...
713             # my ($treeview, $event) = @_;
714             # my $self = $treeview->get_toplevel;
715             return Gtk2::EVENT_PROPAGATE;
716             }
717              
718             # 'query-tooltip' signal on symbols_treeview
719             sub _do_query_tooltip {
720             my ($treeview, $x, $y, $keyboard_tip, $tooltip) = @_;
721             # ### Watchlist _do_query_tooltip() "$x,$y"
722              
723             my ($bin_x, $bin_y, $model, $path, $iter)
724             = $treeview->get_tooltip_context ($x, $y, $keyboard_tip);
725             if (! defined $path) { return 0; }
726              
727             my $symbol = $model->get_value($iter, $model->COL_SYMBOL);
728             if (! defined $symbol) { return 0; }
729             require App::Chart::Latest;
730             my $latest = App::Chart::Latest->get ($symbol);
731              
732             require App::Chart::Database;
733             my $tip = $symbol;
734             if (my $name = ($latest->{'name'}
735             || App::Chart::Database->symbol_name ($symbol))) {
736             $tip .= ' - ' . $name;
737             }
738             $tip .= "\n";
739              
740             if (my $quote_date = $latest->{'quote_date'}) {
741             my $quote_time = $latest->{'quote_time'} || '';
742             $tip .= __x("Quote: {quote_date} {quote_time}",
743             quote_date => $quote_date,
744             quote_time => $quote_time);
745             $tip .= "\n";
746             }
747              
748             if (my $last_date = $latest->{'last_date'}) {
749             my $last_time = $latest->{'last_time'} || '';
750             $tip .= __x("Last: {last_date} {last_time}",
751             last_date => $last_date,
752             last_time => $last_time);
753             $tip .= "\n";
754             }
755              
756             $tip .= __x('{location} time; source {source}',
757             location => App::Chart::TZ->for_symbol($symbol)->name,
758             source => $latest->{'source'});
759              
760             ### $tip
761             $tooltip->set_text ($tip);
762             $treeview->set_tooltip_row ($tooltip, $path);
763             return 1;
764             }
765              
766             sub _do_symlist_entry_activate {
767             my ($entry_or_button) = @_;
768             my $self = $entry_or_button->get_toplevel;
769             my $treeview = $self->{'symlists_treeview'};
770             my $pos = treeview_pos_after_selected_or_top_of_visible ($treeview);
771              
772             my $entry = $self->{'symlist_entry'};
773             my $name = $entry->get_text;
774             require App::Chart::Gtk2::Symlist::User;
775             App::Chart::Gtk2::Symlist::User->add_symlist ($pos, $name);
776              
777             my $path = Gtk2::TreePath->new_from_indices ($pos);
778             Gtk2::Ex::TreeViewBits::scroll_cursor_to_path ($treeview, $path);
779             }
780              
781             # 'activate' signal handler on the Gtk2::Entry for a symbol
782             sub _do_symbol_entry_activate {
783             my ($entry_or_button) = @_;
784             my $self = $entry_or_button->get_toplevel;
785             my $treeview = $self->{'symbols_treeview'};
786             my $pos = treeview_pos_after_selected_or_top_of_visible ($treeview);
787              
788             my $entry = $self->{'symbol_entry'};
789             my $symlist = $self->{'symlist'};
790             if (! $symlist->can_edit) { # supposed to be insensitive anyway
791             $entry->error_bell;
792             return;
793             }
794              
795             # select text for ease of typing another
796             Gtk2::Ex::EntryBits::select_region_noclip ($entry, 0, -1);
797              
798             my $symbol = $entry->get_text;
799             if (my $path = $symlist->find_symbol_path ($symbol)) {
800             # already exists, move to it
801             Gtk2::Ex::TreeViewBits::scroll_cursor_to_path ($treeview, $path);
802             return;
803             }
804             $symlist->insert_with_values ($pos, 0=>$symbol);
805              
806             my $path = Gtk2::TreePath->new_from_indices ($pos);
807             Gtk2::Ex::TreeViewBits::scroll_cursor_to_path ($treeview, $path);
808              
809             # request this, everything else as extras
810             if ($symbol ne '') {
811             require App::Chart::Gtk2::Job::Latest;
812             App::Chart::Gtk2::Job::Latest->start ([$symbol]);
813             }
814             }
815              
816             #------------------------------------------------------------------------------
817             # generic helpers
818              
819             sub treeview_pos_after_selected_or_top_of_visible {
820             my ($treeview) = @_;
821              
822             my ($lo_path, $hi_path) = $treeview->get_visible_range;
823             my ($lo) = $lo_path ? $lo_path->get_indices : (-1);
824             my ($hi) = $hi_path ? $hi_path->get_indices : (-1);
825             my $pos = $lo + 1;
826              
827             my $selection = $treeview->get_selection;
828             my ($sel_path) = $selection->get_selected_rows;
829             if ($sel_path) {
830             my ($sel) = $sel_path->get_indices;
831             if ($sel >= $lo && $sel <= $hi) {
832             $pos = $sel + 1;
833             }
834             }
835             return $pos;
836             }
837              
838             #------------------------------------------------------------------------------
839              
840             sub main {
841             my ($class, $args) = @_;
842              
843             Gtk2->disable_setlocale; # leave LC_NUMERIC alone for version nums
844             Gtk2->init;
845              
846             require Gtk2::Ex::ErrorTextDialog::Handler;
847             Glib->install_exception_handler
848             (\&Gtk2::Ex::ErrorTextDialog::Handler::exception_handler);
849             ## no critic (RequireLocalizedPunctuationVars)
850             $SIG{'__WARN__'} = \&Gtk2::Ex::ErrorTextDialog::Handler::exception_handler;
851             ## use critic
852              
853             require App::Chart::Gtk2::TickerMain;
854             my $symlist = App::Chart::Gtk2::TickerMain::args_to_symlist ($args);
855              
856             my $self = $class->new;
857             $self->set (symlist => $symlist);
858             $self->signal_connect (destroy =>
859             \&App::Chart::Gtk2::TickerMain::_do_destroy_main_quit);
860             $self->show_all;
861             Gtk2->main;
862             }
863              
864             1;
865             __END__
866              
867             =for stopwords watchlist Watchlist Popup
868              
869             =head1 NAME
870              
871             App::Chart::Gtk2::WatchlistDialog -- watchlist dialog module
872              
873             =head1 SYNOPSIS
874              
875             use App::Chart::Gtk2::WatchlistDialog;
876              
877             =head1 WIDGET HIERARCHY
878              
879             C<App::Chart::Gtk2::WatchlistDialog> is a subclass of C<Gtk2::Dialog>.
880              
881             Gtk2::Widget
882             Gtk2::Container
883             Gtk2::Bin
884             Gtk2::Window
885             Gtk2::Dialog
886             App::Chart::Gtk2::WatchlistDialog
887              
888             =head1 DESCRIPTION
889              
890             A C<App::Chart::Gtk2::WatchlistDialog> widget is a watchlist display and dialog.
891              
892             =head1 FUNCTIONS
893              
894             =over 4
895              
896             =item C<< App::Chart::Gtk2::WatchlistDialog->new (key=>value,...) >>
897              
898             Create and return a new Watchlist dialog widget.
899              
900             =back
901              
902             =head1 SEE ALSO
903              
904             L<App::Chart::Gtk2::WatchlistSymbolMenu>
905              
906             =cut
907              
908             # App::Chart::Gtk2::WatchlistDialog->popup();
909             # =item C<< App::Chart::Gtk2::WatchlistDialog->popup () >>
910             #
911             # =item C<< App::Chart::Gtk2::WatchlistDialog->popup ($parent) >>
912             #
913             # Popup a C<Watchlist> dialog. This function creates a C<Watchlist> widget
914             # the first time it's called, and then on subsequent calls just presents that
915             # single dialog.
916             #
917             # If C<$parent> is supplied then a Watchlist on that display is sought, and if
918             # one is created then it's on the same screen as C<$parent>.
919