initial import
[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                                 objects.each { |o| o.alpha = (255 * v).to_i }
87                         end
88                 end
89         end
90
91         class MoveAnimator < ZeroToOneAnimator
92                 def initialize(duration, movement, *objects)
93                         zipped = objects.zip(objects.map { |o| o.geometry[0..1] })
94
95                         super(duration) do |v|
96                                 # decelerate
97                                 v = Math.sin(v * Math::PI / 2.0)
98
99                                 zipped.each do |(o, orig_pos)|
100                                         o.move(orig_pos.first,
101                                                orig_pos.last + (movement * v).to_i)
102                                 end
103                         end
104                 end
105         end
106
107         class MailboxIcon < Evas::Smart
108                 attr_accessor :slot
109
110                 def initialize(evas, label)
111                         super(evas)
112
113                         @slot = nil
114                         @alpha_anim = nil
115
116                         @img = Evas::Image.new(evas)
117                         @label = Evas::Text.new(evas)
118
119                         @objects = [@img, @label]
120
121                         @img.set_color(255, 255, 255, 0)
122                         @label.set_color(255, 255, 255, 0)
123                         @label.set_color(255, 0, 0, 0)
124
125                         @img.set_file(ICON_FILE)
126                         @img.set_fill(0, 0, *@img.get_size)
127
128                         @label.text = label
129                         @label.set_font("VeraBd", 10)
130
131                         resize(*@img.get_size)
132                 end
133
134                 def label
135                         @label.text
136                 end
137
138                 # smart callbacks
139                 def on_show
140                         @objects.each { |o| o.show }
141
142                         @alpha_anim ||= AlphaAnimator.new(2, @img, @label)
143                         @alpha_anim.on_finished { @alpha_anim = nil }
144                 end
145
146                 def on_hide
147                         @objects.each { |o| o.hide }
148
149                         @alpha_anim && @alpha_anim.delete
150                         @alpha_anim = nil
151                 end
152
153                 def on_delete
154                         @objects.each { |o| o.delete }
155                         @objects.clear
156
157                         @alpha_anim && @alpha_anim.delete
158
159                         @img = @label = @alpha_anim = nil
160                 end
161
162                 def on_layer_set(layer)
163                         @objects.each { |o| o.layer = layer }
164                 end
165
166                 def on_stack_above(other)
167                         @objects.each { |o| o.stack_above = other }
168                 end
169
170                 def on_stack_below(other)
171                         @objects.each { |o| o.stack_below = other }
172                 end
173
174                 def on_move(x, y)
175                         @objects.each { |o| o.move(x, y) }
176
177                         @label.center(self)
178                 end
179
180                 def on_resize(w, h)
181                         @img.resize(w, h)
182                 end
183         end
184
185         class FixedSizeArray < Array
186                 def initialize(siz)
187                         super
188                 end
189
190                 def each
191                         super { |item| yield item unless item.nil? }
192                 end
193
194                 def delete_at(i)
195                         self[i] = nil
196                 end
197
198                 undef :push
199                 undef :<<
200                 undef :unshift
201
202                 undef :pop
203                 undef :shift
204                 undef :delete
205         end
206
207         class Container < Evas::Smart
208                 class ContainerError < StandardError; end
209                 class ContainerFullError < ContainerError; end
210                 class ContainerLockedError < ContainerError; end
211
212                 include Enumerable
213
214                 def initialize(evas)
215                         super
216
217                         @bg = Evas::Rectangle.new(evas)
218                         @bg.set_color(0, 0, 0, 255)
219
220                         @icons = FixedSizeArray.new(MAX_ICONS)
221                         @about_to_add = []
222                         @animators = []
223
224                         @lock_count = 0
225                 end
226
227                 def each
228                         @icons.each { |i| yield i unless i.nil? }
229                 end
230
231                 def <<(i)
232                         Kernel.raise(ContainerFullError) if slots_left.zero?
233                         Kernel.raise(ContainerLockedError) if @lock_count > 0
234
235                         i.move_relative(self, 0, 0)
236                         i.slot = next_slot
237                         i.clip = self
238                         i.show
239
240                         # check whether we need to need to move this icon
241                         if slots_left == 1
242                                 @icons[i.slot] = i
243                                 return
244                         end
245
246                         movement = Main.instance.icon_height * (slots_left - 1)
247
248                         @about_to_add << i
249
250                         move_time = 0.25 * slots_left
251                         @animators << MoveAnimator.new(move_time, movement, i)
252
253                         @animators.last.on_finished do |ani|
254                                 @animators.delete(ani)
255                                 @icons[i.slot] = i
256
257                                 # FIXME check whether we can always shift the array instead
258                                 #puts "really added #{i.label} now (slot #{i.slot})"
259                                 @about_to_add.delete(i)
260                         end
261                 end
262
263                 def delete(icon)
264                         # icons that are placed above the one that's deleted need
265                         # to be moved
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                         @icons[i].delete
274                         @icons.delete_at(i)
275
276                         ar = @icons[i..-1].reject { |i| i.nil? }
277                         return if ar.nil?
278
279                         @lock_count += 1
280
281                         @animators << MoveAnimator.new(2, Main.instance.icon_height, *ar)
282                         @animators.last.on_finished do |ani|
283                                 @animators.delete(ani)
284                                 @lock_count -= 1
285                         end
286                 end
287
288                 def length
289                         @icons.nitems
290                 end
291
292                 # smart callbacks
293                 def on_show
294                         @bg.show
295                 end
296
297                 def on_hide
298                         @bg.hide
299                 end
300
301                 def on_delete
302                         @bg.delete
303                         @bg = nil
304                 end
305
306                 def on_layer_set(layer)
307                         @bg.layer = layer
308                 end
309
310                 def on_stack_above(other)
311                         @bg.stack_above = other
312                 end
313
314                 def on_stack_below(other)
315                         @bg.stack_below = other
316                 end
317
318                 def on_move(x, y)
319                         @bg.move(x, y)
320                 end
321
322                 def on_resize(w, h)
323                         @bg.resize(w, h)
324                 end
325
326                 private
327                 def slots_left
328                         MAX_ICONS - @icons.nitems - @about_to_add.length
329                 end
330
331                 def next_slot
332                         @icons.nitems + @about_to_add.length
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.title = "blah"
345                         self.borderless = true
346
347                         @icon_dim = IO.read(ICON_FILE, 8, 16).unpack("NN")
348
349                         self.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                         @server = nil
371                         @timer = Ecore::Timer.new(30, &method(:on_timer))
372                         on_timer
373                 end
374
375                 def icon_height
376                         @icon_dim.last
377                 end
378
379                 def run
380                         Ecore.main_loop_begin
381                 end
382
383                 private
384                 def add_icon(name)
385                         @container << MailboxIcon.new(evas, name)
386                 end
387
388                 def on_timer
389                         return unless @server.nil?
390
391                         mboxes = %w{
392                                 INBOX
393                                 Lists.ba-2005
394                                 Lists.blackbox-devel
395                                 Lists.clc-devel
396                                 Lists.crux
397                                 Lists.cruxcon
398                                 Lists.dri-devel
399                                 Lists.dri-users
400                                 Lists.enlightenment-cvs
401                                 Lists.enlightenment-devel
402                                 Lists.hobix
403                                 Lists.mesa3d-dev
404                                 Lists.ruby-core
405                                 Lists.rubygems-devel
406                                 Lists.vim-ruby-devel
407                                 Lists.xmms2-devel
408                                 Lists.xorg
409                         }
410
411                         s = File.expand_path("~/.e/apps/embrace/config.yaml")
412
413                         File.open(s) do |f|
414                                 @server = IMAP::Session.new(YAML.load(f), mboxes)
415                         end
416
417                         true
418                 end
419
420                 def on_mailbox_status(ev)
421                         md = ev.name.match(/^Lists.(.+)$/)
422                         if md.nil?
423                                 lbl = ev.name
424                         else
425                                 lbl = md.captures.first
426                         end
427
428                         found = @container.find { |i| i.label == lbl }
429
430                         begin
431                                 if ev.count.zero?
432                                         unless found.nil?
433                                                 #puts "removing icon #{lbl}"
434                                                 @container.delete(found)
435                                         else
436                                                 #puts "count == 0, but icon not found (#{lbl})"
437                                         end
438                                 elsif found.nil?
439                                         #puts "adding icon #{lbl}"
440                                         add_icon(lbl)
441                                 else
442                                         #puts "count > 0, but already there (#{lbl})"
443                                 end
444                         rescue Exception => e
445                                 puts e.message
446                         end
447
448                         false
449                 end
450
451                 def on_finished(ev)
452                         @server = nil
453                 end
454         end
455 end
456
457 Embrace::Main.instance.show
458 Embrace::Main.instance.run