use EvasObject#name to identify icons
[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                         self.name = label
114
115                         @slot = nil
116                         @alpha_anim = nil
117
118                         @img = Evas::Image.new(evas)
119                         @label = Evas::Text.new(evas)
120
121                         @objects = [@img, @label]
122                         @objects.each { |o| add_member(o) }
123
124                         @img.set_color(255, 255, 255, 0)
125                         @label.set_color(255, 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                         resize(*@img.get_size)
134                 end
135
136                 # smart callbacks
137                 def on_show
138                         @objects.each { |o| o.show }
139
140                         @alpha_anim ||= AlphaAnimator.new(2, @img, @label)
141                         @alpha_anim.on_finished { @alpha_anim = nil }
142                 end
143
144                 def on_hide
145                         @objects.each { |o| o.hide }
146
147                         @alpha_anim && @alpha_anim.delete
148                         @alpha_anim = nil
149                 end
150
151                 def on_delete
152                         @objects.each { |o| o.delete }
153                         @objects.clear
154
155                         @alpha_anim && @alpha_anim.delete
156
157                         @img = @label = @alpha_anim = nil
158                 end
159
160                 def on_move(x, y)
161                         @objects.each { |o| o.move(x, y) }
162
163                         @label.center(self)
164                 end
165
166                 def on_resize(w, h)
167                         @img.resize(w, h)
168                 end
169         end
170
171         class Container < Evas::Smart
172                 class ContainerError < StandardError; end
173                 class ContainerFullError < ContainerError; end
174                 class ContainerLockedError < ContainerError; end
175
176                 def initialize(evas)
177                         super
178
179                         @bg = Evas::Rectangle.new(evas)
180                         @bg.set_color(0, 0, 0, 255)
181
182                         add_member(@bg)
183
184                         @icons = []
185                         @animators = []
186
187                         @about_to_add = 0
188                         @add_lock_count = 0
189                 end
190
191                 def <<(i)
192                         Kernel.raise(ContainerFullError) if slots_left.zero?
193                         Kernel.raise(ContainerLockedError) unless @add_lock_count.zero?
194
195                         i.move_relative(self, 0, 0)
196                         i.slot = next_slot
197                         i.clip = self
198                         i.show
199
200                         # check whether we need to need to move this icon
201                         if slots_left == 1
202                                 @icons[i.slot] = i
203                                 return
204                         end
205
206                         movement = Main.instance.icon_height * (slots_left - 1)
207
208                         @about_to_add += 1
209
210                         move_time = 0.25 * slots_left
211                         @animators << MoveAnimator.new(move_time, movement, i)
212
213                         @animators.last.on_finished do |ani|
214                                 @animators.delete(ani)
215                                 @icons[i.slot] = i
216
217                                 @about_to_add -= 1
218                         end
219                 end
220
221                 def delete(icon)
222                         i = @icons.index(icon)
223                         return (block_given? ? yield : nil) if i.nil?
224
225                         delete_at(i)
226                 end
227
228                 def delete_at(i)
229                         Kernel.raise(ContainerLockedError) unless @about_to_add.zero?
230                         Kernel.raise(ContainerLockedError) unless @add_lock_count.zero?
231
232                         # icons that are placed above the one that's deleted need
233                         # to be moved
234                         ar = @icons[(i + 1)..-1]
235
236                         @icons[i].delete
237                         @icons.delete_at(i)
238
239                         return if ar.empty?
240
241                         @add_lock_count += 1
242
243                         @animators << MoveAnimator.new(2, Main.instance.icon_height, *ar)
244                         @animators.last.on_finished do |ani|
245                                 @animators.delete(ani)
246                                 @add_lock_count -= 1
247                         end
248                 end
249
250                 # smart callbacks
251                 def on_show
252                         @bg.show
253                 end
254
255                 def on_hide
256                         @bg.hide
257                 end
258
259                 def on_delete
260                         @bg.delete
261                         @bg = nil
262                 end
263
264                 def on_move(x, y)
265                         @bg.move(x, y)
266                 end
267
268                 def on_resize(w, h)
269                         @bg.resize(w, h)
270                 end
271
272                 private
273                 def slots_left
274                         MAX_ICONS - @icons.nitems - @about_to_add
275                 end
276
277                 def next_slot
278                         @icons.nitems + @about_to_add
279                 end
280         end
281
282         class Main < Ecore::Evas::SoftwareX11
283                 include Singleton
284
285                 attr_reader :container
286
287                 def initialize
288                         super
289
290                         self.title = "Embrace"
291                         self.borderless = true
292
293                         @icon_dim = IO.read(ICON_FILE, 8, 16).unpack("NN")
294
295                         on_resize { @container.resize(*geometry[2, 3]) }
296
297                         @container = Container.new(evas)
298                         @container.move(0, 0)
299                         @container.layer = -1
300                         @container.show
301
302                         size = [@icon_dim.first, icon_height * MAX_ICONS]
303                         resize(*size)
304                         set_size_min(*size)
305                         set_size_max(*size)
306
307                         evas.font_path_append("/usr/lib/X11/fonts/TTF")
308
309                         @handlers = [
310                                 Ecore::EventHandler.new(IMAP::MailboxStatusEvent,
311                                                         &method(:on_mailbox_status)),
312                                 Ecore::EventHandler.new(IMAP::FinishedEvent,
313                                                         &method(:on_finished))
314                         ]
315
316                         s = File.expand_path("~/.e/apps/embrace/config.yaml")
317                         @config = YAML.load(File.read(s))
318
319                         @server = nil
320                         @timer = Ecore::Timer.new(30, &method(:on_timer))
321                         on_timer
322                 end
323
324                 def icon_height
325                         @icon_dim.last
326                 end
327
328                 def run
329                         Ecore.main_loop_begin
330                 end
331
332                 private
333                 def add_icon(name)
334                         @container << MailboxIcon.new(evas, name)
335                 end
336
337                 def on_timer
338                         return unless @server.nil?
339
340                         @server = IMAP::Session.new(@config)
341
342                         true
343                 end
344
345                 def on_mailbox_status(ev)
346                         md = ev.name.match(/^Lists.(.+)$/)
347                         if md.nil?
348                                 lbl = ev.name
349                         else
350                                 lbl = md.captures.first
351                         end
352
353                         found = evas.find_object(lbl)
354
355                         begin
356                                 if ev.count.zero?
357                                         unless found.nil?
358                                                 #puts "removing icon #{lbl}"
359                                                 @container.delete(found)
360                                         else
361                                                 #puts "count == 0, but icon not found (#{lbl})"
362                                         end
363                                 elsif found.nil?
364                                         #puts "adding icon #{lbl}"
365                                         add_icon(lbl)
366                                 else
367                                         #puts "count > 0, but already there (#{lbl})"
368                                 end
369                         rescue Exception => e
370                                 puts e.message
371                         end
372
373                         false
374                 end
375
376                 def on_finished(ev)
377                         @server = nil
378                 end
379         end
380 end
381
382 Embrace::Main.instance.show
383 Embrace::Main.instance.run