# Copyright 2006, 2007, 2008, 2009, 2010, 2011 Kevin Ryde
# This file is part of Chart.
#
# Chart is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3, or (at your option) any later version.
#
# Chart is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with Chart. If not, see .
package App::Chart::Gtk2::Ticker;
use 5.008;
use strict;
use warnings;
use Carp;
use Gtk2 1.200; # for working TreeModelFilter modify_func
use Gtk2::Ex::TickerView;
use List::Util qw(min max);
use Locale::TextDomain 'App-Chart';
use App::Chart::Glib::Ex::MoreUtils;
use Glib::Ex::SignalIds;
use Gtk2::Ex::Units;
use App::Chart;
use App::Chart::Gtk2::GUI;
use App::Chart::Gtk2::Symlist;
# uncomment this to run the ### lines
#use Smart::Comments;
BEGIN {
Glib::Type->register_enum ('App::Chart::Gtk2::Ticker::menu_position',
'centre' => 0,
'pointer' => 1);
}
use Glib::Object::Subclass
'Gtk2::Ex::TickerView',
signals => { button_press_event => \&_do_button_press_event,
menu_created => { param_types => ['Gtk2::Menu'],
return_type => undef,
},
menu_popup => { param_types => ['Glib::Int',
'App::Chart::Gtk2::Ticker::menu_position'],
return_type => undef,
class_closure => \&_do_menu_popup_action,
flags => [ 'run-last', 'action' ],
},
},
properties => [Glib::ParamSpec->object
('symlist',
'symlist',
'App::Chart::Gtk2::Symlist object for the symbols to display.',
# App::Chart::Gtk2::Symlist::Join is a ListModelConcat not
# a Symlist subclass, allow that by just Glib::Object here
'Glib::Object',
Glib::G_PARAM_READWRITE),
];
App::Chart::Gtk2::GUI::chart_style_class (__PACKAGE__);
# priority level "gtk" treating this as widget level default, for overriding
# by application or user RC
Gtk2::Rc->parse_string (<<'HERE');
binding "App__Chart__Gtk2__Ticker_keys" {
bind "F10" { "menu_popup" (0, centre) }
bind "Pointer_Button3" { "menu_popup" (3, pointer) }
}
class "App__Chart__Gtk2__Ticker" binding:gtk "App__Chart__Gtk2__Ticker_keys"
HERE
sub INIT_INSTANCE {
my ($self) = @_;
$self->add_events (['button-press-mask', 'key-press-mask']);
$self->set (fixed_height_mode => 1);
$self->set_flags ('can-focus');
my $renderer = Gtk2::CellRendererText->new;
$renderer->set (xpad => Gtk2::Ex::Units::em($self));
$self->pack_start ($renderer, 0);
$self->set_attributes ($renderer, markup => 0);
require App::Chart::Gtk2::Symlist::Favourites;
my $symlist = App::Chart::Gtk2::Symlist::Favourites->instance;
if ($symlist->is_empty) {
require App::Chart::Gtk2::Symlist::All;
my $all = App::Chart::Gtk2::Symlist::All->instance;
if (! $all->is_empty) {
$symlist = $all;
}
}
$self->set (symlist => $symlist);
}
sub FINALIZE_INSTANCE {
my ($self) = @_;
# Gtk2::Menu doesn't go away just by weakening if currently popped-up
# (because it's then a toplevel presumably); doing ->popdown() works, but
# ->destroy() seems the best idea
if (my $menu = $self->{'menu'}) {
$menu->destroy;
}
}
sub SET_PROPERTY {
my ($self, $pspec, $newval) = @_;
my $pname = $pspec->get_name;
my $oldval = $self->{$pname};
if ($pname eq 'symlist' && ($newval||0) != ($oldval||0)) {
### Ticker: "$self changed symlist"
my $symlist = $newval;
if (defined $symlist && ! $symlist->isa('App::Chart::Gtk2::Symlist')) {
croak "App::Chart::Gtk2::Ticker.symlist must be a App::Chart::Gtk2::Symlist";
}
$self->{$pname} = $newval; # per default GET_PROPERTY
require App::Chart::Gtk2::TickerModel;
my $model = $self->{'symlist_model'}
= $symlist && App::Chart::Gtk2::TickerModel->new ($symlist);
my $ref_weak_self = App::Chart::Glib::Ex::MoreUtils::ref_weak ($self);
$self->{'symlist_model_ids'} = $model && do {
Glib::Ex::SignalIds->new
($model,
$model->signal_connect (row_deleted => \&_do_row_deleted,
$ref_weak_self),
$model->signal_connect (row_inserted => \&_do_row_inserted,
$ref_weak_self))
};
$self->set (model => $model);
$self->{'showing_empty'} = 0;
_check_empty ($self);
} else {
$self->{$pname} = $newval; # per default GET_PROPERTY
}
}
# 'button-press-event' class closure
sub _do_button_press_event {
my ($self, $event) = @_;
require App::Chart::Gtk2::Ex::BindingBits;
App::Chart::Gtk2::Ex::BindingBits::activate_button_event
('App__Chart__Gtk2__Ticker_keys', $event, $self);
return shift->signal_chain_from_overridden(@_);
}
# 'menu-popup' action signal class closure
sub _do_menu_popup_action {
my ($self, $button, $where) = @_;
### Ticker: "menu-popup action $button $where"
my $position_func; # undef for mouse position if $where empty or undef
if ($where && $where ne 'pointer') {
$position_func = $self->can("menu_position_func_$where");
unless ($position_func) {
Glib->warning (undef, warn "Ticker: unrecognised menu position '$where', default to mouse pointer");
}
}
my $symbol;
if (! $where || $where eq 'pointer') {
my $event = Gtk2->get_current_event;
require Gtk2::Ex::WidgetBits;
if (my ($x,$y) = Gtk2::Ex::WidgetBits::xy_root_to_widget ($self,
$event->x_root,
$event->y_root)) {
if (my $path = $self->get_path_at_pos ($x, $y)) {
if (my $model = $self->get('symlist')) {
if (my $iter = $model->get_iter ($path)) {
$symbol = $model->get($iter, $model->COL_SYMBOL);
}
}
}
}
}
my $menu = $self->menu;
$menu->set_screen ($self->get_screen);
$menu->set (symbol => $symbol);
$menu->popup (undef, # parent menushell
undef, # parent menuitem
$position_func,
App::Chart::Glib::Ex::MoreUtils::ref_weak($self), # position func data
$button,
Gtk2->get_current_event_time);
}
# 'row-deleted' signal on symlist model
sub _do_row_deleted {
my ($model, $path, $ref_weak_self) = @_;
my $self = $$ref_weak_self || return;
_check_empty ($self);
}
# 'row-inserted' signal on symlist model
sub _do_row_inserted {
my ($model, $path, $iter, $ref_weak_self) = @_;
my $self = $$ref_weak_self || return;
_check_empty ($self);
}
sub _check_empty {
my ($self) = @_;
my $symlist = $self->{'symlist'};
my $empty = !$symlist || $symlist->is_empty;
if ($empty && ! $self->{'showing_empty'}) {
# become empty
my $empty_model = ($self->{'empty_model'} ||= do {
my $liststore = Gtk2::ListStore->new ('Glib::String');
$liststore->append;
$liststore;
});
my $message = $symlist
? __x('{symlistname} list is empty ... ',
symlistname => $symlist->name)
: __('No symlist ... ');
$empty_model->set_value ($empty_model->get_iter_first,
0 => $message);
$self->set (model => $empty_model);
$self->{'showing_empty'} = 1;
} elsif ($self->{'showing_empty'} && ! $empty) {
$self->set (model => $self->{'symlist_model'});
$self->{'showing_empty'} = 0;
}
}
# create and return Gtk2::Menu widget
sub menu {
my ($self) = @_;
return ($self->{'menu'} ||= do {
### Ticker menu doesn't exist, creating
require App::Chart::Gtk2::TickerMenu;
my $menu = App::Chart::Gtk2::TickerMenu->new (ticker => $self);
$self->signal_emit ('menu-created', $menu);
$menu;
});
}
# and also 'activate' signal on Help menu item
sub help {
require App::Chart::Manual;
App::Chart::Manual->open(__p('manual-node','Ticker'));
}
#------------------------------------------------------------------------------
# position funcs
sub menu_position_func_centre {
require Gtk2::Ex::MenuBits;
goto &Gtk2::Ex::MenuBits::position_widget_topcentre;
}
1;
__END__
=for stopwords symlist submenu Symlist boolean ie
=head1 NAME
App::Chart::Gtk2::Ticker -- stock ticker widget
=head1 SYNOPSIS
use App::Chart::Gtk2::Ticker;
my $ticker = App::Chart::Gtk2::Ticker->new;
my $symlist = App::Chart::Gtk2::Symlist::All->instance;
$ticker->set (symlist => $symlist);
=head1 WIDGET HIERARCHY
C is a subclass of C,
Gtk2::Widget
...
Gtk2::Ex::TickerView
App::Chart::Gtk2::Ticker
=head1 DESCRIPTION
A C widget showing stock quotes scrolling across the
window for a given symlist (L)
=head1 FUNCTIONS
=over 4
=item C<< App::Chart::Gtk2::Ticker->new (key => value, ...) >>
Create and return a C widget. Optional key/value pairs
can be given to set initial properties as per C<< Glib::Object->new >>.
=item C<< $ticker->menu() >>
Return the C which is popped up by mouse button 3 in the ticker.
An application can add items to this, such as "Hide" or "Quit", or perhaps a
submenu to change what's displayed.
=item C<< $ticker->refresh() >>
Download fresh prices for the symbols displayed. This is the "Refresh" item
in the button-3 menu.
=item C<< $ticker->help() >>
Open the Chart manual at the section on the ticker. This is the "Help" item
in the button-3 menu.
=back
=head1 PROPERTIES
=over 4
=item symlist (C, default favourites or all)
A Symlist object which is the stock symbols to display.
=back
=head1 SEE ALSO
L, L, C
=cut