1#
2# An Introduction to Tkinter
3#
4# Copyright (c) 1997 by Fredrik Lundh
5#
6# This copyright applies to Dialog, askinteger, askfloat and asktring
7#
8# fredrik@pythonware.com
9# http://www.pythonware.com
10#
11"""This modules handles dialog boxes.
12
13It contains the following public symbols:
14
15SimpleDialog -- A simple but flexible modal dialog box
16
17Dialog -- a base class for dialogs
18
19askinteger -- get an integer from the user
20
21askfloat -- get a float from the user
22
23askstring -- get a string from the user
24"""
25
26from tkinter import *
27from tkinter import messagebox
28
29import tkinter # used at _QueryDialog for tkinter._default_root
30
31
32class SimpleDialog:
33
34    def __init__(self, master,
35                 text='', buttons=[], default=None, cancel=None,
36                 title=None, class_=None):
37        if class_:
38            self.root = Toplevel(master, class_=class_)
39        else:
40            self.root = Toplevel(master)
41        if title:
42            self.root.title(title)
43            self.root.iconname(title)
44        self.message = Message(self.root, text=text, aspect=400)
45        self.message.pack(expand=1, fill=BOTH)
46        self.frame = Frame(self.root)
47        self.frame.pack()
48        self.num = default
49        self.cancel = cancel
50        self.default = default
51        self.root.bind('<Return>', self.return_event)
52        for num in range(len(buttons)):
53            s = buttons[num]
54            b = Button(self.frame, text=s,
55                       command=(lambda self=self, num=num: self.done(num)))
56            if num == default:
57                b.config(relief=RIDGE, borderwidth=8)
58            b.pack(side=LEFT, fill=BOTH, expand=1)
59        self.root.protocol('WM_DELETE_WINDOW', self.wm_delete_window)
60        self._set_transient(master)
61
62    def _set_transient(self, master, relx=0.5, rely=0.3):
63        widget = self.root
64        widget.withdraw() # Remain invisible while we figure out the geometry
65        widget.transient(master)
66        widget.update_idletasks() # Actualize geometry information
67        if master.winfo_ismapped():
68            m_width = master.winfo_width()
69            m_height = master.winfo_height()
70            m_x = master.winfo_rootx()
71            m_y = master.winfo_rooty()
72        else:
73            m_width = master.winfo_screenwidth()
74            m_height = master.winfo_screenheight()
75            m_x = m_y = 0
76        w_width = widget.winfo_reqwidth()
77        w_height = widget.winfo_reqheight()
78        x = m_x + (m_width - w_width) * relx
79        y = m_y + (m_height - w_height) * rely
80        if x+w_width > master.winfo_screenwidth():
81            x = master.winfo_screenwidth() - w_width
82        elif x < 0:
83            x = 0
84        if y+w_height > master.winfo_screenheight():
85            y = master.winfo_screenheight() - w_height
86        elif y < 0:
87            y = 0
88        widget.geometry("+%d+%d" % (x, y))
89        widget.deiconify() # Become visible at the desired location
90
91    def go(self):
92        self.root.wait_visibility()
93        self.root.grab_set()
94        self.root.mainloop()
95        self.root.destroy()
96        return self.num
97
98    def return_event(self, event):
99        if self.default is None:
100            self.root.bell()
101        else:
102            self.done(self.default)
103
104    def wm_delete_window(self):
105        if self.cancel is None:
106            self.root.bell()
107        else:
108            self.done(self.cancel)
109
110    def done(self, num):
111        self.num = num
112        self.root.quit()
113
114
115class Dialog(Toplevel):
116
117    '''Class to open dialogs.
118
119    This class is intended as a base class for custom dialogs
120    '''
121
122    def __init__(self, parent, title = None):
123        '''Initialize a dialog.
124
125        Arguments:
126
127            parent -- a parent window (the application window)
128
129            title -- the dialog title
130        '''
131        Toplevel.__init__(self, parent)
132
133        self.withdraw() # remain invisible for now
134        # If the master is not viewable, don't
135        # make the child transient, or else it
136        # would be opened withdrawn
137        if parent.winfo_viewable():
138            self.transient(parent)
139
140        if title:
141            self.title(title)
142
143        self.parent = parent
144
145        self.result = None
146
147        body = Frame(self)
148        self.initial_focus = self.body(body)
149        body.pack(padx=5, pady=5)
150
151        self.buttonbox()
152
153        if not self.initial_focus:
154            self.initial_focus = self
155
156        self.protocol("WM_DELETE_WINDOW", self.cancel)
157
158        if self.parent is not None:
159            self.geometry("+%d+%d" % (parent.winfo_rootx()+50,
160                                      parent.winfo_rooty()+50))
161
162        self.deiconify() # become visible now
163
164        self.initial_focus.focus_set()
165
166        # wait for window to appear on screen before calling grab_set
167        self.wait_visibility()
168        self.grab_set()
169        self.wait_window(self)
170
171    def destroy(self):
172        '''Destroy the window'''
173        self.initial_focus = None
174        Toplevel.destroy(self)
175
176    #
177    # construction hooks
178
179    def body(self, master):
180        '''create dialog body.
181
182        return widget that should have initial focus.
183        This method should be overridden, and is called
184        by the __init__ method.
185        '''
186        pass
187
188    def buttonbox(self):
189        '''add standard button box.
190
191        override if you do not want the standard buttons
192        '''
193
194        box = Frame(self)
195
196        w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE)
197        w.pack(side=LEFT, padx=5, pady=5)
198        w = Button(box, text="Cancel", width=10, command=self.cancel)
199        w.pack(side=LEFT, padx=5, pady=5)
200
201        self.bind("<Return>", self.ok)
202        self.bind("<Escape>", self.cancel)
203
204        box.pack()
205
206    #
207    # standard button semantics
208
209    def ok(self, event=None):
210
211        if not self.validate():
212            self.initial_focus.focus_set() # put focus back
213            return
214
215        self.withdraw()
216        self.update_idletasks()
217
218        try:
219            self.apply()
220        finally:
221            self.cancel()
222
223    def cancel(self, event=None):
224
225        # put focus back to the parent window
226        if self.parent is not None:
227            self.parent.focus_set()
228        self.destroy()
229
230    #
231    # command hooks
232
233    def validate(self):
234        '''validate the data
235
236        This method is called automatically to validate the data before the
237        dialog is destroyed. By default, it always validates OK.
238        '''
239
240        return 1 # override
241
242    def apply(self):
243        '''process the data
244
245        This method is called automatically to process the data, *after*
246        the dialog is destroyed. By default, it does nothing.
247        '''
248
249        pass # override
250
251
252# --------------------------------------------------------------------
253# convenience dialogues
254
255class _QueryDialog(Dialog):
256
257    def __init__(self, title, prompt,
258                 initialvalue=None,
259                 minvalue = None, maxvalue = None,
260                 parent = None):
261
262        if not parent:
263            parent = tkinter._default_root
264
265        self.prompt   = prompt
266        self.minvalue = minvalue
267        self.maxvalue = maxvalue
268
269        self.initialvalue = initialvalue
270
271        Dialog.__init__(self, parent, title)
272
273    def destroy(self):
274        self.entry = None
275        Dialog.destroy(self)
276
277    def body(self, master):
278
279        w = Label(master, text=self.prompt, justify=LEFT)
280        w.grid(row=0, padx=5, sticky=W)
281
282        self.entry = Entry(master, name="entry")
283        self.entry.grid(row=1, padx=5, sticky=W+E)
284
285        if self.initialvalue is not None:
286            self.entry.insert(0, self.initialvalue)
287            self.entry.select_range(0, END)
288
289        return self.entry
290
291    def validate(self):
292        try:
293            result = self.getresult()
294        except ValueError:
295            messagebox.showwarning(
296                "Illegal value",
297                self.errormessage + "\nPlease try again",
298                parent = self
299            )
300            return 0
301
302        if self.minvalue is not None and result < self.minvalue:
303            messagebox.showwarning(
304                "Too small",
305                "The allowed minimum value is %s. "
306                "Please try again." % self.minvalue,
307                parent = self
308            )
309            return 0
310
311        if self.maxvalue is not None and result > self.maxvalue:
312            messagebox.showwarning(
313                "Too large",
314                "The allowed maximum value is %s. "
315                "Please try again." % self.maxvalue,
316                parent = self
317            )
318            return 0
319
320        self.result = result
321
322        return 1
323
324
325class _QueryInteger(_QueryDialog):
326    errormessage = "Not an integer."
327
328    def getresult(self):
329        return self.getint(self.entry.get())
330
331
332def askinteger(title, prompt, **kw):
333    '''get an integer from the user
334
335    Arguments:
336
337        title -- the dialog title
338        prompt -- the label text
339        **kw -- see SimpleDialog class
340
341    Return value is an integer
342    '''
343    d = _QueryInteger(title, prompt, **kw)
344    return d.result
345
346
347class _QueryFloat(_QueryDialog):
348    errormessage = "Not a floating point value."
349
350    def getresult(self):
351        return self.getdouble(self.entry.get())
352
353
354def askfloat(title, prompt, **kw):
355    '''get a float from the user
356
357    Arguments:
358
359        title -- the dialog title
360        prompt -- the label text
361        **kw -- see SimpleDialog class
362
363    Return value is a float
364    '''
365    d = _QueryFloat(title, prompt, **kw)
366    return d.result
367
368
369class _QueryString(_QueryDialog):
370    def __init__(self, *args, **kw):
371        if "show" in kw:
372            self.__show = kw["show"]
373            del kw["show"]
374        else:
375            self.__show = None
376        _QueryDialog.__init__(self, *args, **kw)
377
378    def body(self, master):
379        entry = _QueryDialog.body(self, master)
380        if self.__show is not None:
381            entry.configure(show=self.__show)
382        return entry
383
384    def getresult(self):
385        return self.entry.get()
386
387
388def askstring(title, prompt, **kw):
389    '''get a string from the user
390
391    Arguments:
392
393        title -- the dialog title
394        prompt -- the label text
395        **kw -- see SimpleDialog class
396
397    Return value is a string
398    '''
399    d = _QueryString(title, prompt, **kw)
400    return d.result
401
402
403if __name__ == '__main__':
404
405    def test():
406        root = Tk()
407        def doit(root=root):
408            d = SimpleDialog(root,
409                         text="This is a test dialog.  "
410                              "Would this have been an actual dialog, "
411                              "the buttons below would have been glowing "
412                              "in soft pink light.\n"
413                              "Do you believe this?",
414                         buttons=["Yes", "No", "Cancel"],
415                         default=0,
416                         cancel=2,
417                         title="Test Dialog")
418            print(d.go())
419            print(askinteger("Spam", "Egg count", initialvalue=12*12))
420            print(askfloat("Spam", "Egg weight\n(in tons)", minvalue=1,
421                           maxvalue=100))
422            print(askstring("Spam", "Egg label"))
423        t = Button(root, text='Test', command=doit)
424        t.pack()
425        q = Button(root, text='Quit', command=t.quit)
426        q.pack()
427        t.mainloop()
428
429    test()
430