mirror of
https://github.com/markqvist/NomadNet.git
synced 2025-05-17 13:20:25 -04:00
Interface Management (1/2)
This commit is contained in:
parent
03d1b22b8f
commit
c280e36a84
6 changed files with 3280 additions and 13 deletions
|
@ -51,7 +51,15 @@ THEMES = {
|
||||||
("browser_controls", "light gray", "default", "default", "#bbb", "default"),
|
("browser_controls", "light gray", "default", "default", "#bbb", "default"),
|
||||||
("progress_full", "black", "light gray", "standout", "#111", "#bbb"),
|
("progress_full", "black", "light gray", "standout", "#111", "#bbb"),
|
||||||
("progress_empty", "light gray", "default", "default", "#ddd", "default"),
|
("progress_empty", "light gray", "default", "default", "#ddd", "default"),
|
||||||
],
|
("interface_title", "", "", "default", "", ""),
|
||||||
|
("interface_title_selected", "bold", "", "bold", "", ""),
|
||||||
|
("connected_status", "dark green", "default", "default", "dark green", "default"),
|
||||||
|
("disconnected_status", "dark red", "default", "default", "dark red", "default"),
|
||||||
|
("placeholder", "dark gray", "default", "default", "dark gray", "default"),
|
||||||
|
("placeholder_text", "dark gray", "default", "default", "dark gray", "default"),
|
||||||
|
("error", "light red,blink", "default", "blink", "#f44,blink", "default"),
|
||||||
|
|
||||||
|
],
|
||||||
},
|
},
|
||||||
THEME_LIGHT: {
|
THEME_LIGHT: {
|
||||||
"urwid_theme": [
|
"urwid_theme": [
|
||||||
|
@ -69,6 +77,7 @@ THEMES = {
|
||||||
("msg_header_ok", "black", "dark green", "standout", "#111", "#6b2"),
|
("msg_header_ok", "black", "dark green", "standout", "#111", "#6b2"),
|
||||||
("msg_header_caution", "black", "yellow", "standout", "#111", "#fd3"),
|
("msg_header_caution", "black", "yellow", "standout", "#111", "#fd3"),
|
||||||
("msg_header_sent", "black", "dark gray", "standout", "#111", "#ddd"),
|
("msg_header_sent", "black", "dark gray", "standout", "#111", "#ddd"),
|
||||||
|
("msg_header_propagated", "black", "light blue", "standout", "#111", "#28b"),
|
||||||
("msg_header_delivered", "black", "light blue", "standout", "#111", "#28b"),
|
("msg_header_delivered", "black", "light blue", "standout", "#111", "#28b"),
|
||||||
("msg_header_failed", "black", "dark gray", "standout", "#000", "#777"),
|
("msg_header_failed", "black", "dark gray", "standout", "#000", "#777"),
|
||||||
("msg_warning_untrusted", "black", "dark red", "standout", "#111", "dark red"),
|
("msg_warning_untrusted", "black", "dark red", "standout", "#111", "dark red"),
|
||||||
|
@ -86,6 +95,13 @@ THEMES = {
|
||||||
("browser_controls", "dark gray", "default", "default", "#444", "default"),
|
("browser_controls", "dark gray", "default", "default", "#444", "default"),
|
||||||
("progress_full", "black", "dark gray", "standout", "#111", "#bbb"),
|
("progress_full", "black", "dark gray", "standout", "#111", "#bbb"),
|
||||||
("progress_empty", "dark gray", "default", "default", "#ddd", "default"),
|
("progress_empty", "dark gray", "default", "default", "#ddd", "default"),
|
||||||
|
("interface_title", "dark gray", "default", "default", "#444", "default"),
|
||||||
|
("interface_title_selected", "dark gray,bold", "default", "bold", "#444,bold", "default"),
|
||||||
|
("connected_status", "dark green", "default", "default", "#4a0", "default"),
|
||||||
|
("disconnected_status", "dark red", "default", "default", "#a22", "default"),
|
||||||
|
("placeholder", "light gray", "default", "default", "#999", "default"),
|
||||||
|
("placeholder_text", "light gray", "default", "default", "#999", "default"),
|
||||||
|
("error", "dark red,blink", "default", "blink", "#a22,blink", "default"),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,6 +144,8 @@ GLYPHS = {
|
||||||
("sent", "/\\", "\u2191", "\U000f0cd8"),
|
("sent", "/\\", "\u2191", "\U000f0cd8"),
|
||||||
("papermsg", "P", "\u25a4", "\uf719"),
|
("papermsg", "P", "\u25a4", "\uf719"),
|
||||||
("qrcode", "QR", "\u25a4", "\uf029"),
|
("qrcode", "QR", "\u25a4", "\uf029"),
|
||||||
|
("selected", "[*] ", "\u25CF", "\u25CF"),
|
||||||
|
("unselected", "[ ] ", "\u25CB", "\u25CB"),
|
||||||
}
|
}
|
||||||
|
|
||||||
class TextUI:
|
class TextUI:
|
||||||
|
@ -163,7 +181,7 @@ class TextUI:
|
||||||
|
|
||||||
if self.app.config["textui"]["glyphs"] == "plain":
|
if self.app.config["textui"]["glyphs"] == "plain":
|
||||||
glyphset = "plain"
|
glyphset = "plain"
|
||||||
elif self.app.config["textui"]["glyphs"] == "unicoode":
|
elif self.app.config["textui"]["glyphs"] == "unicode":
|
||||||
glyphset = "unicode"
|
glyphset = "unicode"
|
||||||
elif self.app.config["textui"]["glyphs"] == "nerdfont":
|
elif self.app.config["textui"]["glyphs"] == "nerdfont":
|
||||||
glyphset = "nerdfont"
|
glyphset = "nerdfont"
|
||||||
|
|
|
@ -109,6 +109,7 @@ class TopicList(urwid.WidgetWrap):
|
||||||
self.topic_list = [
|
self.topic_list = [
|
||||||
GuideEntry(self.app, self, guide_display, "Introduction"),
|
GuideEntry(self.app, self, guide_display, "Introduction"),
|
||||||
GuideEntry(self.app, self, guide_display, "Concepts & Terminology"),
|
GuideEntry(self.app, self, guide_display, "Concepts & Terminology"),
|
||||||
|
GuideEntry(self.app, self, guide_display, "Interfaces"),
|
||||||
GuideEntry(self.app, self, guide_display, "Hosting a Node"),
|
GuideEntry(self.app, self, guide_display, "Hosting a Node"),
|
||||||
GuideEntry(self.app, self, guide_display, "Configuration Options"),
|
GuideEntry(self.app, self, guide_display, "Configuration Options"),
|
||||||
GuideEntry(self.app, self, guide_display, "Keyboard Shortcuts"),
|
GuideEntry(self.app, self, guide_display, "Keyboard Shortcuts"),
|
||||||
|
@ -386,6 +387,10 @@ Links can be inserted into micron documents. See the `*Markup`* section of this
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
TOPIC_INTERFACES = '''
|
||||||
|
>TODO
|
||||||
|
'''
|
||||||
|
|
||||||
TOPIC_CONVERSATIONS = '''>Conversations
|
TOPIC_CONVERSATIONS = '''>Conversations
|
||||||
|
|
||||||
Conversations in Nomad Network
|
Conversations in Nomad Network
|
||||||
|
@ -1247,6 +1252,7 @@ TOPICS = {
|
||||||
"Introduction": TOPIC_INTRODUCTION,
|
"Introduction": TOPIC_INTRODUCTION,
|
||||||
"Concepts & Terminology": TOPIC_CONCEPTS,
|
"Concepts & Terminology": TOPIC_CONCEPTS,
|
||||||
"Conversations": TOPIC_CONVERSATIONS,
|
"Conversations": TOPIC_CONVERSATIONS,
|
||||||
|
"Interfaces": TOPIC_INTERFACES,
|
||||||
"Hosting a Node": TOPIC_HOSTING,
|
"Hosting a Node": TOPIC_HOSTING,
|
||||||
"Configuration Options": TOPIC_CONFIG,
|
"Configuration Options": TOPIC_CONFIG,
|
||||||
"Keyboard Shortcuts": TOPIC_SHORTCUTS,
|
"Keyboard Shortcuts": TOPIC_SHORTCUTS,
|
||||||
|
|
2865
nomadnet/ui/textui/Interfaces.py
Normal file
2865
nomadnet/ui/textui/Interfaces.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -4,6 +4,7 @@ from .Network import *
|
||||||
from .Conversations import *
|
from .Conversations import *
|
||||||
from .Directory import *
|
from .Directory import *
|
||||||
from .Config import *
|
from .Config import *
|
||||||
|
from .Interfaces import *
|
||||||
from .Map import *
|
from .Map import *
|
||||||
from .Log import *
|
from .Log import *
|
||||||
from .Guide import *
|
from .Guide import *
|
||||||
|
@ -16,6 +17,7 @@ class SubDisplays():
|
||||||
self.conversations_display = ConversationsDisplay(self.app)
|
self.conversations_display = ConversationsDisplay(self.app)
|
||||||
self.directory_display = DirectoryDisplay(self.app)
|
self.directory_display = DirectoryDisplay(self.app)
|
||||||
self.config_display = ConfigDisplay(self.app)
|
self.config_display = ConfigDisplay(self.app)
|
||||||
|
self.interface_display = InterfaceDisplay(self.app)
|
||||||
self.map_display = MapDisplay(self.app)
|
self.map_display = MapDisplay(self.app)
|
||||||
self.log_display = LogDisplay(self.app)
|
self.log_display = LogDisplay(self.app)
|
||||||
self.guide_display = GuideDisplay(self.app)
|
self.guide_display = GuideDisplay(self.app)
|
||||||
|
@ -113,6 +115,11 @@ class MainDisplay():
|
||||||
self.sub_displays.active_display = self.sub_displays.config_display
|
self.sub_displays.active_display = self.sub_displays.config_display
|
||||||
self.update_active_sub_display()
|
self.update_active_sub_display()
|
||||||
|
|
||||||
|
def show_interfaces(self, user_data):
|
||||||
|
self.sub_displays.active_display = self.sub_displays.interface_display
|
||||||
|
self.update_active_sub_display()
|
||||||
|
self.sub_displays.interface_display.start()
|
||||||
|
|
||||||
def show_log(self, user_data):
|
def show_log(self, user_data):
|
||||||
self.sub_displays.active_display = self.sub_displays.log_display
|
self.sub_displays.active_display = self.sub_displays.log_display
|
||||||
self.sub_displays.log_display.show()
|
self.sub_displays.log_display.show()
|
||||||
|
@ -171,21 +178,22 @@ class MenuDisplay():
|
||||||
|
|
||||||
self.menu_indicator = urwid.Text("")
|
self.menu_indicator = urwid.Text("")
|
||||||
|
|
||||||
menu_text = (urwid.PACK, self.menu_indicator)
|
menu_text = (urwid.PACK, self.menu_indicator)
|
||||||
button_network = (11, MenuButton("Network", on_press=handler.show_network))
|
button_network = (11, MenuButton("Network", on_press=handler.show_network))
|
||||||
button_conversations = (17, MenuButton("Conversations", on_press=handler.show_conversations))
|
button_conversations = (17, MenuButton("Conversations", on_press=handler.show_conversations))
|
||||||
button_directory = (13, MenuButton("Directory", on_press=handler.show_directory))
|
button_directory = (13, MenuButton("Directory", on_press=handler.show_directory))
|
||||||
button_map = (7, MenuButton("Map", on_press=handler.show_map))
|
button_map = (7, MenuButton("Map", on_press=handler.show_map))
|
||||||
button_log = (7, MenuButton("Log", on_press=handler.show_log))
|
button_log = (7, MenuButton("Log", on_press=handler.show_log))
|
||||||
button_config = (10, MenuButton("Config", on_press=handler.show_config))
|
button_config = (10, MenuButton("Config", on_press=handler.show_config))
|
||||||
button_guide = (9, MenuButton("Guide", on_press=handler.show_guide))
|
button_interfaces = (14, MenuButton("Interfaces", on_press=handler.show_interfaces))
|
||||||
button_quit = (8, MenuButton("Quit", on_press=handler.quit))
|
button_guide = (9, MenuButton("Guide", on_press=handler.show_guide))
|
||||||
|
button_quit = (8, MenuButton("Quit", on_press=handler.quit))
|
||||||
|
|
||||||
# buttons = [menu_text, button_conversations, button_node, button_directory, button_map]
|
# buttons = [menu_text, button_conversations, button_node, button_directory, button_map]
|
||||||
if self.app.config["textui"]["hide_guide"]:
|
if self.app.config["textui"]["hide_guide"]:
|
||||||
buttons = [menu_text, button_conversations, button_network, button_log, button_config, button_quit]
|
buttons = [menu_text, button_conversations, button_network, button_log, button_interfaces, button_config, button_quit]
|
||||||
else:
|
else:
|
||||||
buttons = [menu_text, button_conversations, button_network, button_log, button_config, button_guide, button_quit]
|
buttons = [menu_text, button_conversations, button_network, button_log, button_interfaces, button_config, button_guide, button_quit]
|
||||||
|
|
||||||
columns = MenuColumns(buttons, dividechars=1)
|
columns = MenuColumns(buttons, dividechars=1)
|
||||||
columns.handler = handler
|
columns.handler = handler
|
||||||
|
|
87
nomadnet/vendor/AsciiChart.py
vendored
Normal file
87
nomadnet/vendor/AsciiChart.py
vendored
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
from __future__ import division
|
||||||
|
from math import ceil, floor, isnan
|
||||||
|
# Derived from asciichartpy | https://github.com/kroitor/asciichart/blob/master/asciichartpy/__init__.py
|
||||||
|
class AsciiChart:
|
||||||
|
def __init__(self, glyphset="unicode"):
|
||||||
|
self.symbols = ['┼', '┤', '╶', '╴', '─', '╰', '╭', '╮', '╯', '│']
|
||||||
|
if glyphset == "plain":
|
||||||
|
self.symbols = ['+', '|', '-', '-', '-', '\'', ',', '.', '`', '|']
|
||||||
|
def plot(self, series, cfg=None):
|
||||||
|
if len(series) == 0:
|
||||||
|
return ''
|
||||||
|
if not isinstance(series[0], list):
|
||||||
|
if all(isnan(n) for n in series):
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
series = [series]
|
||||||
|
cfg = cfg or {}
|
||||||
|
minimum = cfg.get('min', min(filter(lambda n: not isnan(n), [j for i in series for j in i])))
|
||||||
|
maximum = cfg.get('max', max(filter(lambda n: not isnan(n), [j for i in series for j in i])))
|
||||||
|
symbols = cfg.get('symbols', self.symbols)
|
||||||
|
if minimum > maximum:
|
||||||
|
raise ValueError('The min value cannot exceed the max value.')
|
||||||
|
interval = maximum - minimum
|
||||||
|
offset = cfg.get('offset', 3)
|
||||||
|
height = cfg.get('height', interval)
|
||||||
|
ratio = height / interval if interval > 0 else 1
|
||||||
|
|
||||||
|
min2 = int(floor(minimum * ratio))
|
||||||
|
max2 = int(ceil(maximum * ratio))
|
||||||
|
|
||||||
|
def clamp(n):
|
||||||
|
return min(max(n, minimum), maximum)
|
||||||
|
|
||||||
|
def scaled(y):
|
||||||
|
return int(round(clamp(y) * ratio) - min2)
|
||||||
|
|
||||||
|
rows = max2 - min2
|
||||||
|
|
||||||
|
width = 0
|
||||||
|
for i in range(0, len(series)):
|
||||||
|
width = max(width, len(series[i]))
|
||||||
|
width += offset
|
||||||
|
|
||||||
|
placeholder = cfg.get('format', '{:8.2f} ')
|
||||||
|
|
||||||
|
result = [[' '] * width for i in range(rows + 1)]
|
||||||
|
|
||||||
|
for y in range(min2, max2 + 1):
|
||||||
|
label = placeholder.format(maximum - ((y - min2) * interval / (rows if rows else 1)))
|
||||||
|
result[y - min2][max(offset - len(label), 0)] = label
|
||||||
|
result[y - min2][offset - 1] = symbols[0] if y == 0 else symbols[1]
|
||||||
|
|
||||||
|
d0 = series[0][0]
|
||||||
|
if not isnan(d0):
|
||||||
|
result[rows - scaled(d0)][offset - 1] = symbols[0]
|
||||||
|
|
||||||
|
for i in range(0, len(series)):
|
||||||
|
for x in range(0, len(series[i]) - 1):
|
||||||
|
d0 = series[i][x + 0]
|
||||||
|
d1 = series[i][x + 1]
|
||||||
|
|
||||||
|
if isnan(d0) and isnan(d1):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isnan(d0) and not isnan(d1):
|
||||||
|
result[rows - scaled(d1)][x + offset] = symbols[2]
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not isnan(d0) and isnan(d1):
|
||||||
|
result[rows - scaled(d0)][x + offset] = symbols[3]
|
||||||
|
continue
|
||||||
|
|
||||||
|
y0 = scaled(d0)
|
||||||
|
y1 = scaled(d1)
|
||||||
|
if y0 == y1:
|
||||||
|
result[rows - y0][x + offset] = symbols[4]
|
||||||
|
continue
|
||||||
|
|
||||||
|
result[rows - y1][x + offset] = symbols[5] if y0 > y1 else symbols[6]
|
||||||
|
result[rows - y0][x + offset] = symbols[7] if y0 > y1 else symbols[8]
|
||||||
|
|
||||||
|
start = min(y0, y1) + 1
|
||||||
|
end = max(y0, y1)
|
||||||
|
for y in range(start, end):
|
||||||
|
result[rows - y][x + offset] = symbols[9]
|
||||||
|
|
||||||
|
return '\n'.join([''.join(row).rstrip() for row in result])
|
283
nomadnet/vendor/additional_urwid_widgets/FormWidgets.py
vendored
Normal file
283
nomadnet/vendor/additional_urwid_widgets/FormWidgets.py
vendored
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue