--- plugins/artdisplay/artdisplay/__init__.py	2007/01/21 18:45:05	1.1
+++ plugins/artdisplay/artdisplay/__init__.py	2007/03/03 23:50:55
@@ -18,19 +18,40 @@
 
 import rhythmdb, rb
 import gtk, gobject
+import pango
 from warnings import warn
 
 from CoverArtDatabase import CoverArtDatabase
+from CoverArtDatabase import ART_SEARCHES_LOCAL, ART_SEARCHES_REMOTE
 
+ui_str = """
+<ui>
+	<popup name="CoverArtWidgetPopup">
+		<menuitem name="CopyCover" action="CopyCover"/>
+		<menuitem name="PasteCover" action="PasteCover"/>
+		<separator/>
+		<menuitem name="SetCover" action="SetCover"/>
+		<menuitem name="SearchCover" action="SearchCover"/>
+	</popup>
+</ui>
+"""
 
 FADE_STEPS = 10
 FADE_TOTAL_TIME = 1000
 ART_MISSING_ICON = 'rhythmbox-missing-artwork'
+ART_MISSING_FILE = None
 WORKING_DELAY = 500
 THROBBER_RATE = 10
 THROBBER = 'gnome-spinner'
 ASPECT_RATIO_MIN = 0.9
 ASPECT_RATIO_MAX = 1.1
+PIXBUF_URI_KEY = 'rhythmbox-artdisplay-uri'
+
+clipboard = gtk.Clipboard ()
+if 'wait_for_image' not in dir (clipboard):
+	# hack for pre pygtk-2.10
+	clipboard.wait_for_image = lambda: None
+	clipboard.wait_is_image_available = lambda: False
 
 def merge_pixbufs (old_pb, new_pb, reserve_pb, step, width, height, mode=gtk.gdk.INTERP_BILINEAR):
 	if width <= 1 and height <= 1:
@@ -73,10 +94,37 @@ def merge_with_background (pixbuf, bgcol
 		pixbuf.copy_area (0, 0, width, height, ret, left, top)
 	return ret
 
+def pixbuf_scaled_keep_aspect (pixbuf, width, height, interp=gtk.gdk.INTERP_BILINEAR):
+	(w, h) = (pixbuf.props.width, pixbuf.props.height)
+	if w <= width and h <= height:
+		return pixbuf
+	elif w <= width:
+		return pixbuf.scale_simple ((w * height) // h, height, interp)
+	elif h <= height:
+		return pixbuf.scale_simple (width, (h * width) // w, interp)
+	scale = min (float (width) / w, float (height) / h)
+	return pixbuf.scale_simple (int (w * scale), int (h * scale), interp)
+
+def load_pixbuf_from_uri (uri, callback, *args):
+	if not uri:
+		callback (None, *args)
+		return
+	def loader_cb (data):
+		pixbuf = None
+		if data and len (data) >= 1000:
+			pbl = gtk.gdk.PixbufLoader ()
+			try:
+				if pbl.write (data) and pbl.close ():
+					pixbuf = pbl.get_pixbuf ()
+			except gobject.GError:
+				pass
+		callback (pixbuf, *args)
+	rb.Loader ().get_url (uri, loader_cb)
+
 class FadingImage (gtk.Misc):
 	__gsignals__ = { 'size-allocate': 'override' }
 	def __init__ (self, missing_image):
-		gobject.GObject.__init__ (self)
+		super (FadingImage, self).__init__ ()
 		self.sc_id = self.connect('screen-changed', self.screen_changed)
 		self.ex_id = self.connect ('expose-event', self.expose)
 		self.sr_id = self.connect ('size-request', self.size_request)
@@ -183,7 +231,10 @@ class FadingImage (gtk.Misc):
 	def render_overlay (self):
 		ret = self.ensure_merged_pixbuf ()
 		if ret and self.anim:
-			if ret is self.missing_pixbuf: ret = ret.copy ()
+			if ret.props.width != self.size:
+				ret = ret.scale_simple (self.size, self.size, gtk.gdk.INTERP_NEAREST)
+			elif ret is self.missing_pixbuf:
+				ret = ret.copy ()
 			x, y, w, h = self.anim_rect ()
 			self.anim.composite (ret, max (x, 0), max (y, 0), w, h, x, y, 1, 1, gtk.gdk.INTERP_BILINEAR, 255)
 		return ret
@@ -235,29 +286,144 @@ gobject.type_register (FadingImage)
 
 class ArtDisplayWidget (FadingImage):
 	__gsignals__ = {
-			'pixbuf-dropped' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (rhythmdb.Entry, gtk.gdk.Pixbuf)),
-			'uri-dropped' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (rhythmdb.Entry, gobject.TYPE_STRING))
+			'copy-cover' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,)),
+			'paste-cover' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,)),
+			'set-cover' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,)),
+			'search-cover' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,)),
+			}
+	__gproperties__ = {
+			'shell' : (rb.Shell, 'shell', 'RbShell this is in', gobject.PARAM_CONSTRUCT | gobject.PARAM_WRITABLE),
+			'entries' : (gobject.type_from_name('GValueArray'), 'entries', 'List of entries this displays artwork for', gobject.PARAM_READWRITE),
+			'working' : (gobject.TYPE_BOOLEAN, 'working', 'Whether a search is in progress', False, gobject.PARAM_READWRITE),
+			'pixbuf' : (gtk.gdk.Pixbuf, 'pixbuf', 'Image being displayed', gobject.PARAM_READWRITE),
+			'uri' : (gobject.TYPE_STRING, 'uri', 'URI of image being displayed', None, gobject.PARAM_READWRITE),
 			}
 
