Source code for psychos.gui.dialog

import tkinter as tk
from tkinter import ttk

[docs] class Dialog: """A versatile dialog form using Tkinter. This dialog can contain: - Interactive fields (Entry or Combobox). - Static labels (added via `add_label`). - OK and/or Cancel buttons. When displayed (via `.show()`): - Returns a dictionary of field values + {"accepted": True} if the user presses OK. - Returns None if the user presses Cancel or closes the dialog window. Args: title (str, optional): Title of the dialog window. Defaults to "" (no title). ok_text (str, optional): Label for the OK button. Defaults to "OK". cancel_text (str, optional): Label for the Cancel button. Defaults to "Cancel". ok_button (bool, optional): Whether to display the OK button. Defaults to True. cancel_button (bool, optional): Whether to display the Cancel button. Defaults to True. theme (str, optional): The ttk theme to use. Defaults to "clam". main_padding (int, optional): Padding around the main container. Defaults to 20. button_padding (int, optional): Padding around the button area. Defaults to 5. wraplength (int, optional): Maximum label width in pixels (wraps text). Defaults to 300. """
[docs] def __init__( self, title="", ok_text="OK", cancel_text="Cancel", ok_button=True, cancel_button=True, theme="clam", main_padding=20, button_padding=5, wraplength=300 ): self.title = title self.ok_text = ok_text self.cancel_text = cancel_text self.ok_button = ok_button self.cancel_button = cancel_button self.theme = theme self.main_padding = main_padding self.button_padding = button_padding self.wraplength = wraplength # List to store field definitions (order matters). # Each item is either: # {"widget_type": "label", "text": "..."} # or {"widget_type": "field", "name": ..., "default": ..., "label": ..., "format": ..., "choices": ...} self.fields = [] # Dictionary to store references to interactive widgets (fields). # Key = field name, Value = {"widget": widget, "format": format_fn} self._widgets = {} # Final result. Will be None if canceled, or a dict if accepted. self._data = None
# ------------------------------------------------------------------------- # Public Methods # -------------------------------------------------------------------------
[docs] def add_field(self, name, default=None, label=None, format=str, choices=None): """Add an interactive field (Entry or Combobox) to the form. Args: name (str): Name (key) of the field in the returned data. default (Any, optional): Default value for the field. For a text entry, this is inserted as the initial text. For a combo box, it sets the initial selection. Defaults to None. label (str, optional): Label to display next to the field. If None, uses `name`. Defaults to None. format (Callable, optional): A function to format/parse the returned value (e.g., `int`). Defaults to `str`. choices (list, optional): If provided, creates a combo box with these choices instead of a text entry. Defaults to None. """ self.fields.append({ "widget_type": "field", "name": name, "default": default, "label": label or name, "format": format, "choices": choices })
[docs] def add_label(self, text): """Add a static label to the form (useful for headers, instructions, or separators). Args: text (str): The text to display in the label. """ self.fields.append({ "widget_type": "label", "text": text })
[docs] def show(self): """Display the dialog form and block until the user responds. Returns: dict or None: - A dictionary containing all field values plus {"accepted": True} if OK is pressed. - None if Cancel is pressed or the dialog is closed. """ # Create and hide the root window self.root = tk.Tk() self.root.withdraw() # Set up the Toplevel dialog window main_frame = self._setup_tinker() # Add widgets (labels/fields) in order self._add_widgets(main_frame) # Add OK/Cancel buttons self._add_buttons(main_frame) # Center and display self._center_dialog() self.dialog_window.grab_set() self.root.mainloop() return self._data
# ------------------------------------------------------------------------- # Internal Helper Methods # ------------------------------------------------------------------------- def _setup_tinker(self): """Initialize the Toplevel dialog window with styling and padding. Returns: ttk.Frame: The main frame inside the dialog window. """ self.dialog_window = tk.Toplevel(self.root) if self.title: self.dialog_window.title(self.title) self.dialog_window.protocol("WM_DELETE_WINDOW", self._on_close) # Apply the chosen theme style = ttk.Style(self.dialog_window) style.theme_use(self.theme) # Create a main frame with padding main_frame = ttk.Frame( self.dialog_window, padding=(self.main_padding, self.main_padding) ) main_frame.pack(fill="both", expand=True) return main_frame def _add_widgets(self, parent): """Add label/field widgets to the given parent frame.""" for idx, field in enumerate(self.fields): if field["widget_type"] == "label": self._create_label_widget(parent, field, idx) else: self._create_field_widget(parent, field, idx) def _create_label_widget(self, parent, field, row_idx): """Create a label widget spanning two columns.""" lbl = ttk.Label(parent, text=field["text"], wraplength=self.wraplength) lbl.grid(row=row_idx, column=0, columnspan=2, padx=5, pady=5, sticky="w") def _create_field_widget(self, parent, field, row_idx): """Create a label and either an Entry or Combobox widget.""" # Create label label_widget = ttk.Label(parent, text=field["label"], wraplength=self.wraplength) label_widget.grid(row=row_idx, column=0, sticky="e", padx=5, pady=5) # Create input widget if field["choices"] is not None: widget = ttk.Combobox(parent, values=field["choices"], state="readonly") # Set default if valid; otherwise pick the first choice if field["default"] is not None and field["default"] in field["choices"]: widget.set(field["default"]) else: widget.current(0) else: widget = ttk.Entry(parent) if field["default"] is not None: widget.insert(0, str(field["default"])) widget.grid(row=row_idx, column=1, padx=5, pady=5, sticky="w") # Store the widget and its formatting function self._widgets[field["name"]] = { "widget": widget, "format": field["format"] } def _add_buttons(self, parent): """Create and place the OK/Cancel buttons.""" button_frame = ttk.Frame(parent, padding=(self.button_padding, self.button_padding)) button_frame.grid(row=len(self.fields), column=0, columnspan=2, pady=(10, 0)) if self.ok_button: ok_btn = ttk.Button(button_frame, text=self.ok_text, command=self._on_ok) ok_btn.pack(side="left", padx=5) if self.cancel_button: cancel_btn = ttk.Button(button_frame, text=self.cancel_text, command=self._on_cancel) cancel_btn.pack(side="right", padx=5) def _center_dialog(self): """Center the dialog on the user's screen.""" self.dialog_window.update_idletasks() # Ensure geometry is accurate width = self.dialog_window.winfo_width() height = self.dialog_window.winfo_height() screen_width = self.dialog_window.winfo_screenwidth() screen_height = self.dialog_window.winfo_screenheight() x = (screen_width // 2) - (width // 2) y = (screen_height // 2) - (height // 2) self.dialog_window.geometry(f"+{x}+{y}") def _on_ok(self): """Gather field values, mark as accepted, and close the dialog.""" data = {} for name, info in self._widgets.items(): widget = info["widget"] format_fn = info["format"] raw_value = widget.get() try: data[name] = format_fn(raw_value) except Exception: # If formatting fails, store the raw string data[name] = raw_value data["accepted"] = True self._data = data self._close_dialog() def _on_cancel(self): """User canceled the dialog.""" self._data = None self._close_dialog() def _on_close(self): """Window close button behaves like cancel.""" self._data = None self._close_dialog() def _close_dialog(self): """Destroy the dialog and stop the event loop.""" self.dialog_window.destroy() self.root.quit()