Compare commits

...

12 Commits

Author SHA1 Message Date
Anna Schumaker ef99951f74 Emmental 3.0.5
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-30 09:26:14 -04:00
Anna Schumaker 0fd391a9fd sidebar: Add keyboard shortcuts
The following shortcuts are implemented:

- <Control>? (<Shift><Control>/) to focus the "filter playlists" entry
- <Control><Alt>g to go to the current playlist
- <Shift><Control>p to open the Playlists section
- <Shift><Control>a to open the Artists section
- <Shift><Control>g to open the Genres section
- <Shift><Control>d to open the Decades section
- <Shift><Control>l to open the Libraries section

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-30 09:24:10 -04:00
Anna Schumaker bc92e72265 sidebar: Give the Header an activate() function
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-30 09:24:10 -04:00
Anna Schumaker 8dae0ed7bd sidebar: Add a button to jump to the current playlist
Implements: #59 ("Jump to Active Playlist button")
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-30 09:23:31 -04:00
Anna Schumaker 1707f87e45 sidebar: Move the Filter into a Gtk.CenterBox
I'm going to add a button to jump to the current playlist, and the first
step is to add an area to put it in the sidebar.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-28 10:09:53 -04:00
Anna Schumaker 7e99fd1ba0 sidebar: Convert the section Group into a View
I change it to inherit from Gtk.Box, and append Sections as they are
added. I also add some stand-alone styling to set it apart as its own
widget.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-28 10:09:53 -04:00
Anna Schumaker a8e7078308 db: Disable incremental filtering
This applies to both the Table and child Playlists models. I'm doing my
own idle handling already for searching, so we can rely on that instead
of needing Gtk to do it. The benefit to this is that we can select
playlists programmatically during startup, since we don't need to worry
about the Table not being fully loaded yet.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-28 10:09:53 -04:00
Anna Schumaker 1d0813f217 db: Give playlist tables a refilter() function
This is used to notify the model that the rows have changed when it's
not automatically being detected. I first noticed this when attempting
to disable incremental filtering, due to the Artist list not getting
refiltered when new child Albums were added to the Artist.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-28 10:09:53 -04:00
Anna Schumaker 725619faf5 db: Improve filtering Tables
I scheduled the filter query with  first=True so it can run without a
long delay during scanning. Additionally, I cancel any pending filter
calls to discard stale arguments that otherwise would be processed.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-28 10:09:53 -04:00
Anna Schumaker 6607e5b0ad db: Give the Idle Queue a way to handle high priority tasks
If we have a high priority task, then we want to push it to the front of
the idle queue so it runs as soon as possible.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-28 10:09:53 -04:00
Anna Schumaker 73019d8eb4 db: Give the Idle Queue a cancel_task() function
It's sometimes desireable to cancel a pending task and re-add it with
new parameters to cut out some unnecessary work.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-28 10:09:53 -04:00
Anna Schumaker 6032e549a5 emmental: Use the gsetup.DEBUG_STR for appending "-debug"
This lets us calculate if we're in debug-mode or not once, and reuse the
result when needed.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-28 10:06:38 -04:00
16 changed files with 272 additions and 74 deletions

View File

@ -21,7 +21,7 @@ from gi.repository import Adw
MAJOR_VERSION = 3
MINOR_VERSION = 0
MICRO_VERSION = 4
MICRO_VERSION = 5
VERSION_NUMBER = f"{MAJOR_VERSION}.{MINOR_VERSION}.{MICRO_VERSION}"
VERSION_STRING = f"Emmental {VERSION_NUMBER}{gsetup.DEBUG_STR}"
@ -182,6 +182,7 @@ class Application(Adw.Application):
side_bar = sidebar.Card(sql=self.db)
self.db.settings.bind_setting("sidebar.artists.show-all", side_bar,
"show-all-artists")
self.__add_accelerators(side_bar.accelerators)
return side_bar
def build_tracklist(self) -> tracklist.Card:

View File

