pandora-scripts: Added new TV Out-Settings
[openpandora.oe.git] / recipes / pandora-system / pandora-scripts / TVoutConfig.py
1 #!/usr/bin/python
2
3 import os
4 import re
5 import sys
6 import gtk
7 import optparse
8
9 from ConfigModel import *
10
11 # ================================================================
12 # Todos and known bugs
13 # ================================================================
14 # Todo
15 # - compute PreviewPane constants PP_DX and PP_DY
16 # - test commandline interface
17
18 # ================================================================
19 # Constants
20 # ================================================================
21
22 # configuration files
23 GUI_DESCRIPTION = 'tvout.glade'
24 PROFILES = '/etc/pandora/conf/tvout-profiles.conf'
25
26 # Shell command to change settings:
27 SET_CONFIG_CMD = '"sudo /usr/pandora/scripts/op_tvout.sh %s"'
28 CONFIG_PARAMS = '-t %(encoding)s -c %(connection)s -l %(layer)s -%(type)ss %(width)s,%(height)s -%(type)sp %(x)s,%(y)s'
29
30 # Maximum vertical resolutions
31 Y_RES_PAL = 574
32 Y_RES_NTSC = 482
33
34 # Profile header string, should be a single line only
35 # This does NOT conflict with profile names.
36 PROF_HEADER = 'Last written configuration:'
37
38 # Paths for reading different configuration options
39 SETTINGS = dict(
40     size='/sys/devices/platform/omapdss/overlay2/output_size',
41     position='/sys/devices/platform/omapdss/overlay2/position',
42     enabled='/sys/devices/platform/omapdss/display1/enabled',
43     connection='/sys/devices/platform/omapdss/display1/venc_type', # if it exists
44     fb0='/sys/class/graphics/fb0/overlays',
45     fb1='/sys/class/graphics/fb1/overlays',
46 )
47
48 # format for saving/loading
49 FILE_ORDER = (
50     "enabled", "encoding", "connection", "layer", "x", "y", "width", "height")
51
52 # Default configurations
53 DC_DISABLED = "Disabled"
54 DC_DISABLED_DICT = dict(
55     enabled="False", encoding="pal", connection="composite", 
56     layer="0", width="658", height="520", x="35", y="35")
57 DC_DEFAULTS = {
58     DC_DISABLED: DC_DISABLED_DICT,
59     }
60
61 # Widget names
62 W_RADIO_BUTTONS = ('pal', 'ntsc', 'composite', 'svideo', 'layer0', 'layer1')
63 W_ADJUSTMENTS = ('x', 'y', 'width', 'height')
64 W_WIDGETS = ('enabled', 'pal', 'ntsc', 'composite', 'svideo', 
65              'layer0', 'layer1', 'width', 'height', 'x', 'y')
66
67 # Preview pane
68 PP_WIDGET = 'previewAlignment'
69 PP_HANDLES = ('xPaned', 'yPaned', 'widthPaned', 'heightPaned')
70 PP_DX = 26.0 # Todo: compute these
71 PP_DY = 14.0 # (they might be theme dependent)
72
73 ENABLED_USERDATA = 'enabled'
74 RADIO_USERDATA = dict(
75     pal='encoding', ntsc='encoding',
76     composite='connection', svideo='connection',
77     layer0='layer', layer1='layer')
78 ADJUSTMENT_USERDATA = ('width', 'height', 'x', 'y')
79
80 # Documentation for commandline options
81 HELP_ENABLED = "Enable TV-out. Valid values: True, False"
82 HELP_ENCODING = "Set encoding type (pal or ntsc)."
83 HELP_CONNECTION = "Set connection type (composite or svideo)"
84 HELP_LAYER = "Sets video layer to either main (0) or HW scaler / overlay (1)"
85 HELP_WIDTH = "Screen width (max 720)"
86 HELP_HEIGHT = "Screen height (max %s for pal and %s for ntsc)" % (Y_RES_PAL, Y_RES_NTSC)
87 HELP_X = "X coordinate of top left corner of the display area. Max 720 - width."
88 HELP_Y = "Y coordinate of top left corner of the display area. Y + HEIGHT may not exceed the max values specified in the height option."
89 HELP_SAVE = "Store current configuration as specified profile."
90 HELP_APPLY = "Activate the currently specified configuration."
91 HELP_LOAD = "Load and apply the specified configuration profile."
92 HELP_DEL = "Delete specified configuration profile."
93
94 # Commandline input type checking
95 TYPECHECK = dict(
96     enabled='(True|False)', layer='(0|1)',
97     encoding='(pal|ntsc)', connection='(composite|svideo)',
98     width='(\d+)', height='(\d+)', x='(\d+)', y='(\d+)',
99 )
100 RE_FORMAT = ' '.join(TYPECHECK[k] for k in FILE_ORDER)
101 BOUNDS = dict(width=(0,720), height=(0, Y_RES_PAL),
102               x=(0,720), y=(0,Y_RES_PAL))
103
104 # ================================================================
105 # Misc. auxiliary functions
106 # ================================================================
107
108 def Validate(value):
109     """Verifies all values in a profile string (see TVoutModel).
110
111     This is used to validate inputs passed through the commandline interface.
112
113     @value: a profile string.
114     """
115     mo = re.match(RE_FORMAT, value)
116     if mo is None:
117         return {}
118     else: # verify bounds
119         values = dict(zip(FILE_ORDER, mo.groups()))
120     
121         x = int(values['x'])
122         y = int(values['y'])
123         w = int(values['width'])
124         h = int(values['height'])
125         Y = Y_RES_PAL if values['encoding'] == 'pal' else Y_RES_NTSC
126         
127         if 720 < x + w:
128             print "Invalid position and/or size specified, x (%s) + width (%s) may not exceed 720." % (x, w)
129             sys.exit(1)
130         if Y < y + h:
131             print "Invalid position and/or size specified, y (%s) + height (%s) may not exceed %s." % (y, h, Y)
132             sys.exit(1)
133
134         if not all(BOUNDS[k][0] <= int(values[k]) <= BOUNDS[k][1] for k in BOUNDS):
135             print "Invalid position and/or size bounds specified"
136             sys.exit(1)
137         else:
138             return values
139
140 # ================================================================
141 # Main model & gui classes
142 # ================================================================
143
144 class TVoutModel(ConfigModel):
145     """Model for TV-out configuration.
146
147     Since not all TV-out settings can be easily read back from the system,
148     the last-saved profile is stored to fill in the gaps (mainly encoding).
149
150     This currently stores the following key/value pairs (all strings):
151     - "enabled": "True" | "False" # TV-out turned on/off
152     - "encoding": "pal" | "ntsc"  # which colour encoding is utilized
153     - "connection": "composite" | "svideo"
154     - "layer": "0" | "1"          # main video layer or hw scaler/overlay
155     - "x": "\d+"                  # left side of area displayed area
156     - "y": "\d+"                  # top side of the displayed area
157     - "width": "\d+"              # right side of the displayed area, 
158                                   # relative to "x"
159     - "height": "\d+"             # bottom side of the displayed area,
160                                   # relative to "y"
161     """
162     def __init__(self, *views):
163         self.last_written = None
164         ConfigModel.__init__(self, PROFILES, *views, **DC_DEFAULTS)
165
166     def fetch_profiles(self):
167         """Loads the profiles.
168
169         The header line is ignored and is included for user documentation only.
170         The second line is the last written profile.
171         The remainder of the file consists of a series of lines which 
172         alternating contain the profile name and a profile.
173
174         Profiles are stored as space separated values, in the following order:
175         <enabled> <encoding> <connection> <layer> <x> <y> <width> <height>
176         """
177         with open(self.profiles_file, 'r') as f:
178             f.readline() # header
179             self.last_written = self.string_to_profile(f.readline().strip()) # last_saved profile
180
181             name = None
182             for line in f:
183                 if name is None:
184                     name = line.rstrip()
185                 else:
186                     self.profiles[name] = self.string_to_profile(line.rstrip())
187                     name = None
188
189     def store_profiles(self):
190         """Write profiles to file.
191
192         Refer to the doc-string of fetch_profiles for the file format.
193         """
194         with open(self.profiles_file, 'w') as f:
195             f.write('%s\n%s\n' % (PROF_HEADER, self.profile_to_string(self.last_written)))
196             for name in sorted(self.profiles.keys()):
197                 f.write('%s\n%s\n' % (name, self.profile_to_string(self.profiles[name])))
198
199     def read_settings(self):
200         """Reads current TV-out configuration from the system.
201
202         The way the encoding is stored is tricky to convert back to pal/ntsc 
203         and might change in later hotfixes / firmware releases. Therefore this
204         value is read from the last written profile instead.
205
206         Layer is determined by interpreting the framebuffer values. 
207         If an unknown configuration is encountered, which is quite possible, 
208         then the last written configuration is read instead.
209         """
210         # Uncomment to test on desktop linux:
211         # self.load_profile(self.last_written)
212         # return
213
214         settings = {}
215         for key, path in SETTINGS.iteritems():
216             try:
217                 with open(path, 'r') as f:
218                     settings[key] = f.readline().strip()
219             except:
220                 settings[key] = self.last_written[key] if key in self.last_written else None
221
222         # This one is quite tricky to read directly from the system, yet unlikely to change often.
223         settings['encoding'] = self.last_written['encoding']
224
225         # These ones are stored as a single value
226         settings['width'], settings['height'] = settings.pop('size').split(',')
227         settings['x'], settings['y'] = settings.pop('position').split(',')
228
229         # To determine the layer we interpret the framebuffer values
230         fb0 = settings.pop('fb0')
231         fb1 = settings.pop('fb1')
232         settings['layer'] = ('1' if fb0 == '0' and fb1 == '1,2' else 
233                              ('0' if fb0 == '0,2' and fb1 == '1' else 
234                               self.last_written['layer']))
235
236         self.load_profile(settings)
237
238     def write_settings(self):
239         """Write configuration to the system.
240
241         This relies on op_tvout.sh to write the configuration, such that
242         the details can be easily changed in later firmware versions.
243
244         Documentation for op_tvout.sh:
245         op_tvout.sh [-d] [-t pal|ntsc] [-c composite|svideo] [-l 0|1] 
246                     [-{p|n}s w,h] [-{p|n}p x,y]
247
248         - op_tvout.sh -d      # just disables tv-out
249         - t ntsc -c composite # enable NTSC/composite mode
250         - l 1      
251           layer, 0 is the main layer and 1 is hardware scaler/overlay
252           (video layer, used by some emus too), only one can be used at a time)
253         - pp 0,0 -ps 640 480 
254           display position and size in 720xSomething space, 
255           this has to be tuned for every TV by user for best results, 
256           can't go out of range of 720xSomething (I think).
257         - np 0,0 -ns 640 480 # same for NTSC
258
259         The script expects you to supply everything at once.
260         """
261         self.last_written.update(self.settings)
262         if self.settings['enabled'] == 'False':
263             #print SET_CONFIG_CMD % '-d'
264             os.system(SET_CONFIG_CMD % '-d')
265         else:
266             pp = self.settings.copy()
267             pp['type'] = self.settings['encoding'][0]
268             #print SET_CONFIG_CMD % (CONFIG_PARAMS % pp)
269             os.system(SET_CONFIG_CMD % (CONFIG_PARAMS % pp))
270
271     def profile_to_string(self, dct):
272         """Converts settings dictionary to a string.
273
274         Profiles are stored as space separated values, in the following order:
275         <enabled> <encoding> <connection> <layer> <x> <y> <width> <height>
276         """
277         return ' '.join(dct[k] for k in FILE_ORDER)
278     
279     def string_to_profile(self, s):
280         """Converts a profile string back to a settings dictionary.
281
282         Profiles are stored as space separated values, in the following order:
283         <enabled> <encoding> <connection> <layer> <x> <y> <width> <height>
284         """
285         return dict(zip(FILE_ORDER, s.split(' ')))
286
287 class TVoutConfig(object):
288     """GUI application for modifying the pandora TV-out configuration
289
290     Design (tvout.glade):
291     +------------+---------------+--------------------+
292     |            | Position----- | Size-------------- |
293     |  Logo      | X: <spin btn> | Width: <spin btn>  |
294     |            | Y: <spin btn> | Height: <spin btn> |
295     +------------+---------------+--------------------+
296     |Encoding    | Overscan                           |
297     |. pal       | +--------------------------------+ |
298     |. ntsc      | |      <undisplayed area>        | |
299     +------------+ |   +-----------------------+    | |
300     |Connection  | |   |                       |    | |
301     |. composite | |   |   <displayed area>    |    | |
302     |. S-video   | |   |                       |    | |
303     +------------+ |   |                       |    | |
304     |Layer       | |   +-----------------------+    | |
305     |. Main      | |                                | |
306     |. overlay   | +--------------------------------+ |
307     +------------+------------------------------------+
308     |[v] enabled | [ Read settings ] [Write settings] |
309     |[delete p.] | {.............|v} [ Save profile ] |  
310     +------------+------------------------------------+
311
312     Important widget names:
313     - X,Y,Width and Height spinbuttons: x,y,width and height
314     - Radio buttons (for Encoding, Connection, Layer):
315       pal, ntsc, composite, svideo, layer0, layer1
316
317       These names correspond to their values as they are written,
318       except for layer0 and layer1 which are stored as strings(!) 0 and 1.
319     - The checkbox is called: enabled
320     - The buttons are called: 
321       readSettings, writeSettings, deleteProfile, saveProfile
322     - The combobox is called: ProfileComboEntry
323     - The displayed area is created by a set of GtkHPaned and GtkVPaned 
324       widgets called: xPaned, yPaned, widthPaned, heightPaned
325       They are contained in a frame called previewAlignment which can be
326       used to determine the total allocated size.
327     """
328
329     def __init__(self):
330         """Initialize the GUI application.
331         
332         Loads glade file, creates widget references, connects unbound handlers
333         (mainly those handlers which require userdata) and finally initializes
334         the model.
335         """
336         builder = gtk.Builder()
337         builder.add_from_file(
338             os.path.join(os.path.dirname(__file__), GUI_DESCRIPTION))
339         builder.connect_signals(self)
340
341         self.widgets = {}
342         for widgetname in W_WIDGETS:
343             self.widgets[widgetname] = builder.get_object(widgetname)
344
345         # Bind radiobutton signals
346         for widget, setting in RADIO_USERDATA.iteritems():
347             self.widgets[widget].connect('clicked', self.on_clicked, 
348                                       setting, widget.lstrip('layer'))
349
350         # Bind adjustment signals
351         for widget in ADJUSTMENT_USERDATA:
352             self.widgets[widget].get_adjustment().connect(
353                 'value_changed', self.on_value_changed, widget)
354         
355
356         # The allocated size of this widget is utilized to convert the 
357         # slider positions to absolute x,y,width and height values.
358         self.previewPane = builder.get_object(PP_WIDGET)
359
360         self.panedwidgets = {}
361         # Bind previewpane handle signals
362         for key in PP_HANDLES:
363             w = builder.get_object(key)
364             self.panedwidgets[key] = w
365             w.connect("notify::position", self.on_paned_position_change, key)
366
367         self.model = TVoutModel(self)
368
369         # We have two sets of widgets manipulating position and size
370         # (spinbuttons and the handles in the previewpane).
371         # To prevent an update race between these sets, the following flags 
372         # are set to block the handlers of the widgets which we are NOT 
373         # manipulating. The spinbuttons are more precise and therefore leading.
374         self.suppress_handles = False
375         self.suppress_spinbuttons = False
376
377         self.statusbar = builder.get_object('statusbar')
378         self.contextid = self.statusbar.get_context_id('')
379
380         self.profiles = gtk.ListStore(str)
381         for profile in DC_DEFAULTS:
382             self.profiles.append([profile])
383         for name in self.model.profiles:
384             self.profiles.append([name])
385
386         self.comboentry = builder.get_object('ProfileComboEntry')
387         self.comboentry.set_model(self.profiles)
388         self.comboentry.set_text_column(0)
389         self.comboentry.connect('changed', self.on_combo_changed)
390
391         self.entry = self.comboentry.get_child()
392         self.entry.connect('activate', self.on_LoadProfile_clicked)
393
394         # we need a reference to change their sensitive
395         # setting according to the profile
396         self.delete = builder.get_object('deleteProfile')
397         self.save = builder.get_object('saveProfile')
398
399         # this also changes sensitive of self.save & self.load
400         self.comboentry.set_active(0)
401
402         self.window = builder.get_object("window")
403         self.window.show_all()
404
405         accelgrp = gtk.AccelGroup()
406         key, mod = gtk.accelerator_parse('<Control>Q')
407         accelgrp.connect_group(key, mod, 0, self.on_window_destroy)
408         self.window.add_accel_group(accelgrp)
409
410         # read current config
411         self.model.read_settings()
412
413     def Notify(self, message):
414         """Displays a message on the statusbar"""
415         self.statusbar.pop(self.contextid)
416         self.statusbar.push(self.contextid, message)
417
418     def get_y_resolution(self):
419         """Returns the (mode-dependent) maximum Y resolution"""
420         return Y_RES_PAL if self.widgets['pal'].get_active() else Y_RES_NTSC
421
422     # --------------------------------
423     # View updates when model changes
424     # --------------------------------
425
426     def _update_profile_list(self, new, profiles):
427         """Show all default profiles and then the custom profiles.
428         
429         @new: name of newly selected profile
430         @profiles: dictionary of all profiles.
431         """
432         self.profiles.clear()
433         activate = 0
434         for i, key in enumerate(DC_DEFAULTS):
435             self.profiles.append([key])
436             if key == new:
437                 activate = i
438         offset = len(DC_DEFAULTS)
439         for i, key in enumerate(sorted(profiles.keys())):
440             self.profiles.append([key])
441             if key == new:
442                 activate = i + offset
443         self.comboentry.set_active(activate)
444             
445     def _update_widgets(self, settings):
446         """Update all widgets to reflect the configuration in settings"""
447         enabled = eval(settings['enabled'])
448         self.widgets['enabled'].set_active(enabled)
449
450         self.widgets[settings['encoding']].set_active(True)
451         self.widgets[settings['connection']].set_active(True)
452         self.widgets['layer'+settings['layer']].set_active(True)
453
454         for key in W_ADJUSTMENTS:
455             w = self.widgets[key]
456             w.get_adjustment().set_value(eval(settings[key]))
457             w.set_sensitive(enabled)
458         
459         for key in W_RADIO_BUTTONS:
460             self.widgets[key].set_sensitive(enabled)
461
462     def update_view(self, reason, *data):
463         """Dispatcher for events generated by the model."""
464         if reason == MODEL_PROFILE_CHANGE:
465             self._update_profile_list(*data)
466         elif reason == MODEL_VALUE_CHANGE:
467             self._update_widgets(*data)
468
469     def update_adjustments(self):
470         """Update all Gtk adjustments to recompute their maximum.
471
472         The value of x+width should not exceed 720.
473         The value of y+height should not exceed the maximum Y resolution.
474         """
475         x = self.widgets['x'].get_adjustment()
476         y = self.widgets['y'].get_adjustment()
477
478         xval = int(self.model.settings['x'])
479         yval = int(self.model.settings['y'])
480         wval = int(self.model.settings['width'])
481         hval = int(self.model.settings['height'])
482
483         # update vertical resolution thresholds
484         y.set_upper(self.get_y_resolution())
485         y.set_value(min(yval, y.get_upper()))
486
487         # update width thresholds
488         width = self.widgets['width'].get_adjustment()
489         width.set_upper(x.get_upper()-xval)
490         width.set_value(min(wval, width.get_upper()))
491
492         # update height thresholds
493         height = self.widgets['height'].get_adjustment()
494         height.set_upper(y.get_upper()-yval)
495         height.set_value(min(hval, height.get_upper()))
496
497     # --------------------------------
498     # Handling of widget changes
499     # --------------------------------
500
501     def on_toggled(self, widget, *data):
502         """Handles changes of the enabled radio button."""
503         enabled = widget.get_active()
504         self.model.settings[ENABLED_USERDATA] = str(enabled)
505         for name, widget in self.widgets.iteritems():
506             if name != 'enabled':
507                 widget.set_sensitive(enabled)
508
509     def on_clicked(self, widget, key=None, value=None):
510         """Handles changes of the radio buttons.
511         
512         @key: the setting being changed, i.e.
513               - "encoding" for pal/ntsc, 
514               - "connection" for composite/svideo
515               - "layer" for layer0/layer1. 
516         These correspond to the keys in the profile settings dictionaries.
517         @value: the value to be written to the dictionary.
518
519         When the encoding changes, so does the maximum Y resolution.
520         Hence it requires the adjustments to be updated.
521         """
522         if key:
523             self.model.settings[key] = value
524         if key == 'encoding':
525             self.update_adjustments()
526
527     def on_value_changed(self, widget, data):
528         """Handles spinbutton events.
529
530         If the user manipulates the spinbutton, the model is updated.
531         Note that suppress_handles is set to prevent the GtkPaned widgets
532         from retriggering an update. The precision of the Paned widgets
533         depends on screen resolution and tends to be less precise than
534         that of the spinbuttons causing an update-race.
535         """
536         if self.suppress_spinbuttons:
537             return
538
539         self.model.settings[data] = str(int(widget.get_value()))
540         self.update_adjustments()
541         yresolution = self.get_y_resolution()
542
543         # update the handle displays, but don't generate a new signal.
544         self.suppress_handles = True
545         self.set_pane_position(data + 'Paned', yresolution)
546         self.suppress_handles = False
547
548     # --------------------------------
549     # Button clicks
550     # --------------------------------
551
552     def on_readSettings_clicked(self, widget, *data):
553         """Handles a click on the readSettings button.
554         
555         Since it doesn't require userdata, this is connected through glade.
556         """
557         self.model.read_settings()
558         self.Notify("Active TV-out configuration loaded.")
559
560     def on_writeSettings_clicked(self, widget, *data):
561         """Handles a click on the writeSettings button.
562
563         Since it doesn't require userdata, this is connected through glade.
564         """
565         self.model.write_settings()
566         self.Notify("TV-out configuration updated.")
567
568     def on_deleteProfile_clicked(self, widget, *data):
569         """Handles a click on the deleteProfile button.
570
571         Since it doesn't require userdata, this is connected through glade.
572         The touch-screen is not the highest precision input and has no 
573         visual feedback before a click. Therefore we better prompt for
574         confirmation. Having an undo instead would probably be better.
575         """
576         name = self.entry.get_text()
577         if name not in self.model.profiles or name in DC_DEFAULTS:
578             self.Notify("Cannot remove profile, please select an existing non-default profile.")
579         else:
580             dialog = gtk.MessageDialog(flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_WARNING,
581                  buttons=gtk.BUTTONS_YES_NO,
582                  message_format="Are you sure you want to delete profile %s?" % name)
583             if dialog.run() == gtk.RESPONSE_YES:
584                 self.model.delete_profile(name)
585             dialog.destroy()
586
587     def on_saveProfile_clicked(self, widget, *data):
588         """Handles a click on the saveProfile button.
589
590         Since it doesn't require userdata, this is connected through glade.
591         Default profiles cannot be overwritten.
592         """
593         name = self.entry.get_text()
594         if name == '' or name in DC_DEFAULTS:
595             self.Notify("Invalid profile name")
596         else:
597             self.model.set_profile(name, self.model.settings)
598             self.Notify("Profile saved as: %s" % name)
599
600     # --------------------------------
601     # ComboBox
602     # --------------------------------
603
604     def _lose_active(self):
605         """Forces the comboboxentry to lose its active selection.
606         
607         This ensures that it also generates a changed signal when we
608         select the same value twice in a row such that a user can undo
609         any changes made to a loaded profile by loading the profile again.
610         """
611         temp = self.entry.get_text()
612         self.entry.set_text('')
613         self.entry.set_text(temp)
614
615     def on_combo_changed(self, widget, *data):
616         """Handles selection in the profile combo box."""
617         name = self.entry.get_text()
618         self.delete.set_sensitive(name in self.model.profiles)
619         self.save.set_sensitive(name != '' and name not in DC_DEFAULTS)
620         if self.comboentry.get_active() != -1:
621             self.on_LoadProfile_clicked(None)
622
623     def on_LoadProfile_clicked(self, widget, *data):
624         """Loads profile in the entry widget (by name).
625
626         It is directly activated by the activation event of the combobox entry
627         (i.e. when the user presses enter) and indirectly called by 
628         on_combo_changed (when the user performs a mouse selection).
629         """
630         self._lose_active()          # force widget to always emit changed signals,
631         name = self.entry.get_text() # ok since we always lookup by text value anyway
632         if not name:
633             return
634         elif name not in self.model.profiles and name not in DC_DEFAULTS:
635             self.Notify("Cannot load profile, please select an existing profile.")
636         else:
637             self.model.load_named_profile(name)
638             self.Notify("Profile loaded, hit 'Write settings' to make it active")
639
640     # --------------------------------
641
642     def set_pane_position(self, key, yresolution):
643         """Sync the GtkPaned widgets with the spinbuttons.
644
645         The Paned handles need their position specified in pixels.
646         This converts an absolute value to a relative position as the pane is 
647         not actually 720 pixels wide (nor high). 
648
649         PP_DX and PP_DY are corrections for the border around the client area.
650         They are measured for a specific theme, I'm not sure if changing theme
651         can invalidate this value but I guess it can.
652         """
653         value = int(self.model.settings[key.rstrip('Paned')])
654         widget = self.panedwidgets[key]
655         if key == 'widthPaned' or key == 'xPaned':
656             maxval = self.previewPane.allocation.width - PP_DX
657             newpos = (value / 720.) * maxval
658         else:
659             maxval = self.previewPane.allocation.height - PP_DY
660             newpos = (value / float(yresolution)) * maxval
661         widget.set_position(int(newpos))
662
663     def on_paned_position_change(self, widget, param, key):
664         """Handle dragging of the GtkPaned widgets.
665
666         This turns of handling of the spinbuttons as otherwise the spinbutton
667         update would re-trigger this event handler causing an update race due
668         to differences in precision.
669         """
670         if self.suppress_handles:
671             return
672
673         yresolution = self.get_y_resolution()
674         self.suppress_spinbuttons = True
675
676         maxval = self.previewPane.allocation.width - PP_DX        
677         for key in ('xPaned', 'widthPaned'):
678             pos = int((self.panedwidgets[key].get_position()/maxval)*720)
679             adj = self.widgets[key.rstrip('Paned')].get_adjustment()
680             adj.set_value(pos)
681             self.model.settings[key.rstrip('Paned')] = str(int(pos))
682
683         maxval = self.previewPane.allocation.height - PP_DY
684         for key in ('yPaned', 'heightPaned'):
685             pos = (self.panedwidgets[key].get_position()/maxval) * yresolution
686             adj = self.widgets[key.rstrip('Paned')].get_adjustment()
687             adj.set_value(int(pos))
688             self.model.settings[key.rstrip('Paned')] = str(int(pos))
689
690         self.update_adjustments()
691         self.suppress_spinbuttons = False
692         
693     def on_window_destroy(self, widget, *data):
694         """Handle application shutdown."""
695         self.Notify("Storing profiles...")
696         self.model.store_profiles()
697         gtk.main_quit()
698
699 # ================================================================
700
701 def main(args):
702     """Runs the application.
703
704     @args: command-line arguments (typically sys.argv).
705     """
706     if len(args) == 1: # no params: run gui app
707         app = TVoutConfig()
708         gtk.main()
709     else: # run command line app
710         parser = optparse.OptionParser()
711
712         parser.add_option('-e', '--enabled', default='', help=HELP_ENABLED)
713         parser.add_option('-t', '--encoding_type', default='', help=HELP_ENCODING)
714         parser.add_option('-c', '--connection_type', default='', help=HELP_CONNECTION)
715         parser.add_option('-l', '--layer', default='', help=HELP_LAYER)
716         parser.add_option('-w', '--width', default='', help=HELP_WIDTH)
717         parser.add_option('-g', '--height', default='', help=HELP_HEIGHT)
718         parser.add_option('-x', '--x_position', default='', help=HELP_X)
719         parser.add_option('-y', '--y_position', default='', help=HELP_Y)
720
721         parser.add_option('-s', '--save_profile', default='', help=HELP_SAVE)
722         parser.add_option('-a', '--apply', default=False, action='store_true', help=HELP_APPLY)
723         parser.add_option('-p', '--load_profile', default='', help=HELP_LOAD)
724         parser.add_option('-d', '--remove_profile', default='', help=HELP_DEL)
725
726         options, args = parser.parse_args()
727                           
728         model = TVoutModel()
729         model.read_settings()
730         if options.enabled:
731             model.settings['enabled'] = options.enabled
732         if options.encoding_type:
733             model.settings['encoding'] = options.encoding_type
734         if options.connection_type:
735             model.settings['connection'] = options.connection_type
736         if options.layer:
737             model.settings['layer'] = options.layer
738         if options.width:
739             model.settings['width'] = options.width
740         if options.height:
741             model.settings['height'] = options.height
742         if options.x_position:
743             model.settings['x'] = options.x_position
744         if options.y_position:
745             model.settings['y'] = options.y_position
746
747         profile_string = model.profile_to_string(model.settings)
748         config = Validate(profile_string)
749
750         if not config:
751             print "Invalid values encountered in profile (%s), terminating." % profile_string
752             sys.exit(1)
753
754         if options.save_profile and options.save_profile not in DC_DEFAULTS:
755             model.set_profile(options.save_profile, self.model.settings)
756         if options.apply:
757             model.write_settings()
758         if options.load_profile:
759             model.load_named_profile(options.load_profile)
760             model.write_settings()
761         if options.remove_profile:
762             model.delete_profile(options.remove_profile)
763
764         if options.save_profile or options.remove_profile:
765             model.store_profiles()
766
767 if __name__ == '__main__':
768     try:
769         main(sys.argv)
770     except Exception, e:
771         print e
772         sys.exit(1)
773     else:
774         sys.exit(0)