[MouseFox logo]
The MouseFox Project
Join The Community Discord Server
[Discord logo]
Edit on GitHub

kvex.widgets.inputpanel

Panel of simple input widgets.

  1"""Panel of simple input widgets."""
  2
  3from dataclasses import dataclass, field
  4from typing import Any, Optional, Callable
  5import functools
  6from .. import kivy as kv
  7from .. import util
  8from .layouts import XAnchor, XDBox, XBox, XCurtain
  9from .label import XLabel
 10from .button import XButton
 11from .input import XInput
 12from .checkbox import XCheckBox
 13from .spinner import XSpinner
 14
 15
 16HEIGHT_UNIT = "40sp"
 17
 18
 19@dataclass
 20class XInputPanelWidget:
 21    """Dataclass to configure a specific `XInputPanel` widget."""
 22
 23    label: str
 24    """Label text."""
 25    widget: str = "str"
 26    """Widget type, one of `INPUT_WIDGET_TYPES`."""
 27    default: Any = None
 28    """Default value of the input widget."""
 29    orientation: str = "horizontal"
 30    """Orientation between label and input widget."""
 31    showing: bool = True
 32    """If widget should be showing."""
 33    label_hint: float = 1
 34    """Size hint of the label relative to the input widget."""
 35    italic: bool = True
 36    """Italicized label."""
 37    bold: bool = False
 38    """Enboldened label."""
 39    halign: Optional[str] = None
 40    """Label text horizontal alignment."""
 41    choices: list = field(default_factory=list)
 42    """Used by choice widgets."""
 43
 44
 45class XInputPanel(XDBox):
 46    """A widget containing arbitrary input widgets.
 47
 48    Intended for forms or configuration user input.
 49    """
 50
 51    reset_text = kv.StringProperty("Reset defaults")
 52    """Text for the reset button, leave empty to hide."""
 53    invoke_text = kv.StringProperty("Send")
 54    """Text to show on the invoke button, leave empty to hide."""
 55    fill_button = kv.BooleanProperty(False)
 56    """Fill reset or invoke button horizontally even if only one is visible."""
 57
 58    def __init__(
 59        self,
 60        widgets: dict[str, XInputPanelWidget],
 61        /,
 62        **kwargs,
 63    ):
 64        """Initialize the class.
 65
 66        Args:
 67            widgets: Dictionary of names to widgets.
 68        """
 69        kwargs = dict(padding=("10sp", 0)) | kwargs
 70        self._controls = controls = XBox()
 71        super().__init__(**kwargs)
 72        main_box = self
 73        self.widgets: dict[str, BaseInputWidget] = dict()
 74        self._curtains: dict[str, XCurtain] = dict()
 75        # Widgets
 76        self._reset_btn = XButton(text=self.reset_text, on_release=self.reset_defaults)
 77        self._invoke_btn = XButton(text=self.invoke_text, on_release=self._do_invoke)
 78        # Input Widgets
 79        for name, w in widgets.items():
 80            iw_cls = INPUT_WIDGET_CLASSES[w.widget]
 81            input_widget = iw_cls(w, self._do_values, self._do_invoke)
 82            curtain = XCurtain(content=input_widget, showing=w.showing)
 83            curtain.set_size(y=input_widget.height)
 84            self.widgets[name] = input_widget
 85            self._curtains[name] = curtain
 86            main_box.add_widget(curtain)
 87        # Controls
 88        controls = XBox()
 89        if self.reset_text:
 90            controls.add_widget(XAnchor.wrap(self._reset_btn, padding=("5dp", 0)))
 91        if self.invoke_text:
 92            controls.add_widget(XAnchor.wrap(self._invoke_btn, padding=("5dp", 0)))
 93        if len(controls.children) == 1:
 94            controls.set_size(hx=1 if self.fill_button else 0.5)
 95            controls = XAnchor.wrap(controls)
 96        controls.set_size(y=HEIGHT_UNIT)
 97        if len(controls.children) > 0:
 98            main_box.add_widget(controls)
 99        # Bindings
