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