# 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)