~ubuntu-branches/ubuntu/karmic/tovid/karmic

« back to all changes in this revision

Viewing changes to libtovid/metagui/gui.py

  • Committer: Bazaar Package Importer
  • Author(s): Matvey Kozhev
  • Date: 2008-01-24 22:04:40 UTC
  • Revision ID: james.westby@ubuntu.com-20080124220440-x7cheljduf1rdgnq
Tags: upstream-0.31
ImportĀ upstreamĀ versionĀ 0.31

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#! /usr/bin/env python
 
2
# gui.py
 
3
 
 
4
"""Classes for creating and laying out GUI applications.
 
5
"""
 
6
 
 
7
__all__ = [
 
8
    'Panel',
 
9
    'HPanel',
 
10
    'VPanel',
 
11
    'Dropdowns',
 
12
    'Drawer',
 
13
    'Tabs',
 
14
    'Application',
 
15
    'GUI']
 
16
 
 
17
import sys
 
18
import Tkinter as tk
 
19
 
 
20
from widget import Widget
 
21
from control import Control, MissingOption
 
22
from libtovid.cli import Command
 
23
 
 
24
### --------------------------------------------------------------------
 
25
### GUI interface-definition API
 
26
### --------------------------------------------------------------------
 
27
 
 
28
class Panel (Widget):
 
29
    """A group of option controls in a rectangular frame.
 
30
 
 
31
    For example:
 
32
 
 
33
        Panel("General",
 
34
            Filename('bgaudio', "Background audio file"),
 
35
            Flag('submenus', "Create submenus"),
 
36
            Number('menu-length', "Length of menu (seconds)")
 
37
            )
 
38
 
 
39
    This creates a panel with three GUI widgets that control command-line
 
40
    options '-bgaudio', '-submenus', and '-menu-length'.
 
41
    """
 
42
    def __init__(self, title='', *contents):
 
43
        """Create a Panel to hold control widgets or sub-panels.
 
44
        
 
45
            title:    Title of panel (name shown in tab bar)
 
46
            contents: One or more Widgets (Controls, Panels, Drawers etc.)
 
47
        """
 
48
        Widget.__init__(self)
 
49
        if type(title) != str:
 
50
            raise TypeError("First argument to Panel must be a text label.")
 
51
        self.title = title
 
52
        self.contents = []
 
53
        for item in contents:
 
54
            if isinstance(item, Widget):
 
55
                self.contents.append(item)
 
56
            else:
 
57
                import traceback
 
58
                # Print helpful info from stack trace
 
59
                last_error = traceback.extract_stack()[0]
 
60
                filename, lineno, foo, code = last_error
 
61
                print "Error on line %(lineno)s of %(filename)s:" % vars()
 
62
                print "    " + code
 
63
                print "Panel '%s' may only contain Widget subclasses" \
 
64
                      " (Controls, Panels, Drawers etc.)" \
 
65
                      " got %s instead" % type(item)
 
66
                sys.exit(1)
 
67
 
 
68
    def draw(self, master, side='top'):
 
69
        """Draw Panel and its contents in the given master.
 
70
        """
 
71
        Widget.draw(self, master)
 
72
        # Get a labeled or unlabeled frame
 
73
        if self.title:
 
74
            frame = tk.LabelFrame(self, text=self.title)
 
75
            frame.pack(fill='both', expand=True)
 
76
        else:
 
77
            frame = self
 
78
        # Draw all contents in the frame
 
79
        for item in self.contents:
 
80
            item.draw(frame)
 
81
            item.pack(side=side, anchor='nw', fill='x', expand=True,
 
82
                  padx=4, pady=4)
 
83
 
 
84
    def get_args(self):
 
85
        """Return a list of all command-line options from contained widgets.
 
86
        Print error messages if any required options are missing.
 
87
        """
 
88
        args = []
 
89
        for item in self.contents:
 
90
            try:
 
91
                args += item.get_args()
 
