Source code for qtpyguihelper.wx.wx_widget_factory

"""
wxPython widget factory for creating widgets based on field configurations.
"""

from typing import Any, Dict, Optional, List
import wx
import wx.lib.scrolledpanel as scrolled
import wx.adv
from datetime import datetime, date, time

from ..config_loader import FieldConfig


[docs] def set_nested_value(data: Dict[str, Any], key_path: str, value: Any): """ Set a value in a nested dictionary using dot notation. """ keys = key_path.split('.') current = data for key in keys[:-1]: if key not in current: current[key] = {} elif not isinstance(current[key], dict): return current = current[key] current[keys[-1]] = value
[docs] def get_nested_value(data: Dict[str, Any], key_path: str, default: Any = None) -> Any: """ Get a value from a nested dictionary using dot notation. """ keys = key_path.split('.') current = data try: for key in keys: if isinstance(current, dict) and key in current: current = current[key] else: return default return current except (KeyError, TypeError): return default
[docs] class WxCustomColorButton(wx.Button): """Custom button widget for color selection in wxPython.""" def __init__(self, parent, initial_color=wx.Colour(255, 255, 255)): super().__init__(parent, label="Choose Color") self.current_color = initial_color self.Bind(wx.EVT_BUTTON, self._on_choose_color) self._update_button_appearance() def _on_choose_color(self, event): """Open color dialog and update button.""" dialog = wx.ColourDialog(self) dialog.GetColourData().SetColour(self.current_color) if dialog.ShowModal() == wx.ID_OK: self.current_color = dialog.GetColourData().GetColour() self._update_button_appearance() dialog.Destroy() def _update_button_appearance(self): """Update button appearance to show current color.""" self.SetBackgroundColour(self.current_color) # Set text color based on background brightness r, g, b = self.current_color.Red(), self.current_color.Green(), self.current_color.Blue() brightness = (r * 299 + g * 587 + b * 114) / 1000 text_color = wx.Colour(255, 255, 255) if brightness < 128 else wx.Colour(0, 0, 0) self.SetForegroundColour(text_color)
[docs] def get_color(self) -> wx.Colour: """Get the current selected color.""" return self.current_color
[docs] def set_color(self, color: wx.Colour): """Set the current color.""" self.current_color = color self._update_button_appearance()
[docs] class WxCustomFileButton(wx.Button): """Custom button widget for file selection in wxPython.""" def __init__(self, parent, file_mode="open"): super().__init__(parent, label="Choose File...") self.file_mode = file_mode self.selected_file = "" self.Bind(wx.EVT_BUTTON, self._on_choose_file) def _on_choose_file(self, event): """Open file dialog and update button.""" if self.file_mode == "save": dialog = wx.FileDialog(self, "Save File", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) else: dialog = wx.FileDialog(self, "Open File", style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) if dialog.ShowModal() == wx.ID_OK: self.selected_file = dialog.GetPath() filename = dialog.GetFilename() self.SetLabel(f"Selected: {filename}") dialog.Destroy()
[docs] def get_file_path(self) -> str: """Get the selected file path.""" return self.selected_file
[docs] def set_file_path(self, path: str): """Set the file path.""" self.selected_file = path if path: import os filename = os.path.basename(path) self.SetLabel(f"Selected: {filename}") else: self.SetLabel("Choose File...")
[docs] class WxWidgetFactory: """Factory class for creating wxPython widgets from field configurations.""" def __init__(self): self.widgets: Dict[str, wx.Window] = {} self.labels: Dict[str, wx.StaticText] = {}
[docs] def create_widget(self, parent: wx.Window, field_config: FieldConfig) -> Optional[wx.Window]: """Create a widget based on the field configuration.""" widget = None if field_config.type == "text": widget = self._create_text_field(parent, field_config) elif field_config.type == "number": widget = self._create_number_field(parent, field_config) elif field_config.type == "int": widget = self._create_int_field(parent, field_config) elif field_config.type == "float": widget = self._create_float_field(parent, field_config) elif field_config.type == "email": widget = self._create_email_field(parent, field_config) elif field_config.type == "password": widget = self._create_password_field(parent, field_config) elif field_config.type == "textarea": widget = self._create_textarea_field(parent, field_config) elif field_config.type == "checkbox": widget = self._create_checkbox_field(parent, field_config) elif field_config.type == "radio": widget = self._create_radio_field(parent, field_config) elif field_config.type == "select": widget = self._create_select_field(parent, field_config) elif field_config.type == "date": widget = self._create_date_field(parent, field_config) elif field_config.type == "time": widget = self._create_time_field(parent, field_config) elif field_config.type == "datetime": widget = self._create_datetime_field(parent, field_config) elif field_config.type == "range": widget = self._create_range_field(parent, field_config) elif field_config.type == "file": widget = self._create_file_field(parent, field_config) elif field_config.type == "color": widget = self._create_color_field(parent, field_config) elif field_config.type == "url": widget = self._create_url_field(parent, field_config) else: # Fallback to text field widget = self._create_text_field(parent, field_config) # Apply common properties if widget and field_config.tooltip: widget.SetToolTip(field_config.tooltip) if widget and field_config.width: size = widget.GetSize() widget.SetSize((field_config.width, size.height)) if widget and field_config.height: size = widget.GetSize() widget.SetSize((size.width, field_config.height)) # Store widget reference if widget: self.widgets[field_config.name] = widget return widget
[docs] def create_label(self, parent: wx.Window, field_config: FieldConfig) -> wx.StaticText: """Create a label for the field.""" label_text = field_config.label if field_config.required: label_text += " *" label = wx.StaticText(parent, label=label_text) self.labels[field_config.name] = label return label
def _create_text_field(self, parent: wx.Window, field_config: FieldConfig) -> wx.TextCtrl: """Create a text input field.""" style = wx.TE_PROCESS_ENTER widget = wx.TextCtrl(parent, style=style) if field_config.placeholder: widget.SetHint(field_config.placeholder) if field_config.default_value: widget.SetValue(str(field_config.default_value)) return widget def _create_number_field(self, parent: wx.Window, field_config: FieldConfig) -> wx.Window: """Create a number input field.""" if (field_config.min_value is not None and isinstance(field_config.min_value, int) and field_config.max_value is not None and isinstance(field_config.max_value, int)): # Use SpinCtrl for integer ranges min_val = int(field_config.min_value) if field_config.min_value is not None else -999999 max_val = int(field_config.max_value) if field_config.max_value is not None else 999999 widget = wx.SpinCtrl(parent, min=min_val, max=max_val) if field_config.default_value is not None: widget.SetValue(int(field_config.default_value)) else: # Use SpinCtrlDouble for float ranges min_val = float(field_config.min_value) if field_config.min_value is not None else -999999.0 max_val = float(field_config.max_value) if field_config.max_value is not None else 999999.0 widget = wx.SpinCtrlDouble(parent, min=min_val, max=max_val) if field_config.default_value is not None: widget.SetValue(float(field_config.default_value)) return widget def _create_int_field(self, parent: wx.Window, field_config: FieldConfig) -> wx.SpinCtrl: """Create an integer input field.""" min_val = int(field_config.min_value) if field_config.min_value is not None else -2147483648 max_val = int(field_config.max_value) if field_config.max_value is not None else 2147483647 widget = wx.SpinCtrl(parent, min=min_val, max=max_val) if field_config.default_value is not None: widget.SetValue(int(field_config.default_value)) return widget def _create_float_field(self, parent: wx.Window, field_config: FieldConfig) -> wx.Window: """Create a float input field.""" # Check if we need special formatting needs_text_field = False if field_config.format_string: format_str = field_config.format_string.lower() if any(char in format_str for char in ['e', '%', 'g']) or ',' in format_str: needs_text_field = True if needs_text_field: return self._create_scientific_float_field(parent, field_config) else: return self._create_spinctrl_float_field(parent, field_config) def _create_spinctrl_float_field(self, parent: wx.Window, field_config: FieldConfig) -> wx.SpinCtrlDouble: """Create a float input field using SpinCtrlDouble.""" min_val = float(field_config.min_value) if field_config.min_value is not None else -999999.0 max_val = float(field_config.max_value) if field_config.max_value is not None else 999999.0 widget = wx.SpinCtrlDouble(parent, min=min_val, max=max_val) # Set decimal places from format string decimals = 2 if field_config.format_string: try: format_str = field_config.format_string.lower() if '.' in format_str and 'f' in format_str: decimal_part = format_str.split('.')[1] decimals = int(decimal_part.replace('f', '')) except (ValueError, IndexError): decimals = 2 widget.SetDigits(decimals) if field_config.default_value is not None: widget.SetValue(float(field_config.default_value)) # Store format string for later use if field_config.format_string: widget.format_string = field_config.format_string widget.field_type = "spinctrl_float" return widget def _create_scientific_float_field(self, parent: wx.Window, field_config: FieldConfig) -> wx.TextCtrl: """Create a float input field using TextCtrl for scientific notation.""" widget = wx.TextCtrl(parent, style=wx.TE_PROCESS_ENTER) # Set default value if field_config.default_value is not None: if field_config.format_string: try: formatted_value = format(float(field_config.default_value), field_config.format_string) widget.SetValue(formatted_value) except (ValueError, TypeError): widget.SetValue(str(field_config.default_value)) else: widget.SetValue(str(field_config.default_value)) # Store format string and field type if field_config.format_string: widget.format_string = field_config.format_string widget.field_type = "scientific_float" # Set hint text based on format if field_config.format_string: format_str = field_config.format_string if 'e' in format_str.lower(): widget.SetHint("e.g., 1.23e+06 or 1.23E-05") elif '%' in format_str: widget.SetHint("e.g., 0.856 (for 85.6%)") elif 'g' in format_str.lower(): widget.SetHint("e.g., 123.456 or 1.23e+06") return widget def _create_email_field(self, parent: wx.Window, field_config: FieldConfig) -> wx.TextCtrl: """Create an email input field.""" widget = wx.TextCtrl(parent, style=wx.TE_PROCESS_ENTER) widget.SetHint(field_config.placeholder or "Enter email address") if field_config.default_value: widget.SetValue(str(field_config.default_value)) return widget def _create_password_field(self, parent: wx.Window, field_config: FieldConfig) -> wx.TextCtrl: """Create a password input field.""" widget = wx.TextCtrl(parent, style=wx.TE_PASSWORD | wx.TE_PROCESS_ENTER) widget.SetHint(field_config.placeholder or "Enter password") return widget def _create_textarea_field(self, parent: wx.Window, field_config: FieldConfig) -> wx.TextCtrl: """Create a textarea field.""" style = wx.TE_MULTILINE | wx.TE_WORDWRAP widget = wx.TextCtrl(parent, style=style) if field_config.default_value: widget.SetValue(str(field_config.default_value)) widget.SetHint(field_config.placeholder or "Enter text...") # Set default size for multiline text if not field_config.height: widget.SetSize((-1, 100)) return widget def _create_checkbox_field(self, parent: wx.Window, field_config: FieldConfig) -> wx.CheckBox: """Create a checkbox field.""" widget = wx.CheckBox(parent, label=field_config.label) if field_config.default_value: widget.SetValue(bool(field_config.default_value)) return widget def _create_radio_field(self, parent: wx.Window, field_config: FieldConfig) -> wx.Panel: """Create radio button group.""" panel = wx.Panel(parent) sizer = wx.BoxSizer(wx.VERTICAL) radio_buttons = [] for i, option in enumerate(field_config.options or []): style = wx.RB_GROUP if i == 0 else 0 radio_button = wx.RadioButton(panel, label=option, style=style) radio_buttons.append(radio_button) sizer.Add(radio_button, 0, wx.ALL, 2) # Set default selection if field_config.default_value == option: radio_button.SetValue(True) panel.SetSizer(sizer) panel.radio_buttons = radio_buttons # Store reference for value access return panel def _create_select_field(self, parent: wx.Window, field_config: FieldConfig) -> wx.Choice: """Create a select (choice) field.""" choices = field_config.options or [] widget = wx.Choice(parent, choices=choices) # Set default selection if field_config.default_value and field_config.default_value in choices: index = choices.index(field_config.default_value) widget.SetSelection(index) return widget def _create_date_field(self, parent: wx.Window, field_config: FieldConfig) -> wx.adv.DatePickerCtrl: """Create a date input field.""" widget = wx.adv.DatePickerCtrl(parent, style=wx.adv.DP_DROPDOWN | wx.adv.DP_SHOWCENTURY) if field_config.default_value: try: # Parse YYYY-MM-DD format year, month, day = map(int, field_config.default_value.split('-')) date_val = wx.DateTime(day, month - 1, year) # wxPython months are 0-based widget.SetValue(date_val) except (ValueError, AttributeError): pass # Use current date as fallback return widget def _create_time_field(self, parent: wx.Window, field_config: FieldConfig) -> wx.adv.TimePickerCtrl: """Create a time input field.""" widget = wx.adv.TimePickerCtrl(parent) if field_config.default_value: try: # Parse HH:MM format hour, minute = map(int, field_config.default_value.split(':')) # Create a valid datetime first, then set the time time_val = wx.DateTime.Now() time_val.SetHour(hour) time_val.SetMinute(minute) time_val.SetSecond(0) widget.SetValue(time_val) except (ValueError, AttributeError): pass # Use current time as fallback return widget def _create_datetime_field(self, parent: wx.Window, field_config: FieldConfig) -> wx.Panel: """Create a datetime input field using separate date and time controls.""" panel = wx.Panel(parent) sizer = wx.BoxSizer(wx.HORIZONTAL) # Create date picker date_picker = wx.adv.DatePickerCtrl(panel, style=wx.adv.DP_DROPDOWN | wx.adv.DP_SHOWCENTURY) time_picker = wx.adv.TimePickerCtrl(panel) sizer.Add(date_picker, 1, wx.EXPAND | wx.RIGHT, 5) sizer.Add(time_picker, 1, wx.EXPAND) panel.SetSizer(sizer) panel.date_picker = date_picker panel.time_picker = time_picker # Set default value if field_config.default_value: try: # Parse ISO datetime format dt = datetime.fromisoformat(field_config.default_value.replace('Z', '+00:00')) date_val = wx.DateTime(dt.day, dt.month - 1, dt.year) date_picker.SetValue(date_val) # Create a valid datetime first, then set the time time_val = wx.DateTime.Now() time_val.SetHour(dt.hour) time_val.SetMinute(dt.minute) time_val.SetSecond(0) time_picker.SetValue(time_val) except (ValueError, AttributeError): pass # Use current datetime as fallback return panel def _create_range_field(self, parent: wx.Window, field_config: FieldConfig) -> wx.Slider: """Create a range (slider) field.""" min_val = int(field_config.min_value) if field_config.min_value is not None else 0 max_val = int(field_config.max_value) if field_config.max_value is not None else 100 default_val = int(field_config.default_value) if field_config.default_value is not None else min_val widget = wx.Slider(parent, value=default_val, minValue=min_val, maxValue=max_val, style=wx.SL_HORIZONTAL | wx.SL_LABELS) return widget def _create_file_field(self, parent: wx.Window, field_config: FieldConfig) -> WxCustomFileButton: """Create a file selection field.""" file_mode = "open" if field_config.default_value == "save": file_mode = "save" widget = WxCustomFileButton(parent, file_mode) return widget def _create_color_field(self, parent: wx.Window, field_config: FieldConfig) -> WxCustomColorButton: """Create a color selection field.""" initial_color = wx.Colour(255, 255, 255) if field_config.default_value: try: # Parse hex color hex_color = field_config.default_value.lstrip('#') r = int(hex_color[0:2], 16) g = int(hex_color[2:4], 16) b = int(hex_color[4:6], 16) initial_color = wx.Colour(r, g, b) except (ValueError, IndexError): pass # Use default color widget = WxCustomColorButton(parent, initial_color) return widget def _create_url_field(self, parent: wx.Window, field_config: FieldConfig) -> wx.TextCtrl: """Create a URL input field.""" widget = wx.TextCtrl(parent, style=wx.TE_PROCESS_ENTER) widget.SetHint(field_config.placeholder or "Enter URL (http://...)") if field_config.default_value: widget.SetValue(str(field_config.default_value)) return widget
[docs] def get_widget_value(self, field_name: str) -> Any: """Get the current value of a widget.""" if field_name not in self.widgets: return None widget = self.widgets[field_name] if isinstance(widget, wx.TextCtrl): return widget.GetValue() elif isinstance(widget, (wx.SpinCtrl, wx.SpinCtrlDouble)): value = widget.GetValue() # Handle format string for SpinCtrlDouble if isinstance(widget, wx.SpinCtrlDouble) and hasattr(widget, 'format_string'): format_string = widget.format_string try: if any(char in format_string.lower() for char in ['e', 'g']): return value else: return float(format(value, format_string.replace('%', ''))) except (ValueError, TypeError): return value return value elif isinstance(widget, wx.CheckBox): return widget.GetValue() elif isinstance(widget, wx.Choice): selection = widget.GetSelection() return widget.GetString(selection) if selection != wx.NOT_FOUND else "" elif isinstance(widget, wx.adv.DatePickerCtrl): date_val = widget.GetValue() return f"{date_val.GetYear():04d}-{date_val.GetMonth()+1:02d}-{date_val.GetDay():02d}" elif isinstance(widget, wx.adv.TimePickerCtrl): time_val = widget.GetValue() return f"{time_val.GetHour():02d}:{time_val.GetMinute():02d}" elif isinstance(widget, wx.Slider): return widget.GetValue() elif isinstance(widget, WxCustomFileButton): return widget.get_file_path() elif isinstance(widget, WxCustomColorButton): color = widget.get_color() return f"#{color.Red():02x}{color.Green():02x}{color.Blue():02x}" elif isinstance(widget, wx.Panel): # Handle radio buttons if hasattr(widget, 'radio_buttons'): for radio_button in widget.radio_buttons: if radio_button.GetValue(): return radio_button.GetLabel() # Handle datetime panel elif hasattr(widget, 'date_picker') and hasattr(widget, 'time_picker'): date_val = widget.date_picker.GetValue() time_val = widget.time_picker.GetValue() dt = datetime(date_val.GetYear(), date_val.GetMonth()+1, date_val.GetDay(), time_val.GetHour(), time_val.GetMinute()) return dt.isoformat() return None
[docs] def set_widget_value(self, field_name: str, value: Any) -> bool: """Set the value of a widget.""" if field_name not in self.widgets: return False widget = self.widgets[field_name] try: if isinstance(widget, wx.TextCtrl): widget.SetValue(str(value)) elif isinstance(widget, (wx.SpinCtrl, wx.SpinCtrlDouble)): widget.SetValue(float(value)) elif isinstance(widget, wx.CheckBox): widget.SetValue(bool(value)) elif isinstance(widget, wx.Choice): # Find the item index for i in range(widget.GetCount()): if widget.GetString(i) == str(value): widget.SetSelection(i) break elif isinstance(widget, wx.adv.DatePickerCtrl): # Parse YYYY-MM-DD format year, month, day = map(int, str(value).split('-')) date_val = wx.DateTime(day, month - 1, year) widget.SetValue(date_val) elif isinstance(widget, wx.adv.TimePickerCtrl): # Parse HH:MM format hour, minute = map(int, str(value).split(':')) # Create a valid datetime first, then set the time time_val = wx.DateTime.Now() time_val.SetHour(hour) time_val.SetMinute(minute) time_val.SetSecond(0) widget.SetValue(time_val) elif isinstance(widget, wx.Slider): widget.SetValue(int(value)) elif isinstance(widget, WxCustomFileButton): widget.set_file_path(str(value)) elif isinstance(widget, WxCustomColorButton): # Parse hex color hex_color = str(value).lstrip('#') r = int(hex_color[0:2], 16) g = int(hex_color[2:4], 16) b = int(hex_color[4:6], 16) color = wx.Colour(r, g, b) widget.set_color(color) elif isinstance(widget, wx.Panel): # Handle radio buttons if hasattr(widget, 'radio_buttons'): for radio_button in widget.radio_buttons: if radio_button.GetLabel() == str(value): radio_button.SetValue(True) break return True except (ValueError, TypeError, AttributeError): return False
[docs] def get_all_values(self) -> Dict[str, Any]: """Get values from all widgets, creating nested dictionaries for dot notation field names.""" values = {} for field_name in self.widgets.keys(): field_value = self.get_widget_value(field_name) if '.' in field_name: set_nested_value(values, field_name, field_value) else: values[field_name] = field_value return values
[docs] def clear_all_widgets(self): """Clear all widget values.""" for field_name in self.widgets.keys(): widget = self.widgets[field_name] if isinstance(widget, wx.TextCtrl): widget.Clear() elif isinstance(widget, (wx.SpinCtrl, wx.SpinCtrlDouble)): widget.SetValue(0) elif isinstance(widget, wx.CheckBox): widget.SetValue(False) elif isinstance(widget, wx.Choice): widget.SetSelection(0) elif isinstance(widget, (wx.adv.DatePickerCtrl, wx.adv.TimePickerCtrl)): widget.SetValue(wx.DateTime.Now()) elif isinstance(widget, wx.Slider): widget.SetValue(widget.GetMin()) elif isinstance(widget, WxCustomFileButton): widget.set_file_path("") elif isinstance(widget, WxCustomColorButton): widget.set_color(wx.Colour(255, 255, 255)) elif isinstance(widget, wx.Panel): if hasattr(widget, 'radio_buttons') and widget.radio_buttons: widget.radio_buttons[0].SetValue(True)