diff --git a/emmental/header/__init__.py b/emmental/header/__init__.py index 81eac09..dba4d9f 100644 --- a/emmental/header/__init__.py +++ b/emmental/header/__init__.py @@ -4,6 +4,8 @@ from gi.repository import GObject from gi.repository import Gtk from gi.repository import Adw from .. import db +if __debug__: + from . import settings SUBTITLE = "The Cheesy Music Player" @@ -23,4 +25,14 @@ class Header(Gtk.HeaderBar): self.bind_property("title", self._title, "title") self.bind_property("subtitle", self._title, "subtitle") + if __debug__: + self._window = settings.Window(sql) + self._settings = Gtk.Button.new_from_icon_name("settings-symbolic") + self._settings.connect("clicked", self.__run_settings) + self.pack_start(self._settings) + self.set_title_widget(self._title) + + def __run_settings(self, button: Gtk.Button) -> None: + if __debug__: + self._window.present() diff --git a/emmental/header/settings.py b/emmental/header/settings.py new file mode 100644 index 0000000..37544c0 --- /dev/null +++ b/emmental/header/settings.py @@ -0,0 +1,66 @@ +# Copyright 2022 (c) Anna Schumaker. +"""A custom Gtk.Dialog for showing Settings.""" +from gi.repository import Gtk +from gi.repository import Adw +from .. import db +from .. import entry +from .. import factory + + +class ValueRow(factory.ListRow): + """A Row for displaying settings values.""" + + def do_bind(self) -> None: + """Bind a db.Setting to this Row.""" + if isinstance(self.item.value, bool): + self.child = Gtk.Switch(halign=Gtk.Align.START) + self.bind_and_set_property("value", "active", bidirectional=True) + elif isinstance(self.item.value, str): + self.child = entry.String(has_frame=False) + self.bind_and_set_property("value", "value", bidirectional=True) + elif isinstance(self.item.value, int): + self.child = entry.Integer(has_frame=False) + self.bind_and_set_property("value", "value", bidirectional=True) + elif isinstance(self.item.value, float): + self.child = entry.Float(has_frame=False) + self.bind_and_set_property("value", "value", bidirectional=True) + + +class Window(Adw.Window): + """A custom window that displays the current settings.""" + + def __init__(self, sql: db.Connection): + """Initialize the Settings window.""" + super().__init__(default_width=500, default_height=500, + title="Emmental Settings", icon_name="settings", + hide_on_close=True, + content=Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)) + self._search = entry.Filter(what="settings") + self._header = Gtk.HeaderBar(title_widget=self._search) + self._selection = Gtk.NoSelection(model=sql.settings) + self._view = Gtk.ColumnView(model=self._selection, + show_row_separators=True) + self._scroll = Gtk.ScrolledWindow(child=self._view, vexpand=True) + + self._scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + + self.__append_column(factory.InscriptionFactory("key"), + "Key", width=400) + self.__append_column(factory.Factory(row_type=ValueRow), + "Value", width=100) + + self.get_content().append(self._header) + self.get_content().append(self._scroll) + + if __debug__: + self.add_css_class("devel") + self._search.connect("search-changed", self.__filter) + + def __append_column(self, factory: factory.Factory, + title: str, *, width: int) -> None: + self._view.append_column(Gtk.ColumnViewColumn(factory=factory, + title=title, + fixed_width=width)) + + def __filter(self, entry: entry.Filter) -> None: + self._selection.get_model().filter(entry.get_query()) diff --git a/icons/scalable/actions/settings-symbolic.svg b/icons/scalable/actions/settings-symbolic.svg new file mode 100644 index 0000000..408d7e5 --- /dev/null +++ b/icons/scalable/actions/settings-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/tests/header/__init__.py b/tests/header/__init__.py new file mode 100644 index 0000000..d645978 --- /dev/null +++ b/tests/header/__init__.py @@ -0,0 +1,2 @@ +# Copyright 2023 (c) Anna Schumaker. +"""Needed to fix the unique basename problem with pytest.""" diff --git a/tests/header/test_header.py b/tests/header/test_header.py index fc57a6f..dbd6d6e 100644 --- a/tests/header/test_header.py +++ b/tests/header/test_header.py @@ -2,6 +2,7 @@ """Tests our application header.""" import emmental import tests.util +import unittest.mock from gi.repository import Gtk from gi.repository import Adw @@ -30,3 +31,18 @@ class TestHeader(tests.util.TestCase): self.assertEqual(self.header.subtitle, emmental.header.SUBTITLE) self.assertEqual(self.header._title.get_subtitle(), emmental.header.SUBTITLE) + + def test_settings(self): + """Check that the Settings window is set up correctly.""" + self.assertIsInstance(self.header._settings, Gtk.Button) + self.assertIsInstance(self.header._window, + emmental.header.settings.Window) + + self.assertEqual(self.header.sql, self.sql) + self.assertEqual(self.header._settings.get_icon_name(), + "settings-symbolic") + + with unittest.mock.patch.object(self.header._window, + "present") as mock_present: + self.header._settings.emit("clicked") + mock_present.assert_called() diff --git a/tests/header/test_settings.py b/tests/header/test_settings.py new file mode 100644 index 0000000..d910dc0 --- /dev/null +++ b/tests/header/test_settings.py @@ -0,0 +1,143 @@ +# Copyright 2022 (c) Anna Schumaker. +"""Tests our Settings dialog.""" +import unittest.mock +import emmental.header.settings +import emmental.entry +import tests.util +from gi.repository import Gtk +from gi.repository import Adw + + +class TestValueRow(tests.util.TestCase): + """Test the settings Value Row.""" + + def setUp(self): + """Set up common variables.""" + super().setUp() + self.listitem = Gtk.ListItem() + self.row = emmental.header.settings.ValueRow(self.listitem) + + def make_setting(self, name: str, type: str, value: any) -> None: + """Create a setting in the database.""" + setting = self.sql.settings.create(name, type, value) + self.listitem.get_item = unittest.mock.Mock(return_value=setting) + + def test_init(self): + """Tetst that the ValueRow is configured correctly.""" + self.assertIsInstance(self.row, emmental.factory.ListRow) + self.assertIsNone(self.row.child) + + def test_boolean_setting(self): + """Test configuring the row for a boolean setting.""" + self.make_setting("test.bool", "gboolean", True) + + self.row.bind() + self.assertIsInstance(self.row.child, Gtk.Switch) + self.assertEqual(self.row.child.get_halign(), Gtk.Align.START) + self.assertTrue(self.row.child.get_active()) + + def test_string_setting(self): + """Test configuring the row for a string setting.""" + self.make_setting("test.string", "gchararray", "Test Text") + + self.row.bind() + self.assertIsInstance(self.row.child, emmental.entry.String) + self.assertFalse(self.row.child.get_has_frame()) + self.assertEqual(self.row.child.value, "Test Text") + + def test_integer_setting(self): + """Test configuring the row for an integer setting.""" + self.make_setting("test.int", "gint", 42) + + self.row.bind() + self.assertIsInstance(self.row.child, emmental.entry.Integer) + self.assertFalse(self.row.child.get_has_frame()) + self.assertEqual(self.row.child.value, 42) + + def test_float_setting(self): + """Test configuring the row for a float setting.""" + self.make_setting("test.float", "gdouble", 1.234) + + self.row.bind() + self.assertIsInstance(self.row.child, emmental.entry.Float) + self.assertFalse(self.row.child.get_has_frame()) + self.assertEqual(self.row.child.value, 1.234) + + +class TestWindow(tests.util.TestCase): + """Test the Settings Window.""" + + def setUp(self): + """Set up common variables.""" + super().setUp() + self.window = emmental.header.settings.Window(sql=self.sql) + + def test_init(self): + """Test that the Settings Window is configured properly.""" + self.assertIsInstance(self.window, Adw.Window) + self.assertEqual(self.window.get_default_size(), (500, 500)) + self.assertEqual(self.window.get_title(), "Emmental Settings") + self.assertEqual(self.window.get_icon_name(), "settings") + self.assertTrue(self.window.get_hide_on_close()) + self.assertTrue(self.window.has_css_class("devel")) + + def test_content(self): + """Test that the Window content was set up properly.""" + box = self.window.get_content() + self.assertIsInstance(box, Gtk.Box) + self.assertEqual(box.get_orientation(), Gtk.Orientation.VERTICAL) + self.assertEqual(box.get_spacing(), 0) + + def test_headerbar(self): + """Test that the Window headerbar was set up properly.""" + self.assertIsInstance(self.window._header, Gtk.HeaderBar) + self.assertEqual(self.window.get_content().get_first_child(), + self.window._header) + + def test_search(self): + """Test the search entry widget.""" + self.assertIsInstance(self.window._search, emmental.entry.Filter) + self.assertEqual(self.window._search.get_placeholder_text(), + "type to filter settings") + self.assertEqual(self.window._header.get_title_widget(), + self.window._search) + + self.window._search.set_text("abcde") + with unittest.mock.patch.object(self.sql.settings, + "filter") as mock_filter: + self.window._search.emit("search-changed") + mock_filter.assert_called_with("*abcde*") + + def test_columnview(self): + """Test the columnview widget.""" + self.assertIsInstance(self.window._scroll, Gtk.ScrolledWindow) + self.assertIsInstance(self.window._selection, Gtk.NoSelection) + self.assertIsInstance(self.window._view, Gtk.ColumnView) + + self.assertEqual(self.window._scroll.get_policy(), + (Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)) + + self.assertEqual(self.window._view.get_model(), self.window._selection) + self.assertEqual(self.window._selection.get_model(), self.sql.settings) + self.assertTrue(self.window._view.get_show_row_separators()) + + self.assertEqual(self.window._header.get_next_sibling(), + self.window._scroll) + self.assertEqual(self.window._scroll.get_child(), self.window._view) + + def test_columns(self): + """Test the view's columns.""" + columns = self.window._view.get_columns() + self.assertEqual(len(columns), 2) + + self.assertIsInstance(columns[0].get_factory(), + emmental.factory.InscriptionFactory) + self.assertEqual(columns[0].get_title(), "Key") + self.assertEqual(columns[0].get_fixed_width(), 400) + + self.assertIsInstance(columns[1].get_factory(), + emmental.factory.Factory) + self.assertEqual(columns[1].get_factory().row_type, + emmental.header.settings.ValueRow) + self.assertEqual(columns[1].get_title(), "Value") + self.assertEqual(columns[1].get_fixed_width(), 100)