~ubuntu-branches/ubuntu/quantal/rhythmbox/quantal

« back to all changes in this revision

Viewing changes to plugins/artdisplay/artdisplay.py

  • Committer: Package Import Robot
  • Author(s): Michael Biebl
  • Date: 2012-06-10 16:19:59 UTC
  • mto: (2.1.30 sid)
  • mto: This revision was merged to the branch mainline in revision 215.
  • Revision ID: package-import@ubuntu.com-20120610161959-3wr8avyq3he0uz68
ImportĀ upstreamĀ versionĀ 2.97

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*-
2
 
#
3
 
# Copyright (C) 2006 - James Livingston
4
 
#
5
 
# This program is free software; you can redistribute it and/or modify
6
 
# it under the terms of the GNU General Public License as published by
7
 
# the Free Software Foundation; either version 2, or (at your option)
8
 
# any later version.
9
 
#
10
 
# The Rhythmbox authors hereby grant permission for non-GPL compatible
11
 
# GStreamer plugins to be used and distributed together with GStreamer
12
 
# and Rhythmbox. This permission is above and beyond the permissions granted
13
 
# by the GPL license by which Rhythmbox is covered. If you modify this code
14
 
# you may extend this exception to your version of the code, but you are not
15
 
# obligated to do so. If you do not wish to do so, delete this exception
16
 
# statement from your version.
17
 
#
18
 
# This program is distributed in the hope that it will be useful,
19
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
20
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21
 
# GNU General Public License for more details.
22
 
#
23
 
# You should have received a copy of the GNU General Public License
24
 
# along with this program; if not, write to the Free Software
25
 
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
26
 
 
27
 
import gi
28
 
 
29
 
from warnings import warn
30
 
 
31
 
import rb
32
 
from gi.repository import GObject, Gtk, Gdk, GdkPixbuf, Gio, Peas
33
 
from gi.repository import RB
34
 
 
35
 
import gettext
36
 
gettext.install('rhythmbox', RB.locale_dir())
37
 
 
38
 
import urllib
39
 
 
40
 
FADE_STEPS = 10
41
 
FADE_TOTAL_TIME = 1000
42
 
ART_MISSING_ICON = 'rhythmbox-missing-artwork'
43
 
WORKING_DELAY = 500
44
 
THROBBER_RATE = 10
45
 
THROBBER = 'process-working'
46
 
ASPECT_RATIO_MIN = 0.9
47
 
ASPECT_RATIO_MAX = 1.1
48
 
 
49
 
def merge_pixbufs (old_pb, new_pb, reserve_pb, step, width, height, mode=GdkPixbuf.InterpType.BILINEAR):
50
 
        if width <= 1 and height <= 1:
51
 
                return None
52
 
        if old_pb is None:
53
 
                if new_pb is None:
54
 
                        return reserve_pb
55
 
                else:
56
 
                        return new_pb.scale_simple (width, height, mode)
57
 
        elif step == 0.0:
58
 
                return old_pb.scale_simple (width, height, mode)
59
 
        elif new_pb is None:
60
 
                if reserve_pb is None:
61
 
                        return None
62
 
                new_pb = reserve_pb
63
 
        sw, sh = (float (width)) / new_pb.props.width, (float (height)) / new_pb.props.height
64
 
        alpha = int (step * 255)
65
 
        ret = old_pb.scale_simple (width, height, mode)
66
 
        new_pb.composite (ret, 0, 0, width, height, 0, 0, sw, sh, mode, alpha)
67
 
        return ret
68
 
 
69
 
def merge_with_background (pixbuf, bgcolor, pad_if_not_near_square):
70
 
 
71
 
        if pixbuf is None:
72
 
                return pixbuf
73
 
        has_alpha = pixbuf.get_has_alpha ()
74
 
        width, height = pixbuf.props.width, pixbuf.props.height
75
 
        if pad_if_not_near_square and (height < width * ASPECT_RATIO_MIN or
76
 
                                       height > width * ASPECT_RATIO_MAX):
77
 
                rw, rh = max (width, height), max (width, height)
78
 
                left, top = (rw - width) // 2, (rh - height) // 2
79
 
        else:
80
 
                if not has_alpha:
