tracklist: Create TrackRow, InscriptionRow, and TrackString widgets

The TrackRow widget is used to bind Tracks to a generic Widget. The
InscriptionRow builds on this to create a Gtk.Inscription that can be
used in derived classes. Finally, the TrackString widget implements
binding a string Track property directly to the Inscription.

I use these widgets to create a Title and Artist column in the
TrackView.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2023-01-11 16:10:49 -05:00
parent 999a3eb523
commit 9edfc4a5b0
9 changed files with 251 additions and 0 deletions

View File

@ -160,6 +160,12 @@ class Application(Adw.Application):
def build_tracklist(self) -> tracklist.Card:
"""Build a new tracklist card."""
track_list = tracklist.Card(sql=self.db)
for column in track_list.columns:
name = column.get_title().lower().replace(" ", "-")
self.db.settings.bind_setting(f"tracklist.{name}.size",
column, "fixed-width")
self.db.settings.bind_setting(f"tracklist.{name}.visible",
column, "visible")
self.factory.bind_property("visible-playlist", track_list, "playlist")
return track_list

View File

@ -69,3 +69,12 @@ button.emmental-delete>image {
button.emmental-stop>image {
color: @red_3;
}
columnview.emmental-track-list > listview > row > cell {
padding: 0px 2px;
min-height: 40px;
}
columnview.emmental-track-list > listview > row > cell > label {
padding: 0px 4px;
}

View File

@ -1,6 +1,7 @@
# Copyright 2022 (c) Anna Schumaker.
"""A card for displaying a list of tracks."""
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
from .. import db
from .. import entry
@ -37,3 +38,8 @@ class Card(Gtk.Box):
def __search_changed(self, filter: entry.Filter) -> None:
self.sql.tracks.filter(filter.get_query())
@GObject.Property(type=Gio.ListModel)
def columns(self) -> Gio.ListModel:
"""Get the columns displayed in the Tracklist."""
return self._trackview.columns

68
emmental/tracklist/row.py Normal file
View File

@ -0,0 +1,68 @@
# Copyright 2023 (c) Anna Schumaker.
"""Widgets for displaying Track information in the TrackView."""
from gi.repository import GObject
from gi.repository import Gtk
from .. import factory
class TrackRow(factory.ListRow):
"""Base class for Track Row widgets."""
property = GObject.Property(type=str)
def __init__(self, listitem: Gtk.ListItem, property: str):
"""Initialize a TrackRow."""
super().__init__(listitem, property=property)
def do_bind(self) -> None:
"""Bind a Track to this Row."""
super().do_bind()
library = self.item.get_library()
self.bind_and_set(library, "online", self, "online")
self.bind_active("active")
@GObject.Property(type=bool, default=False)
def active(self) -> bool:
"""Get the active state of this Row."""
if parent := self.listitem.get_child().get_parent():
if parent := parent.get_parent():
return parent.get_state_flags() & Gtk.StateFlags.CHECKED
return False
@active.setter
def active(self, newval: bool) -> None:
if parent := self.listitem.get_child().get_parent():
if parent := parent.get_parent():
if newval:
parent.set_state_flags(Gtk.StateFlags.CHECKED, False)
else:
parent.unset_state_flags(Gtk.StateFlags.CHECKED)
@GObject.Property(type=bool, default=True)
def online(self) -> bool:
"""Get the online state of this Row."""
return self.listitem.get_activatable()
@online.setter
def online(self, newval: bool) -> None:
self.listitem.set_activatable(newval)
self.child.set_sensitive(newval)
class InscriptionRow(TrackRow):
"""Base class for Track Rows displaying a Gtk.Inscription."""
def __init__(self, listitem: Gtk.ListItem, property: str):
"""Initialize a LabelRow."""
super().__init__(listitem, property)
self.child = Gtk.Inscription(xalign=0.0)
self.child.bind_property("text", self.child, "tooltip-text")
class TrackString(InscriptionRow):
"""An InscriptionRow displaying a string Track property."""
def do_bind(self) -> None:
"""Bind a track string to the Track Inscription."""
super().do_bind()
self.bind_and_set_property(self.property, "text")

View File

@ -1,9 +1,12 @@
# Copyright 2022 (c) Anna Schumaker.
"""A Gtk.ColumnView for displaying Tracks."""
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
from .. import db
from .. import factory
from .. import playlist
from . import row
class TrackView(Gtk.Frame):
@ -25,6 +28,9 @@ class TrackView(Gtk.Frame):
model=self._selection)
self._scrollwin = Gtk.ScrolledWindow(child=self._columnview)
self.__append_column("Title", "title", row.TrackString, width=300)
self.__append_column("Artist", "artist", row.TrackString, width=250)
self.bind_property("playlist", self._filtermodel, "model")
self._selection.bind_property("n-items", self, "n-tracks")
@ -33,6 +39,18 @@ class TrackView(Gtk.Frame):
self.set_child(self._scrollwin)
def __append_column(self, title: str, property: str, row_type: type,
*, width: int = -1, visible: bool = True) -> None:
fctry = factory.Factory(row_type=row_type, property=property)
col = Gtk.ColumnViewColumn(title=title, factory=fctry, visible=visible,
resizable=True, fixed_width=width)
self._columnview.append_column(col)
def __runtime_changed(self, selection: Gtk.MultiSelection,
position: int, removed: int, added: int) -> None:
self.runtime = sum(t.length for t in self._selection)
@GObject.Property(type=Gio.ListModel)
def columns(self) -> Gio.ListModel:
"""Get the ListModel for the columns."""
return self._columnview.get_columns()

View File

