• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Ttk wrapper.
2
3This module provides classes to allow using Tk themed widget set.
4
5Ttk is based on a revised and enhanced version of
6TIP #48 (http://tip.tcl.tk/48) specified style engine.
7
8Its basic idea is to separate, to the extent possible, the code
9implementing a widget's behavior from the code implementing its
10appearance. Widget class bindings are primarily responsible for
11maintaining the widget state and invoking callbacks, all aspects
12of the widgets appearance lies at Themes.
13"""
14
15__version__ = "0.3.1"
16
17__author__ = "Guilherme Polo <ggpolo@gmail.com>"
18
19__all__ = ["Button", "Checkbutton", "Combobox", "Entry", "Frame", "Label",
20           "Labelframe", "LabelFrame", "Menubutton", "Notebook", "Panedwindow",
21           "PanedWindow", "Progressbar", "Radiobutton", "Scale", "Scrollbar",
22           "Separator", "Sizegrip", "Spinbox", "Style", "Treeview",
23           # Extensions
24           "LabeledScale", "OptionMenu",
25           # functions
26           "tclobjs_to_py", "setup_master"]
27
28import tkinter
29from tkinter import _flatten, _join, _stringify, _splitdict
30
31
32def _format_optvalue(value, script=False):
33    """Internal function."""
34    if script:
35        # if caller passes a Tcl script to tk.call, all the values need to
36        # be grouped into words (arguments to a command in Tcl dialect)
37        value = _stringify(value)
38    elif isinstance(value, (list, tuple)):
39        value = _join(value)
40    return value
41
42def _format_optdict(optdict, script=False, ignore=None):
43    """Formats optdict to a tuple to pass it to tk.call.
44
45    E.g. (script=False):
46      {'foreground': 'blue', 'padding': [1, 2, 3, 4]} returns:
47      ('-foreground', 'blue', '-padding', '1 2 3 4')"""
48
49    opts = []
50    for opt, value in optdict.items():
51        if not ignore or opt not in ignore:
52            opts.append("-%s" % opt)
53            if value is not None:
54                opts.append(_format_optvalue(value, script))
55
56    return _flatten(opts)
57
58def _mapdict_values(items):
59    # each value in mapdict is expected to be a sequence, where each item
60    # is another sequence containing a state (or several) and a value
61    # E.g. (script=False):
62    #   [('active', 'selected', 'grey'), ('focus', [1, 2, 3, 4])]
63    #   returns:
64    #   ['active selected', 'grey', 'focus', [1, 2, 3, 4]]
65    opt_val = []
66    for *state, val in items:
67        if len(state) == 1:
68            # if it is empty (something that evaluates to False), then
69            # format it to Tcl code to denote the "normal" state
70            state = state[0] or ''
71        else:
72            # group multiple states
73            state = ' '.join(state) # raise TypeError if not str
74        opt_val.append(state)
75        if val is not None:
76            opt_val.append(val)
77    return opt_val
78
79def _format_mapdict(mapdict, script=False):
80    """Formats mapdict to pass it to tk.call.
81
82    E.g. (script=False):
83      {'expand': [('active', 'selected', 'grey'), ('focus', [1, 2, 3, 4])]}
84
85      returns:
86
87      ('-expand', '{active selected} grey focus {1, 2, 3, 4}')"""
88
89    opts = []
90    for opt, value in mapdict.items():
91        opts.extend(("-%s" % opt,
92                     _format_optvalue(_mapdict_values(value), script)))
93
94    return _flatten(opts)
95
96def _format_elemcreate(etype, script=False, *args, **kw):
97    """Formats args and kw according to the given element factory etype."""
98    specs = ()
99    opts = ()
100    if etype == "image": # define an element based on an image
101        # first arg should be the default image name
102        iname = args[0]
103        # next args, if any, are statespec/value pairs which is almost
104        # a mapdict, but we just need the value
105        imagespec = (iname, *_mapdict_values(args[1:]))
106        if script:
107            specs = (imagespec,)
108        else:
109            specs = (_join(imagespec),)
110        opts = _format_optdict(kw, script)
111
112    if etype == "vsapi":
113        # define an element whose visual appearance is drawn using the
114        # Microsoft Visual Styles API which is responsible for the
115        # themed styles on Windows XP and Vista.
116        # Availability: Tk 8.6, Windows XP and Vista.
117        if len(args) < 3:
118            class_name, part_id = args
119            statemap = (((), 1),)
120        else:
121            class_name, part_id, statemap = args
122        specs = (class_name, part_id, tuple(_mapdict_values(statemap)))
123        opts = _format_optdict(kw, script)
124
125    elif etype == "from": # clone an element
126        # it expects a themename and optionally an element to clone from,
127        # otherwise it will clone {} (empty element)
128        specs = (args[0],) # theme name
129        if len(args) > 1: # elementfrom specified
130            opts = (_format_optvalue(args[1], script),)
131
132    if script:
133        specs = _join(specs)
134        opts = ' '.join(opts)
135        return specs, opts
136    else:
137        return *specs, opts
138
139
140def _format_layoutlist(layout, indent=0, indent_size=2):
141    """Formats a layout list so we can pass the result to ttk::style
142    layout and ttk::style settings. Note that the layout doesn't have to
143    be a list necessarily.
144
145    E.g.:
146      [("Menubutton.background", None),
147       ("Menubutton.button", {"children":
148           [("Menubutton.focus", {"children":
149               [("Menubutton.padding", {"children":
150                [("Menubutton.label", {"side": "left", "expand": 1})]
151               })]
152           })]
153       }),
154       ("Menubutton.indicator", {"side": "right"})
155      ]
156
157      returns:
158
159      Menubutton.background
160      Menubutton.button -children {
161        Menubutton.focus -children {
162          Menubutton.padding -children {
163            Menubutton.label -side left -expand 1
164          }
165        }
166      }
167      Menubutton.indicator -side right"""
168    script = []
169
170    for layout_elem in layout:
171        elem, opts = layout_elem
172        opts = opts or {}
173        fopts = ' '.join(_format_optdict(opts, True, ("children",)))
174        head = "%s%s%s" % (' ' * indent, elem, (" %s" % fopts) if fopts else '')
175
176        if "children" in opts:
177            script.append(head + " -children {")
178            indent += indent_size
179            newscript, indent = _format_layoutlist(opts['children'], indent,
180                indent_size)
181            script.append(newscript)
182            indent -= indent_size
183            script.append('%s}' % (' ' * indent))
184        else:
185            script.append(head)
186
187    return '\n'.join(script), indent
188
189def _script_from_settings(settings):
190    """Returns an appropriate script, based on settings, according to
191    theme_settings definition to be used by theme_settings and
192    theme_create."""
193    script = []
194    # a script will be generated according to settings passed, which
195    # will then be evaluated by Tcl
196    for name, opts in settings.items():
197        # will format specific keys according to Tcl code
198        if opts.get('configure'): # format 'configure'
199            s = ' '.join(_format_optdict(opts['configure'], True))
200            script.append("ttk::style configure %s %s;" % (name, s))
201
202        if opts.get('map'): # format 'map'
203            s = ' '.join(_format_mapdict(opts['map'], True))
204            script.append("ttk::style map %s %s;" % (name, s))
205
206        if 'layout' in opts: # format 'layout' which may be empty
207            if not opts['layout']:
208                s = 'null' # could be any other word, but this one makes sense
209            else:
210                s, _ = _format_layoutlist(opts['layout'])
211            script.append("ttk::style layout %s {\n%s\n}" % (name, s))
212
213        if opts.get('element create'): # format 'element create'
214            eopts = opts['element create']
215            etype = eopts[0]
216
217            # find where args end, and where kwargs start
218            argc = 1 # etype was the first one
219            while argc < len(eopts) and not hasattr(eopts[argc], 'items'):
220                argc += 1
221
222            elemargs = eopts[1:argc]
223            elemkw = eopts[argc] if argc < len(eopts) and eopts[argc] else {}
224            specs, eopts = _format_elemcreate(etype, True, *elemargs, **elemkw)
225
226            script.append("ttk::style element create %s %s %s %s" % (
227                name, etype, specs, eopts))
228
229    return '\n'.join(script)
230
231def _list_from_statespec(stuple):
232    """Construct a list from the given statespec tuple according to the
233    accepted statespec accepted by _format_mapdict."""
234    if isinstance(stuple, str):
235        return stuple
236    result = []
237    it = iter(stuple)
238    for state, val in zip(it, it):
239        if hasattr(state, 'typename'):  # this is a Tcl object
240            state = str(state).split()
241        elif isinstance(state, str):
242            state = state.split()
243        elif not isinstance(state, (tuple, list)):
244            state = (state,)
245        if hasattr(val, 'typename'):
246            val = str(val)
247        result.append((*state, val))
248
249    return result
250
251def _list_from_layouttuple(tk, ltuple):
252    """Construct a list from the tuple returned by ttk::layout, this is
253    somewhat the reverse of _format_layoutlist."""
254    ltuple = tk.splitlist(ltuple)
255    res = []
256
257    indx = 0
258    while indx < len(ltuple):
259        name = ltuple[indx]
260        opts = {}
261        res.append((name, opts))
262        indx += 1
263
264        while indx < len(ltuple): # grab name's options
265            opt, val = ltuple[indx:indx + 2]
266            if not opt.startswith('-'): # found next name
267                break
268
269            opt = opt[1:] # remove the '-' from the option
270            indx += 2
271
272            if opt == 'children':
273                val = _list_from_layouttuple(tk, val)
274
275            opts[opt] = val
276
277    return res
278
279def _val_or_dict(tk, options, *args):
280    """Format options then call Tk command with args and options and return
281    the appropriate result.
282
283    If no option is specified, a dict is returned. If an option is
284    specified with the None value, the value for that option is returned.
285    Otherwise, the function just sets the passed options and the caller
286    shouldn't be expecting a return value anyway."""
287    options = _format_optdict(options)
288    res = tk.call(*(args + options))
289
290    if len(options) % 2: # option specified without a value, return its value
291        return res
292
293    return _splitdict(tk, res, conv=_tclobj_to_py)
294
295def _convert_stringval(value):
296    """Converts a value to, hopefully, a more appropriate Python object."""
297    value = str(value)
298    try:
299        value = int(value)
300    except (ValueError, TypeError):
301        pass
302
303    return value
304
305def _to_number(x):
306    if isinstance(x, str):
307        if '.' in x:
308            x = float(x)
309        else:
310            x = int(x)
311    return x
312
313def _tclobj_to_py(val):
314    """Return value converted from Tcl object to Python object."""
315    if val and hasattr(val, '__len__') and not isinstance(val, str):
316        if getattr(val[0], 'typename', None) == 'StateSpec':
317            val = _list_from_statespec(val)
318        else:
319            val = list(map(_convert_stringval, val))
320
321    elif hasattr(val, 'typename'): # some other (single) Tcl object
322        val = _convert_stringval(val)
323
324    return val
325
326def tclobjs_to_py(adict):
327    """Returns adict with its values converted from Tcl objects to Python
328    objects."""
329    for opt, val in adict.items():
330        adict[opt] = _tclobj_to_py(val)
331
332    return adict
333
334def setup_master(master=None):
335    """If master is not None, itself is returned. If master is None,
336    the default master is returned if there is one, otherwise a new
337    master is created and returned.
338
339    If it is not allowed to use the default root and master is None,
340    RuntimeError is raised."""
341    if master is None:
342        master = tkinter._get_default_root()
343    return master
344
345
346class Style(object):
347    """Manipulate style database."""
348
349    _name = "ttk::style"
350
351    def __init__(self, master=None):
352        master = setup_master(master)
353        self.master = master
354        self.tk = self.master.tk
355
356
357    def configure(self, style, query_opt=None, **kw):
358        """Query or sets the default value of the specified option(s) in
359        style.
360
361        Each key in kw is an option and each value is either a string or
362        a sequence identifying the value for that option."""
363        if query_opt is not None:
364            kw[query_opt] = None
365        result = _val_or_dict(self.tk, kw, self._name, "configure", style)
366        if result or query_opt:
367            return result
368
369
370    def map(self, style, query_opt=None, **kw):
371        """Query or sets dynamic values of the specified option(s) in
372        style.
373
374        Each key in kw is an option and each value should be a list or a
375        tuple (usually) containing statespecs grouped in tuples, or list,
376        or something else of your preference. A statespec is compound of
377        one or more states and then a value."""
378        if query_opt is not None:
379            result = self.tk.call(self._name, "map", style, '-%s' % query_opt)
380            return _list_from_statespec(self.tk.splitlist(result))
381
382        result = self.tk.call(self._name, "map", style, *_format_mapdict(kw))
383        return {k: _list_from_statespec(self.tk.splitlist(v))
384                for k, v in _splitdict(self.tk, result).items()}
385
386
387    def lookup(self, style, option, state=None, default=None):
388        """Returns the value specified for option in style.
389
390        If state is specified it is expected to be a sequence of one
391        or more states. If the default argument is set, it is used as
392        a fallback value in case no specification for option is found."""
393        state = ' '.join(state) if state else ''
394
395        return self.tk.call(self._name, "lookup", style, '-%s' % option,
396            state, default)
397
398
399    def layout(self, style, layoutspec=None):
400        """Define the widget layout for given style. If layoutspec is
401        omitted, return the layout specification for given style.
402
403        layoutspec is expected to be a list or an object different than
404        None that evaluates to False if you want to "turn off" that style.
405        If it is a list (or tuple, or something else), each item should be
406        a tuple where the first item is the layout name and the second item
407        should have the format described below:
408
409        LAYOUTS
410
411            A layout can contain the value None, if takes no options, or
412            a dict of options specifying how to arrange the element.
413            The layout mechanism uses a simplified version of the pack
414            geometry manager: given an initial cavity, each element is
415            allocated a parcel. Valid options/values are:
416
417                side: whichside
418                    Specifies which side of the cavity to place the
419                    element; one of top, right, bottom or left. If
420                    omitted, the element occupies the entire cavity.
421
422                sticky: nswe
423                    Specifies where the element is placed inside its
424                    allocated parcel.
425
426                children: [sublayout... ]
427                    Specifies a list of elements to place inside the
428                    element. Each element is a tuple (or other sequence)
429                    where the first item is the layout name, and the other
430                    is a LAYOUT."""
431        lspec = None
432        if layoutspec:
433            lspec = _format_layoutlist(layoutspec)[0]
434        elif layoutspec is not None: # will disable the layout ({}, '', etc)
435            lspec = "null" # could be any other word, but this may make sense
436                           # when calling layout(style) later
437
438        return _list_from_layouttuple(self.tk,
439            self.tk.call(self._name, "layout", style, lspec))
440
441
442    def element_create(self, elementname, etype, *args, **kw):
443        """Create a new element in the current theme of given etype."""
444        *specs, opts = _format_elemcreate(etype, False, *args, **kw)
445        self.tk.call(self._name, "element", "create", elementname, etype,
446            *specs, *opts)
447
448
449    def element_names(self):
450        """Returns the list of elements defined in the current theme."""
451        return tuple(n.lstrip('-') for n in self.tk.splitlist(
452            self.tk.call(self._name, "element", "names")))
453
454
455    def element_options(self, elementname):
456        """Return the list of elementname's options."""
457        return tuple(o.lstrip('-') for o in self.tk.splitlist(
458            self.tk.call(self._name, "element", "options", elementname)))
459
460
461    def theme_create(self, themename, parent=None, settings=None):
462        """Creates a new theme.
463
464        It is an error if themename already exists. If parent is
465        specified, the new theme will inherit styles, elements and
466        layouts from the specified parent theme. If settings are present,
467        they are expected to have the same syntax used for theme_settings."""
468        script = _script_from_settings(settings) if settings else ''
469
470        if parent:
471            self.tk.call(self._name, "theme", "create", themename,
472                "-parent", parent, "-settings", script)
473        else:
474            self.tk.call(self._name, "theme", "create", themename,
475                "-settings", script)
476
477
478    def theme_settings(self, themename, settings):
479        """Temporarily sets the current theme to themename, apply specified
480        settings and then restore the previous theme.
481
482        Each key in settings is a style and each value may contain the
483        keys 'configure', 'map', 'layout' and 'element create' and they
484        are expected to have the same format as specified by the methods
485        configure, map, layout and element_create respectively."""
486        script = _script_from_settings(settings)
487        self.tk.call(self._name, "theme", "settings", themename, script)
488
489
490    def theme_names(self):
491        """Returns a list of all known themes."""
492        return self.tk.splitlist(self.tk.call(self._name, "theme", "names"))
493
494
495    def theme_use(self, themename=None):
496        """If themename is None, returns the theme in use, otherwise, set
497        the current theme to themename, refreshes all widgets and emits
498        a <<ThemeChanged>> event."""
499        if themename is None:
500            # Starting on Tk 8.6, checking this global is no longer needed
501            # since it allows doing self.tk.call(self._name, "theme", "use")
502            return self.tk.eval("return $ttk::currentTheme")
503
504        # using "ttk::setTheme" instead of "ttk::style theme use" causes
505        # the variable currentTheme to be updated, also, ttk::setTheme calls
506        # "ttk::style theme use" in order to change theme.
507        self.tk.call("ttk::setTheme", themename)
508
509
510class Widget(tkinter.Widget):
511    """Base class for Tk themed widgets."""
512
513    def __init__(self, master, widgetname, kw=None):
514        """Constructs a Ttk Widget with the parent master.
515
516        STANDARD OPTIONS
517
518            class, cursor, takefocus, style
519
520        SCROLLABLE WIDGET OPTIONS
521
522            xscrollcommand, yscrollcommand
523
524        LABEL WIDGET OPTIONS
525
526            text, textvariable, underline, image, compound, width
527
528        WIDGET STATES
529
530            active, disabled, focus, pressed, selected, background,
531            readonly, alternate, invalid
532        """
533        master = setup_master(master)
534        tkinter.Widget.__init__(self, master, widgetname, kw=kw)
535
536
537    def identify(self, x, y):
538        """Returns the name of the element at position x, y, or the empty
539        string if the point does not lie within any element.
540
541        x and y are pixel coordinates relative to the widget."""
542        return self.tk.call(self._w, "identify", x, y)
543
544
545    def instate(self, statespec, callback=None, *args, **kw):
546        """Test the widget's state.
547
548        If callback is not specified, returns True if the widget state
549        matches statespec and False otherwise. If callback is specified,
550        then it will be invoked with *args, **kw if the widget state
551        matches statespec. statespec is expected to be a sequence."""
552        ret = self.tk.getboolean(
553                self.tk.call(self._w, "instate", ' '.join(statespec)))
554        if ret and callback is not None:
555            return callback(*args, **kw)
556
557        return ret
558
559
560    def state(self, statespec=None):
561        """Modify or inquire widget state.
562
563        Widget state is returned if statespec is None, otherwise it is
564        set according to the statespec flags and then a new state spec
565        is returned indicating which flags were changed. statespec is
566        expected to be a sequence."""
567        if statespec is not None:
568            statespec = ' '.join(statespec)
569
570        return self.tk.splitlist(str(self.tk.call(self._w, "state", statespec)))
571
572
573class Button(Widget):
574    """Ttk Button widget, displays a textual label and/or image, and
575    evaluates a command when pressed."""
576
577    def __init__(self, master=None, **kw):
578        """Construct a Ttk Button widget with the parent master.
579
580        STANDARD OPTIONS
581
582            class, compound, cursor, image, state, style, takefocus,
583            text, textvariable, underline, width
584
585        WIDGET-SPECIFIC OPTIONS
586
587            command, default, width
588        """
589        Widget.__init__(self, master, "ttk::button", kw)
590
591
592    def invoke(self):
593        """Invokes the command associated with the button."""
594        return self.tk.call(self._w, "invoke")
595
596
597class Checkbutton(Widget):
598    """Ttk Checkbutton widget which is either in on- or off-state."""
599
600    def __init__(self, master=None, **kw):
601        """Construct a Ttk Checkbutton widget with the parent master.
602
603        STANDARD OPTIONS
604
605            class, compound, cursor, image, state, style, takefocus,
606            text, textvariable, underline, width
607
608        WIDGET-SPECIFIC OPTIONS
609
610            command, offvalue, onvalue, variable
611        """
612        Widget.__init__(self, master, "ttk::checkbutton", kw)
613
614
615    def invoke(self):
616        """Toggles between the selected and deselected states and
617        invokes the associated command. If the widget is currently
618        selected, sets the option variable to the offvalue option
619        and deselects the widget; otherwise, sets the option variable
620        to the option onvalue.
621
622        Returns the result of the associated command."""
623        return self.tk.call(self._w, "invoke")
624
625
626class Entry(Widget, tkinter.Entry):
627    """Ttk Entry widget displays a one-line text string and allows that
628    string to be edited by the user."""
629
630    def __init__(self, master=None, widget=None, **kw):
631        """Constructs a Ttk Entry widget with the parent master.
632
633        STANDARD OPTIONS
634
635            class, cursor, style, takefocus, xscrollcommand
636
637        WIDGET-SPECIFIC OPTIONS
638
639            exportselection, invalidcommand, justify, show, state,
640            textvariable, validate, validatecommand, width
641
642        VALIDATION MODES
643
644            none, key, focus, focusin, focusout, all
645        """
646        Widget.__init__(self, master, widget or "ttk::entry", kw)
647
648
649    def bbox(self, index):
650        """Return a tuple of (x, y, width, height) which describes the
651        bounding box of the character given by index."""
652        return self._getints(self.tk.call(self._w, "bbox", index))
653
654
655    def identify(self, x, y):
656        """Returns the name of the element at position x, y, or the
657        empty string if the coordinates are outside the window."""
658        return self.tk.call(self._w, "identify", x, y)
659
660
661    def validate(self):
662        """Force revalidation, independent of the conditions specified
663        by the validate option. Returns False if validation fails, True
664        if it succeeds. Sets or clears the invalid state accordingly."""
665        return self.tk.getboolean(self.tk.call(self._w, "validate"))
666
667
668class Combobox(Entry):
669    """Ttk Combobox widget combines a text field with a pop-down list of
670    values."""
671
672    def __init__(self, master=None, **kw):
673        """Construct a Ttk Combobox widget with the parent master.
674
675        STANDARD OPTIONS
676
677            class, cursor, style, takefocus
678
679        WIDGET-SPECIFIC OPTIONS
680
681            exportselection, justify, height, postcommand, state,
682            textvariable, values, width
683        """
684        Entry.__init__(self, master, "ttk::combobox", **kw)
685
686
687    def current(self, newindex=None):
688        """If newindex is supplied, sets the combobox value to the
689        element at position newindex in the list of values. Otherwise,
690        returns the index of the current value in the list of values
691        or -1 if the current value does not appear in the list."""
692        if newindex is None:
693            res = self.tk.call(self._w, "current")
694            if res == '':
695                return -1
696            return self.tk.getint(res)
697        return self.tk.call(self._w, "current", newindex)
698
699
700    def set(self, value):
701        """Sets the value of the combobox to value."""
702        self.tk.call(self._w, "set", value)
703
704
705class Frame(Widget):
706    """Ttk Frame widget is a container, used to group other widgets
707    together."""
708
709    def __init__(self, master=None, **kw):
710        """Construct a Ttk Frame with parent master.
711
712        STANDARD OPTIONS
713
714            class, cursor, style, takefocus
715
716        WIDGET-SPECIFIC OPTIONS
717
718            borderwidth, relief, padding, width, height
719        """
720        Widget.__init__(self, master, "ttk::frame", kw)
721
722
723class Label(Widget):
724    """Ttk Label widget displays a textual label and/or image."""
725
726    def __init__(self, master=None, **kw):
727        """Construct a Ttk Label with parent master.
728
729        STANDARD OPTIONS
730
731            class, compound, cursor, image, style, takefocus, text,
732            textvariable, underline, width
733
734        WIDGET-SPECIFIC OPTIONS
735
736            anchor, background, font, foreground, justify, padding,
737            relief, text, wraplength
738        """
739        Widget.__init__(self, master, "ttk::label", kw)
740
741
742class Labelframe(Widget):
743    """Ttk Labelframe widget is a container used to group other widgets
744    together. It has an optional label, which may be a plain text string
745    or another widget."""
746
747    def __init__(self, master=None, **kw):
748        """Construct a Ttk Labelframe with parent master.
749
750        STANDARD OPTIONS
751
752            class, cursor, style, takefocus
753
754        WIDGET-SPECIFIC OPTIONS
755            labelanchor, text, underline, padding, labelwidget, width,
756            height
757        """
758        Widget.__init__(self, master, "ttk::labelframe", kw)
759
760LabelFrame = Labelframe # tkinter name compatibility
761
762
763class Menubutton(Widget):
764    """Ttk Menubutton widget displays a textual label and/or image, and
765    displays a menu when pressed."""
766
767    def __init__(self, master=None, **kw):
768        """Construct a Ttk Menubutton with parent master.
769
770        STANDARD OPTIONS
771
772            class, compound, cursor, image, state, style, takefocus,
773            text, textvariable, underline, width
774
775        WIDGET-SPECIFIC OPTIONS
776
777            direction, menu
778        """
779        Widget.__init__(self, master, "ttk::menubutton", kw)
780
781
782class Notebook(Widget):
783    """Ttk Notebook widget manages a collection of windows and displays
784    a single one at a time. Each child window is associated with a tab,
785    which the user may select to change the currently-displayed window."""
786
787    def __init__(self, master=None, **kw):
788        """Construct a Ttk Notebook with parent master.
789
790        STANDARD OPTIONS
791
792            class, cursor, style, takefocus
793
794        WIDGET-SPECIFIC OPTIONS
795
796            height, padding, width
797
798        TAB OPTIONS
799
800            state, sticky, padding, text, image, compound, underline
801
802        TAB IDENTIFIERS (tab_id)
803
804            The tab_id argument found in several methods may take any of
805            the following forms:
806
807                * An integer between zero and the number of tabs
808                * The name of a child window
809                * A positional specification of the form "@x,y", which
810                  defines the tab
811                * The string "current", which identifies the
812                  currently-selected tab
813                * The string "end", which returns the number of tabs (only
814                  valid for method index)
815        """
816        Widget.__init__(self, master, "ttk::notebook", kw)
817
818
819    def add(self, child, **kw):
820        """Adds a new tab to the notebook.
821
822        If window is currently managed by the notebook but hidden, it is
823        restored to its previous position."""
824        self.tk.call(self._w, "add", child, *(_format_optdict(kw)))
825
826
827    def forget(self, tab_id):
828        """Removes the tab specified by tab_id, unmaps and unmanages the
829        associated window."""
830        self.tk.call(self._w, "forget", tab_id)
831
832
833    def hide(self, tab_id):
834        """Hides the tab specified by tab_id.
835
836        The tab will not be displayed, but the associated window remains
837        managed by the notebook and its configuration remembered. Hidden
838        tabs may be restored with the add command."""
839        self.tk.call(self._w, "hide", tab_id)
840
841
842    def identify(self, x, y):
843        """Returns the name of the tab element at position x, y, or the
844        empty string if none."""
845        return self.tk.call(self._w, "identify", x, y)
846
847
848    def index(self, tab_id):
849        """Returns the numeric index of the tab specified by tab_id, or
850        the total number of tabs if tab_id is the string "end"."""
851        return self.tk.getint(self.tk.call(self._w, "index", tab_id))
852
853
854    def insert(self, pos, child, **kw):
855        """Inserts a pane at the specified position.
856
857        pos is either the string end, an integer index, or the name of
858        a managed child. If child is already managed by the notebook,
859        moves it to the specified position."""
860        self.tk.call(self._w, "insert", pos, child, *(_format_optdict(kw)))
861
862
863    def select(self, tab_id=None):
864        """Selects the specified tab.
865
866        The associated child window will be displayed, and the
867        previously-selected window (if different) is unmapped. If tab_id
868        is omitted, returns the widget name of the currently selected
869        pane."""
870        return self.tk.call(self._w, "select", tab_id)
871
872
873    def tab(self, tab_id, option=None, **kw):
874        """Query or modify the options of the specific tab_id.
875
876        If kw is not given, returns a dict of the tab option values. If option
877        is specified, returns the value of that option. Otherwise, sets the
878        options to the corresponding values."""
879        if option is not None:
880            kw[option] = None
881        return _val_or_dict(self.tk, kw, self._w, "tab", tab_id)
882
883
884    def tabs(self):
885        """Returns a list of windows managed by the notebook."""
886        return self.tk.splitlist(self.tk.call(self._w, "tabs") or ())
887
888
889    def enable_traversal(self):
890        """Enable keyboard traversal for a toplevel window containing
891        this notebook.
892
893        This will extend the bindings for the toplevel window containing
894        this notebook as follows:
895
896            Control-Tab: selects the tab following the currently selected
897                         one
898
899            Shift-Control-Tab: selects the tab preceding the currently
900                               selected one
901
902            Alt-K: where K is the mnemonic (underlined) character of any
903                   tab, will select that tab.
904
905        Multiple notebooks in a single toplevel may be enabled for
906        traversal, including nested notebooks. However, notebook traversal
907        only works properly if all panes are direct children of the
908        notebook."""
909        # The only, and good, difference I see is about mnemonics, which works
910        # after calling this method. Control-Tab and Shift-Control-Tab always
911        # works (here at least).
912        self.tk.call("ttk::notebook::enableTraversal", self._w)
913
914
915class Panedwindow(Widget, tkinter.PanedWindow):
916    """Ttk Panedwindow widget displays a number of subwindows, stacked
917    either vertically or horizontally."""
918
919    def __init__(self, master=None, **kw):
920        """Construct a Ttk Panedwindow with parent master.
921
922        STANDARD OPTIONS
923
924            class, cursor, style, takefocus
925
926        WIDGET-SPECIFIC OPTIONS
927
928            orient, width, height
929
930        PANE OPTIONS
931
932            weight
933        """
934        Widget.__init__(self, master, "ttk::panedwindow", kw)
935
936
937    forget = tkinter.PanedWindow.forget # overrides Pack.forget
938
939
940    def insert(self, pos, child, **kw):
941        """Inserts a pane at the specified positions.
942
943        pos is either the string end, and integer index, or the name
944        of a child. If child is already managed by the paned window,
945        moves it to the specified position."""
946        self.tk.call(self._w, "insert", pos, child, *(_format_optdict(kw)))
947
948
949    def pane(self, pane, option=None, **kw):
950        """Query or modify the options of the specified pane.
951
952        pane is either an integer index or the name of a managed subwindow.
953        If kw is not given, returns a dict of the pane option values. If
954        option is specified then the value for that option is returned.
955        Otherwise, sets the options to the corresponding values."""
956        if option is not None:
957            kw[option] = None
958        return _val_or_dict(self.tk, kw, self._w, "pane", pane)
959
960
961    def sashpos(self, index, newpos=None):
962        """If newpos is specified, sets the position of sash number index.
963
964        May adjust the positions of adjacent sashes to ensure that
965        positions are monotonically increasing. Sash positions are further
966        constrained to be between 0 and the total size of the widget.
967
968        Returns the new position of sash number index."""
969        return self.tk.getint(self.tk.call(self._w, "sashpos", index, newpos))
970
971PanedWindow = Panedwindow # tkinter name compatibility
972
973
974class Progressbar(Widget):
975    """Ttk Progressbar widget shows the status of a long-running
976    operation. They can operate in two modes: determinate mode shows the
977    amount completed relative to the total amount of work to be done, and
978    indeterminate mode provides an animated display to let the user know
979    that something is happening."""
980
981    def __init__(self, master=None, **kw):
982        """Construct a Ttk Progressbar with parent master.
983
984        STANDARD OPTIONS
985
986            class, cursor, style, takefocus
987
988        WIDGET-SPECIFIC OPTIONS
989
990            orient, length, mode, maximum, value, variable, phase
991        """
992        Widget.__init__(self, master, "ttk::progressbar", kw)
993
994
995    def start(self, interval=None):
996        """Begin autoincrement mode: schedules a recurring timer event
997        that calls method step every interval milliseconds.
998
999        interval defaults to 50 milliseconds (20 steps/second) if omitted."""
1000        self.tk.call(self._w, "start", interval)
1001
1002
1003    def step(self, amount=None):
1004        """Increments the value option by amount.
1005
1006        amount defaults to 1.0 if omitted."""
1007        self.tk.call(self._w, "step", amount)
1008
1009
1010    def stop(self):
1011        """Stop autoincrement mode: cancels any recurring timer event
1012        initiated by start."""
1013        self.tk.call(self._w, "stop")
1014
1015
1016class Radiobutton(Widget):
1017    """Ttk Radiobutton widgets are used in groups to show or change a
1018    set of mutually-exclusive options."""
1019
1020    def __init__(self, master=None, **kw):
1021        """Construct a Ttk Radiobutton with parent master.
1022
1023        STANDARD OPTIONS
1024
1025            class, compound, cursor, image, state, style, takefocus,
1026            text, textvariable, underline, width
1027
1028        WIDGET-SPECIFIC OPTIONS
1029
1030            command, value, variable
1031        """
1032        Widget.__init__(self, master, "ttk::radiobutton", kw)
1033
1034
1035    def invoke(self):
1036        """Sets the option variable to the option value, selects the
1037        widget, and invokes the associated command.
1038
1039        Returns the result of the command, or an empty string if
1040        no command is specified."""
1041        return self.tk.call(self._w, "invoke")
1042
1043
1044class Scale(Widget, tkinter.Scale):
1045    """Ttk Scale widget is typically used to control the numeric value of
1046    a linked variable that varies uniformly over some range."""
1047
1048    def __init__(self, master=None, **kw):
1049        """Construct a Ttk Scale with parent master.
1050
1051        STANDARD OPTIONS
1052
1053            class, cursor, style, takefocus
1054
1055        WIDGET-SPECIFIC OPTIONS
1056
1057            command, from, length, orient, to, value, variable
1058        """
1059        Widget.__init__(self, master, "ttk::scale", kw)
1060
1061
1062    def configure(self, cnf=None, **kw):
1063        """Modify or query scale options.
1064
1065        Setting a value for any of the "from", "from_" or "to" options
1066        generates a <<RangeChanged>> event."""
1067        retval = Widget.configure(self, cnf, **kw)
1068        if not isinstance(cnf, (type(None), str)):
1069            kw.update(cnf)
1070        if any(['from' in kw, 'from_' in kw, 'to' in kw]):
1071            self.event_generate('<<RangeChanged>>')
1072        return retval
1073
1074
1075    def get(self, x=None, y=None):
1076        """Get the current value of the value option, or the value
1077        corresponding to the coordinates x, y if they are specified.
1078
1079        x and y are pixel coordinates relative to the scale widget
1080        origin."""
1081        return self.tk.call(self._w, 'get', x, y)
1082
1083
1084class Scrollbar(Widget, tkinter.Scrollbar):
1085    """Ttk Scrollbar controls the viewport of a scrollable widget."""
1086
1087    def __init__(self, master=None, **kw):
1088        """Construct a Ttk Scrollbar with parent master.
1089
1090        STANDARD OPTIONS
1091
1092            class, cursor, style, takefocus
1093
1094        WIDGET-SPECIFIC OPTIONS
1095
1096            command, orient
1097        """
1098        Widget.__init__(self, master, "ttk::scrollbar", kw)
1099
1100
1101class Separator(Widget):
1102    """Ttk Separator widget displays a horizontal or vertical separator
1103    bar."""
1104
1105    def __init__(self, master=None, **kw):
1106        """Construct a Ttk Separator with parent master.
1107
1108        STANDARD OPTIONS
1109
1110            class, cursor, style, takefocus
1111
1112        WIDGET-SPECIFIC OPTIONS
1113
1114            orient
1115        """
1116        Widget.__init__(self, master, "ttk::separator", kw)
1117
1118
1119class Sizegrip(Widget):
1120    """Ttk Sizegrip allows the user to resize the containing toplevel
1121    window by pressing and dragging the grip."""
1122
1123    def __init__(self, master=None, **kw):
1124        """Construct a Ttk Sizegrip with parent master.
1125
1126        STANDARD OPTIONS
1127
1128            class, cursor, state, style, takefocus
1129        """
1130        Widget.__init__(self, master, "ttk::sizegrip", kw)
1131
1132
1133class Spinbox(Entry):
1134    """Ttk Spinbox is an Entry with increment and decrement arrows
1135
1136    It is commonly used for number entry or to select from a list of
1137    string values.
1138    """
1139
1140    def __init__(self, master=None, **kw):
1141        """Construct a Ttk Spinbox widget with the parent master.
1142
1143        STANDARD OPTIONS
1144
1145            class, cursor, style, takefocus, validate,
1146            validatecommand, xscrollcommand, invalidcommand
1147
1148        WIDGET-SPECIFIC OPTIONS
1149
1150            to, from_, increment, values, wrap, format, command
1151        """
1152        Entry.__init__(self, master, "ttk::spinbox", **kw)
1153
1154
1155    def set(self, value):
1156        """Sets the value of the Spinbox to value."""
1157        self.tk.call(self._w, "set", value)
1158
1159
1160class Treeview(Widget, tkinter.XView, tkinter.YView):
1161    """Ttk Treeview widget displays a hierarchical collection of items.
1162
1163    Each item has a textual label, an optional image, and an optional list
1164    of data values. The data values are displayed in successive columns
1165    after the tree label."""
1166
1167    def __init__(self, master=None, **kw):
1168        """Construct a Ttk Treeview with parent master.
1169
1170        STANDARD OPTIONS
1171
1172            class, cursor, style, takefocus, xscrollcommand,
1173            yscrollcommand
1174
1175        WIDGET-SPECIFIC OPTIONS
1176
1177            columns, displaycolumns, height, padding, selectmode, show
1178
1179        ITEM OPTIONS
1180
1181            text, image, values, open, tags
1182
1183        TAG OPTIONS
1184
1185            foreground, background, font, image
1186        """
1187        Widget.__init__(self, master, "ttk::treeview", kw)
1188
1189
1190    def bbox(self, item, column=None):
1191        """Returns the bounding box (relative to the treeview widget's
1192        window) of the specified item in the form x y width height.
1193
1194        If column is specified, returns the bounding box of that cell.
1195        If the item is not visible (i.e., if it is a descendant of a
1196        closed item or is scrolled offscreen), returns an empty string."""
1197        return self._getints(self.tk.call(self._w, "bbox", item, column)) or ''
1198
1199
1200    def get_children(self, item=None):
1201        """Returns a tuple of children belonging to item.
1202
1203        If item is not specified, returns root children."""
1204        return self.tk.splitlist(
1205                self.tk.call(self._w, "children", item or '') or ())
1206
1207
1208    def set_children(self, item, *newchildren):
1209        """Replaces item's child with newchildren.
1210
1211        Children present in item that are not present in newchildren
1212        are detached from tree. No items in newchildren may be an
1213        ancestor of item."""
1214        self.tk.call(self._w, "children", item, newchildren)
1215
1216
1217    def column(self, column, option=None, **kw):
1218        """Query or modify the options for the specified column.
1219
1220        If kw is not given, returns a dict of the column option values. If
1221        option is specified then the value for that option is returned.
1222        Otherwise, sets the options to the corresponding values."""
1223        if option is not None:
1224            kw[option] = None
1225        return _val_or_dict(self.tk, kw, self._w, "column", column)
1226
1227
1228    def delete(self, *items):
1229        """Delete all specified items and all their descendants. The root
1230        item may not be deleted."""
1231        self.tk.call(self._w, "delete", items)
1232
1233
1234    def detach(self, *items):
1235        """Unlinks all of the specified items from the tree.
1236
1237        The items and all of their descendants are still present, and may
1238        be reinserted at another point in the tree, but will not be
1239        displayed. The root item may not be detached."""
1240        self.tk.call(self._w, "detach", items)
1241
1242
1243    def exists(self, item):
1244        """Returns True if the specified item is present in the tree,
1245        False otherwise."""
1246        return self.tk.getboolean(self.tk.call(self._w, "exists", item))
1247
1248
1249    def focus(self, item=None):
1250        """If item is specified, sets the focus item to item. Otherwise,
1251        returns the current focus item, or '' if there is none."""
1252        return self.tk.call(self._w, "focus", item)
1253
1254
1255    def heading(self, column, option=None, **kw):
1256        """Query or modify the heading options for the specified column.
1257
1258        If kw is not given, returns a dict of the heading option values. If
1259        option is specified then the value for that option is returned.
1260        Otherwise, sets the options to the corresponding values.
1261
1262        Valid options/values are:
1263            text: text
1264                The text to display in the column heading
1265            image: image_name
1266                Specifies an image to display to the right of the column
1267                heading
1268            anchor: anchor
1269                Specifies how the heading text should be aligned. One of
1270                the standard Tk anchor values
1271            command: callback
1272                A callback to be invoked when the heading label is
1273                pressed.
1274
1275        To configure the tree column heading, call this with column = "#0" """
1276        cmd = kw.get('command')
1277        if cmd and not isinstance(cmd, str):
1278            # callback not registered yet, do it now
1279            kw['command'] = self.master.register(cmd, self._substitute)
1280
1281        if option is not None:
1282            kw[option] = None
1283
1284        return _val_or_dict(self.tk, kw, self._w, 'heading', column)
1285
1286
1287    def identify(self, component, x, y):
1288        """Returns a description of the specified component under the
1289        point given by x and y, or the empty string if no such component
1290        is present at that position."""
1291        return self.tk.call(self._w, "identify", component, x, y)
1292
1293
1294    def identify_row(self, y):
1295        """Returns the item ID of the item at position y."""
1296        return self.identify("row", 0, y)
1297
1298
1299    def identify_column(self, x):
1300        """Returns the data column identifier of the cell at position x.
1301
1302        The tree column has ID #0."""
1303        return self.identify("column", x, 0)
1304
1305
1306    def identify_region(self, x, y):
1307        """Returns one of:
1308
1309        heading: Tree heading area.
1310        separator: Space between two columns headings;
1311        tree: The tree area.
1312        cell: A data cell.
1313
1314        * Availability: Tk 8.6"""
1315        return self.identify("region", x, y)
1316
1317
1318    def identify_element(self, x, y):
1319        """Returns the element at position x, y.
1320
1321        * Availability: Tk 8.6"""
1322        return self.identify("element", x, y)
1323
1324
1325    def index(self, item):
1326        """Returns the integer index of item within its parent's list
1327        of children."""
1328        return self.tk.getint(self.tk.call(self._w, "index", item))
1329
1330
1331    def insert(self, parent, index, iid=None, **kw):
1332        """Creates a new item and return the item identifier of the newly
1333        created item.
1334
1335        parent is the item ID of the parent item, or the empty string
1336        to create a new top-level item. index is an integer, or the value
1337        end, specifying where in the list of parent's children to insert
1338        the new item. If index is less than or equal to zero, the new node
1339        is inserted at the beginning, if index is greater than or equal to
1340        the current number of children, it is inserted at the end. If iid
1341        is specified, it is used as the item identifier, iid must not
1342        already exist in the tree. Otherwise, a new unique identifier
1343        is generated."""
1344        opts = _format_optdict(kw)
1345        if iid is not None:
1346            res = self.tk.call(self._w, "insert", parent, index,
1347                "-id", iid, *opts)
1348        else:
1349            res = self.tk.call(self._w, "insert", parent, index, *opts)
1350
1351        return res
1352
1353
1354    def item(self, item, option=None, **kw):
1355        """Query or modify the options for the specified item.
1356
1357        If no options are given, a dict with options/values for the item
1358        is returned. If option is specified then the value for that option
1359        is returned. Otherwise, sets the options to the corresponding
1360        values as given by kw."""
1361        if option is not None:
1362            kw[option] = None
1363        return _val_or_dict(self.tk, kw, self._w, "item", item)
1364
1365
1366    def move(self, item, parent, index):
1367        """Moves item to position index in parent's list of children.
1368
1369        It is illegal to move an item under one of its descendants. If
1370        index is less than or equal to zero, item is moved to the
1371        beginning, if greater than or equal to the number of children,
1372        it is moved to the end. If item was detached it is reattached."""
1373        self.tk.call(self._w, "move", item, parent, index)
1374
1375    reattach = move # A sensible method name for reattaching detached items
1376
1377
1378    def next(self, item):
1379        """Returns the identifier of item's next sibling, or '' if item
1380        is the last child of its parent."""
1381        return self.tk.call(self._w, "next", item)
1382
1383
1384    def parent(self, item):
1385        """Returns the ID of the parent of item, or '' if item is at the
1386        top level of the hierarchy."""
1387        return self.tk.call(self._w, "parent", item)
1388
1389
1390    def prev(self, item):
1391        """Returns the identifier of item's previous sibling, or '' if
1392        item is the first child of its parent."""
1393        return self.tk.call(self._w, "prev", item)
1394
1395
1396    def see(self, item):
1397        """Ensure that item is visible.
1398
1399        Sets all of item's ancestors open option to True, and scrolls
1400        the widget if necessary so that item is within the visible
1401        portion of the tree."""
1402        self.tk.call(self._w, "see", item)
1403
1404
1405    def selection(self):
1406        """Returns the tuple of selected items."""
1407        return self.tk.splitlist(self.tk.call(self._w, "selection"))
1408
1409
1410    def _selection(self, selop, items):
1411        if len(items) == 1 and isinstance(items[0], (tuple, list)):
1412            items = items[0]
1413
1414        self.tk.call(self._w, "selection", selop, items)
1415
1416
1417    def selection_set(self, *items):
1418        """The specified items becomes the new selection."""
1419        self._selection("set", items)
1420
1421
1422    def selection_add(self, *items):
1423        """Add all of the specified items to the selection."""
1424        self._selection("add", items)
1425
1426
1427    def selection_remove(self, *items):
1428        """Remove all of the specified items from the selection."""
1429        self._selection("remove", items)
1430
1431
1432    def selection_toggle(self, *items):
1433        """Toggle the selection state of each specified item."""
1434        self._selection("toggle", items)
1435
1436
1437    def set(self, item, column=None, value=None):
1438        """Query or set the value of given item.
1439
1440        With one argument, return a dictionary of column/value pairs
1441        for the specified item. With two arguments, return the current
1442        value of the specified column. With three arguments, set the
1443        value of given column in given item to the specified value."""
1444        res = self.tk.call(self._w, "set", item, column, value)
1445        if column is None and value is None:
1446            return _splitdict(self.tk, res,
1447                              cut_minus=False, conv=_tclobj_to_py)
1448        else:
1449            return res
1450
1451
1452    def tag_bind(self, tagname, sequence=None, callback=None):
1453        """Bind a callback for the given event sequence to the tag tagname.
1454        When an event is delivered to an item, the callbacks for each
1455        of the item's tags option are called."""
1456        self._bind((self._w, "tag", "bind", tagname), sequence, callback, add=0)
1457
1458
1459    def tag_configure(self, tagname, option=None, **kw):
1460        """Query or modify the options for the specified tagname.
1461
1462        If kw is not given, returns a dict of the option settings for tagname.
1463        If option is specified, returns the value for that option for the
1464        specified tagname. Otherwise, sets the options to the corresponding
1465        values for the given tagname."""
1466        if option is not None:
1467            kw[option] = None
1468        return _val_or_dict(self.tk, kw, self._w, "tag", "configure",
1469            tagname)
1470
1471
1472    def tag_has(self, tagname, item=None):
1473        """If item is specified, returns 1 or 0 depending on whether the
1474        specified item has the given tagname. Otherwise, returns a list of
1475        all items which have the specified tag.
1476
1477        * Availability: Tk 8.6"""
1478        if item is None:
1479            return self.tk.splitlist(
1480                self.tk.call(self._w, "tag", "has", tagname))
1481        else:
1482            return self.tk.getboolean(
1483                self.tk.call(self._w, "tag", "has", tagname, item))
1484
1485
1486# Extensions
1487
1488class LabeledScale(Frame):
1489    """A Ttk Scale widget with a Ttk Label widget indicating its
1490    current value.
1491
1492    The Ttk Scale can be accessed through instance.scale, and Ttk Label
1493    can be accessed through instance.label"""
1494
1495    def __init__(self, master=None, variable=None, from_=0, to=10, **kw):
1496        """Construct a horizontal LabeledScale with parent master, a
1497        variable to be associated with the Ttk Scale widget and its range.
1498        If variable is not specified, a tkinter.IntVar is created.
1499
1500        WIDGET-SPECIFIC OPTIONS
1501
1502            compound: 'top' or 'bottom'
1503                Specifies how to display the label relative to the scale.
1504                Defaults to 'top'.
1505        """
1506        self._label_top = kw.pop('compound', 'top') == 'top'
1507
1508        Frame.__init__(self, master, **kw)
1509        self._variable = variable or tkinter.IntVar(master)
1510        self._variable.set(from_)
1511        self._last_valid = from_
1512
1513        self.label = Label(self)
1514        self.scale = Scale(self, variable=self._variable, from_=from_, to=to)
1515        self.scale.bind('<<RangeChanged>>', self._adjust)
1516
1517        # position scale and label according to the compound option
1518        scale_side = 'bottom' if self._label_top else 'top'
1519        label_side = 'top' if scale_side == 'bottom' else 'bottom'
1520        self.scale.pack(side=scale_side, fill='x')
1521        # Dummy required to make frame correct height
1522        dummy = Label(self)
1523        dummy.pack(side=label_side)
1524        dummy.lower()
1525        self.label.place(anchor='n' if label_side == 'top' else 's')
1526
1527        # update the label as scale or variable changes
1528        self.__tracecb = self._variable.trace_add('write', self._adjust)
1529        self.bind('<Configure>', self._adjust)
1530        self.bind('<Map>', self._adjust)
1531
1532
1533    def destroy(self):
1534        """Destroy this widget and possibly its associated variable."""
1535        try:
1536            self._variable.trace_remove('write', self.__tracecb)
1537        except AttributeError:
1538            pass
1539        else:
1540            del self._variable
1541        super().destroy()
1542        self.label = None
1543        self.scale = None
1544
1545
1546    def _adjust(self, *args):
1547        """Adjust the label position according to the scale."""
1548        def adjust_label():
1549            self.update_idletasks() # "force" scale redraw
1550
1551            x, y = self.scale.coords()
1552            if self._label_top:
1553                y = self.scale.winfo_y() - self.label.winfo_reqheight()
1554            else:
1555                y = self.scale.winfo_reqheight() + self.label.winfo_reqheight()
1556
1557            self.label.place_configure(x=x, y=y)
1558
1559        from_ = _to_number(self.scale['from'])
1560        to = _to_number(self.scale['to'])
1561        if to < from_:
1562            from_, to = to, from_
1563        newval = self._variable.get()
1564        if not from_ <= newval <= to:
1565            # value outside range, set value back to the last valid one
1566            self.value = self._last_valid
1567            return
1568
1569        self._last_valid = newval
1570        self.label['text'] = newval
1571        self.after_idle(adjust_label)
1572
1573    @property
1574    def value(self):
1575        """Return current scale value."""
1576        return self._variable.get()
1577
1578    @value.setter
1579    def value(self, val):
1580        """Set new scale value."""
1581        self._variable.set(val)
1582
1583
1584class OptionMenu(Menubutton):
1585    """Themed OptionMenu, based after tkinter's OptionMenu, which allows
1586    the user to select a value from a menu."""
1587
1588    def __init__(self, master, variable, default=None, *values, **kwargs):
1589        """Construct a themed OptionMenu widget with master as the parent,
1590        the resource textvariable set to variable, the initially selected
1591        value specified by the default parameter, the menu values given by
1592        *values and additional keywords.
1593
1594        WIDGET-SPECIFIC OPTIONS
1595
1596            style: stylename
1597                Menubutton style.
1598            direction: 'above', 'below', 'left', 'right', or 'flush'
1599                Menubutton direction.
1600            command: callback
1601                A callback that will be invoked after selecting an item.
1602        """
1603        kw = {'textvariable': variable, 'style': kwargs.pop('style', None),
1604              'direction': kwargs.pop('direction', None)}
1605        Menubutton.__init__(self, master, **kw)
1606        self['menu'] = tkinter.Menu(self, tearoff=False)
1607
1608        self._variable = variable
1609        self._callback = kwargs.pop('command', None)
1610        if kwargs:
1611            raise tkinter.TclError('unknown option -%s' % (
1612                next(iter(kwargs.keys()))))
1613
1614        self.set_menu(default, *values)
1615
1616
1617    def __getitem__(self, item):
1618        if item == 'menu':
1619            return self.nametowidget(Menubutton.__getitem__(self, item))
1620
1621        return Menubutton.__getitem__(self, item)
1622
1623
1624    def set_menu(self, default=None, *values):
1625        """Build a new menu of radiobuttons with *values and optionally
1626        a default value."""
1627        menu = self['menu']
1628        menu.delete(0, 'end')
1629        for val in values:
1630            menu.add_radiobutton(label=val,
1631                command=(
1632                    None if self._callback is None
1633                    else lambda val=val: self._callback(val)
1634                ),
1635                variable=self._variable)
1636
1637        if default:
1638            self._variable.set(default)
1639
1640
1641    def destroy(self):
1642        """Destroy this widget and its associated variable."""
1643        try:
1644            del self._variable
1645        except AttributeError:
1646            pass
1647        super().destroy()
1648