81
 
                        return pixbuf
82
 
                rw, rh, left, top = width, height, 0, 0
83
 
        ret = GdkPixbuf.Pixbuf.new (GdkPixbuf.Colorspace.RGB, False, 8, rw, rh)
84
 
        ret.fill ((int(bgcolor.red * 255) << 24) | (int(bgcolor.green * 255) << 16) | (int(bgcolor.blue * 255) << 8) | int(bgcolor.alpha * 255))
85
 
        if has_alpha:
86
 
                pixbuf.composite (ret, left, top, width, height, left, top, 1.0, 1.0, GdkPixbuf.InterpType.NEAREST, 255)
87
 
        else:
88
 
                pixbuf.copy_area (0, 0, width, height, ret, left, top)
89
 
        return ret
90
 
 
91
 
class FadingImage (Gtk.Misc):
92
 
        __gsignals__ = {
93
 
                'get-max-size' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_INT, ())
94
 
        }
95
 
        def __init__ (self, missing_image):
96
 
                Gtk.Misc.__init__(self)
97
 
                self.sc_id = self.connect('screen-changed', self.screen_changed)
98
 
                self.sa_id = self.connect('size-allocate', self.size_allocate_cb)
99
 
                self.resize_id, self.fade_id, self.anim_id = 0, 0, 0
100
 
                self.missing_image = missing_image
101
 
                self.size = 100
102
 
                self.screen_changed (self, None)
103
 
                self.old_pixbuf, self.new_pixbuf = None, None
104
 
                self.merged_pixbuf, self.missing_pixbuf = None, None
105
 
                self.fade_step = 0.0
106
 
                self.anim, self.anim_frames, self.anim_size = None, None, 0
107
 
 
108
 
        def disconnect_handlers (self):
109
 
                self.disconnect(self.sc_id)
110
 
                self.disconnect(self.sa_id)
111
 
                self.icon_theme.disconnect(self.tc_id)
112
 
                for id in self.resize_id, self.fade_id, self.anim_id:
113
 
                        if id != 0:
114
 
                                GObject.source_remove (id)
115
 
 
116
 
        def screen_changed (self, widget, old_screen):
117
 
                if old_screen:
118
 
                        self.icon_theme.disconnect (self.tc_id)
119
 
                self.icon_theme = Gtk.IconTheme.get_for_screen (self.get_screen ())
120
 
                self.tc_id = self.icon_theme.connect ('changed', self.theme_changed)
121
 
                self.theme_changed (self.icon_theme)
122
 
 
123
 
        def reload_anim_frames (self):
124
 
                icon_info = self.icon_theme.lookup_icon (THROBBER, -1, 0)
125
 
                size = icon_info.get_base_size ()
126
 
                icon = GdkPixbuf.Pixbuf.new_from_file (icon_info.get_filename ())
127
 
                self.anim_frames = [ # along, then down
128
 
                                icon.new_subpixbuf (x * size, y * size, size, size)
129
 
                                for y in range (int (icon.props.height / size))
130
 
                                for x in range (int (icon.props.width / size))]
131
 
                self.anim_size = size
132
 
 
133
 
        def theme_changed (self, icon_theme):
134
 
                try:
135
 
                        self.reload_anim_frames ()
136
 
                except Exception, e:
137
 
                        warn ("Throbber animation not loaded: %s" % e, Warning)
138
 
                self.reload_util_pixbufs ()
139
 
 
140
 
        def reload_util_pixbufs (self):
141
 
                if self.size <= 1:
142
 
                        return
143
 
                try:
144
 
                        missing_pixbuf = self.icon_theme.load_icon (ART_MISSING_ICON, self.size, 0)
145
 
                except:
146
 
                        try:
147
 
                                missing_pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size (self.missing_image, self.size, self.size)
148
 
                        except Exception, e:
149
 
                                warn ("Missing artwork icon not found: %s" % e, Warning)
150
 
                                return
151
 
 
152
 
                bgcolor = self.get_style_context().get_background_color(Gtk.StateType.NORMAL)
153
 
                self.missing_pixbuf = merge_with_background (missing_pixbuf, bgcolor, False)
154
 
 
155
 
        def size_allocate_cb (self, widget, allocation):
