Source code for ascii_designer.tk_generic_var

"""Tkinter generic variable.

Comes with tools:

* ``Invalid`` special value
* ``gt0``, ``ge0``, ``nullable`` converter compositors

Also, it sports the ``validated_hook`` property for all-in-one validation and conversion.
"""

__all__ = [
    "GenericVar",
    "Invalid",
    "ge0",
    "gt0",
    "nullable",
]

from typing import Callable
import weakref
from tkinter import Variable, Widget


[docs] class Invalid: """Sentinel object to signal that the read value was invalid. ``Invalid`` will returned as itself (i.e. not as instance). """
[docs] class GenericVar(Variable): """Generic variable. A conversion function can be specified to get a "native" value from the string representation and vice-versa. By default, behaves like a StringVar. ``master`` can be given as master widget. ``value`` is an optional value (defaults to "") ``name`` is an optional Tcl name (defaults to PY_VARnum). If ``name`` matches an existing variable and ``value`` is omitted then the existing value is retained. """ _default = "" def __init__(self, master=None, value=None, name=None, convert=str, convert_set=str, validated_hook=None): self.convert = convert """ Gives the conversion to apply when ``get``-ting the value. ``convert`` takes the string (widget content) as single argument and returns a converted value. Any Exceptions will be caught (see `get`). """ self.convert_set = convert_set """ Gives the function to apply to a value passed in to ``set``. In most cases the default (``str``) will be sufficient. """ self.validated_hook = validated_hook Variable.__init__(self, master, value, name) @property def validated_hook(self) -> Callable[[bool], None]: """If set, adds a side-effect to ``get`` depending on whether the value was valid or not. It shall be a callback taking a single bool argument ``valid``; which is True or False depending on whether ``convert`` raised an exception. If you set ``validated_hook`` to a Tk widget, we will automatically convert it into a callback: * For a Tk widget, foreground color will be set to ``red`` / original color depending on ``valid``. * For a Ttk widget, ``invalid`` state will be set/reset. Note that in most themes, by default no visual change happens, unless you configured an appropriate style map. """ return self._validated_hook @validated_hook.setter def validated_hook(self, val): if isinstance(val, Widget): val = _make_hook_for_widget(val) self._validated_hook = val
[docs] def get(self): """Return converted value of variable. If conversion failed, returns :py:obj:`.Invalid`.""" # We use here the fact, that when used in a widget, the value will be # retrieved directly instead through .get(). Thus the widget will always "see" the str representation. value = self._tk.globalgetvar(self._name) try: value = self.convert(value) except Exception as e: value = Invalid if self._validated_hook: self._validated_hook(value is not Invalid) return value
[docs] def set(self, value): value = self.convert_set(value) super().set(value)
def _make_hook_for_widget(widget): wwidget = weakref.ref(widget) if hasattr(widget, "state"): # TTK def setvalid(valid): widget = wwidget() if widget is None: return widget.state(["!invalid"] if valid else ["invalid"]) else: # plain Tk orig_color = widget["foreground"] def setvalid(valid): widget = wwidget() if widget is None: return widget["foreground"] = orig_color if valid else "red" return setvalid
[docs] def gt0(convert): '''Applies ``convert``, then raises if value is not greater than 0. ``convert`` must return something number-like. >>> generic_var.convert = gt0(int) ''' def wrap_convert(x): x = convert(x) if not x > 0: raise ValueError("Expected number greater than 0") return x return wrap_convert
[docs] def ge0(convert): '''Applies ``convert``, then raises if value is not greater or equal to 0. ``convert`` must return something number-like. >>> generic_var.convert = ge0(int) ''' def wrap_convert(x): x = convert(x) if not x >= 0: raise ValueError("Expected number greater or equal to 0") return x return wrap_convert
[docs] def nullable(convert): '''Creates a converter that returns ``None`` on empty string, otherwise applies given converter. >>> generic_var = GenericVar(tkroot, convert=nullable(float)) >>> generic_var.set("1.0") >>> generic_var.get() 1.0 >>> generic_var.set("") >>> generic_var.get() None >>> generic_var.set("foo") >>> generic_var.get() <class Invalid> ''' def wrap_convert(x): if x == "": return None else: return convert(x) return wrap_convert