Interface Management (1/2)

This commit is contained in:
RFNexus 2025-03-09 15:20:36 -04:00
parent 03d1b22b8f
commit c280e36a84
6 changed files with 3280 additions and 13 deletions

View file

@ -0,0 +1,283 @@
import urwid
class DialogLineBox(urwid.LineBox):
def __init__(self, body, parent=None, title="?"):
super().__init__(body, title=title)
self.parent = parent
def keypress(self, size, key):
if key == "esc":
if self.parent and hasattr(self.parent, "dismiss_dialog"):
self.parent.dismiss_dialog()
return None
return super().keypress(size, key)
class Placeholder(urwid.Edit):
def __init__(self, caption="", edit_text="", placeholder="", **kwargs):
super().__init__(caption, edit_text, **kwargs)
self.placeholder = placeholder
def render(self, size, focus=False):
if not self.edit_text and not focus:
placeholder_widget = urwid.Text(("placeholder", self.placeholder))
return placeholder_widget.render(size, focus)
else:
return super().render(size, focus)
class Dropdown(urwid.WidgetWrap):
signals = ['change'] # emit for urwid.connect_signal fn
def __init__(self, label, options, default=None):
self.label = label
self.options = options
self.selected = default if default is not None else options[0]
self.main_text = f"{self.selected}"
self.main_button = urwid.SelectableIcon(self.main_text, 0)
self.main_button = urwid.AttrMap(self.main_button, "button_normal", "button_focus")
self.option_widgets = []
for opt in options:
icon = urwid.SelectableIcon(opt, 0)
icon = urwid.AttrMap(icon, "list_normal", "list_focus")
self.option_widgets.append(icon)
self.options_walker = urwid.SimpleFocusListWalker(self.option_widgets)
self.options_listbox = urwid.ListBox(self.options_walker)
self.dropdown_box = None # will be created on open_dropdown
self.pile = urwid.Pile([self.main_button])
self.dropdown_visible = False
super().__init__(self.pile)
def open_dropdown(self):
if not self.dropdown_visible:
height = len(self.options)
self.dropdown_box = urwid.BoxAdapter(self.options_listbox, height)
self.pile.contents.append((self.dropdown_box, self.pile.options()))
self.dropdown_visible = True
self.pile.focus_position = 1
self.options_walker.set_focus(0)
def close_dropdown(self):
if self.dropdown_visible:
self.pile.contents.pop() # remove the dropdown_box
self.dropdown_visible = False
self.pile.focus_position = 0
self.dropdown_box = None
def keypress(self, size, key):
if not self.dropdown_visible:
if key == "enter":
self.open_dropdown()
return None
return self.main_button.keypress(size, key)
else:
if key == "enter":
focus_result = self.options_walker.get_focus()
if focus_result is not None:
focus_widget = focus_result[0]
new_val = focus_widget.base_widget.text
old_val = self.selected
self.selected = new_val
self.main_button.base_widget.set_text(f"{self.selected}")
if old_val != new_val:
self._emit('change', new_val)
self.close_dropdown()
return None
return self.dropdown_box.keypress(size, key)
def get_value(self):
return self.selected
class ValidationError(urwid.Text):
def __init__(self, message=""):
super().__init__(("error", message))
class FormField:
def __init__(self, config_key, transform=None):
self.config_key = config_key
self.transform = transform or (lambda x: x)
class FormEdit(Placeholder, FormField):
def __init__(self, config_key, caption="", edit_text="", placeholder="", validation_types=None, transform=None, **kwargs):
Placeholder.__init__(self, caption, edit_text, placeholder, **kwargs)
FormField.__init__(self, config_key, transform)
self.validation_types = validation_types or []
self.error_widget = urwid.Text("")
self.error = None
def get_value(self):
return self.transform(self.edit_text.strip())
def validate(self):
value = self.edit_text.strip()
self.error = None
for validation in self.validation_types:
if validation == "required":
if not value:
self.error = "This field is required"
break
elif validation == "number":
if value and not value.replace('-', '').replace('.', '').isdigit():
self.error = "This field must be a number"
break
elif validation == "float":
try:
if value:
float(value)
except ValueError:
self.error = "This field must be decimal number"
break
self.error_widget.set_text(("error", self.error or ""))
return self.error is None
class FormCheckbox(urwid.CheckBox, FormField):
def __init__(self, config_key, label="", state=False, validation_types=None, transform=None, **kwargs):
urwid.CheckBox.__init__(self, label, state, **kwargs)
FormField.__init__(self, config_key, transform)
self.validation_types = validation_types or []
self.error_widget = urwid.Text("")
self.error = None
def get_value(self):
return self.transform(self.get_state())
def validate(self):
value = self.get_state()
self.error = None
for validation in self.validation_types:
if validation == "required":
if not value:
self.error = "This field is required"
break
self.error_widget.set_text(("error", self.error or ""))
return self.error is None
class FormDropdown(Dropdown, FormField):
signals = ['change']
def __init__(self, config_key, label, options, default=None, validation_types=None, transform=None):
self.options = [str(opt) for opt in options]
if default is not None:
default_str = str(default)
if default_str in self.options:
default = default_str
elif transform:
try:
default_transformed = transform(default_str)
for opt in self.options:
if transform(opt) == default_transformed:
default = opt
break
except:
default = self.options[0]
else:
default = self.options[0]
else:
default = self.options[0]
Dropdown.__init__(self, label, self.options, default)
FormField.__init__(self, config_key, transform)
self.validation_types = validation_types or []
self.error_widget = urwid.Text("")
self.error = None
if hasattr(self, 'main_button'):
self.main_button.base_widget.set_text(str(default))
def get_value(self):
return self.transform(self.selected)
def validate(self):
value = self.get_value()
self.error = None
for validation in self.validation_types:
if validation == "required":
if not value:
self.error = "This field is required"
break
self.error_widget.set_text(("error", self.error or ""))
return self.error is None
def open_dropdown(self):
if not self.dropdown_visible:
super().open_dropdown()
try:
current_index = self.options.index(self.selected)
self.options_walker.set_focus(current_index)
except ValueError:
pass
class FormMultiList(urwid.Pile, FormField):
def __init__(self, config_key, placeholder="", validation_types=None, transform=None, **kwargs):
self.entries = []
self.error_widget = urwid.Text("")
self.error = None
self.placeholder = placeholder
self.validation_types = validation_types or []
first_entry = self.create_entry_row()
self.entries.append(first_entry)
self.add_button = urwid.Button("+ Add Another", on_press=self.add_entry)
add_button_padded = urwid.Padding(self.add_button, left=2, right=2)
pile_widgets = [first_entry, add_button_padded]
urwid.Pile.__init__(self, pile_widgets)
FormField.__init__(self, config_key, transform)
def create_entry_row(self):
edit = urwid.Edit("", "")
entry_row = urwid.Columns([
('weight', 1, edit),
(3, urwid.Button("×", on_press=lambda button: self.remove_entry(button, entry_row))),
])
return entry_row
def remove_entry(self, button, entry_row):
if len(self.entries) > 1:
self.entries.remove(entry_row)
self.contents = [(w, self.options()) for w in self.get_pile_widgets()]
def add_entry(self, button):
new_entry = self.create_entry_row()
self.entries.append(new_entry)
self.contents = [(w, self.options()) for w in self.get_pile_widgets()]
def get_pile_widgets(self):
return self.entries + [urwid.Padding(self.add_button, left=2, right=2)]
def get_value(self):
values = []
for entry in self.entries:
edit_widget = entry.contents[0][0]
value = edit_widget.edit_text.strip()
if value:
values.append(value)
return self.transform(values)
def validate(self):
values = self.get_value()
self.error = None
for validation in self.validation_types:
if validation == "required" and not values:
self.error = "At least one entry is required"
break
self.error_widget.set_text(("error", self.error or ""))
return self.error is None