tktools.py 10.9 KB
Newer Older
1 2 3 4 5 6 7 8 9
"""Assorted Tk-related subroutines used in Grail."""


import string
from types import *
from Tkinter import *

def _clear_entry_widget(event):
    try:
Guido van Rossum's avatar
Guido van Rossum committed
10 11
        widget = event.widget
        widget.delete(0, INSERT)
12 13 14 15 16 17 18 19 20 21 22 23 24 25
    except: pass
def install_keybindings(root):
    root.bind_class('Entry', '<Control-u>', _clear_entry_widget)


def make_toplevel(master, title=None, class_=None):
    """Create a Toplevel widget.

    This is a shortcut for a Toplevel() instantiation plus calls to
    set the title and icon name of the widget.

    """

    if class_:
Guido van Rossum's avatar
Guido van Rossum committed
26
        widget = Toplevel(master, class_=class_)
27
    else:
Guido van Rossum's avatar
Guido van Rossum committed
28
        widget = Toplevel(master)
29
    if title:
Guido van Rossum's avatar
Guido van Rossum committed
30 31
        widget.title(title)
        widget.iconname(title)
32 33 34 35 36 37 38 39 40 41 42 43 44 45
    return widget

def set_transient(widget, master, relx=0.5, rely=0.3, expose=1):
    """Make an existing toplevel widget transient for a master.

    The widget must exist but should not yet have been placed; in
    other words, this should be called after creating all the
    subwidget but before letting the user interact.
    """

    widget.withdraw() # Remain invisible while we figure out the geometry
    widget.transient(master)
    widget.update_idletasks() # Actualize geometry information
    if master.winfo_ismapped():
Guido van Rossum's avatar
Guido van Rossum committed
46 47 48 49
        m_width = master.winfo_width()
        m_height = master.winfo_height()
        m_x = master.winfo_rootx()
        m_y = master.winfo_rooty()
50
    else:
Guido van Rossum's avatar
Guido van Rossum committed
51 52 53
        m_width = master.winfo_screenwidth()
        m_height = master.winfo_screenheight()
        m_x = m_y = 0
54 55 56 57 58 59
    w_width = widget.winfo_reqwidth()
    w_height = widget.winfo_reqheight()
    x = m_x + (m_width - w_width) * relx
    y = m_y + (m_height - w_height) * rely
    widget.geometry("+%d+%d" % (x, y))
    if expose:
Guido van Rossum's avatar
Guido van Rossum committed
60
        widget.deiconify()      # Become visible at the desired location
61 62 63 64
    return widget


def make_scrollbars(parent, hbar, vbar, pack=1, class_=None, name=None,
Guido van Rossum's avatar
Guido van Rossum committed
65
                    takefocus=0):
66 67 68 69 70 71 72 73 74 75 76 77 78

    """Subroutine to create a frame with scrollbars.

    This is used by make_text_box and similar routines.

    Note: the caller is responsible for setting the x/y scroll command
    properties (e.g. by calling set_scroll_commands()).

    Return a tuple containing the hbar, the vbar, and the frame, where
    hbar and vbar are None if not requested.

    """
    if class_:
Guido van Rossum's avatar
Guido van Rossum committed
79 80
        if name: frame = Frame(parent, class_=class_, name=name)
        else: frame = Frame(parent, class_=class_)
81
    else:
Guido van Rossum's avatar
Guido van Rossum committed
82 83
        if name: frame = Frame(parent, name=name)
        else: frame = Frame(parent)
84 85

    if pack:
Guido van Rossum's avatar
Guido van Rossum committed
86
        frame.pack(fill=BOTH, expand=1)
87 88 89

    corner = None
    if vbar:
Guido van Rossum's avatar
Guido van Rossum committed
90 91 92 93 94 95 96 97 98 99 100 101
        if not hbar:
            vbar = Scrollbar(frame, takefocus=takefocus)
            vbar.pack(fill=Y, side=RIGHT)
        else:
            vbarframe = Frame(frame, borderwidth=0)
            vbarframe.pack(fill=Y, side=RIGHT)
            vbar = Scrollbar(frame, name="vbar", takefocus=takefocus)
            vbar.pack(in_=vbarframe, expand=1, fill=Y, side=TOP)
            sbwidth = vbar.winfo_reqwidth()
            corner = Frame(vbarframe, width=sbwidth, height=sbwidth)
            corner.propagate(0)
            corner.pack(side=BOTTOM)
102
    else:
Guido van Rossum's avatar
Guido van Rossum committed
103
        vbar = None
104 105

    if hbar:
Guido van Rossum's avatar
Guido van Rossum committed
106 107 108
        hbar = Scrollbar(frame, orient=HORIZONTAL, name="hbar",
                         takefocus=takefocus)
        hbar.pack(fill=X, side=BOTTOM)
109
    else:
Guido van Rossum's avatar
Guido van Rossum committed
110
        hbar = None
111 112 113 114 115 116 117 118 119 120 121 122 123

    return hbar, vbar, frame


