tracklist: Create an AlbumCover TrackRow

The AlbumCover shows a cover.jpg image for a specific Album in a column.
I also need to do some special handling so generate a tooltip to show a
larger version of the image.

I try to cache the AlbumCover Texture to cut down on disk accesses,
since we'll usually end up loading the same image several times for each
track in an album.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2023-01-17 15:19:36 -05:00
parent b3dcd3c0b9
commit 97bf9d48db
6 changed files with 125 additions and 14 deletions

View File

@ -78,3 +78,10 @@ columnview.emmental-track-list > listview > row > cell {
columnview.emmental-track-list > listview > row > cell > label {
padding: 0px 4px;
}
columnview.emmental-track-list > listview > row > cell > picture {
padding: 4px 0px;
min-height: 36px;
min-width: 36px;
border-radius: 15%;
}

View File

@ -4,6 +4,7 @@ import datetime
import dateutil.tz
import pathlib
from gi.repository import GObject
from gi.repository import Gdk
from gi.repository import Gtk
from .. import buttons
from .. import factory
@ -41,15 +42,26 @@ class TrackRow(factory.ListRow):
self.album_binding = album.bind_property(self.property,
self.child, child_prop)
def bind_album_to_self(self, self_prop: str) -> None:
"""Bind an album property to the TrackRow child."""
album = self.item.get_medium().get_album()
self.set_property(self_prop, album.get_property(self.property))
self.album_binding = album.bind_property(self.property,
self, self_prop)
def bind_to_self(self, item_prop: str, child_prop: str) -> None:
"""Bind an item property directly to a TrackRow property."""
self.bind_and_set(self.item, item_prop, self, child_prop)
def rebind_album(self, child_prop: str) -> None:
def rebind_album(self, child_prop: str, to_self: bool = False) -> None:
"""Rebind an album property to the TrackRow child."""
if self.album_binding is not None:
self.album_binding.unbind()
self.bind_album(child_prop)
if to_self:
self.bind_album_to_self(child_prop)
else:
self.bind_album(child_prop)
@GObject.Property(type=bool, default=False)
def active(self) -> bool:
@ -228,6 +240,45 @@ class AlbumString(InscriptionRow):
self.bind_to_self("mediumid", "mediumid")
class AlbumCover(TrackRow):
"""A Track Row to display Album art."""
Cache = dict()
filepath = GObject.Property(type=GObject.TYPE_PYOBJECT)
def __init__(self, listitem: Gtk.ListItem, property: str):
"""Initialize an Album Cover row."""
super().__init__(listitem, property)
self.child = Gtk.Picture(content_fit=Gtk.ContentFit.COVER)
self.connect("notify", self.__update_album_cover)
self.child.connect("query-tooltip", self.__query_tooltip)
def __update_album_cover(self, row: TrackRow, param) -> None:
match param.name:
case "mediumid": self.rebind_album("filepath", to_self=True)
case "filepath":
if self.filepath is None:
texture = None
elif (texture := AlbumCover.Cache.get(self.filepath)) is None:
texture = Gdk.Texture.new_from_filename(str(self.filepath))
AlbumCover.Cache[self.filepath] = texture
self.child.set_paintable(texture)
self.child.set_has_tooltip(texture is not None)
def __query_tooltip(self, child: Gtk.Picture, x: int, y: int,
keyboard_mode: bool, tooltip: Gtk.Tooltip) -> bool:
texture = AlbumCover.Cache.get(self.filepath)
tooltip.set_custom(Gtk.Picture.new_for_paintable(texture))
return True
def do_bind(self) -> None:
"""Bind the Album Art path to the filepath property."""
super().do_bind()
self.bind_to_self("mediumid", "mediumid")
class FavoriteButton(TrackRow):
"""A TrackRow with an toggle to set Track favorite status."""

View File

@ -28,6 +28,7 @@ class TrackView(Gtk.Frame):
model=self._selection)
self._scrollwin = Gtk.ScrolledWindow(child=self._columnview)
self.__append_column("Art", "cover", row.AlbumCover, resizable=False)
self.__append_column("Fav", "favorite", row.FavoriteButton,
resizable=False)
self.__append_column("Track", "number", row.TracknoString,

View File

@ -121,6 +121,7 @@ class TestSettings(unittest.TestCase):
def test_save_tracklist_column_width(self, mock_stdout: io.StringIO):
"""Test saving tracklist column widths."""
self.assertEqual(self.settings["tracklist.art.size"], -1)
self.assertEqual(self.settings["tracklist.fav.size"], -1)
self.assertEqual(self.settings["tracklist.track.size"], 55)
self.assertEqual(self.settings["tracklist.title.size"], 300)
@ -136,6 +137,7 @@ class TestSettings(unittest.TestCase):
for column in self.win.tracklist.columns:
column.set_fixed_width(123)
self.assertEqual(self.settings["tracklist.art.size"], 123)
self.assertEqual(self.settings["tracklist.fav.size"], 123)
self.assertEqual(self.settings["tracklist.track.size"], 123)
self.assertEqual(self.settings["tracklist.title.size"], 123)
@ -150,6 +152,7 @@ class TestSettings(unittest.TestCase):
def test_save_tracklist_column_visible(self, mock_stdout: io.StringIO):
"""Test saving tracklist column visibility."""
self.assertTrue(self.settings["tracklist.art.visible"])
self.assertTrue(self.settings["tracklist.fav.visible"])
self.assertTrue(self.settings["tracklist.track.visible"])
self.assertTrue(self.settings["tracklist.title.visible"])
@ -165,6 +168,7 @@ class TestSettings(unittest.TestCase):
for column in self.win.tracklist.columns:
column.set_visible(not column.get_visible())
self.assertFalse(self.settings["tracklist.art.visible"])
self.assertFalse(self.settings["tracklist.fav.visible"])
self.assertFalse(self.settings["tracklist.track.visible"])
self.assertFalse(self.settings["tracklist.title.visible"])

View File

@ -8,6 +8,7 @@ import emmental.tracklist.row
import tests.util
import unittest.mock
from gi.repository import GObject
from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import Adw
@ -20,7 +21,8 @@ class TestTrackRowWidgets(tests.util.TestCase):
super().setUp()
self.sql.playlists.load(now=True)
self.library = self.sql.libraries.create(pathlib.Path("/a/b"))
self.album = self.sql.albums.create("Test Album", "Artist", "2023")
self.album = self.sql.albums.create("Test Album", "Artist", "2023",
cover=tests.util.COVER_JPG)
self.medium = self.sql.media.create(self.album, "", number=1)
self.year = self.sql.years.create(2023)
self.track = self.sql.tracks.create(self.library,
@ -168,6 +170,38 @@ class TestTrackRowWidgets(tests.util.TestCase):
row.unbind()
self.assertIsNone(row.album_binding)
def test_album_cover(self):
"""Test the Album Cover widget."""
self.assertDictEqual(emmental.tracklist.row.AlbumCover.Cache, {})
cache = emmental.tracklist.row.AlbumCover.Cache
row = emmental.tracklist.row.AlbumCover(self.listitem, "cover")
self.assertIsInstance(row, emmental.tracklist.row.TrackRow)
self.assertIsInstance(row.child, Gtk.Picture)
self.assertEqual(row.property, "cover")
self.assertEqual(row.child.get_content_fit(), Gtk.ContentFit.COVER)
self.assertIsNone(row.filepath)
row.bind()
self.assertEqual(row.filepath, tests.util.COVER_JPG)
self.assertEqual(len(cache), 1)
self.assertIsInstance(cache[tests.util.COVER_JPG], Gdk.Texture)
self.assertEqual(row.child.get_paintable(),
cache[tests.util.COVER_JPG])
self.assertTrue(row.child.get_has_tooltip())
self.album.cover = None
self.assertIsNone(row.filepath)
self.assertIsNone(row.child.get_paintable())
self.assertFalse(row.child.get_has_tooltip())
album = self.sql.albums.create("Other Album", "Other Artist", "2023",
cover=tests.util.COVER_JPG)
medium = self.sql.media.create(album, "Other Medium", number=4)
self.track.mediumid = medium.mediumid
self.assertEqual(row.filepath, tests.util.COVER_JPG)
def test_favorite_button(self):
"""Test the Favorite Button widget."""
row = emmental.tracklist.row.FavoriteButton(self.listitem, "favorite")

View File

@ -93,7 +93,21 @@ class TestTrackViewColumns(tests.util.TestCase):
self.columns = self.trackview.columns
self.listitem = Gtk.ListItem()
def test_favorite_column(self, i: int = 0):
def test_artwork_column(self, i: int = 0):
"""Test the favorite column."""
self.assertEqual(self.columns[i].get_title(), "Art")
self.assertEqual(self.columns[i].get_fixed_width(), -1)
self.assertFalse(self.columns[i].get_resizable())
self.assertTrue(self.columns[i].get_visible())
factory = self.columns[i].get_factory()
self.assertIsInstance(factory, emmental.factory.Factory)
self.assertEqual(factory.row_type, emmental.tracklist.row.AlbumCover)
factory.emit("setup", self.listitem)
self.assertEqual(self.listitem.listrow.property, "cover")
def test_favorite_column(self, i: int = 1):
"""Test the favorite column."""
self.assertEqual(self.columns[i].get_title(), "Fav")
self.assertEqual(self.columns[i].get_fixed_width(), -1)
@ -108,7 +122,7 @@ class TestTrackViewColumns(tests.util.TestCase):
factory.emit("setup", self.listitem)
self.assertEqual(self.listitem.listrow.property, "favorite")
def test_trackno_column(self, i: int = 1):
def test_trackno_column(self, i: int = 2):
"""Test the track number column."""
self.assertEqual(self.columns[i].get_title(), "Track")
self.assertEqual(self.columns[i].get_fixed_width(), 55)
@ -125,7 +139,7 @@ class TestTrackViewColumns(tests.util.TestCase):
self.assertEqual(self.listitem.listrow.child.get_xalign(), 1.0)
self.assertTrue(self.listitem.listrow.child.has_css_class("numeric"))
def test_title_column(self, i: int = 2):
def test_title_column(self, i: int = 3):
"""Test the title column."""
self.assertEqual(self.columns[i].get_title(), "Title")
self.assertEqual(self.columns[i].get_fixed_width(), 300)
@ -141,7 +155,7 @@ class TestTrackViewColumns(tests.util.TestCase):
self.assertEqual(self.listitem.listrow.child.get_xalign(), 0.0)
self.assertFalse(self.listitem.listrow.child.has_css_class("numeric"))
def test_length_column(self, i: int = 3):
def test_length_column(self, i: int = 4):
"""Test the length column."""
self.assertEqual(self.columns[i].get_title(), "Length")
self.assertEqual(self.columns[i].get_fixed_width(), -1)
@ -157,7 +171,7 @@ class TestTrackViewColumns(tests.util.TestCase):
self.assertEqual(self.listitem.listrow.child.get_xalign(), 1.0)
self.assertTrue(self.listitem.listrow.child.has_css_class("numeric"))
def test_artist_column(self, i: int = 4):
def test_artist_column(self, i: int = 5):
"""Test the artist column."""
self.assertEqual(self.columns[i].get_title(), "Artist")
self.assertEqual(self.columns[i].get_fixed_width(), 250)
@ -173,7 +187,7 @@ class TestTrackViewColumns(tests.util.TestCase):
self.assertEqual(self.listitem.listrow.child.get_xalign(), 0.0)
self.assertFalse(self.listitem.listrow.child.has_css_class("numeric"))
def test_album_artist_column(self, i: int = 5):
def test_album_artist_column(self, i: int = 6):
"""Test the album artist column."""
self.assertEqual(self.columns[i].get_title(), "Album Artist")
self.assertEqual(self.columns[i].get_fixed_width(), 250)
@ -189,7 +203,7 @@ class TestTrackViewColumns(tests.util.TestCase):
self.assertEqual(self.listitem.listrow.child.get_xalign(), 0.0)
self.assertFalse(self.listitem.listrow.child.has_css_class("numeric"))
def test_release_column(self, i: int = 6):
def test_release_column(self, i: int = 7):
"""Test the release date column."""
self.assertEqual(self.columns[i].get_title(), "Release")
self.assertEqual(self.columns[i].get_fixed_width(), 115)
@ -205,7 +219,7 @@ class TestTrackViewColumns(tests.util.TestCase):
self.assertEqual(self.listitem.listrow.child.get_xalign(), 0.0)
self.assertTrue(self.listitem.listrow.child.has_css_class("numeric"))
def test_playcount_column(self, i: int = 7):
def test_playcount_column(self, i: int = 8):
"""Test the play count column."""
self.assertEqual(self.columns[i].get_title(), "Play Count")
self.assertEqual(self.columns[i].get_fixed_width(), 135)
@ -222,7 +236,7 @@ class TestTrackViewColumns(tests.util.TestCase):
self.assertEqual(self.listitem.listrow.child.get_xalign(), 0.0)
self.assertTrue(self.listitem.listrow.child.has_css_class("numeric"))
def test_last_started_column(self, i: int = 8):
def test_last_started_column(self, i: int = 9):
"""Test the last started column."""
self.assertEqual(self.columns[i].get_title(), "Last Started")
self.assertEqual(self.columns[i].get_fixed_width(), 250)
@ -239,7 +253,7 @@ class TestTrackViewColumns(tests.util.TestCase):
self.assertEqual(self.listitem.listrow.child.get_xalign(), 0.0)
self.assertTrue(self.listitem.listrow.child.has_css_class("numeric"))
def test_last_played_column(self, i: int = 9):
def test_last_played_column(self, i: int = 10):
"""Test the last played column."""
self.assertEqual(self.columns[i].get_title(), "Last Played")
self.assertEqual(self.columns[i].get_fixed_width(), 250)
@ -256,7 +270,7 @@ class TestTrackViewColumns(tests.util.TestCase):
self.assertEqual(self.listitem.listrow.child.get_xalign(), 0.0)
self.assertTrue(self.listitem.listrow.child.has_css_class("numeric"))
def test_filepath_column(self, i: int = 10):
def test_filepath_column(self, i: int = 11):
"""Test the last played column."""
self.assertEqual(self.columns[i].get_title(), "Filepath")
self.assertEqual(self.columns[i].get_fixed_width(), -1)