100        self.bind(
101            reset_text=self._on_reset_text,
102            invoke_text=self._on_invoke_text,
103        )
104        self.register_event_type("on_invoke")
105        self.register_event_type("on_values")
106
107    def on_fill_button(self, w, fill: bool):
108        """Adjust control buttons frame size hint."""
109        if len(self._controls.children) == 1:
110            self._controls.set_size(hx=1 if fill else 0.5)
111        else:
112            self._controls.set_size(hx=1)
113
114    def get_value(self, widget_name: str, /) -> Any:
115        """Get a value by name."""
116        widget = self.widgets[widget_name]
117        return widget.get_value()
118
119    def get_values(self) -> dict[str, Any]:
120        """Get all values."""
121        return {name: iw.get_value() for name, iw in self.widgets.items()}
122
123    def reset_defaults(self, *args, **kwargs):
124        """Reset all values to their defaults."""
125        for iw in self.widgets.values():
126            iw.set_value()
127
128    def set_focus(self, widget_name: str, /):
129        """Focus a widget by name."""
130        widget = self.widgets[widget_name]
131        widget.set_focus()
132
133    def set_enabled(self, widget_name: str, set_as: bool = True, /):
134        """Enable or disable a widget by name."""
135        widget = self.widgets[widget_name]
136        widget.set_enabled(set_as)
137
138    def set_showing(self, widget_name: str, set_as: bool = True, /):
139        """Show or hide a widget by name."""
140        curtain = self._curtains[widget_name]
141        curtain.showing = set_as
142
143    def on_invoke(self, values: dict):
144        """Triggered when the invoke button is pressed or otherwise sent by user."""
145        pass
146
147    def on_values(self, values: dict):
148        """Triggered when any of the values change."""
149        pass
150
151    def _do_invoke(self, *args):
152        self.dispatch("on_invoke", self.get_values())
153
154    def _do_values(self, *args):
155        self.dispatch("on_values", self.get_values())
156
157    def _on_reset_text(self, w, text):
158        self._reset_btn.text = text
159
160    def _on_invoke_text(self, w, text):
161        self._invoke_btn.text = text
162
163
164class BaseInputWidget(XBox):
165    def __init__(self, w: XInputPanelWidget, on_value: Callable, on_invoke: Callable):
166        assert w.widget == self.wtype
167        self.specification = w
168        super().__init__(orientation=w.orientation)
169        default_halign = "right" if w.orientation == "horizontal" else "center"
170        # Build
171        self.label = XLabel(
172            text=w.label,
173            padding=(10, 5),
174            italic=w.italic,
175            bold=w.bold,
176            halign=w.halign or default_halign,
177        )
178        self.widget = self._get_widget(w, on_value, on_invoke)
179        assert self.widget is not None
180        # Assemble
181        height = util.sp2pixels(HEIGHT_UNIT) * (1 + (w.orientation == "vertical"))
182        self.set_size(y=height)
183        self.label.set_size(hx=w.label_hint if w.orientation == "horizontal" else 1)
184        self.add_widgets(self.label, self.widget)
185
186    def set_enabled(self, set_as: Optional[bool] = None, /):
187        if set_as is None:
188            set_as = not self.label.disabled
189        self.label.disabled = set_as
190
191    def set_focus(self):
192        pass
193
194
195class StringInputWidget(BaseInputWidget):
196    wtype = "str"
197    _entry_class = XInput
198    _text_default = ""
199    _password = False
200
201    def _get_widget(
202        self,
203        w: XInputPanelWidget,
204        on_value: Callable,
205        on_invoke: Callable,
206    ):
207        self._entry = self._entry_class(
208            text=str(w.default or self._text_default),
209            password=self._password,
210            select_on_focus=True,
211        )
212        self._entry.bind(text=on_value, on_text_validate=on_invoke)
213        return self._entry
214
215    def get_value(self) -> str:
216        return self._entry.text
217
218    def set_value(self, value: Optional[str] = None, /):
219        if value is None:
220            value = self.specification.default or self._text_default
221        self._entry.text = value
222
223    def set_enabled(self, set_as: Optional[bool] = None, /):
224        super().set_enabled(set_as)
225        if set_as is None:
226            set_as = not self._entry.disabled
227        self._entry.disabled = set_as
228
229    def set_focus(self):
230        self._entry.focus = True
231        self._entry.select_all()
232
233
234class BooleanInputWidget(BaseInputWidget):
235    wtype = "bool"
236
237    def _get_widget(
238        self,
239        w: XInputPanelWidget,
240        on_value: Callable,
241        on_invoke: Callable,
242    ):
243        self._checkbox = XCheckBox(active=w.default or False)
244        self._checkbox.bind(active=on_value)
245        if w.orientation == "vertical":
246            return self._checkbox
247        self._checkbox.set_size(x=HEIGHT_UNIT)
248        frame = XAnchor(anchor_x="left")
249        frame.add_widget(self._checkbox)
250        return frame
251
252    def get_value(self) -> bool:
253        return self._checkbox.active
254
255    def set_value(self, value: Optional[bool] = None, /):
256        if value is None:
257            value = self.specification.default
258        self._checkbox.active = value
259
260    def set_enabled(self, set_as: Optional[bool] = None, /):
261        super().set_enabled(set_as)
262        if set_as is None:
263            set_as = not self._checkbox.disabled
264        self._checkbox.disabled = set_as
265
266    def set_focus(self):
267        self._checkbox.focus = True
268
269
270class IntInputWidget(StringInputWidget):
271    wtype = "int"
272    _entry_class = functools.partial(XInput, input_filter="int")
273    _text_default = "0"
274
275    def get_value(self) -> int:
276        return int(self._entry.text or 0)
277
278    def set_value(self, value: Optional[int] = None, /):
279        if value is None:
280            value = self.specification.default or 0
281        self._entry.text = str(value)
282
283
284class FloatInputWidget(StringInputWidget):
285    wtype = "float"
286    _entry_class = functools.partial(XInput, input_filter="float")
287    _text_default = "0"
288
289    def get_value(self) -> float:
290        return float(self._entry.text or 0)
291
292    def set_value(self, value: Optional[float] = None, /):
293        if value is None:
294            value = self.specification.default or 0
295        self._entry.text = str(value)
296
297
298class PasswordInputWidget(StringInputWidget):
299    wtype = "password"
300    _password = True
301
302
303class ChoiceInputWidget(BaseInputWidget):
304    wtype = "choice"
305
306    def _get_widget(
307        self,
308        w: XInputPanelWidget,
309        on_value: Callable,
310        on_invoke: Callable,
311    ):
312        self._spinner = XSpinner(
313            text=w.default or "",
314            values=w.choices,
315            text_autoupdate=True,
316        )
317        self._spinner.bind(text=on_value)
318        return self._spinner
319
320    def get_value(self) -> bool:
321        return self._spinner.text
322
323    def set_value(self, value: Optional[str] = None, /):
324        if value is None:
325            value = self.specification.default or ""
326        self._spinner.text = value
327
328    def set_enabled(self, set_as: Optional[bool] = None, /):
329        super().set_enabled(set_as)
330        if set_as is None:
331            set_as = not self._spinner.disabled
332        self._spinner.disabled = set_as
333
334
335INPUT_WIDGET_CLASSES: dict[str, BaseInputWidget] = dict(
336    str=StringInputWidget,
337    bool=BooleanInputWidget,
338    int=IntInputWidget,
339    float=FloatInputWidget,
340    password=PasswordInputWidget,
341    choice=ChoiceInputWidget,
342)
343INPUT_WIDGET_TYPES = tuple(INPUT_WIDGET_CLASSES.keys())
344"""Input widget types."""
345
346
347__all__ = (
348    "XInputPanel",
349    "XInputPanelWidget",
350    "INPUT_WIDGET_TYPES",
351)
class XInputPanel(kvex.widgets.layouts.XDBox):
 46class XInputPanel(XDBox):
 47    """A widget containing arbitrary input widgets.
 48
 49    Intended for forms or configuration user input.
 50    """
 51
 52    reset_text = kv.StringProperty("Reset defaults")
 53    """Text for the reset button, leave empty to hide."""
 54    invoke_text = kv.StringProperty("Send")
 55    """Text to show on the invoke button, leave empty to hide."""
 56    fill_button = kv.BooleanProperty(False)
 57    """Fill reset or invoke button horizontally even if only one is visible."""
 58
 59    def __init__(
 60        self,
 61        widgets: dict[str, XInputPanelWidget],
 62        /,
 63        **kwargs,
 64    ):
 65        """Initialize the class.
 66
 67        Args:
 68            widgets: Dictionary of names to widgets.
 69        """
 70        kwargs = dict(padding=("10sp", 0)) | kwargs
 71        self._controls = controls = XBox()
 72        super().__init__(**kwargs)
 73        main_box = self
 74        self.widgets: dict[str, BaseInputWidget] = dict()
 75        self._curtains: dict[str, XCurtain] = dict()
 76        # Widgets
 77        self._reset_btn = XButton(text=self.reset_text, on_release=self.reset_defaults)
 78        self._invoke_btn = XButton(text=self.invoke_text, on_release=self._do_invoke)
 79        # Input Widgets
 80        for name, w in widgets.items():
 81            iw_cls = INPUT_WIDGET_CLASSES[w.widget]
 82            input_widget = iw_cls(w, self._do_values, self._do_invoke)
 83            curtain = XCurtain(content=input_widget, showing=w.showing)
 84            curtain.set_size(y=input_widget.height)
 85            self.widgets[name] = input_widget
 86            self._curtains[name] = curtain
 87            main_box.add_widget(curtain)
 88        # Controls
 89        controls = XBox()
 90        if self.reset_text:
 91            controls.add_widget(XAnchor.wrap(self._reset_btn, padding=("5dp", 0)))
 92        if self.invoke_text:
 93            controls.add_widget(XAnchor.wrap(self._invoke_btn, padding=("5dp", 0)))
 94        if len(controls.children) == 1:
 95            controls.set_size(hx=1 if self.fill_button else 0.5)
 96            controls = XAnchor.wrap(controls)
 97        controls.set_size(y=HEIGHT_UNIT)
 98        if len(controls.children) > 0:
 99            main_box.add_widget(controls)
