emmental/emmental/tracklist/__init__.py
Anna Schumaker 717fdf39cd tracklist: Add keyboard shortcuts
The following shortcuts are implemented:

- Escape to unselect any selected tracks
- Delete to remove selected tracks from the current playlist
- <Control>/ to focus the "filter tracks" entry
- <Control>l to cycle the loop state of the current playlist
- <Control>s to toggle the shuffle state of the current playlist
- <Control>Up to move the selected track up one position
- <Control>Down to move the selected track down one position

I also change the volume up and down shortcuts to use the <Shift>
modifier. This matches how other Header shortcuts are triggered, and
frees up the non-shifted versions to use here.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-08-24 09:28:22 -04:00

162 lines
6.6 KiB
Python

# Copyright 2022 (c) Anna Schumaker.
"""A card for displaying a list of tracks."""
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gtk
from ..action import ActionEntry
from ..playlist.playlist import Playlist
from ..playlist.previous import Previous
from .. import db
from .. import entry
from . import buttons
from . import footer
from . import selection
from . import trackview
class Card(Gtk.Box):
"""Our Tracklist."""
sql = GObject.Property(type=db.Connection)
def __init__(self, sql: db.Connection, **kwargs):
"""Set up the Tracklist widget."""
super().__init__(sql=sql, orientation=Gtk.Orientation.VERTICAL,
**kwargs)
self._top_left = Gtk.Box()
self._top_right = Gtk.Box(sensitive=False)
self._top_box = Gtk.CenterBox(margin_start=6, margin_end=6)
self._filter = entry.Filter("tracks", hexpand=True,
margin_start=100, margin_end=100)
self._trackview = trackview.TrackView(sql)
self._osd = selection.OSD(sql, self._trackview.selection_model,
child=self._trackview)
self._visible_cols = buttons.VisibleColumns(self._trackview.columns)
self._unselect = Gtk.Button(icon_name="edit-select-none-symbolic",
tooltip_text="unselect all tracks",
has_frame=False, sensitive=False)
self._loop = buttons.LoopButton()
self._shuffle = buttons.ShuffleButton()
self._sort = buttons.SortButton()
self._footer = footer.Footer(margin_start=6, margin_end=6,
margin_top=6, margin_bottom=6)
self._top_left.append(self._visible_cols)
self._top_left.append(self._unselect)
self._top_right.append(self._loop)
self._top_right.append(self._shuffle)
self._top_right.append(self._sort)
self._top_box.set_start_widget(self._top_left)
self._top_box.set_center_widget(self._filter)
self._top_box.set_end_widget(self._top_right)
self.append(self._top_box)
self.append(Gtk.Separator())
self.append(self._osd)
self.append(Gtk.Separator())
self.append(self._footer)
self._osd.bind_property("have-selected", self._trackview,
"have-selected")
self._osd.bind_property("n-selected", self._trackview, "n-selected")
self._trackview.bind_property("n-tracks", self._footer, "count")
self._trackview.bind_property("n-selected", self._footer, "selected")
self._trackview.bind_property("runtime", self._footer, "runtime")
self._trackview.bind_property("have-selected", self._unselect,
"sensitive")
self._filter.connect("search-changed", self.__search_changed)
self._unselect.connect("clicked", self._osd.clear_selection)
self._loop.connect("notify::state", self.__update_loop_state)
self._shuffle.connect("notify::active", self.__update_shuffle_state)
self._sort.connect("notify::sort-order", self.__update_sort_order)
self._top_box.add_css_class("toolbar")
self.add_css_class("card")
def __playlist_notify(self, playlist: Playlist, param) -> None:
match param.name:
case "loop":
self._loop.state = playlist.loop
case "playlist":
self.__set_button_state()
case "shuffle":
self._shuffle.active = playlist.shuffle
case "sort-order":
self._sort.sort_order = playlist.sort_order
def __set_button_state(self) -> None:
can_disable = self.playlist.playlist != self.sql.playlists.collection
self._loop.can_disable = can_disable
self._loop.state = self.playlist.loop
self._shuffle.active = self.playlist.shuffle
self._sort.set_sort_order(self.playlist.sort_order)
self._osd.reset()
def __update_loop_state(self, loop: buttons.LoopButton, param) -> None:
if self.playlist.loop != loop.state:
self.playlist.loop = loop.state
def __update_shuffle_state(self, shuffle: buttons.ShuffleButton,
param) -> None:
if self.playlist.shuffle != shuffle.active:
self.playlist.shuffle = shuffle.active
def __update_sort_order(self, sort: buttons.SortButton, param) -> None:
if self.playlist.sort_order != sort.sort_order:
self.playlist.sort_order = sort.sort_order
def __search_changed(self, filter: entry.Filter) -> None:
self.sql.tracks.filter(filter.get_query())
def __scroll_idle(self, track: db.tracks.Track) -> bool:
self._trackview.scroll_to_track(track)
return GLib.SOURCE_REMOVE
def scroll_to_track(self, track: db.tracks.Track) -> None:
"""Scroll to the requested Track."""
if self.playlist is not None:
GLib.idle_add(self.__scroll_idle, track)
@GObject.Property(type=Gio.ListModel)
def columns(self) -> Gio.ListModel:
"""Get the columns displayed in the Tracklist."""
return self._trackview.columns
@GObject.Property(type=Playlist)
def playlist(self) -> Playlist:
"""Get the currently visible Playlist."""
return self._trackview.playlist
@playlist.setter
def playlist(self, newval: Playlist) -> None:
if self._trackview.playlist is not None:
self._trackview.playlist.disconnect_by_func(self.__playlist_notify)
self._trackview.playlist = newval
self._osd.playlist = newval
if newval is not None:
self._top_right.set_sensitive(not isinstance(newval, Previous))
self.__set_button_state()
newval.connect("notify", self.__playlist_notify)
@property
def accelerators(self) -> list[ActionEntry]:
"""Get a list of accelerators for the Tracklist."""
return [ActionEntry("focus-search-track", self._filter.grab_focus,
"<Control>slash"),
ActionEntry("clear-selected-tracks", self._unselect.activate,
"Escape", enabled=(self._unselect, "sensitive")),
ActionEntry("cycle-loop", self._loop.activate,
"<Control>l", enabled=(self._top_right,
"sensitive")),
ActionEntry("toggle-shuffle", self._shuffle.activate,
"<Control>s", enabled=(self._top_right,
"sensitive"))] + \
self._osd.accelerators