273 lines
11 KiB
Python
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"))]
|