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)