tracklist: Create LengthString, PlayCountString, TimestampString, and PathString TrackRows

These are specially configured TrackRows that take a non-string Track
property and convert it into a string displayed in the Inscription. I
use them to add Length, Play Count, Last Started, Last Played, and
Filepath columns to the TrackView.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2023-01-12 16:37:32 -05:00
parent 9edfc4a5b0
commit 915a59a46b
5 changed files with 282 additions and 5 deletions

View File

@ -1,5 +1,8 @@
# Copyright 2023 (c) Anna Schumaker.
"""Widgets for displaying Track information in the TrackView."""
import datetime
import dateutil.tz
import pathlib
from gi.repository import GObject
from gi.repository import Gtk
from .. import factory
@ -52,11 +55,14 @@ class TrackRow(factory.ListRow):
class InscriptionRow(TrackRow):
"""Base class for Track Rows displaying a Gtk.Inscription."""
def __init__(self, listitem: Gtk.ListItem, property: str):
def __init__(self, listitem: Gtk.ListItem, property: str,
xalign: float = 0.0, numeric: bool = False):
"""Initialize a LabelRow."""
super().__init__(listitem, property)
self.child = Gtk.Inscription(xalign=0.0)
self.child = Gtk.Inscription(xalign=xalign)
self.child.bind_property("text", self.child, "tooltip-text")
if numeric:
self.child.add_css_class("numeric")
class TrackString(InscriptionRow):
@ -66,3 +72,88 @@ class TrackString(InscriptionRow):
"""Bind a track string to the Track Inscription."""
super().do_bind()
self.bind_and_set_property(self.property, "text")
class LengthString(InscriptionRow):
"""An InscriptionRow displaying a length Track property."""
def do_bind(self) -> None:
"""Bind the track length to the length property."""
super().do_bind()
self.bind_and_set(self.item, self.property, self, "length")
@GObject.Property(type=float)
def length(self) -> float:
"""Get the current length."""
return getattr(self, "__length", 0.0)
@length.setter
def length(self, newval: float) -> None:
self.__length = newval
self.child.set_text("{}:{:02d}".format(*divmod(round(newval), 60)))
class PlayCountString(InscriptionRow):
"""An InscriptionRow displaying a playcount Track property."""
def do_bind(self) -> None:
"""Bind the track playcount to the playcount property."""
super().do_bind()
self.bind_and_set(self.item, self.property, self, "playcount")
@GObject.Property(type=int)
def playcount(self) -> int:
"""Get the current playcount."""
return getattr(self, "__playcount", 0)
@playcount.setter
def playcount(self, newval: int) -> None:
self.__playcount = newval
match newval:
case 0: self.child.set_text("Unplayed")
case 1: self.child.set_text("Played 1 time")
case _: self.child.set_text(f"Played {newval} times")
class TimestampString(InscriptionRow):
"""An InscriptionRow displaying a datetime Track property."""
TZ_UTC = dateutil.tz.tzutc()
def do_bind(self) -> None:
"""Bind the track datetime property to the timestamp property."""
super().do_bind()
self.bind_and_set(self.item, self.property, self, "timestamp")
@GObject.Property(type=GObject.TYPE_PYOBJECT)
def timestamp(self) -> datetime.datetime:
"""Get the current timestamp."""
return getattr(self, "__timestamp", None)
@timestamp.setter
def timestamp(self, newval: datetime.datetime) -> None:
self.__timestamp = newval
if newval is None:
self.child.set_text("Never")
else:
local = newval.replace(tzinfo=TimestampString.TZ_UTC).astimezone()
self.child.set_text(local.replace(tzinfo=None).strftime("%c"))
class PathString(InscriptionRow):
"""An InscriptionRow displaying a pathlib.Path Track property."""
def do_bind(self) -> None:
"""Bind the track path property to the path property."""
super().do_bind()
self.bind_and_set(self.item, self.property, self, "path")
@GObject.Property(type=GObject.TYPE_PYOBJECT)
def path(self) -> pathlib.Path:
"""Get the current path."""
return getattr(self, "__path", None)
@path.setter
def path(self, newval: pathlib.Path) -> None:
self.__path = newval
self.child.set_text(str(newval))

View File

