emmental/emmental/tracklist/selection.py

273 lines
11 KiB
Python

# 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,
"<Control>Up",
enabled=(self._move, "can-move-up")),
ActionEntry("move-track-down", self._move._down.activate,
"<Control>Down",
enabled=(self._move, "can-move-down"))]