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