156
 
                if self.resize_id == 0:
157
 
                        self.resize_id = GObject.idle_add (self.after_resize)
158
 
 
159
 
                max_size = self.emit ('get-max-size')
160
 
                self.size = min (self.get_allocated_width (), max_size)
161
 
 
162
 
        def after_resize (self):
163
 
                self.reload_util_pixbufs ()
164
 
                self.merged_pixbuf = None
165
 
                self.resize_id = 0
166
 
                self.queue_draw ()
167
 
                return False
168
 
 
169
 
        def do_get_request_mode(self):
170
 
                return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH
171
 
 
172
 
        def do_get_preferred_width(self):
173
 
                # maybe set minimum width here?
174
 
                return (0, 0)
175
 
 
176
 
        def do_get_preferred_height_for_width(self, width):
177
 
                max_size = self.emit ('get-max-size')
178
 
                size = min(self.get_allocated_width(), max_size)
179
 
                return (size, size)
180
 
 
181
 
        def do_draw (self, cr):
182
 
                if not self.ensure_merged_pixbuf ():
183
 
                        return False
184
 
 
185
 
                if self.merged_pixbuf.props.width != self.size:
186
 
                        draw_pb = self.merged_pixbuf.scale_simple (self.size, self.size, GdkPixbuf.InterpType.NEAREST)
187
 
                else:
188
 
                        draw_pb = self.merged_pixbuf
189
 
 
190
 
                # center the image if we're wider than we are tall
191
 
                pad = (self.get_allocation().width - self.size) / 2
192
 
 
193
 
                left = pad
194
 
                right = pad + self.size
195
 
                top = 0
196
 
                bottom = self.size
197
 
                if right > left and bottom > top:
198
 
                        Gdk.cairo_set_source_pixbuf(cr, draw_pb, pad, 0)
199
 
                        cr.rectangle(left, top, right - left, bottom - top)
200
 
                        cr.fill()
201
 
 
202
 
                if self.anim:
203
 
                        x, y, w, h = self.anim_rect ()
204
 
                        Gdk.cairo_set_source_pixbuf(cr, self.anim, max(0, x), max(0, y))
205
 
                        cr.rectangle(max(0, x), max(0, y), w, h)
206
 
                        cr.fill()
207
 
 
208
 
                return False
209
 
 
210
 
        def anim_rect (self):
211
 
                alloc_width = self.get_allocated_width()
212
 
                alloc_height = self.get_allocated_height()
213
 
                return ((alloc_width - self.anim_size) / 2,
214
 
                        (alloc_height - self.anim_size) / 2,
215
 
                        min (self.anim_size, alloc_width),
216
 
                        min (self.anim_size, alloc_height))
217
 
 
218
 
        def ensure_merged_pixbuf (self):
219
 
                if self.merged_pixbuf is None:
220
 
                        alloc_width = self.get_allocated_width()
221
 
                        alloc_height = self.get_allocated_height()
222
 
                        self.merged_pixbuf = merge_pixbufs (self.old_pixbuf, self.new_pixbuf, self.missing_pixbuf, self.fade_step, alloc_width, alloc_height)
223
 
                return self.merged_pixbuf
224
 
 
225
 
        def render_overlay (self):
226
 
                ret = self.ensure_merged_pixbuf ()
227
 
                if ret and self.anim:
228
 
                        if ret is self.missing_pixbuf: ret = ret.copy ()
229
 
                        x, y, w, h = self.anim_rect ()
230
 
                        self.anim.composite (ret, max (x, 0), max (y, 0), w, h, x, y, 1, 1, GdkPixbuf.InterpType.BILINEAR, 255)
231
 
                return ret
232
 
 
233
 
        def fade_art (self, first_time):
234
 
                self.fade_step += 1.0 / FADE_STEPS
235
 
                if self.fade_step > 0.999:
236
 
                        self.old_pixbuf = None
237
 
                        self.fade_id = 0
238
 
                self.merged_pixbuf = None
239
 
                if first_time:
240
 
                        self.fade_id = GObject.timeout_add ((FADE_TOTAL_TIME / FADE_STEPS), self.fade_art, False)
241
 
                        return False
