324 lines
12 KiB
Python
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)
|