-	def __init__ (self, missing_image):
-		super (ArtDisplayWidget, self).__init__ (missing_image)
+	def __init__ (self):
+		super (ArtDisplayWidget, self).__init__ (ART_MISSING_FILE)
 		self.set_padding (0, 5)
 		self.ddg_id = self.connect ('drag-data-get', self.drag_data_get)
 		self.ddr_id = self.connect ('drag-data-received', self.drag_data_received)
-		self.current_entry, self.working = None, False
-		self.current_pixbuf, self.current_uri = None, None
+		self.entries, self.working = [], False
+		self.shell, self.pixbuf, self.uri = None, None, None
+		self.art_db = CoverArtDatabase ()
 
 		self.tooltips = gtk.Tooltips ()
 		self.tooltips.enable ()
-	
+
 	def disconnect_handlers (self):
  		super (ArtDisplayWidget, self).disconnect_handlers ()
 		self.disconnect (self.ddg_id)
 		self.disconnect (self.ddr_id)
+		if self.shell:
+			db = self.shell.get_property ("db")
+			db.disconnect (self.eemn_art_id)
+
+	def do_get_property (self, property):
+		if property.name == 'entries':
+			return self.entries
+		elif property.name == 'working':
+			return self.working
+		elif property.name == 'pixbuf':
+			return self.pixbuf
+		elif property.name == 'uri':
+			return self.uri
+		else:
+			raise AttributeError, 'unknown property %s' % property.name
 	
+	def do_set_property (self, property, value):
+		if property.name == 'shell':
+			if self.shell:
+				db = self.shell.get_property ("db")
+				db.disconnect (self.eemn_art_id)
+			self.shell = value
+			if self.shell:
+				db = self.shell.get_property ("db")
+				self.eemn_art_id = db.connect_after ('entry-extra-metadata-notify::rb:coverArt', self.cover_art_notify)
+		elif property.name == 'entries':
+			if self.entries != value:
+				self.entries = value
+				self.props.pixbuf, self.props.uri = None, None
+				self.props.working = bool (self.entries)
+				self.update_tooltips ()
+				if self.entries:
+					db = self.shell.get_property ("db")
+					pixbuf = db.entry_request_extra_metadata (self.entries[0], 'rb:coverArt')
+					if pixbuf:
+						self.cover_art_notify (db, self.entries[0], 'rb:coverArt', pixbuf)
+		elif property.name == 'working':
+			self.working = value
+			self.set_current_art (self.pixbuf, self.working)
+			self.update_tooltips ()
+		elif property.name == 'pixbuf':
+			self.pixbuf = value
+			self.set_current_art (self.pixbuf, self.working)
+			self.update_dnd_targets ()
+		elif property.name == 'uri':
+			self.uri = value
+			self.update_dnd_targets ()
+		else:
+			raise AttributeError, 'unknown property %s' % property.name
+
+	def do_copy_cover (self, item_or_action):
+		targets = None
+		if self.pixbuf:
+			targets = gtk.target_list_add_image_targets (targets, writable=True)
+		if self.uri:
+			targets = gtk.target_list_add_uri_targets (targets)
+			targets = gtk.target_list_add_text_targets (targets)
+		def get_func (clipboard, selection_data, info, (pixbuf, uri)):
+			if pixbuf:
+				selection_data.set_pixbuf (pixbuf)
+			if uri:
+				selection_data.set_uris ([uri])
+				selection_data.set_text (uri)
+		clipboard.set_with_data (targets, get_func, lambda *args: None, (self.pixbuf, self.uri))
+	def emit_copy_cover (self, item_or_action):
+		self.emit ('copy-cover', item_or_action)
+
+	def do_paste_cover (self, item_or_action):
+		db = self.shell.get_property ("db")
+		pixbuf = clipboard.wait_for_image ()
+		if pixbuf:
+			for entry in self.entries:
+				self.art_db.set_pixbuf (db, entry, pixbuf, self.on_get_pixbuf_completed)
+			return
+		text = clipboard.wait_for_text ()
+		if text:
+			for entry in self.entries:
+				self.art_db.set_pixbuf_from_uri (db, entry, text, self.on_get_pixbuf_completed)
+	def emit_paste_cover (self, item_or_action):
+		self.emit ('paste-cover', item_or_action)
+
+	def do_set_cover (self, widget_or_action):
+		entries = self.entries
+		db = self.shell.get_property ("db")
+		acd = ArtChooserDialog (self.get_toplevel ())
+		if acd.run () == gtk.RESPONSE_ACCEPT:
+			for entry in entries:
+				self.art_db.set_pixbuf_from_uri (db, entry, acd.get_uri (), self.on_get_pixbuf_completed)
+		acd.destroy ()
+	def emit_set_cover (self, widget_or_action):
+		self.emit ('set-cover', widget_or_action)
+
+	def do_search_cover (self, widget_or_action):
+		if not self.entries:
+			return
+		entries = self.entries
+		asd = ArtSearchDialog (self.get_toplevel ())
+		db = self.shell.get_property ("db")
+		asd.begin_search (db, entries[0], rb.Loader ())
+		if asd.run () == gtk.RESPONSE_ACCEPT:
+			for entry in entries:
+				self.art_db.set_pixbuf_from_uri (db, entry, asd.layout.selected_uri, self.on_get_pixbuf_completed)
+		asd.destroy ()
+	def emit_search_cover (self, widget_or_action):
+		self.emit ('search-cover', widget_or_action)
+
 	def update_dnd_targets (self):
 		targets = None