242
 
                self.queue_resize ()
243
 
                return (self.fade_step <= 0.999)
244
 
 
245
 
        def animation_advance (self, counter, first_time):
246
 
                self.anim = self.anim_frames[counter[0]]
247
 
                counter[0] = (counter[0] + 1) % len(self.anim_frames)
248
 
                x, y, w, h = self.anim_rect ()
249
 
                self.queue_draw_area (max (x, 0), max (y, 0), w, h)
250
 
                if first_time:
251
 
                        self.anim_id = GObject.timeout_add (int (1000 / THROBBER_RATE), self.animation_advance, counter, False)
252
 
                        return False
253
 
                return True
254
 
 
255
 
        def set_current_art (self, pixbuf, working):
256
 
                if self.props.visible and self.get_allocated_width() > 1:
257
 
                        self.old_pixbuf = self.render_overlay ()
258
 
                else:
259
 
                        self.old_pixbuf = None  # don't fade
260
 
 
261
 
                bgcolor = self.get_style_context().get_background_color(Gtk.StateType.NORMAL)
262
 
                self.new_pixbuf = merge_with_background (pixbuf, bgcolor, True)
263
 
                self.merged_pixbuf = None
264
 
                self.fade_step = 0.0
265
 
                self.anim = None
266
 
                if self.fade_id != 0:
267
 
                        GObject.source_remove (self.fade_id)
268
 
                        self.fade_id = 0
269
 
                if self.old_pixbuf is not None:
270
 
                        self.fade_id = GObject.timeout_add (working and WORKING_DELAY or (FADE_TOTAL_TIME / FADE_STEPS), self.fade_art, working)
271
 
                if working and self.anim_id == 0 and self.anim_frames:
272
 
                        self.anim_id = GObject.timeout_add (WORKING_DELAY, self.animation_advance, [0], True)
273
 
                if not working and self.anim_id != 0:
274
 
                        GObject.source_remove (self.anim_id)
275
 
                        self.anim_id = 0
276
 
                self.queue_resize ()
277
 
 
278
 
GObject.type_register (FadingImage)
279
 
 
280
 
 
281
 
class ArtDisplayWidget (FadingImage):
282
 
 
283
 
        def __init__ (self, missing_image):
284
 
                super (ArtDisplayWidget, self).__init__ (missing_image)
285
 
                self.set_padding (0, 5)
286
 
                self.qt_id = self.connect ('query-tooltip', self.query_tooltip)
287
 
                self.props.has_tooltip = True
288
 
                self.current_entry, self.working = None, False
289
 
                self.current_pixbuf, self.current_uri = None, None
290
 
 
291
 
        def disconnect_handlers (self):
292
 
                super (ArtDisplayWidget, self).disconnect_handlers ()
293
 
 
294
 
        def query_tooltip (self, widget, x, y, keyboard_mode, tooltip):
295
 
                if (self.tooltip_image, self.tooltip_text) != (None, None):
296
 
                        tooltip.set_text(self.tooltip_text)
297
 
                        tooltip.set_icon(self.tooltip_image)
298
 
                        return True
299
 
                else:
300
 
                        return False
301
 
 
302
 
        def set (self, entry, pixbuf, uri, tooltip_image, tooltip_text, working):
303
 
                self.current_entry = entry
304
 
                self.current_pixbuf = pixbuf
305
 
                self.current_uri = uri
306
 
                self.set_current_art (pixbuf, working)
307
 
 
308
 
                self.tooltip_image = None
309
 
                if not self.current_entry:
310
 
                        self.tooltip_text = None
311
 
                elif working:
312
 
                        self.tooltip_text = _("Searching...")
313
 
                elif (tooltip_image, tooltip_text) != (None, None):
314
 
                        self.tooltip_image = tooltip_image
315
 
                        self.tooltip_text = tooltip_text
316
 
                else:
317
 
                        self.tooltip_text = None
318
 
 
319
 
 
320
 
GObject.type_register (ArtDisplayWidget)
321
 
 
322
 
 
323
 
class ArtDisplayPlugin (GObject.GObject, Peas.Activatable):
324
 
        __gtype_name__ = 'ArtDisplayPlugin'