92
            except MissingOption, missing:
 
93
                print "Missing a required option: " + missing.option
 
94
        return args
 
95
 
 
96
### --------------------------------------------------------------------
 
97
 
 
98
class HPanel (Panel):
 
99
    """A panel with widgets packed left-to-right"""
 
100
    def __init__(self, title='', *contents):
 
101
        Panel.__init__(self, title, *contents)
 
102
 
 
103
    def draw(self, master):
 
104
        Panel.draw(self, master, 'left')
 
105
 
 
106
### --------------------------------------------------------------------
 
107
 
 
108
class VPanel (Panel):
 
109
    """A panel with widgets packed top-to-bottom"""
 
110
    def __init__(self, title='', *contents):
 
111
        Panel.__init__(self, title, *contents)
 
112
 
 
113
    def draw(self, master):
 
114
        Panel.draw(self, master, 'top')
 
115
 
 
116
### --------------------------------------------------------------------
 
117
from support import ComboBox, ListVar
 
118
from control import Control
 
119
from libtovid.odict import Odict
 
120
 
 
121
class Dropdowns (Panel):
 
122
    """A Panel that uses dropdowns for selecting and setting options.
 
123
    
 
124
    Given a list of controls, the Dropdowns panel displays, initially,
 
125
    a single dropdown list. Each control is a choice in the list, and
 
126
    is shown as two columns, option and label (with help shown in a tooltip).
 
127
    
 
128
    Selecting a control causes that control to be displayed, so that it
 
129
    may be set to the desired value (along with a "remove" button to discard
 
130
    the control). The dropdown list is shifted downward, so another control
 
131
    and option may be set.
 
132
    """
 
133
    def __init__(self, title='', *contents):
 
134
        Panel.__init__(self, title, *contents)
 
135
        # Controls, indexed by option
 
136
        self.controls = Odict()
 
137
        for control in self.contents:
 
138
            if not isinstance(control, Control):
 
139
                raise TypeError("Can only add Controls to a Dropdown")
 
140
            self.controls[control.option] = control
 
141
 
 
142
    def draw(self, master):
 
143
        if self.title:
 
144
            tk.LabelFrame.__init__(self, master, text=self.title,
 
145
                                   padx=8, pady=8)
 
146
        else:
 
147
            tk.LabelFrame.__init__(self, master, bd=0, text='',
 
148
                                   padx=8, pady=8)
 
149
        self.choices = ListVar(items=self.controls.keys())
 
150
        self.chosen = tk.StringVar()
 
151
        self.chooser = ComboBox(self, self.choices, variable=self.chosen,
 
152
                                command=self.choose_new)
 
153
        self.chooser.pack(fill='both', expand=True)
 
154
 
 
155
    def choose_new(self, event=None):
 
156
        """Create and display the chosen control."""
 
157
        chosen = self.chosen.get()
 
158
        if chosen == '':
 
159
            return
 
160
        self.chooser.pack_forget()
 
161
        # Put control and remove button in a frame
 
162
        frame = tk.Frame(self)
 
163
        button = tk.Button(frame, text="X",
 
164
                           command=lambda:self.remove(chosen))
 
165
        button.pack(side='left')
 
166
        control = self.controls[chosen]
 
167
        control.draw(frame)
 
168
        control.pack(side='left', fill='x', expand=True)
 
169
        frame.pack(fill='x', expand=True)
 
170
        # Remove the chosen control/panel from the list of available ones
 
171
        self.choices.remove(chosen)
 
172
        self.chooser.pack()
 
173
 
 
174
    def remove(self, option):
 
175
        """Remove a given option's control from the interface."""
 
176
        frame = self.controls[option].master
 
177
        frame.pack_forget()
 
178
        frame.destroy()
 
179
        # Make this option available in the dropdown
 
180
        self.choices.append(option)
 
181
 
 
182
    def get_args(self):
 
183
        """Return a list of all command-line options from contained widgets.
 
184
        """
 