@ -118,3 +118,25 @@ class TestSettings(unittest.TestCase):
self.assertTrue(self.settings["sidebar.artists.show-all"])
self.assertTrue(self.app.build_window().sidebar.show_all_artists)
def test_save_tracklist_column_width(self, mock_stdout: io.StringIO):
"""Test saving tracklist column widths."""
self.assertEqual(self.settings["tracklist.title.size"], 300)
self.assertEqual(self.settings["tracklist.artist.size"], 250)
for column in self.win.tracklist.columns:
column.set_fixed_width(123)
self.assertEqual(self.settings["tracklist.title.size"], 123)
self.assertEqual(self.settings["tracklist.artist.size"], 123)
def test_save_tracklist_column_visible(self, mock_stdout: io.StringIO):
"""Test saving tracklist column visibility."""
self.assertTrue(self.settings["tracklist.title.visible"])
self.assertTrue(self.settings["tracklist.artist.visible"])
for column in self.win.tracklist.columns:
column.set_visible(not column.get_visible())
self.assertFalse(self.settings["tracklist.title.visible"])
self.assertFalse(self.settings["tracklist.artist.visible"])

View File

@ -0,0 +1,81 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests our TrackView Row widgets."""
import pathlib
import emmental.factory
import emmental.tracklist.row
import tests.util
import unittest.mock
from gi.repository import Gtk
from gi.repository import Adw
class TestTrackRowWidgets(tests.util.TestCase):
"""Tests our Track Row widgets."""
def setUp(self):
"""Set up common variables."""
super().setUp()
self.sql.playlists.load()
self.library = self.sql.libraries.create(pathlib.Path("/a/b"))
self.album = self.sql.albums.create("Test Album", "Artist", "2023")
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,
pathlib.Path("/a/b/1.ogg"),
self.medium, self.year,
title="Test Title")
self.listitem = Gtk.ListItem()
self.listitem.get_item = unittest.mock.Mock(return_value=self.track)
self.columnrow = Adw.Bin()
self.listrow = Adw.Bin(child=self.columnrow)
def test_track_row(self):
"""Test the base Track Row."""
row = emmental.tracklist.row.TrackRow(self.listitem, "property")
self.assertIsInstance(row, emmental.factory.ListRow)
self.assertEqual(row.property, "property")
row.child = Gtk.Label()
self.columnrow.set_child(row.child)
self.library.online = False
self.track.active = True
row.bind()
self.assertFalse(row.online)
self.assertFalse(self.listitem.get_activatable())
self.assertFalse(row.child.get_sensitive())
self.assertTrue(row.active)
self.assertTrue(self.listrow.get_state_flags() &
Gtk.StateFlags.CHECKED)
self.library.online = True
self.track.active = False
self.assertTrue(row.online)
self.assertTrue(self.listitem.get_activatable())
self.assertTrue(row.child.get_sensitive())
self.assertFalse(row.active)
self.assertFalse(self.listrow.get_state_flags() &
Gtk.StateFlags.CHECKED)
def test_inscription_row(self):
"""Test the base Inscription Row."""
row = emmental.tracklist.row.InscriptionRow(self.listitem, "property")
self.assertIsInstance(row, emmental.tracklist.row.TrackRow)
self.assertIsInstance(row.child, Gtk.Inscription)
self.assertEqual(row.child.get_xalign(), 0.0)
row.child.set_text("Test Text")
self.assertEqual(row.child.get_tooltip_text(), "Test Text")
def test_track_string(self):
"""Test the Track String widget."""
row = emmental.tracklist.row.TrackString(self.listitem, "title")
self.assertIsInstance(row, emmental.tracklist.row.InscriptionRow)
self.assertEqual(row.property, "title")
row.bind()
self.assertEqual(row.child.get_text(), "Test Title")
self.track.title = "New Title"
self.assertEqual(row.child.get_text(), "New Title")

View File

@ -65,6 +65,8 @@ class TestTracklist(tests.util.TestCase):
self.assertEqual(self.tracklist._trackview.get_margin_start(), 6)
self.assertEqual(self.tracklist._trackview.get_margin_end(), 6)
self.assertEqual(self.tracklist.columns,
self.tracklist._trackview.columns)
def test_playlist(self):
"""Test the playlist property."""

View File

@ -51,6 +51,8 @@ class TestTrackView(tests.util.TestCase):
self.assertIsInstance(self.trackview._columnview, Gtk.ColumnView)
self.assertEqual(self.trackview._columnview.get_model(),
self.trackview._selection)
self.assertEqual(self.trackview._columnview.get_columns(),
self.trackview.columns)
self.assertTrue(self.trackview._columnview.get_hexpand())
self.assertTrue(self.trackview._columnview.get_vexpand())
@ -79,3 +81,40 @@ class TestTrackView(tests.util.TestCase):
self.trackview._selection.set_model(self.playlist)
self.db_plist.add_track(self.track)
self.assertEqual(self.trackview.runtime, 10.0)
class TestTrackViewColumns(tests.util.TestCase):
"""Test the TrackView Columns."""
def setUp(self):
"""Set up common variables."""
super().setUp()
self.trackview = emmental.tracklist.trackview.TrackView(self.sql)
self.columns = self.trackview.columns
self.listitem = Gtk.ListItem()
def test_title_column(self, i: int = 0):
"""Test the title column."""
self.assertEqual(self.columns[i].get_title(), "Title")
self.assertEqual(self.columns[i].get_fixed_width(), 300)
self.assertTrue(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.TrackString)
factory.emit("setup", self.listitem)
self.assertEqual(self.listitem.listrow.property, "title")
def test_artist_column(self, i: int = 1):
"""Test the artist column."""
self.assertEqual(self.columns[i].get_title(), "Artist")
self.assertEqual(self.columns[i].get_fixed_width(), 250)
self.assertTrue(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.TrackString)
factory.emit("setup", self.listitem)
self.assertEqual(self.listitem.listrow.property, "artist")