Since I don't know where the plugins upstream currently is, I'll just
post the upgrade to animosd.py to this bug report and trust it will find
its way.

Changes:
* All rendering done in Cairo now with support for transparency
* The title window is now a subclass of gtk.Window and splits out all
  rendering from the plugin class
* A new window is created for every title instead of being reused
* Fade in/out now uses time values instead of a fixed step every time
  the idle hook is called (fade does not get lengthened to extremes when
  quodlibet is not much idle, such as when reading the library)
* Uses new configuration variable names

Still works on non-composited screens with the old screenshot / manual
compositing trick.  Configuration format has changed so it uses the new
variable names and all customization has to be redone.  There is no
automatic conversion.

Test it by dropping it in ~/.quodlibet/plugins/events/ until it gets
packaged.
# Copyright (C) 2008 Andreas Bombe
# Copyright (C) 2005  Michael Urman
# Based on osd.py (C) 2005 Ton van den Heuvel, Joe Wreshnig
#                 (C) 2004 Gustavo J. A. M. Carneiro
#
# 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.
#

import gtk, gobject, pango, cairo, pangocairo
import config
import qltk
from qltk.textedit import PatternEdit
from parse import XMLFromPattern


def Label(text):
    l = gtk.Label(text)
    l.set_alignment(0.0, 0.5)
    return l


