CoverArt Browser  v2.0
Browse your cover-art albums in Rhythmbox
/home/foss/Downloads/coverart-browser/coverart_coverflowview.py
00001 # -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
00002 #
00003 # Copyright (C) 2012 - fossfreedom
00004 # Copyright (C) 2012 - Agustin Carrasco
00005 #
00006 # This program is free software; you can redistribute it and/or modify
00007 # it under the terms of the GNU General Public License as published by
00008 # the Free Software Foundation; either version 2, or (at your option)
00009 # any later version.
00010 #
00011 # This program is distributed in the hope that it will be useful,
00012 # but WITHOUT ANY WARRANTY; without even the implied warranty of
00013 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00014 # GNU General Public License for more details.
00015 #
00016 # You should have received a copy of the GNU General Public License
00017 # along with this program; if not, write to the Free Software
00018 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
00019 
00020 import json
00021 import os
00022 from xml.sax.saxutils import escape
00023 
00024 from gi.repository import Gdk
00025 from gi.repository import Gtk
00026 from gi.repository import GLib
00027 from gi.repository import GObject
00028 from gi.repository import Gio
00029 
00030 from coverart_browser_prefs import GSetting
00031 from coverart_browser_prefs import webkit_support
00032 from coverart_widgets import AbstractView
00033 from coverart_widgets import PanedCollapsible
00034 import rb
00035 
00036 
00037 class FlowShowingPolicy(GObject.Object):
00038     '''
00039     Policy that mostly takes care of how and when things should be showed on
00040     the view that makes use of the `AlbumsModel`.
00041     '''
00042 
00043     def __init__(self, flow_view):
00044         super(FlowShowingPolicy, self).__init__()
00045 
00046         self._flow_view = flow_view
00047         self.counter = 0
00048         self._has_initialised = False
00049 
00050     def initialise(self, album_manager):
00051         if self._has_initialised:
00052             return
00053 
00054         self._has_initialised = True
00055         self._album_manager = album_manager
00056         self._model = album_manager.model
00057 
00058 
00059 class CoverFlowView(AbstractView):
00060     __gtype_name__ = "CoverFlowView"
00061 
00062     name = 'coverflowview'
00063 
00064     #properties
00065     flow_background = GObject.property(type=str, default='W')
00066     flow_automatic = GObject.property(type=bool, default=False)
00067     flow_scale = GObject.property(type=int, default=100)
00068     flow_hide = GObject.property(type=bool, default=False)
00069     flow_width = GObject.property(type=int, default=600)
00070     flow_appearance = GObject.property(type=str, default='coverflow')
00071     flow_max = GObject.property(type=int, default=100)
00072     panedposition = PanedCollapsible.Paned.EXPAND
00073 
00074     def __init__(self):
00075         super(CoverFlowView, self).__init__()
00076 
00077         self.show_policy = FlowShowingPolicy(self)
00078         if webkit_support():
00079             from gi.repository import WebKit
00080 
00081             self.view = WebKit.WebView()
00082         else:
00083             self.view = None
00084 
00085         self._last_album = None
00086         self._has_initialised = False
00087         self._filter_changed_inprogress = False
00088         self._on_first_use = True
00089 
00090     def _connect_properties(self):
00091         gs = GSetting()
00092         settings = gs.get_setting(gs.Path.PLUGIN)
00093         settings.bind(gs.PluginKey.FLOW_APPEARANCE, self,
00094                       'flow_appearance', Gio.SettingsBindFlags.GET)
00095         settings.bind(gs.PluginKey.FLOW_HIDE_CAPTION, self,
00096                       'flow_hide', Gio.SettingsBindFlags.GET)
00097         settings.bind(gs.PluginKey.FLOW_SCALE, self,
00098                       'flow_scale', Gio.SettingsBindFlags.GET)
00099         settings.bind(gs.PluginKey.FLOW_AUTOMATIC, self,
00100                       'flow_automatic', Gio.SettingsBindFlags.GET)
00101         settings.bind(gs.PluginKey.FLOW_BACKGROUND_COLOUR, self,
00102                       'flow_background', Gio.SettingsBindFlags.GET)
00103         settings.bind(gs.PluginKey.FLOW_WIDTH, self,
00104                       'flow_width', Gio.SettingsBindFlags.GET)
00105         settings.bind(gs.PluginKey.FLOW_MAX, self,
00106                       'flow_max', Gio.SettingsBindFlags.GET)
00107 
00108     def _connect_signals(self, source):
00109         self.connect('notify::flow-background',
00110                      self.filter_changed)
00111         self.connect('notify::flow-scale',
00112                      self.filter_changed)
00113         self.connect('notify::flow-hide',
00114                      self.filter_changed)
00115         self.connect('notify::flow-width',
00116                      self.filter_changed)
00117         self.connect('notify::flow-appearance',
00118                      self.filter_changed)
00119         self.connect('notify::flow-max',
00120                      self.filter_changed)
00121 
00122     def filter_changed(self, *args):
00123         # we can get several filter_changed calls per second
00124         # lets simplify the processing & potential flickering when the
00125         # call to this method has slowed stopped
00126 
00127         self._filter_changed_event = True
00128 
00129         if self._filter_changed_inprogress:
00130             return
00131 
00132         self._filter_changed_inprogress = True
00133 
00134         def filter_events(*args):
00135             if not self._filter_changed_event:
00136                 self._filter_changed()
00137                 self._filter_changed_inprogress = False
00138             else:
00139                 self._filter_changed_event = False
00140                 return True
00141 
00142         Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 250, filter_events, None)
00143 
00144 
00145     def _filter_changed(self, *args):
00146         path = rb.find_plugin_file(self.plugin, 'coverflow/index.html')
00147         f = open(path)
00148         string = f.read()
00149         f.close()
00150 
00151         if self.flow_background == 'W':
00152             background_colour = 'white'
00153             if len(self.album_manager.model.store) <= self.flow_max:
00154                 foreground_colour = 'white'
00155             else:
00156                 foreground_colour = 'black'
00157         else:
00158             background_colour = 'black'
00159             if len(self.album_manager.model.store) <= self.flow_max:
00160                 foreground_colour = 'black'
00161             else:
00162                 foreground_colour = 'white'
00163 
00164         string = string.replace('#BACKGROUND_COLOUR', background_colour)
00165         string = string.replace('#FOREGROUND_COLOUR', foreground_colour)
00166         string = string.replace('#FACTOR', str(float(self.flow_scale) / 100))
00167 
00168         if self.flow_hide:
00169             caption = ""
00170         else:
00171             caption = '<div class="globalCaption"></div>'
00172 
00173         string = string.replace('#GLOBAL_CAPTION', caption)
00174 
00175         addon = background_colour
00176         if self.flow_appearance == 'flow-vert':
00177             addon += " vertical"
00178         elif self.flow_appearance == 'carousel':
00179             addon += " carousel"
00180         elif self.flow_appearance == 'roundabout':
00181             addon += " roundabout"
00182 
00183         string = string.replace('#ADDON', addon)
00184 
00185         string = string.replace('#WIDTH', str(self.flow_width))
00186 
00187         identifier = self.flow.get_identifier(self.last_album)
00188         if not identifier:
00189             identifier = "'start'"
00190         else:
00191             identifier = str(identifier)
00192 
00193         string = string.replace('#START', identifier)
00194 
00195         #TRANSLATORS: for example 'Number of covers limited to 150'
00196         display_message = _("Number of covers limited to %d") % self.flow_max
00197         string = string.replace('#MAXCOVERS',
00198                                 '<p>' + display_message + '</p>')
00199 
00200         items = self.flow.initialise(self.album_manager.model, self.flow_max)
00201 
00202         string = string.replace('#ITEMS', items)
00203 
00204         base = os.path.dirname(path) + "/"
00205         Gdk.threads_enter()
00206         print(string)
00207         self.view.load_string(string, "text/html", "UTF-8", "file://" + base)
00208         Gdk.threads_leave()
00209 
00210         if self._on_first_use:
00211             self._on_first_use = False
00212             Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 250,
00213                                     self.source.show_hide_pane, (self.last_album, PanedCollapsible.Paned.EXPAND))
00214 
00215     def get_view_icon_name(self):
00216         return "flowview.png"
00217 
00218     def initialise(self, source):
00219         if self._has_initialised:
00220             return
00221 
00222         self._has_initialised = True
00223 
00224         super(CoverFlowView, self).initialise(source)
00225 
00226         self.album_manager = source.album_manager
00227         self.ext_menu_pos = 6
00228 
00229         self._connect_properties()
00230         self._connect_signals(source)
00231 
00232         # lets check that all covers have finished loading before
00233         # initialising the flowcontrol and other signals
00234         if not self.album_manager.cover_man.has_finished_loading:
00235             self.album_manager.cover_man.connect('load-finished', self._covers_loaded)
00236         else:
00237             self._covers_loaded()
00238 
00239     def _covers_loaded(self, *args):
00240         self.flow = FlowControl(self)
00241         self.view.connect("notify::title", self.flow.receive_message_signal)
00242 
00243         #self.album_manager.model.connect('album-updated', self.flow.update_album, self.view)
00244         #self.album_manager.model.connect('visual-updated', self.flow.update_album, self.view)
00245         self.album_manager.model.connect('album-updated', self.filter_changed)
00246         self.album_manager.model.connect('visual-updated', self.filter_changed)
00247         self.album_manager.model.connect('filter-changed', self.filter_changed)
00248 
00249         self.filter_changed()
00250 
00251     @property
00252     def last_album(self):
00253         return self._last_album
00254 
00255     @last_album.setter
00256     def last_album(self, new_album):
00257         if self._last_album != new_album:
00258             self._last_album = new_album
00259             self.source.click_count = 0
00260             self.selectionchanged_callback()
00261 
00262     def item_rightclicked_callback(self, album):
00263         self.last_album = album
00264         self.popup.popup(self.source, 'popup_menu', 3, Gtk.get_current_event_time())
00265 
00266     def item_clicked_callback(self, album):
00267         '''
00268         Callback called when the user clicks somewhere on the flow_view.
00269         Along with source "show_hide_pane", takes care of showing/hiding the bottom
00270         pane after a second click on a selected album.
00271         '''
00272         # to expand the entry view
00273         if self.flow_automatic:
00274             self.source.click_count += 1
00275 
00276         self.last_album = album
00277 
00278         if self.source.click_count == 1:
00279             Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 250,
00280                                     self.source.show_hide_pane, album)
00281 
00282     def item_activated_callback(self, album):
00283         '''
00284         Callback called when the flow view is double clicked. It plays the selected album
00285         '''
00286         self.last_album = album
00287         self.source.play_selected_album()
00288 
00289         return True
00290 
00291     def item_drop_callback(self, album, webpath):
00292         '''
00293         Callback called when something is dropped onto the flow view - hopefully a webpath
00294         to a picture
00295         '''
00296         print("item_drop_callback %s" % webpath)
00297         print("dropped on album %s" % album)
00298         self.album_manager.cover_man.update_cover(album, uri=webpath)
00299 
00300     def get_selected_objects(self):
00301         if self.last_album:
00302             return [self.last_album]
00303         else:
00304             return []
00305 
00306     def select_and_scroll_to_path(self, path):
00307         album = self.source.album_manager.model.get_from_path(path)
00308         self.flow.scroll_to_album(album, self.view)
00309         self.item_clicked_callback(album)
00310 
00311     def switch_to_view(self, source, album):
00312         self.initialise(source)
00313         self.show_policy.initialise(source.album_manager)
00314 
00315         self.last_album = album
00316         self.scroll_to_album(self.last_album)
00317 
00318     def grab_focus(self):
00319         self.view.grab_focus()
00320 
00321     def scroll_to_album(self, album):
00322         self.flow.scroll_to_album(album, self.view)
00323 
00324 
00325 class FlowControl(object):
00326     def __init__(self, callback_view):
00327         self.callback_view = callback_view
00328         self.album_identifier = {}
00329 
00330     def get_identifier(self, album):
00331         index = -1
00332         for row in self.album_identifier:
00333             if self.album_identifier[row] == album:
00334                 index = row
00335                 break
00336 
00337         if index == -1:
00338             return None
00339         else:
00340             return row
00341 
00342     def update_album(self, model, album_path, album_iter, webview):
00343         album = model.get_from_path(album_path)
00344         index = -1
00345         for row in self.album_identifier:
00346             if self.album_identifier[row] == album:
00347                 index = row
00348                 break
00349 
00350         if index == -1:
00351             return
00352 
00353         obj = {}
00354         obj['filename'] = album.cover.original
00355         obj['title'] = album.artist
00356         obj['caption'] = album.name
00357         obj['identifier'] = str(index)
00358 
00359         webview.execute_script("update_album('%s')" % json.dumps(obj))
00360 
00361     def receive_message_signal(self, webview, param):
00362         # this will be key to passing stuff back and forth - need
00363         # to develop some-sort of message protocol to distinguish "events"
00364 
00365         title = webview.get_title()
00366         if (not title) or (title == '"clear"'):
00367             return
00368 
00369         args = json.loads(title)
00370         try:
00371             signal = args["signal"]
00372         except:
00373             print("unhandled: %s " % title)
00374             return
00375 
00376         if signal == 'clickactive':
00377             self.callback_view.item_clicked_callback(self.album_identifier[int(args['param'][0])])
00378         elif signal == 'rightclickactive':
00379             self.callback_view.item_rightclicked_callback(
00380                 self.album_identifier[int(args['param'][0])])
00381         elif signal == 'doubleclickactive':
00382             self.callback_view.item_activated_callback(self.album_identifier[int(args['param'][0])])
00383         elif signal == 'dropactive':
00384             self.callback_view.item_drop_callback(self.album_identifier[int(args['param'][0])],
00385                                                   args['param'][1])
00386         else:
00387             print("unhandled signal: %s" % signal)
00388 
00389     def scroll_to_album(self, album, webview):
00390         for row in self.album_identifier:
00391             if self.album_identifier[row] == album:
00392                 webview.execute_script("scroll_to_identifier('%s')" % str(row))
00393                 break
00394 
00395     def initialise(self, model, max_covers):
00396 
00397         album_col = model.columns['album']
00398         index = 0
00399         items = ""
00400         self.album_identifier = {}
00401 
00402         def html_elements(fullfilename, title, caption, identifier):
00403 
00404             return '<div class="item"><img class="content" src="' + \
00405                    escape(fullfilename) + '" title="' + \
00406                    escape(title) + '" identifier="' + \
00407                    identifier + '"/> <div class="caption">' + \
00408                    escape(caption) + '</div> </div>'
00409 
00410 
00411         for row in model.store:
00412 
00413             cover = row[album_col].cover.original
00414             cover = cover.replace(
00415                 'rhythmbox-missing-artwork.svg',
00416                 'rhythmbox-missing-artwork.png')  ## need a white vs black when we change the background colour
00417 
00418             self.album_identifier[index] = row[album_col]
00419             items += html_elements(
00420                 fullfilename=cover,
00421                 caption=row[album_col].name,
00422                 title=row[album_col].artist,
00423                 identifier=str(index))
00424 
00425             index += 1
00426 
00427             if index == max_covers:
00428                 break
00429 
00430         if index != 0:
00431             #self.callback_view.last_album = self.album_identifier[0]
00432             pass
00433         else:
00434             self.callback_view.last_album = None
00435 
00436         return items
 All Classes Functions