@ -29,7 +29,17 @@ class TrackView(Gtk.Frame):
self._scrollwin = Gtk.ScrolledWindow(child=self._columnview)
self.__append_column("Title", "title", row.TrackString, width=300)
self.__append_column("Length", "length", row.LengthString,
xalign=1.0, numeric=True)
self.__append_column("Artist", "artist", row.TrackString, width=250)
self.__append_column("Play Count", "playcount", row.PlayCountString,
width=135, numeric=True)
self.__append_column("Last Started", "laststarted",
row.TimestampString, width=250,
visible=False, numeric=True)
self.__append_column("Last Played", "lastplayed",
row.TimestampString, width=250, numeric=True)
self.__append_column("Filepath", "path", row.PathString, visible=False)
self.bind_property("playlist", self._filtermodel, "model")
self._selection.bind_property("n-items", self, "n-tracks")
@ -40,8 +50,9 @@ 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)
*, width: int = -1, visible: bool = True,
**kwargs) -> None:
fctry = factory.Factory(row_type=row_type, property=property, **kwargs)
col = Gtk.ColumnViewColumn(title=title, factory=fctry, visible=visible,
resizable=True, fixed_width=width)
self._columnview.append_column(col)

View File

@ -122,21 +122,41 @@ 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.title.size"], 300)
self.assertEqual(self.settings["tracklist.length.size"], -1)
self.assertEqual(self.settings["tracklist.artist.size"], 250)
self.assertEqual(self.settings["tracklist.play-count.size"], 135)
self.assertEqual(self.settings["tracklist.last-started.size"], 250)
self.assertEqual(self.settings["tracklist.last-played.size"], 250)
self.assertEqual(self.settings["tracklist.filepath.size"], -1)
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.length.size"], 123)
self.assertEqual(self.settings["tracklist.artist.size"], 123)
self.assertEqual(self.settings["tracklist.play-count.size"], 123)
self.assertEqual(self.settings["tracklist.last-started.size"], 123)
self.assertEqual(self.settings["tracklist.last-played.size"], 123)
self.assertEqual(self.settings["tracklist.filepath.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.length.visible"])
self.assertTrue(self.settings["tracklist.artist.visible"])
self.assertTrue(self.settings["tracklist.play-count.visible"])
self.assertFalse(self.settings["tracklist.last-started.visible"])
self.assertTrue(self.settings["tracklist.last-played.visible"])
self.assertFalse(self.settings["tracklist.filepath.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.length.visible"])
self.assertFalse(self.settings["tracklist.artist.visible"])
self.assertFalse(self.settings["tracklist.play-count.visible"])
self.assertTrue(self.settings["tracklist.last-started.visible"])
self.assertFalse(self.settings["tracklist.last-played.visible"])
self.assertTrue(self.settings["tracklist.filepath.visible"])

View File