class OSDWindow(gtk.Window):
    __gsignals__ = {
            'expose-event': 'override',
            'fade-finished': (gobject.SIGNAL_RUN_LAST, None, (bool,)),
            }

    def __init__(self, conf, song):
        gtk.Window.__init__(self, gtk.WINDOW_POPUP)
        self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_NOTIFICATION)

        # for non-composite operation
        self.background_pixbuf = None
        self.titleinfo_surface = None

        screen = self.get_screen()
        cmap = screen.get_rgba_colormap()
        if cmap is None:
            cmap = screen.get_rgb_colormap()
        self.set_colormap(cmap)

        self.conf = conf
        self.iteration_source = None

        cover = song.find_cover()
        try:
            if cover is not None:
                cover = gtk.gdk.pixbuf_new_from_file(cover.name)
        except gobject.GError, gerror:
            print 'Error while loading cover image:', gerror.message
        except:
            from traceback import print_exc
            print_exc()

        # now calculate size of window
        mgeo = screen.get_monitor_geometry(0)
        coverwidth = min(120, mgeo.width // 8)
        textwidth = mgeo.width - 2 * (conf.border + conf.margin)
        if cover is not None:
            textwidth -= coverwidth + conf.border
            coverheight = int(cover.get_height() * (float(coverwidth) / cover.get_width()))
        else:
            coverheight = 0
        self.cover_pixbuf = cover
        self.cover_rectangle = gtk.gdk.Rectangle(conf.border, conf.border,
                coverwidth, coverheight)

        layout = self.create_pango_layout('')
        layout.set_alignment(pango.ALIGN_CENTER)
        layout.set_font_description(pango.FontDescription(conf.font))
        layout.set_markup(XMLFromPattern(conf.string) % song)
        layout.set_width(pango.SCALE * textwidth)
        layoutsize = layout.get_pixel_size()
        if layoutsize[0] < textwidth:
            layout.set_width(pango.SCALE * layoutsize[0])
            layoutsize = layout.get_pixel_size()
        self.title_layout = layout

        winw = layoutsize[0] + 2 * conf.border
        if cover is not None:
            winw += coverwidth + conf.border
        winh = max(coverheight, layoutsize[1]) + 2 * conf.border
        self.set_default_size(winw, winh)

        winx = int((mgeo.width - winw) * conf.pos_x)
        winx = max(conf.margin, min(mgeo.width - conf.margin - winw, winx))
        winy = int((mgeo.height - winh) * conf.pos_y)
        winy = max(conf.margin, min(mgeo.height - conf.margin - winh, winy))
        self.move(winx + mgeo.x, winy + mgeo.y)

    def do_expose_event(self, event):
        cr = self.window.cairo_create()

        if self.is_composited():
            # the simple case
            self.draw_title_info(cr)
            return

        # manual transparency rendering follows
        back_pbuf = self.background_pixbuf
        title_surface = self.titleinfo_surface
        walloc = self.allocation
        wpos = self.get_position()

        if back_pbuf is None:
            root = self.get_screen().get_root_window()
            back_pbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8,
                    walloc.width, walloc.height)
            back_pbuf.get_from_drawable(root, root.get_colormap(),
                    wpos[0], wpos[1], 0, 0, walloc.width, walloc.height)
            self.background_pixbuf = back_pbuf

        if title_surface is None:
            title_surface = gtk.gdk.Pixmap(self.window, walloc.width, walloc.height)
            titlecr = title_surface.cairo_create()
            self.draw_title_info(titlecr)

        cr.set_operator(cairo.OPERATOR_SOURCE)
        if back_pbuf is not None:
            cr.set_source_pixbuf(back_pbuf, 0, 0)
        else:
            cr.set_source_rgb(0.3, 0.3, 0.3)
        cr.paint()
        cr.set_operator(cairo.OPERATOR_OVER)
        cr.set_source_pixmap(title_surface, 0, 0)
        cr.paint_with_alpha(self.get_opacity())

    def draw_title_info(self, cr):
        #cr.set_line_width(1.0)
        do_shadow = self.conf.shadow is not None
        do_outline = self.conf.outline is not None

        # clear with configured background fill
        cr.set_operator(cairo.OPERATOR_SOURCE)
        cr.set_source_rgba(*self.conf.fill)
        cr.paint()

        cr.set_operator(cairo.OPERATOR_OVER)

        # draw border
        if self.conf.bcolor is not None:
            cr.set_source_rgb(*self.conf.bcolor)
            a = self.allocation
            cr.rectangle(a.x, a.y, a.width, a.height)
            cr.stroke()

        textx = self.conf.border

        if self.cover_pixbuf is not None:
            rect = self.cover_rectangle
            textx += rect.width + self.conf.border
            pbuf = self.cover_pixbuf
            transmat = cairo.Matrix()

            if do_shadow:
                cr.set_source_rgb(*self.conf.shadow)
                cr.rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height)
                cr.fill()

            if do_outline:
                cr.set_source_rgb(*self.conf.outline)
                cr.rectangle(rect)
                cr.stroke()

            cr.set_source_pixbuf(pbuf, 0, 0)
            transmat.scale(pbuf.get_width() / float(rect.width),
                    pbuf.get_height() / float(rect.height))
            transmat.translate(-rect.x, -rect.y)
            cr.get_source().set_matrix(transmat)
            cr.rectangle(rect)
            cr.fill()

        pcc = pangocairo.CairoContext(cr)
        pcc.update_layout(self.title_layout)

        if do_shadow:
            cr.set_source_rgb(*self.conf.shadow)
            cr.move_to(textx + 2, self.conf.border + 2)
            pcc.show_layout(self.title_layout)
        if do_outline:
            cr.set_source_rgb(*self.conf.outline)
            cr.move_to(textx, self.conf.border)
            pcc.layout_path(self.title_layout)
            cr.stroke()
        cr.set_source_rgb(*self.conf.text)
        cr.move_to(textx, self.conf.border)
        pcc.show_layout(self.title_layout)

    def fade_in(self):
        self.do_fade_inout(True)

    def fade_out(self):
        self.do_fade_inout(False)

    def do_fade_inout(self, fadein):
        fadein = bool(fadein)

        self.fading_in = fadein
        now = gobject.get_current_time()

        fraction = self.get_opacity()
        if not fadein:
            fraction = 1.0 - fraction
        self.fade_start_time = now - fraction * self.conf.fadetime

        if self.iteration_source is None:
            self.iteration_source = gobject.timeout_add(self.conf.ms,
                    self.fade_iteration_callback)

    def fade_iteration_callback(self):
        delta = gobject.get_current_time() - self.fade_start_time
        fraction = delta / self.conf.fadetime

        if self.fading_in:
            self.set_opacity(fraction)
        else:
            self.set_opacity(1.0 - fraction)

        if not self.is_composited():
            self.queue_draw()

        if fraction >= 1.0:
            self.iteration_source = None
            self.emit('fade-finished', self.fading_in)
            return False
        return True


