+#!/usr/bin/env ruby
+
+# vim:syn=ruby
+#
+# Copyright (c) 2006 Tilman Sauerbeck (tilman at code-monkey de)
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of version 2 of the GNU General Public License as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+
+require "ecore"
+require "ecore_x"
+require "ecore_evas"
+require "singleton"
+require "yaml"
+require "embrace/imap"
+
+PKG_NAME = "embrace"
+DATADIR = "/usr/local/share/#{PKG_NAME}/"
+
+class Evas::EvasObject
+ def move_relative(obj, x, y)
+ # FIXME investigate whether there's an easier way
+ move(*obj.geometry[0..1].zip([x, y]).map { |(a, b)| a + b })
+ end
+
+ def center(obj)
+ a = geometry
+ b = obj.geometry
+
+ move_relative(obj, (b[2] / 2) - (a[2] / 2),
+ (b[3] / 2) - (a[3] / 2))
+ end
+
+ def alpha=(alpha)
+ set_color(*(get_color[0..-2] << alpha))
+ end
+end
+
+module Embrace
+ VERSION = "0.0.1"
+ ICON_FILE = DATADIR + "l33t_MAI_envelope.png"
+ MAX_ICONS = 11
+
+ class ZeroToOneAnimator < Ecore::Animator
+ def initialize(duration)
+ @finished = nil
+
+ started = Time.now
+
+ super() do
+ v = [(Time.now - started) / duration, 1.0].min
+
+ yield v
+
+ @finished.call(self) unless (v < 1.0) || @finished.nil?
+ v < 1.0
+ end
+ end
+
+ def delete
+ super
+
+ @finished = nil
+ end
+
+ def on_finished(&block)
+ @finished = block
+ end
+ end
+
+ # an animator that runs for the specified number of seconds,
+ # and yields values between 0 and 255
+ class AlphaAnimator < ZeroToOneAnimator
+ def initialize(duration, *objects)
+ super(duration) do |v|
+ objects.each { |o| o.alpha = (255 * v).to_i }
+ end
+ end
+ end
+
+ class MoveAnimator < ZeroToOneAnimator
+ def initialize(duration, movement, *objects)
+ zipped = objects.zip(objects.map { |o| o.geometry[0..1] })
+
+ super(duration) do |v|
+ # decelerate
+ v = Math.sin(v * Math::PI / 2.0)
+
+ zipped.each do |(o, orig_pos)|
+ o.move(orig_pos.first,
+ orig_pos.last + (movement * v).to_i)
+ end
+ end
+ end
+ end
+
+ class MailboxIcon < Evas::Smart
+ attr_accessor :slot
+
+ def initialize(evas, label)
+ super(evas)
+
+ @slot = nil
+ @alpha_anim = nil
+
+ @img = Evas::Image.new(evas)
+ @label = Evas::Text.new(evas)
+
+ @objects = [@img, @label]
+
+ @img.set_color(255, 255, 255, 0)
+ @label.set_color(255, 255, 255, 0)
+ @label.set_color(255, 0, 0, 0)
+
+ @img.set_file(ICON_FILE)
+ @img.set_fill(0, 0, *@img.get_size)
+
+ @label.text = label
+ @label.set_font("VeraBd", 10)
+
+ resize(*@img.get_size)
+ end
+
+ def label
+ @label.text
+ end
+
+ # smart callbacks
+ def on_show
+ @objects.each { |o| o.show }
+
+ @alpha_anim ||= AlphaAnimator.new(2, @img, @label)
+ @alpha_anim.on_finished { @alpha_anim = nil }
+ end
+
+ def on_hide
+ @objects.each { |o| o.hide }
+
+ @alpha_anim && @alpha_anim.delete
+ @alpha_anim = nil
+ end
+
+ def on_delete
+ @objects.each { |o| o.delete }
+ @objects.clear
+
+ @alpha_anim && @alpha_anim.delete
+
+ @img = @label = @alpha_anim = nil
+ end
+
+ def on_layer_set(layer)
+ @objects.each { |o| o.layer = layer }
+ end
+
+ def on_stack_above(other)
+ @objects.each { |o| o.stack_above = other }
+ end
+
+ def on_stack_below(other)
+ @objects.each { |o| o.stack_below = other }
+ end
+
+ def on_move(x, y)
+ @objects.each { |o| o.move(x, y) }
+
+ @label.center(self)
+ end
+
+ def on_resize(w, h)
+ @img.resize(w, h)
+ end
+ end
+
+ class FixedSizeArray < Array
+ def initialize(siz)
+ super
+ end
+
+ def each
+ super { |item| yield item unless item.nil? }
+ end
+
+ def delete_at(i)
+ self[i] = nil
+ end
+
+ undef :push
+ undef :<<
+ undef :unshift
+
+ undef :pop
+ undef :shift
+ undef :delete
+ end
+
+ class Container < Evas::Smart
+ class ContainerError < StandardError; end
+ class ContainerFullError < ContainerError; end
+ class ContainerLockedError < ContainerError; end
+
+ include Enumerable
+
+ def initialize(evas)
+ super
+
+ @bg = Evas::Rectangle.new(evas)
+ @bg.set_color(0, 0, 0, 255)
+
+ @icons = FixedSizeArray.new(MAX_ICONS)
+ @about_to_add = []
+ @animators = []
+
+ @lock_count = 0
+ end
+
+ def each
+ @icons.each { |i| yield i unless i.nil? }
+ end
+
+ def <<(i)
+ Kernel.raise(ContainerFullError) if slots_left.zero?
+ Kernel.raise(ContainerLockedError) if @lock_count > 0
+
+ i.move_relative(self, 0, 0)
+ i.slot = next_slot
+ i.clip = self
+ i.show
+
+ # check whether we need to need to move this icon
+ if slots_left == 1
+ @icons[i.slot] = i
+ return
+ end
+
+ movement = Main.instance.icon_height * (slots_left - 1)
+
+ @about_to_add << i
+
+ move_time = 0.25 * slots_left
+ @animators << MoveAnimator.new(move_time, movement, i)
+
+ @animators.last.on_finished do |ani|
+ @animators.delete(ani)
+ @icons[i.slot] = i
+
+ # FIXME check whether we can always shift the array instead
+ #puts "really added #{i.label} now (slot #{i.slot})"
+ @about_to_add.delete(i)
+ end
+ end
+
+ def delete(icon)
+ # icons that are placed above the one that's deleted need
+ # to be moved
+ i = @icons.index(icon)
+ return (block_given? ? yield : nil) if i.nil?
+
+ delete_at(i)
+ end
+
+ def delete_at(i)
+ @icons[i].delete
+ @icons.delete_at(i)
+
+ ar = @icons[i..-1].reject { |i| i.nil? }
+ return if ar.nil?
+
+ @lock_count += 1
+
+ @animators << MoveAnimator.new(2, Main.instance.icon_height, *ar)
+ @animators.last.on_finished do |ani|
+ @animators.delete(ani)
+ @lock_count -= 1
+ end
+ end
+
+ def length
+ @icons.nitems
+ end
+
+ # smart callbacks
+ def on_show
+ @bg.show
+ end
+
+ def on_hide
+ @bg.hide
+ end
+
+ def on_delete
+ @bg.delete
+ @bg = nil
+ end
+
+ def on_layer_set(layer)
+ @bg.layer = layer
+ end
+
+ def on_stack_above(other)
+ @bg.stack_above = other
+ end
+
+ def on_stack_below(other)
+ @bg.stack_below = other
+ end
+
+ def on_move(x, y)
+ @bg.move(x, y)
+ end
+
+ def on_resize(w, h)
+ @bg.resize(w, h)
+ end
+
+ private
+ def slots_left
+ MAX_ICONS - @icons.nitems - @about_to_add.length
+ end
+
+ def next_slot
+ @icons.nitems + @about_to_add.length
+ end
+ end
+
+ class Main < Ecore::Evas::SoftwareX11
+ include Singleton
+
+ attr_reader :container
+
+ def initialize
+ super
+
+ self.title = "blah"
+ self.borderless = true
+
+ @icon_dim = IO.read(ICON_FILE, 8, 16).unpack("NN")
+
+ self.on_resize { @container.resize(*geometry[2, 3]) }
+
+ @container = Container.new(evas)
+ @container.move(0, 0)
+ @container.layer = -1
+ @container.show
+
+ size = [@icon_dim.first, icon_height * MAX_ICONS]
+ resize(*size)
+ set_size_min(*size)
+ set_size_max(*size)
+
+ evas.font_path_append("/usr/lib/X11/fonts/TTF")
+
+ @handlers = [
+ Ecore::EventHandler.new(IMAP::MailboxStatusEvent,
+ &method(:on_mailbox_status)),
+ Ecore::EventHandler.new(IMAP::FinishedEvent,
+ &method(:on_finished))
+ ]
+
+ @server = nil
+ @timer = Ecore::Timer.new(30, &method(:on_timer))
+ on_timer
+ end
+
+ def icon_height
+ @icon_dim.last
+ end
+
+ def run
+ Ecore.main_loop_begin
+ end
+
+ private
+ def add_icon(name)
+ @container << MailboxIcon.new(evas, name)
+ end
+
+ def on_timer
+ return unless @server.nil?
+
+ mboxes = %w{
+ INBOX
+ Lists.ba-2005
+ Lists.blackbox-devel
+ Lists.clc-devel
+ Lists.crux
+ Lists.cruxcon
+ Lists.dri-devel
+ Lists.dri-users
+ Lists.enlightenment-cvs
+ Lists.enlightenment-devel
+ Lists.hobix
+ Lists.mesa3d-dev
+ Lists.ruby-core
+ Lists.rubygems-devel
+ Lists.vim-ruby-devel
+ Lists.xmms2-devel
+ Lists.xorg
+ }
+
+ s = File.expand_path("~/.e/apps/embrace/config.yaml")
+
+ File.open(s) do |f|
+ @server = IMAP::Session.new(YAML.load(f), mboxes)
+ end
+
+ true
+ end
+
+ def on_mailbox_status(ev)
+ md = ev.name.match(/^Lists.(.+)$/)
+ if md.nil?
+ lbl = ev.name
+ else
+ lbl = md.captures.first
+ end
+
+ found = @container.find { |i| i.label == lbl }
+
+ begin
+ if ev.count.zero?
+ unless found.nil?
+ #puts "removing icon #{lbl}"
+ @container.delete(found)
+ else
+ #puts "count == 0, but icon not found (#{lbl})"
+ end
+ elsif found.nil?
+ #puts "adding icon #{lbl}"
+ add_icon(lbl)
+ else
+ #puts "count > 0, but already there (#{lbl})"
+ end
+ rescue Exception => e
+ puts e.message
+ end
+
+ false
+ end
+
+ def on_finished(ev)
+ @server = nil
+ end
+ end
+end
+
+Embrace::Main.instance.show
+Embrace::Main.instance.run