1"""Assorted Tk-related subroutines used in Grail."""
2
3
4from types import *
5from Tkinter import *
6
7def _clear_entry_widget(event):
8    try:
9        widget = event.widget
10        widget.delete(0, INSERT)
11    except: pass
12def install_keybindings(root):
13    root.bind_class('Entry', '<Control-u>', _clear_entry_widget)
14
15
16def make_toplevel(master, title=None, class_=None):
17    """Create a Toplevel widget.
18
19    This is a shortcut for a Toplevel() instantiation plus calls to
20    set the title and icon name of the widget.
21
22    """
23
24    if class_:
25        widget = Toplevel(master, class_=class_)
26    else:
27        widget = Toplevel(master)
28    if title:
29        widget.title(title)
30        widget.iconname(title)
31    return widget
32
33def set_transient(widget, master, relx=0.5, rely=0.3, expose=1):
34    """Make an existing toplevel widget transient for a master.
35
36    The widget must exist but should not yet have been placed; in
37    other words, this should be called after creating all the
38    subwidget but before letting the user interact.
39    """
40
41    widget.withdraw() # Remain invisible while we figure out the geometry
42    widget.transient(master)
43    widget.update_idletasks() # Actualize geometry information
44    if master.winfo_ismapped():
45        m_width = master.winfo_width()
46        m_height = master.winfo_height()
47        m_x = master.winfo_rootx()
48        m_y = master.winfo_rooty()
49    else:
50        m_width = master.winfo_screenwidth()
51        m_height = master.winfo_screenheight()
52        m_x = m_y = 0
53    w_width = widget.winfo_reqwidth()
54    w_height = widget.winfo_reqheight()
55    x = m_x + (m_width - w_width) * relx
56    y = m_y + (m_height - w_height) * rely
57    widget.geometry("+%d+%d" % (x, y))
58    if expose:
59        widget.deiconify()      # Become visible at the desired location
60    return widget
61
62
63def make_scrollbars(parent, hbar, vbar, pack=1, class_=None, name=None,
64                    takefocus=0):
65
66    """Subroutine to create a frame with scrollbars.
67
68    This is used by make_text_box and similar routines.
69
70    Note: the caller is responsible for setting the x/y scroll command
71    properties (e.g. by calling set_scroll_commands()).
72
73    Return a tuple containing the hbar, the vbar, and the frame, where
74    hbar and vbar are None if not requested.
75
76    """
77    if class_:
78        if name: frame = Frame(parent, class_=class_, name=name)
79        else: frame = Frame(parent, class_=class_)
80    else:
81        if name: frame = Frame(parent, name=name)
82        else: frame = Frame(parent)
83
84    if pack:
85        frame.pack(fill=BOTH, expand=1)
86
87    corner = None
88    if vbar:
89        if not hbar:
90            vbar = Scrollbar(frame, takefocus=takefocus)
91            vbar.pack(fill=Y, side=RIGHT)
92        else:
93            vbarframe = Frame(frame, borderwidth=0)
94            vbarframe.pack(fill=Y, side=RIGHT)
95            vbar = Scrollbar(frame, name="vbar", takefocus=takefocus)
96            vbar.pack(in_=vbarframe, expand=1, fill=Y, side=TOP)
97            sbwidth = vbar.winfo_reqwidth()
98            corner = Frame(vbarframe, width=sbwidth, height=sbwidth)
99            corner.propagate(0)
100            corner.pack(side=BOTTOM)
101    else:
102        vbar = None
103
104    if hbar:
105        hbar = Scrollbar(frame, orient=HORIZONTAL, name="hbar",
106                         takefocus=takefocus)
107        hbar.pack(fill=X, side=BOTTOM)
108    else:
109        hbar = None
110
111    return hbar, vbar, frame
112
113
114def set_scroll_commands(widget, hbar, vbar):
115
116    """Link a scrollable widget to its scroll bars.
117
118    The scroll bars may be empty.
119
120    """
121
122    if vbar:
123        widget['yscrollcommand'] = (vbar, 'set')
124        vbar['command'] = (widget, 'yview')
125
126    if hbar:
127        widget['xscrollcommand'] = (hbar, 'set')
128        hbar['command'] = (widget, 'xview')
129
130    widget.vbar = vbar
131    widget.hbar = hbar
132
133
134def make_text_box(parent, width=0, height=0, hbar=0, vbar=1,
135                  fill=BOTH, expand=1, wrap=WORD, pack=1,
136                  class_=None, name=None, takefocus=None):
137
138    """Subroutine to create a text box.
139
140    Create:
141    - a both-ways filling and expanding frame, containing:
142      - a text widget on the left, and
143      - possibly a vertical scroll bar on the right.
144      - possibly a horizonta; scroll bar at the bottom.
145
146    Return the text widget and the frame widget.
147
148    """
149    hbar, vbar, frame = make_scrollbars(parent, hbar, vbar, pack,
150                                        class_=class_, name=name,
151                                        takefocus=takefocus)
152
153    widget = Text(frame, wrap=wrap, name="text")
154    if width: widget.config(width=width)
155    if height: widget.config(height=height)
156    widget.pack(expand=expand, fill=fill, side=LEFT)
157
158    set_scroll_commands(widget, hbar, vbar)
159
160    return widget, frame
161
162
163def make_list_box(parent, width=0, height=0, hbar=0, vbar=1,
164                  fill=BOTH, expand=1, pack=1, class_=None, name=None,
165                  takefocus=None):
166
167    """Subroutine to create a list box.
168
169    Like make_text_box().
170    """
171    hbar, vbar, frame = make_scrollbars(parent, hbar, vbar, pack,
172                                        class_=class_, name=name,
173                                        takefocus=takefocus)
174
175    widget = Listbox(frame, name="listbox")
176    if width: widget.config(width=width)
177    if height: widget.config(height=height)
178    widget.pack(expand=expand, fill=fill, side=LEFT)
179
180    set_scroll_commands(widget, hbar, vbar)
181
182    return widget, frame
183
184
185def make_canvas(parent, width=0, height=0, hbar=1, vbar=1,
186                fill=BOTH, expand=1, pack=1, class_=None, name=None,
187                takefocus=None):
188
189    """Subroutine to create a canvas.
190
191    Like make_text_box().
192
193    """
194
195    hbar, vbar, frame = make_scrollbars(parent, hbar, vbar, pack,
196                                        class_=class_, name=name,
197                                        takefocus=takefocus)
198
199    widget = Canvas(frame, scrollregion=(0, 0, width, height), name="canvas")
200    if width: widget.config(width=width)
201    if height: widget.config(height=height)
202    widget.pack(expand=expand, fill=fill, side=LEFT)
203
204    set_scroll_commands(widget, hbar, vbar)
205
206    return widget, frame
207
208
209
210def make_form_entry(parent, label, borderwidth=None):
211
212    """Subroutine to create a form entry.
213
214    Create:
215    - a horizontally filling and expanding frame, containing:
216      - a label on the left, and
217      - a text entry on the right.
218
219    Return the entry widget and the frame widget.
220
221    """
222
223    frame = Frame(parent)
224    frame.pack(fill=X)
225
226    label = Label(frame, text=label)
227    label.pack(side=LEFT)
228
229    if borderwidth is None:
230        entry = Entry(frame, relief=SUNKEN)
231    else:
232        entry = Entry(frame, relief=SUNKEN, borderwidth=borderwidth)
233    entry.pack(side=LEFT, fill=X, expand=1)
234
235    return entry, frame
236
237# This is a slightly modified version of the function above.  This
238# version does the proper alighnment of labels with their fields.  It
239# should probably eventually replace make_form_entry altogether.
240#
241# The one annoying bug is that the text entry field should be
242# expandable while still aligning the colons.  This doesn't work yet.
243#
244def make_labeled_form_entry(parent, label, entrywidth=20, entryheight=1,
245                            labelwidth=0, borderwidth=None,
246                            takefocus=None):
247    """Subroutine to create a form entry.
248
249    Create:
250    - a horizontally filling and expanding frame, containing:
251      - a label on the left, and
252      - a text entry on the right.
253
254    Return the entry widget and the frame widget.
255    """
256    if label and label[-1] != ':': label = label + ':'
257
258    frame = Frame(parent)
259
260    label = Label(frame, text=label, width=labelwidth, anchor=E)
261    label.pack(side=LEFT)
262    if entryheight == 1:
263        if borderwidth is None:
264            entry = Entry(frame, relief=SUNKEN, width=entrywidth)
265        else:
266            entry = Entry(frame, relief=SUNKEN, width=entrywidth,
267                          borderwidth=borderwidth)
268        entry.pack(side=RIGHT, expand=1, fill=X)
269        frame.pack(fill=X)
270    else:
271        entry = make_text_box(frame, entrywidth, entryheight, 1, 1,
272                              takefocus=takefocus)
273        frame.pack(fill=BOTH, expand=1)
274
275    return entry, frame, label
276
277
278def make_double_frame(master=None, class_=None, name=None, relief=RAISED,
279                      borderwidth=1):
280    """Create a pair of frames suitable for 'hosting' a dialog."""
281    if name:
282        if class_: frame = Frame(master, class_=class_, name=name)
283        else: frame = Frame(master, name=name)
284    else:
285        if class_: frame = Frame(master, class_=class_)
286        else: frame = Frame(master)
287    top = Frame(frame, name="topframe", relief=relief,
288                borderwidth=borderwidth)
289    bottom = Frame(frame, name="bottomframe")
290    bottom.pack(fill=X, padx='1m', pady='1m', side=BOTTOM)
291    top.pack(expand=1, fill=BOTH, padx='1m', pady='1m')
292    frame.pack(expand=1, fill=BOTH)
293    top = Frame(top)
294    top.pack(expand=1, fill=BOTH, padx='2m', pady='2m')
295
296    return frame, top, bottom
297
298
299def make_group_frame(master, name=None, label=None, fill=Y,
300                     side=None, expand=None, font=None):
301    """Create nested frames with a border and optional label.
302
303    The outer frame is only used to provide the decorative border, to
304    control packing, and to host the label.  The inner frame is packed
305    to fill the outer frame and should be used as the parent of all
306    sub-widgets.  Only the inner frame is returned.
307
308    """
309    font = font or "-*-helvetica-medium-r-normal-*-*-100-*-*-*-*-*-*"
310    outer = Frame(master, borderwidth=2, relief=GROOVE)
311    outer.pack(expand=expand, fill=fill, side=side)
312    if label:
313        Label(outer, text=label, font=font, anchor=W).pack(fill=X)
314    inner = Frame(master, borderwidth='1m', name=name)
315    inner.pack(expand=1, fill=BOTH, in_=outer)
316    inner.forget = outer.forget
317    return inner
318
319
320def unify_button_widths(*buttons):
321    """Make buttons passed in all have the same width.
322
323    Works for labels and other widgets with the 'text' option.
324
325    """
326    wid = 0
327    for btn in buttons:
328        wid = max(wid, len(btn["text"]))
329    for btn in buttons:
330        btn["width"] = wid
331
332
333def flatten(msg):
334    """Turn a list or tuple into a single string -- recursively."""
335    t = type(msg)
336    if t in (ListType, TupleType):
337        msg = ' '.join(map(flatten, msg))
338    elif t is ClassType:
339        msg = msg.__name__
340    else:
341        msg = str(msg)
342    return msg
343
344
345def boolean(s):
346    """Test whether a string is a Tk boolean, without error checking."""
347    if s.lower() in ('', '0', 'no', 'off', 'false'): return 0
348    else: return 1
349
350
351def test():
352    """Test make_text_box(), make_form_entry(), flatten(), boolean()."""
353    import sys
354    root = Tk()
355    entry, eframe = make_form_entry(root, 'Boolean:')
356    text, tframe = make_text_box(root)
357    def enter(event, entry=entry, text=text):
358        s = boolean(entry.get()) and '\nyes' or '\nno'
359        text.insert('end', s)
360    entry.bind('<Return>', enter)
361    entry.insert(END, flatten(sys.argv))
362    root.mainloop()
363
364
365if __name__ == '__main__':
366    test()
367