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