# Copyright 2023 (c) Anna Schumaker. """An OSD to show when tracks are selected.""" from gi.repository import GObject from gi.repository import Gdk from gi.repository import Gtk from gi.repository import Adw from ..action import ActionEntry from ..buttons import PopoverButton from .. import db from .. import playlist class PlaylistRow(Gtk.ListBoxRow): """A ListBoxRow widget for Playlists.""" name = GObject.Property(type=str) image = GObject.Property(type=GObject.TYPE_PYOBJECT) def __init__(self, name: str, image: GObject.TYPE_PYOBJECT): """Initialize a PlaylistRow.""" super().__init__(child=Gtk.Box(margin_start=6, margin_end=6, margin_top=6, margin_bottom=6, spacing=6), name=name) match name: case "Favorite Tracks": icon_name = "heart-filled-symbolic" case "Queued Tracks": icon_name = "music-queue-symbolic" case _: icon_name = "playlist2-symbolic" self._icon = Adw.Avatar(size=32, text=name, icon_name=icon_name) self._label = Gtk.Label.new(name) self.connect("notify::image", self.__image_changed) self.image = image self.props.child.append(self._icon) self.props.child.append(self._label) def __image_changed(self, row: Gtk.ListBoxRow, param: GObject.ParamSpec) -> None: if self.image is not None and self.image.is_file(): texture = Gdk.Texture.new_from_filename(str(self.image)) else: texture = None self._icon.set_custom_image(texture) class UserTracksFilter(Gtk.Filter): """Filters for tracks with user-tracks set to True.""" playlist = GObject.Property(type=db.playlist.Playlist) def __init__(self): """Initialize the UserTracksFilter.""" super().__init__() self.connect("notify::playlist", self.__playlist_changed) def __playlist_changed(self, filter: Gtk.Filter, param) -> None: self.changed(Gtk.FilterChange.DIFFERENT) def do_match(self, playlist: db.playlist.Playlist) -> bool: """Check if a specific playlist has user-tracks set to True.""" return playlist.user_tracks and playlist != self.playlist class PlaylistView(Gtk.ListBox): """A ListView for selecting Playlists.""" playlist = GObject.Property(type=db.playlist.Playlist) def __init__(self, sql: db.Connection): """Initialize the PlaylistView.""" super().__init__(selection_mode=Gtk.SelectionMode.NONE) self._filtered = Gtk.FilterListModel(model=sql.playlists, filter=UserTracksFilter()) self.bind_property("playlist", self._filtered.get_filter(), "playlist") self.bind_model(self._filtered, self.__create_func) self.connect("row-activated", self.__row_activated) self.add_css_class("boxed-list") def __row_activated(self, box: Gtk.ListBox, row: PlaylistRow) -> None: self.emit("playlist-selected", self._filtered[row.get_index()]) def __create_func(self, playlist: db.playlist.Playlist) -> PlaylistRow: row = PlaylistRow(playlist.name, playlist.image) playlist.bind_property("image", row, "image") return row @GObject.Signal(arg_types=(db.playlists.Playlist,)) def playlist_selected(self, playlist: db.playlists.Playlist) -> None: """Signal that the user has selected a Playlist.""" class MoveButtons(Gtk.Box): """Buttons for moving Tracks in the playlist.""" can_move_down = GObject.Property(type=bool, default=False) can_move_up = GObject.Property(type=bool, default=False) def __init__(self, **kwargs) -> None: """Initialize the Move Buttons.""" super().__init__(**kwargs) self._down = Gtk.Button(icon_name="go-down-symbolic", tooltip_text="move selected track down", hexpand=True, sensitive=False) self._up = Gtk.Button(icon_name="go-up-symbolic", tooltip_text="move selected track up", hexpand=True, sensitive=False) self.bind_property("can-move-down", self._down, "sensitive") self.bind_property("can-move-up", self._up, "sensitive") self._down.connect("clicked", self.__clicked, "move-down") self._up.connect("clicked", self.__clicked, "move-up") self._down.add_css_class("opaque") self._down.add_css_class("pill") self._up.add_css_class("opaque") self._up.add_css_class("pill") self.add_css_class("emmental-move-buttons") self.add_css_class("large-icons") self.add_css_class("linked") self.append(self._up) self.append(self._down) def __clicked(self, button: Gtk.Button, signal: str) -> None: self.emit(signal) @GObject.Signal def move_down(self) -> None: """Signal that the move down button was clicked.""" @GObject.Signal def move_up(self) -> None: """Signal that the move up button was clicked.""" class OSD(Gtk.Overlay): """An Overlay with extra controls for the Tracklist.""" sql = GObject.Property(type=db.Connection) playlist = GObject.Property(type=playlist.playlist.Playlist) selection = GObject.Property(type=Gtk.SelectionModel) have_selected = GObject.Property(type=bool, default=False) n_selected = GObject.Property(type=int) def __init__(self, sql: db.Connection, selection: Gtk.SelectionModel, **kwargs): """Initialize an OSD.""" super().__init__(sql=sql, selection=selection, **kwargs) self._add = PopoverButton(child=Adw.ButtonContent(label="Add", icon_name="list-add-symbolic"), tooltip_text="add selected tracks " "to a playlist", halign=Gtk.Align.START, valign=Gtk.Align.END, margin_start=16, margin_bottom=16, direction=Gtk.ArrowType.UP, visible=False, popover_child=PlaylistView(sql)) self._remove = Gtk.Button(child=Adw.ButtonContent(label="Remove", icon_name="list-remove-symbolic"), tooltip_text="remove selected tracks", halign=Gtk.Align.END, valign=Gtk.Align.END, margin_end=16, margin_bottom=16, visible=False) self._move = MoveButtons(halign=Gtk.Align.CENTER, valign=Gtk.Align.END, margin_bottom=16, sensitive=False, visible=False) self._sizegroup = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL) self._add.add_css_class("suggested-action") self._add.add_css_class("pill") self._remove.add_css_class("destructive-action") self._remove.add_css_class("pill") self.selection.connect("selection-changed", self.__selection_changed) self._add.popover_child.connect("playlist-selected", self.__add_tracks) self._remove.connect("clicked", self.__remove_clicked) self._move.connect("move-down", self.__move_track_down) self._move.connect("move-up", self.__move_track_up) self._sizegroup.add_widget(self._add) self._sizegroup.add_widget(self._remove) self._sizegroup.add_widget(self._move) self.add_overlay(self._add) self.add_overlay(self._remove) self.add_overlay(self._move) def __get_selected_tracks(self) -> list: selection = self.selection.get_selection() return [self.selection.get_item(selection.get_nth(n)) for n in range(selection.get_size())] def __add_tracks(self, view: PlaylistView, playlist: db.playlists.Playlist) -> None: for track in self.__get_selected_tracks(): playlist.add_track(track) self.sql.commit() self.clear_selection() def __remove_clicked(self, button: Gtk.Button) -> None: if self.playlist is not None: for track in self.__get_selected_tracks(): self.playlist.remove_track(track) self.sql.commit() self.clear_selection() def __move_track_down(self, move: MoveButtons) -> None: if self.playlist is not None: index = self.selection.get_selection().get_nth(0) self.selection.get_model().set_incremental(False) self.playlist.move_track_down(self.selection[index]) self.sql.commit() self.selection.get_model().set_incremental(True) self.__update_visibility() def __move_track_up(self, move: MoveButtons) -> None: if self.playlist is not None: index = self.selection.get_selection().get_nth(0) self.selection.get_model().set_incremental(False) self.playlist.move_track_up(self.selection[index]) self.sql.commit() self.selection.get_model().set_incremental(True) self.__update_visibility() def __selection_changed(self, selection: Gtk.SelectionModel, position: int, n_items: int) -> None: self.n_selected = selection.get_selection().get_size() self.have_selected = self.n_selected > 0 self.__update_visibility() def __update_visibility(self) -> None: db_plist = None if self.playlist is None else self.playlist.playlist user = False if db_plist is None else db_plist.user_tracks movable = False if db_plist is None else db_plist.tracks_movable self._add.set_visible(db_plist is not None and self.have_selected) self._remove.set_visible(user and self.have_selected) self._move.set_visible(movable and self.have_selected) if self.n_selected == 1: index = self.selection.get_selection().get_nth(0) self._move.set_sensitive(True) self._move.can_move_down = index < len(self.selection) - 1 self._move.can_move_up = index > 0 else: self._move.set_sensitive(False) def clear_selection(self, *args) -> None: """Clear the current selection.""" self.selection.unselect_all() def reset(self) -> None: """Reset the OSD.""" self.selection.unselect_all() self.__selection_changed(self.selection, 0, 0) if self.playlist is not None: self._add.popover_child.playlist = self.playlist.playlist @property def accelerators(self) -> list[ActionEntry]: """Get a list of accelerators for the OSD.""" return [ActionEntry("remove-selected-tracks", self._remove.activate, "Delete", enabled=(self._remove, "visible")), ActionEntry("move-track-up", self._move._up.activate, "Up", enabled=(self._move, "can-move-up")), ActionEntry("move-track-down", self._move._down.activate, "Down", enabled=(self._move, "can-move-down"))]