#!/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 "evas" require "ecore" require "ecore_x" require "ecore_evas" require "singleton" require "yaml" require "embrace/imap" PKG_NAME = "embrace" PREFIX = nil DATADIR = "#{PREFIX || "/usr/local"}/share/#{PKG_NAME}/" module Embrace VERSION = "0.1.0" ICON_FILE = DATADIR + "l33t_MAI_envelope.png" 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, object) super(duration) do |v| a = compute_alpha(v) object.set_color(a, a, a, a) end end def compute_alpha(v) (255 * v).to_i end end class InverseAlphaAnimator < AlphaAnimator def compute_alpha(v) super((1.0 - v).abs) 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 class FadeOutFinishedEvent < Ecore::Event attr_reader :icon def initialize(icon) super() @icon = icon end end attr_accessor :slot def initialize(evas, label) super(evas) self.name = label @slot = nil @alpha_anim = nil @img = Evas::Image.new(evas) @label = Evas::Text.new(evas) @objects = [@img, @label] @objects.each { |o| add_member(o) } set_color(0, 0, 0, 0) @img.set_file(ICON_FILE) @img.set_fill(0, 0, *@img.get_size) @label.text = name @label.set_font("VeraBd", 10) a = @label.geometry b = *@img.get_size @label_offset_x = (b[0] / 2) - (a[2] / 2) @label_offset_y = (b[1] / 2) - (a[3] / 2) resize(*@img.get_size) end def fade_in show @alpha_anim ||= AlphaAnimator.new(2, self) @alpha_anim.on_finished { @alpha_anim = nil } end def fade_out @alpha_anim ||= InverseAlphaAnimator.new(2, self) @alpha_anim.on_finished do @alpha_anim = nil FadeOutFinishedEvent.raise(self) end end # smart callbacks def smart_show @objects.each { |o| o.show } end def smart_hide @objects.each { |o| o.hide } @alpha_anim && @alpha_anim.delete @alpha_anim = nil end def smart_delete @objects.each { |o| o.delete } @objects.clear @alpha_anim && @alpha_anim.delete @img = @label = @alpha_anim = nil end def smart_move(x, y) @img.move(x, y) # center the label on the image @label.move(x + @label_offset_x, y + @label_offset_y) end def smart_resize(w, h) @img.resize(w, h) end def smart_color_set(r, g, b, a) @img.set_color(r, g, b, a) @label.set_color(r, 0, 0, a) end end class Container < Evas::Smart class ContainerError < StandardError; end class ContainerFullError < ContainerError; end class ContainerLockedError < ContainerError; end def initialize(evas) super @bg = Evas::Rectangle.new(evas) @bg.set_color(0, 0, 0, 0) add_member(@bg) @icons = [] @animators = [] @about_to_add = 0 @add_lock_count = 0 @handlers = [ Ecore::EventHandler.new(MailboxIcon::FadeOutFinishedEvent, &method(:on_icon_fade_out_finished)) ] end def can_add? !slots_left.zero? && @add_lock_count.zero? end def can_delete? @about_to_add.zero? && @add_lock_count.zero? end def <<(i) Kernel.raise(ContainerFullError) if slots_left.zero? Kernel.raise(ContainerLockedError) unless @add_lock_count.zero? geo = geometry i.move(geo[0], geo[1] + geo[3] % Main.instance.icon_height) i.slot = next_slot i.clip = self i.fade_in # 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 += 1 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 @about_to_add -= 1 end end def delete(icon) i = @icons.index(icon) return (block_given? ? yield : nil) if i.nil? delete_at(i) end def delete_at(i) Kernel.raise(ContainerLockedError) unless @about_to_add.zero? Kernel.raise(ContainerLockedError) unless @add_lock_count.zero? @add_lock_count += 1 @icons[i].fade_out end def on_icon_fade_out_finished(ev) i = @icons.index(ev.icon) ev.icon.delete @icons.delete_at(i) # icons that are placed above the one that's deleted need # to be moved. check whether are there any first if i == @icons.length @add_lock_count -= 1 return end ar = @icons[i..-1] @animators << MoveAnimator.new(2, Main.instance.icon_height, *ar) @animators.last.on_finished do |ani| @animators.delete(ani) @add_lock_count -= 1 end end # smart callbacks def smart_show @bg.show end def smart_hide @bg.hide end def smart_delete @bg.delete @bg = nil end def smart_move(x, y) @bg.move(x, y) end def smart_resize(w, h) @bg.resize(w, h) end private def max_icons geometry.pop / Main.instance.icon_height end def slots_left max_icons - @icons.nitems - @about_to_add end def next_slot @icons.nitems + @about_to_add end end class Main < Ecore::Evas::SoftwareX11 include Singleton attr_reader :container def initialize super self.has_alpha = true self.title = "Embrace" self.borderless = true @icon_dim = IO.read(ICON_FILE, 8, 16).unpack("NN") on_resize { @container.resize(*geometry[2, 3]) } @container = Container.new(evas) @container.move(0, 0) @container.layer = -1 @container.show size = [@icon_dim.first] arg = ARGV.shift if arg.nil? size << icon_height * 11 else size << arg.to_i end 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)) ] s = File.expand_path("~/.e/apps/embrace/config.yaml") @config = YAML.load(File.read(s)) @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 on_timer @server ||= IMAP::Session.new(@config) 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 = evas.find_object(lbl) if ev.count.zero? && !found.nil? && @container.can_delete? @container.delete(found) elsif !ev.count.zero? && found.nil? && @container.can_add? @container << MailboxIcon.new(evas, lbl) end false end def on_finished(ev) @server = nil end end end Embrace::Main.instance.show Embrace::Main.instance.run