pep-mklive/pylibraries/ttkbootstrap/dialogs/dialogs.py

1880 lines
63 KiB
Python

"""
This module contains various base dialog base classes that can be
used to create custom dialogs for the end user.
These classes serve as the basis for the pre-defined static helper
methods in the `Messagebox`, and `Querybox` container classes.
"""
import calendar
import textwrap
import locale
from datetime import datetime
from tkinter import font
import ttkbootstrap as ttk
from ttkbootstrap import utility
from ttkbootstrap.icons import Icon
from ttkbootstrap.constants import *
from tkinter import BaseWidget
from ttkbootstrap.localization import MessageCatalog
class Dialog(BaseWidget):
"""A simple dialog base class."""
def __init__(self, parent=None, title="", alert=False):
"""
Parameters:
parent (Widget):
Makes the window the logical parent of the message box.
The messagebox is displayed on top of its parent window.
title (str):
The string displayed as the title of the message box.
This option is ignored on Mac OS X, where platform
guidelines forbid the use of a title on this kind of
dialog.
alert (bool):
Ring the display's bell when the dialog is shown.
"""
BaseWidget._setup(self, parent, {})
self._winsys = self.master.tk.call("tk", "windowingsystem")
self._parent = parent
self._toplevel = None
self._title = title or " "
self._result = None
self._alert = alert
self._initial_focus = None
def _locate(self):
toplevel = self._toplevel
if self._parent is None:
master = toplevel.master
else:
master = self._parent
x = master.winfo_rootx()
y = master.winfo_rooty()
toplevel.geometry(f"+{x}+{y}")
def show(self, position=None):
"""Show the popup dialog
Parameters:
position: Tuple[int, int]
The x and y coordinates used to position the dialog. By
default the dialog will anchor at the NW corner of the
parent window.
"""
self._result = None
self.build()
if position is None:
self._locate()
else:
try:
x, y = position
self._toplevel.geometry(f'+{x}+{y}')
except:
self._locate()
self._toplevel.deiconify()
if self._alert:
self._toplevel.bell()
if self._initial_focus:
self._initial_focus.focus_force()
self._toplevel.grab_set()
self._toplevel.wait_window()
def create_body(self, master):
"""Create the dialog body.
This method should be overridden and is called by the `build`
method. Set the `self._initial_focus` for the widget that
should receive the initial focus.
Parameters:
master (Widget):
The parent widget.
"""
raise NotImplementedError
def create_buttonbox(self, master):
"""Create the dialog button box.
This method should be overridden and is called by the `build`
method. Set the `self._initial_focus` for the button that
should receive the intial focus.
Parameters:
master (Widget):
The parent widget.
"""
raise NotImplementedError
def build(self):
"""Build the dialog from settings"""
# setup toplevel based on widowing system
if self._winsys == "win32":
self._toplevel = ttk.Toplevel(
transient=self.master,
title=self._title,
resizable=(0, 0),
minsize=(250, 15),
iconify=True,
)
else:
self._toplevel = ttk.Toplevel(
transient=self.master,
title=self._title,
resizable=(0, 0),
windowtype="dialog",
iconify=True,
)
self._toplevel.withdraw() # reset the iconify state
# bind <Escape> event to window close
self._toplevel.bind("<Escape>", lambda _: self._toplevel.destroy())
# set position of popup from parent window
# self._locate()
# create widgets
self.create_body(self._toplevel)
self.create_buttonbox(self._toplevel)
# update the window before showing
self._toplevel.update_idletasks()
@property
def result(self):
"""Returns the result of the dialog."""
return self._result
class MessageDialog(Dialog):
"""A simple modal dialog class that can be used to build simple
message dialogs.
Displays a message and a set of buttons. Each of the buttons in the
message window is identified by a unique symbolic name. After the
message window is popped up, the message box awaits for the user to
select one of the buttons. Then it returns the symbolic name of the
selected button. Use a `Toplevel` widget for more advanced modal
dialog designs.
"""
def __init__(
self,
message,
title=" ",
buttons=None,
command=None,
width=50,
parent=None,
alert=False,
default=None,
padding=(20, 20),
icon=None,
**kwargs,
):
"""
Parameters:
message (str):
A message to display in the message box.
title (str):
The string displayed as the title of the message box.
This option is ignored on Mac OS X, where platform
guidelines forbid the use of a title on this kind of
dialog.
buttons (List[str]):
A list of buttons to appear at the bottom of the popup
messagebox. The buttons can be a list of strings which
will define the symbolic name and the button text.
`['OK', 'Cancel']`. Alternatively, you can assign a
bootstyle to each button by using the colon to separate the
button text and the bootstyle. If no colon is found, then
the style is set to 'primary' by default.
`['OK:success','Cancel:danger']`.
command (Tuple[Callable, str]):
The function to invoke when the user closes the dialog.
The actual command is a tuple that consists of the
function to call and the symbolic name of the button that
closes the dialog.
width (int):
The maximum number of characters per line in the message.
If the text stretches beyond the limit, the line will break
at the word.
parent (Widget):
Makes the window the logical parent of the message box.
The messagebox is displayed on top of its parent window.
alert (bool):
Ring the display's bell when the dialog is shown.
default (str):
The symbolic name of the default button. The default
button is invoked when the the <Return> key is pressed.
If no default is provided, the right-most button in the
button list will be set as the default.,
padding (Union[int, Tuple[int]]):
The amount of space between the border and the widget
contents.
icon (str):
An image path, path-like object or image data to be
displayed to the left of the text.
**kwargs (Dict):
Other optional keyword arguments.
Example:
```python
root = tk.Tk()
md = MessageDialog("Displays a message with buttons.")
md.show()
```
"""
super().__init__(parent, title, alert)
self._message = message
self._command = command
self._width = width
self._alert = alert
self._default = (default,)
self._padding = padding
self._icon = icon
self._localize = kwargs.get("localize")
if buttons is None:
self._buttons = [
f"{MessageCatalog.translate('Cancel')}:secondary",
f"{MessageCatalog.translate('OK')}:primary",
]
else:
self._buttons = buttons
def create_body(self, master):
"""Overrides the parent method; adds the message section."""
container = ttk.Frame(master, padding=self._padding)
if self._icon:
try:
# assume this is image data
self._img = ttk.PhotoImage(data=self._icon)
icon_lbl = ttk.Label(container, image=self._img)
icon_lbl.pack(side=LEFT, padx=5)
except:
try:
# assume this is a file path
self._img = ttk.PhotoImage(file=self._icon)
icon_lbl = ttk.Label(container, image=self._img)
icon_lbl.pack(side=LEFT, padx=5)
except:
# icon is neither data nor a valid file path
print("MessageDialog icon is invalid")
if self._message:
for msg in self._message.split("\n"):
message = "\n".join(textwrap.wrap(msg, width=self._width))
message_label = ttk.Label(container, text=message)
message_label.pack(pady=(0, 3), fill=X, anchor=N)
container.pack(fill=X, expand=True)
def create_buttonbox(self, master):
"""Overrides the parent method; adds the message buttonbox"""
frame = ttk.Frame(master, padding=(5, 5))
button_list = []
for i, button in enumerate(self._buttons[::-1]):
cnf = button.split(":")
if len(cnf) == 2:
text, bootstyle = cnf
else:
text = cnf[0]
bootstyle = "secondary"
if self._localize == True:
text = MessageCatalog.translate(text)
btn = ttk.Button(frame, bootstyle=bootstyle, text=text)
btn.configure(command=lambda b=btn: self.on_button_press(b))
btn.pack(padx=2, side=RIGHT)
btn.lower() # set focus traversal left-to-right
button_list.append(btn)
if self._default is not None and text == self._default:
self._initial_focus = btn
elif self._default is None and i == 0:
self._initial_focus = btn
# bind default button to return key press and set focus
self._toplevel.bind("<Return>", lambda _, b=btn: b.invoke())
self._toplevel.bind("<KP_Enter>", lambda _, b=btn: b.invoke())
ttk.Separator(self._toplevel).pack(fill=X)
frame.pack(side=BOTTOM, fill=X, anchor=S)
if not self._initial_focus:
self._initial_focus = button_list[0]
def on_button_press(self, button):
"""Save result, destroy the toplevel, and execute command."""
self._result = button["text"]
command = self._command
if command is not None:
command()
self._toplevel.destroy()
def show(self, position=None):
"""Create and display the popup messagebox."""
super().show(position)
class QueryDialog(Dialog):
"""A simple modal dialog class that can be used to build simple
data input dialogs. Displays a prompt, and input box, and a set of
buttons. Additional data manipulation can be performed on the
user input post-hoc by overriding the `apply` method.
Use a `Toplevel` widget for more advanced modal dialog designs.
"""
def __init__(
self,
prompt,
title=" ",
initialvalue="",
minvalue=None,
maxvalue=None,
width=65,
datatype=str,
padding=(20, 20),
parent=None,
):
"""
Parameters:
prompt (str):
A message to display in the message box above the entry
widget.
title (str):
The string displayed as the title of the message box.
This option is ignored on Mac OS X, where platform
guidelines forbid the use of a title on this kind of
dialog.
initialvalue (Any):
The initial value in the entry widget.
minvalue (Any):
The minimum allowed value. Only valid for int and float
data types.
maxvalue (Any):
The maximum allowed value. Only valid for int and float
data types.
width (int):
The maximum number of characters per line in the
message. If the text stretches beyond the limit, the
line will break at the word.
parent (Widget):
Makes the window the logical parent of the message box.
The messagebox is displayed on top of its parent
window.
padding (Union[int, Tuple[int]]):
The amount of space between the border and the widget
contents.
datatype (Union[int, str, float]):
The data type used to validate the entry value.
"""
super().__init__(parent, title)
self._prompt = prompt
self._initialvalue = initialvalue
self._minvalue = minvalue
self._maxvalue = maxvalue
self._width = width
self._datatype = datatype
self._padding = padding
self._result = None
def create_body(self, master):
"""Overrides the parent method; adds the message and input
section."""
frame = ttk.Frame(master, padding=self._padding)
if self._prompt:
for p in self._prompt.split("\n"):
prompt = "\n".join(textwrap.wrap(p, width=self._width))
prompt_label = ttk.Label(frame, text=prompt)
prompt_label.pack(pady=(0, 5), fill=X, anchor=N)
entry = ttk.Entry(master=frame)
entry.insert(END, self._initialvalue)
entry.pack(pady=(0, 5), fill=X)
entry.bind("<Return>", self.on_submit)
entry.bind("<KP_Enter>", self.on_submit)
entry.bind("<Escape>", self.on_cancel)
frame.pack(fill=X, expand=True)
self._initial_focus = entry
def create_buttonbox(self, master):
"""Overrides the parent method; adds the message buttonbox"""
frame = ttk.Frame(master, padding=(5, 10))
submit = ttk.Button(
master=frame,
bootstyle="primary",
text=MessageCatalog.translate("Submit"),
command=self.on_submit,
)
submit.pack(padx=5, side=RIGHT)
submit.lower() # set focus traversal left-to-right
cancel = ttk.Button(
master=frame,
bootstyle="secondary",
text=MessageCatalog.translate("Cancel"),
command=self.on_cancel,
)
cancel.pack(padx=5, side=RIGHT)
cancel.lower() # set focus traversal left-to-right
ttk.Separator(self._toplevel).pack(fill=X)
frame.pack(side=BOTTOM, fill=X, anchor=S)
def on_submit(self, *_):
"""Save result, destroy the toplevel, and apply any post-hoc
data manipulations."""
self._result = self._initial_focus.get()
valid_result = self.validate()
if not valid_result:
return # keep toplevel open for valid response
self._toplevel.destroy()
self.apply()
def on_cancel(self, *_):
"""Close the toplevel and return empty."""
self._toplevel.destroy()
return
def validate(self):
"""Validate the data
This method is called automatically to validate the data before
the dialog is destroyed. Can be subclassed and overridden.
"""
# no default checks required for string data types
if self._datatype not in [float, int, complex]:
return True
# convert result to appropriate data type
try:
self._result = self._datatype(self._result)
except ValueError:
msg = MessageCatalog.translate("Should be of data type")
Messagebox.ok(
message=f"{msg} `{self._datatype}`",
title=MessageCatalog.translate("Invalid data type"),
parent=self._toplevel
)
return False
# max value range
if self._maxvalue is not None:
if self._result > self._maxvalue:
msg = MessageCatalog.translate("Number cannot be greater than")
Messagebox.ok(
message=f"{msg} {self._maxvalue}",
title=MessageCatalog.translate("Out of range"),
parent=self._toplevel
)
return False
# min value range
if self._minvalue is not None:
if self._result < self._minvalue:
msg = MessageCatalog.translate("Number cannot be less than")
Messagebox.ok(
message=f"{msg} {self._minvalue}",
title=MessageCatalog.translate("Out of range"),
parent=self._toplevel
)
return False
# valid result
return True
def apply(self):
"""Process the data.
This method is called automatically to process the data after
the dialog is destroyed. By default, it does nothing.
"""
pass # override
class DatePickerDialog:
"""A dialog that displays a calendar popup and returns the
selected date as a datetime object.
The current date is displayed by default unless the `startdate`
parameter is provided.
The month can be changed by clicking the chevrons to the left
and right of the month-year title.
Left-click the arrow to move the calendar by one month.
Right-click the arrow to move the calendar by one year.
Right-click the title to reset the calendar to the start date.
The starting weekday can be changed with the `firstweekday`
parameter for geographies that do not start the calendar on
Sunday, which is the default.
The widget grabs focus and all screen events until released.
If you want to cancel a date selection, click the 'X' button
at the top-right corner of the widget.
The bootstyle api may be used to change the style of the widget.
The available colors include -> primary, secondary, success,
info, warning, danger, light, dark.
![](../../assets/dialogs/date-picker-dialog.png)
"""
locale.setlocale(locale.LC_ALL, locale.setlocale(locale.LC_TIME, ""))
def __init__(
self,
parent=None,
title=" ",
firstweekday=6,
startdate=None,
bootstyle=PRIMARY,
):
"""
Parameters:
parent (Widget):
The parent widget; the popup will appear to the
bottom-right of the parent widget. If no parent is
provided, the widget is centered on the screen.
title (str):
The text that appears on the titlebar.
firstweekday (int):
Specifies the first day of the week. 0=Monday,
1=Tuesday, etc...
startdate (datetime):
The date to be in focus when the widget is
displayed.
bootstyle (str):
The following colors can be used to change the color of
the title and hover / pressed color -> primary,
secondary, info, warning, success, danger, light, dark.
"""
self.parent = parent
self.root = ttk.Toplevel(
title=title,
transient=self.parent,
resizable=(False, False),
topmost=True,
minsize=(226, 1),
iconify=True,
)
self.firstweekday = firstweekday
self.startdate = startdate or datetime.today().date()
self.bootstyle = bootstyle or PRIMARY
self.date_selected = self.startdate
self.date = startdate or self.date_selected
self.calendar = calendar.Calendar(firstweekday=firstweekday)
self.titlevar = ttk.StringVar()
self.datevar = ttk.IntVar()
self._setup_calendar()
self.root.grab_set()
self.root.wait_window()
def _setup_calendar(self):
"""Setup the calendar widget"""
# create the widget containers
self.frm_calendar = ttk.Frame(
master=self.root, padding=0, borderwidth=0, relief=FLAT
)
self.frm_calendar.pack(fill=BOTH, expand=YES)
self.frm_title = ttk.Frame(self.frm_calendar, padding=(3, 3))
self.frm_title.pack(fill=X)
self.frm_header = ttk.Frame(self.frm_calendar, bootstyle=SECONDARY)
self.frm_header.pack(fill=X)
# setup the toplevel widget
self.root.withdraw() # reset the iconify state
self.frm_calendar.update_idletasks() # actualize geometry
# create visual components
self._draw_titlebar()
self._draw_calendar()
# make toplevel visible
self._set_window_position()
self.root.deiconify()
def _update_widget_bootstyle(self):
self.frm_title.configure(bootstyle=self.bootstyle)
self.title.configure(bootstyle=f"{self.bootstyle}-inverse")
self.prev_period.configure(style=f"Chevron.{self.bootstyle}.TButton")
self.next_period.configure(style=f"Chevron.{self.bootstyle}.TButton")
def _draw_calendar(self):
self._update_widget_bootstyle()
self._set_title()
self._current_month_days()
self.frm_dates = ttk.Frame(self.frm_calendar)
self.frm_dates.pack(fill=BOTH, expand=YES)
for row, weekday_list in enumerate(self.monthdays):
for col, day in enumerate(weekday_list):
self.frm_dates.columnconfigure(col, weight=1)
if day == 0:
ttk.Label(
master=self.frm_dates,
text=self.monthdates[row][col].day,
anchor=CENTER,
padding=5,
bootstyle=SECONDARY,
).grid(row=row, column=col, sticky=NSEW)
else:
if all(
[
day == self.date_selected.day,
self.date.month == self.date_selected.month,
self.date.year == self.date_selected.year,
]
):
day_style = "secondary-toolbutton"
else:
day_style = f"{self.bootstyle}-calendar"
def selected(x=row, y=col):
self._on_date_selected(x, y)
btn = ttk.Radiobutton(
master=self.frm_dates,
variable=self.datevar,
value=day,
text=day,
bootstyle=day_style,
padding=5,
command=selected,
)
btn.grid(row=row, column=col, sticky=NSEW)
def _draw_titlebar(self):
"""Draw the calendar title bar which includes the month title
and the buttons that increment and decrement the selected
month.
In addition to the previous and next MONTH commands that are
assigned to the button press, a "right-click" event is assigned
to each button that causes the calendar to move to the previous
and next YEAR.
"""
# create and pack the title and action buttons
self.prev_period = ttk.Button(
master=self.frm_title, text="«", command=self.on_prev_month
)
self.prev_period.pack(side=LEFT)
self.title = ttk.Label(
master=self.frm_title,
textvariable=self.titlevar,
anchor=CENTER,
font="-weight bold",
)
self.title.pack(side=LEFT, fill=X, expand=YES)
self.next_period = ttk.Button(
master=self.frm_title,
text="»",
command=self.on_next_month,
)
self.next_period.pack(side=LEFT)
# bind "year" callbacks to action buttons
self.prev_period.bind("<Button-3>", self.on_prev_year, "+")
self.next_period.bind("<Button-3>", self.on_next_year, "+")
self.title.bind("<Button-1>", self.on_reset_date)
# create and pack days of the week header
for col in self._header_columns():
ttk.Label(
master=self.frm_header,
text=col,
anchor=CENTER,
padding=5,
bootstyle=(SECONDARY, INVERSE),
).pack(side=LEFT, fill=X, expand=YES)
def _set_title(self):
_titledate = f'{self.date.strftime("%B %Y")}'
self.titlevar.set(value=_titledate.capitalize())
def _current_month_days(self):
"""Fetch the day numbers and dates for all days in the current
month. `monthdays` is a list of days as integers, and
`monthdates` is a list of `datetime` objects.
"""
self.monthdays = self.calendar.monthdayscalendar(
year=self.date.year, month=self.date.month
)
self.monthdates = self.calendar.monthdatescalendar(
year=self.date.year, month=self.date.month
)
def _header_columns(self):
"""Create and return a list of weekdays to be used as a header
in the calendar. The order of the weekdays is based on the
`firstweekday` property.
Returns:
List[str]:
A list of weekday column names for the calendar header.
"""
weekdays = [
MessageCatalog.translate("Mo"),
MessageCatalog.translate("Tu"),
MessageCatalog.translate("We"),
MessageCatalog.translate("Th"),
MessageCatalog.translate("Fr"),
MessageCatalog.translate("Sa"),
MessageCatalog.translate("Su"),
]
header = weekdays[self.firstweekday :] + weekdays[: self.firstweekday]
return header
def _on_date_selected(self, row, col):
"""Callback for selecting a date.
An index is assigned to each date button that corresponds to
the dates in the `monthdates` matrix. When the user clicks a
button to select a date, the index from this button is used
to lookup the date value of the button based on the row and
column index reference. This value is saved in the
`date_selected` property and the `Toplevel` is destroyed.
Parameters:
index (Tuple[int, int]):
A row and column index of the date selected; to be
found in the `monthdates` matrix.
Returns:
datetime:
The date selected
"""
self.date_selected = self.monthdates[row][col]
self.root.destroy()
def _selection_callback(func):
"""Calls the decorated `func` and redraws the calendar."""
def inner(self, *args):
func(self, *args)
self.frm_dates.destroy()
self._draw_calendar()
return inner
@_selection_callback
def on_next_month(self):
"""Increment the calendar data to the next month"""
year, month = self._nextmonth(self.date.year, self.date.month)
self.date = datetime(year=year, month=month, day=1).date()
@_selection_callback
def on_next_year(self, *_):
"""Increment the calendar data to the next year"""
year = self.date.year + 1
month = self.date.month
self.date = datetime(year=year, month=month, day=1).date()
@_selection_callback
def on_prev_month(self):
"""Decrement the calendar to the previous year"""
year, month = self._prevmonth(self.date.year, self.date.month)
self.date = datetime(year=year, month=month, day=1).date()
@_selection_callback
def on_prev_year(self, *_):
year = self.date.year - 1
month = self.date.month
self.date = datetime(year=year, month=month, day=1).date()
@_selection_callback
def on_reset_date(self, *_):
"""Set the calendar to the start date"""
self.date = self.startdate
def _set_window_position(self):
"""Move the window the to bottom-right of the parent widget, or
the top-left corner of the master window if no parent is
provided.
"""
if self.parent:
xpos = self.parent.winfo_rootx() + self.parent.winfo_width()
ypos = self.parent.winfo_rooty() + self.parent.winfo_height()
self.root.geometry(f"+{xpos}+{ypos}")
else:
xpos = self.root.master.winfo_rootx()
ypos = self.root.master.winfo_rooty()
self.root.geometry(f"+{xpos}+{ypos}")
@staticmethod
def _nextmonth(year, month):
if month == 12:
return year + 1, 1
else:
return year, month + 1
@staticmethod
def _prevmonth(year, month):
if month == 1:
return year - 1, 12
else:
return year, month - 1
class FontDialog(Dialog):
"""A dialog that displays a variety of options for choosing a font.
This dialog constructs and returns a `Font` object based on the
options selected by the user. The initial font is based on OS
settings and will vary.
The font object is returned when the **Ok** button is pressed and
can be passed to any widget that accepts a _font_ configuration
option.
![](../../assets/dialogs/querybox-get-font.png)
"""
def __init__(self, title="Font Selector", parent=None):
title = MessageCatalog.translate(title)
super().__init__(parent=parent, title=title)
self._style = ttk.Style()
self._default = font.nametofont("TkDefaultFont")
self._actual = self._default.actual()
self._size = ttk.Variable(value=self._actual["size"])
self._family = ttk.Variable(value=self._actual["family"])
self._slant = ttk.Variable(value=self._actual["slant"])
self._weight = ttk.Variable(value=self._actual["weight"])
self._overstrike = ttk.Variable(value=self._actual["overstrike"])
self._underline = ttk.Variable(value=self._actual["underline"])
self._preview_font = font.Font()
self._slant.trace_add("write", self._update_font_preview)
self._weight.trace_add("write", self._update_font_preview)
self._overstrike.trace_add("write", self._update_font_preview)
self._underline.trace_add("write", self._update_font_preview)
_headingfont = font.nametofont("TkHeadingFont")
_headingfont.configure(weight="bold")
self._update_font_preview()
self._families = set([self._family.get()])
for f in font.families():
if all([f, not f.startswith("@"), "emoji" not in f.lower()]):
self._families.add(f)
def create_body(self, master):
width = utility.scale_size(master, 600)
height = utility.scale_size(master, 500)
self._toplevel.geometry(f"{width}x{height}")
family_size_frame = ttk.Frame(master, padding=10)
family_size_frame.pack(fill=X, anchor=N)
self._initial_focus = self._font_families_selector(family_size_frame)
self._font_size_selector(family_size_frame)
self._font_options_selectors(master, padding=10)
self._font_preview(master, padding=10)
def create_buttonbox(self, master):
container = ttk.Frame(master, padding=(5, 10))
container.pack(fill=X)
ok_btn = ttk.Button(
master=container,
bootstyle="primary",
text=MessageCatalog.translate("OK"),
command=self._on_submit,
)
ok_btn.pack(side=RIGHT, padx=5)
ok_btn.bind("<Return>", lambda _: ok_btn.invoke())
cancel_btn = ttk.Button(
master=container,
bootstyle="secondary",
text=MessageCatalog.translate("Cancel"),
command=self._on_cancel,
)
cancel_btn.pack(side=RIGHT, padx=5)
cancel_btn.bind("<Return>", lambda _: cancel_btn.invoke())
def _font_families_selector(self, master):
container = ttk.Frame(master)
container.pack(fill=BOTH, expand=YES, side=LEFT)
header = ttk.Label(
container,
text=MessageCatalog.translate("Family"),
font="TkHeadingFont",
)
header.pack(fill=X, pady=(0, 2), anchor=N)
listbox = ttk.Treeview(
master=container,
height=5,
show="",
columns=[0],
)
listbox.column(0, width=utility.scale_size(listbox, 250))
listbox.pack(side=LEFT, fill=BOTH, expand=YES)
listbox_vbar = ttk.Scrollbar(
container,
command=listbox.yview,
orient=VERTICAL,
bootstyle="rounded",
)
listbox_vbar.pack(side=RIGHT, fill=Y)
listbox.configure(yscrollcommand=listbox_vbar.set)
for f in self._families:
listbox.insert("", iid=f, index=END, tags=[f], values=[f])
listbox.tag_configure(f, font=(f, self._size.get()))
iid = self._family.get()
listbox.selection_set(iid) # select default value
listbox.see(iid) # ensure default is visible
listbox.bind(
"<<TreeviewSelect>>", lambda e: self._on_select_font_family(e)
)
return listbox
def _font_size_selector(self, master):
container = ttk.Frame(master)
container.pack(side=LEFT, fill=Y, padx=(10, 0))
header = ttk.Label(
container,
text=MessageCatalog.translate("Size"),
font="TkHeadingFont",
)
header.pack(fill=X, pady=(0, 2), anchor=N)
sizes_listbox = ttk.Treeview(container, height=7, columns=[0], show="")
sizes_listbox.column(0, width=utility.scale_size(sizes_listbox, 24))
sizes = [*range(8, 13), *range(13, 30, 2), 36, 48, 72]
for s in sizes:
sizes_listbox.insert("", iid=s, index=END, values=[s])
iid = self._size.get()
sizes_listbox.selection_set(iid)
sizes_listbox.see(iid)
sizes_listbox.bind(
"<<TreeviewSelect>>", lambda e: self._on_select_font_size(e)
)
sizes_listbox_vbar = ttk.Scrollbar(
master=container,
orient=VERTICAL,
command=sizes_listbox.yview,
bootstyle="round",
)
sizes_listbox.configure(yscrollcommand=sizes_listbox_vbar.set)
sizes_listbox.pack(side=LEFT, fill=Y, expand=YES, anchor=N)
sizes_listbox_vbar.pack(side=LEFT, fill=Y, expand=YES)
def _font_options_selectors(self, master, padding: int):
container = ttk.Frame(master, padding=padding)
container.pack(fill=X, padx=2, pady=2, anchor=N)
weight_lframe = ttk.Labelframe(
container, text=MessageCatalog.translate("Weight"), padding=5
)
weight_lframe.pack(side=LEFT, fill=X, expand=YES)
opt_normal = ttk.Radiobutton(
master=weight_lframe,
text=MessageCatalog.translate("normal"),
value="normal",
variable=self._weight,
)
opt_normal.invoke()
opt_normal.pack(side=LEFT, padx=5, pady=5)
opt_bold = ttk.Radiobutton(
master=weight_lframe,
text=MessageCatalog.translate("bold"),
value="bold",
variable=self._weight,
)
opt_bold.pack(side=LEFT, padx=5, pady=5)
slant_lframe = ttk.Labelframe(
container, text=MessageCatalog.translate("Slant"), padding=5
)
slant_lframe.pack(side=LEFT, fill=X, padx=10, expand=YES)
opt_roman = ttk.Radiobutton(
master=slant_lframe,
text=MessageCatalog.translate("roman"),
value="roman",
variable=self._slant,
)
opt_roman.invoke()
opt_roman.pack(side=LEFT, padx=5, pady=5)
opt_italic = ttk.Radiobutton(
master=slant_lframe,
text=MessageCatalog.translate("italic"),
value="italic",
variable=self._slant,
)
opt_italic.pack(side=LEFT, padx=5, pady=5)
effects_lframe = ttk.Labelframe(
container, text=MessageCatalog.translate("Effects"), padding=5
)
effects_lframe.pack(side=LEFT, padx=(2, 0), fill=X, expand=YES)
opt_underline = ttk.Checkbutton(
master=effects_lframe,
text=MessageCatalog.translate("underline"),
variable=self._underline,
)
opt_underline.pack(side=LEFT, padx=5, pady=5)
opt_overstrike = ttk.Checkbutton(
master=effects_lframe,
text=MessageCatalog.translate("overstrike"),
variable=self._overstrike,
)
opt_overstrike.pack(side=LEFT, padx=5, pady=5)
def _font_preview(self, master, padding: int):
container = ttk.Frame(master, padding=padding)
container.pack(fill=BOTH, expand=YES, anchor=N)
header = ttk.Label(
container,
text=MessageCatalog.translate("Preview"),
font="TkHeadingFont",
)
header.pack(fill=X, pady=2, anchor=N)
content = MessageCatalog.translate(
"The quick brown fox jumps over the lazy dog."
)
self._preview_text = ttk.Text(
master=container,
height=3,
font=self._preview_font,
highlightbackground=self._style.colors.primary,
)
self._preview_text.insert(END, content)
self._preview_text.pack(fill=BOTH, expand=YES)
container.pack_propagate(False)
def _on_select_font_family(self, e):
tree: ttk.Treeview = self._toplevel.nametowidget(e.widget)
fontfamily = tree.selection()[0]
self._family.set(value=fontfamily)
self._update_font_preview()
def _on_select_font_size(self, e):
tree: ttk.Treeview = self._toplevel.nametowidget(e.widget)
fontsize = tree.selection()[0]
self._size.set(value=fontsize)
self._update_font_preview()
def _on_submit(self) -> font.Font:
self._toplevel.destroy()
return self.result
def _on_cancel(self):
self._toplevel.destroy()
def _update_font_preview(self, *_):
family = self._family.get()
size = self._size.get()
slant = self._slant.get()
overstrike = self._overstrike.get()
underline = self._underline.get()
self._preview_font.config(
family=family,
size=size,
slant=slant,
overstrike=overstrike,
underline=underline,
)
try:
self._preview_text.configure(font=self._preview_font)
except:
pass
self._result = self._preview_font
class Messagebox:
"""This class contains various static methods that show popups with
a message to the end user with various arrangments of buttons
and alert options."""
@staticmethod
def show_info(message, title=" ", parent=None, alert=False, **kwargs):
"""Display a modal dialog box with an OK button and an INFO
icon.
![](../../assets/dialogs/messagebox-show-info.png)
Parameters:
message (str):
A message to display in the message box.
title (str):
The string displayed as the title of the messagebox. This
option is ignored on Mac OS X, where platform guidelines
forbid the use of a title on this kind of dialog.
parent (Union[Window, Toplevel]):
Makes the window the logical parent of the message box. The
message box is displayed on top of its parent window.
alert (bool):
Specified whether to ring the display bell.
**kwargs (Dict):
Other optional keyword arguments.
"""
dialog = MessageDialog(
message=message,
title=title,
alert=alert,
parent=parent,
buttons=["OK:primary"],
icon=Icon.info,
localize=True,
**kwargs
)
if "position" in kwargs:
position = kwargs.pop("position")
else:
position = None
dialog.show(position)
@staticmethod
def show_warning(message, title=" ", parent=None, alert=True, **kwargs):
"""Display a modal dialog box with an OK button and a
warning icon. Also will ring the display bell.
![](../../assets/dialogs/messagebox-show-warning.png)
Parameters:
message (str):
A message to display in the message box.
title (str):
The string displayed as the title of the messagebox. This
option is ignored on Mac OS X, where platform guidelines
forbid the use of a title on this kind of dialog.
parent (Union[Window, Toplevel]):
Makes the window the logical parent of the message box. The
message box is displayed on top of its parent window.
alert (bool):
Specified whether to ring the display bell.
**kwargs (Dict):
Other optional keyword arguments.
"""
dialog = MessageDialog(
message=message,
title=title,
parent=parent,
buttons=["OK:primary"],
icon=Icon.warning,
alert=alert,
localize=True,
**kwargs,
)
if "position" in kwargs:
position = kwargs.pop("position")
else:
position = None
dialog.show(position)
@staticmethod
def show_error(message, title=" ", parent=None, alert=True, **kwargs):
"""Display a modal dialog box with an OK button and an
error icon. Also will ring the display bell.
![](../../assets/dialogs/messagebox-show-error.png)
Parameters:
message (str):
A message to display in the message box.
title (str):
The string displayed as the title of the messagebox. This
option is ignored on Mac OS X, where platform guidelines
forbid the use of a title on this kind of dialog.
parent (Union[Window, Toplevel]):
Makes the window the logical parent of the message box. The
message box is displayed on top of its parent window.
alert (bool):
Specified whether to ring the display bell.
**kwargs (Dict):
Other optional keyword arguments.
"""
dialog = MessageDialog(
message=message,
title=title,
parent=parent,
buttons=["OK:primary"],
icon=Icon.error,
alert=alert,
localize=True,
**kwargs,
)
if "position" in kwargs:
position = kwargs.pop("position")
else:
position = None
dialog.show(position)
@staticmethod
def show_question(
message,
title=" ",
parent=None,
buttons=["No:secondary", "Yes:primary"],
alert=True,
**kwargs,
):
"""Display a modal dialog box with yes, no buttons and a
question icon. Also will ring the display bell. You may also
change the button scheme using the `buttons` parameter.
![](../../assets/dialogs/messagebox-show-question.png)
Parameters:
message (str):
A message to display in the message box.
title (str):
The string displayed as the title of the messagebox. This
option is ignored on Mac OS X, where platform guidelines
forbid the use of a title on this kind of dialog.
parent (Union[Window, Toplevel]):
Makes the window the logical parent of the message box. The
message box is displayed on top of its parent window.
buttons (List[str]):
A list of buttons to appear at the bottom of the popup
messagebox. The buttons can be a list of strings which
will define the symbolic name and the button text.
`['OK', 'Cancel']`. Alternatively, you can assign a
bootstyle to each button by using the colon to separate the
button text and the bootstyle. If no colon is found, then
the style is set to 'primary' by default.
`['Yes:success','No:danger']`.
alert (bool):
Specified whether to ring the display bell.
**kwargs (Dict):
Other optional keyword arguments.
Returns:
Union[str, None]:
The symbolic name of the button pressed, or None if the
window is closed without pressing a button.
"""
dialog = MessageDialog(
message=message,
title=title,
parent=parent,
buttons=buttons,
icon=Icon.question,
alert=alert,
localize=True,
**kwargs,
)
if "position" in kwargs:
position = kwargs.pop("position")
else:
position = None
dialog.show(position)
return dialog.result
@staticmethod
def ok(message, title=" ", alert=False, parent=None, **kwargs):
"""Display a modal dialog box with an OK button and and optional
bell alert.
![](../../assets/dialogs/messagebox-ok.png)
Parameters:
message (str):
A message to display in the message box.
title (str):
The string displayed as the title of the messagebox. This
option is ignored on Mac OS X, where platform guidelines
forbid the use of a title on this kind of dialog.
alert (bool):
Specified whether to ring the display bell.
parent (Union[Window, Toplevel]):
Makes the window the logical parent of the message box. The
message box is displayed on top of its parent window.
**kwargs (Dict):
Other optional keyword arguments.
"""
dialog = MessageDialog(
title=title,
message=message,
parent=parent,
alert=alert,
buttons=["OK:primary"],
localize=True,
**kwargs,
)
if "position" in kwargs:
position = kwargs.pop("position")
else:
position = None
dialog.show(position)
@staticmethod
def okcancel(message, title=" ", alert=False, parent=None, **kwargs):
"""Displays a modal dialog box with OK and Cancel buttons and
return the symbolic name of the button pressed.
![](../../assets/dialogs/messagebox-ok-cancel.png)
Parameters:
message (str):
A message to display in the message box.
title (str):
The string displayed as the title of the messagebox. This
option is ignored on Mac OS X, where platform guidelines
forbid the use of a title on this kind of dialog.
alert (bool):
Specified whether to ring the display bell.
parent (Union[Window, Toplevel]):
Makes the window the logical parent of the message box. The
message box is displayed on top of its parent window.
**kwargs (Dict):
Other optional keyword arguments.
Returns:
Union[str, None]:
The symbolic name of the button pressed, or None if the
window is closed without pressing a button.
"""
dialog = MessageDialog(
title=title,
message=message,
parent=parent,
alert=alert,
localize=True,
**kwargs,
)
if "position" in kwargs:
position = kwargs.pop("position")
else:
position = None
dialog.show(position)
return dialog.result
@staticmethod
def yesno(message, title=" ", alert=False, parent=None, **kwargs):
"""Display a modal dialog box with YES and NO buttons and return
the symbolic name of the button pressed.
![](../../assets/dialogs/messagebox-yes-no.png)
Parameters:
message (str):
A message to display in the message box.
title (str):
The string displayed as the title of the messagebox. This
option is ignored on Mac OS X, where platform guidelines
forbid the use of a title on this kind of dialog.
alert (bool):
Specified whether to ring the display bell.
parent (Union[Window, Toplevel]):
Makes the window the logical parent of the message box. The
message box is displayed on top of its parent window.
**kwargs (Dict):
Other optional keyword arguments.
Returns:
Union[str, None]:
The symbolic name of the button pressed, or None if the
window is closed without pressing a button.
"""
dialog = MessageDialog(
title=title,
message=message,
parent=parent,
buttons=["No", "Yes:primary"],
alert=alert,
localize=True,
**kwargs,
)
if "position" in kwargs:
position = kwargs.pop("position")
else:
position = None
dialog.show(position)
return dialog.result
@staticmethod
def yesnocancel(message, title=" ", alert=False, parent=None, **kwargs):
"""Display a modal dialog box with YES, NO, and Cancel buttons,
and return the symbolic name of the button pressed.
![](../../assets/dialogs/messagebox-yes-no-cancel.png)
Parameters:
message (str):
A message to display in the message box.
title (str):
The string displayed as the title of the messagebox. This
option is ignored on Mac OS X, where platform guidelines
forbid the use of a title on this kind of dialog.
alert (bool):
Specified whether to ring the display bell.
parent (Union[Window, Toplevel]):
Makes the window the logical parent of the message box. The
message box is displayed on top of its parent window.
**kwargs (Dict):
Optional keyword arguments.
Returns:
Union[str, None]:
The symbolic name of the button pressed, or None if the
window is closed without pressing a button.
"""
dialog = MessageDialog(
title=title,
message=message,
parent=parent,
alert=alert,
buttons=["Cancel", "No", "Yes:primary"],
localize=True,
**kwargs,
)
if "position" in kwargs:
position = kwargs.pop("position")
else:
position = None
dialog.show(position)
return dialog.result
@staticmethod
def retrycancel(message, title=" ", alert=False, parent=None, **kwargs):
"""Display a modal dialog box with RETRY and Cancel buttons;
returns the symbolic name of the button pressed.
![](../../assets/dialogs/messagebox-retry-cancel.png)
Parameters:
message (str):
A message to display in the message box.
title (str):
The string displayed as the title of the messagebox. This
option is ignored on Mac OS X, where platform guidelines
forbid the use of a title on this kind of dialog.
alert (bool):
Specified whether to ring the display bell.
parent (Union[Window, Toplevel]):
Makes the window the logical parent of the message box. The
message box is displayed on top of its parent window.
**kwargs (Dict):
Other optional keyword arguments.
Returns:
Union[str, None]:
The symbolic name of the button pressed, or None if the
window is closed without pressing a button.
"""
dialog = MessageDialog(
title=title,
message=message,
parent=parent,
alert=alert,
buttons=["Cancel", "Retry:primary"],
localize=True,
**kwargs,
)
if "position" in kwargs:
position = kwargs.pop("position")
else:
position = None
dialog.show(position)
return dialog.result
class Querybox:
"""This class contains various static methods that request data
from the end user."""
@staticmethod
def get_color(
parent=None, title="Color Chooser", initialcolor=None, **kwargs
):
"""Show a color picker and return the select color when the
user pressed OK.
![](../../assets/dialogs/querybox-get-color.png)
Parameters:
parent (Widget):
The parent widget.
title (str):
Optional text that appears on the titlebar.
initialcolor (str):
The initial color to display in the 'Current' color
frame.
Returns:
Tuple[rgb, hsl, hex]:
The selected color in various colors models.
"""
from ttkbootstrap.dialogs.colorchooser import ColorChooserDialog
dialog = ColorChooserDialog(parent, title, initialcolor)
if "position" in kwargs:
position = kwargs.pop("position")
else:
position = None
dialog.show(position)
return dialog.result
@staticmethod
def get_date(
parent=None,
title=" ",
firstweekday=6,
startdate=None,
bootstyle="primary",
):
"""Shows a calendar popup and returns the selection.
![](../../assets/dialogs/querybox-get-date.png)
Parameters:
parent (Widget):
The parent widget; the popup will appear to the
bottom-right of the parent widget. If no parent is
provided, the widget is centered on the screen.
title (str):
The text that appears on the popup titlebar.
firstweekday (int):
Specifies the first day of the week. `0` is Monday, `6` is
Sunday (the default).
startdate (datetime):
The date to be in focus when the widget is displayed;
bootstyle (str):
The following colors can be used to change the color of the
title and hover / pressed color -> primary, secondary, info,
warning, success, danger, light, dark.
Returns:
datetime:
The date selected; the current date if no date is selected.
"""
chooser = DatePickerDialog(
parent=parent,
title=title,
firstweekday=firstweekday,
startdate=startdate,
bootstyle=bootstyle,
)
return chooser.date_selected
@staticmethod
def get_string(
prompt="", title=" ", initialvalue=None, parent=None, **kwargs
):
"""Request a string type input from the user.
![](../../assets/dialogs/querybox-get-string.png)
Parameters:
prompt (str):
A message to display in the message box above the entry
widget.
title (str):
The string displayed as the title of the message box. This
option is ignored on Mac OS X, where platform guidelines
forbid the use of a title on this kind of dialog.
initialvalue (Any):
The initial value in the entry widget.
parent (Widget):
Makes the window the logical parent of the message box. The
messagebox is displayed on top of its parent window.
**kwargs (Dict):
Other optional keyword arguments.
Returns:
str:
The string value of the entry widget.
"""
initialvalue = initialvalue or ""
if "position" in kwargs:
position = kwargs.pop("position")
else:
position = None
dialog = QueryDialog(
prompt, title, initialvalue, parent=parent, **kwargs
)
dialog.show(position)
return dialog._result
@staticmethod
def get_integer(
prompt="",
title=" ",
initialvalue=None,
minvalue=None,
maxvalue=None,
parent=None,
**kwargs,
):
"""Request an integer type input from the user.
![](../../assets/dialogs/querybox-get-integer.png)
Parameters:
prompt (str):
A message to display in the message box above the entry
widget.
title (str):
The string displayed as the title of the message box. This
option is ignored on Mac OS X, where platform guidelines
forbid the use of a title on this kind of dialog.
initialvalue (int):
The initial value in the entry widget.
minvalue (int):
The minimum allowed value.
maxvalue (int):
The maximum allowed value.
parent (Widget):
Makes the window the logical parent of the message box. The
messagebox is displayed on top of its parent window.
**kwargs (Dict):
Other optional keyword arguments.
Returns:
int:
The integer value of the entry widget.
"""
initialvalue = initialvalue or ""
if "position" in kwargs:
position = kwargs.pop("position")
else:
position = None
dialog = QueryDialog(
prompt,
title,
initialvalue,
minvalue,
maxvalue,
datatype=int,
parent=parent,
**kwargs,
)
dialog.show(position)
return dialog._result
@staticmethod
def get_float(
prompt="",
title=" ",
initialvalue=None,
minvalue=None,
maxvalue=None,
parent=None,
**kwargs,
):
"""Request a float type input from the user.
![](../../assets/dialogs/querybox-get-float.png)
Parameters:
prompt (str):
A message to display in the message box above the entry
widget.
title (str):
The string displayed as the title of the message box. This
option is ignored on Mac OS X, where platform guidelines
forbid the use of a title on this kind of dialog.
initialvalue (float):
The initial value in the entry widget.
minvalue (float):
The minimum allowed value.
maxvalue (float):
The maximum allowed value.
parent (Widget):
Makes the window the logical parent of the message box. The
messagebox is displayed on top of its parent window.
**kwargs (Dict):
Other optional keyword arguments.
Returns:
float:
The float value of the entry widget.
"""
initialvalue = initialvalue or ""
if "position" in kwargs:
position = kwargs.pop("position")
else:
position = None
dialog = QueryDialog(
prompt,
title,
initialvalue,
minvalue,
maxvalue,
datatype=float,
parent=parent,
**kwargs,
)
dialog.show(position)
return dialog._result
@staticmethod
def get_font(parent=None, **kwargs):
"""Request a customized font
![](../../assets/dialogs/querybox-get-font.png)
Parameters:
parent (Widget):
Makes the window the logical parent of the dialog box. The
dialog is displayed on top of its parent window.
**kwargs (Dict):
Other keyword arguments.
Returns:
Font:
A font object.
"""
if "position" in kwargs:
position = kwargs.pop("position")
else:
position = None
dialog = FontDialog(parent=parent, **kwargs)
dialog.show(position)
return dialog.result