-		if self.current_entry:
+		if self.entries:
 			targets = gtk.target_list_add_image_targets (targets)
 			targets = gtk.target_list_add_uri_targets (targets)
 			targets = gtk.target_list_add_text_targets (targets)
@@ -267,56 +433,365 @@ class ArtDisplayWidget (FadingImage):
 			self.drag_dest_unset ()
 
 		targets = None
-		if self.current_pixbuf:
+		if self.pixbuf:
 			targets = gtk.target_list_add_image_targets (targets, writable=True)
-		if self.current_uri:
+		if self.uri:
 			targets = gtk.target_list_add_uri_targets (targets)
 		if targets:
 			self.drag_source_set (gtk.gdk.BUTTON1_MASK, targets, gtk.gdk.ACTION_COPY)
 		else:
 			self.drag_source_unset ()
 
-	def update_tooltips (self, working):
-		if not self.current_entry:
+	def update_tooltips (self):
+		if not self.entries:
 			self.tooltips.set_tip (self, None)
-		elif working:
+		elif self.working:
 			self.tooltips.set_tip (self, _("Searching... drop artwork here"))
 		else:
 			self.tooltips.set_tip (self, _("Drop artwork here"))
 
-	def set (self, entry, pixbuf, uri, working):
-		self.current_entry = entry
-		self.current_pixbuf = pixbuf
-		self.current_uri = uri
-		self.set_current_art (pixbuf, working)
+	def cover_art_notify (self, db, entry, field, metadata):
+		if entry not in self.entries:
+			return
+		uri = metadata and metadata.get_data (PIXBUF_URI_KEY) or None
+		self.props.pixbuf, self.props.uri = metadata, uri
+		self.props.working = False
+		self.set_current_art (metadata, False)
 		self.update_dnd_targets ()
-		self.update_tooltips (working)
+		self.update_tooltips ()
 
 	def drag_data_get (self, widget, drag_context, selection_data, info, timestamp):
-		if self.current_pixbuf:
-			selection_data.set_pixbuf (self.current_pixbuf)
-		if self.current_uri:
-			selection_data.set_uris ([self.current_uri])
+		if self.pixbuf:
+			selection_data.set_pixbuf (self.pixbuf)
+		if self.uri:
+			selection_data.set_uris ([self.uri])
+			selection_data.set_text (self.uri)
 
 	def drag_data_received (self, widget, drag_context, x, y, selection_data, info, timestamp):
-		entry = self.current_entry
 		pixbuf = selection_data.get_pixbuf ()
 		uris = selection_data.get_uris ()
 		text = selection_data.get_text ()
+		db = self.shell.get_property ("db")
 		if pixbuf:
-			self.emit ('pixbuf-dropped', entry, pixbuf)
+			for entry in self.entries:
+				self.art_db.set_pixbuf (db, entry, pixbuf, self.on_get_pixbuf_completed)
 		elif uris:
-			self.emit ('uri-dropped', entry, uris[0])
+			for entry in self.entries:
+				self.art_db.set_pixbuf_from_uri (db, entry, uri, self.on_get_pixbuf_completed)
 		elif text:
