window: Add a custom Window class

This window is set up with specific areas for our header, sidebar, now
playing info, and tracklist. It also implements a post_toast() function
so toast notifications can be displayed to the user.

Implements: #44 ("Create a new 3WayPane widget")
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2022-05-30 11:47:14 -04:00
parent 767f0c1584
commit 5d1c11e64e
3 changed files with 207 additions and 0 deletions

View File

@ -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;
}

72
emmental/window.py Normal file
View File

@ -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()

125
tests/test_window.py Normal file
View File

@ -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()