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