feat: Control widget for numeric fields
Change-Id: I00680f69dab06dedafde990dba3de1eeb994164a
diff --git a/dcs/config/ui/num_widget.py b/dcs/config/ui/num_widget.py
new file mode 100644
index 0000000..7652469
--- /dev/null
+++ b/dcs/config/ui/num_widget.py
@@ -0,0 +1,62 @@
+# SPDX-License-Identifier: MPL-2.0
+# Copyright (c) 2022 Philipp Le <philipp@philipple.de>.
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+
+from typing import Type
+from tkinter import ttk, StringVar
+from pydantic import ConstrainedInt, ConstrainedFloat
+
+from .base import make_config_widget_base
+
+
+class NumWidget(make_config_widget_base(ttk.Spinbox)):
+ MODEL_TYPE = int | float
+
+ def sync_out(self) -> None:
+ try:
+ setattr(self.model, self.model_key, self.type_(self._stringvar.get()))
+ self.notify_change()
+ except ValueError:
+ # Tolerate temporarily invalid values
+ pass
+
+ def sync_in(self) -> None:
+ val = getattr(self.model, self.model_key)
+ self._stringvar.set(str(val))
+
+ def _config_widget(self) -> None:
+ self._stringvar = StringVar(self.parent)
+ self.sync_in()
+ self.configure(textvariable=self._stringvar, command=lambda: self.sync_out())
+ self.bind('<KeyRelease>', lambda e: self.sync_out())
+ if issubclass(self.type_, ConstrainedInt):
+ tp: Type[ConstrainedInt] = self.type_
+ self.configure(increment=1)
+ if tp.gt is not None:
+ self.configure(from_=tp.gt + 1)
+ if tp.ge is not None:
+ self.configure(from_=tp.ge)
+ if tp.lt is not None:
+ self.configure(to=tp.lt - 1)
+ if tp.le is not None:
+ self.configure(to=tp.le)
+ elif issubclass(self.type_, ConstrainedFloat):
+ tp: Type[ConstrainedFloat] = self.type_
+ self.configure(increment=tp.multiple_of)
+ if tp.gt is not None:
+ self.configure(from_=tp.gt)
+ if tp.ge is not None:
+ self.configure(from_=tp.ge)
+ if tp.lt is not None:
+ self.configure(to=tp.lt)
+ if tp.le is not None:
+ self.configure(to=tp.le)
+ elif issubclass(self.type_, float):
+ raise TypeError(f'Please use ConstrainedFloat for {self.model_key}')
+ elif issubclass(self.type_, int):
+ raise TypeError(f'Please use ConstrainedInt for {self.model_key}')
+ else:
+ print(f'Unknown type f{repr(self.type_)}')
diff --git a/dcs/config/ui/ui_create.py b/dcs/config/ui/ui_create.py
index 1bff499..16d4ede 100644
--- a/dcs/config/ui/ui_create.py
+++ b/dcs/config/ui/ui_create.py
@@ -10,9 +10,11 @@
from pydantic import BaseModel
from .base import ConfigWidget
+from .num_widget import NumWidget
_CLASS_REGISTRY: List[Type[ConfigWidget]] = [
+ NumWidget,
]
diff --git a/test/test_config_ui.py b/test/test_config_ui.py
index 688f3ca..7b23027 100644
--- a/test/test_config_ui.py
+++ b/test/test_config_ui.py
@@ -7,15 +7,16 @@
import unittest
from typing import List
from pydantic import conint, confloat
-from tkinter import ttk
+from tkinter import Tk, ttk
from enum import IntEnum
+import toml
from dcs.config.store import Store, ConfigObject, ConfigControlFrame
from dcs.config.ui import ui_create
class ExampleSubFrame(ConfigControlFrame):
- pass
+ wdg_id: ttk.Spinbox
@ui_create
@@ -25,6 +26,10 @@
def make_config_widget(self, parent: ttk.Widget) -> ConfigControlFrame:
frm = ExampleSubFrame(parent)
+ frm.pack()
+ ttk.Label(frm, text="Id:").grid(row=0, column=0)
+ frm.wdg_id = self.ui_create_id(frm)
+ frm.wdg_id.grid(row=0, column=1)
return frm
@@ -73,3 +78,49 @@
cfg: ExampleConfig = store.get_config(ExampleConfig)
self.assertIn('ui_create_id', dir(cfg))
+
+
+class UIInteractiveTest(unittest.TestCase):
+ def setUp(self) -> None:
+ self.root = Tk()
+ self.frm = ttk.Frame(self.root, padding=10)
+ self.frm.pack()
+
+ self.store = Store.new('default')
+
+ def _print_cfg(self) -> None:
+ print(repr(self.store.get_config(ExampleConfig)))
+ print(toml.dumps(self.store.dump_obj()))
+ #print(self.store.get_config(ExampleConfig).json())
+
+ def _run_ui(self) -> bool:
+ self.res = False
+
+ def act(ok: bool) -> None:
+ self.root.destroy()
+ self.res = ok
+
+ self.ok_btn = ttk.Button(self.root, text='Test OK', command=lambda: act(True))
+ self.ok_btn.pack()
+ self.ok_fail = ttk.Button(self.root, text='Test Fail', command=lambda: act(False))
+ self.ok_fail.pack()
+
+ self.root.mainloop()
+
+ return self.res
+
+ def test_int_widget(self):
+ cfg = self.store.get_config(ExampleConfig)
+ wdg = cfg.ui_create_id(self.frm)
+ wdg.pack()
+ wdg.listen_change(lambda x, y, z: self._print_cfg())
+
+ self.assertTrue(self._run_ui())
+
+ def test_float_widget(self):
+ cfg = self.store.get_config(ExampleConfig)
+ wdg = cfg.ui_create_flt(self.frm)
+ wdg.pack()
+ wdg.listen_change(lambda x, y, z: self._print_cfg())
+
+ self.assertTrue(self._run_ui())