--- plugins/artdisplay/artdisplay/__init__.py	2007/01/21 18:45:05	1.1
+++ plugins/artdisplay/artdisplay/__init__.py	2007/01/21 19:35:00
@@ -18,9 +18,11 @@
 
 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
 
 
 FADE_STEPS = 10
@@ -73,6 +75,33 @@ 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 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):
@@ -311,6 +340,216 @@ class ArtDisplayWidget (FadingImage):
 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 ()
+		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.sections = {}
+		self.ids = []
+		self.connect ("size-allocate", self.size_allocate)
+		self.selected_pixbuf = None
+		self.selected_uri = None
+
+	def do_size_request (self, requisition):
+		widths = []
+		for expander, iconview, model in self.sections.values ():
+			w, h = expander.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")
+
+
 class ArtDisplayPlugin (rb.Plugin):
 	def __init__ (self):
 		rb.Plugin.__init__ (self)
@@ -327,10 +566,52 @@ class ArtDisplayPlugin (rb.Plugin):
 		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.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_uri = None
 		self.playing_entry_changed (sp, sp.get_playing_entry ())
+		self.ui_str = """
+<ui>
+	<menubar name="MenuBar">
+		<menu name="EditMenu" action="Edit">
+			<separator/>
+			<menuitem name="SetCover" action="SetCover"/>
+			<menuitem name="SearchCover" action="SearchCover"/>
+			<separator/>
+		</menu>
+	</menubar>
+	<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>
+"""
+		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.set_cover)
+		self.set_cover_action.set_sensitive (False)
+		self.search_cover_action = gtk.Action ('SearchCover', _('_Search for c_over art...'), _('Search for cover art on the Internet'), gtk.STOCK_FIND)
+		self.search_cover_action.connect ('activate', self.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.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.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 (self.ui_str);
 	
 	def deactivate (self, shell):
 		self.shell = None
@@ -345,6 +626,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 ())
@@ -352,6 +636,12 @@ 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 entry == self.current_entry:
 			return
@@ -361,15 +651,17 @@ class ArtDisplayPlugin (rb.Plugin):
 		self.art_widget.show ()
 		# Intitates search in the database (which checks art cache, internet etc.)
 		self.current_entry = entry
-		self.current_pixbuf = None
+		self.current_pixbuf, self.current_uri = None, None
 		self.art_db.get_pixbuf(db, entry, self.on_get_pixbuf_completed)
+		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.current_pixbuf, self.current_uri = pixbuf, uri
 		self.art_widget.set (entry, pixbuf, uri, False)
+		self.set_actions_sensitive ()
 		if pixbuf:
 			db = self.shell.get_property ("db")
 			# This might be from a playing-changed signal, 
@@ -391,24 +683,82 @@ class ArtDisplayPlugin (rb.Plugin):
 		self.art_db.cancel_get_pixbuf (entry)
 		if self.current_pixbuf == metadata:
 			return
+		self.current_pixbuf, self.current_uri = metadata, None
 		self.art_widget.set (entry, metadata, None, False)
+		self.set_actions_sensitive ()
 
 	def cover_art_uri_notify (self, db, entry, field, metadata):
 		if entry != self.current_entry or not metadata:
 			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
-		rb.Loader().get_url (uri, loader_cb)
+		self.current_uri = str (metadata)
+		def pixbuf_cb (pixbuf):
+			if pixbuf:
+				self.art_db.cancel_get_pixbuf (entry)
+				self.on_get_pixbuf_completed (entry, pixbuf, self.current_uri)
+		load_pixbuf_from_uri (self.current_uri, pixbuf_cb)
+
+	def set_cover (self, action):
+		entry = self.current_entry
+		acd = ArtChooserDialog (self.shell.props.window)
+		if acd.run () == gtk.RESPONSE_ACCEPT:
+			db = self.shell.get_property ("db")
+			self.art_db.set_pixbuf_from_uri (db, entry, acd.get_uri (), self.on_get_pixbuf_completed)
+		acd.destroy ()
+
+	def search_cover (self, action):
+		entry = self.current_entry
+		db = self.shell.get_property ("db")
+		asd = ArtSearchDialog (self.shell.props.window)
+		loader = rb.Loader ()
+		def on_results (engine, entry, results, id, title):
+			uris = results and map (str, engine.get_best_match_urls (results)) or None
+			if not uris:
+				asd.layout.section_set_title (id, _("%s: <i>no results</i>") % title)
+				return
+			counts = [0, len(uris)]
+			def set_title ():
+				asd.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):
+					asd.layout.section_add_item (id, 
+							pixbuf, uri)
+					counts [0] += 1
+					set_title ()
+				load_pixbuf_from_uri (uri, on_pixbuf, uri)
+			set_title ()
+		for Engine in ART_SEARCHES_LOCAL + ART_SEARCHES_REMOTE:
+			id, title = Engine.__name__, Engine.__name__
+			asd.layout.add_section (id, _("%s: <i>searching...</i>") % title)
+			engine = Engine (loader)
+			engine.search (db, entry, on_results, id, title)
+		if asd.run () == gtk.RESPONSE_ACCEPT:
+			self.art_db.set_pixbuf_from_uri (db, entry, asd.layout.selected_uri, self.on_get_pixbuf_completed)
+		asd.destroy ()
+
+	def copy_cover (self, action):
+		if self.current_pixbuf:
+			gtk.Clipboard ().set_image (self.current_pixbuf)
+		if self.current_uri:
+			gtk.Clipboard ().set_text (self.current_uri)
+
+	def paste_cover (self, action):
+		entry = self.current_entry
+		db = self.shell.get_property ("db")
+		pixbuf = gtk.Clipboard ().wait_for_image ()
+		if pixbuf:
+			self.art_db.set_pixbuf (db, entry, pixbuf, self.on_get_pixbuf_completed)
+			return
+		text = gtk.Clipboard ().wait_for_text ()
+		if text:
+			self.art_db.set_pixbuf_from_uri (db, entry, text, self.on_get_pixbuf_completed)
+
+	def on_art_press (self, widget, event):
+		if event.type != gtk.gdk.BUTTON_PRESS:
+			return
+		if event.button != 3:
+			return
+		self.paste_cover_action.set_sensitive (bool (self.current_entry) and (gtk.Clipboard ().wait_is_text_available() or gtk.Clipboard ().wait_is_image_available()))
+		self.shell.get_ui_manager ().get_widget ("/CoverArtWidgetPopup").popup (None, None, None, event.button, event.time)
 
 	def on_set_pixbuf (self, widget, entry, pixbuf):
 		db = self.shell.get_property ("db")
