4
"""Control widget classes.
6
This module defines a Control class and several derivatives. A Control is a
7
special-purpose GUI widget for setting a value such as a number or filename.
27
from widget import Widget
28
from support import ListVar
32
### --------------------------------------------------------------------
33
class MissingOption (Exception):
34
def __init__(self, option):
37
### --------------------------------------------------------------------
38
# Map python types to Tkinter variable types
46
### --------------------------------------------------------------------
47
from tooltip import ToolTip
49
class Control (Widget):
50
"""A widget that controls the value of a command-line option.
52
A Control is a specialized GUI widget that controls a command-line option
53
via a local variable, accessed via get() and set() methods.
55
Control subclasses may have any number of sub-widgets such as labels,
56
buttons or entry boxes; one of the sub-widgets should be linked to the
57
controlled variable via an option like:
59
entry = Entry(self, textvariable=self.variable)
61
See the Control subclasses below for examples of how self.variable,
62
get() and set() are used.
71
"""Create a Control for an option.
73
vartype: Type of stored variable (str, bool, int, float, list)
74
label: Label shown in the GUI for the Control
75
option: Command-line option associated with this Control, or
76
'' to create a positional argument
77
default: Default value for the Control
78
help: Help text to show in a tooltip
79
**kwargs: Keyword arguments of the form key1=arg1, key2=arg2
81
Keyword arguments allowed:
83
pull=Control(...): Mirror value from another Control
84
required=True: Required option, must be set or run will fail
85
filter=function: Text-filtering function for pulled values
86
toggles=True: May be toggled on and off
89
self.vartype = vartype
93
self.default = default or vartype()
97
# TODO: Find a way to condense/separate keyword handling
98
# Controls a mandatory option?
100
if 'required' in kwargs:
101
self.required = bool(kwargs['required'])
102
# Has an enable/disable toggle button
104
if 'toggles' in self.kwargs:
105
self.toggles = bool(self.kwargs['toggles'])
106
# List of Controls to copy updated values to
108
if 'pull' in self.kwargs:
109
if not isinstance(self.kwargs['pull'], Control):
110
raise TypeError("Can only pull values from a Control.")
111
pull_from = self.kwargs['pull']
112
pull_from.copy_to(self)
113
# Filter expression when pulling from another Control
115
if 'filter' in self.kwargs:
116
if not callable(self.kwargs['filter']):
117
raise TypeError("Pull filter must be a function.")
118
self.filter = self.kwargs['filter']
120
def copy_to(self, control):
121
"""Update another control whenever this control's value changes.
123
if not isinstance(control, Control):
124
raise TypeError("Can only copy values to a Control.")
125
self.copies.append(control)
127
def draw(self, master):
128
"""Draw the control widgets in the given master.
130
Override this method in derived classes, and call the base
133
Control.draw(self, master)
136
Widget.draw(self, master)
137
# Create tk.Variable to store Control's value
138
if self.vartype in _vartypes:
139
self.variable = _vartypes[self.vartype]()
141
self.variable = tk.Variable()
144
self.variable.set(self.default)
147
self.tooltip = ToolTip(self, text=self.help, delay=1000)
148
# Draw enabler checkbox
150
self.enabled = tk.BooleanVar()
151
self.check = tk.Checkbutton(self, text='',
152
variable=self.enabled,
153
command=self.enabler)
154
self.check.pack(side='left')
157
"""Post-draw initialization.
163
"""Enable or disable the Control when the checkbox is toggled.
165
if self.enabled.get():
169
self.check.config(state='normal')
172
"""Return the value of the Control's variable."""
173
# self.variable isn't set until draw() is called
174
if not self.variable:
175
raise "Must call draw() before get()"
176
return self.variable.get()
178
def set(self, value):
179
"""Set the Control's variable to the given value."""
180
# self.variable isn't set until draw() is called
181
if not self.variable:
182
raise "Must call draw() before set()"
183
self.variable.set(value)
184
# Update controls that are copying this control's value
185
for control in self.copies:
186
control.variable.set(value)
189
"""Return a list of arguments for passing this command-line option.
190
draw() must be called before this function.
192
# TODO: Raise exception if draw() hasn't been called
196
# For toggles, return
198
if not self.enabled.get():
200
# Skip if unmodified or empty
201
elif value == self.default or value == []:
202
# ...unless it's required
204
raise MissingOption(self.option)
209
if self.option != '':
210
args.append(self.option)
212
if type(value) == list:
220
"""Return a Python code representation of this Control."""
221
# Get derived class name
222
control = str(self.__class__).split('.')[-1]
223
return "%s('%s', '%s')" % (control, self.option, self.label)
225
### --------------------------------------------------------------------
226
### Control subclasses
227
### --------------------------------------------------------------------
229
class Flag (Control):
230
"""A widget for controlling a yes/no value."""
237
"""Create a Flag widget with the given label and default value.
239
label: Text label for the flag
240
option: Command-line flag passed
241
default: Default value (True or False)
242
help: Help text to show in a tooltip
244
Control.__init__(self, bool, label, option, default, help, **kwargs)
245
# Enable an associated control when this Flag is True
247
if 'enables' in kwargs:
248
self.enables = kwargs['enables']
249
if not isinstance(self.enables, Widget):
250
raise "A Flag can only enable a Widget (Control or Panel)"
252
def draw(self, master):
253
"""Draw control widgets in the given master."""
254
Control.draw(self, master)
255
self.check = tk.Checkbutton(self, text=self.label,
256
variable=self.variable,
257
command=self.enabler)
258
self.check.pack(side='left')
259
# Draw any controls enabled by this one
261
self.enables.draw(self)
262
self.enables.pack(side='left', fill='x', expand=True)
265
self.enables.disable()
269
"""Enable/disable a Control based on the value of the Flag."""
273
self.enables.enable()
275
self.enables.disable()
278
"""Return a list of arguments for passing this command-line option.
279
draw() must be called before this function.
282
if self.get() == True:
284
args.append(self.option)
286
args.extend(self.enables.get_args())
289
### --------------------------------------------------------------------
291
class FlagGroup (Control):
292
"""A wrapper widget for grouping Flag controls, and allowing
293
mutually-exclusive flags.
300
"""Create a FlagGroup with the given label and state.
302
label: Label for the group
303
state: 'normal' for regular Flags, 'exclusive' for
304
mutually-exclusive Flags
305
*flags: All additional arguments are Flag controls
307
Control.__init__(self, str, '', label, '', '')
312
def draw(self, master):
313
"""Draw Flag controls in the given master."""
314
Control.draw(self, master)
315
frame = tk.LabelFrame(self, text=self.label)
316
frame.pack(fill='x', expand=True)
317
for flag in self.flags:
319
flag.check.bind('<Button-1>', self.select)
320
flag.pack(anchor='nw', side='top', fill='x', expand=True)
323
def select(self, event):
324
"""Event handler called when a Flag is selected."""
325
# For normal flags, nothing to do
326
if self.state != 'exclusive':
328
# For exclusive flags, clear all but the clicked Flag
329
for flag in self.flags:
330
if flag.check != event.widget:
335
"""Return a list of arguments for setting the relevant flag(s)."""
337
for flag in self.flags:
338
if flag.option != 'none':
339
args.extend(flag.get_args())
342
### --------------------------------------------------------------------
343
from libtovid.odict import Odict, convert_list
344
from support import ComboBox
346
class Choice (Control):
347
"""A widget for choosing one of several options.
358
"""Initialize Choice widget with the given label and list of choices.
360
label: Text label for the choices
361
option: Command-line option to set
362
default: Default choice, or None to use first choice in list
363
help: Help text to show in a tooltip
364
choices: Available choices, in string form: 'one|two|three'
365
or list form: ['one', 'two', 'three'], or as a
366
list-of-lists: [['a', "Use A"], ['b', "Use B"], ..].
367
A dictionary is also allowed, as long as you don't
368
care about preserving choice order.
369
style: 'radio' for radiobuttons, 'dropdown' for a drop-down list
371
self.choices = convert_list(choices)
372
Control.__init__(self, str, label, option,
373
default or self.choices.values()[0],
375
if style not in ['radio', 'dropdown']:
376
raise ValueError("Choice style must be 'radio' or 'dropdown'")
378
self.packside = packside
380
def draw(self, master):
381
"""Draw control widgets in the given master."""
382
Control.draw(self, master)
383
if self.style == 'radio':
384
frame = tk.LabelFrame(self, text=self.label)
385
frame.pack(anchor='nw', fill='x')
387
for choice, label in self.choices.items():
388
self.rb[choice] = tk.Radiobutton(frame,
389
text=label, value=choice, variable=self.variable)
390
self.rb[choice].pack(anchor='nw', side=self.packside)
391
else: # dropdown/combobox
392
tk.Label(self, text=self.label).pack(side='left')
393
self.combo = ComboBox(self, self.choices.keys(),
394
variable=self.variable)
395
self.combo.pack(side='left')
398
### --------------------------------------------------------------------
400
class Number (Control):
401
"""A widget for choosing or entering a number"""
412
"""Create a number-setting widget.
414
label: Text label describing the meaning of the number
415
option: Command-line option to set
416
default: Default value, or None to use minimum
417
help: Help text to show in a tooltip
418
min, max: Range of allowable numbers (inclusive)
419
style: 'spin' for a spinbox, or 'scale' for a slider
420
units: Units of measurement (ex. "kbits/sec"), used as a label
424
Control.__init__(self, int, label, option, default,
431
def draw(self, master):
432
"""Draw control widgets in the given master."""
433
Control.draw(self, master)
434
tk.Label(self, name='label', text=self.label).pack(side='left')
435
if self.style == 'spin':
436
self.number = tk.Spinbox(self, from_=self.min, to=self.max,
437
width=4, textvariable=self.variable)
438
self.number.pack(side='left')
439
tk.Label(self, name='units', text=self.units).pack(side='left')
442
tk.Label(self, name='units', text=self.units).pack(side='left')
443
self.number = tk.Scale(self, from_=self.min, to=self.max,
444
tickinterval=(self.max - self.min),
445
variable=self.variable, orient='horizontal')
446
self.number.pack(side='left', fill='x', expand=True)
449
def enable(self, enabled=True):
450
"""Enable or disable all sub-widgets."""
451
# Overridden to make Scale widget look disabled
452
Control.enable(self, enabled)
453
if self.style == 'scale':
455
self.number['fg'] = 'black'
456
self.number['troughcolor'] = 'white'
458
self.number['fg'] = '#A3A3A3'
459
self.number['troughcolor'] = '#D9D9D9'
462
### --------------------------------------------------------------------
464
class Text (Control):
465
"""A widget for entering a line of text"""
473
label: Label for the text
474
option: Command-line option to set
475
default: Default value of text widget
476
help: Help text to show in a tooltip
478
Control.__init__(self, str, label, option, default, help, **kwargs)
480
def draw(self, master):
481
"""Draw control widgets in the given master."""
482
Control.draw(self, master)
483
tk.Label(self, text=self.label, justify='left').pack(side='left')
484
self.entry = tk.Entry(self, textvariable=self.variable)
485
self.entry.pack(side='left', fill='x', expand=True)
488
### --------------------------------------------------------------------
492
"""A widget for entering a space-separated list of text items"""
499
Text.__init__(self, label, option, default, help, **kwargs)
501
def draw(self, master):
502
"""Draw control widgets in the given master."""
503
Text.draw(self, master)
507
"""Split text into a list at whitespace boundaries."""
508
text = Text.get(self)
509
return shlex.split(text)
511
def set(self, listvalue):
512
"""Set a value to a list, joined with spaces."""
513
text = ' '.join(listvalue)
517
### --------------------------------------------------------------------
518
from tkFileDialog import asksaveasfilename, askopenfilename
520
class Filename (Control):
521
"""A widget for entering or browsing for a filename"""
528
desc='Select a file to load',
530
"""Create a Filename with label, text entry, and browse button.
532
label: Text of label next to file entry box
533
option: Command-line option to set
534
default: Default filename
535
help: Help text to show in a tooltip
536
action: Do you intend to 'load' or 'save' this file?
537
desc: Brief description (shown in title bar of file
540
Control.__init__(self, str, label, option, default, help, **kwargs)
544
def draw(self, master):
545
"""Draw control widgets in the given master."""
546
Control.draw(self, master)
547
# Create and pack widgets
548
tk.Label(self, text=self.label, justify='left').pack(side='left')
549
self.entry = tk.Entry(self, textvariable=self.variable)
550
self.button = tk.Button(self, text="Browse...", command=self.browse)
551
self.entry.pack(side='left', fill='x', expand=True)
552
self.button.pack(side='left')
555
def browse(self, event=None):
556
"""Event handler when browse button is pressed"""
557
if self.action == 'save':
558
filename = asksaveasfilename(parent=self, title=self.desc)
560
filename = askopenfilename(parent=self, title=self.desc)
561
# Got a filename? Display it
565
### --------------------------------------------------------------------
566
import tkColorChooser
568
class Color (Control):
569
"""A widget for choosing a color"""
576
"""Create a widget that opens a color-chooser dialog.
578
label: Text label describing the color to be selected
579
option: Command-line option to set
580
default: Default color (named color or hexadecimal RGB)
581
help: Help text to show in a tooltip
583
Control.__init__(self, str, label, option, default, help, **kwargs)
585
def draw(self, master):
586
"""Draw control widgets in the given master."""
587
Control.draw(self, master)
588
tk.Label(self, text=self.label).pack(side='left')
589
self.button = tk.Button(self, text="None", command=self.change)
590
self.button.pack(side='left')
594
"""Choose a color, and set the button's label and color to match."""
595
rgb, color = tkColorChooser.askcolor(self.get())
598
self.button.config(text=color, foreground=color)
600
### --------------------------------------------------------------------
602
class Font (Control):
603
"""A font selector widget"""
610
"""Create a widget that opens a font chooser dialog.
612
label: Text label for the font
613
option: Command-line option to set
614
default: Default font
615
help: Help text to show in a tooltip
617
Control.__init__(self, str, label, option, default, help, **kwargs)
619
def draw(self, master):
620
"""Draw control widgets in the given master."""
621
Control.draw(self, master)
622
tk.Label(self, text=self.label).pack(side='left')
623
self.button = tk.Button(self, textvariable=self.variable,
625
self.button.pack(side='left', padx=8)
629
"""Open a font chooser to select a font."""
630
chooser = support.FontChooser()
632
self.variable.set(chooser.result)
634
### --------------------------------------------------------------------
635
from support import DragList
637
class TextList (Control):
638
"""A widget for listing and editing several text strings"""
645
Control.__init__(self, list, label, option, default, help, **kwargs)
647
def draw(self, master):
648
"""Draw control widgets in the given master."""
649
Control.draw(self, master)
650
frame = tk.LabelFrame(self, text=self.label)
651
frame.pack(fill='x', expand=True)
652
self.selected = tk.StringVar()
653
self.listbox = DragList(frame, choices=self.variable,
654
chosen=self.selected)
655
self.listbox.pack(fill='x', expand=True)
656
# TODO: Event handling to allow editing items
657
self.editbox = tk.Entry(frame, width=30, textvariable=self.selected)
658
self.editbox.bind('<Return>', self.setTitle)
659
self.editbox.pack(fill='x', expand=True)
662
def setTitle(self, event):
663
"""Event handler when Enter is pressed after editing a title."""
664
index = self.listbox.curindex
665
self.variable[index] = self.selected.get()
666
# TODO: Select next item in list and focus the editbox
668
### --------------------------------------------------------------------
669
from tkFileDialog import askopenfilenames
671
class FileList (Control):
672
"""A widget for listing several filenames"""
679
Control.__init__(self, list, label, option, default, help, **kwargs)
681
def draw(self, master):
682
"""Draw control widgets in the given master."""
683
Control.draw(self, master)
684
frame = tk.LabelFrame(self, text=self.label)
685
frame.pack(fill='x', expand=True)
687
self.listbox = DragList(frame, choices=self.variable,
689
self.listbox.pack(fill='x', expand=True)
691
group = tk.Frame(frame)
692
self.add = tk.Button(group, text="Add...", command=self.addFiles)
693
self.remove = tk.Button(group, text="Remove", command=self.removeFiles)
694
self.add.pack(side='left', fill='x', expand=True)
695
self.remove.pack(side='left', fill='x', expand=True)
699
def select(self, event=None):
700
"""Event handler when a filename in the list is selected.
705
"""Event handler to add files to the list"""
706
files = askopenfilenames(parent=self, title='Add files')
707
self.listbox.add(*files)
708
for dest in self.copies:
709
self.listbox.linked = dest.listbox
710
dest.listbox.linked = self.listbox
712
titles = [dest.filter(file) for file in files]
713
dest.listbox.add(*titles)
715
dest.listbox.add(*files)
717
def removeFiles(self):
718
"""Event handler to remove selected files from the list"""
719
# TODO: Support multiple selection
720
selected = self.listbox.curindex
721
self.listbox.delete(selected)
722
for control in self.copies:
723
control.listbox.delete(selected)
725
### --------------------------------------------------------------------
727
# Exported control classes, indexed by name
731
'Filename': Filename,
733
'FlagGroup': FlagGroup,
738
'FileList': FileList,
739
'TextList': TextList}
741
### --------------------------------------------------------------------
744
if __name__ == '__main__':
746
for name, control in CONTROLS.items():
747
frame = tk.LabelFrame(root, text=name, padx=10, pady=10,
748
font=('Helvetica', 10, 'bold'))
749
frame.pack(fill='both', expand=True)
752
widget.pack(fill='both')