Plugins: animosd.py

File animosd.py, 16.9 kB (added by msabatini@gmail.com, 9 months ago)

Display song information on your screen when it changes.

Line 
1 # Copyright (C) 2005  Michael Urman
2 # Based on osd.py (C) 2005 Ton van den Heuvel, Joe Wreshnig
3 #                 (C) 2004 Gustavo J. A. M. Carneiro
4 #
5 # Minor adaptations by Mike Sabatini
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of version 2 of the GNU General Public License as
9 # published by the Free Software Foundation.
10 #
11
12 import gtk, gobject, pango
13 import config
14 import qltk
15 from qltk.textedit import PatternEdit
16 from parse import XMLFromPattern
17
18 def Label(text):
19     l = gtk.Label(text)
20     l.set_alignment(0.0, 0.5)
21     return l
22
23 from plugins.events import EventPlugin
24
25 class AnimOsd(EventPlugin):
26     PLUGIN_ID = "Animated On-Screen Display"
27     PLUGIN_NAME = _("Animated On-Screen Display")
28     PLUGIN_DESC = _("Display song information on your screen when it changes.")
29     PLUGIN_VERSION = "0.23"
30
31     def PluginPreferences(self, parent):
32         def set_text(button):
33             color = button.get_color()
34             cstring = "#%02x%02x%02x" % (
35                 color.red//256, color.green//256, color.blue//256)
36             config.set("plugins", "animosd_text", cstring)
37             self.conf.text = cstring
38        
39         def set_fill(button):
40             color = button.get_color()
41             cstring = "#%02x%02x%02x%02x" % (
42                 color.red//256, color.green//256, color.blue//256,
43                 button.get_alpha()//256)
44             config.set("plugins", "animosd_fill", cstring)
45             self.conf.fill = cstring
46
47         def set_font(button):
48             font = button.get_font_name()
49             config.set("plugins", "animosd_font", font)
50             self.conf.font = font
51
52         def change_delay(button):
53             value = int(button.get_value() * 1000)
54             config.set("plugins", "animosd_delay", str(value))
55             self.conf.delay = value
56
57         def change_position_x(button):
58             value = button.get_active()
59             config.set("plugins", "animosd_pos_x", str(value))
60             self.conf.pos[0] = value
61
62         def change_position_y(button):
63             value = button.get_active() / 2.0
64             config.set("plugins", "animosd_pos_y", str(value))
65             self.conf.pos[1] = value
66
67         def edit_string(button):
68             w = PatternEdit(button, AnimOsd.conf.string)
69             w.child.text = self.conf.string
70             w.apply.connect_object_after('clicked', set_string, w)
71
72         def set_string(window):
73             value = window.child.text
74             config.set("plugins", "animosd_string", value)
75             self.conf.string = value
76
77         vb = gtk.VBox(spacing=2)
78
79         cb = gtk.combo_box_new_text()
80         cb.append_text(_("Display on top of screen"))
81         cb.append_text(_("Display in middle of screen"))
82         cb.append_text(_("Display on bottom of screen"))
83         cb.set_active(int(self.conf.pos[1] * 2.0))
84         cb.connect('changed', change_position_y)
85         vb.pack_start(cb, expand=False)
86
87         x = gtk.combo_box_new_text()
88         x.append_text(_("Display on left of screen"))
89         x.append_text(_("Display on right of screen"))
90         x.set_active(int(self.conf.pos[0]))
91         x.connect('changed', change_position_x)
92         vb.pack_start(x, expand=False)
93
94         font = gtk.FontButton()
95         font.set_font_name(self.conf.font)
96         font.connect('font-set', set_font)
97         vb.pack_start(font, expand=False)
98
99         hb = gtk.HBox(spacing=3)
100         timeout = gtk.SpinButton(
101             gtk.Adjustment(
102             self.conf.delay/1000.0, 0, 60, 0.1, 1.0, 1.0), 0.1, 1)
103         timeout.set_numeric(True)
104         timeout.connect('value-changed', change_delay)
105
106         hb.pack_start(Label("Display delay: "), expand=False)
107         hb.pack_start(timeout, expand=False);
108         hb.pack_start(Label("seconds"), expand=False)
109         vb.pack_start(hb, expand=False)
110
111         t = gtk.Table(2, 2)
112         t.set_col_spacings(3)
113         b = gtk.ColorButton(color=gtk.gdk.color_parse(self.conf.text))
114         l = Label(_("_Text:"))
115         l.set_mnemonic_widget(b); l.set_use_underline(True)
116         t.attach(l, 0, 1, 0, 1, xoptions=gtk.FILL)
117         t.attach(b, 1, 2, 0, 1)
118         b.connect('color-set', set_text)
119         b = gtk.ColorButton(color=gtk.gdk.color_parse(self.conf.fill[:7]))
120         b.set_use_alpha(True)
121         b.set_alpha(int(self.conf.fill[-2:], base=16))
122         b.connect('color-set', set_fill)
123         l = Label(_("_Fill:"))
124         l.set_mnemonic_widget(b); l.set_use_underline(True)
125         t.attach(l, 0, 1, 1, 2, xoptions=gtk.FILL)
126         t.attach(b, 1, 2, 1, 2)
127
128         f = qltk.Frame(label=_("Colors"), child=t)
129         f.set_border_width(12)
130         vb.pack_start(f, expand=False, fill=False)
131
132         string = qltk.Button(_("_Edit Display"), gtk.STOCK_EDIT)
133         string.connect('clicked', edit_string)
134         vb.pack_start(string, expand=False)
135         return vb
136
137     class conf(object):
138         pos = [1.0, 1.0] # position of window 0--1 horizontal, 0--1 vertical
139         margin = 2 # never any closer to the screen edge than this
140         border = 4 # text/cover this far apart, from edge
141         step = 32 # of 256, how far to jump each step of animation
142         ms = 80 # wait this many milliseconds between steps
143         delay = 2500 # wait this many milliseconds before hiding
144         font = "Sans 22"
145         text = "#ffd096" # main font color
146         outline = "#202020" # color or None - surrounds text and cover
147         shadow = "#000000" # color or None - shadows outline or text and cover
148         fill = "#40404080" # color+alpha or None - fills rectangular area
149         bcolor = "#000000" # color or None - borders rectangular area
150         # song information to use - like in main window
151         string = r'''<album|\<b\><album>\</b\><discnumber| - Disc <discnumber>><part| - \<b\><part>\</b\>><tracknumber| - <tracknumber>>
152 >\<span weight='bold' size='large'\><title>\</span\> - <~length><version|
153 \<small\>\<i\><version>\</i\>\</small\>><~people|
154 by <~people>>'''
155
156     def __init__(self):
157         window = self.__window = gtk.Window(gtk.WINDOW_POPUP)
158         window.add_events(gtk.gdk.BUTTON_PRESS_MASK)
159         window.connect('button-press-event', self.__buttonpress)
160         darea = self.__darea = gtk.DrawingArea()
161         window.add(self.__darea)
162         darea.show()
163         darea.realize()
164         layout = self.__layout = window.create_pango_layout("")
165         layout.set_justify(False)
166         layout.set_alignment(pango.ALIGN_CENTER)
167         layout.set_wrap(pango.WRAP_WORD)
168         self.__step = 0 # 0=invisible; 255=fully visible
169         self.__stepby = 0
170         self.__song = None
171         self.__next = None
172         self.__screen = gtk.gdk.screen_get_default()
173         geom = gtk.gdk.Screen.get_monitor_geometry(self.__screen, 0)
174         self.__screenwidth = geom.width
175         self.__screenheight = geom.height
176         self.__coverwidth = min(120, self.__screenwidth // 8)
177         self.__width = self.__height = self.__coverwidth + 2 * self.conf.border
178         self.__delayhide = None
179
180         for key, value in [
181             ("text", "#ffd096"),
182             ("fill", "#40404080"),
183             ("font", "Sans 22")]:
184             try: value = config.get("plugins", "animosd_" + key)
185             except: config.set("plugins", "animosd_" + key, value)
186             setattr(self.conf, key, value)
187         try:
188                     self.conf.delay = config.getint("plugins", "animosd_delay")
189         except:
190                     config.set("plugins", "animosd_delay", str(self.conf.delay))
191         try:
192                     self.conf.pos = [config.getfloat("plugins", "animosd_pos_x"), config.getfloat("plugins", "animosd_pos_y")]
193         except:
194                     config.set("plugins", "animosd_pos_y", str(self.conf.pos[1]))
195                     config.set("plugins", "animosd_pos_x", str(self.conf.pos[0]))
196         try:
197                     self.conf.string = config.get("plugins", "animosd_string")
198         except:
199                     config.set("plugins", "animosd_string", self.conf.string)
200
201     # for rapid debugging
202     def plugin_single_song(self, song): self.plugin_on_song_started(song)
203
204     def plugin_on_song_started(self, song):
205         self.__next = song
206         gobject.idle_add(self.show)
207
208     def wait_until_hidden(self):
209         while self.__stepby < 0:
210             gtk.main_iteration()
211
212     def hide(self):
213         if self.__step <= 0:
214             return
215
216         # cancel any pending hides.
217         if self.__delayhide is not None:
218             gobject.source_remove(self.__delayhide)
219             self.__delayhide = None
220
221         if self.__stepby == 0:
222             self.__stepby = -self.conf.step
223             gobject.timeout_add(self.conf.ms, self.render)
224
225     def show(self):
226         if self.__step > 0:
227             self.hide()
228             return
229
230         if self.__next is not None:
231             self.__song = self.__next
232             self.__next = None
233         if self.__song is None:
234             return
235
236         if self.__step >= 255:
237             return
238
239         self.render_setup(self.__song)
240
241         if self.__stepby == 0:
242             self.__stepby = self.conf.step
243             gobject.timeout_add(self.conf.ms, self.render)
244
245     def render_setup(self, song):
246         # size cover
247         cover = self.__cover = song.find_cover()
248         if cover is not None:
249             try:
250                 self.__cover = gtk.gdk.pixbuf_new_from_file_at_size(
251                     cover.name, self.__coverwidth, self.__coverwidth)
252                 cw = self.__cover.get_width()
253                 ch = self.__cover.get_height()
254                 self.__coverx = self.conf.border + (self.__coverwidth - cw) // 2
255                 self.__covery = self.conf.border + (self.__coverwidth - ch) // 2
256             except:
257                 from traceback import print_exc; print_exc()
258                 self.__cover = None
259
260         # size text
261         tw = (self.__screenwidth - 2 * (self.conf.border + self.conf.margin))
262         if self.__cover is not None:
263             tw -= self.__coverwidth + self.conf.border
264         layout = self.__layout
265         layout.set_font_description(pango.FontDescription(self.conf.font))
266         layout.set_markup(XMLFromPattern(self.conf.string) % song)
267         layout.set_width(pango.SCALE * tw)
268         self.__textsize = layout.get_pixel_size()
269         layout.set_width(pango.SCALE * min(self.__textsize[0], tw))
270         self.__textsize = layout.get_pixel_size()
271
272         # size window to text + cover
273         w = self.__textsize[0] + 2 * self.conf.border
274         h = max(self.__cover and self.__coverwidth or 0,
275                 self.__textsize[1]) + 2 * self.conf.border
276         if self.__cover is not None:
277             w += self.__coverwidth + self.conf.border
278             self.__covery = (h - ch) // 2
279         darea = self.__darea
280         darea.set_size_request(w, h)
281         self.__width = w
282         self.__height = h
283
284         # figure positions
285         sw, sh = self.__screenwidth, self.__screenheight
286         m = self.conf.margin
287         x = int((sw - w) * self.conf.pos[0])
288         x = self.__winx = max(m, min(sw - m - w, x))
289         y = int((sh - h) * self.conf.pos[1])
290         y = self.__winy = max(m, min(sh - m - h, y))
291
292         self.__textx = self.conf.border
293         if self.__cover is not None:
294             self.__textx += self.__coverwidth + self.conf.border
295         self.__texty = (h - self.__textsize[1]) // 2
296
297         # scrape root for pseudo transparancy
298         root = gtk.gdk.Screen.get_root_window(self.__screen)
299         self.__bg = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, 0, 8, w, h)
300         self.__bg.get_from_drawable(root, root.get_colormap(),
301                 x, y, 0, 0, w, h)
302
303         # iter 1: prerender the whole thing, including composite; then
304         # recomposite for alpha effect
305
306         mask = gtk.gdk.Pixmap(darea.window, w, h, 1)
307         maskoff = gtk.gdk.GC(mask)
308         maskoff.set_colormap(darea.window.get_colormap())
309         maskoff.set_foreground(gtk.gdk.Color(pixel=0))
310         mask.draw_rectangle(maskoff, True, 0, 0, w, h)
311         maskon = maskoff
312         del maskoff
313         maskon.set_foreground(gtk.gdk.Color(pixel=-1))
314
315         # panel background overlay
316         dareacmap = darea.get_colormap()
317         img = gtk.gdk.Pixmap(darea.window, w, h)
318         bg_gc = gtk.gdk.GC(img)
319         bg_gc.copy(darea.style.fg_gc[gtk.STATE_NORMAL])
320         bg_gc.set_colormap(darea.window.get_colormap())
321         if self.conf.fill is not None:
322             bg_gc.set_foreground(dareacmap.alloc_color(self.conf.fill[:7]))
323             img.draw_rectangle(bg_gc, True, 0, 0, w, h)
324             mask.draw_rectangle(maskon, True, 0, 0, w, h)
325
326         # composite with root
327         try: alpha = int(self.conf.fill[7:], 16)
328         except (ValueError,TypeError): alpha = 0
329         buf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, 0, 8, w, h)
330         buf.get_from_drawable(img, img.get_colormap(), 0, 0, 0, 0, w, h)
331         if self.conf.fill is not None: # else just use pure background
332             self.__bg.composite(buf, 0, 0, w, h, 0, 0, 1, 1,
333                     gtk.gdk.INTERP_NEAREST, 255-alpha)
334         img.draw_pixbuf(darea.style.fg_gc[gtk.STATE_NORMAL], buf, 0, 0, 0, 0)
335
336         # border
337         if self.conf.bcolor is not None:
338             bg_gc.set_foreground(dareacmap.alloc_color(self.conf.bcolor))
339             img.draw_rectangle(bg_gc, False, 0, 0, w - 1, h - 1)
340             mask.draw_rectangle(maskon, False, 0, 0, w - 1, h - 1)
341
342         # text
343         fg_gc = gtk.gdk.GC(img)
344         fg_gc.copy(darea.style.fg_gc[gtk.STATE_NORMAL])
345         fg_gc.set_colormap(dareacmap)
346         tx = self.__textx
347         ty = self.__texty
348
349         if self.conf.shadow is not None:
350             fg_gc.set_foreground(dareacmap.alloc_color(self.conf.shadow))
351             img.draw_layout(fg_gc, tx + 2, ty + 2, layout)
352             if self.conf.fill is None:
353                 mask.draw_layout(maskon, tx + 2, ty + 2, layout)
354
355         if self.conf.outline is not None:
356             fg_gc.set_foreground(dareacmap.alloc_color(self.conf.outline))
357             for dx,dy in [(-1,-1), (-1, 0), (-1, 1),
358                           ( 0,-1),          ( 0, 1),
359                           ( 1,-1), ( 1, 0), ( 1, 1)]:
360                     img.draw_layout(fg_gc, tx + dx, ty + dy, layout)
361                     if self.conf.fill is None:
362                         mask.draw_layout(maskon, tx + dx, ty + dy, layout)
363
364         fg_gc.set_foreground(dareacmap.alloc_color(self.conf.text))
365         img.draw_layout(fg_gc, tx, ty, layout)
366         if self.conf.fill is None:
367             mask.draw_layout(maskon, tx, ty, layout)
368
369         # cover
370         if self.__cover is not None:
371             if self.conf.shadow is not None:
372                 fg_gc.set_foreground(dareacmap.alloc_color(self.conf.shadow))
373                 img.draw_rectangle(bg_gc, True,
374                         self.__coverx + 2, self.__covery + 2, cw, ch)
375                 mask.draw_rectangle(maskon, True,
376                         self.__coverx + 2, self.__covery + 2, cw, ch)
377
378             if self.conf.outline is not None:
379                 fg_gc.set_foreground(dareacmap.alloc_color(self.conf.outline))
380                 img.draw_rectangle(bg_gc, False,
381                         self.__coverx - 1, self.__covery - 1, cw + 1, ch + 1)
382                 mask.draw_rectangle(maskon, False,
383                         self.__coverx - 1, self.__covery - 1, cw + 1, ch + 1)
384
385             img.draw_pixbuf(darea.style.fg_gc[gtk.STATE_NORMAL],
386                     self.__cover, 0, 0, self.__coverx, self.__covery)
387             mask.draw_rectangle(maskon, True,
388                     0, 0, self.__coverx, self.__covery)
389
390         self.__img = img
391         self.__window.shape_combine_mask(mask, 0, 0)
392         self.__window.move(x, y)
393         self.__window.resize(w, h)
394
395     def render(self):
396         w = self.__width
397         h = self.__height
398         x = self.__winx
399         y = self.__winy
400         darea = self.__darea
401         self.__step = max(0, min(255, self.__step + self.__stepby))
402
403         if 0 < self.__step < 255:
404             # recomposite
405             buf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, 0, 8, w, h)
406             buf.get_from_drawable(self.__img, self.__img.get_colormap(),
407                     0, 0, 0, 0, w, h)
408             self.__bg.composite(buf, 0, 0, w, h,
409                     0, 0, 1, 1, gtk.gdk.INTERP_NEAREST, 255-self.__step)
410             img = gtk.gdk.Pixmap(darea.window, w, h)
411             img.draw_pixbuf(darea.style.fg_gc[gtk.STATE_NORMAL],
412                     buf, 0, 0, 0, 0)
413         else:
414             img = self.__img
415
416         darea.window.set_back_pixmap(img, False)
417         darea.queue_draw_area(0, 0, w, h)
418
419         # has it finished hiding?
420         if self.__step <= 0:
421             del self.__bg
422             self.__window.hide()
423             self.__stepby = 0
424             self.__step = 0
425             self.__song = None
426             if self.__next is not None:
427                 gobject.timeout_add(self.conf.ms, self.show)
428             return
429
430         gobject.idle_add(self.__window.show)
431
432         # has it finished showing?
433         if self.__step >= 255:
434             self.__stepby = 0
435             self.__step = 255
436             self.__delayhide = gobject.timeout_add(self.conf.delay, self.hide)
437             return
438
439         # or is it just a normal update? (keep updates coming)
440         return True
441
442     def __buttonpress(self, *args):
443         self.__stepby = self.__step = 0
444         self.__window.hide()