185
        args = []
 
186
        for control in self.contents:
 
187
            if control.active:
 
188
                args += control.get_args()
 
189
        return args
 
190
 
 
191
 
 
192
### --------------------------------------------------------------------
 
193
 
 
194
class Drawer (Widget):
 
195
    """Like a Panel, but may be hidden or "closed" like a drawer."""
 
196
    def __init__(self, title='', *contents):
 
197
        Widget.__init__(self)
 
198
        self.panel = Panel(title, *contents)
 
199
        self.visible = False
 
200
        
 
201
    def draw(self, master):
 
202
        Widget.draw(self, master)
 
203
        # Checkbutton
 
204
        button = tk.Button(self, text=self.panel.title,
 
205
                           command=self.show_hide)
 
206
        button.pack(anchor='nw', fill='x', expand=True)
 
207
        # Draw panel, but don't pack
 
208
        self.panel.draw(self)
 
209
 
 
210
    def show_hide(self):
 
211
        # Hide if showing
 
212
        if self.visible:
 
213
            self.panel.pack_forget()
 
214
            self.visible = False
 
215
        # Show if hidden
 
216
        else:
 
217
            self.panel.pack(anchor='nw', fill='both', expand=True)
 
218
            self.visible = True
 
219
    
 
220
    def get_args(self):
 
221
        return self.panel.get_args()
 
222
 
 
223
### --------------------------------------------------------------------
 
224
 
 
225
class Tabs (Widget):
 
226
    """A widget with tab buttons that switch between several panels.
 
227
    """
 
228
    def __init__(self, *panels, **kwargs):
 
229
        """Create tabs that switch between several Panels.
 
230
        """
 
231
        Widget.__init__(self)
 
232
        self.index = 0
 
233
        self.panels = []
 
234
        for panel in panels:
 
235
            if not isinstance(panel, Panel):
 
236
                raise TypeError("Tabs may only contain Panels")
 
237
            self.panels.append(panel)
 
238
 
 
239
    def add(self, panel):
 
240
        """Add the given Panel to the Tabs.
 
241
        """
 
242
        self.panels.append(panel)
 
243
 
 
244
    def draw(self, master, side='top'):
 
245
        """Draw the Tabs widget in the given master."""
 
246
        Widget.draw(self, master)
 
247
        self.selected = tk.IntVar()
 
248
        self.side = side
 
249
        # Tkinter configuration common to all tab buttons
 
250
        config = {
 
251
            'variable': self.selected,
 
252
            'command': self.change,
 
253
            'selectcolor': 'white',
 
254
            'relief': 'sunken',
 
255
            'offrelief': 'groove',
 
256
            'indicatoron': 0,
 
257
            'padx': 4, 'pady': 4
 
258
            }
 
259
        # Frame to hold tab buttons
 
260
        self.buttons = tk.Frame(self)
 
261
        # For tabs on left or right, pack tab buttons vertically
 
262
        if self.side in ['left', 'right']:
 
263
            button_side = 'top'
 
264
            bar_anchor = 'n'
 
265
            bar_fill = 'y'
 
266
        else:
 
267
            button_side = 'left'
 
268
            bar_anchor = 'w'
 
269
            bar_fill = 'x'
 
270
        # Tab buttons, numbered from 0
 
271
        for index, panel in enumerate(self.panels):
 
272
            button = tk.Radiobutton(self.buttons, text=panel.title,
 
273
                                    value=index, **config)
 
274
            button.pack(anchor='nw', side=button_side,
 
275
                        fill='both', expand=True)
 
276
            panel.draw(self)
 
277
        self.buttons.pack(anchor=bar_anchor, side=self.side,
 
278
                          fill=bar_fill)
 
279
        # Activate the first tab
 
280
        self.selected.set(0)
 
281
        self.change()
 
282
 
 
283
    def change(self):
 
284
        """Switch to the selected tab's frame.
 
285
        """
 
