Fade out icons before removing them.
[embrace.git] / bin / embrace
1 #!/usr/bin/env ruby
2
3 # vim:syn=ruby
4 #
5 # Copyright (c) 2006 Tilman Sauerbeck (tilman at code-monkey de)
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of version 2 of the GNU General Public License as
9 # published by the Free Software Foundation.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software Foundation,
18 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
19
20 require "ecore"
21 require "ecore_x"
22 require "ecore_evas"
23 require "singleton"
24 require "yaml"
25 require "embrace/imap"
26
27 PKG_NAME = "embrace"
28 DATADIR = "/usr/local/share/#{PKG_NAME}/"
29
30 class Evas::EvasObject
31         def move_relative(obj, x, y)
32                 # FIXME investigate whether there's an easier way
33                 move(*obj.geometry[0..1].zip([x, y]).map { |(a, b)| a + b })
34         end
35
36         def center(obj)
37                 a = geometry
38                 b = obj.geometry
39
40                 move_relative(obj, (b[2] / 2) - (a[2] / 2),
41                                    (b[3] / 2) - (a[3] / 2))
42         end
43
44         def alpha=(alpha)
45                 set_color(*(get_color[0..-2] << alpha))
46         end
47 end
48
49 module Embrace
50         VERSION = "0.0.1"
51         ICON_FILE = DATADIR + "l33t_MAI_envelope.png"
52         MAX_ICONS = 11
53
54         class ZeroToOneAnimator < Ecore::Animator
55                 def initialize(duration)
56                         @finished = nil
57
58                         started = Time.now
59
60                         super() do
61                                 v = [(Time.now - started) / duration, 1.0].min
62
63                                 yield v
64
65                                 @finished.call(self) unless (v < 1.0) || @finished.nil?
66                                 v < 1.0
67                         end
68                 end
69
70                 def delete
71                         super
72
73                         @finished = nil
74                 end
75
76                 def on_finished(&block)
77                         @finished = block
78                 end
79         end
80
81         # an animator that runs for the specified number of seconds,
82         # and yields values between 0 and 255
83         class AlphaAnimator < ZeroToOneAnimator
84                 def initialize(duration, *objects)
85                         super(duration) do |v|
86                                 a = compute_alpha(v)
87                                 objects.each { |o| o.alpha = a }
88                         end
89                 end
90
91                 def compute_alpha(v)
92                         (255 * v).to_i
93                 end
94         end
95
96         class InverseAlphaAnimator < AlphaAnimator
97                 def compute_alpha(v)
98                         super((1.0 - v).abs)
99                 end
100         end
101
102         class MoveAnimator < ZeroToOneAnimator
103                 def initialize(duration, movement, *objects)
104                         zipped = objects.zip(objects.map { |o| o.geometry[0..1] })
105
106                         super(duration) do |v|
107                                 # decelerate
108                                 v = Math.sin(v * Math::PI / 2.0)
109
110                                 zipped.each do |(o, orig_pos)|
111                                         o.move(orig_pos.first,
112                                                orig_pos.last + (movement * v).to_i)
113                                 end
114                         end
115                 end
116         end
117
118         class MailboxIcon < Evas::Smart
119                 class FadeOutFinishedEvent < Ecore::Event
120                         attr_reader :icon
121
122                         def initialize(icon)
123                                 super()
124
125                                 @icon = icon
126                         end
127                 end
128
129                 attr_accessor :slot
130
131                 def initialize(evas, label)
132                         super(evas)
133
134                         self.name = label
135
136                         @slot = nil
137                         @alpha_anim = nil
138
139                         @img = Evas::Image.new(evas)
140                         @label = Evas::Text.new(evas)
141
142                         @objects = [@img, @label]
143                         @objects.each { |o| add_member(o) }
144
145                         set_color(255, 255, 255, 0)
146                         @img.set_color(255, 255, 255, 0)
147                         @label.set_color(255, 0, 0, 0)
148
149                         @img.set_file(ICON_FILE)
150                         @img.set_fill(0, 0, *@img.get_size)
151
152                         @label.text = name
153                         @label.set_font("VeraBd", 10)
154
155                         resize(*@img.get_size)
156                 end
157
158                 def fade_in
159                         show
160
161                         @alpha_anim ||= AlphaAnimator.new(2, self)
162                         @alpha_anim.on_finished { @alpha_anim = nil }
163                 end
164
165                 def fade_out
166                         @alpha_anim ||= InverseAlphaAnimator.new(2, self)
167                         @alpha_anim.on_finished do
168                                 @alpha_anim = nil
169                                 FadeOutFinishedEvent.raise(self)
170                         end
171                 end
172
173                 # smart callbacks
174                 def smart_show
175                         @objects.each { |o| o.show }
176                 end
177
178                 def smart_hide
179                         @objects.each { |o| o.hide }
180
181                         @alpha_anim && @alpha_anim.delete
182                         @alpha_anim = nil
183                 end
184
185                 def smart_delete
186                         @objects.each { |o| o.delete }
187                         @objects.clear
188
189                         @alpha_anim && @alpha_anim.delete
190
191                         @img = @label = @alpha_anim = nil
192                 end
193
194                 def smart_move(x, y)
195                         @objects.each { |o| o.move(x, y) }
196
197                         @label.center(self)
198                 end
199
200                 def smart_resize(w, h)
201                         @img.resize(w, h)
202                 end
203
204                 def smart_color_set(r, g, b, a)
205                         @objects.each { |o| o.alpha = a }
206                 end
207         end
208
209         class Container < Evas::Smart
210                 class ContainerError < StandardError; end
211                 class ContainerFullError < ContainerError; end
212                 class ContainerLockedError < ContainerError; end
213
214                 def initialize(evas)
215                         super
216
217                         @bg = Evas::Rectangle.new(evas)
218                         @bg.set_color(0, 0, 0, 8)
219
220                         add_member(@bg)
221
222                         @icons = []
223                         @animators = []
224
225                         @about_to_add = 0
226                         @add_lock_count = 0
227
228                         @handlers = [
229                                 Ecore::EventHandler.new(MailboxIcon::FadeOutFinishedEvent,
230                                                         &method(:on_icon_fade_out_finished))
231                         ]
232                 end
233
234                 def can_add?
235                         !slots_left.zero? && @add_lock_count.zero?
236                 end
237
238                 def can_delete?
239                         @about_to_add.zero? && @add_lock_count.zero?
240                 end
241
242                 def <<(i)
243                         Kernel.raise(ContainerFullError) if slots_left.zero?
244                         Kernel.raise(ContainerLockedError) unless @add_lock_count.zero?
245
246                         i.move_relative(self, 0, 0)
247                         i.slot = next_slot
248                         i.clip = self
249                         i.fade_in
250
251                         # check whether we need to need to move this icon
252                         if slots_left == 1
253                                 @icons[i.slot] = i
254                                 return
255                         end
256
257                         movement = Main.instance.icon_height * (slots_left - 1)
258
259                         @about_to_add += 1
260
261                         move_time = 0.25 * slots_left
262                         @animators << MoveAnimator.new(move_time, movement, i)
263
264                         @animators.last.on_finished do |ani|
265                                 @animators.delete(ani)
266                                 @icons[i.slot] = i
267
268                                 @about_to_add -= 1
269                         end
270                 end
271
272                 def delete(icon)
273                         i = @icons.index(icon)
274                         return (block_given? ? yield : nil) if i.nil?
275
276                         delete_at(i)
277                 end
278
279                 def delete_at(i)
280                         Kernel.raise(ContainerLockedError) unless @about_to_add.zero?
281                         Kernel.raise(ContainerLockedError) unless @add_lock_count.zero?
282
283                         @add_lock_count += 1
284                         @icons[i].fade_out
285                 end
286
287                 def on_icon_fade_out_finished(ev)
288                         i = @icons.index(ev.icon)
289                         ev.icon.delete
290                         @icons.delete_at(i)
291
292                         # icons that are placed above the one that's deleted need
293                         # to be moved. check whether are there any first
294                         if i == @icons.length
295                                 @add_lock_count -= 1
296                                 return
297                         end
298
299                         ar = @icons[i..-1]
300
301                         @animators << MoveAnimator.new(2, Main.instance.icon_height, *ar)
302                         @animators.last.on_finished do |ani|
303                                 @animators.delete(ani)
304                                 @add_lock_count -= 1
305                         end
306                 end
307
308                 # smart callbacks
309                 def smart_show
310                         @bg.show
311                 end
312
313                 def smart_hide
314                         @bg.hide
315                 end
316
317                 def smart_delete
318                         @bg.delete
319                         @bg = nil
320                 end
321
322                 def smart_move(x, y)
323                         @bg.move(x, y)
324                 end
325
326                 def smart_resize(w, h)
327                         @bg.resize(w, h)
328                 end
329
330                 private
331                 def slots_left
332                         MAX_ICONS - @icons.nitems - @about_to_add
333                 end
334
335                 def next_slot
336                         @icons.nitems + @about_to_add
337                 end
338         end
339
340         class Main < Ecore::Evas::SoftwareX11
341                 include Singleton
342
343                 attr_reader :container
344
345                 def initialize
346                         super
347
348                         self.has_alpha = true
349                         self.title = "Embrace"
350                         self.borderless = true
351
352                         @icon_dim = IO.read(ICON_FILE, 8, 16).unpack("NN")
353
354                         on_resize { @container.resize(*geometry[2, 3]) }
355
356                         @container = Container.new(evas)
357                         @container.move(0, 0)
358                         @container.layer = -1
359                         @container.show
360
361                         size = [@icon_dim.first, icon_height * MAX_ICONS]
362                         resize(*size)
363                         set_size_min(*size)
364                         set_size_max(*size)
365
366                         evas.font_path_append("/usr/lib/X11/fonts/TTF")
367
368                         @handlers = [
369                                 Ecore::EventHandler.new(IMAP::MailboxStatusEvent,
370                                                         &method(:on_mailbox_status)),
371                                 Ecore::EventHandler.new(IMAP::FinishedEvent,
372                                                         &method(:on_finished))
373                         ]
374
375                         s = File.expand_path("~/.e/apps/embrace/config.yaml")
376                         @config = YAML.load(File.read(s))
377
378                         @server = nil
379                         @timer = Ecore::Timer.new(30, &method(:on_timer))
380                         on_timer
381                 end
382
383                 def icon_height
384                         @icon_dim.last
385                 end
386
387                 def run
388                         Ecore.main_loop_begin
389                 end
390
391                 private
392                 def on_timer
393                         return unless @server.nil?
394
395                         @server = IMAP::Session.new(@config)
396
397                         true
398                 end
399
400                 def on_mailbox_status(ev)
401                         md = ev.name.match(/^Lists.(.+)$/)
402                         if md.nil?
403                                 lbl = ev.name
404                         else
405                                 lbl = md.captures.first
406                         end
407
408                         found = evas.find_object(lbl)
409
410                         if ev.count.zero? && !found.nil? && @container.can_delete?
411                                 @container.delete(found)
412                         elsif !ev.count.zero? && found.nil? && @container.can_add?
413                                 @container << MailboxIcon.new(evas, lbl)
414                         end
415
416                         false
417                 end
418
419                 def on_finished(ev)
420                         @server = nil
421                 end
422         end
423 end
424
425 Embrace::Main.instance.show
426 Embrace::Main.instance.run