@ -52,6 +52,11 @@ class Queue(GObject.GObject):
self.running = False
self._idle_id = None
def cancel_task(self, func: typing.Callable) -> None:
"""Remove all instances of a specific task from the Idle Queue."""
self._tasks = [t for t in self._tasks if t[0] != func]
self.__update_counters()
def complete(self) -> None:
"""Complete all pending tasks."""
if self.running:
@ -60,12 +65,13 @@ class Queue(GObject.GObject):
self.cancel()
def push(self, func: typing.Callable, *args,
now: bool = False) -> bool | None:
now: bool = False, first: bool = False) -> bool | None:
"""Add a task to the Idle Queue."""
if not self.enabled or now:
return func(*args)
self._tasks.append((func, *args))
pos = 0 if first else len(self._tasks)
self._tasks.insert(pos, (func, *args))
self.total += 1
self.__start()

View File

@ -55,7 +55,6 @@ class Playlist(table.Row):
self.child_set = table.TableSubset(child_table, keys=child_keys)
self.children = Gtk.FilterListModel.new(self.child_set,
child_table.get_filter())
self.children.set_incremental(True)
def do_update(self, column: str) -> bool:
"""Update a Playlist object."""
@ -69,6 +68,8 @@ class Playlist(table.Row):
def add_child(self, child: typing.Self) -> None:
"""Add a child Playlist to this Playlist."""
self.child_set.add_row(child)
if self.child_set.keyset.n_keys == 1:
self.table.refilter(Gtk.FilterChange.LESS_STRICT)
def add_track(self, track: Track, *, idle: bool = False) -> None:
"""Add a Track to this Playlist."""
@ -110,6 +111,8 @@ class Playlist(table.Row):
def remove_child(self, child: typing.Self) -> None:
"""Remove a child Playlist from this Playlist."""
self.child_set.remove_row(child)
if self.child_set.keyset.n_keys == 0:
self.table.refilter(Gtk.FilterChange.MORE_STRICT)
def remove_track(self, track: table.Row, *, idle: bool = False) -> None:
"""Remove a Track from this Playlist."""
@ -154,6 +157,10 @@ class Table(table.Table):
def __create_tree(self, plist: Playlist) -> Gtk.FilterListModel | None:
return plist.children
def __refilter(self, change_how: Gtk.FilterChange) -> bool:
self.get_filter().changed(change_how)
return True
def do_add_track(self, playlist: Playlist, track: Track) -> bool:
"""Add a Track to the Playlist."""
raise NotImplementedError
@ -255,6 +262,11 @@ class Table(table.Table):
playlist.sort_order = "user"
return res
def refilter(self, change_how: Gtk.FilterChange) -> None:
"""Schedule refiltering the Table."""
self.queue.cancel_task(self.__refilter)
self.queue.push(self.__refilter, change_how, first=True)
def remove_system_track(self, playlist: Playlist, track: Track) -> bool:
"""Remove a Track from a system Playlist."""
return self.sql("""DELETE FROM system_tracks

View File

@ -149,7 +149,7 @@ class Table(Gtk.FilterListModel):
filter: KeySet | None = None,
queue: Queue | None = None, **kwargs):
"""Set up our Table object."""
super().__init__(sql=sql, rows=dict(), incremental=True,
super().__init__(sql=sql, rows=dict(),
store=store.SortedList(self.get_sort_key),
filter=(filter if filter else KeySet()),
queue=(queue if queue else Queue()), **kwargs)
@ -227,7 +227,8 @@ class Table(Gtk.FilterListModel):
def filter(self, glob: str | None, *, now: bool = False) -> None:
"""Filter the displayed Rows."""
if glob is not None:
self.queue.push(self._filter_idle, glob, now=now)
self.queue.cancel_task(self._filter_idle)
self.queue.push(self._filter_idle, glob, now=now, first=True)
else:
self.get_filter().keys = None

View File

@ -17,7 +17,7 @@ gi.importlib.import_module("gi.repository.Gtk")
gi.importlib.import_module("gi.repository.Gst").init(sys.argv)
DEBUG_STR = "-debug" if __debug__ else ""
APPLICATION_ID = f"com.nowheycreamery.emmental{'-debug' if __debug__ else ''}"
APPLICATION_ID = f"com.nowheycreamery.emmental{DEBUG_STR}"
CSS_FILE = pathlib.Path(__file__).parent / "emmental.css"
CSS_PRIORITY = gi.repository.Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION

View File

@ -2,10 +2,11 @@
"""Implement the MPRIS2 Specification."""
from gi.repository import GObject
from gi.repository import Gio
from .. import gsetup
from . import application
from . import player
MPRIS2_ID = f"org.mpris.MediaPlayer2.emmental{'-debug' if __debug__ else ''}"
MPRIS2_ID = f"org.mpris.MediaPlayer2.emmental{gsetup.DEBUG_STR}"
class Connection(GObject.GObject):

View File

@ -8,6 +8,7 @@ from . import genre
from . import library
from . import playlist
from . import section
from ..action import ActionEntry
from .. import db
from .. import entry
@ -23,33 +24,41 @@ class Card(Gtk.Box):
"""Set up the Sidebar widget."""
super().__init__(sql=sql, orientation=Gtk.Orientation.VERTICAL,
sensitive=False, **kwargs)
self._filter = entry.Filter("playlists")
self._header = Gtk.CenterBox()
self._filter = entry.Filter("playlists", hexpand=True)
self._jump = Gtk.Button(icon_name="go-jump-symbolic",
tooltip_text="scroll to current playlist")
self._playlists = playlist.Section(self.sql.playlists)
self._artists = artist.Section(self.sql.artists, self.sql.albums)
self._genres = genre.Section(self.sql.genres)
self._decades = decade.Section(self.sql.decades, self.sql.years)
self._libraries = library.Section(self.sql.libraries)
self._group = section.Group(sql)
self._view = section.View(sql)
self.append(self._filter)
self._header.set_center_widget(self._filter)
self._header.set_end_widget(self._jump)
self.append(self._header)
for sect in [self._playlists, self._artists, self._genres,
self._decades, self._libraries]:
self.append(sect)
self._group.add(sect)
self._view.add(sect)
self.append(self._view)
self._group.bind_property("selected-playlist",
self, "selected-playlist")
self._view.bind_property("selected-playlist",
self, "selected-playlist")
self.bind_property("show-all-artists", self._artists, "show-all",
GObject.BindingFlags.BIDIRECTIONAL)
self._filter.connect("search-changed", self.__search_changed)
self._jump.connect("clicked", self.__jump_to_playlist)
self.sql.connect("table-loaded", self.__table_loaded)
self.add_css_class("background")
self.add_css_class("linked")
self._header.add_css_class("toolbar")
self.add_css_class("card")
def __jump_to_playlist(self, jump: Gtk.Button) -> None:
self.select_playlist(self.sql.active_playlist)
def __search_changed(self, entry: entry.Filter) -> None:
self.sql.filter(entry.get_query())
@ -78,3 +87,21 @@ class Card(Gtk.Box):
section.active = True
section.select_playlist(playlist)
@property
def accelerators(self) -> list[ActionEntry]:
"""Get a list of accelerators for the Sidebar."""
return [ActionEntry("focus-search-playlist", self._filter.grab_focus,
"<Control>question", enabled=(self, "sensitive")),
ActionEntry("goto-active-playlist", self._jump.activate,
"<Control><Alt>g", enabled=(self, "sensitive")),
ActionEntry("goto-playlists", self._playlists.activate,
"<Shift><Control>p", enabled=(self, "sensitive")),
ActionEntry("goto-artists", self._artists.activate,
"<Shift><Control>a", enabled=(self, "sensitive")),
ActionEntry("goto-genres", self._genres.activate,
"<Shift><Control>g", enabled=(self, "sensitive")),
ActionEntry("goto-decades", self._decades.activate,
"<Shift><Control>d", enabled=(self, "sensitive")),
ActionEntry("goto-libraries", self._libraries.activate,
"<Shift><Control>l", enabled=(self, "sensitive"))]

View File

@ -55,7 +55,7 @@ class Header(Gtk.Box):
self.bind_property("reveal-widget", self._revealer, "child")
self.bind_property("animation", self._revealer, "transition-type")
self._clicked.connect("released", self.__clicked)
self._clicked.connect("released", self.activate)
self.connect("notify::active", self.__notify_active)
self._box.append(self._icon)
@ -70,12 +70,12 @@ class Header(Gtk.Box):
self.append(self._overlay)
self.append(self._revealer)
def __clicked(self, gesture: Gtk.GestureClick, n_press: int,
x: int, y: int) -> None:
self.active = True
def __notify_active(self, header, param) -> None:
if self.active:
self._arrow.set_state_flags(Gtk.StateFlags.CHECKED, False)
else:
self._arrow.unset_state_flags(Gtk.StateFlags.CHECKED)
def activate(self, *args) -> None:
"""Activate the Header."""
self.active = True

View File

@ -99,8 +99,8 @@ class Section(header.Header):
"""Signal that the selected playlist has changed."""
class Group(GObject.GObject):
"""A group of sections."""
class View(Gtk.Box):
"""A widget for displaying a group of sections."""
sql = GObject.Property(type=db.Connection)
current = GObject.Property(type=Section)
@ -108,8 +108,8 @@ class Group(GObject.GObject):
selected_playlist = GObject.Property(type=db.playlist.Playlist)
def __init__(self, sql: db.Connection):
"""Initialize a Section Group."""
super().__init__(sql=sql)
"""Initialize a Section View."""
super().__init__(sql=sql, orientation=Gtk.Orientation.VERTICAL)
self._sections = []
def __on_active(self, section: Section, param: GObject.ParamSpec) -> None:
@ -145,6 +145,7 @@ class Group(GObject.GObject):
def add(self, section: Section) -> None:
"""Add a section to the group."""
self._sections.append(section)
self.append(section)
section.connect("notify::active", self.__on_active)
section.connect("playlist-activated", self.__playlist_activated)
section.connect("playlist-selected", self.__playlist_selected)

View File

@ -51,6 +51,26 @@ class TestIdleQueue(unittest.TestCase):
self.assertEqual(self.queue.total, 0)
self.assertEqual(self.queue.progress, 0.0)
def test_cancel_task(self, mock_idle_add: unittest.mock.Mock,
mock_source_removed: unittest.mock.Mock):
"""Test canceling a specific task."""
self.queue.push(1)
self.queue.push(2)
self.queue.push(1)
self.queue.cancel_task(1)
self.assertListEqual(self.queue._tasks, [(2,)])
self.assertEqual(self.queue.total, 3)
self.assertAlmostEqual(self.queue.progress, 2 / 3)
mock_source_removed.assert_not_called()
self.queue.cancel_task(2)
self.assertListEqual(self.queue._tasks, [])
self.assertIsNone(self.queue._idle_id)
self.assertEqual(self.queue.total, 0)
self.assertEqual(self.queue.progress, 0.0)
mock_source_removed.assert_called_with(42)
def test_complete(self, mock_idle_add: unittest.mock.Mock,
mock_source_removed: unittest.mock.Mock):
"""Test completing queued tasks."""
@ -119,6 +139,17 @@ class TestIdleQueue(unittest.TestCase):
mock_idle_add.assert_not_called()
func.assert_called_with(1)
def test_push_first(self, mock_idle_add: unittest.mock.Mock,
mock_source_removed: unittest.mock.Mock):
"""Test pushing an idle task with first=True."""
self.queue.push(1)
self.queue.push(0, first=True)
self.assertListEqual(self.queue._tasks, [(0,), (1,)])
self.queue.push(2, first=False)
self.assertListEqual(self.queue._tasks, [(0,), (1,), (2,)])
self.queue.push(3)
self.assertListEqual(self.queue._tasks, [(0,), (1,), (2,), (3,)])
def test_push_many_enabled(self, mock_idle_add: unittest.mock.Mock,
mock_source_removed: unittest.mock.Mock):
"""Test adding several calls to one function at one time."""

View File

@ -21,6 +21,7 @@ class TestPlaylistRow(unittest.TestCase):
self.table.move_track_up = unittest.mock.Mock(return_value=True)
self.table.get_trackids = unittest.mock.Mock(return_value={1, 2, 3})
self.table.get_track_order = unittest.mock.Mock()
self.table.refilter = unittest.mock.Mock()
self.table.queue = emmental.db.idle.Queue()
self.table.update = unittest.mock.Mock(return_value=True)
@ -81,7 +82,7 @@ class TestPlaylistRow(unittest.TestCase):
table.get_filter())
self.assertEqual(self.playlist.children.get_model(),
self.playlist.child_set)
self.assertTrue(self.playlist.children.get_incremental())
self.assertFalse(self.playlist.children.get_incremental())
playlist2 = emmental.db.playlist.Playlist(table=self.table,
propertyid=2, name="Plist2")
@ -114,11 +115,16 @@ class TestPlaylistRow(unittest.TestCase):
def test_add_child(self):
"""Test adding a child playlist to the playlist."""
table = emmental.db.table.Table(None)
child = tests.util.table.MockRow(table=table, number=1)
child1 = tests.util.table.MockRow(table=table, number=1)
child2 = tests.util.table.MockRow(table=table, number=2)
self.playlist.add_children(table, set())
self.playlist.add_child(child)
self.assertIn(child, self.playlist.child_set)
self.playlist.add_child(child1)
self.assertIn(child1, self.playlist.child_set)
self.table.refilter.assert_called_with(Gtk.FilterChange.LESS_STRICT)
self.playlist.add_child(child2)
self.table.refilter.assert_called_once()
def test_add_track(self):
"""Test adding a track to the playlist."""
@ -179,12 +185,19 @@ class TestPlaylistRow(unittest.TestCase):
def test_remove_child(self):
"""Test removing a child playlist from the playlist."""
table = emmental.db.table.Table(None)
child = tests.util.table.MockRow(table=table, number=1)
child1 = tests.util.table.MockRow(table=table, number=1)
child2 = tests.util.table.MockRow(table=table, number=2)
self.playlist.add_children(table, set())
self.playlist.add_child(child1)
self.playlist.add_child(child2)
self.table.refilter.reset_mock()
self.playlist.add_child(child)
self.playlist.remove_child(child)
self.assertFalse(child in self.playlist.child_set)
self.playlist.remove_child(child1)
self.assertFalse(child1 in self.playlist.child_set)
self.table.refilter.assert_not_called()
self.playlist.remove_child(child2)
self.table.refilter.assert_called_with(Gtk.FilterChange.MORE_STRICT)
def test_remove_track(self):
"""Test removing a track from the playlist."""
@ -403,6 +416,27 @@ class TestPlaylistTable(tests.util.TestCase):
self.table.move_track_up(plist, self.track)
self.assertEqual(plist.sort_order, "user")
def test_refilter(self):
"""Test refiltering the playlist table."""
self.table.queue.push(unittest.mock.Mock())
with unittest.mock.patch.object(self.table.get_filter(),
"changed") as mock_changed:
self.table.refilter(Gtk.FilterChange.MORE_STRICT)
self.assertEqual(self.table.queue[0],
(self.table._Table__refilter,
Gtk.FilterChange.MORE_STRICT))
mock_changed.assert_not_called()
self.table.refilter(Gtk.FilterChange.LESS_STRICT)
self.assertEqual(self.table.queue[0],
(self.table._Table__refilter,
Gtk.FilterChange.LESS_STRICT))
mock_changed.assert_not_called()
self.table.queue.complete()
mock_changed.assert_called_with(Gtk.FilterChange.LESS_STRICT)
def test_remove_track(self):
"""Test adding a track to a playlist."""
self.assertTrue(self.table.system_tracks)

View File

@ -233,7 +233,7 @@ class TestTable(tests.util.TestCase):
self.assertEqual(self.table.get_model(), self.table.store)
self.assertEqual(self.table.store.key_func, self.table.get_sort_key)
self.assertDictEqual(self.table.rows, {})
self.assertTrue(self.table.get_incremental())
self.assertFalse(self.table.get_incremental())
filter2 = emmental.db.table.KeySet()
queue2 = emmental.db.idle.Queue()
@ -368,9 +368,13 @@ class TestTableFunctions(tests.util.TestCase):
"""Test filtering Rows in the table."""
for n in [1, 121, 212, 333]:
self.table.create(number=n)
self.table.queue.push(unittest.mock.Mock())
self.table.filter("*3*")
self.assertIsNone(self.table.get_filter().keys)
self.assertEqual(self.table.queue[0], (self.table._filter_idle, "*3*"))
self.table.filter("*2*")
self.assertIsNone(self.table.get_filter().keys)
self.assertEqual(self.table.queue[0], (self.table._filter_idle, "*2*"))
self.table.queue.complete()

View File

@ -165,3 +165,9 @@ class TestHeader(unittest.TestCase):
self.assertTrue(self.header.active)
flags = self.header._arrow.get_state_flags()
self.assertTrue(flags & Gtk.StateFlags.CHECKED)
def test_activate(self):
"""Test the activate() function."""
self.assertFalse(self.header.active)
self.header.activate()
self.assertTrue(self.header.active)

View File

@ -4,7 +4,6 @@ import emmental.db
import emmental.sidebar.section
import tests.util
import unittest.mock
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gtk
@ -150,7 +149,7 @@ class TestGroup(tests.util.TestCase):
def setUp(self):
"""Set up common variables."""
super().setUp()
self.group = emmental.sidebar.section.Group(self.sql)
self.view = emmental.sidebar.section.View(self.sql)
self.row_type = emmental.sidebar.row.TreeRow
self.section1 = emmental.sidebar.section.Section(self.sql.playlists,
self.row_type)
@ -161,35 +160,40 @@ class TestGroup(tests.util.TestCase):
def test_init(self):
"""Test that the Group is set up properly."""
self.assertIsInstance(self.group, GObject.GObject)
self.assertListEqual(self.group._sections, [])
self.assertEqual(self.group.sql, self.sql)
self.assertIsInstance(self.view, Gtk.Box)
self.assertListEqual(self.view._sections, [])
self.assertEqual(self.view.sql, self.sql)
self.assertEqual(self.view.get_orientation(),
Gtk.Orientation.VERTICAL)
def test_add(self):
"""Test adding sections to the Group."""
self.group.add(self.section1)
self.assertListEqual(self.group._sections, [self.section1])
self.group.add(self.section2)
self.assertListEqual(self.group._sections,
self.view.add(self.section1)
self.assertListEqual(self.view._sections, [self.section1])
self.assertEqual(self.view.get_first_child(), self.section1)
self.view.add(self.section2)
self.assertListEqual(self.view._sections,
[self.section1, self.section2])
self.assertEqual(self.section1.get_next_sibling(), self.section2)
def test_current(self):
"""Test the current section property."""
self.group.add(self.section1)
self.group.add(self.section2)
self.assertIsNone(self.group.current)
self.view.add(self.section1)
self.view.add(self.section2)
self.assertIsNone(self.view.current)
self.section1.active = True
self.assertEqual(self.group.current, self.section1)
self.assertEqual(self.view.current, self.section1)
self.section2.active = True
self.assertEqual(self.group.current, self.section2)
self.assertEqual(self.view.current, self.section2)
self.assertFalse(self.section1.active)
def test_animation(self):
"""Test setting the section animation style."""
self.group.add(self.section1)
self.group.add(self.section2)
self.view.add(self.section1)
self.view.add(self.section2)
self.section1.active = True
self.assertEqual(self.section1.animation,
@ -201,8 +205,8 @@ class TestGroup(tests.util.TestCase):
def test_playlist_activated(self):
"""Test responding to the section playlist-activated signal."""
self.group.add(self.section1)
self.group.add(self.section2)
self.view.add(self.section1)
self.view.add(self.section2)
self.assertIsNone(self.sql.active_playlist)
playlist = self.sql.playlists.create("Test Playlist")
@ -215,16 +219,16 @@ class TestGroup(tests.util.TestCase):
def test_selections(self):
"""Test the selected section & playlist properties."""
self.group.add(self.section1)
self.group.add(self.section2)
self.view.add(self.section1)
self.view.add(self.section2)
self.assertIsNone(self.group.selected_section)
self.assertIsNone(self.group.selected_playlist)
self.assertIsNone(self.view.selected_section)
self.assertIsNone(self.view.selected_playlist)
genre = self.sql.genres.create("Test Genre")
self.section2.emit("playlist-selected", genre)
self.assertEqual(self.group.selected_section, self.section2)
self.assertEqual(self.group.selected_playlist, genre)
self.assertEqual(self.view.selected_section, self.section2)
self.assertEqual(self.view.selected_playlist, genre)
self.section2.active = True
treerow = self.section2._selection.get_selected_item()

View File

@ -23,23 +23,46 @@ class TestSidebar(tests.util.TestCase):
Gtk.Orientation.VERTICAL)
self.assertFalse(self.sidebar.get_sensitive())
self.assertTrue(self.sidebar.has_css_class("background"))
self.assertTrue(self.sidebar.has_css_class("linked"))
self.assertTrue(self.sidebar.has_css_class("card"))
def test_header(self):
"""Test the Sidebar header."""
self.assertIsInstance(self.sidebar._header, Gtk.CenterBox)
self.assertEqual(self.sidebar.get_first_child(), self.sidebar._header)
self.assertTrue(self.sidebar._header.has_css_class("toolbar"))
def test_filter(self):
"""Test the Sidebar filter entry."""
self.assertIsInstance(self.sidebar._filter, emmental.entry.Filter)
self.assertEqual(self.sidebar.get_first_child(), self.sidebar._filter)
self.assertEqual(self.sidebar._header.get_center_widget(),
self.sidebar._filter)
self.assertEqual(self.sidebar._filter.get_placeholder_text(),
"type to filter playlists")
self.assertTrue(self.sidebar._filter.get_hexpand())
with unittest.mock.patch.object(self.sql, "filter") as mock_filter:
self.sidebar._filter.set_text("test text")
self.sidebar._filter.emit("search-changed")
mock_filter.assert_called_with("*test text*")
def test_jump(self):
"""Test the jump button."""
self.assertIsInstance(self.sidebar._jump, Gtk.Button)
self.assertEqual(self.sidebar._header.get_end_widget(),
self.sidebar._jump)
self.assertEqual(self.sidebar._jump.get_icon_name(),
"go-jump-symbolic")
self.assertEqual(self.sidebar._jump.get_tooltip_text(),
"scroll to current playlist")
self.sql.playlists.load(now=True)
with unittest.mock.patch.object(self.sidebar,
"select_playlist") as mock_select:
self.sidebar._jump.emit("clicked")
mock_select.assert_called_with(self.sql.active_playlist)
def test_sensitivity_and_startup(self):
"""Test setting the sidebar sensitivity when all tables have loaded."""
tables = [t for t in self.sql.playlist_tables()]
@ -73,15 +96,17 @@ class TestSidebar(tests.util.TestCase):
self.assertIsNone(self.sidebar.selected_playlist)
playlist1 = self.sql.playlists.create("Playlist 1")
self.sidebar._group.selected_playlist = playlist1
self.sidebar._view.selected_playlist = playlist1
self.assertEqual(self.sidebar.selected_playlist, playlist1)
def test_group(self):
"""Test that sidebar sections are part of the same Group."""
self.assertIsInstance(self.sidebar._group,
emmental.sidebar.section.Group)
def test_view(self):
"""Test that sidebar sections are in the View."""
self.assertIsInstance(self.sidebar._view,
emmental.sidebar.section.View)
self.assertEqual(self.sidebar._header.get_next_sibling(),
self.sidebar._view)
self.assertListEqual(self.sidebar._group._sections,
self.assertListEqual(self.sidebar._view._sections,
[self.sidebar._playlists,
self.sidebar._artists,
self.sidebar._genres,
@ -101,7 +126,7 @@ class TestSidebar(tests.util.TestCase):
self.assertIsInstance(self.sidebar._libraries,
emmental.sidebar.library.Section)
self.assertEqual(self.sidebar._filter.get_next_sibling(),
self.assertEqual(self.sidebar._view.get_first_child(),
self.sidebar._playlists)
self.assertEqual(self.sidebar._playlists.get_next_sibling(),
self.sidebar._artists)
@ -161,3 +186,37 @@ class TestSidebar(tests.util.TestCase):
self.sidebar.select_playlist(library)
self.assertTrue(self.sidebar._libraries.active)
self.assertEqual(self.sidebar.selected_playlist, library)
def test_accelerators(self):
"""Check that the accelerators list is set up properly."""
entries = [("focus-search-playlist", self.sidebar._filter.grab_focus,
["<Control>question"]),
("goto-active-playlist", self.sidebar._jump.activate,
["<Control><Alt>g"]),
("goto-playlists", self.sidebar._playlists.activate,
["<Shift><Control>p"]),
("goto-artists", self.sidebar._artists.activate,
["<Shift><Control>a"]),
("goto-genres", self.sidebar._genres.activate,
["<Shift><Control>g"]),
("goto-decades", self.sidebar._decades.activate,
["<Shift><Control>d"]),
("goto-libraries", self.sidebar._libraries.activate,
["<Shift><Control>l"])]
accels = self.sidebar.accelerators
self.assertIsInstance(accels, list)
for i, (name, func, accel) 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)
enabled = self.sidebar.get_sensitive()
self.assertEqual(accels[i].enabled, enabled)
self.sidebar.set_sensitive(not enabled)
self.assertEqual(accels[i].enabled, not enabled)
self.assertEqual(len(accels), i + 1)

View File

@ -22,9 +22,9 @@ class TestEmmental(unittest.TestCase):
"""Check that version constants have been set properly."""
self.assertEqual(emmental.MAJOR_VERSION, 3)
self.assertEqual(emmental.MINOR_VERSION, 0)
self.assertEqual(emmental.MICRO_VERSION, 4)
self.assertEqual(emmental.VERSION_NUMBER, "3.0.4")
self.assertEqual(emmental.VERSION_STRING, "Emmental 3.0.4-debug")
self.assertEqual(emmental.MICRO_VERSION, 5)
self.assertEqual(emmental.VERSION_NUMBER, "3.0.5")
self.assertEqual(emmental.VERSION_STRING, "Emmental 3.0.5-debug")
def test_application(self):
"""Check that the application instance is initialized properly."""
@ -63,7 +63,7 @@ class TestEmmental(unittest.TestCase):
mock_startup.assert_called()
mock_load.assert_called()
mock_add_window.assert_called_with(self.application.win)
mock_set_useragent.assert_called_with("emmental-debug", "3.0.4")
mock_set_useragent.assert_called_with("emmental-debug", "3.0.5")
@unittest.mock.patch("sys.stdout")
@unittest.mock.patch("gi.repository.Adw.Application.add_window")
@ -205,6 +205,17 @@ class TestEmmental(unittest.TestCase):
self.application.player = emmental.audio.Player()
win = self.application.build_window()
for action, accel in [("app.focus-search-playlist",
"<Control>question"),
("app.goto-active-playlist", "<Control><Alt>g"),
("app.goto-playlists", "<Shift><Control>p"),
("app.goto-artists", "<Shift><Control>a"),
("app.goto-genres", "<Shift><Control>g"),
("app.goto-decades", "<Shift><Control>d"),
("app.goto-libraries", "<Shift><Control>l")]:
self.assertEqual(self.application.get_accels_for_action(action),
[accel])
self.assertEqual(win.sidebar.sql, self.application.db)
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)