emmental/emmental/tracklist/row.py

324 lines
12 KiB
Python

# 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 buttons
from .. import factory
from .. import texture
class TrackRow(factory.ListRow):
"""Base class for Track Row widgets."""
property = GObject.Property(type=str)
mediumid = GObject.Property(type=int)
album_binding = GObject.Property(type=GObject.Binding)
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")
def do_unbind(self) -> None:
"""Extra handling to make sure we unbind the album property."""
super().do_unbind()
if self.album_binding is not None:
self.album_binding.unbind()
self.album_binding = None
def bind_album(self, child_prop: str) -> None:
"""Bind an album property to the TrackRow child."""
album = self.item.get_medium().get_album()
self.child.set_property(child_prop, album.get_property(self.property))
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, to_self: bool = False) -> None:
"""Rebind an album property to the TrackRow child."""
if self.album_binding is not None:
self.album_binding.unbind()
if to_self:
self.bind_album_to_self(child_prop)
else:
self.bind_album(child_prop)
@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)
@GObject.Property(type=Gtk.Widget)
def listrow(self) -> Gtk.Widget:
"""Test property for active track styling."""
if child := self.listitem.props.child:
if cell := child.props.parent:
return cell.props.parent
return None
class InscriptionRow(TrackRow):
"""Base class for Track Rows displaying a Gtk.Inscription."""
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=xalign)
self.child.bind_property("text", self.child, "tooltip-text")
if numeric:
self.child.add_css_class("numeric")
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")
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))
class TracknoString(InscriptionRow):
"""A FormatString that combines medium and track numbers."""
number = GObject.Property(type=int)
def __init__(self, *args, **kwargs):
"""Initialize a TracknoString."""
super().__init__(*args, **kwargs)
self.connect("notify", self.__update_trackno)
def __update_trackno(self, row: InscriptionRow, param) -> None:
"""Update the trackno string when properties change."""
match param.name:
case "mediumid": self.notify("number")
case "number":
mediumno = self.item.get_medium().number
self.child.set_text("{}-{:02d}".format(mediumno, self.number))
def do_bind(self) -> None:
"""Bind track and medium numbers to the Track Inscription."""
super().do_bind()
self.bind_to_self("mediumid", "mediumid")
self.bind_to_self("number", "number")
class AlbumString(InscriptionRow):
"""A Track Row to display Album properties."""
def __init__(self, *args, **kwargs):
"""Initialize a AlbumString."""
super().__init__(*args, **kwargs)
self.connect("notify::mediumid", self.__update_album)
def __update_album(self, row: InscriptionRow, param) -> None:
self.rebind_album("text")
def do_bind(self) -> None:
"""Bind an album string to the AlbumString."""
super().do_bind()
self.bind_to_self("mediumid", "mediumid")
class MediumString(InscriptionRow):
"""A Track Row to display Album and Medium names."""
album = GObject.Property(type=str)
medium = GObject.Property(type=str)
def __init__(self, listitem: Gtk.ListItem, property: str, **kwargs):
"""Initialize a MediumName inscription."""
super().__init__(listitem, property, **kwargs)
self.connect("notify", self.__update_string)
def __unbind_medium_name(self) -> None:
binding = getattr(self, "medium_binding", None)
if binding is not None:
binding.unbind()
setattr(self, "medium_binding", None)
def __rebind_medium_name(self) -> None:
medium = self.item.get_medium()
self.medium = medium.name
setattr(self, "medium_binding",
medium.bind_property("name", self, "medium"))
def __update_string(self, row: InscriptionRow, param) -> None:
match param.name:
case "mediumid":
self.rebind_album("album", to_self=True)
self.__rebind_medium_name()
case "album" | "medium":
medium = f":\n{self.medium}" if len(self.medium) else ""
album = self.item.get_medium().get_album().name
self.child.set_text(f"{album}{medium}")
def do_bind(self) -> None:
"""Bind an album name and medium name to the Inscription."""
super().do_bind()
self.bind_to_self("mediumid", "mediumid")
def do_unbind(self) -> None:
"""Unbind the album and medium name from the Inscription."""
super().do_unbind()
self.__unbind_medium_name()
class AlbumCover(TrackRow):
"""A Track Row to display Album art."""
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":
tex = texture.CACHE[self.filepath]
self.child.set_paintable(tex)
self.child.set_has_tooltip(tex is not None)
def __query_tooltip(self, child: Gtk.Picture, x: int, y: int,
keyboard_mode: bool, tooltip: Gtk.Tooltip) -> bool:
tex = texture.CACHE[self.filepath]
tooltip.set_custom(Gtk.Picture.new_for_paintable(tex))
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."""
def __init__(self, listitem: Gtk.ListItem, property: str) -> None:
"""Initialize a Favorite Button."""
super().__init__(listitem, property=property)
self.child = buttons.ImageToggle("heart-filled", "heart-outline-thick",
large_icon=False, has_frame=False,
valign=Gtk.Align.CENTER)
def do_bind(self):
"""Bind a track property to the Toggle Button."""
super().do_bind()
self.bind_and_set_property(self.property, "active", bidirectional=True)