100        # Bindings
101        self.bind(
102            reset_text=self._on_reset_text,
103            invoke_text=self._on_invoke_text,
104        )
105        self.register_event_type("on_invoke")
106        self.register_event_type("on_values")
107
108    def on_fill_button(self, w, fill: bool):
109        """Adjust control buttons frame size hint."""
110        if len(self._controls.children) == 1:
111            self._controls.set_size(hx=1 if fill else 0.5)
112        else:
113            self._controls.set_size(hx=1)
114
115    def get_value(self, widget_name: str, /) -> Any:
116        """Get a value by name."""
117        widget = self.widgets[widget_name]
118        return widget.get_value()
119
120    def get_values(self) -> dict[str, Any]:
121        """Get all values."""
122        return {name: iw.get_value() for name, iw in self.widgets.items()}
123
124    def reset_defaults(self, *args, **kwargs):
125        """Reset all values to their defaults."""
126        for iw in self.widgets.values():
127            iw.set_value()
128
129    def set_focus(self, widget_name: str, /):
130        """Focus a widget by name."""
131        widget = self.widgets[widget_name]
132        widget.set_focus()
133
134    def set_enabled(self, widget_name: str, set_as: bool = True, /):
135        """Enable or disable a widget by name."""
136        widget = self.widgets[widget_name]
137        widget.set_enabled(set_as)
138
139    def set_showing(self, widget_name: str, set_as: bool = True, /):
140        """Show or hide a widget by name."""
141        curtain = self._curtains[widget_name]
142        curtain.showing = set_as
143
144    def on_invoke(self, values: dict):
145        """Triggered when the invoke button is pressed or otherwise sent by user."""
146        pass
147
148    def on_values(self, values: dict):
149        """Triggered when any of the values change."""
150        pass
151
152    def _do_invoke(self, *args):
153        self.dispatch("on_invoke", self.get_values())
154
155    def _do_values(self, *args):
156        self.dispatch("on_values", self.get_values())
157
158    def _on_reset_text(self, w, text):
159        self._reset_btn.text = text
160
161    def _on_invoke_text(self, w, text):
162        self._invoke_btn.text = text

