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 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 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 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)