diff --git a/emmental/__init__.py b/emmental/__init__.py index ae87671..920b563 100644 --- a/emmental/__init__.py +++ b/emmental/__init__.py @@ -9,6 +9,7 @@ from . import header from . import mpris2 from . import nowplaying from . import options +from . import sidebar from . import window from gi.repository import GObject from gi.repository import GLib @@ -92,18 +93,23 @@ class Application(Adw.Application): playing.connect("play", self.player.play) playing.connect("pause", self.player.pause) playing.connect("seek", self.__on_seek) - return playing + def build_sidebar(self) -> sidebar.Card: + """Build a new sidebar card.""" + return sidebar.Card(sql=self.db) + def build_window(self) -> window.Window: """Build a new window instance.""" win = window.Window(VERSION_STRING, header=self.build_header(), - now_playing=self.build_now_playing()) + now_playing=self.build_now_playing(), + sidebar=self.build_sidebar()) for (setting, property) in [("window.width", "default-width"), ("window.height", "default-height"), - ("now-playing.size", "now-playing-size")]: + ("now-playing.size", "now-playing-size"), + ("sidebar.size", "sidebar-size")]: self.db.settings.bind_setting(setting, win, property) return win diff --git a/emmental/sidebar/__init__.py b/emmental/sidebar/__init__.py new file mode 100644 index 0000000..3b2f70e --- /dev/null +++ b/emmental/sidebar/__init__.py @@ -0,0 +1,34 @@ +# Copyright 2022 (c) Anna Schumaker. +"""A card for displaying the list of playlists.""" +from gi.repository import GObject +from gi.repository import Gtk +from .. import db +from .. import entry + + +class Card(Gtk.Box): + """Our playlist Sidebar.""" + + sql = GObject.Property(type=db.Connection) + + def __init__(self, sql: db.Connection, **kwargs): + """Set up the Sidebar widget.""" + super().__init__(sql=sql, orientation=Gtk.Orientation.VERTICAL, + sensitive=False, **kwargs) + self._filter = entry.Filter("playlists") + + self.append(self._filter) + + self._filter.connect("search-changed", self.__search_changed) + self.sql.connect("table-loaded", self.__table_loaded) + + self.add_css_class("background") + self.add_css_class("linked") + self.add_css_class("card") + + def __search_changed(self, entry: entry.Filter) -> None: + self.sql.filter(entry.get_query()) + + def __table_loaded(self, sql: db.Connection, table: db.table.Table): + loaded = {tbl.loaded for tbl in sql.playlist_tables()} + self.set_sensitive(False not in loaded) diff --git a/emmental/window.py b/emmental/window.py index 7ae0003..e1899c1 100644 --- a/emmental/window.py +++ b/emmental/window.py @@ -28,6 +28,7 @@ class Window(Adw.Window): header = GObject.Property(type=Gtk.Widget) sidebar = GObject.Property(type=Gtk.Widget) + sidebar_size = GObject.Property(type=int, default=300) now_playing = GObject.Property(type=Gtk.Widget) now_playing_size = GObject.Property(type=int, default=250) tracklist = GObject.Property(type=Gtk.Widget) @@ -43,6 +44,7 @@ class Window(Adw.Window): start_child=self.now_playing, end_child=self.tracklist) self._outer_pane = _make_pane(Gtk.Orientation.HORIZONTAL, + position=self.sidebar_size, start_child=self.sidebar, end_child=self._inner_pane) self._toast = Adw.ToastOverlay(child=self._outer_pane) @@ -53,6 +55,8 @@ class Window(Adw.Window): self.bind_property("header", self._header, "child") self.bind_property("sidebar", self._outer_pane, "start-child") + self.bind_property("sidebar-size", self._outer_pane, "position", + GObject.BindingFlags.BIDIRECTIONAL) self.bind_property("now-playing", self._inner_pane, "start-child") self.bind_property("now-playing-size", self._inner_pane, "position", GObject.BindingFlags.BIDIRECTIONAL) diff --git a/tests/sidebar/test_sidebar.py b/tests/sidebar/test_sidebar.py new file mode 100644 index 0000000..c10ff78 --- /dev/null +++ b/tests/sidebar/test_sidebar.py @@ -0,0 +1,49 @@ +# Copyright 2022 (c) Anna Schumaker. +"""Tests our playlist Sidebar card.""" +import emmental.sidebar +import tests.util +import unittest.mock +from gi.repository import Gtk + + +class TestSidebar(tests.util.TestCase): + """Tests the Sidebar card.""" + + def setUp(self): + """Set up common variables.""" + super().setUp() + self.sidebar = emmental.sidebar.Card(sql=self.sql) + + def test_init(self): + """Test that the Sidebar has been set up correctly.""" + self.assertIsInstance(self.sidebar, Gtk.Box) + + self.assertEqual(self.sidebar.sql, self.sql) + self.assertEqual(self.sidebar.get_orientation(), + Gtk.Orientation.VERTICAL) + + 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_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._filter.get_placeholder_text(), + "type to filter playlists") + + 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_sensitivity(self): + """Test setting the sidebar sensitivity when all tables have loaded.""" + tables = [t for t in self.sql.playlist_tables()] + self.assertFalse(self.sidebar.get_sensitive()) + + for table in tables: + self.sql.emit("table-loaded", table) + self.assertEqual(self.sidebar.get_sensitive(), table == tables[-1]) diff --git a/tests/test_emmental.py b/tests/test_emmental.py index 4291f3f..dccf7eb 100644 --- a/tests/test_emmental.py +++ b/tests/test_emmental.py @@ -103,6 +103,7 @@ class TestEmmental(unittest.TestCase): self.assertIsInstance(win, emmental.window.Window) self.assertIsInstance(win.header, emmental.header.Header) self.assertIsInstance(win.now_playing, emmental.nowplaying.Card) + self.assertIsInstance(win.sidebar, emmental.sidebar.Card) self.assertEqual(win.header.title, emmental.VERSION_STRING) @@ -141,6 +142,15 @@ class TestEmmental(unittest.TestCase): self.application.autopause = 2 self.assertEqual(win.now_playing.autopause, 2) + @unittest.mock.patch("sys.stdout", new_callable=io.StringIO) + def test_sidebar(self, mock_stdout: io.StringIO): + """Check that the sidebar widget is wired up properly.""" + self.application.db = emmental.db.Connection() + self.application.player = emmental.audio.Player() + win = self.application.build_window() + + self.assertEqual(win.sidebar.sql, self.application.db) + @unittest.mock.patch("sys.stdout", new_callable=io.StringIO) def test_replaygain(self, mock_stdout: io.StringIO): """Test setting replaygain modes.""" diff --git a/tests/test_settings.py b/tests/test_settings.py index e4a7a3d..d06f37f 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -98,3 +98,13 @@ class TestSettings(unittest.TestCase): self.assertFalse(self.settings["now-playing.prefer-artist"]) self.assertFalse(self.app.build_window().now_playing.prefer_artist) + + def test_save_sidebar_size(self, mock_stdout: io.StringIO): + """Check saving and loading the sidebar widget size.""" + self.assertEqual(self.win.sidebar_size, 300) + self.assertEqual(self.settings["sidebar.size"], 300) + + self.win.sidebar_size = 400 + self.assertEqual(self.settings["sidebar.size"], 400) + + self.assertEqual(self.app.build_window().sidebar_size, 400) diff --git a/tests/test_window.py b/tests/test_window.py index 6799af7..3941690 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -80,6 +80,19 @@ class TestWindow(unittest.TestCase): self.assertEqual(window2._outer_pane.get_start_child(), window2.sidebar) + def test_sidebar_size(self): + """Check setting the size of the sidebar area.""" + self.assertEqual(self.window.sidebar_size, 300) + self.assertEqual(self.window._outer_pane.get_position(), 300) + + self.window.sidebar_size = 100 + self.assertEqual(self.window.sidebar_size, 100) + self.assertEqual(self.window._outer_pane.get_position(), 100) + + self.window._outer_pane.set_position(200) + self.assertEqual(self.window.sidebar_size, 200) + self.assertEqual(self.window._outer_pane.get_position(), 200) + def test_now_playing(self): """Check setting a widget to the now_playing area.""" self.assertIsNone(self.window.now_playing)