A widget containing arbitrary input widgets.

Intended for forms or configuration user input.

XInputPanel( widgets: dict[str, kvex.widgets.inputpanel.XInputPanelWidget], /, **kwargs)
 59    def __init__(
 60        self,
 61        widgets: dict[str, XInputPanelWidget],
 62        /,
 63        **kwargs,
 64    ):
 65        """Initialize the class.
 66
 67        Args:
 68            widgets: Dictionary of names to widgets.
 69        """
 70        kwargs = dict(padding=("10sp", 0)) | kwargs
 71        self._controls = controls = XBox()
 72        super().__init__(**kwargs)
 73        main_box = self
 74        self.widgets: dict[str, BaseInputWidget] = dict()
 75        self._curtains: dict[str, XCurtain] = dict()
 76        # Widgets
 77        self._reset_btn = XButton(text=self.reset_text, on_release=self.reset_defaults)
 78        self._invoke_btn = XButton(text=self.invoke_text, on_release=self._do_invoke)
 79        # Input Widgets
 80        for name, w in widgets.items():
 81            iw_cls = INPUT_WIDGET_CLASSES[w.widget]
 82            input_widget = iw_cls(w, self._do_values, self._do_invoke)
 83            curtain = XCurtain(content=input_widget, showing=w.showing)
 84            curtain.set_size(y=input_widget.height)
 85            self.widgets[name] = input_widget
 86            self._curtains[name] = curtain
 87            main_box.add_widget(curtain)
 88        # Controls
 89        controls = XBox()
 90        if self.reset_text:
 91            controls.add_widget(XAnchor.wrap(self._reset_btn, padding=("5dp", 0)))
 92        if self.invoke_text:
 93            controls.add_widget(XAnchor.wrap(self._invoke_btn, padding=("5dp", 0)))
 94        if len(controls.children) == 1:
 95            controls.set_size(hx=1 if self.fill_button else 0.5)
 96            controls = XAnchor.wrap(controls)
 97        controls.set_size(y=HEIGHT_UNIT)
 98        if len(controls.children) > 0:
 99            main_box.add_widget(controls)