@ -1,5 +1,7 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests our TrackView Row widgets."""
import datetime
import dateutil.tz
import pathlib
import emmental.factory
import emmental.tracklist.row
@ -65,10 +67,16 @@ class TestTrackRowWidgets(tests.util.TestCase):
self.assertIsInstance(row, emmental.tracklist.row.TrackRow)
self.assertIsInstance(row.child, Gtk.Inscription)
self.assertEqual(row.child.get_xalign(), 0.0)
self.assertFalse(row.child.has_css_class("numeric"))
row.child.set_text("Test Text")
self.assertEqual(row.child.get_tooltip_text(), "Test Text")
row = emmental.tracklist.row.InscriptionRow(self.listitem, "property",
xalign=1.0, numeric=True)
self.assertEqual(row.child.get_xalign(), 1.0)
self.assertTrue(row.child.has_css_class("numeric"))
def test_track_string(self):
"""Test the Track String widget."""
row = emmental.tracklist.row.TrackString(self.listitem, "title")
@ -79,3 +87,61 @@ class TestTrackRowWidgets(tests.util.TestCase):
self.assertEqual(row.child.get_text(), "Test Title")
self.track.title = "New Title"
self.assertEqual(row.child.get_text(), "New Title")
def test_length_string(self):
"""Test the LengthString widget."""
row = emmental.tracklist.row.LengthString(self.listitem, "length")
self.assertIsInstance(row, emmental.tracklist.row.InscriptionRow)
self.assertEqual(row.property, "length")
self.assertEqual(row.length, 0.0)
row.bind()
self.assertEqual(row.child.get_text(), "0:00")
self.track.length = 123.45
self.assertEqual(row.child.get_text(), "2:03")
self.track.length = 123.84
self.assertEqual(row.child.get_text(), "2:04")
def test_playcount_string(self):
"""Test the PlayCountString widget."""
row = emmental.tracklist.row.PlayCountString(self.listitem,
"playcount")
self.assertIsInstance(row, emmental.tracklist.row.InscriptionRow)
self.assertEqual(row.property, "playcount")
self.assertEqual(row.playcount, 0)
row.bind()
self.assertEqual(row.child.get_text(), "Unplayed")
self.track.playcount = 1
self.assertEqual(row.child.get_text(), "Played 1 time")
self.track.playcount = 2
self.assertEqual(row.child.get_text(), "Played 2 times")
self.track.playcount = 3
self.assertEqual(row.child.get_text(), "Played 3 times")
def test_timestamp_string(self):
"""Test the TimestampString widget."""
row = emmental.tracklist.row.TimestampString(self.listitem,
"laststarted")
self.assertIsInstance(row, emmental.tracklist.row.InscriptionRow)
self.assertEqual(row.TZ_UTC, dateutil.tz.tzutc())
self.assertEqual(row.property, "laststarted")
self.assertEqual(row.timestamp, None)
row.bind()
self.assertEqual(row.child.get_text(), "Never")
local = datetime.datetime.now()
utc = local.astimezone(dateutil.tz.tzutc())
self.track.laststarted = utc.replace(tzinfo=None)
self.assertEqual(row.child.get_text(), local.strftime("%c"))
def test_path_string(self):
"""Test the PathString widget."""
row = emmental.tracklist.row.PathString(self.listitem, "path")
self.assertIsInstance(row, emmental.tracklist.row.InscriptionRow)
self.assertEqual(row.property, "path")
self.assertEqual(row.path, None)
row.bind()
self.assertEqual(row.child.get_text(), str(pathlib.Path("/a/b/1.ogg")))

View File

@ -103,10 +103,29 @@ class TestTrackViewColumns(tests.util.TestCase):
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")
self.assertEqual(self.listitem.listrow.child.get_xalign(), 0.0)
self.assertFalse(self.listitem.listrow.child.has_css_class("numeric"))
def test_artist_column(self, i: int = 1):
def test_length_column(self, i: int = 1):
"""Test the length column."""
self.assertEqual(self.columns[i].get_title(), "Length")
self.assertEqual(self.columns[i].get_fixed_width(), -1)
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.LengthString)
factory.emit("setup", self.listitem)
self.assertEqual(self.listitem.listrow.property, "length")
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 = 2):
"""Test the artist column."""
self.assertEqual(self.columns[i].get_title(), "Artist")
self.assertEqual(self.columns[i].get_fixed_width(), 250)
@ -116,5 +135,75 @@ class TestTrackViewColumns(tests.util.TestCase):
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")
self.assertEqual(self.listitem.listrow.child.get_xalign(), 0.0)
self.assertFalse(self.listitem.listrow.child.has_css_class("numeric"))
def test_playcount_column(self, i: int = 3):
"""Test the play count column."""
self.assertEqual(self.columns[i].get_title(), "Play Count")
self.assertEqual(self.columns[i].get_fixed_width(), 135)
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.PlayCountString)
factory.emit("setup", self.listitem)
self.assertEqual(self.listitem.listrow.property, "playcount")
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 = 4):
"""Test the last started column."""
self.assertEqual(self.columns[i].get_title(), "Last Started")
self.assertEqual(self.columns[i].get_fixed_width(), 250)
self.assertTrue(self.columns[i].get_resizable())
self.assertFalse(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.TimestampString)
factory.emit("setup", self.listitem)
self.assertEqual(self.listitem.listrow.property, "laststarted")
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 = 5):
"""Test the last played column."""
self.assertEqual(self.columns[i].get_title(), "Last Played")
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.TimestampString)
factory.emit("setup", self.listitem)
self.assertEqual(self.listitem.listrow.property, "lastplayed")
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 = 6):
"""Test the last played column."""
self.assertEqual(self.columns[i].get_title(), "Filepath")
self.assertEqual(self.columns[i].get_fixed_width(), -1)
self.assertTrue(self.columns[i].get_resizable())
self.assertFalse(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.PathString)
factory.emit("setup", self.listitem)
self.assertEqual(self.listitem.listrow.property, "path")
self.assertEqual(self.listitem.listrow.child.get_xalign(), 0.0)
self.assertFalse(self.listitem.listrow.child.has_css_class("numeric"))