-			self.emit ('uri-dropped', entry, text)
+			for entry in self.entries:
+				self.art_db.set_pixbuf_from_uri (db, entry, uri, self.on_get_pixbuf_completed)
+	
+	def on_get_pixbuf_completed (self, entry, pixbuf, uri):
+		# This might be from a playing-changed signal, 
+		# in which case consumers won't be ready yet.
+		if pixbuf:
+			pixbuf.set_data (PIXBUF_URI_KEY, uri)
+		def on_idle_cb ():
+			db = self.shell.get_property ("db")
+			db.emit_entry_extra_metadata_notify (entry, "rb:coverArt", pixbuf)
+			return False
+		gobject.idle_add (on_idle_cb)
 gobject.type_register (ArtDisplayWidget)
 
 
+class PreviewWidget (gtk.VBox):
+	def __init__ (self, width=200, height=200):
+		super (PreviewWidget, self).__init__ ()
+		self.i = gtk.Image ()
+		self.l = gtk.Label (_("No preview"))
+		self.add (self.i)
+		self.add (self.l)
+		self.width, self.height = width, height
+		self.i.set_size_request (self.width, self.height)
+
+	def set_uri (self, uri):
+		load_pixbuf_from_uri (uri, self.set_pixbuf)
+
+	def set_pixbuf (self, p):
+		if p:
+			(w, h) = (p.props.width, p.props.height)
+			self.l.set_text (_("%d × %d pixels") % (w, h))
+			self.i.set_from_pixbuf (pixbuf_scaled_keep_aspect (p, self.width, self.height))
+		else:
+			self.i.set_from_pixbuf (None)
+			self.l.set_text (_("No preview"))
+
+
+class ArtChooserDialog (gtk.FileChooserDialog):
+	def __init__ (self, parent=None):
+		super (ArtChooserDialog, self).__init__ (_("Select cover"), 
+				parent, gtk.FILE_CHOOSER_ACTION_OPEN,
+				(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, 
+					gtk.STOCK_APPLY, gtk.RESPONSE_ACCEPT))
+		self.set_default_response (gtk.RESPONSE_ACCEPT)
+		filter = gtk.FileFilter()
+		filter.add_pixbuf_formats()
+		self.set_filter (filter)
+		self.preview = PreviewWidget (150, 150)
+		self.set_preview_widget (self.preview)
+		self.set_preview_widget_active (True)
+		self.connect ("update-preview", self.update_preview)
+		self.preview.show_all ()
+
+	def update_preview (self, chooser):
+		self.preview.set_uri (self.get_preview_uri ())
+
+
+class SearchLayout (gtk.Layout):
+	__gsignals__ = { 
+			'size-request': 'override',
+			'selection-changed': (gobject.SIGNAL_RUN_LAST, 
+				gobject.TYPE_NONE, ()),
+			'activated': (gobject.SIGNAL_RUN_LAST, 
+				gobject.TYPE_NONE, ()),
+			}
+	def __init__ (self, icon_size=48):
+		super (SearchLayout, self).__init__ ()
+		self.icon_size = icon_size
+		self.connect ("size-allocate", self.size_allocate)
+		self.clear ()
+
+	def clear (self):
+		self.sections = {}
+		self.ids = []
+		self.selected_pixbuf = None
+		self.selected_uri = None
+		for child in self.get_children ():
+			child.destroy ()
+
+	def do_size_request (self, requisition):
+		widths = []
+		for expander, iconview, model in self.sections.values ():
+			w, h = expander.size_request ()
+			widths.append (w)
+			w, h = iconview.size_request ()
+			widths.append (w)
+		(requisition.width, requisition.height) = (max(widths), 1)
+
+	def size_allocate (self, widget, allocation):
+		if allocation.width != self.get_size ()[0]:
+			self.layout_contents (allocation)
+	
+	def layout_contents (self, allocation=None):
+		y = 0
+		rects = {}
+		alloc = allocation or self.allocation
+		for id in self.ids:
+			expander, iconview, model = self.sections[id]
+			w, h = expander.size_request ()
+			exp_rect = gtk.gdk.Rectangle (0, y, alloc.width, h)
+			y += h + expander.props.spacing
+
+			iconview.set_size_request (-1, -1)	# Don't ask.
+			iconview.set_size_request (alloc.width, -1)
+			w, h = iconview.size_request ()
+			iv_rect = gtk.gdk.Rectangle (0, y, alloc.width, h)
+			if iconview.props.visible and h > 0:
+				y += h
+
+			rects[id] = (exp_rect, iv_rect)
+		w, h = self.get_size ()
+		if (w, h) != (alloc.width, y):
+			self.set_size (alloc.width, y)
+		need_relayout = False
+		for id in self.ids:
+			expander, iconview, model = self.sections[id]
+			exp_rect, iv_rect = rects[id]
+			if tuple (exp_rect) != tuple (expander.allocation):
+				expander.size_allocate (exp_rect)
+				self.move (expander, 0, exp_rect.y)
+			if tuple (iv_rect) != tuple (iconview.allocation):
+				iconview.set_size_request (-1, -1)
+				iconview.size_allocate (iv_rect)
+				if iconview.size_request()[1] != iv_rect.height:
+					need_relayout = True
+				self.move (iconview, 0, iv_rect.y)
+		if need_relayout:
+			gobject.idle_add (self.layout_contents)
+		return False
+
+	def add_section (self, id, title):
+		expander = gtk.Expander (title)
+		expander.set_use_markup (True)
+		expander.set_expanded (True)
+		model = gtk.ListStore (gtk.gdk.Pixbuf, gobject.TYPE_STRING, gtk.gdk.Pixbuf)
+		iconview = gtk.IconView (model)
+		iconview.set_pixbuf_column (0)
+		def on_selection_changed (iconview):
+			items = iconview.get_selected_items ()
+			if items:
+				(self.selected_pixbuf, self.selected_uri) = model.get (model.get_iter (items[0]), 2, 1)
+			else:
+				(self.selected_pixbuf, self.selected_uri) = (None, None)
+			self.emit ('selection-changed')
+		iconview.connect ("selection-changed", on_selection_changed)
+		def on_focus (iconview, event):
+			on_selection_changed (iconview)
+		iconview.connect ("focus-in-event", on_focus)
+		def on_activated (iconview, path):
+			self.emit ('activated')
+		iconview.connect ("item_activated", on_activated)
+		iconview.props.item_width = self.icon_size
+		expander.connect ("notify::expanded", 
+				self.on_expander_expanded, iconview, model)
+		self.put (expander, 0, 0)
+		self.put (iconview, 0, 0)
+		expander.show ()
+		iconview.hide ()
+		self.sections[id] = (expander, iconview, model)
+		self.ids.append (id)
+		self.layout_contents ()
+
+	def on_expander_expanded (self, expander, param_spec, iconview, model):
+		if expander.props.expanded and model.get_iter_first ():
+			iconview.show ()
+		else:
+			iconview.hide ()
+		self.layout_contents ()
+
+	def section_set_title (self, id, title):
+		expander, iconview, model = self.sections[id]
+		expander.set_label (title)
+
+	def section_add_item (self, id, pixbuf, uri):
+		expander, iconview, model = self.sections[id]
+		if pixbuf:
+			model.append ((pixbuf_scaled_keep_aspect (pixbuf, self.icon_size, self.icon_size), uri, pixbuf))
+		else:
+			model.append ((self.render_icon (gtk.STOCK_MISSING_IMAGE, gtk.ICON_SIZE_DIALOG), uri, None))
+		self.on_expander_expanded (expander, None, iconview, model)
+		gobject.idle_add (self.layout_contents)
+gobject.type_register (SearchLayout)
+gtk.rc_parse_string("""
+	style "bg-white" { bg[NORMAL] = "white" }
+	widget_class "*<%s>" style "bg-white"
+	widget_class "*<%s>.*<GtkExpander>" style "bg-white"
+""" % (gobject.type_name (SearchLayout), gobject.type_name (SearchLayout)))
+
+
+class ArtSearchDialog (gtk.Dialog):
+	def __init__ (self, parent=None):
+		super (ArtSearchDialog, self).__init__ (_("Select cover"), 
+				parent, gtk.DIALOG_DESTROY_WITH_PARENT,
+				(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, 
+					gtk.STOCK_APPLY, gtk.RESPONSE_ACCEPT))
+		self.set_default_response (gtk.RESPONSE_ACCEPT)
+		self.set_default_size (480, 320)
+		self.set_has_separator (False)
+		hbox = gtk.HBox ()
+		hbox.set_border_width (12)
+		scroll = gtk.ScrolledWindow ()
+		self.layout = SearchLayout ()
+		scroll.add (self.layout)
+		scroll.set_policy (gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+		scroll.set_shadow_type (gtk.SHADOW_IN)
+		hbox.pack_start (scroll, True, True)
+		vbox = gtk.VBox ()
+		self.preview_label = gtk.Label ()
+		self.preview_label.set_ellipsize (pango.ELLIPSIZE_MIDDLE)
+		self.preview = PreviewWidget ()
+		vbox.pack_start (self.preview_label, False, False)
+		vbox.pack_start (self.preview, True, True)
+		hbox.pack_start (vbox, False, False)
+		hbox.show_all ()
+		self.vbox.add (hbox)
+		self.layout.connect ("selection-changed", self.layout_selection_changed)
+		self.layout.connect ("activated", self.layout_activated)
+		self.connect ("response", self.response_cb)
+
+	def layout_selection_changed (self, layout):
+		self.preview.set_pixbuf (self.layout.selected_pixbuf)
+		self.preview_label.set_text (self.layout.selected_uri or "")
+
+	def layout_activated (self, layout):
+		self.activate_default ()
+
+	def response_cb (self, dialog, id):
+		if id == gtk.RESPONSE_ACCEPT and not self.layout.selected_pixbuf:
+			self.stop_emission ("response")
+
+	def begin_search (self, db, entry, loader):
+		for Engine in ART_SEARCHES_LOCAL + ART_SEARCHES_REMOTE:
+			id, title = Engine.__name__, Engine.__name__
+			self.layout.add_section (id, _("%s: <i>searching...</i>") % title)
+			engine = Engine (loader)
+			engine.search (db, entry, self.on_results, id, title)
+
+	def on_results (self, engine, entry, results, id, title):
+		uris = results and map(str, engine.get_best_match_urls (results)) or None
+		if not uris:
+			self.layout.section_set_title (id, _("%s: <i>no results</i>") % title)
+			return
+		counts = [0, len(uris)]
+		def set_title ():
+			self.layout.section_set_title (id, _.im_self.ngettext("%s: <i>%d of %d result</i>", "%s: <i>%d of %d results</i>", counts[1]) % (title, counts[0], counts[1]))
+		for uri in uris:
+			def on_pixbuf (pixbuf, uri):
+				self.layout.section_add_item (id, pixbuf, uri)
+				counts [0] += 1
+				set_title ()
+			load_pixbuf_from_uri (uri, on_pixbuf, uri)
+		set_title ()
+
+
+class ArtPane (object):
+	def __init__ (self, shell, song_info):
+		self.pane = gtk.VBox (False, 12)
+		self.art_widget = gobject.new (ArtDisplayWidget, shell=shell)
+		self.art_widget.add_events (gtk.gdk.BUTTON_PRESS_MASK)
+		self.art_widget.connect ('button-press-event', self.on_art_press)
+		self.set_button = gtk.Button (_('Set c_over art...'))
+		self.set_button.props.image = gtk.image_new_from_stock (gtk.STOCK_FILE, gtk.ICON_SIZE_BUTTON)
+		self.set_button.connect ('clicked', self.art_widget.emit_set_cover)
+		self.search_button = gtk.Button (_('_Search for cover art...'))
+		self.search_button.props.image = gtk.image_new_from_stock (gtk.STOCK_FIND, gtk.ICON_SIZE_BUTTON)
+		self.search_button.connect ('clicked', self.art_widget.emit_search_cover)
+		self.pane.add (self.art_widget)
+		self.pane.add (self.set_button)
+		self.pane.add (self.search_button)
+		self.pane.show_all ()
+		song_info.append_page (_("Cover art"), self.pane)
+		db = shell.get_property ("db")
+		song_info.connect('notify::current-entry', self.entry_changed)
+		self.entry_changed (song_info, None)
+	
+	def entry_changed (self, song_info, pspec):
+		entry = song_info.get_property ('current-entry')
+		if entry:
+			self.art_widget.props.entries = (entry,)
+			self.pane.set_sensitive (True)
+		else:
+			entries = song_info.get_property ('selected-entries')
+			self.art_widget.props.entries = entries
+			self.pane.set_sensitive (bool (entries))
+	
+	def on_art_press (self, widget, event):
+		if event.type != gtk.gdk.BUTTON_PRESS:
+			return
+		if event.button != 3:
+			return
+		menu = gtk.Menu ()
+		copy_item = gtk.ImageMenuItem (gtk.STOCK_COPY)
+		copy_item.set_sensitive (bool (self.art_widget.props.pixbuf) or bool (self.art_widget.props.uri))
+		copy_item.connect ('activate', self.art_widget.emit_copy_cover)
+		paste_item = gtk.ImageMenuItem (gtk.STOCK_PASTE)
+		paste_item.set_sensitive ((clipboard.wait_is_text_available() or clipboard.wait_is_image_available()))
+		paste_item.connect ('activate', self.art_widget.emit_paste_cover)
+		menu.add (copy_item)
+		menu.add (paste_item)
+		menu.show_all ()
+		menu.popup (None, None, None, event.button, event.time)
+
+
 class ArtDisplayPlugin (rb.Plugin):
 	def __init__ (self):
 		rb.Plugin.__init__ (self)
 
 	def activate (self, shell):
+		global ART_MISSING_FILE
+		if not ART_MISSING_FILE:
+			ART_MISSING_FILE = self.find_file (ART_MISSING_ICON + ".svg")
 		self.shell = shell
 		sp = shell.get_player ()
 		self.player_cb_ids = (
@@ -331,16 +804,40 @@ class ArtDisplayPlugin (rb.Plugin):
 			db.connect_after ('entry-extra-metadata-notify::rb:coverArt-uri', self.cover_art_uri_notify),
 			db.connect_after ('entry-extra-metadata-gather', self.cover_art_uri_gather),
 		)
-		self.art_widget = ArtDisplayWidget (self.find_file (ART_MISSING_ICON + ".svg"))
-		self.art_widget.connect ('pixbuf-dropped', self.on_set_pixbuf)
-		self.art_widget.connect ('uri-dropped', self.on_set_uri)
+		self.art_widget = gobject.new (ArtDisplayWidget, shell=shell)
+		self.art_widget.add_events (gtk.gdk.BUTTON_PRESS_MASK)
+		self.art_widget.connect ('button-press-event', self.on_art_press)
 		shell.add_widget (self.art_widget, rb.SHELL_UI_LOCATION_SIDEBAR)
 		self.art_db = CoverArtDatabase ()
-		self.current_entry, self.current_pixbuf = None, None
+		self.current_entry, self.have_run_search = None, False
+		self.current_pixbuf, self.current_uri = None, None
 		self.playing_entry_changed (sp, sp.get_playing_entry ())
+		self.csi_id = shell.connect('create-song-info', self.create_song_info)
+
+		self.set_cover_action = gtk.Action ('SetCover', _('Set c_over art...'), _('Set cover art from a file on the system'), gtk.STOCK_FILE)
+		self.set_cover_action.connect ('activate', self.art_widget.emit_set_cover)
+		self.set_cover_action.set_sensitive (False)
+		self.search_cover_action = gtk.Action ('SearchCover', _('_Search for cover art...'), _('Search for cover art on the Internet'), gtk.STOCK_FIND)
+		self.search_cover_action.connect ('activate', self.art_widget.emit_search_cover)
+		self.search_cover_action.set_sensitive (False)
+		self.copy_cover_action = gtk.Action ('CopyCover', _('Copy'), _('Copy cover art to clipboard'), gtk.STOCK_COPY)
+		self.copy_cover_action.connect ('activate', self.art_widget.emit_copy_cover)
+		self.copy_cover_action.set_sensitive (False)
+		self.paste_cover_action = gtk.Action ('PasteCover', _('Paste'), _('Paste cover art from clipboard'), gtk.STOCK_PASTE)
+		self.paste_cover_action.connect ('activate', self.art_widget.emit_paste_cover)
+		self.paste_cover_action.set_sensitive (False)
+		self.action_group = gtk.ActionGroup ('ArtDisplayPluginActions')
+		self.action_group.add_action (self.set_cover_action)
+		self.action_group.add_action (self.search_cover_action)
+		self.action_group.add_action (self.copy_cover_action)
+		self.action_group.add_action (self.paste_cover_action)
+		uim = shell.get_ui_manager ()
+		uim.insert_action_group (self.action_group, 0)
+		self.ui_id = uim.add_ui_from_string (ui_str);
 
 	def deactivate (self, shell):
 		self.shell = None
+		shell.disconnect (self.csi_id)
 
 		sp = shell.get_player ()
 		for id in self.player_cb_ids:
@@ -356,6 +853,9 @@ class ArtDisplayPlugin (rb.Plugin):
 		self.art_widget.disconnect_handlers ()
 		self.art_widget = None
 		self.art_db = None
+		uim = shell.get_ui_manager ()
+		uim.remove_action_group (self.action_group)
+		uim.remove_ui (self.ui_id)
 
 	def playing_changed (self, sp, playing):
 		self.set_entry(sp.get_playing_entry ())
@@ -363,72 +863,60 @@ class ArtDisplayPlugin (rb.Plugin):
 	def playing_entry_changed (self, sp, entry):
 		self.set_entry(entry)
 
+	def set_actions_sensitive (self):
+		self.set_cover_action.set_sensitive (bool (self.current_entry))
+		self.search_cover_action.set_sensitive (bool (self.current_entry))
+		self.copy_cover_action.set_sensitive (bool (self.current_entry) and bool (self.current_pixbuf))
+		self.paste_cover_action.set_sensitive (bool (self.current_entry))
+
 	def set_entry (self, entry):
+		if not entry:
+			self.art_widget.props.entries = []
+			self.art_widget.hide ()
+			return
 		if entry == self.current_entry:
 			return
 		db = self.shell.get_property ("db")
 
-		self.art_widget.set (entry, None, None, True)
+		self.art_widget.props.entries = [entry]
 		self.art_widget.show ()
-		# Intitates search in the database (which checks art cache, internet etc.)
-		self.current_entry = entry
-		self.current_pixbuf = None
-		self.art_db.get_pixbuf(db, entry, self.on_get_pixbuf_completed)
+		self.current_entry, self.have_run_search = entry, False
+		self.current_pixbuf, self.current_uri = None, None
+		metadata = db.entry_request_extra_metadata (entry, "rb:coverArt")
+		if isinstance (metadata, gtk.gdk.Pixbuf):
+			self.on_get_pixbuf_completed (entry, metadata, None)
+		self.set_actions_sensitive ()
 
 	def on_get_pixbuf_completed(self, entry, pixbuf, uri):
-		# Set the pixbuf for the entry returned from the art db
-		if entry != self.current_entry:
-			return
-		self.current_pixbuf = pixbuf
-		self.art_widget.set (entry, pixbuf, uri, False)
+		# This might be from a playing-changed signal, 
+		# in which case consumers won't be ready yet.
 		if pixbuf:
+			pixbuf.set_data (PIXBUF_URI_KEY, uri)
+		def on_idle_cb ():
 			db = self.shell.get_property ("db")
-			# This might be from a playing-changed signal,
-			# in which case consumers won't be ready yet.
-			def idle_emit_art():
-				db.emit_entry_extra_metadata_notify (entry, "rb:coverArt", pixbuf)
-				return False
-			gobject.idle_add(idle_emit_art)
+			db.emit_entry_extra_metadata_notify (entry, "rb:coverArt", pixbuf)
+			return False
+		gobject.idle_add (on_idle_cb)
 
 	def cover_art_request (self, db, entry):
-		if entry == self.current_entry:
+		if entry == self.current_entry and self.have_run_search:
 			return self.current_pixbuf
+		# Initiate search in database (checks art cache, internet etc.)
+		self.art_db.get_pixbuf(db, entry, self.on_get_pixbuf_completed)
 
 	def cover_art_notify (self, db, entry, field, metadata):
-		if entry != self.current_entry:
-			return
-		if not isinstance (metadata, gtk.gdk.Pixbuf):
-			return
+		self.have_run_search = True
 		self.art_db.cancel_get_pixbuf (entry)
-		if self.current_pixbuf == metadata:
-			return
-		self.art_widget.set (entry, metadata, None, False)
+		uri = metadata and metadata.get_data (PIXBUF_URI_KEY) or None
+		self.current_pixbuf, self.current_uri = metadata, uri
+		self.set_actions_sensitive ()
 
+	# service to other plugins: fetch emitted uris
 	def cover_art_uri_notify (self, db, entry, field, metadata):
-		if entry != self.current_entry:
-			return
-
-		if not metadata:
-			print "got no-cover-art notification"
-			self.art_widget.set (entry, None, None, False)
-			db.emit_entry_extra_metadata_notify (entry, "rb:coverArt", None)
-			return
-
-		uri = str (metadata)
-		def loader_cb (data):
-			if data and len (data) >= 1000:
-				pbl = gtk.gdk.PixbufLoader ()
-				try:
-					if pbl.write (data) and pbl.close ():
-						pixbuf = pbl.get_pixbuf ()
-						if pixbuf:
-							self.art_db.cancel_get_pixbuf (entry)
-							self.on_get_pixbuf_completed (entry, pixbuf, uri)
-				except GError:
-					pass
+		def pixbuf_cb (pixbuf, uri):
+			if pixbuf:
+				self.on_get_pixbuf_completed (entry, pixbuf, uri)
+		load_pixbuf_from_uri (metadata, pixbuf_cb, metadata)
-
-		print "got cover art URI notification: %s" % (uri)
-		rb.Loader().get_url (uri, loader_cb)
 
 	def cover_art_uri_request (self, db, entry):
 		if entry == self.current_entry:
@@ -438,10 +938,11 @@ class ArtDisplayPlugin (rb.Plugin):
 		if entry == self.current_entry and self.art_widget.current_uri:
 			metadata ['rb:coverArt-uri'] = self.art_widget.current_uri
 
+	def create_song_info (self, shell, song_info, is_multiple):
+		x = ArtPane (shell, song_info)
+
-	def on_set_pixbuf (self, widget, entry, pixbuf):
-		db = self.shell.get_property ("db")
-		self.art_db.set_pixbuf (db, entry, pixbuf, self.on_get_pixbuf_completed)
-
-	def on_set_uri (self, widget, entry, uri):
-		db = self.shell.get_property ("db")
-		self.art_db.set_pixbuf_from_uri (db, entry, uri, self.on_get_pixbuf_completed)
+	def on_art_press (self, widget, event):
+		if event.type != gtk.gdk.BUTTON_PRESS or event.button != 3:
+			return
+		self.paste_cover_action.set_sensitive (bool (self.current_entry) and (clipboard.wait_is_text_available() or clipboard.wait_is_image_available()))
+		self.shell.get_ui_manager ().get_widget ("/CoverArtWidgetPopup").popup (None, None, None, event.button, event.time)
