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>
This commit is contained in:
Anna Schumaker 2023-08-22 16:42:07 -04:00
parent 9cf980d967
commit 717fdf39cd
8 changed files with 109 additions and 6 deletions

View File

@ -195,6 +195,8 @@ class Application(Adw.Application):
self.db.settings.bind_setting(f"tracklist.{name}.visible",
column, "visible")
self.factory.bind_property("visible-playlist", track_list, "playlist")
self.__add_accelerators(track_list.accelerators)
return track_list
def build_window(self) -> window.Window:

View File

@ -124,9 +124,9 @@ class Header(Gtk.HeaderBar):
"""Get a list of accelerators for the Header."""
res = [ActionEntry("open-file", self._open.activate, "<Control>o"),
ActionEntry("decrease-volume", self._volume.decrement,
"<Control>Down"),
"<Shift><Control>Down"),
ActionEntry("increase-volume", self._volume.increment,
"<Control>Up"),
"<Shift><Control>Up"),
ActionEntry("toggle-bg-mode", self._background.activate,
"<Shift><Control>b")]
if __debug__:

View File

@ -4,6 +4,7 @@ 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
@ -143,3 +144,18 @@ class Card(Gtk.Box):
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

View File

@ -4,6 +4,7 @@ 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
@ -257,3 +258,15 @@ class OSD(Gtk.Overlay):
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"))]

View File

@ -185,9 +185,9 @@ class TestHeader(tests.util.TestCase):
"""Check that the accelerators list is set up properly."""
entries = [("open-file", self.header._open.activate, "<Control>o"),
("decrease-volume", self.header._volume.decrement,
"<Control>Down"),
"<Shift><Control>Down"),
("increase-volume", self.header._volume.increment,
"<Control>Up"),
"<Shift><Control>Up"),
("toggle-bg-mode", self.header._background.activate,
"<Shift><Control>b"),
("edit-settings", self.header._settings.activate,

View File

@ -128,8 +128,8 @@ class TestEmmental(unittest.TestCase):
self.application.build_window()
for action, accel in [("app.open-file", "<Control>o"),
("app.decrease-volume", "<Control>Down"),
("app.increase-volume", "<Control>Up"),
("app.decrease-volume", "<Shift><Control>Down"),
("app.increase-volume", "<Shift><Control>Up"),
("app.toggle-bg-mode", "<Shift><Control>b"),
("app.edit-settings", "<Shift><Control>s")]:
self.assertEqual(self.application.get_accels_for_action(action),
@ -227,6 +227,16 @@ class TestEmmental(unittest.TestCase):
self.application.player = emmental.audio.Player()
win = self.application.build_window()
for action, accel in [("app.focus-search-track", "<Control>slash"),
("app.clear-selected-tracks", "Escape"),
("app.cycle-loop", "<Control>l"),
("app.toggle-shuffle", "<Control>s"),
("app.remove-selected-tracks", "Delete"),
("app.move-track-up", "<Control>Up"),
("app.move-track-down", "<Control>Down")]:
self.assertEqual(self.application.get_accels_for_action(action),
[accel])
self.assertEqual(win.tracklist.sql, self.application.db)
playlist = self.application.db.playlists.create("Test Playlist")

View File

@ -456,3 +456,28 @@ class TestOsd(tests.util.TestCase):
self.osd.reset()
mock_unselect.assert_called()
self.assertFalse(self.osd.have_selected)
def test_accelerators(self):
"""Test that the accelerators list is set up properly."""
entries = [("remove-selected-tracks", self.osd._remove.activate,
["Delete"], self.osd._remove, "visible"),
("move-track-up", self.osd._move._up.activate,
["<Control>Up"], self.osd._move, "can-move-up"),
("move-track-down", self.osd._move._down.activate,
["<Control>Down"], self.osd._move, "can-move-down")]
accels = self.osd.accelerators
self.assertIsInstance(accels, list)
for i, (name, func, accel, gobject, prop) in enumerate(entries):
with self.subTest(action=name):
self.assertIsInstance(accels[i], emmental.action.ActionEntry)
self.assertEqual(accels[i].name, name)
self.assertEqual(accels[i].func, func)
self.assertEqual(accels[i].accels, accel)
if gobject and prop:
enabled = gobject.get_property(prop)
self.assertEqual(accels[i].enabled, enabled)
gobject.set_property(prop, not enabled)
self.assertEqual(accels[i].enabled, not enabled)

View File

@ -250,3 +250,40 @@ class TestTracklist(tests.util.TestCase):
self.assertEqual(self.tracklist._Card__scroll_idle(None),
GLib.SOURCE_REMOVE)
mock_scroll.assert_called_with(None)
def test_accelerators(self):
"""Check that the accelerators list is set up properly."""
entries = [("focus-search-track", self.tracklist._filter.grab_focus,
["<Control>slash"], None, None),
("clear-selected-tracks", self.tracklist._unselect.activate,
["Escape"], self.tracklist._unselect, "sensitive"),
("cycle-loop", self.tracklist._loop.activate,
["<Control>l"], self.tracklist._top_right, "sensitive"),
("toggle-shuffle", self.tracklist._shuffle.activate,
["<Control>s"], self.tracklist._top_right, "sensitive")]
accels = self.tracklist.accelerators
self.assertIsInstance(accels, list)
for i, (name, func, accel, gobject, prop) in enumerate(entries):
with self.subTest(action=name):
self.assertIsInstance(accels[i], emmental.action.ActionEntry)
self.assertEqual(accels[i].name, name)
self.assertEqual(accels[i].func, func)
self.assertListEqual(accels[i].accels, accel)
if gobject and prop:
enabled = gobject.get_property(prop)
self.assertEqual(accels[i].enabled, enabled)
gobject.set_property(prop, not enabled)
self.assertEqual(accels[i].enabled, not enabled)
start = len(entries)
osd_accels = self.tracklist._osd.accelerators
for i, accel in enumerate(osd_accels):
with self.subTest(name=accel.name):
self.assertIsInstance(accels[start + i],
emmental.action.ActionEntry)
self.assertEqual(accels[start + i].name, accel.name)
self.assertEqual(accels[start + i].func, accel.func)
self.assertListEqual(accels[start + i].accels, accel.accels)