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