286
        # Unpack the existing panel
 
287
        self.panels[self.index].pack_forget()
 
288
        # Pack the newly-selected panel
 
289
        selected = self.selected.get()
 
290
        self.panels[selected].pack(side=self.side, fill='both', expand=True)
 
291
        # Remember this tab's index
 
292
        self.index = selected
 
293
 
 
294
    def get_args(self):
 
295
        """Return command-line arguments from each of the tab Panels.
 
296
        """
 
297
        args = []
 
298
        for panel in self.panels:
 
299
            args += panel.get_args()
 
300
        return args
 
301
 
 
302
### --------------------------------------------------------------------
 
303
import tkMessageBox
 
304
 
 
305
class Application (Widget):
 
306
    """Graphical frontend for a command-line program
 
307
    """
 
308
    def __init__(self, program, panels=None, style=None):
 
309
        """Define a GUI application frontend for a command-line program.
 
310
        
 
311
            program: Command-line program that the GUI is a frontend for
 
312
            panels:  List of Panels (groups of widgets), containing controls
 
313
                     for the given program's options. Use [panel] to pass
 
314
                     a single panel. If there are multiple panels, a tabbed
 
315
                     application is created.
 
316
 
 
317
        After defining the Application, call run() to show/execute it.
 
318
        """
 
319
        Widget.__init__(self)
 
320
        self.program = program
 
321
        # TODO: Friendlier error-handling
 
322
        if not type(panels) == list or len(panels) == 0:
 
323
            raise TypeError("Application needs a list of Panels")
 
324
        self.panels = panels or []
 
325
        self.showing = False
 
326
        self.frame = None
 
327
 
 
328
    def draw(self, master):
 
329
        """Draw the Application in the given master.
 
330
        """
 
331
        Widget.draw(self, master)
 
332
        # Single-panel application
 
333
        if len(self.panels) == 1:
 
334
            panel = self.panels[0]
 
335
            panel.draw(self)
 
336
            panel.pack(anchor='n', fill='x', expand=True)
 
337
        # Multi-panel (tabbed) application
 
338
        else:
 
339
            tabs = Tabs(*self.panels)
 
340
            tabs.draw(self)
 
341
            tabs.pack(anchor='n', fill='x', expand=True)
 
342
        # "Run" button
 
343
        button = tk.Button(self, text="Run %s now" % self.program,
 
344
                           command=self.execute)
 
345
        button.pack(anchor='s', fill='x')
 
346
 
 
347
    def get_args(self):
 
348
        """Get a list of all command-line arguments from all panels.
 
349
        """
 
350
        if isinstance(self.panels, Panel):
 
351
            return self.panels.get_args()
 
352
        elif isinstance(self.panels, list):
 
353
            args = []
 
354
            for panel in self.panels:
 
355
                args += panel.get_args()
 
356
            return args
 
357
 
 
358
    def execute(self):
 
359
        """Run the program with all the supplied options.
 
360
        """
 
361
        args = self.get_args()
 
362
        command = Command(self.program, *args)
 
363
 
 
364
        print "Running command:", command
 
365
        # Verify with user
 
366
        if tkMessageBox.askyesno(message="Run %s now?" % self.program):
 
367
            # Kind of hackish...
 
368
            root = self.master.master
 
369
            root.withdraw()
 
370
            try:
 
371
                command.run()
 
372
            except KeyboardInterrupt:
 
373
                tkMessageBox.showerror(message="todisc was interrupted!")
 
374
            else:
 
375
                tkMessageBox.showinfo(message="todisc finished running!")
 
376
            root.deiconify()
 
377
 
 
378
### --------------------------------------------------------------------
 
379
from support import ConfigWindow, Style
 
380
import os
 
381
DEFAULT_CONFIG = os.path.expanduser('~/.metagui/config')
 
382
 
 
383
class GUI (tk.Tk):
 
384
    """GUI with one or more Applications
 
385
    """
 
