082c17e673a1971976b77e1bec43a6fe56e0536d
[picture-display.git] / lib / Display / Notifications.pm
1 package Display::Notifications;
2
3 use Clutter;
4 use POE::Session;
5 use Set::Object;
6 use strict;
7
8 my $delay = 15;
9
10 sub new {
11   my ($proto, $kernel, $session, $stage) = @_;
12   my $class = ref($proto) || $proto;
13
14   my $self = {
15     'kernel' => $kernel,
16     'session' => $session,
17     'stage' => $stage,
18     'moving' => 0,
19   };
20
21   $self->{'blocks'} = Set::Object->new();
22   @{ $self->{'new'} } = ();
23   @{ $self->{'paths'} } = ();
24   @{ $self->{'pending_move'} } = ();
25
26   bless ($self, $class);
27
28   $self->{'kernel'}->state('notifications_add', $self, 'add');
29   $self->{'kernel'}->state('notifications_expire', $self, 'expire');
30
31   return $self;
32 }
33
34 sub add {
35   my ($self, $kernel, $notification, $logo_file) = @_[OBJECT, KERNEL, ARG0, ARG1];
36
37   my $text = undef;
38   if ($notification->isa("Clutter::Label")) {
39     $text = $notification;
40   } else {
41     $text = Clutter::Label->new('Sans 20', $notification);
42     $text->set_color(Clutter::Color->parse('White'));
43     $text->set_ellipsize('end');
44   }
45
46   my $block = Clutter::Group->new();
47   $block->set_opacity(0);
48
49   my $bg = Clutter::Rectangle->new(Clutter::Color->parse('Black'));
50   $bg->set_width($self->{'stage'}->get_width() - 20);
51   $bg->set_opacity(100);
52
53   $bg->set_height($text->get_height() + 10);
54
55   $text->set_position(5, 5);
56
57   $block->set_height($bg->get_height());
58   $block->set_anchor_point(1, $block->get_height());
59   $block->set_position(10, $self->{'stage'}->get_height() - 10);
60
61   $block->add($bg);
62   $block->add($text);
63
64   if (defined $logo_file) {
65     my $logo = Clutter::Texture->new($logo_file);
66     $block->add($logo);
67     $logo->set('keep-aspect-ratio' => 1);
68     $logo->set('sync-size' => 1);
69     $logo->set_height($text->get_height());
70     $logo->set_position(5, 5);
71
72     $text->set_width($bg->get_width() - $logo->get_width() - 20 - 20);
73     $text->set_x(20 + $logo->get_width() + 20);
74   } else {
75     $text->set_width($bg->get_width() - 20);
76     $text->set_x(20);
77   }
78
79   if ($self->{'blocks'}->size() > 0) {
80     if (! $self->{'moving'}) {
81       push @{ $self->{'new'} }, $block;
82
83       $self->move_blocks();
84     } else {
85       push @{ $self->{'pending_move'} }, $block;
86     }
87   } else {
88     push @{ $self->{'new'} }, $block;
89
90     fade_in(undef, $self);
91   }
92 }
93
94 sub expire {
95   my ($self) = @_[OBJECT];
96
97   for my $block ($self->{'blocks'}->members()) {
98     if ($block->{'expire'} <= time()) {
99       my $old_effect =
100         Clutter::EffectTemplate->new_for_duration(1000,
101                                                   'main::smoothstep_inc' );
102       my $old =
103         Clutter::Effect->fade($old_effect, $block->{'block'}, 0,
104                               $self->can('post_fade_out'), $self);
105       $old->start();
106     }
107   }
108
109   # What a hack!  It seems that multiple alarm_ardds aren't being
110   # honoured.  So, we check and see when the next expire should run, and
111   # call ourselves again then.
112   while (scalar(@{ $self->{'expire_times'} }) > 0
113       && $self->{'expire_times'}[0] <= time()) {
114     my $old = shift @{ $self->{'expire_times'} };
115   }
116
117   if (defined $self->{'expire_times'}[0]) {
118     my $timer = shift @{ $self->{'expire_times'} };
119     $self->{'kernel'}->alarm_add('notifications_expire', $timer);
120   }
121
122   return 1;
123 }
124
125 sub post_fade_out {
126   my ($old_timeline, $self) = @_;
127
128   for my $block ($self->{'blocks'}->members()) {
129     if ($block->{'expire'} <= time()) {
130       $self->{'blocks'}->remove($block);
131
132       $block->{'block'}->remove_all()
133     }
134   }
135 }
136
137 sub fade_in {
138   my ($score, $self) = @_;
139
140   # Clean out no longer required state.
141   $self->{'paths'} = ();
142
143   while (my $block = shift @{ $self->{'new'} }) {
144     if (defined $block) {
145       $self->{'stage'}->add($block);
146       my $effect = Clutter::EffectTemplate->new_for_duration(1000, 'main::smoothstep_inc');
147       my $timeline = Clutter::Effect->fade($effect, $block, 255);
148
149       my $expire = time() + $delay;
150
151       $self->{'blocks'}->insert( {
152         'expire' => $expire, 
153         'block'  => $block
154       });
155
156       $self->{'kernel'}->delay_add('notifications_expire', $delay);
157       push @{ $self->{'expire_times'} }, $expire;
158
159       $timeline->start();
160       $self->{'moving'} = 0;
161     }
162   }
163
164   # Check and see if there are any blocks that have been added since we
165   # started moving, and start the process of showing them.
166   if (scalar(@{ $self->{'pending_move'} }) > 0) {
167     while (my $block = shift @{ $self->{'pending_move'} }) {
168       push @{ $self->{'new'} }, $block;
169     }
170
171     $self->move_blocks();
172   }
173 }
174
175 sub move_blocks {
176   my ($self) = @_;
177
178   # We're in the process of moving blocks.
179   $self->{'moving'} = 1;
180
181   my $score = Clutter::Score->new();
182   $score->signal_connect('completed' => $self->can('fade_in'), $self);
183
184   # Work out the total height
185   my $height = 10;
186   for my $new (@{ $self->{'new'} }) {
187     $height += $new->get_height();
188   }
189
190   for my $obj ($self->{'blocks'}->members()) {
191     my $block = $obj->{'block'};
192     my $timeline = Clutter::Timeline->new_for_duration(1000);
193     my $alpha = Clutter::Alpha->new($timeline, 'main::smoothstep_inc');
194
195     my $path = Clutter::Behaviour::Path->new($alpha,
196       [ $block->get_position() ],
197       [ $block->get_x(), $block->get_y() - $height ]);
198     $path->apply($block);
199
200     push @{ $self->{'paths'} }, $path;
201
202     $score->append(undef, $timeline);
203   }
204
205   $score->start();
206   $self->{'score'} = $score;
207 }
208
209 1;