# -*- coding: utf-8 -*- # # This file is part of the Rhythmbox Desktop Art plug-in # # Copyright © 2008 Mathias Nedrebø < mathias at nedrebo dot org > # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from __future__ import division import sys import gobject import gtk, cairo, pango import gconf import rsvg from roundedrec import roundedrec from ConfigDialog import ConfigDialog # CONSTANTS UNKNOWN_COVER = -1 POSITION_NW = 'nw' POSITION_NE = 'ne' POSITION_SW = 'sw' POSITION_SE = 'se' gconf_plugin_path = '/apps/rhythmbox/plugins/desktop-art' def get_icon_path(theme, name, size): icon = theme.lookup_icon(name, size, gtk.ICON_LOOKUP_FORCE_SVG) return (icon and icon.get_filename()) def gconf_path(key): return '%s/%s' % (gconf_plugin_path, key) def read_gconf_values(values, keys): gc = gconf.client_get_default() for key in keys: val = gc.get_without_default(gconf_path(key)) if val.type == gconf.VALUE_FLOAT: values[key] = val.get_float() elif val.type == gconf.VALUE_INT: values[key] = val.get_int() elif val.type == gconf.VALUE_STRING: values[key] = val.get_string() elif val.type == gconf.VALUE_BOOL: values[key] = val.get_bool() # Parse color strings if 'color' in key: values['%s_r' % key] = int(values[key][ 1: 5], 16) / int('ffff', 16) values['%s_g' % key] = int(values[key][ 5: 9], 16) / int('ffff', 16) values['%s_b' % key] = int(values[key][ 9:13], 16) / int('ffff', 16) values['%s_a' % key] = int(values[key][13:17], 16) / int('ffff', 16) def reread_gconf_value(conf, keys, key): if key in keys: read_gconf_values(conf, [key]) class _ContextMenu(gtk.Menu): def __init__(self, desktop_control, configure_glade_file, shell): gtk.Menu.__init__(self) self.shell = shell self.show_player = gtk.CheckMenuItem('_Show Music Player', True) self.show_player.connect('activate', self.toggle_player_visibility) self.add(self.show_player) self.add(gtk.SeparatorMenuItem()) preferences = gtk.ImageMenuItem(gtk.STOCK_PREFERENCES) conf_dialog = ConfigDialog(configure_glade_file, gconf_plugin_path, self) preferences.connect('activate', self.show_preferences_dialog, desktop_control, configure_glade_file) self.add(preferences) self.show_all() def show(self, event): self.show_player.set_active(self.shell.props.visibility) self.popup(None, None, None, event.button, event.time) def toggle_player_visibility(self, menu_item): self.shell.props.visibility = menu_item.get_active() def show_preferences_dialog(self, menu_item, desktop_control, configure_glade_file): conf_dialog = ConfigDialog(configure_glade_file, gconf_plugin_path, desktop_control) conf_dialog.run() class DesktopControl(gtk.DrawingArea): def __init__(self, icons, shell, player, conf_glade): gtk.DrawingArea.__init__(self) self.connect("expose_event", self.expose) self.shell = shell self.cover_image = CoverImage(icons) self.song_info = SongInfo() self.desktop_buttons = DesktopButtons(icons, player) self.context_menu = _ContextMenu(self, conf_glade, shell) self.draw_border = False # Find and set up icon and font icon_theme = gtk.icon_theme_get_default() icon_theme.connect('changed', self.icon_theme_changed, [self.cover_image, self.desktop_buttons]) gc = gconf.client_get_default() gc.add_dir('/apps/nautilus/preferences', gconf.CLIENT_PRELOAD_NONE) gc.notify_add('/apps/nautilus/preferences/desktop_font', self.font_changed, [self.song_info]) self.add_events(gtk.gdk.ENTER_NOTIFY_MASK | gtk.gdk.LEAVE_NOTIFY_MASK | gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.BUTTON_PRESS_MASK) self.mouse_over = False self.hover_time_out = None self.connect('enter-notify-event', self.enter_leave) self.connect('leave-notify-event', self.enter_leave) self.connect('motion-notify-event', self.mouse_motion, self.desktop_buttons) self.connect('button-press-event', self.button_press, self.desktop_buttons) self.gconf_keys = ['background_color', 'roundness', 'hover_size', 'border', 'draw_reflection', 'reflection_height', 'reflection_intensity', 'blur', 'text_position'] self.conf = {} read_gconf_values(self.conf, self.gconf_keys) self.set_gconf_callbacks([self, self.cover_image, self.song_info, self.desktop_buttons]) def set_gconf_callbacks(self, affected): gc = gconf.client_get_default() for entry in gc.all_entries(gconf_plugin_path): path = entry.get_key() key = path.split('/')[-1] gc.notify_add(path, self.gconf_cb, {'key': key, 'affected': affected}) def gconf_cb(self, client, cnxn_id, entry, ud): for af in ud['affected']: reread_gconf_value(af.conf, af.gconf_keys, ud['key']) self.queue_draw() def button_press(self, w, e, affected): if e.button == 1: if not affected.button_press(): self.shell.props.visibility = not self.shell.props.visibility elif e.button == 3: self.context_menu.show(e) def mouse_motion(self, w, e, affected): if affected.set_mouse_position(self, e.x, e.y): self.queue_draw() def font_changed(self, client, cnxn_id, entry, affected): for a in affected: a.font_changed(entry.get_value().get_string()) self.queue_draw() def icon_theme_changed(self, icon_theme, affected): for a in affected: a.icon_theme_changed(icon_theme) def enter_leave(self, w, e): if self.hover_time_out: gobject.source_remove(self.hover_time_out) self.hover_time_out = None hover = e.type == gtk.gdk.ENTER_NOTIFY self.hover_time_out = gobject.timeout_add(350, self.set_hover, hover) def set_hover(self, hover): tmp = self.mouse_over self.mouse_over = hover if tmp != hover: self.queue_draw() def expose(self, widget, event): cc = self.window.cairo_create() cc.rectangle(event.area.x, event.area.y, event.area.width, event.area.height) cc.clip() self.draw(cc) def draw(self, cc): # Clear cairo context cc.set_source_rgba(0, 0, 0, 0) cc.set_operator(cairo.OPERATOR_SOURCE) cc.paint() # Scale the context so that the cover image area is 1 x 1 rect = self.get_allocation() if self.conf['draw_reflection']: cover_area_size = min(rect.width - self.conf['blur']/2, (rect.height - self.conf['blur']/2) / (1 + self.conf['reflection_height'])) else: cover_area_size = min(rect.width - self.conf['blur']/2, (rect.height - self.conf['blur']/2)) if self.conf['text_position'] in [POSITION_SW, POSITION_NW]: x_trans = int(round(rect.width - cover_area_size - self.conf['blur']/2)) else: x_trans = int(round(self.conf['blur']/2)) cc.translate(x_trans, 0) cc.scale(cover_area_size, cover_area_size) cc.push_group() self.song_info.draw(cc) if self.mouse_over: self.desktop_buttons.draw(cc) cc.save() cc.translate((1 - self.conf['hover_size']) / 2, self.conf['border']) cc.scale(self.conf['hover_size'], self.conf['hover_size']) if (self.conf['text_position'] in [POSITION_NW, POSITION_NE]) and (not self.mouse_over): if(self.cover_image.w > self.cover_image.h): cc.save() cc.translate(0, (self.cover_image.h - self.cover_image.w) / self.cover_image.w) self.cover_image.draw(cc, cover_area_size) if (self.conf['text_position'] in [POSITION_NW, POSITION_NE]) and (not self.mouse_over): if(self.cover_image.w > self.cover_image.h): cc.restore() if self.mouse_over: cc.restore() graphics = cc.pop_group() # Draw main graphics cc.set_source(graphics) cc.set_operator(cairo.OPERATOR_OVER) cc.paint() # Draw reflections if self.conf['draw_reflection']: cc.save() cc.set_operator(cairo.OPERATOR_ADD) cc.translate(0, 2.02) if (self.conf['text_position'] in [POSITION_NW, POSITION_NE]) and (not self.mouse_over): if(self.cover_image.w > self.cover_image.h): cc.save() cc.translate(0, 2 * (self.cover_image.h - self.cover_image.w) / self.cover_image.w) cc.scale(1, -1) cc.push_group() x_scale = cc.get_matrix()[0] r1 = int(self.conf['blur'] / 2 + 1.5) r0 = r1 - self.conf['blur'] - 1 bn = (self.conf['blur'] + 1)**2 for dx in xrange(r0, r1): for dy in xrange(r0, r1): cc.save() cc.translate(dx/x_scale, dy/x_scale) cc.set_source(graphics) cc.paint_with_alpha(1/bn) cc.restore() graphics = cc.pop_group() cc.set_source(graphics) shadow_mask = cairo.LinearGradient(0, 1 - self.conf['reflection_height'], 0, 1) shadow_mask.add_color_stop_rgba(0, 0, 0, 0, 0) shadow_mask.add_color_stop_rgba(1, 0, 0, 0, self.conf['reflection_intensity']) cc.mask(shadow_mask) if (self.conf['text_position'] in [POSITION_NW, POSITION_NE]) and (not self.mouse_over): if(self.cover_image.w > self.cover_image.h): cc.restore() cc.restore() # Input mask, only the cover image is clickable # Will, (and should) only work if parent is gtk.Window pixmask = gtk.gdk.Pixmap(None, int(cover_area_size), int(cover_area_size), 1) ccmask = pixmask.cairo_create() roundedrec(ccmask, 0, 0, cover_area_size, cover_area_size, self.conf['roundness']) ccmask.fill() self.get_parent().input_shape_combine_mask(pixmask, int(x_trans), 0) # Draw border if self.draw_border: cc.identity_matrix() cc.rectangle(0, 0, rect.width, rect.height) cc.set_line_width(2) cc.set_source_rgba(1, 1, 1, 0.35) cc.set_dash([10,10], 0) cc.stroke_preserve() cc.set_source_rgba(0, 0, 0, 0.35) cc.set_dash([10,10], 10) cc.stroke() def set_song(self, playing=False, cover_image=None, song_info=None): self.cover_image.set_image(cover_image) self.song_info.set_text(song_info) self.desktop_buttons.set_playing(playing) self.queue_draw() def set_draw_border(self, val=False): self.draw_border = val self.queue_draw() class SongInfo(): tags = {'title' : ['', ''], 'artist' : ['', ''], 'album' : ['', '']} font = gconf.client_get_default().get_string('/apps/nautilus/preferences/desktop_font') def __init__(self, song_info=None): self.set_text(song_info) self.gconf_keys = ['border', 'text_position', 'text_color', 'text_shadow_color'] self.conf = {} read_gconf_values(self.conf, self.gconf_keys) def font_changed(self, font): self.font = font def set_text(self, song_info): self.text = '' if song_info: for key in ('title', 'artist', 'album'): if song_info[key]: self.text += '%s%s%s\n' % (self.tags[key][0], song_info[key].replace('&', '&'), self.tags[key][1]) self.text = self.text[:-1] def draw(self, cc): if self.text: cc.save() x_scale = cc.get_matrix()[0] x_trans = cc.get_matrix()[4] cc.identity_matrix() layout = cc.create_layout() layout.set_markup(self.text) layout.set_font_description(pango.FontDescription(self.font)) txw, txh = layout.get_size() if self.conf['text_position'] in [POSITION_SW, POSITION_NW]: x_trans = x_trans - txw / pango.SCALE - x_scale * self.conf['border'] layout.set_alignment(pango.ALIGN_RIGHT) else: x_trans = x_trans + x_scale * (1 + self.conf['border']) layout.set_alignment(pango.ALIGN_LEFT) if self.conf['text_position'] in [POSITION_NE, POSITION_NW]: y_trans = x_scale * self.conf['border'] / 2 else: y_trans = x_scale * (1 - self.conf['border'] / 2) - txh / pango.SCALE cc.translate(x_trans, y_trans) # Draw text shadow cc.translate(1,1) cc.set_source_rgba(self.conf['text_shadow_color_r'], self.conf['text_shadow_color_g'], self.conf['text_shadow_color_b'], self.conf['text_shadow_color_a']) cc.show_layout(layout) # Draw text cc.translate(-1,-1) cc.set_source_rgba(self.conf['text_color_r'], self.conf['text_color_g'], self.conf['text_color_b'], self.conf['text_color_a']) cc.show_layout(layout) cc.restore() class DesktopButtons(): icon_keys = ['previous', 'play', 'next'] def __init__(self, icons, player): self.icons = icons self.player = player self.idata = {} for k in self.icon_keys: self.idata[(k, 'cairo_path')] = None self.idata[(k, 'hover')] = False self.icon_theme_changed(gtk.icon_theme_get_default()) self.playing = player.get_playing() self.gconf_keys = ['roundness', 'hover_size', 'border', 'background_color'] self.conf = {} read_gconf_values(self.conf, self.gconf_keys) def set_playing(self, playing): self.playing = playing def button_press(self): if self.idata[('previous', 'hover')]: self.player.do_previous() return True elif self.idata[('play', 'hover')]: self.player.playpause() return True elif self.idata[('next', 'hover')]: self.player.do_next() return True return False def set_mouse_position(self, w, x, y): redraw = False for k in self.icon_keys: if self.idata[(k, 'cairo_path')]: cc = w.window.cairo_create() cc.append_path(self.idata[(k, 'cairo_path')]) hover = cc.in_fill(x,y) if hover != self.idata[(k, 'hover')]: self.idata[(k, 'hover')] = hover redraw = True return redraw def icon_theme_changed(self, icon_theme): for k in self.icon_keys: self.idata[(k, 'icon_path')] = get_icon_path(icon_theme, self.icons[k], self.icons['size']) try: self.idata[(k, 'image')] = rsvg.Handle(file=self.idata[(k, 'icon_path')]) self.idata[(k, 'w')] = self.idata[(k, 'image')].props.width self.idata[(k, 'h')] = self.idata[(k, 'image')].props.height self.idata[(k, 'draw')] = self.draw_svg_icon except: try: self.idata[(k, 'image')] = gtk.gdk.pixbuf_new_from_file(self.idata[(k, 'icon_path')]) self.idata[(k, 'w')] = self.idata[(k, 'image')].get_width() self.idata[(k, 'h')] = self.idata[(k, 'image')].get_height() self.idata[(k, 'draw')] = self.draw_pixbuf_icon except: sys.exit('ERROR: No media icons found.') self.idata[(k, 'dim')] = max(self.idata[(k, 'w')], self.idata[(k, 'h')]) self.idata[(k, 'scale')] = 1 / self.idata[(k, 'dim')] def draw(self, cc): cc.save() cc.set_operator(cairo.OPERATOR_OVER) cc.set_source_rgba(self.conf['background_color_r'], self.conf['background_color_g'], self.conf['background_color_b'], self.conf['background_color_a'] + 0.1) roundedrec(cc, 0, 0, 1, 1, self.conf['roundness']) cc.fill() y = self.conf['hover_size'] + 2 * self.conf['border'] h = 1 - y - self.conf['border'] n = len(self.icon_keys) w = (1 - (2 + n - 1) * self.conf['border']) / n cc.translate(self.conf['border'], y) for k in self.icon_keys: self.draw_icon(cc, k, w, h, self.idata[(k, 'hover')]) cc.fill() cc.translate(self.conf['border'] + w, 0) cc.restore() def draw_icon(self, cc, key, w, h, hover): cc.save() cc.save() cc.scale(w, h) roundedrec(cc, 0, 0, 1, 1, self.conf['roundness']) cc.save() cc.identity_matrix() self.idata[(key, 'cairo_path')] = cc.copy_path() cc.restore() if hover: cc.set_source_rgba(1, 1, 1, 0.3) else: if self.playing and key == 'play': cc.set_source_rgba(0, 0, 0, 1) else: cc.set_source_rgba(0, 0, 0, 0.3) cc.fill() cc.restore() x = max(0, (w-h)/2) y = max(0, (h-w)/2) cc.translate(x, y) d = min(h, w) cc.scale(d, d) self.idata[(key, 'draw')](cc, key) cc.restore() def draw_svg_icon(self, cc, key): cc.push_group() cc.set_operator(cairo.OPERATOR_OVER) cc.scale(self.idata[(key, 'scale')], self.idata[(key, 'scale')]) self.idata[(key, 'image')].render_cairo(cc) cc.set_source(cc.pop_group()) roundedrec(cc, 0, 0, 1, 1, self.conf['roundness']) cc.fill() def draw_pixbuf_icon(self, cc, key): cc.scale(self.idata[(key, 'scale')], self.idata[(key, 'scale')]) cc.set_source_pixbuf(self.idata[(key, 'image')], 0, 0) roundedrec(cc, 0, 0, self.idata[(key, 'w')], self.idata[(key, 'h')], self.conf['roundness']) cc.fill() class CoverImage(): def __init__(self, icons): self.icons = icons self.icon_theme_changed(gtk.icon_theme_get_default()) self.gconf_keys = ['roundness', 'background_color'] self.conf = {} read_gconf_values(self.conf, self.gconf_keys) def icon_theme_changed(self, icon_theme): not_playing_image = get_icon_path(icon_theme, self.icons['not_playing'], self.icons['size']) unknown_cover_image = get_icon_path(icon_theme, self.icons['unknown_cover'], self.icons['size']) # Check if shown image needs to be updated image = False if self.get_current_image() == self.get_not_playing_image(): image = None elif self.get_current_image() == self.get_unknown_cover_image(): image = UNKNOWN_COVER self.set_not_playing_image(not_playing_image) self.set_unknown_cover_image(unknown_cover_image) if image != False: self.set_image(image) def set_image(self, image=None): if not image: image = self.get_not_playing_image() if image == UNKNOWN_COVER: image = self.get_unknown_cover_image() if not image: self.draw = self.draw_background else: try: self.image = rsvg.Handle(file=image) self.w = self.image.props.width self.h = self.image.props.height self.draw = self.draw_svg except: try: self.image = gtk.gdk.pixbuf_new_from_file(image) self.w = self.image.get_width() self.h = self.image.get_height() self.draw = self.draw_pixbuf except: pass self.dim = max(self.w, self.h) self.x = (self.dim - self.w) / 2 self.y = self.dim - self.h self.scale = 1 / self.dim self.current_image = image def draw_background(self, cc, size = None): cc.save() cc.set_source_rgba(self.conf['background_color_r'], self.conf['background_color_g'], self.conf['background_color_b'], self.conf['background_color_a']) roundedrec(cc, 0, 0, 1, 1, self.conf['roundness']) cc.fill() cc.restore() def draw_svg(self, cc, size = None): cc.save() cc.scale(self.scale, self.scale) cc.push_group() cc.set_operator(cairo.OPERATOR_OVER) cc.set_source_rgba(self.conf['background_color_r'], self.conf['background_color_g'], self.conf['background_color_b'], self.conf['background_color_a']) cc.paint() cc.translate(self.x, self.y) self.image.render_cairo(cc) cc.set_source(cc.pop_group()) roundedrec(cc, self.x, self.y, self.w, self.h, self.conf['roundness']) cc.fill() cc.restore() def draw_pixbuf(self, cc, size = None): img_scale = size/self.dim scaled_image = self.image.scale_simple(int(self.w * img_scale + 1.5), int(self.h * img_scale + 1.5), gtk.gdk.INTERP_TILES) cc.save() cc.set_operator(cairo.OPERATOR_SOURCE) cc.scale(self.scale, self.scale) roundedrec(cc, self.x, self.y, self.w, self.h, self.conf['roundness']) cc.set_source_rgba(self.conf['background_color_r'], self.conf['background_color_g'], self.conf['background_color_b'], self.conf['background_color_a']) cc.fill_preserve() cc.set_operator(cairo.OPERATOR_OVER) cc.scale(1/img_scale, 1/img_scale) cc.set_source_pixbuf(scaled_image, img_scale * self.x, img_scale * self.y) cs = cc.get_source() try: ## 3 = cairo.EXTEND_PAD, but doesn't appear in pycairo before 1.6 cs.set_extend(3) except AttributeError : cs.set_extend(cairo.EXTEND_REFLECT) cc.fill() cc.restore() def set_not_playing_image(self, image): self.not_playing_image = image def get_not_playing_image(self): try: return self.not_playing_image except: return None def set_unknown_cover_image(self, image): self.unknown_cover_image = image def get_unknown_cover_image(self): try: return self.unknown_cover_image except: return None def get_current_image(self): try: return self.current_image except: return None def set_current_image(self, image): self.current_image = image