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 { columnview.emmental-track-list > listview > row > cell > label {
padding: 0px 4px; 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 dateutil.tz
import pathlib import pathlib
from gi.repository import GObject from gi.repository import GObject
from gi.repository import Gdk
from gi.repository import Gtk from gi.repository import Gtk
from .. import buttons from .. import buttons
from .. import factory from .. import factory
@ -41,15 +42,26 @@ class TrackRow(factory.ListRow):
self.album_binding = album.bind_property(self.property, self.album_binding = album.bind_property(self.property,
self.child, child_prop) 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: def bind_to_self(self, item_prop: str, child_prop: str) -> None:
"""Bind an item property directly to a TrackRow property.""" """Bind an item property directly to a TrackRow property."""
self.bind_and_set(self.item, item_prop, self, child_prop) 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.""" """Rebind an album property to the TrackRow child."""
if self.album_binding is not None: if self.album_binding is not None:
self.album_binding.unbind() 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) @GObject.Property(type=bool, default=False)
def active(self) -> bool: def active(self) -> bool:
@ -228,6 +240,45 @@ class AlbumString(InscriptionRow):
self.bind_to_self("mediumid", "mediumid") 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): class FavoriteButton(TrackRow):
"""A TrackRow with an toggle to set Track favorite status.""" """A TrackRow with an toggle to set Track favorite status."""

View File

@ -28,6 +28,7 @@ class TrackView(Gtk.Frame):
model=self._selection) model=self._selection)
self._scrollwin = Gtk.ScrolledWindow(child=self._columnview) self._scrollwin = Gtk.ScrolledWindow(child=self._columnview)
self.__append_column("Art", "cover", row.AlbumCover, resizable=False)
self.__append_column("Fav", "favorite", row.FavoriteButton, self.__append_column("Fav", "favorite", row.FavoriteButton,
resizable=False) resizable=False)
self.__append_column("Track", "number", row.TracknoString, 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): def test_save_tracklist_column_width(self, mock_stdout: io.StringIO):
"""Test saving tracklist column widths.""" """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.fav.size"], -1)
self.assertEqual(self.settings["tracklist.track.size"], 55) self.assertEqual(self.settings["tracklist.track.size"], 55)
self.assertEqual(self.settings["tracklist.title.size"], 300) self.assertEqual(self.settings["tracklist.title.size"], 300)
@ -136,6 +137,7 @@ class TestSettings(unittest.TestCase):
for column in self.win.tracklist.columns: for column in self.win.tracklist.columns:
column.set_fixed_width(123) 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.fav.size"], 123)
self.assertEqual(self.settings["tracklist.track.size"], 123) self.assertEqual(self.settings["tracklist.track.size"], 123)
self.assertEqual(self.settings["tracklist.title.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): def test_save_tracklist_column_visible(self, mock_stdout: io.StringIO):
"""Test saving tracklist column visibility.""" """Test saving tracklist column visibility."""
self.assertTrue(self.settings["tracklist.art.visible"])
self.assertTrue(self.settings["tracklist.fav.visible"]) self.assertTrue(self.settings["tracklist.fav.visible"])
self.assertTrue(self.settings["tracklist.track.visible"]) self.assertTrue(self.settings["tracklist.track.visible"])
self.assertTrue(self.settings["tracklist.title.visible"]) self.assertTrue(self.settings["tracklist.title.visible"])
@ -165,6 +168,7 @@ class TestSettings(unittest.TestCase):
for column in self.win.tracklist.columns: for column in self.win.tracklist.columns:
column.set_visible(not column.get_visible()) 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.fav.visible"])
self.assertFalse(self.settings["tracklist.track.visible"]) self.assertFalse(self.settings["tracklist.track.visible"])
self.assertFalse(self.settings["tracklist.title.visible"]) self.assertFalse(self.settings["tracklist.title.visible"])

View File

@ -8,6 +8,7 @@ import emmental.tracklist.row
import tests.util import tests.util
import unittest.mock import unittest.mock
from gi.repository import GObject from gi.repository import GObject
from gi.repository import Gdk
from gi.repository import Gtk from gi.repository import Gtk
from gi.repository import Adw from gi.repository import Adw
@ -20,7 +21,8 @@ class TestTrackRowWidgets(tests.util.TestCase):
super().setUp() super().setUp()
self.sql.playlists.load(now=True) self.sql.playlists.load(now=True)
self.library = self.sql.libraries.create(pathlib.Path("/a/b")) 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.medium = self.sql.media.create(self.album, "", number=1)
self.year = self.sql.years.create(2023) self.year = self.sql.years.create(2023)
self.track = self.sql.tracks.create(self.library, self.track = self.sql.tracks.create(self.library,
@ -168,6 +170,38 @@ class TestTrackRowWidgets(tests.util.TestCase):
row.unbind() row.unbind()
self.assertIsNone(row.album_binding) 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): def test_favorite_button(self):
"""Test the Favorite Button widget.""" """Test the Favorite Button widget."""
row = emmental.tracklist.row.FavoriteButton(self.listitem, "favorite") 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.columns = self.trackview.columns
self.listitem = Gtk.ListItem() 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.""" """Test the favorite column."""
self.assertEqual(self.columns[i].get_title(), "Fav") self.assertEqual(self.columns[i].get_title(), "Fav")
self.assertEqual(self.columns[i].get_fixed_width(), -1) self.assertEqual(self.columns[i].get_fixed_width(), -1)
@ -108,7 +122,7 @@ class TestTrackViewColumns(tests.util.TestCase):
factory.emit("setup", self.listitem) factory.emit("setup", self.listitem)
self.assertEqual(self.listitem.listrow.property, "favorite") 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.""" """Test the track number column."""
self.assertEqual(self.columns[i].get_title(), "Track") self.assertEqual(self.columns[i].get_title(), "Track")
self.assertEqual(self.columns[i].get_fixed_width(), 55) 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.assertEqual(self.listitem.listrow.child.get_xalign(), 1.0)
self.assertTrue(self.listitem.listrow.child.has_css_class("numeric")) 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.""" """Test the title column."""
self.assertEqual(self.columns[i].get_title(), "Title") self.assertEqual(self.columns[i].get_title(), "Title")
self.assertEqual(self.columns[i].get_fixed_width(), 300) 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.assertEqual(self.listitem.listrow.child.get_xalign(), 0.0)
self.assertFalse(self.listitem.listrow.child.has_css_class("numeric")) 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.""" """Test the length column."""
self.assertEqual(self.columns[i].get_title(), "Length") self.assertEqual(self.columns[i].get_title(), "Length")
self.assertEqual(self.columns[i].get_fixed_width(), -1) 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.assertEqual(self.listitem.listrow.child.get_xalign(), 1.0)
self.assertTrue(self.listitem.listrow.child.has_css_class("numeric")) 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.""" """Test the artist column."""
self.assertEqual(self.columns[i].get_title(), "Artist") self.assertEqual(self.columns[i].get_title(), "Artist")
self.assertEqual(self.columns[i].get_fixed_width(), 250) 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.assertEqual(self.listitem.listrow.child.get_xalign(), 0.0)
self.assertFalse(self.listitem.listrow.child.has_css_class("numeric")) 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.""" """Test the album artist column."""
self.assertEqual(self.columns[i].get_title(), "Album Artist") self.assertEqual(self.columns[i].get_title(), "Album Artist")
self.assertEqual(self.columns[i].get_fixed_width(), 250) 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.assertEqual(self.listitem.listrow.child.get_xalign(), 0.0)
self.assertFalse(self.listitem.listrow.child.has_css_class("numeric")) 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.""" """Test the release date column."""
self.assertEqual(self.columns[i].get_title(), "Release") self.assertEqual(self.columns[i].get_title(), "Release")
self.assertEqual(self.columns[i].get_fixed_width(), 115) 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.assertEqual(self.listitem.listrow.child.get_xalign(), 0.0)
self.assertTrue(self.listitem.listrow.child.has_css_class("numeric")) 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.""" """Test the play count column."""
self.assertEqual(self.columns[i].get_title(), "Play Count") self.assertEqual(self.columns[i].get_title(), "Play Count")
self.assertEqual(self.columns[i].get_fixed_width(), 135) 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.assertEqual(self.listitem.listrow.child.get_xalign(), 0.0)
self.assertTrue(self.listitem.listrow.child.has_css_class("numeric")) 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.""" """Test the last started column."""
self.assertEqual(self.columns[i].get_title(), "Last Started") self.assertEqual(self.columns[i].get_title(), "Last Started")
self.assertEqual(self.columns[i].get_fixed_width(), 250) 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.assertEqual(self.listitem.listrow.child.get_xalign(), 0.0)
self.assertTrue(self.listitem.listrow.child.has_css_class("numeric")) 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.""" """Test the last played column."""
self.assertEqual(self.columns[i].get_title(), "Last Played") self.assertEqual(self.columns[i].get_title(), "Last Played")
self.assertEqual(self.columns[i].get_fixed_width(), 250) 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.assertEqual(self.listitem.listrow.child.get_xalign(), 0.0)
self.assertTrue(self.listitem.listrow.child.has_css_class("numeric")) 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.""" """Test the last played column."""
self.assertEqual(self.columns[i].get_title(), "Filepath") self.assertEqual(self.columns[i].get_title(), "Filepath")
self.assertEqual(self.columns[i].get_fixed_width(), -1) self.assertEqual(self.columns[i].get_fixed_width(), -1)