header/dialog: Create a Settings Dialog

This dialog is used to manually edit the settings in the database. I
bind the properties in such a way that changes are seen instantly.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2022-09-24 10:12:06 -04:00
parent 50270bd04c
commit 4cd1e89493
6 changed files with 241 additions and 0 deletions

View File

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

View File

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

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 7.5 1.019531 c -0.550781 0 -0.996094 0.445313 -0.996094 0.996094 v 0.453125 c -0.472656 0.128906 -0.929687 0.320312 -1.355468 0.566406 l -0.324219 -0.324218 c -0.390625 -0.390626 -1.019531 -0.390626 -1.410157 0 l -0.703124 0.707031 c -0.390626 0.390625 -0.390626 1.019531 0 1.410156 l 0.320312 0.320313 c -0.246094 0.425781 -0.433594 0.882812 -0.5625 1.355468 h -0.453125 c -0.550781 0 -0.996094 0.445313 -0.996094 0.996094 v 1 c 0 0.550781 0.445313 0.996094 0.996094 0.996094 h 0.449219 c 0.132812 0.472656 0.320312 0.929687 0.566406 1.355468 l -0.320312 0.320313 c -0.390626 0.390625 -0.390626 1.019531 0 1.410156 l 0.703124 0.707031 c 0.390626 0.390626 1.019532 0.390626 1.410157 0 l 0.320312 -0.320312 c 0.429688 0.242188 0.882813 0.433594 1.359375 0.558594 v 0.457031 c 0 0.550781 0.445313 0.996094 0.996094 0.996094 h 0.996094 c 0.554687 0 1 -0.445313 1 -0.996094 v -0.453125 c 0.472656 -0.128906 0.929687 -0.320312 1.355468 -0.566406 l 0.320313 0.324218 c 0.390625 0.390626 1.019531 0.390626 1.410156 0 l 0.707031 -0.707031 c 0.390626 -0.390625 0.390626 -1.019531 0 -1.410156 l -0.320312 -0.320313 c 0.242188 -0.425781 0.433594 -0.882812 0.558594 -1.355468 h 0.453125 c 0.554687 0 1 -0.445313 1 -0.996094 v -1 c 0 -0.550781 -0.445313 -0.996094 -1 -0.996094 h -0.449219 c -0.128906 -0.472656 -0.320312 -0.929687 -0.566406 -1.355468 l 0.324218 -0.320313 c 0.390626 -0.390625 0.390626 -1.019531 0 -1.410156 l -0.707031 -0.707031 c -0.390625 -0.390626 -1.019531 -0.390626 -1.410156 0 l -0.320313 0.320312 c -0.425781 -0.242188 -0.882812 -0.429688 -1.355468 -0.558594 v -0.457031 c 0 -0.550781 -0.445313 -0.996094 -1 -0.996094 z m 0.515625 3.976563 c 1.660156 0 3 1.34375 3 3 s -1.339844 3 -3 3 c -1.65625 0 -3 -1.34375 -3 -3 s 1.34375 -3 3 -3 z m 0 0" fill="#222222"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

2
tests/header/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# Copyright 2023 (c) Anna Schumaker.
"""Needed to fix the unique basename problem with pytest."""

View File

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

View File

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