#!/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