Arquivos
coverart-browser/coverart_artistview.py
2014-12-03 19:32:49 +00:00

1204 linhas
42 KiB
Python

# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
#
# Copyright (C) 2012 - fossfreedom
# Copyright (C) 2012 - Agustin Carrasco
#
# 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, 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 St, Fifth Floor, Boston, MA 02110-1301 USA.
import os
import tempfile
import shutil
from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import GdkPixbuf
from gi.repository import RB
from coverart_browser_prefs import GSetting
from coverart_album import Album
from coverart_album import AlbumsModel
from coverart_album import CoverManager
from coverart_widgets import AbstractView
from coverart_utils import SortedCollection
from coverart_widgets import PanedCollapsible
from coverart_toolbar import ToolbarObject
from coverart_utils import idle_iterator
from coverart_utils import dumpstack
from coverart_utils import create_pixbuf_from_file_at_size
from coverart_extdb import CoverArtExtDB
import coverart_rb3compat as rb3compat
from coverart_rb3compat import Menu
import rb
def create_temporary_copy(path):
temp_dir = tempfile.gettempdir()
filename = tempfile.mktemp()
temp_path = os.path.join(temp_dir, filename)
shutil.copy2(path, temp_path)
return temp_path
ARTIST_LOAD_CHUNK = 50
class Artist(GObject.Object):
'''
An album. It's conformed from one or more tracks, and many of it's
information is deduced from them.
:param name: `str` name of the artist.
:param cover: `Cover` cover for this artist.
'''
# signals
__gsignals__ = {
'modified': (GObject.SIGNAL_RUN_FIRST, None, ()),
'emptied': (GObject.SIGNAL_RUN_LAST, None, ()),
'cover-updated': (GObject.SIGNAL_RUN_LAST, None, ())
}
__hash__ = GObject.__hash__
def __init__(self, name, cover):
super(Artist, self).__init__()
self.name = name
self._cover = None
self.cover = cover
self._signals_id = {}
@property
def cover(self):
return self._cover
@cover.setter
def cover(self, new_cover):
# if self._cover:
# self._cover.disconnect(self._cover_resized_id)
self._cover = new_cover
#self._cover_resized_id = self._cover.connect('resized',
# lambda *args: self.emit('cover-updated'))
self.emit('cover-updated')
def create_ext_db_key(self):
'''
Returns an `RB.ExtDBKey`
'''
key = RB.ExtDBKey.create_lookup('artist', self.name)
return key
class ArtistsModel(GObject.Object):
'''
Model that contains artists, keeps them sorted, filtered and provides an
external `Gtk.TreeModel` interface to use as part of a Gtk interface.
The `Gtk.TreeModel` haves the following structure:
column 0 -> string containing the artist name
column 1 -> pixbuf of the artist's cover.
column 2 -> instance of the artist or album itself.
column 3 -> boolean that indicates if the row should be shown
column 4 -> blank text column to pad the view correctly
column 5 -> markup containing formatted text
column 6 -> blank text for the expander column
'''
# signals
__gsignals__ = {
'update-path': (GObject.SIGNAL_RUN_LAST, None, (object,)),
'visual-updated': ((GObject.SIGNAL_RUN_LAST, None, (object, object)))
}
# list of columns names and positions on the TreeModel
columns = {'tooltip': 0, 'pixbuf': 1,
'artist_album': 2, 'show': 3,
'empty': 4, 'markup': 5, 'expander': 6}
def __init__(self, album_manager):
super(ArtistsModel, self).__init__()
self.album_manager = album_manager
self._iters = {}
self._albumiters = {}
self._artists = SortedCollection(
key=lambda artist: getattr(artist, 'name'))
self._tree_store = Gtk.TreeStore(str, GdkPixbuf.Pixbuf, object,
bool, str, str, str)
# sorting idle call
self._sort_process = None
# create the filtered store that's used with the view
self._filtered_store = self._tree_store.filter_new()
self._filtered_store.set_visible_column(ArtistsModel.columns['show'])
self._tree_sort = Gtk.TreeModelSort(model=self._filtered_store)
# self._tree_sort.set_default_sort_func(lambda *unused: 0)
self._tree_sort.set_sort_func(0, self._compare, None)
self._connect_signals()
def _connect_signals(self):
self.connect('update-path', self._on_update_path)
self.album_manager.model.connect('filter-changed', self._on_album_filter_changed)
def _on_album_filter_changed(self, *args):
if len(self._iters) == 0:
return
artists = list(set(row[AlbumsModel.columns['album']].artist for row in self.album_manager.model.store))
for artist in self._iters:
self.show(artist, artist in artists)
def _compare(self, model, row1, row2, user_data):
if not model.iter_has_child(row1) or \
not model.iter_has_child(row2):
return 0
sort_column = 0
value1 = RB.search_fold(model.get_value(row1, sort_column))
value2 = RB.search_fold(model.get_value(row2, sort_column))
if value1 < value2:
return -1
elif value1 == value2:
return 0
else:
return 1
@property
def store(self):
# return self._filtered_store
return self._tree_sort
def add(self, artist):
'''
Add an artist to the model.
:param artist: `Artist` to be added to the model.
'''
# generate necessary values
values = self._generate_artist_values(artist)
# insert the values
pos = self._artists.insert(artist)
tree_iter = self._tree_store.insert(None, pos, values)
child_iter = self._tree_store.insert(tree_iter, pos, values) # dummy child row so that the expand is available
# connect signals
ids = (artist.connect('modified', self._artist_modified),
artist.connect('cover-updated', self._cover_updated),
artist.connect('emptied', self.remove))
if not artist.name in self._iters:
self._iters[artist.name] = {}
self._iters[artist.name] = {'artist_album': artist,
'iter': tree_iter, 'dummy_iter': child_iter, 'ids': ids}
return tree_iter
def _emit_signal(self, tree_iter, signal):
# we get the filtered path and iter since that's what the outside world
# interacts with
tree_path = self._filtered_store.convert_child_path_to_path(
self._tree_store.get_path(tree_iter))
if tree_path:
# if there's no path, the album doesn't show on the filtered model
# so no one needs to know
tree_iter = self._filtered_store.get_iter(tree_path)
self.emit(signal, tree_path, tree_iter)
def remove(self, *args):
print("artist remove")
def _cover_updated(self, artist):
tree_iter = self._iters[artist.name]['iter']
if self._tree_store.iter_is_valid(tree_iter):
# only update if the iter is valid
pixbuf = artist.cover.pixbuf
self._tree_store.set_value(tree_iter, self.columns['pixbuf'],
pixbuf)
self._emit_signal(tree_iter, 'visual-updated')
def _artist_modified(self, *args):
print("artist modified")
def _on_update_path(self, widget, treepath):
'''
called when update-path signal is called
'''
artist = self.get_from_path(treepath)
albums = self.album_manager.model.get_all()
self.add_album_to_artist(artist, albums)
def add_album_to_artist(self, artist, albums):
'''
Add an album to the artist in the model.
:param artist: `Artist` for the album to be added to (i.e. the parent)
:param album: array of `Album` which are the children of the Artist
'''
# get the artist iter
artist_iter = self._iters[artist.name]['iter']
# now remove the dummy_iter - if this fails, we've removed this
# before and have no need to add albums
if 'dummy_iter' in self._iters[artist.name]:
self._iters[artist.name]['album'] = []
for album in albums:
if artist.name == album.artist and not (album in self._albumiters):
# now for all matching albums that were found lets add to the model
# generate necessary values
values = self._generate_album_values(album)
# insert the values
tree_iter = self._tree_store.append(artist_iter, values)
self._albumiters[album] = {}
self._albumiters[album]['iter'] = tree_iter
self._iters[artist.name]['album'].append(tree_iter)
# connect signals
ids = (album.connect('modified', self._album_modified),
album.connect('cover-updated', self._album_coverupdate),
album.connect('emptied', self._album_emptied))
self._albumiters[album]['ids'] = ids
if 'dummy_iter' in self._iters[artist.name]:
self._tree_store.remove(self._iters[artist.name]['dummy_iter'])
del self._iters[artist.name]['dummy_iter']
self.sort() # ensure the added albums are sorted correctly
def _album_modified(self, album):
print("album modified")
print(album)
if not (album in self._albumiters):
print("not found in albumiters")
return
tree_iter = self._albumiters[album]['iter']
if self._tree_store.iter_is_valid(tree_iter):
# only update if the iter is valid
# generate and update values
tooltip, pixbuf, album, show, blank, markup, empty = \
self._generate_album_values(album)
self._tree_store.set(tree_iter, self.columns['tooltip'], tooltip,
self.columns['markup'], markup, self.columns['show'], show)
self.sort() # ensure the added albums are sorted correctly
def _album_emptied(self, album):
'''
Removes this album from the model.
:param album: `Album` to be removed from the model.
'''
print('album emptied')
print(album)
print(album.artist)
if not (album in self._albumiters):
print("not found in albumiters")
return
artist = self.get(album.artist)
album_iter = self._albumiters[album]['iter']
self._iters[album.artist]['album'].remove(album_iter)
self._tree_store.remove(album_iter)
# disconnect signals
for sig_id in self._albumiters[album]['ids']:
album.disconnect(sig_id)
del self._albumiters[album]
# test if there are any more albums for this artist otherwise just cleanup
if len(self._iters[album.artist]['album']) == 0:
self.remove(artist)
self._on_album_filter_changed(_)
def _album_coverupdate(self, album):
tooltip, pixbuf, album, show, blank, markup, empty = self._generate_album_values(album)
self._tree_store.set_value(self._albumiters[album]['iter'],
self.columns['pixbuf'], pixbuf)
def _generate_artist_values(self, artist):
tooltip = artist.name
pixbuf = artist.cover.pixbuf
show = True
return tooltip, pixbuf, artist, show, '', \
GLib.markup_escape_text(tooltip), ''
def _generate_album_values(self, album):
tooltip = album.name
pixbuf = album.cover.pixbuf.scale_simple(48, 48, GdkPixbuf.InterpType.BILINEAR)
show = True
rating = album.rating
if int(rating) > 0:
rating = u'\u2605' * int(rating)
else:
rating = ''
year = ' (' + str(album.real_year) + ')'
track_count = album.track_count
if track_count == 1:
detail = rb3compat.unicodedecode(_(' with 1 track'), 'UTF-8')
else:
detail = rb3compat.unicodedecode(_(' with %d tracks') %
track_count, 'UTF-8')
duration = album.duration / 60
if duration == 1:
detail += rb3compat.unicodedecode(_(' and a duration of 1 minute'), 'UTF-8')
else:
detail += rb3compat.unicodedecode(_(' and a duration of %d minutes') %
duration, 'UTF-8')
tooltip = rb3compat.unicodestr(tooltip, 'utf-8')
tooltip = rb3compat.unicodeencode(tooltip, 'utf-8')
import cgi
formatted = '<b><i>' + \
cgi.escape(rb3compat.unicodedecode(tooltip, 'utf-8')) + \
'</i></b>' + \
year + \
' ' + rating + \
'\n<small>' + \
GLib.markup_escape_text(detail) + \
'</small>'
return tooltip, pixbuf, album, show, '', formatted, ''
def remove(self, artist):
'''
Removes this artist from the model.
:param artist: `Artist` to be removed from the model.
'''
self._artists.remove(artist)
self._tree_store.remove(self._iters[artist.name]['iter'])
del self._iters[artist.name]
def contains(self, artist_name):
'''
Indicates if the model contains a specific artist.
:param artist_name: `str` name of the artist.
'''
return artist_name in self._iters
def get(self, artist_name):
'''
Returns the requested Artist.
:param artist_name: `str` name of the artist.
'''
return self._iters[artist_name]['artist_album']
def get_albums(self, artist_name):
'''
Returns the displayed albums for the requested artist
:param artist_name: `str` name of the artist.
'''
albums = []
artist_iter = self._iters[artist_name]['iter']
next_iter = self._tree_store.iter_children(artist_iter)
while next_iter != None:
albums.append(self._tree_store[next_iter][self.columns['artist_album']])
next_iter = self._tree_store.iter_next(next_iter)
# if 'album' in self._iters[artist_name]:
# for album_iter in self._iters[artist_name]['album']:
# path = self._tree_store.get_path(album_iter)
# if path:
# tree_path = self._filtered_store.convert_child_path_to_path(
# )
# albums.append(self.get_from_path(tree_path))
return albums
def get_all(self):
'''
Returns a collection of all the artists in this model.
'''
return self._artists
def get_from_path(self, path):
'''
Returns the Artist or Album referenced by a `Gtk.TreeModelSort` path.
:param path: `Gtk.TreePath` referencing the artist.
'''
return self.store[path][self.columns['artist_album']]
def get_path(self, artist):
print(artist.name)
print(self._iters[artist.name]['iter'])
return self._tree_store.get_path(
self._iters[artist.name]['iter'])
def get_from_ext_db_key(self, key):
'''
Returns the requested artist.
:param key: ext_db_key
'''
# get the album name and artist
name = key.get_field('artist')
# first check if there's a direct match
artist = self.get(name) if self.contains(name) else None
return artist
def show(self, artist_name, show):
'''
filters/unfilters an artist, making it visible to the publicly available model's
`Gtk.TreeModel`
:param artist: str containing the name of the artist to show or hide.
:param show: `bool` indcating whether to show(True) or hide(False) the
artist.
'''
artist_iter = self._iters[artist_name]['iter']
if self._tree_store.iter_is_valid(artist_iter):
self._tree_store.set_value(artist_iter, self.columns['show'], show)
def sort(self):
albums = SortedCollection(key=lambda album: getattr(album, 'name'))
gs = GSetting()
source_settings = gs.get_setting(gs.Path.PLUGIN)
key = source_settings[gs.PluginKey.SORT_BY_ARTIST]
order = source_settings[gs.PluginKey.SORT_ORDER_ARTIST]
sort_keys = {
'name_artist': ('album_sort', 'album_sort'),
'year_artist': ('real_year', 'calc_year_sort'),
'rating_artist': ('rating', 'album_sort')
}
props = sort_keys[key]
def key_function(album):
keys = [getattr(album, prop) for prop in props]
return keys
# remember the current sort then remove the sort order
# because sorting will only work in unsorted lists
sortSettings = self.store.get_sort_column_id()
self.store.set_sort_column_id(-1, Gtk.SortType.ASCENDING)
for artist in self._iters:
albums.clear()
albums.key = key_function
if 'album' in self._iters[artist] and len(self._iters[artist]['album']) > 1:
# we only need to sort an artists albums if there is more than one album
# sort all the artists albums
for album_iter in self._iters[artist]['album']:
albums.insert(self._tree_store[album_iter][self.columns['artist_album']])
if not order:
albums = reversed(albums)
# now we iterate through the sorted artist albums. Look and swap iters
# according to where they are in the tree store
artist_iter = self._iters[artist]['iter']
next_iter = self._tree_store.iter_children(artist_iter)
for album in albums:
if self._tree_store[next_iter][self.columns['artist_album']] != album:
self._tree_store.swap(next_iter, self._albumiters[album]['iter'])
next_iter = self._albumiters[album]['iter']
next_iter = self._tree_store.iter_next(next_iter)
# now we have finished sorting, reapply the sort
if sortSettings[0]:
self.store.set_sort_column_id(*sortSettings)
class ArtistCellRenderer(Gtk.CellRendererPixbuf):
def __init__(self):
super(ArtistCellRenderer, self).__init__()
def do_render(self, cr, widget,
background_area,
cell_area,
flags):
newpix = self.props.pixbuf # .copy()
# newpix = newpix.scale_simple(48,48,GdkPixbuf.InterpType.BILINEAR)
Gdk.cairo_set_source_pixbuf(cr, newpix, 0, 0)
cr.paint()
class ArtistLoader(GObject.Object):
'''
Loads Artists - updating the model accordingly.
:param artist_manager: `artist_manager` responsible for this loader.
'''
# signals
__gsignals__ = {
'artists-load-finished': (GObject.SIGNAL_RUN_LAST, None, (object,)),
'model-load-finished': (GObject.SIGNAL_RUN_LAST, None, ())
}
def __init__(self, artist_manager, album_manager):
super(ArtistLoader, self).__init__()
self.shell = artist_manager.shell
self._connect_signals()
self._album_manager = album_manager
self._artist_manager = artist_manager
self.model = artist_manager.model
def load_artists(self):
print("load_artists")
albums = self._album_manager.model.get_all()
model = list(set(album.artist for album in albums))
self._load_artists(iter(model), artists={}, model=model,
total=len(model), progress=0.)
@idle_iterator
def _load_artists(self):
def process(row, data):
# allocate the artist
artist = Artist(row, self._artist_manager.cover_man.unknown_cover)
data['artists'][row] = artist
def after(data):
# update the progress
data['progress'] += ARTIST_LOAD_CHUNK
self._album_manager.progress = data['progress'] / data['total']
def error(exception):
print('Error processing entries: ' + str(exception))
def finish(data):
self._album_manager.progress = 1
self.emit('artists-load-finished', data['artists'])
return ARTIST_LOAD_CHUNK, process, after, error, finish
@idle_iterator
def _load_model(self):
def process(artist, data):
# add the artists to the model
self._artist_manager.model.add(artist)
def after(data):
data['progress'] += ARTIST_LOAD_CHUNK
# update the progress
self._album_manager.progress = 1 - data['progress'] / data['total']
def error(exception):
dumpstack("Something awful happened!")
print('Error(2) while adding artists to the model: ' + str(exception))
def finish(data):
self._album_manager.progress = 1
self.emit('model-load-finished')
print("finished")
# return False
return ARTIST_LOAD_CHUNK, process, after, error, finish
def _connect_signals(self):
# connect signals for updating the albums
# self.entry_changed_id = self._album_manager.db.connect('entry-changed',
# self._entry_changed_callback)
pass
def do_artists_load_finished(self, artists):
self._load_model(iter(list(artists.values())), total=len(artists), progress=0.)
self._album_manager.model.connect('album-added', self._on_album_added)
def _on_album_added(self, album_model, album):
'''
called when album-manager album-added signal is invoked
'''
print(album.artist)
if self._artist_manager.model.contains(album.artist):
print("contains artist")
artist = self._artist_manager.model.get(album.artist)
self._artist_manager.model.add_album_to_artist(artist, [album])
else:
print("new artist")
artist = Artist(album.artist, self._artist_manager.cover_man.unknown_cover)
self._artist_manager.model.add(artist)
class ArtistCoverManager(CoverManager):
force_lastfm_check = True
def __init__(self, plugin, artist_manager):
self.cover_db = CoverArtExtDB(name='artist-art')
super(ArtistCoverManager, self).__init__(plugin, artist_manager)
self.cover_size = 72
# create unknown cover and shadow for covers
self.create_unknown_cover(plugin)
def create_unknown_cover(self, plugin):
# create the unknown cover
self.unknown_cover = self.create_cover(
rb.find_plugin_file(plugin, 'img/microphone.png'))
super(ArtistCoverManager, self).create_unknown_cover(plugin)
def update_pixbuf_cover(self, coverobject, pixbuf):
# if it's a pixbuf, assign it to all the artist for the artist
key = RB.ExtDBKey.create_storage('artist', coverobject.name)
self.cover_db.store(key, RB.ExtDBSourceType.USER_EXPLICIT,
pixbuf)
class ArtistManager(GObject.Object):
'''
Main construction that glues together the different managers, the loader
and the model. It takes care of initializing all the system.
:param plugin: `Peas.PluginInfo` instance.
:param current_view: `ArtistView` where the Artists are shown.
'''
# singleton instance
instance = None
# properties
progress = GObject.property(type=float, default=0)
# signals
__gsignals__ = {
'sort': (GObject.SIGNAL_RUN_LAST, None, (object,))
}
def __init__(self, plugin, album_manager, shell):
super(ArtistManager, self).__init__()
self.db = plugin.shell.props.db
self.shell = shell
self.plugin = plugin
self.cover_man = ArtistCoverManager(plugin, self)
self.cover_man.album_manager = album_manager
self.model = ArtistsModel(album_manager)
self.loader = ArtistLoader(self, album_manager)
# connect signals
self._connect_signals()
def _connect_signals(self):
'''
Connects the manager to all the needed signals for it to work.
'''
self.loader.connect('model-load-finished', self._load_finished_callback)
self.connect('sort', self._sort_artist)
def _sort_artist(self, widget, param):
toolbar_type = param
if not toolbar_type or toolbar_type == "artist":
self.model.sort()
def _load_finished_callback(self, *args):
self.cover_man.load_covers()
class ArtistShowingPolicy(GObject.Object):
'''
Policy that mostly takes care of how and when things should be showed on
the view that makes use of the `AlbumsModel`.
'''
def __init__(self, flow_view):
super(ArtistShowingPolicy, self).__init__()
self._flow_view = flow_view
self.counter = 0
self._has_initialised = False
def initialise(self, album_manager):
if self._has_initialised:
return
self._has_initialised = True
self._album_manager = album_manager
self._model = album_manager.model
class ArtistView(Gtk.TreeView, AbstractView):
__gtype_name__ = "ArtistView"
name = 'artistview'
icon_automatic = GObject.property(type=bool, default=True)
panedposition = PanedCollapsible.Paned.COLLAPSE
__gsignals__ = {
'update-toolbar': (GObject.SIGNAL_RUN_LAST, None, ())
}
def __init__(self, *args, **kwargs):
super(ArtistView, self).__init__(*args, **kwargs)
self._external_plugins = None
self.gs = GSetting()
self.show_policy = ArtistShowingPolicy(self)
self.view = self
self._has_initialised = False
self._last_row_was_artist = False
def initialise(self, source):
if self._has_initialised:
return
self._has_initialised = True
self.view_name = "artist_view"
super(ArtistView, self).initialise(source)
self.album_manager = source.album_manager
self.shell = source.shell
self.props.has_tooltip = True
self.set_enable_tree_lines(True)
col = Gtk.TreeViewColumn(' ', Gtk.CellRendererText(), text=6)
self.append_column(col)
pixbuf = Gtk.CellRendererPixbuf()
col = Gtk.TreeViewColumn(_('Covers'), pixbuf, pixbuf=1)
self.append_column(col)
col = Gtk.TreeViewColumn(_('Artist'), Gtk.CellRendererText(), markup=5)
self._artist_col = col
col.set_clickable(True)
col.set_sort_column_id(0)
col.set_sort_indicator(True)
col.connect('clicked', self._artist_sort_clicked)
self.append_column(col)
col = Gtk.TreeViewColumn('', Gtk.CellRendererText(), text=4)
self.append_column(col) # dummy column to expand horizontally
self.artist_manager = self.album_manager.artist_man
self.artist_manager.model.store.set_sort_column_id(0, Gtk.SortType.ASCENDING)
self.set_model(self.artist_manager.model.store)
# setup iconview drag&drop support
# first drag and drop on the coverart view to receive coverart
self.enable_model_drag_dest([], Gdk.DragAction.COPY)
self.drag_dest_add_image_targets()
self.drag_dest_add_text_targets()
self.connect('drag-drop', self.on_drag_drop)
self.connect('drag-data-received',
self.on_drag_data_received)
# lastly support drag-drop from coverart to devices/nautilus etc
# n.b. enabling of drag-source is controlled by the selection-changed to ensure
# we dont allow drag from artists
self.connect('drag-begin', self.on_drag_begin)
self._targets = Gtk.TargetList.new([Gtk.TargetEntry.new("text/uri-list", 0, 0)])
# N.B. values taken from rhythmbox v2.97 widgets/rb_entry_view.c
self._targets.add_uri_targets(1)
self.connect("drag-data-get", self.on_drag_data_get)
# define artist specific popup menu
self.artist_popup_menu = Menu(self.plugin, self.shell)
self.artist_popup_menu.load_from_file('ui/coverart_artist_pop_rb2.ui',
'ui/coverart_artist_pop_rb3.ui')
signals = \
{'play_album_menu_item': self.source.play_album_menu_item_callback,
'queue_album_menu_item': self.source.queue_album_menu_item_callback,
'add_to_playing_menu_item': self.source.add_playlist_menu_item_callback,
'new_playlist': self.source.add_playlist_menu_item_callback,
'artist_cover_search_menu_item': self.cover_search_menu_item_callback
}
self.artist_popup_menu.connect_signals(signals)
self.artist_popup_menu.connect('pre-popup', self.pre_popup_menu_callback)
# connect properties and signals
self._connect_properties()
self._connect_signals()
def _connect_properties(self):
setting = self.gs.get_setting(self.gs.Path.PLUGIN)
setting.bind(self.gs.PluginKey.ICON_AUTOMATIC, self,
'icon_automatic', Gio.SettingsBindFlags.GET)
def _connect_signals(self):
self.connect('row-activated', self._row_activated)
self.connect('row-expanded', self._row_expanded)
self.connect('button-press-event', self._row_click)
self.get_selection().connect('changed', self._selection_changed)
self.connect('query-tooltip', self._query_tooltip)
def _artist_sort_clicked(self, *args):
# in the absence of an apparent way to remove the unsorted default_sort_func
# find out if we are now in an unsorted state - if we are
# throw another clicked event so that we remain sorted.
value, order = self.artist_manager.model.store.get_sort_column_id()
if order == None:
self._artist_col.emit('clicked')
def cover_search_menu_item_callback(self, *args):
self.artist_manager.cover_man.search_covers(self.get_selected_objects(just_artist=True),
callback=self.source.update_request_status_bar)
def _query_tooltip(self, widget, x, y, key, tooltip):
try:
winx, winy = self.convert_widget_to_bin_window_coords(x, y)
treepath, treecolumn, cellx, celly = self.get_path_at_pos(winx, winy)
active_object = self.artist_manager.model.get_from_path(treepath)
# active_object=self.artist_manager.model.store[treepath][self.artist_manager.model.columns['artist_album']]
if isinstance(active_object, Artist) and \
treecolumn.get_title() == _('Covers') and \
active_object.cover.original != self.artist_manager.cover_man.unknown_cover.original:
# we display the tooltip if the row is an artist and the column
# is actually the artist cover itself
pixbuf = GdkPixbuf.Pixbuf.new_from_file(active_object.cover.original)
src_width = pixbuf.get_width()
src_height = pixbuf.get_height()
factor = min(float(256) / float(src_width), float(256) / float(src_height))
new_width = int(src_width * factor + 0.5)
new_height = int(src_height * factor + 0.5)
pixbuf = create_pixbuf_from_file_at_size(
active_object.cover.original, new_width, new_height)
tooltip.set_icon(pixbuf)
return True
else:
return False
except:
pass
def _row_expanded(self, treeview, treeiter, treepath):
'''
event called when clicking the expand icon on the treeview
'''
self._row_activated(treeview, treepath, _)
def _row_activated(self, treeview, treepath, treeviewcolumn):
'''
event called when double clicking on the tree-view or by keyboard ENTER
'''
active_object = self.artist_manager.model.get_from_path(treepath)
if isinstance(active_object, Artist):
self.artist_manager.model.emit('update-path', treepath)
else:
# we need to play this album
self.source.play_selected_album(self.source.favourites)
def pre_popup_menu_callback(self, *args):
'''
callback when artist popup menu is about to be displayed
'''
state, sensitive = self.shell.props.shell_player.get_playing()
if not state:
sensitive = False
# self.popup_menu.get_menu_object('add_to_playing_menu_item')
self.artist_popup_menu.set_sensitive('add_to_playing_menu_item', sensitive)
self.source.playlist_menu_item_callback()
def _row_click(self, widget, event):
'''
event called when clicking on a row
'''
print('_row_click')
try:
treepath, treecolumn, cellx, celly = self.get_path_at_pos(event.x, event.y)
except:
return
active_object = self.artist_manager.model.get_from_path(treepath)
if not isinstance(active_object, Album):
self.source.artist_info.emit('selected', active_object.name, None)
if self.icon_automatic:
# reset counter so that we get correct double click action for albums
self.source.click_count = 0
if treecolumn != self.get_expander_column():
if self.row_expanded(treepath) and event.button == 1 and self._last_row_was_artist:
self.collapse_row(treepath)
else:
self.expand_row(treepath, False)
self._last_row_was_artist = True
if event.button == 3:
# on right click
# display popup
self.artist_popup_menu.popup(self.source, 'popup_menu', 3,
Gtk.get_current_event_time())
print('_row click artist exit')
return
if event.button == 1:
# on click
# to expand the entry view
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
shift = event.state & Gdk.ModifierType.SHIFT_MASK
if self.icon_automatic and not self._last_row_was_artist:
self.source.click_count += 1 if not ctrl and not shift else 0
if self.source.click_count == 1:
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 250,
self.source.show_hide_pane, active_object)
elif event.button == 3:
# on right click
# display popup
self.popup.popup(self.source, 'popup_menu', 3,
Gtk.get_current_event_time())
self._last_row_was_artist = False
print('_row_click album exit')
return
def get_view_icon_name(self):
return "artistview.png"
def _selection_changed(self, *args):
selected = self.get_selected_objects(just_artist=True)
print(selected)
if len(selected) == 0:
self.source.entry_view.clear()
return
if isinstance(selected[0], Artist):
print("selected artist")
self.unset_rows_drag_source() # turn off drag-drop for artists
self.source.entryviewpane.update_cover(selected[0],
self.artist_manager)
else:
print("selected album")
self.source.update_with_selection()
# now turnon drag-drop for album.
self.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
[], Gdk.DragAction.COPY)
self.drag_source_set_target_list(self._targets)
def switch_to_coverpane(self, cover_search_pane):
'''
called from the source to update the coverpane when
it is switched from the track pane
This overrides the base method
'''
selected = self.get_selected_objects(just_artist=True)
if selected:
manager = self.get_default_manager()
self.source.entryviewpane.cover_search(selected[0],
manager)
def get_selected_objects(self, just_artist=False):
'''
finds what has been selected
returns an array of `Album`
'''
selection = self.get_selection()
model, treeiter = selection.get_selected()
if treeiter:
active_object = model.get_value(treeiter, ArtistsModel.columns['artist_album'])
if isinstance(active_object, Album):
# have chosen an album then just return that album
return [active_object]
else:
# must have chosen an artist - return all albums for the artist by default
# or just the artist itself
if not just_artist:
return self.artist_manager.model.get_albums(active_object.name)
else:
return [active_object]
return []
def switch_to_view(self, source, album):
self.initialise(source)
self.show_policy.initialise(source.album_manager)
self.scroll_to_album(album)
def scroll_to_album(self, album):
if album:
print("switch to artist view")
print(album)
artist = self.artist_manager.model.get(album.artist)
path = self.artist_manager.model.get_path(artist)
print(artist)
print(path)
path = self.artist_manager.model.store.convert_child_path_to_path(path)
print(path)
if path:
self.scroll_to_cell(path, self._artist_col)
self.expand_row(path, False)
self.set_cursor(path)
def do_update_toolbar(self, *args):
self.source.toolbar_manager.set_enabled(False, ToolbarObject.SORT_BY)
self.source.toolbar_manager.set_enabled(False, ToolbarObject.SORT_ORDER)
self.source.toolbar_manager.set_enabled(True, ToolbarObject.SORT_BY_ARTIST)
self.source.toolbar_manager.set_enabled(True, ToolbarObject.SORT_ORDER_ARTIST)
def on_drag_drop(self, widget, context, x, y, time):
'''
Callback called when a drag operation finishes over the view
of the source. It decides if the dropped item can be processed as
an image to use as a cover.
'''
# stop the propagation of the signal (deactivates superclass callback)
widget.stop_emission_by_name('drag-drop')
# obtain the path of the icon over which the drag operation finished
drop_info = self.get_dest_row_at_pos(x, y)
path = None
if drop_info:
path, position = drop_info
result = path is not None
if result:
target = self.drag_dest_find_target(context, None)
widget.drag_get_data(context, target, time)
return result
def on_drag_data_received(self, widget, drag_context, x, y, data, info,
time):
'''
Callback called when the drag source has prepared the data (pixbuf)
for us to use.
'''
# stop the propagation of the signal (deactivates superclass callback)
widget.stop_emission_by_name('drag-data-received')
# get the artist and the info and ask the loader to update the cover
path, position = self.get_dest_row_at_pos(x, y)
artist_album = widget.get_model()[path][2]
pixbuf = data.get_pixbuf()
if isinstance(artist_album, Album):
manager = self.album_manager
else:
manager = self.artist_manager
if pixbuf:
manager.cover_man.update_cover(artist_album, pixbuf)
else:
uri = data.get_text()
manager.cover_man.update_cover(artist_album, uri=uri)
# call the context drag_finished to inform the source about it
drag_context.finish(True, False, time)
def on_drag_data_get(self, widget, drag_context, data, info, time):
'''
Callback called when the drag destination (playlist) has
requested what album (icon) has been dragged
'''
uris = []
for album in widget.get_selected_objects():
for track in album.get_tracks():
uris.append(track.location)
data.set_uris(uris)
# stop the propagation of the signal (deactivates superclass callback)
widget.stop_emission_by_name('drag-data-get')
def on_drag_begin(self, widget, context):
'''
Callback called when the drag-drop from coverview has started
Changes the drag icon as appropriate
'''
album_number = len(widget.get_selected_objects())
if album_number == 1:
item = Gtk.STOCK_DND
else:
item = Gtk.STOCK_DND_MULTIPLE
widget.drag_source_set_icon_stock(item)
widget.stop_emission_by_name('drag-begin')
def get_default_manager(self):
'''
the default manager for this view is the artist_manager
'''
return self.artist_manager