100        # Bindings
101        self.bind(
102            reset_text=self._on_reset_text,
103            invoke_text=self._on_invoke_text,
104        )
105        self.register_event_type("on_invoke")
106        self.register_event_type("on_values")

Initialize the class.

Arguments:
  • widgets: Dictionary of names to widgets.
reset_text

Text for the reset button, leave empty to hide.

invoke_text

Text to show on the invoke button, leave empty to hide.

fill_button

Fill reset or invoke button horizontally even if only one is visible.

def on_fill_button(self, w, fill: bool):
108    def on_fill_button(self, w, fill: bool):
109        """Adjust control buttons frame size hint."""
110        if len(self._controls.children) == 1:
111            self._controls.set_size(hx=1 if fill else 0.5)
112        else:
113            self._controls.set_size(hx=1)

Adjust control buttons frame size hint.

def get_value(self, widget_name: str, /) -> Any:
115    def get_value(self, widget_name: str, /) -> Any:
116        """Get a value by name."""
117        widget = self.widgets[widget_name]
118        return widget.get_value()

Get a value by name.

def get_values(self) -> dict[str, typing.Any]:
120    def get_values(self) -> dict[str, Any]:
121        """Get all values."""
122        return {name: iw.get_value() for name, iw in self.widgets.items()}

Get all values.

def reset_defaults(self, *args, **kwargs):
124    def reset_defaults(self, *args, **kwargs):
125        """Reset all values to their defaults."""
126        for iw in self.widgets.values():
127            iw.set_value()

Reset all values to their defaults.

def set_focus(self, widget_name: str, /):
129    def set_focus(self, widget_name: str, /):
130        """Focus a widget by name."""
131        widget = self.widgets[widget_name]
132        widget.set_focus()

Focus a widget by name.

def set_enabled(self, widget_name: str, set_as: bool = True, /):
134    def set_enabled(self, widget_name: str, set_as: bool = True, /):
135        """Enable or disable a widget by name."""
136        widget = self.widgets[widget_name]
137        widget.set_enabled(set_as)