from plugins.events import EventPlugin

class AnimOsd(EventPlugin):
    PLUGIN_ID = "Animated On-Screen Display"
    PLUGIN_NAME = _("Animated On-Screen Display")
    PLUGIN_DESC = _("Display song information on your screen when it changes.")
    PLUGIN_VERSION = "1.0"

    def PluginPreferences(self, parent):
        def __coltofloat(x):
            return x / 65535.0
        def __floattocol(x):
            return int(x * 65535)

        def set_text(button):
            color = button.get_color()
            color = map(__coltofloat, (color.red, color.green, color.blue))
            config.set("plugins", "animosd2_text",
                    "%f %f %f" % (color[0], color[1], color[2]))
            self.conf.text = tuple(color)
        
        def set_fill(button):
            color = button.get_color()
            color = map(__coltofloat, (color.red, color.green, color.blue,
                button.get_alpha()))
            config.set("plugins", "animosd2_fill",
                    "%f %f %f %f" % (color[0], color[1], color[2], color[3]))
            self.conf.fill = tuple(color)

        def set_font(button):
            font = button.get_font_name()
            config.set("plugins", "animosd2_font", font)
            self.conf.font = font

        def change_delay(button):
            value = int(button.get_value() * 1000)
            config.set("plugins", "animosd2_delay", str(value))
            self.conf.delay = value

        def change_position(button):
            value = button.get_active() / 2.0
            config.set("plugins", "animosd2_pos_y", str(value))
            self.conf.pos_y = value

        def edit_string(button):
            w = PatternEdit(button, AnimOsd.conf.string)
            w.child.text = self.conf.string
            w.apply.connect_object_after('clicked', set_string, w)

        def set_string(window):
            value = window.child.text
            config.set("plugins", "animosd2_string", value)
            self.conf.string = value

        vb = gtk.VBox(spacing=6)

        cb = gtk.combo_box_new_text()
        cb.append_text(_("Display on top of screen"))
        cb.append_text(_("Display in middle of screen"))
        cb.append_text(_("Display on bottom of screen"))
        cb.set_active(int(self.conf.pos_y * 2.0))
        cb.connect('changed', change_position)
        vb.pack_start(cb, expand=False)

        font = gtk.FontButton()
        font.set_font_name(self.conf.font)
        font.connect('font-set', set_font)
        vb.pack_start(font, expand=False)

        hb = gtk.HBox(spacing=3)
        timeout = gtk.SpinButton(
            gtk.Adjustment(
            self.conf.delay/1000.0, 0, 60, 0.1, 1.0, 1.0), 0.1, 1)
        timeout.set_numeric(True)
        timeout.connect('value-changed', change_delay)

        hb.pack_start(Label("Display delay: "), expand=False)
        hb.pack_start(timeout, expand=False);
        hb.pack_start(Label("seconds"), expand=False)
        vb.pack_start(hb, expand=False)

        t = gtk.Table(2, 2)
        t.set_col_spacings(3)
        b = gtk.ColorButton(color=gtk.gdk.Color(*map(__floattocol, self.conf.text)))
        l = Label(_("_Text:"))
        l.set_mnemonic_widget(b); l.set_use_underline(True)
        t.attach(l, 0, 1, 0, 1, xoptions=gtk.FILL)
        t.attach(b, 1, 2, 0, 1)
        b.connect('color-set', set_text)
        b = gtk.ColorButton(color=gtk.gdk.Color(*map(__floattocol, self.conf.fill[0:3])))
        b.set_use_alpha(True)
        b.set_alpha(__floattocol(self.conf.fill[3]))
        b.connect('color-set', set_fill)
        l = Label(_("_Fill:"))
        l.set_mnemonic_widget(b); l.set_use_underline(True)
        t.attach(l, 0, 1, 1, 2, xoptions=gtk.FILL)
        t.attach(b, 1, 2, 1, 2)

        f = qltk.Frame(label=_("Colors"), child=t)
        f.set_border_width(12)
        vb.pack_start(f, expand=False, fill=False)

        string = qltk.Button(_("_Edit Display"), gtk.STOCK_EDIT)
        string.connect('clicked', edit_string)
        vb.pack_start(string, expand=False)
        return vb

    class conf(object):
        pos_x = 0.5 # position of window 0--1 horizontal
        pos_y = 0.0 # position of window 0--1 vertical
        margin = 16 # never any closer to the screen edge than this
        border = 8 # text/cover this far apart, from edge
        fadetime = 1.5 # take this many seconds to fade in or out
        ms = 40 # wait this many milliseconds between steps
        delay = 2500 # wait this many milliseconds before hiding
        font = "Sans 22"
        text = (1.0, 0.8125, 0.586) # main font color
        outline = (0.125, 0.125, 0.125) # color or None - surrounds text and cover
        shadow = (0.0, 0.0, 0.0) # color or None - shadows outline or text and cover
        fill = (0.25, 0.25, 0.25, 0.5) # color+alpha or None - fills rectangular area
        bcolor = (0.0, 0.0, 0.0) # color or None - borders rectangular area
        # song information to use - like in main window
        string = r'''<album|\<b\><album>\</b\><discnumber| - Disc <discnumber>><part| - \<b\><part>\</b\>><tracknumber| - <tracknumber>>
>\<span weight='bold' size='large'\><title>\</span\> - <~length><version|
\<small\>\<i\><version>\</i\>\</small\>><~people|
by <~people>>'''

    def __init__(self):
        self.__current_window = None

        # now load config, resetting values which had errors to their default
        def str_to_tuple(s):
            return tuple(map(float, s.split()))
        def tuple_to_str(t):
            return ' '.join(map(str, t))
        config_map = [
            ('text', config.get, str_to_tuple, tuple_to_str),
            ('fill', config.get, str_to_tuple, tuple_to_str),
            ('font', config.get, None, str),
            ('delay', config.getint, None, str),
            ('pos_y', config.getfloat, None, str),
            ('string', config.get, None, str),
            ]
        for key, cget, getconv, setconv in config_map:
            try:
                value = cget('plugins', 'animosd2_' + key)
                if getconv is not None:
                    value = getconv(value)
            except:
                config.set('plugins', 'animosd2_' + key,
                        setconv(getattr(self.conf, key)))
            else:
                setattr(self.conf, key, value)

    # for rapid debugging
    def plugin_single_song(self, song):
        self.plugin_on_song_started(song)

    def plugin_on_song_started(self, song):
        if self.__current_window is not None:
            if self.__current_window.is_composited():
                self.__current_window.fade_out()
            else:
                self.__current_window.hide()
                self.__current_window.destroy()

        if song is None:
            self.__current_window = None
            return

        window = OSDWindow(self.conf, song)
        window.add_events(gtk.gdk.BUTTON_PRESS_MASK)
        window.connect('button-press-event', self.__buttonpress)
        window.connect('fade-finished', self.__fade_finished)
        self.__current_window = window

        window.set_opacity(0.0)
        window.show()
        window.fade_in()

    def start_fade_out(self, window):
        window.fade_out()
        return False

    def __buttonpress(self, window, event):
        window.hide()
        if self.__current_window is window:
            self.__current_window = None
        window.destroy()

    def __fade_finished(self, window, fade_in):
        if fade_in:
            gobject.timeout_add(self.conf.delay, self.start_fade_out, window)
        else:
            window.hide()
            if self.__current_window is window:
                self.__current_window = None
            # Delay destroy, apparantly the hide does not quite register if
            # the destroy is done immediately.  The compiz animation plugin
            # then sometimes triggers and causes undesirable effects while the
            # popup should already be invisible.
            gobject.timeout_add(1000, window.destroy)

Reply via email to