FormWidgets.py
1 import urwid 2 3 class DialogLineBox(urwid.LineBox): 4 def __init__(self, body, parent=None, title="?"): 5 super().__init__(body, title=title) 6 self.parent = parent 7 8 def keypress(self, size, key): 9 if key == "esc": 10 if self.parent and hasattr(self.parent, "dismiss_dialog"): 11 self.parent.dismiss_dialog() 12 return None 13 return super().keypress(size, key) 14 15 class Placeholder(urwid.Edit): 16 def __init__(self, caption="", edit_text="", placeholder="", **kwargs): 17 super().__init__(caption, edit_text, **kwargs) 18 self.placeholder = placeholder 19 20 def render(self, size, focus=False): 21 if not self.edit_text and not focus: 22 placeholder_widget = urwid.Text(("placeholder", self.placeholder)) 23 return placeholder_widget.render(size, focus) 24 else: 25 return super().render(size, focus) 26 27 class Dropdown(urwid.WidgetWrap): 28 signals = ['change'] # emit for urwid.connect_signal fn 29 30 def __init__(self, label, options, default=None): 31 self.label = label 32 self.options = options 33 self.selected = default if default is not None else options[0] 34 35 self.main_text = f"{self.selected}" 36 self.main_button = urwid.SelectableIcon(self.main_text, 0) 37 self.main_button = urwid.AttrMap(self.main_button, "button_normal", "button_focus") 38 39 self.option_widgets = [] 40 for opt in options: 41 icon = urwid.SelectableIcon(opt, 0) 42 icon = urwid.AttrMap(icon, "list_normal", "list_focus") 43 self.option_widgets.append(icon) 44 45 self.options_walker = urwid.SimpleFocusListWalker(self.option_widgets) 46 self.options_listbox = urwid.ListBox(self.options_walker) 47 self.dropdown_box = None # will be created on open_dropdown 48 49 self.pile = urwid.Pile([self.main_button]) 50 self.dropdown_visible = False 51 52 super().__init__(self.pile) 53 54 def open_dropdown(self): 55 if not self.dropdown_visible: 56 height = len(self.options) 57 self.dropdown_box = urwid.BoxAdapter(self.options_listbox, height) 58 self.pile.contents.append((self.dropdown_box, self.pile.options())) 59 self.dropdown_visible = True 60 self.pile.focus_position = 1 61 self.options_walker.set_focus(0) 62 63 def close_dropdown(self): 64 if self.dropdown_visible: 65 self.pile.contents.pop() # remove the dropdown_box 66 self.dropdown_visible = False 67 self.pile.focus_position = 0 68 self.dropdown_box = None 69 70 def keypress(self, size, key): 71 if not self.dropdown_visible: 72 if key == "enter": 73 self.open_dropdown() 74 return None 75 return self.main_button.keypress(size, key) 76 else: 77 if key == "enter": 78 focus_result = self.options_walker.get_focus() 79 if focus_result is not None: 80 focus_widget = focus_result[0] 81 new_val = focus_widget.base_widget.text 82 old_val = self.selected 83 self.selected = new_val 84 self.main_button.base_widget.set_text(f"{self.selected}") 85 86 if old_val != new_val: 87 self._emit('change', new_val) 88 89 self.close_dropdown() 90 return None 91 return self.dropdown_box.keypress(size, key) 92 93 def get_value(self): 94 return self.selected 95 96 class ValidationError(urwid.Text): 97 def __init__(self, message=""): 98 super().__init__(("error", message)) 99 100 class FormField: 101 def __init__(self, config_key, transform=None): 102 self.config_key = config_key 103 self.transform = transform or (lambda x: x) 104 105 class FormEdit(Placeholder, FormField): 106 def __init__(self, config_key, caption="", edit_text="", placeholder="", validation_types=None, transform=None, **kwargs): 107 Placeholder.__init__(self, caption, edit_text, placeholder, **kwargs) 108 FormField.__init__(self, config_key, transform) 109 self.validation_types = validation_types or [] 110 self.error_widget = urwid.Text("") 111 self.error = None 112 113 def get_value(self): 114 return self.transform(self.edit_text.strip()) 115 116 def validate(self): 117 value = self.edit_text.strip() 118 self.error = None 119 120 for validation in self.validation_types: 121 if validation == "required": 122 if not value: 123 self.error = "This field is required" 124 break 125 elif validation == "number": 126 if value and not value.replace('-', '').replace('.', '').isdigit(): 127 self.error = "This field must be a number" 128 break 129 elif validation == "float": 130 try: 131 if value: 132 float(value) 133 except ValueError: 134 self.error = "This field must be decimal number" 135 break 136 137 self.error_widget.set_text(("error", self.error or "")) 138 return self.error is None 139 140 class FormCheckbox(urwid.CheckBox, FormField): 141 def __init__(self, config_key, label="", state=False, validation_types=None, transform=None, **kwargs): 142 urwid.CheckBox.__init__(self, label, state, **kwargs) 143 FormField.__init__(self, config_key, transform) 144 self.validation_types = validation_types or [] 145 self.error_widget = urwid.Text("") 146 self.error = None 147 148 def get_value(self): 149 return self.transform(self.get_state()) 150 151 def validate(self): 152 153 value = self.get_state() 154 self.error = None 155 156 for validation in self.validation_types: 157 if validation == "required": 158 if not value: 159 self.error = "This field is required" 160 break 161 162 self.error_widget.set_text(("error", self.error or "")) 163 return self.error is None 164 165 class FormDropdown(Dropdown, FormField): 166 signals = ['change'] 167 168 def __init__(self, config_key, label, options, default=None, validation_types=None, transform=None): 169 self.options = [str(opt) for opt in options] 170 171 if default is not None: 172 default_str = str(default) 173 if default_str in self.options: 174 default = default_str 175 elif transform: 176 try: 177 default_transformed = transform(default_str) 178 for opt in self.options: 179 if transform(opt) == default_transformed: 180 default = opt 181 break 182 except: 183 default = self.options[0] 184 else: 185 default = self.options[0] 186 else: 187 default = self.options[0] 188 189 Dropdown.__init__(self, label, self.options, default) 190 FormField.__init__(self, config_key, transform) 191 192 self.validation_types = validation_types or [] 193 self.error_widget = urwid.Text("") 194 self.error = None 195 196 if hasattr(self, 'main_button'): 197 self.main_button.base_widget.set_text(str(default)) 198 199 def get_value(self): 200 return self.transform(self.selected) 201 202 def validate(self): 203 value = self.get_value() 204 self.error = None 205 206 for validation in self.validation_types: 207 if validation == "required": 208 if not value: 209 self.error = "This field is required" 210 break 211 212 self.error_widget.set_text(("error", self.error or "")) 213 return self.error is None 214 215 def open_dropdown(self): 216 if not self.dropdown_visible: 217 super().open_dropdown() 218 try: 219 current_index = self.options.index(self.selected) 220 self.options_walker.set_focus(current_index) 221 except ValueError: 222 pass 223 224 class FormMultiList(urwid.Pile, FormField): 225 def __init__(self, config_key, placeholder="", validation_types=None, transform=None, **kwargs): 226 self.entries = [] 227 self.error_widget = urwid.Text("") 228 self.error = None 229 self.placeholder = placeholder 230 self.validation_types = validation_types or [] 231 232 first_entry = self.create_entry_row() 233 self.entries.append(first_entry) 234 235 self.add_button = urwid.Button("+ Add Another", on_press=self.add_entry) 236 add_button_padded = urwid.Padding(self.add_button, left=2, right=2) 237 238 pile_widgets = [first_entry, add_button_padded] 239 urwid.Pile.__init__(self, pile_widgets) 240 FormField.__init__(self, config_key, transform) 241 242 def create_entry_row(self): 243 edit = urwid.Edit("", "") 244 entry_row = urwid.Columns([ 245 ('weight', 1, edit), 246 (3, urwid.Button("×", on_press=lambda button: self.remove_entry(button, entry_row))), 247 ]) 248 return entry_row 249 250 def remove_entry(self, button, entry_row): 251 if len(self.entries) > 1: 252 self.entries.remove(entry_row) 253 self.contents = [(w, self.options()) for w in self.get_pile_widgets()] 254 255 def add_entry(self, button): 256 new_entry = self.create_entry_row() 257 self.entries.append(new_entry) 258 259 self.contents = [(w, self.options()) for w in self.get_pile_widgets()] 260 261 def get_pile_widgets(self): 262 return self.entries + [urwid.Padding(self.add_button, left=2, right=2)] 263 264 def get_value(self): 265 values = [] 266 for entry in self.entries: 267 edit_widget = entry.contents[0][0] 268 value = edit_widget.edit_text.strip() 269 if value: 270 values.append(value) 271 return self.transform(values) 272 273 def validate(self): 274 values = self.get_value() 275 self.error = None 276 277 for validation in self.validation_types: 278 if validation == "required" and not values: 279 self.error = "At least one entry is required" 280 break 281 282 self.error_widget.set_text(("error", self.error or "")) 283 return self.error is None 284 285 286 class FormMultiTable(urwid.Pile, FormField): 287 def __init__(self, config_key, fields, validation_types=None, transform=None, **kwargs): 288 self.entries = [] 289 self.fields = fields 290 self.error_widget = urwid.Text("") 291 self.error = None 292 self.validation_types = validation_types or [] 293 294 header_columns = [('weight', 3, urwid.Text(("list_focus", "Name")))] 295 for field_key, field_config in self.fields.items(): 296 header_columns.append(('weight', 2, urwid.Text(("list_focus", field_config.get("label", field_key))))) 297 header_columns.append((4, urwid.Text(("list_focus", "")))) 298 299 self.header_row = urwid.Columns(header_columns) 300 301 first_entry = self.create_entry_row() 302 self.entries.append(first_entry) 303 304 self.add_button = urwid.Button("+ Add ", on_press=self.add_entry) 305 add_button_padded = urwid.Padding(self.add_button, left=2, right=2) 306 307 pile_widgets = [ 308 self.header_row, 309 urwid.Divider("-"), 310 first_entry, 311 add_button_padded 312 ] 313 314 urwid.Pile.__init__(self, pile_widgets) 315 FormField.__init__(self, config_key, transform) 316 317 def create_entry_row(self, name="", values=None): 318 if values is None: 319 values = {} 320 321 name_edit = urwid.Edit("", name) 322 323 columns = [('weight', 3, name_edit)] 324 325 field_widgets = {} 326 for field_key, field_config in self.fields.items(): 327 field_value = values.get(field_key, "") 328 329 if field_config.get("type") == "checkbox": 330 widget = urwid.CheckBox("", state=bool(field_value)) 331 elif field_config.get("type") == "dropdown": 332 # TODO: dropdown in MultiTable 333 widget = urwid.Edit("", str(field_value)) 334 else: 335 widget = urwid.Edit("", str(field_value)) 336 337 field_widgets[field_key] = widget 338 columns.append(('weight', 2, widget)) 339 340 remove_button = urwid.Button("×", on_press=lambda button: self.remove_entry(button, entry_row)) 341 columns.append((4, remove_button)) 342 343 entry_row = urwid.Columns(columns) 344 entry_row.name_edit = name_edit 345 entry_row.field_widgets = field_widgets 346 347 return entry_row 348 349 def remove_entry(self, button, entry_row): 350 if len(self.entries) > 1: 351 self.entries.remove(entry_row) 352 self.contents = [(w, self.options()) for w in self.get_pile_widgets()] 353 354 def add_entry(self, button): 355 new_entry = self.create_entry_row() 356 self.entries.append(new_entry) 357 358 self.contents = [(w, self.options()) for w in self.get_pile_widgets()] 359 360 def get_pile_widgets(self): 361 return [ 362 self.header_row, 363 urwid.Divider("-") 364 ] + self.entries + [ 365 urwid.Padding(self.add_button, left=2, right=2) 366 ] 367 368 def get_value(self): 369 values = {} 370 for entry in self.entries: 371 name = entry.name_edit.edit_text.strip() 372 if name: 373 subinterface = {} 374 subinterface["interface_enabled"] = True 375 376 for field_key, widget in entry.field_widgets.items(): 377 field_config = self.fields.get(field_key, {}) 378 379 if hasattr(widget, "get_state"): 380 value = widget.get_state() 381 elif hasattr(widget, "edit_text"): 382 value = widget.edit_text.strip() 383 384 transform = field_config.get("transform") 385 if transform and value: 386 try: 387 value = transform(value) 388 except (ValueError, TypeError): 389 value = "" 390 391 if value: 392 subinterface[field_key] = value 393 394 values[name] = subinterface 395 396 return self.transform(values) if self.transform else values 397 398 def set_value(self, value): 399 self.entries = [] 400 401 if not value: 402 self.entries.append(self.create_entry_row()) 403 else: 404 for name, config in value.items(): 405 self.entries.append(self.create_entry_row(name=name, values=config)) 406 407 self.contents = [(w, self.options()) for w in self.get_pile_widgets()] 408 409 def validate(self): 410 values = self.get_value() 411 self.error = None 412 413 for validation in self.validation_types: 414 if validation == "required" and not values: 415 self.error = "At least one subinterface is required" 416 break 417 418 self.error_widget.set_text(("error", self.error or "")) 419 return self.error is None 420 421 422 class FormKeyValuePairs(urwid.Pile, FormField): 423 def __init__(self, config_key, validation_types=None, transform=None, **kwargs): 424 self.entries = [] 425 self.error_widget = urwid.Text("") 426 self.error = None 427 self.validation_types = validation_types or [] 428 429 header_columns = [ 430 ('weight', 1, urwid.AttrMap(urwid.Text("Parameter Key"), "multitable_header")), 431 ('weight', 1, urwid.AttrMap(urwid.Text("Parameter Value"), "multitable_header")), 432 (4, urwid.AttrMap(urwid.Text("Action"), "multitable_header")) 433 ] 434 435 self.header_row = urwid.AttrMap(urwid.Columns(header_columns), "multitable_header") 436 437 first_entry = self.create_entry_row() 438 self.entries.append(first_entry) 439 440 self.add_button = urwid.Button("+ Add Parameter", on_press=self.add_entry) 441 add_button_padded = urwid.Padding(self.add_button, left=2, right=2) 442 443 pile_widgets = [ 444 self.header_row, 445 urwid.Divider("-"), 446 first_entry, 447 add_button_padded 448 ] 449 450 urwid.Pile.__init__(self, pile_widgets) 451 FormField.__init__(self, config_key, transform) 452 453 def create_entry_row(self, key="", value=""): 454 key_edit = urwid.Edit("", key) 455 value_edit = urwid.Edit("", value) 456 457 remove_button = urwid.Button("×", on_press=lambda button: self.remove_entry(button, entry_row)) 458 459 entry_row = urwid.Columns([ 460 ('weight', 1, key_edit), 461 ('weight', 1, value_edit), 462 (4, remove_button) 463 ]) 464 465 entry_row.key_edit = key_edit 466 entry_row.value_edit = value_edit 467 468 return entry_row 469 470 def remove_entry(self, button, entry_row): 471 if len(self.entries) > 1: 472 self.entries.remove(entry_row) 473 self.contents = [(w, self.options()) for w in self.get_pile_widgets()] 474 475 def add_entry(self, button): 476 new_entry = self.create_entry_row() 477 self.entries.append(new_entry) 478 479 self.contents = [(w, self.options()) for w in self.get_pile_widgets()] 480 481 def get_pile_widgets(self): 482 return [ 483 self.header_row, 484 urwid.Divider("-") 485 ] + self.entries + [ 486 urwid.Padding(self.add_button, left=2, right=2) 487 ] 488 489 def get_value(self): 490 values = {} 491 for entry in self.entries: 492 key = entry.key_edit.edit_text.strip() 493 value = entry.value_edit.edit_text.strip() 494 495 if key: 496 if value.isdigit(): 497 values[key] = int(value) 498 elif value.replace('.', '', 1).isdigit() and value.count('.') <= 1: 499 values[key] = float(value) 500 elif value.lower() == 'true': 501 values[key] = True 502 elif value.lower() == 'false': 503 values[key] = False 504 else: 505 values[key] = value 506 507 return self.transform(values) if self.transform else values 508 509 def set_value(self, value): 510 self.entries = [] 511 512 if not value or not isinstance(value, dict): 513 self.entries.append(self.create_entry_row()) 514 else: 515 for key, val in value.items(): 516 self.entries.append(self.create_entry_row(key=key, value=str(val))) 517 518 self.contents = [(w, self.options()) for w in self.get_pile_widgets()] 519 520 def validate(self): 521 values = self.get_value() 522 self.error = None 523 524 keys = [entry.key_edit.edit_text.strip() for entry in self.entries 525 if entry.key_edit.edit_text.strip()] 526 if len(keys) != len(set(keys)): 527 self.error = "Duplicate keys are not allowed" 528 self.error_widget.set_text(("error", self.error)) 529 return False 530 531 for validation in self.validation_types: 532 if validation == "required" and not values: 533 self.error = "Atleast one parameter is required" 534 break 535 536 self.error_widget.set_text(("error", self.error or "")) 537 return self.error is None