386
    def __init__(self, title, applications, width=600, height=600,
 
387
                 inifile=None):
 
388
        """Create a GUI for the given applications.
 
389
        
 
390
            title:        Text shown in the title bar
 
391
            applications: List of Applications to included in the GUI
 
392
            width:        Initial width of GUI window in pixels
 
393
            height:       Initial height of GUI window in pixels
 
394
            inifile:      Name of an .ini-formatted file with GUI configuration
 
395
        """
 
396
        tk.Tk.__init__(self)
 
397
        self.geometry("%dx%d" % (width, height))
 
398
        self.title(title)
 
399
        # TODO: Friendlier error-handling
 
400
        if not type(applications) == list or len(applications) == 0:
 
401
            raise TypeError("GUI needs a list of Applications")
 
402
        self.apps = applications
 
403
        self.width = width
 
404
        self.height = height
 
405
        self.inifile = inifile or DEFAULT_CONFIG
 
406
        self.style = Style()
 
407
        if os.path.exists(self.inifile):
 
408
            print "Loading style from config file", self.inifile
 
409
            self.style.load(self.inifile)
 
410
        else:
 
411
            print "Creating config file", self.inifile
 
412
            self.style.save(self.inifile)
 
413
        # Show hidden file option in file dialogs
 
414
        self.tk.call('namespace', 'import', '::tk::dialog::file::')
 
415
        self.tk.call('set', '::tk::dialog::file::showHiddenBtn',  '1')
 
416
        self.tk.call('set', '::tk::dialog::file::showHiddenVar',  '0')
 
417
 
 
418
    def run(self):
 
419
        """Run the GUI"""
 
420
        self.draw()
 
421
        self.draw_menu(self)
 
422
        # Enter the main event handler
 
423
        self.mainloop()
 
424
        # TODO: Interrupt handling
 
425
 
 
426
    def draw(self):
 
427
        """Draw widgets."""
 
428
        self.style.apply(self)
 
429
        self.frame = tk.Frame(self, width=self.width, height=self.height)
 
430
        self.frame.pack(fill='both', expand=True)
 
431
        self.resizable(width=True, height=True)
 
432
        # Single-application GUI
 
433
        if len(self.apps) == 1:
 
434
            app = self.apps[0]
 
435
            app.draw(self.frame)
 
436
            app.pack(anchor='n', fill='both', expand=True)
 
437
        # Multi-application (tabbed) GUI
 
438
        else:
 
439
            tabs = Tabs(self.frame, 'top')
 
440
            for app in self.apps:
 
441
                app.draw(tabs)
 
442
                tabs.add(app.program, app)
 
443
            tabs.draw()
 
444
            tabs.pack(anchor='n', fill='both', expand=True)
 
445
        #self.frame.pack_propagate(False)
 
446
    
 
447
    def redraw(self):
 
448
        self.frame.destroy()
 
449
        self.draw()
 
450
 
 
451
    def draw_menu(self, window):
 
452
        """Draw a menu bar in the given top-level window.
 
453
        """
 
454
        # Create and add the menu bar
 
455
        menubar = tk.Menu(window)
 
456
        window.config(menu=menubar)
 
457
        # File menu
 
458
        filemenu = tk.Menu(menubar, tearoff=False)
 
459
        filemenu.add_command(label="Config", command=self.show_config)
 
460
        filemenu.add_separator()
 
461
        filemenu.add_command(label="Exit", command=self.quit)
 
462
        menubar.add_cascade(label="File", menu=filemenu)
 
463
 
 
464
    def show_config(self):
 
465
        """Open the GUI configuration dialog."""
 
466
        config = ConfigWindow(self, self.style)
 
467
        if config.result:
 
468
            self.style = config.result
 
469
            print "Saving configuration to", self.inifile
 
470
            self.style.save(self.inifile)
 
471
            self.style.apply(self)
 
472
            self.redraw()
 
473
 
 
474
### --------------------------------------------------------------------
 
475