from typing import Optional, Union, Tuple, Dict, Any, Literal import customtkinter as ctk from customtkinter import CTkFont, ThemeManager from ..utils import Strings, Icons, wrap_text class CTkMessageBox(ctk.CTkToplevel): header_map = { "info": Strings.info, "warning": Strings.warning, "error": Strings.error, "yesno": Strings.yesno } icon_map = { "info": Icons.info, "warning": Icons.warning, "error": Icons.error, "yesno": Icons.yesno } buttons_map = { "info": {"Ok": {"output": True, "row": 0, "column": 0, "sticky": "center"}}, "warning": { "Ok": {"output": True, "row": 0, "column": 0, "sticky": "e"}, "Cancel": {"output": False, "row": 0, "column": 1, "sticky": "w"} }, "error": {"Ok": {"output": True, "row": 0, "column": 0, "sticky": "center"}}, "yesno": { "Yes": {"output": True, "row": 0, "width": 60, "padx": (0, 75)}, "No": {"output": False, "row": 0, "width": 60, "padx": (0, 0)} } } def __init__(self, title: str = "CTkDialog", text: str = "CTkDialog", mode: Literal["info", "warning", "error", "yesno"] = "info", timeout: int = 0, font: Optional[Union[tuple, CTkFont]] = None, max_message_line_width: int = 500, fg_color: Optional[Union[str, Tuple[str, str]]] = None, text_color: Optional[Union[str, Tuple[str, str]]] = None, button_fg_color: Optional[Union[str, Tuple[str, str]]] = None, button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, button_text_color: Optional[Union[str, Tuple[str, str]]] = None, entry_fg_color: Optional[Union[str, Tuple[str, str]]] = None, entry_border_color: Optional[Union[str, Tuple[str, str]]] = None, entry_text_color: Optional[Union[str, Tuple[str, str]]] = None, parent=None, header=None, buttons: Dict[str, Any] | str = 'auto'): super().__init__(fg_color=fg_color) self.resizable(False, False) self.withdraw() self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(button_hover_color) self._button_fg_color = ThemeManager.theme["CTkButton"]["fg_color"] if button_fg_color is None else self._check_color_type(button_fg_color) self._button_hover_color = ThemeManager.theme["CTkButton"]["hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) self._button_text_color = ThemeManager.theme["CTkButton"]["text_color"] if button_text_color is None else self._check_color_type(button_text_color) self._entry_fg_color = ThemeManager.theme["CTkEntry"]["fg_color"] if entry_fg_color is None else self._check_color_type(entry_fg_color) self._entry_border_color = ThemeManager.theme["CTkEntry"]["border_color"] if entry_border_color is None else self._check_color_type(entry_border_color) self._entry_text_color = ThemeManager.theme["CTkEntry"]["text_color"] if entry_text_color is None else self._check_color_type(entry_text_color) self._running: bool = False self._header = header self._mode = mode self._text = str(text) self._timeout = timeout self._font = font or ("Arial", 12) self._input = None _text_slt = [] self._x = 0 for line in self._text.split('\n'): if len(line) * 7 > 700: wrapped_lines = wrap_text(line, max_message_line_width) _text_slt.extend(wrapped_lines) else: _text_slt.append(line) self._x = max(self._x, max(len(w) * 7 for w in _text_slt)) self._x += 20 # Add 20 pixels for padding on the left and right self._x += 10 # Add 10 pixels for padding on the left and right self._x = max(self._x, (len(self.header_map[mode]) * 16) + 25 + 20) # Set width to the length of the title if it's longer than the message self._text = "\n".join(_text_slt) self._y = 0 self._y += 24 + 10 + 5 # Add 24 pixels for the header (icon + text) and 15 pixels for padding self._y += len(_text_slt) * 12 # Calculate height based on the number of lines in self._text self._y += 15 + 10 # Add 25 pixels for padding between the header and the message if timeout > 0: if self._header: header = self._header(self, self.title(), disable_hide=True, disable_close=True) header.pack(fill="x", pady=(0, 0)) self.geometry(f"{self._x}x{self._y}") self.overrideredirect(True) # remove window decorations self.after(self._timeout, self._on_closing) # close window after timeout self._buttons = {} else: self._y += 30 + 10 + 12 # Add 30 pixels for the buttons self.geometry(f"{self._x}x{self._y+12}") # Add 12 pixels to the height for the title bar if buttons == "auto": self._buttons = self.buttons_map.get(self._mode, {}) elif isinstance(buttons, str): self._buttons = {buttons: {"output": True, "row": 0, "column": 0, "sticky": "center"}} self.attributes("-topmost", True) # stay on top self._center() self.transient(parent) self.title(title) self._create_widgets() self.protocol("WM_DELETE_WINDOW", self._on_closing) def _on_closing(self): self.withdraw() self.grab_release() self.destroy() def _center(self): x = (self.winfo_screenwidth() - self._x) // 2 y = (self.winfo_screenheight() - self._y) // 2 self.geometry(f'{x}+{y}') def _create_widgets(self): # Иконки для типов сообщений icon_text = self.icon_map.get(self._mode, "?") main_frame = ctk.CTkFrame(self) main_frame.focus_set() main_frame.pack(pady=(10, 10), padx=10, fill="x") # Верхняя часть с иконкой и текстом сообщения main_frame.grid_columnconfigure(0, weight=1) # Первый столбец main_frame.grid_columnconfigure(1, weight=2) # Второй столбец # Иконка и текст сообщения icon_label = ctk.CTkLabel(main_frame, text=icon_text, font=("Arial", 24)) icon_label.grid(row=0, column=0, padx=(10, 0), pady=(10, 5), sticky="w") header_label = ctk.CTkLabel(main_frame, text=self.header_map.get(self._mode, "Unknown"), font=("Arial", 16)) header_label.grid(row=0, column=0, padx=(10+40, 10), pady=(12, 3), sticky="w", columnspan=2) message_label = ctk.CTkLabel(main_frame, text=self._text, font=self._font, justify="left") message_label.grid(row=1, column=0, padx=(15, 10), pady=5, rowspan=2, sticky="w") for t, setts in self._buttons.items(): btn = ctk.CTkButton(main_frame, text=t, command=lambda b=setts['output']: self._on_button_click(b), width=setts.get("width", 10)) row = setts.get("row", 0) + 3 if setts.get("sticky") == "center": btn.grid(row=row, column=0, padx=10, pady=10, sticky="we", columnspan=2) else: btn.grid(row=row, column=setts.get("column", 0), padx=setts.get("padx", 10), pady=setts.get("pady", 10), sticky=setts.get("sticky", "e")) def _on_button_click(self, button): self._input = button self.grab_release() self._on_closing() def get_output(self): self.lift() self.grab_set() self.deiconify() self.master.wait_window(self) if self._timeout > 0 and not self._input: return return self._buttons.get(self._input, self._input) class messagebox: @staticmethod def showinfo(title: str | None, message: str, timeout: int = 0, parent=None, header=None): return CTkMessageBox(title=title, text=message, mode="info", timeout=timeout, parent=parent).get_output() @staticmethod def showwarning(title: str| None, message: str, timeout: int = 0, parent=None, header=None): return CTkMessageBox(title=title, text=message, mode="warning", timeout=timeout, parent=parent).get_output() @staticmethod def showerror(title: str | None, message: str, timeout: int = 0, parent=None, header=None): return CTkMessageBox(title=title, text=message, mode="error", timeout=timeout, parent=parent).get_output() @staticmethod def yesno(title: str | None, message: str, timeout: int = 0, parent=None, header=None): return CTkMessageBox(title=title, text=message, mode="yesno", timeout=timeout, parent=parent).get_output() # if __name__ == '__main__': # root = ctk.CTk() # root.geometry("200x150") # text = ctk.CTkLabel(root, text="Test the message box") # text.pack(pady=5) # info_button = ctk.CTkButton(root, text="Info", command=lambda: CTkMessageBox(title="Info", text="This is an info message", mode="info", timeout=1000)) # info_button.pack(pady=5) # warn_button = ctk.CTkButton(root, text="Warn", command=lambda: CTkMessageBox(title="Warning", text="This is a warning message", mode="warning")) # warn_button.pack(pady=5) # error_button = ctk.CTkButton(root, text="Error", command=lambda: CTkMessageBox(title="Error", text="This is an error message", mode="error")) # error_button.pack(pady=5) # root.mainloop()