325
 
        object = GObject.property(type=GObject.GObject)
326
 
 
327
 
        def __init__ (self):
328
 
                GObject.GObject.__init__ (self)
329
 
 
330
 
        def do_activate (self):
331
 
                shell = self.object
332
 
                sp = shell.props.shell_player
333
 
                self.player_cb_ids = (
334
 
                        sp.connect ('playing-song-changed', self.playing_entry_changed),
335
 
                        sp.connect ('playing-changed', self.playing_changed)
336
 
                )
337
 
                self.art_store = RB.ExtDB(name="album-art")
338
 
                self.art_widget = ArtDisplayWidget (rb.find_plugin_file (self, ART_MISSING_ICON + ".svg"))
339
 
                self.art_widget.connect ('get-max-size', self.get_max_art_size)
340
 
                self.art_widget.connect ('button-press-event', self.on_button_press)
341
 
                self.art_container = Gtk.VBox ()
342
 
                self.art_container.pack_start (self.art_widget, True, True, 6)
343
 
                shell.add_widget (self.art_container, RB.ShellUILocation.SIDEBAR, False, True)
344
 
                self.current_entry, self.current_pixbuf = None, None
345
 
                self.playing_entry_changed (sp, sp.get_playing_entry ())
346
 
 
347
 
        def do_deactivate (self):
348
 
 
349
 
                shell = self.object
350
 
                sp = shell.props.shell_player
351
 
                for id in self.player_cb_ids:
352
 
                        sp.disconnect (id)
353
 
                self.player_cb_ids = ()
354
 
 
355
 
                shell.remove_widget (self.art_container, RB.ShellUILocation.SIDEBAR)
356
 
                self.art_widget.disconnect_handlers ()
357
 
                self.art_widget = None
358
 
                self.art_container = None
359
 
 
360
 
        def playing_changed (self, sp, playing):
361
 
                self.set_entry(sp.get_playing_entry ())
362
 
 
363
 
        def playing_entry_changed (self, sp, entry):
364
 
                self.set_entry(entry)
365
 
 
366
 
        def set_entry (self, entry):
367
 
                if rb.entry_equal(entry, self.current_entry):
368
 
                        return
369
 
 
370
 
                self.art_widget.set (entry, None, None, None, None, True)
371
 
                self.art_container.show_all ()
372
 
                self.current_entry = entry
373
 
                self.current_pixbuf = None
374
 
 
375
 
                if entry is not None:
376
 
                        key = entry.create_ext_db_key (RB.RhythmDBPropType.ALBUM)
377
 
                        self.art_store.request(key, self.art_store_request_cb, entry)
378
 
 
379
 
        def art_store_request_cb(self, key, filename, data, entry):
380
 
                if rb.entry_equal(entry, self.current_entry) is False:
381
 
                        # track changed while we were searching
382
 
                        return
383
 
 
384
 
                if isinstance(data, GdkPixbuf.Pixbuf):
385
 
                        self.current_pixbuf = data
386
 
                        uri = "file://" + urllib.pathname2url(filename)
387
 
                        self.art_widget.set (entry, self.current_pixbuf, uri, None, None, False)
388
 
                else:
389
 
                        self.art_widget.set (entry, None, None, None, None, False)
390
 
 
391
 
        def get_max_art_size (self, widget):
392
 
                # limit the art image to a third of the window height to prevent it from
393
 
                # forcing the window to resize, obscuring everything else, and so on.
394
 
                shell = self.object
395
 
                (width, height) = shell.props.window.get_size()
396
 
                return height / 3
397
 
 
398
 
        def on_button_press (self, widget, event):
399
 
                # on double clicks, open the cover image (if there is one) in the default
400
 
                # image viewer
401
 
 
402
 
                doubleclick = Gdk.EventType._2BUTTON_PRESS
403
 
                if event.type != doubleclick or event.button != 1:
404
 
                        return
405
 
 
406
 
                if self.art_widget.current_uri is None:
407
 
                        return
408
 
 
409
 
                f = Gio.file_new_for_uri(self.art_widget.current_uri)
410
 
                shell = self.object
411
 
                Gtk.show_uri(shell.props.window.get_screen(), f.get_uri(), event.time)