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