def set_scroll_commands(widget, hbar, vbar):

    """Link a scrollable widget to its scroll bars.

    The scroll bars may be empty.

    """

    if vbar:
Guido van Rossum's avatar
Guido van Rossum committed
124 125
        widget['yscrollcommand'] = (vbar, 'set')
        vbar['command'] = (widget, 'yview')
126 127

    if hbar:
Guido van Rossum's avatar
Guido van Rossum committed
128 129
        widget['xscrollcommand'] = (hbar, 'set')
        hbar['command'] = (widget, 'xview')
130 131 132 133 134 135

    widget.vbar = vbar
    widget.hbar = hbar


def make_text_box(parent, width=0, height=0, hbar=0, vbar=1,
Guido van Rossum's avatar
Guido van Rossum committed
136 137
                  fill=BOTH, expand=1, wrap=WORD, pack=1,
                  class_=None, name=None, takefocus=None):
138 139 140 141 142 143 144 145 146 147 148 149 150

    """Subroutine to create a text box.

    Create:
    - a both-ways filling and expanding frame, containing:
      - a text widget on the left, and
      - possibly a vertical scroll bar on the right.
      - possibly a horizonta; scroll bar at the bottom.

    Return the text widget and the frame widget.

    """
    hbar, vbar, frame = make_scrollbars(parent, hbar, vbar, pack,
Guido van Rossum's avatar
Guido van Rossum committed
151 152
                                        class_=class_, name=name,
                                        takefocus=takefocus)
153 154 155 156 157 158 159 160 161 162 163 164

    widget = Text(frame, wrap=wrap, name="text")
    if width: widget.config(width=width)
    if height: widget.config(height=height)
    widget.pack(expand=expand, fill=fill, side=LEFT)

    set_scroll_commands(widget, hbar, vbar)

    return widget, frame


def make_list_box(parent, width=0, height=0, hbar=0, vbar=1,
Guido van Rossum's avatar
Guido van Rossum committed
165 166
                  fill=BOTH, expand=1, pack=1, class_=None, name=None,
                  takefocus=None):
167 168 169 170 171 172

    """Subroutine to create a list box.

    Like make_text_box().
    """
    hbar, vbar, frame = make_scrollbars(parent, hbar, vbar, pack,
Guido van Rossum's avatar
Guido van Rossum committed
173 174
                                        class_=class_, name=name,
                                        takefocus=takefocus)
175 176 177 178 179 180 181 182 183 184 185 186

    widget = Listbox(frame, name="listbox")
    if width: widget.config(width=width)
    if height: widget.config(height=height)
    widget.pack(expand=expand, fill=fill, side=LEFT)

    set_scroll_commands(widget, hbar, vbar)

    return widget, frame


def make_canvas(parent, width=0, height=0, hbar=1, vbar=1,
Guido van Rossum's avatar
Guido van Rossum committed
187 188
                fill=BOTH, expand=1, pack=1, class_=None, name=None,
                takefocus=None):
189 190 191 192 193 194 195 196

    """Subroutine to create a canvas.

    Like make_text_box().

    """

    hbar, vbar, frame = make_scrollbars(parent, hbar, vbar, pack,
Guido van Rossum's avatar
Guido van Rossum committed
197 198
                                        class_=class_, name=name,
                                        takefocus=takefocus)
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230

    widget = Canvas(frame, scrollregion=(0, 0, width, height), name="canvas")
    if width: widget.config(width=width)
    if height: widget.config(height=height)
    widget.pack(expand=expand, fill=fill, side=LEFT)

    set_scroll_commands(widget, hbar, vbar)

    return widget, frame



def make_form_entry(parent, label, borderwidth=None):

    """Subroutine to create a form entry.

    Create:
    - a horizontally filling and expanding frame, containing:
      - a label on the left, and
      - a text entry on the right.

    Return the entry widget and the frame widget.

    """

    frame = Frame(parent)
    frame.pack(fill=X)

    label = Label(frame, text=label)
    label.pack(side=LEFT)

    if borderwidth is None:
Guido van Rossum's avatar
Guido van Rossum committed
231
        entry = Entry(frame, relief=SUNKEN)
232
    else:
Guido van Rossum's avatar
Guido van Rossum committed
233
        entry = Entry(frame, relief=SUNKEN, borderwidth=borderwidth)
234 235 236 237 238 239 240 241 242 243 244 245
    entry.pack(side=LEFT, fill=X, expand=1)

    return entry, frame

# This is a slightly modified version of the function above.  This
# version does the proper alighnment of labels with their fields.  It
# should probably eventually replace make_form_entry altogether.
#
# The one annoying bug is that the text entry field should be
# expandable while still aligning the colons.  This doesn't work yet.
#
def make_labeled_form_entry(parent, label, entrywidth=20, entryheight=1,
Guido van Rossum's avatar
Guido van Rossum committed
246 247
                            labelwidth=0, borderwidth=None,
                            takefocus=None):
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
    """Subroutine to create a form entry.

    Create:
    - a horizontally filling and expanding frame, containing:
      - a label on the left, and
      - a text entry on the right.

    Return the entry widget and the frame widget.
    """
    if label and label[-1] != ':': label = label + ':'

    frame = Frame(parent)

    label = Label(frame, text=label, width=labelwidth, anchor=E)
    label.pack(side=LEFT)
    if entryheight == 1:
Guido van Rossum's avatar
Guido van Rossum committed
264 265 266 267 268 269 270
        if borderwidth is None:
            entry = Entry(frame, relief=SUNKEN, width=entrywidth)
        else:
            entry = Entry(frame, relief=SUNKEN, width=entrywidth,
                          borderwidth=borderwidth)
        entry.pack(side=RIGHT, expand=1, fill=X)
        frame.pack(fill=X)
271
    else:
Guido van Rossum's avatar
Guido van Rossum committed
272 273 274
        entry = make_text_box(frame, entrywidth, entryheight, 1, 1,
                              takefocus=takefocus)
        frame.pack(fill=BOTH, expand=1)
275 276 277 278 279

    return entry, frame, label


def make_double_frame(master=None, class_=None, name=None, relief=RAISED,
Guido van Rossum's avatar
Guido van Rossum committed
280
                      borderwidth=1):
281 282
    """Create a pair of frames suitable for 'hosting' a dialog."""
    if name:
Guido van Rossum's avatar
Guido van Rossum committed
283 284
        if class_: frame = Frame(master, class_=class_, name=name)
        else: frame = Frame(master, name=name)
285
    else:
Guido van Rossum's avatar
Guido van Rossum committed
286 287
        if class_: frame = Frame(master, class_=class_)
        else: frame = Frame(master)
288
    top = Frame(frame, name="topframe", relief=relief,
Guido van Rossum's avatar
Guido van Rossum committed
289
                borderwidth=borderwidth)
290 291 292 293 294 295 296 297 298 299 300
    bottom = Frame(frame, name="bottomframe")
    bottom.pack(fill=X, padx='1m', pady='1m', side=BOTTOM)
    top.pack(expand=1, fill=BOTH, padx='1m', pady='1m')
    frame.pack(expand=1, fill=BOTH)
    top = Frame(top)
    top.pack(expand=1, fill=BOTH, padx='2m', pady='2m')

    return frame, top, bottom


def make_group_frame(master, name=None, label=None, fill=Y,
Guido van Rossum's avatar
Guido van Rossum committed
301
                     side=None, expand=None, font=None):
302 303 304 305 306 307 308 309 310 311 312 313
    """Create nested frames with a border and optional label.

    The outer frame is only used to provide the decorative border, to
    control packing, and to host the label.  The inner frame is packed
    to fill the outer frame and should be used as the parent of all
    sub-widgets.  Only the inner frame is returned.

    """
    font = font or "-*-helvetica-medium-r-normal-*-*-100-*-*-*-*-*-*"
    outer = Frame(master, borderwidth=2, relief=GROOVE)
    outer.pack(expand=expand, fill=fill, side=side)
    if label:
Guido van Rossum's avatar
Guido van Rossum committed
314
        Label(outer, text=label, font=font, anchor=W).pack(fill=X)
315 316 317 318 319 320 321 322 323 324 325 326 327 328
    inner = Frame(master, borderwidth='1m', name=name)
    inner.pack(expand=1, fill=BOTH, in_=outer)
    inner.forget = outer.forget
    return inner


def unify_button_widths(*buttons):
    """Make buttons passed in all have the same width.

    Works for labels and other widgets with the 'text' option.

    """
    wid = 0
    for btn in buttons:
Guido van Rossum's avatar
Guido van Rossum committed
329
        wid = max(wid, len(btn["text"]))
330
    for btn in buttons:
Guido van Rossum's avatar
Guido van Rossum committed
331
        btn["width"] = wid
332 333 334 335 336 337


def flatten(msg):
    """Turn a list or tuple into a single string -- recursively."""
    t = type(msg)
    if t in (ListType, TupleType):
Guido van Rossum's avatar
Guido van Rossum committed
338
        msg = string.join(map(flatten, msg))
339
    elif t is ClassType:
Guido van Rossum's avatar
Guido van Rossum committed
340
        msg = msg.__name__
341
    else:
Guido van Rossum's avatar
Guido van Rossum committed
342
        msg = str(msg)
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
    return msg


def boolean(s):
    """Test whether a string is a Tk boolean, without error checking."""
    if string.lower(s) in ('', '0', 'no', 'off', 'false'): return 0
    else: return 1


def test():
    """Test make_text_box(), make_form_entry(), flatten(), boolean()."""
    import sys
    root = Tk()
    entry, eframe = make_form_entry(root, 'Boolean:')
    text, tframe = make_text_box(root)
    def enter(event, entry=entry, text=text):
Guido van Rossum's avatar
Guido van Rossum committed
359 360
        s = boolean(entry.get()) and '\nyes' or '\nno'
        text.insert('end', s)
361 362 363 364 365 366 367
    entry.bind('<Return>', enter)
    entry.insert(END, flatten(sys.argv))
    root.mainloop()


if __name__ == '__main__':
    test()