Source code for mytk.dialog

import pathlib
from enum import StrEnum
from tkinter import Toplevel

from .base import Base
from .button import Button
from .images import Image
from .labels import Label
from .views import View


[docs] class Dialog(Base): """A modal dialog window with configurable buttons and content."""
[docs] class Replies(StrEnum): """Standard reply values returned by dialog buttons.""" Ok = "Ok" Cancel = "Cancel" Abort = "Abort" Timedout = "Timedout"
[docs] @classmethod def showinfo(cls, message, title="Info", auto_click=(None, None), auto_position="center"): """Show an informational dialog with the given message.""" diag = SimpleDialog( dialog_type="info", title=title, message=message, auto_click=auto_click, auto_position=auto_position, ) return diag.run()
[docs] @classmethod def showwarning(cls, message, title="Warning", auto_click=(None, None), auto_position="center"): """Show a warning dialog with the given message.""" diag = SimpleDialog( dialog_type="warning", title=title, message=message, auto_click=auto_click, auto_position=auto_position, ) return diag.run()
[docs] @classmethod def showerror(cls, message, title="Error", auto_click=(None, None), auto_position="center"): """Show an error dialog with the given message.""" diag = SimpleDialog( dialog_type="error", title=title, message=message, auto_click=auto_click, auto_position=auto_position, ) return diag.run()
[docs] @classmethod def showprogress(cls, message, title="Info", auto_click=(None, None)): """Show a progress dialog with the given message.""" diag = Dialog( dialog_type="error", title=title, message=message, auto_click=auto_click ) return diag.run()
def __init__( self, title, buttons_labels=None, geometry=None, auto_position=None, auto_click=(None, None), *args, **kwargs ): from .utils import parse_geometry super().__init__(*args, **kwargs) _, offset_str = parse_geometry(geometry) if offset_str is not None and auto_position is not None: raise ValueError( f"Conflicting position: geometry already contains an offset " f"({offset_str!r}) and auto_position={auto_position!r} was also given. " f"Use one or the other." ) self.title = title self.geometry = geometry self.auto_position = auto_position self.reply = None self.auto_click = auto_click[0] self.timeout = auto_click[1] self.entries = {} if buttons_labels is None: self.buttons_labels = [Dialog.Replies.Ok] else: self.buttons_labels = buttons_labels self.buttons = {} @property def is_disabled(self): """Whether the dialog and its children are disabled.""" return getattr(self, '_disabled', False) @is_disabled.setter def is_disabled(self, value): self._disabled = value if self.widget is not None: self._propagate_disabled(self.widget, value)
[docs] def create_widget(self, master, **kwargs): """Create the Toplevel dialog window and populate its contents.""" self.parent = None self.widget = Toplevel() self.widget.title(self.title) if self.geometry is not None: self.widget.geometry(self.geometry) self.populate_widget_body() self.populate_buttons() self.all_resize_weight(1) if self.auto_position is not None: from .utils import apply_window_position, parse_geometry size_str, _ = parse_geometry(self.geometry) apply_window_position(self.widget, self.auto_position, size_str)
[docs] def populate_buttons(self): """Create and layout the dialog action buttons.""" cols, rows = self.widget.grid_size() control_buttons = View(width=200, height=30) control_buttons.grid_into( widget=self.widget, column=0, row=rows, columnspan=cols, pady=10, padx=10, sticky="sew", ) self.row_resize_weight(rows, 1) control_buttons.column_resize_weight(1, 1) self.buttons = self.create_behavior_buttons() for i, button_label in enumerate(self.buttons_labels): button = self.buttons[button_label] button.grid_into( control_buttons, column=2 - i, row=1, pady=5, padx=5, sticky="nse", )
[docs] def populate_widget_body(self): """Populate the main body of the dialog. Override in subclasses.""" pass
[docs] def run(self): """Display the dialog modally and return the user reply.""" self.create_widget(master=None) if self.auto_click is not None: _button = self.buttons[self.auto_click] if ( self.auto_click == Dialog.Replies.Ok ): # I am unable to get button.widget.invoke to work self.widget.after( self.timeout, lambda: self.user_clicked_ok(None, None) ) elif self.auto_click == Dialog.Replies.Cancel: self.widget.after( self.timeout, lambda: self.user_clicked_cancel(None, None) ) elif self.timeout is not None: self.widget.after(self.timeout, self.user_timeout) self.widget.grab_set() # ensure all input goes to our window, including shortcut enter self.widget.wait_window() return self.reply
[docs] def create_behavior_buttons(self): """Create Ok and Cancel buttons based on the configured button labels.""" if not self.buttons: if Dialog.Replies.Ok in self.buttons_labels: button = Button( Dialog.Replies.Ok, user_event_callback=self.user_clicked_ok ) self.buttons[Dialog.Replies.Ok] = button if Dialog.Replies.Cancel in self.buttons_labels: self.buttons[Dialog.Replies.Cancel] = Button( Dialog.Replies.Cancel, user_event_callback=self.user_clicked_cancel, ) return self.buttons
[docs] def user_clicked_ok(self, event, button=None): """Handle the Ok button click by setting the reply and closing.""" self.reply = Dialog.Replies.Ok self.widget.destroy()
[docs] def user_clicked_cancel(self, event, button=None): """Handle the Cancel button click by setting the reply and closing.""" self.reply = Dialog.Replies.Cancel self.widget.destroy()
[docs] class SimpleDialog(Dialog): """A ready-made dialog that displays an icon and message for info, warning, or error.""" def __init__(self, dialog_type, message, *args, **kwargs): super().__init__(*args, **kwargs) self.dialog_type = dialog_type self.message = message
[docs] def populate_widget_body(self): """Display the dialog icon and message label in the body area.""" self.widget.wait_visibility() # can't grab until window appears, so we wait resource_directory = pathlib.Path(__file__).parent / "resources" if self.dialog_type == "error": icon = Image(filepath=resource_directory / "error.png") elif self.dialog_type == "warning": icon = Image(filepath=resource_directory / "warning.png") elif self.dialog_type == "info": icon = Image(filepath=resource_directory / "info.png") else: icon = Image(filepath=resource_directory / "info.png") icon.is_rescalable = False icon.grid_into(self, column=0, row=0, pady=20, padx=20, sticky="") label1 = Label( text=self.message, wrapping=True, width=30, wraplength=300, justify="center", ) label1.grid_into( widget=self.widget, column=1, columnspan=2, row=0, pady=5, padx=5, sticky="nsew", ) self.column_resize_weight(0, 0) self.column_resize_weight(1, 1) self.widget.resizable(False, False)
[docs] def populate_buttons(self): """Create buttons and assign default keyboard shortcuts.""" super().populate_buttons() self.assign_default_key_shortcuts()
[docs] def assign_default_key_shortcuts(self): """Bind Return to Ok and Escape to Cancel.""" if Dialog.Replies.Ok in self.buttons: self.widget.bind("<Return>", self.user_clicked_ok) self.buttons[Dialog.Replies.Ok].set_as_default() self.widget.bind("<Escape>", self.user_clicked_cancel)