mirror of
https://github.com/python/cpython.git
synced 2026-06-28 03:41:13 +00:00
Rework SimpleDialog and Dialog to match the look and feel of the native Tk dialogs ::tk_dialog and ::tk::MessageBox. * SimpleDialog is a Python port of ::tk_dialog and Dialog a base class modelled on ::tk::MessageBox. Both adopt the message-box keyboard conventions: button accelerators, a default ring that follows the keyboard focus, and a <Return> binding that invokes the focused button. * Both classes gain a use_ttk parameter that selects the classic Tk or the themed ttk widgets. It controls the widget set and the appearance that the two procedures style differently, but not the keyboard behaviour. * Update _place_window with the Tk 9.1 placement refinements. * The new helpers _temp_grab_focus (a modal grab/focus context manager), _underline_ampersand and _find_alt_key_target (ports of the Tk accelerator-key procedures) can be reused by other tkinter dialogs, as _setup_dialog already is. * Fix several defects uncovered while comparing with the Tcl sources. * Improve the simpledialog demo. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
815 lines
28 KiB
Python
815 lines
28 KiB
Python
#
|
|
# An Introduction to Tkinter
|
|
#
|
|
# Copyright (c) 1997 by Fredrik Lundh
|
|
#
|
|
# This copyright applies to Dialog, askinteger, askfloat and asktring
|
|
#
|
|
# fredrik@pythonware.com
|
|
# http://www.pythonware.com
|
|
#
|
|
"""This modules handles dialog boxes.
|
|
|
|
It contains the following public symbols:
|
|
|
|
SimpleDialog -- A simple but flexible modal dialog box
|
|
|
|
Dialog -- a base class for dialogs
|
|
|
|
askinteger -- get an integer from the user
|
|
|
|
askfloat -- get a float from the user
|
|
|
|
askstring -- get a string from the user
|
|
"""
|
|
|
|
import tkinter
|
|
from tkinter import Button, Label, Tk, Toplevel, TclError
|
|
from tkinter import _get_temp_root, _destroy_temp_root
|
|
from tkinter import messagebox
|
|
from tkinter import ttk
|
|
from tkinter.constants import *
|
|
import contextlib
|
|
|
|
__all__ = ["SimpleDialog", "Dialog", "askinteger", "askfloat", "askstring"]
|
|
|
|
|
|
# The standard dialog icons, which tk::MessageBox draws with themed images
|
|
# instead of the classic monochrome bitmaps.
|
|
_ICON_IMAGES = {
|
|
'error': '::tk::icons::error',
|
|
'info': '::tk::icons::information',
|
|
'question': '::tk::icons::question',
|
|
'warning': '::tk::icons::warning',
|
|
}
|
|
|
|
|
|
# Based on the Tk ::tk_dialog procedure, themed like ::tk::MessageBox by
|
|
# default.
|
|
class SimpleDialog:
|
|
|
|
def _widget(self, klass, master, **kw):
|
|
# Create a themed (ttk) or classic (tkinter) widget.
|
|
return getattr(ttk if self.use_ttk else tkinter, klass)(master, **kw)
|
|
|
|
def __init__(self, master,
|
|
text='', buttons=[], default=None, cancel=None,
|
|
title=None, class_=None, *, bitmap=None, detail='',
|
|
use_ttk=True):
|
|
# Use the themed (ttk) widgets (modelled on tk::MessageBox) by default,
|
|
# or the classic Tk widgets (tk_dialog) if use_ttk is false.
|
|
self.use_ttk = use_ttk
|
|
|
|
# 1. Create the top-level window and divide it into top
|
|
# and bottom parts.
|
|
|
|
class_ = class_ or 'Dialog'
|
|
self.root = Toplevel(master, class_=class_)
|
|
# The default value of the title is space (" ") not the empty string
|
|
# because for some window managers, a
|
|
# w.title("")
|
|
# causes the window title to be w._name instead of the empty string.
|
|
self.root.title(title or ' ')
|
|
self.root.iconname(class_)
|
|
toplevel = master.winfo_toplevel()
|
|
if toplevel.winfo_viewable():
|
|
self.root.wm_transient(toplevel)
|
|
_setup_dialog(self.root)
|
|
if self.use_ttk:
|
|
self.root.configure(
|
|
background=ttk.Style(self.root).lookup('.', 'background'))
|
|
|
|
bot = self._widget('Frame', self.root, name='bot')
|
|
top = self._widget('Frame', self.root, name='top')
|
|
# The classic dialog (tk_dialog) gives its frames a raised border on
|
|
# X11; the themed one (tk::MessageBox) does not.
|
|
if not self.use_ttk and self.root._windowingsystem == 'x11':
|
|
bot.configure(relief=RAISED, bd=1)
|
|
top.configure(relief=RAISED, bd=1)
|
|
bot.pack(side=BOTTOM, fill=BOTH)
|
|
top.pack(side=TOP, fill=BOTH, expand=1)
|
|
bot.grid_anchor(CENTER)
|
|
|
|
# 2. Fill the top part with bitmap and message (use the option
|
|
# database for -wraplength and -font so that they can be
|
|
# overridden by the caller).
|
|
|
|
master.option_add(f'*{class_}.msg.wrapLength', '3i', 'widgetDefault')
|
|
master.option_add(f'*{class_}.msg.font', 'TkCaptionFont', 'widgetDefault')
|
|
master.option_add(f'*{class_}.dtl.wrapLength', '3i', 'widgetDefault')
|
|
master.option_add(f'*{class_}.dtl.font', 'TkDefaultFont', 'widgetDefault')
|
|
|
|
# tk::MessageBox and tk_dialog pad the top part differently.
|
|
pad = '2m' if self.use_ttk else '3m'
|
|
|
|
# The bitmap is packed first to claim the whole left side; the message
|
|
# and detail stack on its right, as in tk::MessageBox.
|
|
if bitmap:
|
|
if self.root._windowingsystem == 'aqua' and bitmap == 'error':
|
|
bitmap = 'stop'
|
|
image = _ICON_IMAGES.get(bitmap) if self.use_ttk else None
|
|
if image is not None and self.root.winfo_depth() >= 4:
|
|
# tk::MessageBox draws the standard icons with themed images.
|
|
self.bitmap = ttk.Label(self.root, name='bitmap', image=image)
|
|
else:
|
|
# ttk.Label has no -bitmap option, so use a classic label.
|
|
self.bitmap = Label(self.root, name='bitmap', bitmap=bitmap)
|
|
# The themed dialog anchors the icon to the top (like the bitmap's
|
|
# "nw" sticky in tk::MessageBox); the classic one centers it.
|
|
anchor = N if self.use_ttk else CENTER
|
|
self.bitmap.pack(in_=top, side=LEFT, anchor=anchor,
|
|
padx=pad, pady=pad)
|
|
|
|
self.message = self._widget('Label', self.root, name='msg',
|
|
justify=LEFT, text=text)
|
|
self.detail = None
|
|
if self.use_ttk:
|
|
# tk::MessageBox anchors the message to the top-left corner.
|
|
self.message.configure(anchor=NW)
|
|
# The message expands to fill the space, unless there is a detail
|
|
# message below it which takes the extra space instead (cf.
|
|
# tk::MessageBox).
|
|
self.message.pack(in_=top, side=TOP, expand=not detail, fill=BOTH,
|
|
padx=pad, pady=pad)
|
|
if detail:
|
|
self.detail = self._widget('Label', self.root, name='dtl',
|
|
justify=LEFT, text=detail)
|
|
if self.use_ttk:
|
|
self.detail.configure(anchor=NW)
|
|
self.detail.pack(in_=top, side=TOP, expand=1, fill=BOTH,
|
|
padx=pad, pady=(0, pad))
|
|
|
|
self.frame = bot
|
|
self.num = default
|
|
self.cancel = cancel
|
|
self.default = default
|
|
|
|
# 3. Create a row of buttons at the bottom of the dialog. Each entry
|
|
# of "buttons" is either a label, or a mapping of button options -- like
|
|
# the "[name ?-option value ...?]" button specs in tk::MessageBox.
|
|
|
|
# tk::MessageBox and tk_dialog space the buttons differently.
|
|
padx, pady = ('3m', '2m') if self.use_ttk else ('7.5p', '3p')
|
|
self._buttons = []
|
|
for i, but in enumerate(buttons):
|
|
opts = {'text': but} if isinstance(but, str) else dict(but)
|
|
b = self._widget('Button', self.root, name=f'button{i}', **opts)
|
|
# The dialog controls the command and the default ring, overriding
|
|
# anything set in the button options (cf. tk::MessageBox).
|
|
b.configure(command=(lambda self=self, i=i: self.done(i)),
|
|
default=ACTIVE if i == default else NORMAL)
|
|
# Alt + the underlined character (an "underline" button option)
|
|
# invokes the button (cf. tk::AmpWidget in tk::MessageBox).
|
|
b.bind('<<AltUnderlined>>', lambda e: e.widget.invoke())
|
|
b.grid(in_=bot, column=i, row=0, sticky=EW, padx=padx, pady=pady)
|
|
# tk::MessageBox makes the buttons equal width; tk_dialog does not.
|
|
bot.grid_columnconfigure(i, uniform='buttons' if self.use_ttk else '')
|
|
# We boost the size of some Mac buttons for l&f
|
|
if self.root._windowingsystem == 'aqua':
|
|
if str(opts.get('text', '')).lower() in ('ok', 'cancel'):
|
|
bot.grid_columnconfigure(i, minsize=90)
|
|
b.grid_configure(pady=7)
|
|
self._buttons.append(b)
|
|
|
|
# 4. Bind <Return> to invoke the focused button, or the default button
|
|
# if the focus is elsewhere. Unlike tk_dialog (which tracks the focus
|
|
# by rebinding <Return> on <<PrevWindow>>/<<NextWindow>>), this reads
|
|
# the live focus, like tk::MessageBox, so it also works with the mouse.
|
|
|
|
def on_return(event):
|
|
if event.widget.winfo_class() in ('Button', 'TButton'):
|
|
event.widget.invoke()
|
|
else:
|
|
self.return_event(event)
|
|
self.root.bind('<Return>', on_return)
|
|
# Alt + an underlined character invokes the matching button (cf.
|
|
# ::tk::AltKeyInDialog, bound by tk::MessageBox).
|
|
self.root.bind('<Alt-Key>', self._alt_key)
|
|
# The default ring follows the keyboard focus among the buttons
|
|
# (cf. tk::MessageBox).
|
|
self.root.bind('<FocusIn>', lambda e: self._set_default(e.widget, ACTIVE))
|
|
self.root.bind('<FocusOut>', lambda e: self._set_default(e.widget, NORMAL))
|
|
|
|
# 5. Bind <Destroy> to record the cancel index, in case the window is
|
|
# destroyed by something else (e.g. its parent being destroyed).
|
|
|
|
def on_destroy(event):
|
|
self.num = cancel
|
|
self.root.quit()
|
|
self.root.bind('<Destroy>', on_destroy)
|
|
|
|
self.root.protocol('WM_DELETE_WINDOW', self.wm_delete_window)
|
|
_place_window(self.root, master)
|
|
|
|
def go(self):
|
|
self.root.wait_visibility()
|
|
if self.default is not None:
|
|
focus = self._buttons[self.default]
|
|
else:
|
|
focus = self.root
|
|
|
|
with _temp_grab_focus(self.root, focus):
|
|
try:
|
|
self.root.mainloop()
|
|
finally:
|
|
try:
|
|
# It's possible that the window has already been destroyed,
|
|
# hence this "try/except". Delete the Destroy handler so that
|
|
# self.num doesn't get reset by it.
|
|
self.root.bind('<Destroy>', '')
|
|
except TclError:
|
|
pass
|
|
return self.num
|
|
|
|
def return_event(self, event):
|
|
if self.default is None:
|
|
self.root.bell()
|
|
else:
|
|
self.done(self.default)
|
|
|
|
def wm_delete_window(self):
|
|
if self.cancel is None:
|
|
self.root.bell()
|
|
else:
|
|
self.done(self.cancel)
|
|
|
|
def done(self, num):
|
|
self.num = num
|
|
self.root.quit()
|
|
|
|
def _alt_key(self, event):
|
|
# Invoke the button whose accelerator matches the Alt key.
|
|
target = _find_alt_key_target(self.root, event.char)
|
|
if target is not None:
|
|
target.event_generate('<<AltUnderlined>>')
|
|
|
|
def _set_default(self, widget, state):
|
|
# Set a button's default ring.
|
|
if widget.winfo_class() in ('Button', 'TButton'):
|
|
widget.configure(default=state)
|
|
|
|
|
|
# A base class for custom dialogs, with a button box modelled on
|
|
# ::tk::MessageBox.
|
|
class Dialog(Toplevel):
|
|
|
|
'''Class to open dialogs.
|
|
|
|
This class is intended as a base class for custom dialogs
|
|
'''
|
|
|
|
def _widget(self, klass, master, **kw):
|
|
# Create a themed (ttk) or classic (tkinter) widget.
|
|
return getattr(ttk if self.use_ttk else tkinter, klass)(master, **kw)
|
|
|
|
def _frame(self, name):
|
|
# The classic dialog (tk_dialog) gives its frames a raised border on
|
|
# X11; the themed one (tk::MessageBox) does not.
|
|
frame = self._widget('Frame', self, name=name)
|
|
if not self.use_ttk and self._windowingsystem == 'x11':
|
|
frame.configure(relief=RAISED, bd=1)
|
|
return frame
|
|
|
|
def __init__(self, parent, title=None, *, use_ttk=False):
|
|
'''Initialize a dialog.
|
|
|
|
Arguments:
|
|
|
|
parent -- a parent window (the application window)
|
|
|
|
title -- the dialog title
|
|
|
|
use_ttk -- use the classic Tk widgets (the default), or the themed
|
|
(ttk) widgets if true
|
|
'''
|
|
# Use the classic Tk widgets by default, for compatibility: the themed
|
|
# (ttk) widgets set a themed background that classic widgets added by a
|
|
# subclass in body() would not match. The query dialogs opt into ttk.
|
|
self.use_ttk = use_ttk
|
|
|
|
master = parent
|
|
if master is None:
|
|
master = _get_temp_root()
|
|
|
|
Toplevel.__init__(self, master, class_='Dialog')
|
|
|
|
if self.use_ttk:
|
|
# Use a single background colour for the whole dialog so that it
|
|
# blends with the ttk widgets (cf. tk::MessageBox).
|
|
self.configure(
|
|
background=ttk.Style(self).lookup('.', 'background'))
|
|
|
|
self.withdraw() # remain invisible for now
|
|
# If the parent is not viewable, don't
|
|
# make the child transient, or else it
|
|
# would be opened withdrawn
|
|
if parent is not None and parent.winfo_viewable():
|
|
self.transient(parent)
|
|
|
|
self.title(title or ' ')
|
|
self.iconname('Dialog')
|
|
|
|
_setup_dialog(self)
|
|
|
|
self.parent = parent
|
|
|
|
self.result = None
|
|
|
|
body = self._frame('top')
|
|
self.initial_focus = self.body(body)
|
|
body.pack(side=TOP, fill=BOTH, expand=1)
|
|
|
|
self.buttonbox()
|
|
|
|
if self.initial_focus is None:
|
|
self.initial_focus = self
|
|
|
|
self.protocol("WM_DELETE_WINDOW", self.cancel)
|
|
|
|
_place_window(self, parent)
|
|
|
|
# wait for window to appear on screen before calling grab_set
|
|
self.wait_visibility()
|
|
# Dialog destroys itself in ok()/cancel(), so let _temp_grab_focus
|
|
# save/restore the focus and grab without destroying the window.
|
|
with _temp_grab_focus(self, self.initial_focus, destroy=False):
|
|
self.wait_window(self)
|
|
|
|
def destroy(self):
|
|
'''Destroy the window'''
|
|
self.initial_focus = None
|
|
Toplevel.destroy(self)
|
|
_destroy_temp_root(self.master)
|
|
|
|
#
|
|
# construction hooks
|
|
|
|
def body(self, master):
|
|
'''create dialog body.
|
|
|
|
return widget that should have initial focus.
|
|
This method should be overridden, and is called
|
|
by the __init__ method.
|
|
'''
|
|
pass
|
|
|
|
def buttonbox(self):
|
|
'''add standard button box.
|
|
|
|
override if you do not want the standard buttons
|
|
'''
|
|
|
|
box = self._frame('bot')
|
|
|
|
# tk::MessageBox and tk_dialog space the buttons differently.
|
|
padx, pady = ('3m', '2m') if self.use_ttk else ('7.5p', '3p')
|
|
for i, (name, label, command) in enumerate(
|
|
(('ok', '&OK', self.ok), ('cancel', '&Cancel', self.cancel))):
|
|
# Create a button with an accelerator key marked by "&" in the text
|
|
# (cf. tk::AmpWidget).
|
|
text, underline = _underline_ampersand(label)
|
|
b = self._widget('Button', self, name=name, text=text,
|
|
underline=underline, command=command,
|
|
default=ACTIVE if name == 'ok' else NORMAL)
|
|
b.bind('<<AltUnderlined>>', lambda e: e.widget.invoke())
|
|
b.grid(in_=box, column=i, row=0, sticky=EW, padx=padx, pady=pady)
|
|
# tk::MessageBox makes the buttons equal width; tk_dialog does not.
|
|
box.grid_columnconfigure(i, uniform='buttons' if self.use_ttk else '')
|
|
if self._windowingsystem == 'aqua':
|
|
box.grid_columnconfigure(i, minsize=90)
|
|
b.grid_configure(pady=7)
|
|
|
|
# Alt + an underlined character invokes the matching button (cf.
|
|
# ::tk::AltKeyInDialog, bound by tk::MessageBox).
|
|
self.bind('<Alt-Key>', self._alt_key)
|
|
# The default ring follows the keyboard focus among the buttons
|
|
# (cf. tk::MessageBox).
|
|
self.bind('<FocusIn>', lambda e: self._set_default(e.widget, ACTIVE))
|
|
self.bind('<FocusOut>', lambda e: self._set_default(e.widget, NORMAL))
|
|
self.bind('<Return>', self._return_event)
|
|
self.bind('<Escape>', self.cancel)
|
|
|
|
box.pack(side=BOTTOM, fill=BOTH)
|
|
box.grid_anchor(CENTER)
|
|
|
|
def _set_default(self, widget, state):
|
|
# Set a button's default ring.
|
|
if widget.winfo_class() in ('Button', 'TButton'):
|
|
widget.configure(default=state)
|
|
|
|
def _return_event(self, event):
|
|
# Invoke the focused button, or accept the dialog if the focus is
|
|
# elsewhere (e.g. in an entry).
|
|
widget = event.widget
|
|
if widget.winfo_class() in ('Button', 'TButton'):
|
|
widget.invoke()
|
|
else:
|
|
self.ok()
|
|
|
|
def _alt_key(self, event):
|
|
# Invoke the button whose accelerator matches the Alt key.
|
|
target = _find_alt_key_target(self, event.char)
|
|
if target is not None:
|
|
target.event_generate('<<AltUnderlined>>')
|
|
|
|
#
|
|
# standard button semantics
|
|
|
|
def ok(self, event=None):
|
|
|
|
if not self.validate():
|
|
self.initial_focus.focus_set() # put focus back
|
|
return
|
|
|
|
self.withdraw()
|
|
self.update_idletasks()
|
|
|
|
try:
|
|
self.apply()
|
|
finally:
|
|
self.cancel()
|
|
|
|
def cancel(self, event=None):
|
|
self.destroy()
|
|
|
|
#
|
|
# command hooks
|
|
|
|
def validate(self):
|
|
'''validate the data
|
|
|
|
This method is called automatically to validate the data before the
|
|
dialog is destroyed. By default, it always validates OK.
|
|
'''
|
|
|
|
return True # override
|
|
|
|
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
|
|
|
|
|
|
# Place a toplevel window at the center of parent or screen
|
|
# This is a Python implementation of ::tk::PlaceWindow.
|
|
def _wm_dimension(w, command):
|
|
# tk::WMFrameWidth and tk::WMTitleHeight (added in Tk 9.1) return the size
|
|
# of the window manager decoration. They are 0 except in SDL2 builds of
|
|
# Tk, and are missing in older versions.
|
|
try:
|
|
return int(w.tk.call(command))
|
|
except TclError:
|
|
return 0
|
|
|
|
|
|
def _place_window(w, parent=None):
|
|
w.wm_withdraw() # Remain invisible while we figure out the geometry
|
|
w.update_idletasks() # Actualize geometry information
|
|
|
|
screenwidth = w.winfo_screenwidth()
|
|
screenheight = w.winfo_screenheight()
|
|
minwidth = w.winfo_reqwidth()
|
|
minheight = w.winfo_reqheight()
|
|
maxwidth = w.winfo_vrootwidth()
|
|
maxheight = w.winfo_vrootheight()
|
|
# "wm geometry" operates in window manager coordinates and thus includes a
|
|
# possible decoration frame and the title bar.
|
|
framewidth = _wm_dimension(w, '::tk::WMFrameWidth')
|
|
titleheight = _wm_dimension(w, '::tk::WMTitleHeight')
|
|
constrain = False
|
|
if minwidth + 2*framewidth > screenwidth:
|
|
minwidth = screenwidth - 2*framewidth
|
|
constrain = True
|
|
if minheight + titleheight + framewidth > screenheight:
|
|
minheight = screenheight - titleheight - framewidth
|
|
constrain = True
|
|
|
|
if parent is not None and parent.winfo_ismapped():
|
|
# Center the window over the parent (which must be mapped).
|
|
x = parent.winfo_rootx() + (parent.winfo_width() - minwidth) // 2
|
|
y = parent.winfo_rooty() + (parent.winfo_height() - minheight) // 2
|
|
# Make sure that the window is on the screen and does not cover the
|
|
# window manager decoration.
|
|
vrootx = w.winfo_vrootx()
|
|
vrooty = w.winfo_vrooty()
|
|
x = min(x, vrootx + maxwidth - minwidth - framewidth)
|
|
x = max(x, vrootx + framewidth)
|
|
y = min(y, vrooty + maxheight - minheight - framewidth)
|
|
y = max(y, vrooty + titleheight)
|
|
if w._windowingsystem == 'aqua':
|
|
# Avoid the native menu bar which sits on top of everything.
|
|
y = max(y, 22 + titleheight)
|
|
else:
|
|
# Center the window on the screen.
|
|
x = (screenwidth - minwidth) // 2
|
|
y = (screenheight - minheight) // 2
|
|
|
|
w.wm_maxsize(maxwidth, maxheight)
|
|
geometry = f'{minwidth}x{minheight}' if constrain else ''
|
|
geometry += '+%d+%d' % (x - framewidth, y - titleheight)
|
|
w.wm_geometry(geometry)
|
|
w.wm_deiconify() # Become visible at the desired location
|
|
|
|
|
|
def _underline_ampersand(text):
|
|
# Like tk::UnderlineAmpersand: "&&" is a literal "&"; a single "&" marks
|
|
# the following character as the underlined accelerator. Return the text
|
|
# without the markers and the index of the accelerator (-1 if none).
|
|
chars = []
|
|
underline = -1
|
|
i = 0
|
|
while i < len(text):
|
|
if text[i] == '&':
|
|
if text[i+1:i+2] == '&':
|
|
chars.append('&')
|
|
i += 2
|
|
continue
|
|
if underline < 0:
|
|
underline = len(chars)
|
|
else:
|
|
chars.append(text[i])
|
|
i += 1
|
|
return ''.join(chars), underline
|
|
|
|
|
|
def _find_alt_key_target(widget, char):
|
|
# Like tk::FindAltKeyTarget: find the widget whose underlined character
|
|
# matches CHAR, searching the widget and its descendants.
|
|
if widget.winfo_class() in ('Button', 'Checkbutton', 'Label', 'Radiobutton',
|
|
'TButton', 'TCheckbutton', 'TLabel',
|
|
'TRadiobutton'):
|
|
try:
|
|
under = int(widget.cget('underline'))
|
|
except (TclError, ValueError):
|
|
under = -1
|
|
text = str(widget.cget('text'))
|
|
if 0 <= under < len(text) and char.lower() == text[under].lower():
|
|
return widget
|
|
for child in widget.winfo_children():
|
|
target = _find_alt_key_target(child, char)
|
|
if target is not None:
|
|
return target
|
|
return None
|
|
|
|
|
|
def _setup_dialog(w):
|
|
if w._windowingsystem == "aqua":
|
|
if w.info_patchlevel() >= (9, 1):
|
|
w.wm_attributes(stylemask='titled')
|
|
else:
|
|
w.tk.call('::tk::unsupported::MacWindowStyle', 'style',
|
|
w, 'moveableModal', '')
|
|
elif w._windowingsystem == "x11":
|
|
w.wm_attributes(type="dialog")
|
|
|
|
# --------------------------------------------------------------------
|
|
# convenience dialogues
|
|
|
|
class _QueryDialog(Dialog):
|
|
|
|
def __init__(self, title, prompt,
|
|
initialvalue=None,
|
|
minvalue = None, maxvalue = None,
|
|
parent = None, *, use_ttk=True):
|
|
|
|
self.prompt = prompt
|
|
self.minvalue = minvalue
|
|
self.maxvalue = maxvalue
|
|
|
|
self.initialvalue = initialvalue
|
|
|
|
Dialog.__init__(self, parent, title, use_ttk=use_ttk)
|
|
|
|
def destroy(self):
|
|
self.entry = None
|
|
Dialog.destroy(self)
|
|
|
|
def body(self, master):
|
|
|
|
# Wrap a long prompt, like tk::MessageBox wraps its message at 3 inches.
|
|
w = self._widget('Label', master, anchor=NW, text=self.prompt,
|
|
justify=LEFT, wraplength='3i')
|
|
w.grid(in_=master, padx='2m', pady='2m', sticky=NSEW)
|
|
|
|
self.entry = self._widget('Entry', master, name='entry')
|
|
self.entry.grid(row=1, in_=master, padx='2m', pady=(0, '2m'), sticky=NSEW)
|
|
master.grid_rowconfigure(1, weight=1)
|
|
# The prompt and entry expand to the full width, like tk::MessageBox
|
|
# gives weight to its message column.
|
|
master.grid_columnconfigure(0, weight=1)
|
|
|
|
if self.initialvalue is not None:
|
|
self.entry.insert(0, self.initialvalue)
|
|
self.entry.select_range(0, END)
|
|
|
|
return self.entry
|
|
|
|
def validate(self):
|
|
try:
|
|
result = self.getresult()
|
|
except ValueError:
|
|
messagebox.showwarning(
|
|
"Illegal value",
|
|
self.errormessage + "\nPlease try again",
|
|
parent = self
|
|
)
|
|
return False
|
|
|
|
if self.minvalue is not None and result < self.minvalue:
|
|
messagebox.showwarning(
|
|
"Too small",
|
|
"The allowed minimum value is %s. "
|
|
"Please try again." % self.minvalue,
|
|
parent = self
|
|
)
|
|
return False
|
|
|
|
if self.maxvalue is not None and result > self.maxvalue:
|
|
messagebox.showwarning(
|
|
"Too large",
|
|
"The allowed maximum value is %s. "
|
|
"Please try again." % self.maxvalue,
|
|
parent = self
|
|
)
|
|
return False
|
|
|
|
self.result = result
|
|
|
|
return True
|
|
|
|
|
|
class _QueryInteger(_QueryDialog):
|
|
errormessage = "Not an integer."
|
|
|
|
def getresult(self):
|
|
return self.getint(self.entry.get())
|
|
|
|
|
|
def askinteger(title, prompt, **kw):
|
|
'''get an integer from the user
|
|
|
|
Arguments:
|
|
|
|
title -- the dialog title
|
|
prompt -- the label text
|
|
**kw -- see SimpleDialog class
|
|
|
|
Return value is an integer
|
|
'''
|
|
d = _QueryInteger(title, prompt, **kw)
|
|
return d.result
|
|
|
|
|
|
class _QueryFloat(_QueryDialog):
|
|
errormessage = "Not a floating-point value."
|
|
|
|
def getresult(self):
|
|
return self.getdouble(self.entry.get())
|
|
|
|
|
|
def askfloat(title, prompt, **kw):
|
|
'''get a float from the user
|
|
|
|
Arguments:
|
|
|
|
title -- the dialog title
|
|
prompt -- the label text
|
|
**kw -- see SimpleDialog class
|
|
|
|
Return value is a float
|
|
'''
|
|
d = _QueryFloat(title, prompt, **kw)
|
|
return d.result
|
|
|
|
|
|
class _QueryString(_QueryDialog):
|
|
def __init__(self, *args, **kw):
|
|
if "show" in kw:
|
|
self.__show = kw["show"]
|
|
del kw["show"]
|
|
else:
|
|
self.__show = None
|
|
_QueryDialog.__init__(self, *args, **kw)
|
|
|
|
def body(self, master):
|
|
entry = _QueryDialog.body(self, master)
|
|
if self.__show is not None:
|
|
entry.configure(show=self.__show)
|
|
return entry
|
|
|
|
def getresult(self):
|
|
return self.entry.get()
|
|
|
|
|
|
def askstring(title, prompt, **kw):
|
|
'''get a string from the user
|
|
|
|
Arguments:
|
|
|
|
title -- the dialog title
|
|
prompt -- the label text
|
|
**kw -- see SimpleDialog class
|
|
|
|
Return value is a string
|
|
'''
|
|
d = _QueryString(title, prompt, **kw)
|
|
return d.result
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def _temp_grab_focus(grab, focus=None, destroy=True):
|
|
old_focus = grab.focus_get()
|
|
old_grab = grab.grab_current()
|
|
if old_grab is not None and old_grab.winfo_exists():
|
|
old_status = old_grab.grab_status()
|
|
else:
|
|
old_status = None
|
|
# The "grab" command will fail if another application
|
|
# already holds the grab. So catch it.
|
|
try:
|
|
grab.grab_set()
|
|
except TclError:
|
|
pass
|
|
try:
|
|
if focus is not None and focus.winfo_exists():
|
|
focus.focus_set()
|
|
|
|
yield
|
|
|
|
finally:
|
|
if old_focus is not None:
|
|
try:
|
|
old_focus.focus_set()
|
|
except TclError:
|
|
pass
|
|
try:
|
|
grab.grab_release()
|
|
except TclError:
|
|
pass
|
|
if destroy:
|
|
try:
|
|
grab.destroy()
|
|
except TclError:
|
|
pass
|
|
if (old_grab is not None and old_grab.winfo_exists()
|
|
and old_grab.winfo_ismapped()):
|
|
# The "grab" command will fail if another application
|
|
# already holds the grab. So catch it.
|
|
try:
|
|
if old_status == 'global':
|
|
old_grab.grab_set_global()
|
|
else:
|
|
old_grab.grab_set()
|
|
except TclError:
|
|
pass
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
def test():
|
|
root = Tk()
|
|
use_ttk = tkinter.BooleanVar(root, value=True)
|
|
|
|
def test_dialog():
|
|
d = SimpleDialog(root,
|
|
text="This is a test dialog. "
|
|
"Would this have been an actual dialog, "
|
|
"the buttons below would have been glowing "
|
|
"in soft pink light.\n"
|
|
"Do you believe this?",
|
|
buttons=[{'text': 'Yes', 'underline': 0},
|
|
{'text': 'No', 'underline': 0},
|
|
{'text': 'Cancel', 'underline': 0}],
|
|
default=0,
|
|
cancel=2,
|
|
title="Test Dialog",
|
|
bitmap='question',
|
|
detail="Alt+Y, Alt+N and Alt+C work too.",
|
|
use_ttk=use_ttk.get())
|
|
print(d.go())
|
|
|
|
def test_integer():
|
|
print(askinteger("Spam", "Egg count", initialvalue=12*12,
|
|
use_ttk=use_ttk.get()))
|
|
|
|
def test_float():
|
|
print(askfloat("Spam", "Egg weight\n(in tons)", minvalue=1,
|
|
maxvalue=100, use_ttk=use_ttk.get()))
|
|
|
|
def test_string():
|
|
print(askstring("Spam", "Egg label", use_ttk=use_ttk.get()))
|
|
|
|
tkinter.Checkbutton(root, text='Use themed (ttk) widgets',
|
|
variable=use_ttk).pack(fill=X)
|
|
Button(root, text='SimpleDialog', command=test_dialog).pack(fill=X)
|
|
Button(root, text='askinteger', command=test_integer).pack(fill=X)
|
|
Button(root, text='askfloat', command=test_float).pack(fill=X)
|
|
Button(root, text='askstring', command=test_string).pack(fill=X)
|
|
Button(root, text='Quit', command=root.quit).pack(fill=X)
|
|
root.mainloop()
|
|
|
|
test()
|