diff --git a/emmental/emmental.css b/emmental/emmental.css index 65bce02..90cd921 100644 --- a/emmental/emmental.css +++ b/emmental/emmental.css @@ -1 +1,11 @@ /* Copyright 2022 (c) Anna Schumaker. */ + +/* Make the Gtk.Paned separator transparent with extra padding */ +paned.emmental-pane>separator { + opacity: 0; + padding: 8px; +} + +*.emmental-padding { + padding: 8px; +} diff --git a/emmental/window.py b/emmental/window.py new file mode 100644 index 0000000..9f231ec --- /dev/null +++ b/emmental/window.py @@ -0,0 +1,72 @@ +# Copyright 2022 (c) Anna Schumaker. +"""A custom Adw.Window configured for our application.""" +from gi.repository import GObject +from gi.repository import Gtk +from gi.repository import Adw + + +def _make_pane(orientation: Gtk.Orientation, + start_child: Gtk.Widget = None, + end_child: Gtk.Widget = None) -> Gtk.Paned: + pane = Gtk.Paned(orientation=orientation, hexpand=True, vexpand=True, + shrink_start_child=False, resize_start_child=False, + start_child=start_child, end_child=end_child) + pane.add_css_class("emmental-pane") + return pane + + +class Window(Adw.Window): + """Our custom Adw.Window. + + Our Window is configured with 4 regions for widgets: + * An area to place a CSD header bar + * An area for a sidebar + * An area to show now-playing information + * An area to display a tracklist + """ + + header = GObject.Property(type=Gtk.Widget) + sidebar = GObject.Property(type=Gtk.Widget) + now_playing = GObject.Property(type=Gtk.Widget) + tracklist = GObject.Property(type=Gtk.Widget) + + def __init__(self, version: str, **kwargs): + """Initialize our Window.""" + super().__init__(icon_name="emmental", title=version, + default_width=1600, default_height=900, **kwargs) + self._box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + self._header = Adw.Bin(child=self.header) + self._inner_pane = _make_pane(Gtk.Orientation.VERTICAL, + start_child=self.now_playing, + end_child=self.tracklist) + self._outer_pane = _make_pane(Gtk.Orientation.HORIZONTAL, + start_child=self.sidebar, + end_child=self._inner_pane) + self._toast = Adw.ToastOverlay(child=self._outer_pane) + + self._outer_pane.add_css_class("emmental-padding") + if __debug__: + self.add_css_class("devel") + + self.bind_property("header", self._header, "child") + self.bind_property("sidebar", self._outer_pane, "start-child") + self.bind_property("now-playing", self._inner_pane, "start-child") + self.bind_property("tracklist", self._inner_pane, "end-child") + + self._box.append(self._header) + self._box.append(self._toast) + self.set_content(self._box) + + def close(self, *args) -> None: + """Close the window.""" + super().close() + + def post_toast(self, title: str) -> Adw.Toast: + """Create a new Adw.Toas and add it to the window.""" + toast = Adw.Toast.new(title) + self._toast.add_toast(toast) + return toast + + def present(self, *args) -> None: + """Present the window.""" + super().present() diff --git a/tests/test_window.py b/tests/test_window.py new file mode 100644 index 0000000..b9507d2 --- /dev/null +++ b/tests/test_window.py @@ -0,0 +1,125 @@ +# Copyright 2022 (c) Anna Schumaker. +"""Tests our application window.""" +import unittest +import emmental.window +from gi.repository import Adw +from gi.repository import Gtk + + +class TestWindow(unittest.TestCase): + """Test case for our custom Gtk.Window.""" + + def setUp(self): + """Set up common variables.""" + self.window = emmental.window.Window("Test 1.2.3") + + def tearDown(self): + """Clean up.""" + self.window.close() + + def test_init(self): + """Check that the window gets set up properly.""" + self.assertIsInstance(self.window, Adw.Window) + self.assertIsInstance(self.window._box, Gtk.Box) + self.assertIsInstance(self.window._header, Adw.Bin) + self.assertIsInstance(self.window._outer_pane, Gtk.Paned) + self.assertIsInstance(self.window._inner_pane, Gtk.Paned) + self.assertIsInstance(self.window._toast, Adw.ToastOverlay) + self.assertTrue(self.window.has_css_class("devel")) + + self.assertEqual(self.window.get_icon_name(), "emmental") + self.assertEqual(self.window.get_title(), "Test 1.2.3") + self.assertEqual(self.window.get_default_size(), (1600, 900)) + + def test_content(self): + """Check that the Window content is set up properly.""" + self.assertEqual(self.window._box.get_orientation(), + Gtk.Orientation.VERTICAL) + self.assertEqual(self.window._box.get_spacing(), 0) + self.assertEqual(self.window.get_content(), self.window._box) + self.assertEqual(self.window._box.get_first_child(), + self.window._header) + self.assertEqual(self.window._header.get_next_sibling(), + self.window._toast) + self.assertEqual(self.window._toast.get_child(), + self.window._outer_pane) + self.assertEqual(self.window._outer_pane.get_end_child(), + self.window._inner_pane) + self.assertTrue(self.window._outer_pane.has_css_class( + "emmental-padding")) + + subtests = [(self.window._outer_pane, Gtk.Orientation.HORIZONTAL), + (self.window._inner_pane, Gtk.Orientation.VERTICAL)] + for pane, orientation in subtests: + self.assertEqual(pane.get_orientation(), orientation) + self.assertFalse(pane.get_shrink_start_child()) + self.assertFalse(pane.get_resize_start_child()) + self.assertTrue(pane.get_hexpand()) + self.assertTrue(pane.get_vexpand()) + self.assertTrue(pane.has_css_class("emmental-pane")) + + def test_header(self): + """Check setting a widget to the header area.""" + self.assertIsNone(self.window.header) + self.window.header = Gtk.Label() + self.assertEqual(self.window._header.get_child(), self.window.header) + + window2 = emmental.window.Window(version="1.2.3", header=Gtk.Label()) + self.assertIsInstance(window2.header, Gtk.Label) + self.assertEqual(window2._header.get_child(), window2.header) + + def test_sidebar(self): + """Check setting a widget to the sidebar area.""" + self.assertIsNone(self.window.sidebar) + self.window.sidebar = Gtk.Label() + self.assertEqual(self.window._outer_pane.get_start_child(), + self.window.sidebar) + + window2 = emmental.window.Window(version="1.2.3", sidebar=Gtk.Label()) + self.assertIsInstance(window2.sidebar, Gtk.Label) + self.assertEqual(window2._outer_pane.get_start_child(), + window2.sidebar) + + def test_now_playing(self): + """Check setting a widget to the now_playing area.""" + self.assertIsNone(self.window.now_playing) + self.window.now_playing = Gtk.Label() + self.assertEqual(self.window._inner_pane.get_start_child(), + self.window.now_playing) + + window2 = emmental.window.Window(version="1.2.3", + now_playing=Gtk.Label()) + self.assertIsInstance(window2.now_playing, Gtk.Label) + self.assertEqual(window2._inner_pane.get_start_child(), + window2.now_playing) + + def test_tracklist(self): + """Check setting a widget to the tracklist area.""" + self.assertIsNone(self.window.tracklist) + self.window.tracklist = Gtk.Label() + self.assertEqual(self.window._inner_pane.get_end_child(), + self.window.tracklist) + + window2 = emmental.window.Window(version="1.2.3", + tracklist=Gtk.Label()) + self.assertIsInstance(window2.tracklist, Gtk.Label) + self.assertEqual(window2._inner_pane.get_end_child(), + window2.tracklist) + + def test_post_toast(self): + """Test posting a Toast message to the window.""" + toast = self.window.post_toast("Test Toast") + self.assertIsInstance(toast, Adw.Toast) + self.assertEqual(toast.get_title(), "Test Toast") + + def test_close(self): + """Test calling close().""" + with unittest.mock.patch.object(Gtk.Window, "close") as mock_close: + self.window.close(1, 2, 3, 4, 5) + mock_close.assert_called() + + def test_present(self): + """Test calling present().""" + with unittest.mock.patch.object(Gtk.Window, "present") as mock_present: + self.window.present(1, 2, 3, 4, 5) + mock_present.assert_called()