op_tvout: never use zero width/height
[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         # The system may have 0,0 w,h; use last_written or defaults if it does
230         for key in 'width', 'height':
231             if settings[key] == '0':
232                 settings[key] = (self.last_written[key] if self.last_written[key] != '0'
233                                  else DC_DISABLED_DICT[key])
234
235         # 0,0 pos is more likely to be intended, but still fallback to last_written
236         for key in 'x', 'y':
237             if settings[key] == '0':
238                 settings[key] = self.last_written[key]
239
240         # To determine the layer we interpret the framebuffer values
241         fb0 = settings.pop('fb0')
242         fb1 = settings.pop('fb1')
243         settings['layer'] = ('1' if fb0 == '0' and fb1 == '1,2' else 
244                              ('0' if fb0 == '0,2' and fb1 == '1' else 
245                               self.last_written['layer']))
246
247         self.load_profile(settings)
248
249     def write_settings(self):
250         """Write configuration to the system.
251
252         This relies on op_tvout.sh to write the configuration, such that
253         the details can be easily changed in later firmware versions.
254
255         Documentation for op_tvout.sh:
256         op_tvout.sh [-d] [-t pal|ntsc] [-c composite|svideo] [-l 0|1] 
257                     [-{p|n}s w,h] [-{p|n}p x,y]
258
259         - op_tvout.sh -d      # just disables tv-out
260         - t ntsc -c composite # enable NTSC/composite mode
261         - l 1      
262           layer, 0 is the main layer and 1 is hardware scaler/overlay
263           (video layer, used by some emus too), only one can be used at a time)
264         - pp 0,0 -ps 640 480 
265           display position and size in 720xSomething space, 
266           this has to be tuned for every TV by user for best results, 
267           can't go out of range of 720xSomething (I think).
268         - np 0,0 -ns 640 480 # same for NTSC
269
270         The script expects you to supply everything at once.
271         """
272         self.last_written.update(self.settings)
273         if self.settings['enabled'] == 'False':
274             #print SET_CONFIG_CMD % '-d'
275             os.system(SET_CONFIG_CMD % '-d')
276         else:
277             pp = self.settings.copy()
278             pp['type'] = self.settings['encoding'][0]
279             #print SET_CONFIG_CMD % (CONFIG_PARAMS % pp)
280             os.system(SET_CONFIG_CMD % (CONFIG_PARAMS % pp))
281
282     def profile_to_string(self, dct):
283         """Converts settings dictionary to a string.
284
285         Profiles are stored as space separated values, in the following order:
286         <enabled> <encoding> <connection> <layer> <x> <y> <width> <height>
287         """
288         return ' '.join(dct[k] for k in FILE_ORDER)
289     
290     def string_to_profile(self, s):
291         """Converts a profile string back to a settings dictionary.
292
293         Profiles are stored as space separated values, in the following order:
294         <enabled> <encoding> <connection> <layer> <x> <y> <width> <height>
295         """
296         return dict(zip(FILE_ORDER, s.split(' ')))
297
298 class TVoutConfig(object):
299     """GUI application for modifying the pandora TV-out configuration
300
301     Design (tvout.glade):
302     +------------+---------------+--------------------+
303     |            | Position----- | Size-------------- |
304     |  Logo      | X: <spin btn> | Width: <spin btn>  |
305     |            | Y: <spin btn> | Height: <spin btn> |
306     +------------+---------------+--------------------+
307     |Encoding    | Overscan                           |
308     |. pal       | +--------------------------------+ |
309     |. ntsc      | |      <undisplayed area>        | |
310     +------------+ |   +-----------------------+    | |
311     |Connection  | |   |                       |    | |
312     |. composite | |   |   <displayed area>    |    | |
313     |. S-video   | |   |                       |    | |
314     +------------+ |   |                       |    | |
315     |Layer       | |   +-----------------------+    | |
316     |. Main      | |                                | |
317     |. overlay   | +--------------------------------+ |
318     +------------+------------------------------------+
319     |[v] enabled | [ Read settings ] [Write settings] |
320     |[delete p.] | {.............|v} [ Save profile ] |  
321     +------------+------------------------------------+
322
323     Important widget names:
324     - X,Y,Width and Height spinbuttons: x,y,width and height
325     - Radio buttons (for Encoding, Connection, Layer):
326       pal, ntsc, composite, svideo, layer0, layer1
327
328       These names correspond to their values as they are written,
329       except for layer0 and layer1 which are stored as strings(!) 0 and 1.
330     - The checkbox is called: enabled
331     - The buttons are called: 
332       readSettings, writeSettings, deleteProfile, saveProfile
333     - The combobox is called: ProfileComboEntry
334     - The displayed area is created by a set of GtkHPaned and GtkVPaned 
335       widgets called: xPaned, yPaned, widthPaned, heightPaned
336       They are contained in a frame called previewAlignment which can be
337       used to determine the total allocated size.
338     """
339
340     def __init__(self):
341         """Initialize the GUI application.
342         
343         Loads glade file, creates widget references, connects unbound handlers
344         (mainly those handlers which require userdata) and finally initializes
345         the model.
346         """
347         builder = gtk.Builder()
348         builder.add_from_file(
349             os.path.join(os.path.dirname(__file__), GUI_DESCRIPTION))
350         builder.connect_signals(self)
351
352         self.widgets = {}
353         for widgetname in W_WIDGETS:
354             self.widgets[widgetname] = builder.get_object(widgetname)
355
356         # Bind radiobutton signals
357         for widget, setting in RADIO_USERDATA.iteritems():
358             self.widgets[widget].connect('clicked', self.on_clicked, 
359                                       setting, widget.lstrip('layer'))
360
361         # Bind adjustment signals
362         for widget in ADJUSTMENT_USERDATA:
363             self.widgets[widget].get_adjustment().connect(
364                 'value_changed', self.on_value_changed, widget)
365         
366
367         # The allocated size of this widget is utilized to convert the 
368         # slider positions to absolute x,y,width and height values.
369         self.previewPane = builder.get_object(PP_WIDGET)
370
371         self.panedwidgets = {}
372         # Bind previewpane handle signals
373         for key in PP_HANDLES:
374             w = builder.get_object(key)
375             self.panedwidgets[key] = w
376             w.connect("notify::position", self.on_paned_position_change, key)
377
378         self.model = TVoutModel(self)
379
380         # We have two sets of widgets manipulating position and size
381         # (spinbuttons and the handles in the previewpane).
382         # To prevent an update race between these sets, the following flags 
383         # are set to block the handlers of the widgets which we are NOT 
384         # manipulating. The spinbuttons are more precise and therefore leading.
385         self.suppress_handles = False
386         self.suppress_spinbuttons = False
387
388         self.statusbar = builder.get_object('statusbar')
389         self.contextid = self.statusbar.get_context_id('')
390
391         self.profiles = gtk.ListStore(str)
392         for profile in DC_DEFAULTS:
393             self.profiles.append([profile])
394         for name in self.model.profiles:
395             self.profiles.append([name])
396
397         self.comboentry = builder.get_object('ProfileComboEntry')
398         self.comboentry.set_model(self.profiles)
399         self.comboentry.set_text_column(0)
400         self.comboentry.connect('changed', self.on_combo_changed)
401
402         self.entry = self.comboentry.get_child()
403         self.entry.connect('activate', self.on_LoadProfile_clicked)
404
405         # we need a reference to change their sensitive
406         # setting according to the profile
407         self.delete = builder.get_object('deleteProfile')
408         self.save = builder.get_object('saveProfile')
409
410         # this also changes sensitive of self.save & self.load
411         self.comboentry.set_active(0)
412
413         self.window = builder.get_object("window")
414         self.window.show_all()
415
416         accelgrp = gtk.AccelGroup()
417         key, mod = gtk.accelerator_parse('<Control>Q')
418         accelgrp.connect_group(key, mod, 0, self.on_window_destroy)
419         self.window.add_accel_group(accelgrp)
420
421         # read current config
422         self.model.read_settings()
423
424     def Notify(self, message):
425         """Displays a message on the statusbar"""
426         self.statusbar.pop(self.contextid)
427         self.statusbar.push(self.contextid, message)
428
429     def get_y_resolution(self):
430         """Returns the (mode-dependent) maximum Y resolution"""
431         return Y_RES_PAL if self.widgets['pal'].get_active() else Y_RES_NTSC
432
433     # --------------------------------
434     # View updates when model changes
435     # --------------------------------
436
437     def _update_profile_list(self, new, profiles):
438         """Show all default profiles and then the custom profiles.
439         
440         @new: name of newly selected profile
441         @profiles: dictionary of all profiles.
442         """
443         self.profiles.clear()
444         activate = 0
445         for i, key in enumerate(DC_DEFAULTS):
446             self.profiles.append([key])
447             if key == new:
448                 activate = i
449         offset = len(DC_DEFAULTS)
450         for i, key in enumerate(sorted(profiles.keys())):
451             self.profiles.append([key])
452             if key == new:
453                 activate = i + offset
454         self.comboentry.set_active(activate)
455             
456     def _update_widgets(self, settings):
457         """Update all widgets to reflect the configuration in settings"""
458         enabled = eval(settings['enabled'])
459         self.widgets['enabled'].set_active(enabled)
460
461         self.widgets[settings['encoding']].set_active(True)
462         self.widgets[settings['connection']].set_active(True)
463         self.widgets['layer'+settings['layer']].set_active(True)
464
465         for key in W_ADJUSTMENTS:
466             w = self.widgets[key]
467             w.get_adjustment().set_value(eval(settings[key]))
468             w.set_sensitive(enabled)
469         
470         for key in W_RADIO_BUTTONS:
471             self.widgets[key].set_sensitive(enabled)
472
473     def update_view(self, reason, *data):
474         """Dispatcher for events generated by the model."""
475         if reason == MODEL_PROFILE_CHANGE:
476             self._update_profile_list(*data)
477         elif reason == MODEL_VALUE_CHANGE:
478             self._update_widgets(*data)
479
480     def update_adjustments(self):
481         """Update all Gtk adjustments to recompute their maximum.
482
483         The value of x+width should not exceed 720.
484         The value of y+height should not exceed the maximum Y resolution.
485         """
486         x = self.widgets['x'].get_adjustment()
487         y = self.widgets['y'].get_adjustment()
488
489         xval = int(self.model.settings['x'])
490         yval = int(self.model.settings['y'])
491         wval = int(self.model.settings['width'])
492         hval = int(self.model.settings['height'])
493
494         # update vertical resolution thresholds
495         y.set_upper(self.get_y_resolution())
496         y.set_value(min(yval, y.get_upper()))
497
498         # update width thresholds
499         width = self.widgets['width'].get_adjustment()
500         width.set_upper(x.get_upper()-xval)
501         width.set_value(min(wval, width.get_upper()))
502
503         # update height thresholds
504         height = self.widgets['height'].get_adjustment()
505         height.set_upper(y.get_upper()-yval)
506         height.set_value(min(hval, height.get_upper()))
507
508     # --------------------------------
509     # Handling of widget changes
510     # --------------------------------
511
512     def on_toggled(self, widget, *data):
513         """Handles changes of the enabled radio button."""
514         enabled = widget.get_active()
515         self.model.settings[ENABLED_USERDATA] = str(enabled)
516         for name, widget in self.widgets.iteritems():
517             if name != 'enabled':
518                 widget.set_sensitive(enabled)
519
520     def on_clicked(self, widget, key=None, value=None):
521         """Handles changes of the radio buttons.
522         
523         @key: the setting being changed, i.e.
524               - "encoding" for pal/ntsc, 
525               - "connection" for composite/svideo
526               - "layer" for layer0/layer1. 
527         These correspond to the keys in the profile settings dictionaries.
528         @value: the value to be written to the dictionary.
529
530         When the encoding changes, so does the maximum Y resolution.
531         Hence it requires the adjustments to be updated.
532         """
533         if key:
534             self.model.settings[key] = value
535         if key == 'encoding':
536             self.update_adjustments()
537
538     def on_value_changed(self, widget, data):
539         """Handles spinbutton events.
540
541         If the user manipulates the spinbutton, the model is updated.
542         Note that suppress_handles is set to prevent the GtkPaned widgets
543         from retriggering an update. The precision of the Paned widgets
544         depends on screen resolution and tends to be less precise than
545         that of the spinbuttons causing an update-race.
546         """
547         if self.suppress_spinbuttons:
548             return
549
550         self.model.settings[data] = str(int(widget.get_value()))
551         self.update_adjustments()
552         yresolution = self.get_y_resolution()
553
554         # update the handle displays, but don't generate a new signal.
555         self.suppress_handles = True
556         self.set_pane_position(data + 'Paned', yresolution)
557         self.suppress_handles = False
558
559     # --------------------------------
560     # Button clicks
561     # --------------------------------
562
563     def on_readSettings_clicked(self, widget, *data):
564         """Handles a click on the readSettings button.
565         
566         Since it doesn't require userdata, this is connected through glade.
567         """
568         self.model.read_settings()
569         self.Notify("Active TV-out configuration loaded.")
570
571     def on_writeSettings_clicked(self, widget, *data):
572         """Handles a click on the writeSettings button.
573
574         Since it doesn't require userdata, this is connected through glade.
575         """
576         self.model.write_settings()
577         self.Notify("TV-out configuration updated.")
578
579     def on_deleteProfile_clicked(self, widget, *data):
580         """Handles a click on the deleteProfile button.
581
582         Since it doesn't require userdata, this is connected through glade.
583         The touch-screen is not the highest precision input and has no 
584         visual feedback before a click. Therefore we better prompt for
585         confirmation. Having an undo instead would probably be better.
586         """
587         name = self.entry.get_text()
588         if name not in self.model.profiles or name in DC_DEFAULTS:
589             self.Notify("Cannot remove profile, please select an existing non-default profile.")
590         else:
591             dialog = gtk.MessageDialog(flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_WARNING,
592                  buttons=gtk.BUTTONS_YES_NO,
593                  message_format="Are you sure you want to delete profile %s?" % name)
594             if dialog.run() == gtk.RESPONSE_YES:
595                 self.model.delete_profile(name)
596             dialog.destroy()
597
598     def on_saveProfile_clicked(self, widget, *data):
599         """Handles a click on the saveProfile button.
600
601         Since it doesn't require userdata, this is connected through glade.
602         Default profiles cannot be overwritten.
603         """
604         name = self.entry.get_text()
605         if name == '' or name in DC_DEFAULTS:
606             self.Notify("Invalid profile name")
607         else:
608             self.model.set_profile(name, self.model.settings)
609             self.Notify("Profile saved as: %s" % name)
610
611     # --------------------------------
612     # ComboBox
613     # --------------------------------
614
615     def _lose_active(self):
616         """Forces the comboboxentry to lose its active selection.
617         
618         This ensures that it also generates a changed signal when we
619         select the same value twice in a row such that a user can undo
620         any changes made to a loaded profile by loading the profile again.
621         """
622         temp = self.entry.get_text()
623         self.entry.set_text('')
624         self.entry.set_text(temp)
625
626     def on_combo_changed(self, widget, *data):
627         """Handles selection in the profile combo box."""
628         name = self.entry.get_text()
629         self.delete.set_sensitive(name in self.model.profiles)
630         self.save.set_sensitive(name != '' and name not in DC_DEFAULTS)
631         if self.comboentry.get_active() != -1:
632             self.on_LoadProfile_clicked(None)
633
634     def on_LoadProfile_clicked(self, widget, *data):
635         """Loads profile in the entry widget (by name).
636
637         It is directly activated by the activation event of the combobox entry
638         (i.e. when the user presses enter) and indirectly called by 
639         on_combo_changed (when the user performs a mouse selection).
640         """
641         self._lose_active()          # force widget to always emit changed signals,
642         name = self.entry.get_text() # ok since we always lookup by text value anyway
643         if not name:
644             return
645         elif name not in self.model.profiles and name not in DC_DEFAULTS:
646             self.Notify("Cannot load profile, please select an existing profile.")
647         else:
648             self.model.load_named_profile(name)
649             self.Notify("Profile loaded, hit 'Write settings' to make it active")
650
651     # --------------------------------
652
653     def set_pane_position(self, key, yresolution):
654         """Sync the GtkPaned widgets with the spinbuttons.
655
656         The Paned handles need their position specified in pixels.
657         This converts an absolute value to a relative position as the pane is 
658         not actually 720 pixels wide (nor high). 
659
660         PP_DX and PP_DY are corrections for the border around the client area.
661         They are measured for a specific theme, I'm not sure if changing theme
662         can invalidate this value but I guess it can.
663         """
664         value = int(self.model.settings[key.rstrip('Paned')])
665         widget = self.panedwidgets[key]
666         if key == 'widthPaned' or key == 'xPaned':
667             maxval = self.previewPane.allocation.width - PP_DX
668             newpos = (value / 720.) * maxval
669         else:
670             maxval = self.previewPane.allocation.height - PP_DY
671             newpos = (value / float(yresolution)) * maxval
672         widget.set_position(int(newpos))
673
674     def on_paned_position_change(self, widget, param, key):
675         """Handle dragging of the GtkPaned widgets.
676
677         This turns of handling of the spinbuttons as otherwise the spinbutton
678         update would re-trigger this event handler causing an update race due
679         to differences in precision.
680         """
681         if self.suppress_handles:
682             return
683
684         yresolution = self.get_y_resolution()
685         self.suppress_spinbuttons = True
686
687         maxval = self.previewPane.allocation.width - PP_DX        
688         for key in ('xPaned', 'widthPaned'):
689             pos = int((self.panedwidgets[key].get_position()/maxval)*720)
690             adj = self.widgets[key.rstrip('Paned')].get_adjustment()
691             adj.set_value(pos)
692             self.model.settings[key.rstrip('Paned')] = str(int(pos))
693
694         maxval = self.previewPane.allocation.height - PP_DY
695         for key in ('yPaned', 'heightPaned'):
696             pos = (self.panedwidgets[key].get_position()/maxval) * yresolution
697             adj = self.widgets[key.rstrip('Paned')].get_adjustment()
698             adj.set_value(int(pos))
699             self.model.settings[key.rstrip('Paned')] = str(int(pos))
700
701         self.update_adjustments()
702         self.suppress_spinbuttons = False
703         
704     def on_window_destroy(self, widget, *data):
705         """Handle application shutdown."""
706         self.Notify("Storing profiles...")
707         self.model.store_profiles()
708         gtk.main_quit()
709
710 # ================================================================
711
712 def main(args):
713     """Runs the application.
714
715     @args: command-line arguments (typically sys.argv).
716     """
717     if len(args) == 1: # no params: run gui app
718         app = TVoutConfig()
719         gtk.main()
720     else: # run command line app
721         parser = optparse.OptionParser()
722
723         parser.add_option('-e', '--enabled', default='', help=HELP_ENABLED)
724         parser.add_option('-t', '--encoding_type', default='', help=HELP_ENCODING)
725         parser.add_option('-c', '--connection_type', default='', help=HELP_CONNECTION)
726         parser.add_option('-l', '--layer', default='', help=HELP_LAYER)
727         parser.add_option('-w', '--width', default='', help=HELP_WIDTH)
728         parser.add_option('-g', '--height', default='', help=HELP_HEIGHT)
729         parser.add_option('-x', '--x_position', default='', help=HELP_X)
730         parser.add_option('-y', '--y_position', default='', help=HELP_Y)
731
732         parser.add_option('-s', '--save_profile', default='', help=HELP_SAVE)
733         parser.add_option('-a', '--apply', default=False, action='store_true', help=HELP_APPLY)
734         parser.add_option('-p', '--load_profile', default='', help=HELP_LOAD)
735         parser.add_option('-d', '--remove_profile', default='', help=HELP_DEL)
736
737         options, args = parser.parse_args()
738                           
739         model = TVoutModel()
740         model.read_settings()
741         if options.enabled:
742             model.settings['enabled'] = options.enabled
743         if options.encoding_type:
744             model.settings['encoding'] = options.encoding_type
745         if options.connection_type:
746             model.settings['connection'] = options.connection_type
747         if options.layer:
748             model.settings['layer'] = options.layer
749         if options.width:
750             model.settings['width'] = options.width
751         if options.height:
752             model.settings['height'] = options.height
753         if options.x_position:
754             model.settings['x'] = options.x_position
755         if options.y_position:
756             model.settings['y'] = options.y_position
757
758         profile_string = model.profile_to_string(model.settings)
759         config = Validate(profile_string)
760
761         if not config:
762             print "Invalid values encountered in profile (%s), terminating." % profile_string
763             sys.exit(1)
764
765         if options.save_profile and options.save_profile not in DC_DEFAULTS:
766             model.set_profile(options.save_profile, self.model.settings)
767         if options.apply:
768             model.write_settings()
769         if options.load_profile:
770             model.load_named_profile(options.load_profile)
771             model.write_settings()
772         if options.remove_profile:
773             model.delete_profile(options.remove_profile)
774
775         if options.save_profile or options.remove_profile:
776             model.store_profiles()
777
778 if __name__ == '__main__':
779     try:
780         main(sys.argv)
781     except Exception, e:
782         print e
783         sys.exit(1)
784     else:
785         sys.exit(0)