Enable or disable a widget by name.

def set_showing(self, widget_name: str, set_as: bool = True, /):
139    def set_showing(self, widget_name: str, set_as: bool = True, /):
140        """Show or hide a widget by name."""
141        curtain = self._curtains[widget_name]
142        curtain.showing = set_as

Show or hide a widget by name.

def on_invoke(self, values: dict):
144    def on_invoke(self, values: dict):
145        """Triggered when the invoke button is pressed or otherwise sent by user."""
146        pass

Triggered when the invoke button is pressed or otherwise sent by user.

def on_values(self, values: dict):
148    def on_values(self, values: dict):
149        """Triggered when any of the values change."""
150        pass

Triggered when any of the values change.

Inherited Members
kivy.uix.gridlayout.GridLayout
spacing
padding
cols
rows
col_default_width
row_default_height
col_force_default
row_force_default
cols_minimum
rows_minimum
minimum_width
minimum_height
minimum_size
orientation
get_max_widgets
on_children
do_layout
kivy.uix.layout.Layout
remove_widget
layout_hint_with_bounds
kivy.uix.widget.Widget
proxy_ref
apply_class_lang_rules
collide_point
collide_widget
on_motion
on_touch_down
on_touch_move
on_touch_up
on_kv_post
clear_widgets
register_for_motion_event
unregister_for_motion_event
export_to_png
export_as_image
get_root_window
get_parent_window
walk
walk_reverse
to_widget
to_window
to_parent
to_local
get_window_matrix
x
y
width
height
pos
size
get_right
set_right
right
get_top
set_top
top
get_center_x
set_center_x
center_x
get_center_y
set_center_y
center_y
center
cls
children
parent
size_hint_x
size_hint_y
size_hint
pos_hint
size_hint_min_x
size_hint_min_y
size_hint_min
size_hint_max_x
size_hint_max_y
size_hint_max
ids
opacity
on_opacity
canvas
get_disabled
set_disabled
inc_disabled
dec_disabled
disabled
motion_filter
kivy._event.EventDispatcher
register_event_type
unregister_event_types
unregister_event_type
is_event_type
bind
unbind
fbind
funbind
unbind_uid
get_property_observers
events
dispatch
dispatch_generic
dispatch_children
setter
getter
property
properties
create_property
apply_property
@dataclass
class XInputPanelWidget:
20@dataclass
21class XInputPanelWidget:
22    """Dataclass to configure a specific `XInputPanel` widget."""
23
24    label: str
25    """Label text."""
26    widget: str = "str"
27    """Widget type, one of `INPUT_WIDGET_TYPES`."""
28    default: Any = None
29    """Default value of the input widget."""
30    orientation: str = "horizontal"
31    """Orientation between label and input widget."""
32    showing: bool = True
33    """If widget should be showing."""
34    label_hint: float = 1
35    """Size hint of the label relative to the input widget."""
36    italic: bool = True
37    """Italicized label."""
38    bold: bool = False
39    """Enboldened label."""
40    halign: Optional[str] = None
41    """Label text horizontal alignment."""
42    choices: list = field(default_factory=list)
43    """Used by choice widgets."""

Dataclass to configure a specific XInputPanel widget.

XInputPanelWidget( label: str, widget: str = 'str', default: Any = None, orientation: str = 'horizontal', showing: bool = True, label_hint: float = 1, italic: bool = True, bold: bool = False, halign: Optional[str] = None, choices: list = <factory>)
label: str

Label text.

widget: str = 'str'

Widget type, one of INPUT_WIDGET_TYPES.

default: Any = None

Default value of the input widget.

orientation: str = 'horizontal'

Orientation between label and input widget.

showing: bool = True

If widget should be showing.

label_hint: float = 1

Size hint of the label relative to the input widget.

italic: bool = True

Italicized label.

bold: bool = False

Enboldened label.

halign: Optional[str] = None

Label text horizontal alignment.

choices: list

Used by choice widgets.

INPUT_WIDGET_TYPES = ('str', 'bool', 'int', 'float', 'password', 'choice')

Input widget types.