1 """The OptionsBox widget is used to edit an OptionGroup.
2 For simple applications, rox.edit_options() provides an
3 easy way to edit the options.
5 You can add new types of option by appending to widget_registry (new
6 in ROX-Lib 1.9.13). Return a list of widgets (which are packed into either an
7 HBox or a VBox). For example, to add a button widget:
9 def build_button(box, node, label):
10 button = g.Button(label)
11 box.may_add_tip(button, node)
12 button.connect('clicked', my_button_handler)
14 OptionsBox.widget_registry['button'] = build_button
16 You can then create such a button in Options.xml with:
18 <button label='...'>Tooltip</button>
20 For widgets that have options, your build function will be called with
21 the option as a third parameter. You should register get and set methods,
22 and arrange for box.check_widget to be called when the user changes the
25 def build_toggle(box, node, label, option):
26 toggle = g.CheckButton(label)
27 box.may_add_tip(toggle, node)
29 box.handlers[option] = (
30 lambda: str(toggle.get_active()),
31 lambda: toggle.set_active(option.int_value))
33 toggle.connect('toggled', lambda w: box.check_widget(option))
36 OptionsBox.widget_registry['mytoggle'] = build_toggle
39 from rox
import g
, options
, _
41 from xml
.dom
import Node
, minidom
47 """Return all the text directly inside this DOM Node."""
48 return ''.join([text
.nodeValue
for text
in node
.childNodes
49 if text
.nodeType
== Node
.TEXT_NODE
])
51 class OptionsBox(g
.Dialog
):
52 """OptionsBox can be sub-classed to provide your own widgets, by
53 creating extra build_* functions. Each build funtion takes a DOM
54 Element from the <app_dir>/Options.xml file and returns a list of
55 GtkWidgets to add to the box. The function should be named after
56 the element (<foo> -> def build_foo()).
58 When creating the widget, self.handlers[option] should be set to
59 a pair of functions (get, set) called to get and set the value
62 When the widget is modified, call self.check_widget(option) to
63 update the stored values.
65 def __init__(self
, options_group
, options_xml
, translation
= None):
66 """options_xml is an XML file, usually <app_dir>/Options.xml,
67 which defines the layout of the OptionsBox.
69 It contains an <options> root element containing (nested)
70 <section> elements. Each <section> contains a number of widgets,
71 some of which correspond to options. The build_* functions are
78 <section title='First section'>
79 <label>Here are some options</label>
80 <entry name='default_name' label='Default file name'>
81 When saving an untitled file, use this name as the default.
83 <section title='Nested section'>
89 assert isinstance(options_group
, options
.OptionGroup
)
91 if translation
is None:
93 if hasattr(__main__
.__builtins
__, '_'):
94 translation
= __main__
.__builtins
__._
96 translation
= lambda x
: x
99 g
.Dialog
.__init
__(self
)
100 self
.tips
= g
.Tooltips()
101 self
.set_has_separator(False)
103 self
.options
= options_group
104 self
.set_title(('%s options') % options_group
.program
)
105 self
.set_position(g
.WIN_POS_CENTER
)
107 button
= rox
.ButtonMixed(g
.STOCK_UNDO
, _('_Revert'))
108 self
.add_action_widget(button
, REVERT
)
109 self
.tips
.set_tip(button
, _('Restore all options to how they were '
110 'when the window was opened'))
112 self
.add_button(g
.STOCK_OK
, g
.RESPONSE_OK
)
114 doc
= minidom
.parse(options_xml
)
115 assert doc
.documentElement
.localName
== 'options'
117 self
.handlers
= {} # Option -> (get, set)
118 self
.revert
= {} # Option -> old value
120 self
.build_window_frame()
124 for section
in doc
.documentElement
.childNodes
:
125 if section
.nodeType
!= Node
.ELEMENT_NODE
:
127 if section
.localName
!= 'section':
128 print "Unknown section", section
130 self
.build_section(section
, None)
133 self
.tree_view
.expand_all()
135 self
.sections_swin
.hide()
139 def destroyed(widget
):
145 rox
.report_exception()
146 self
.connect('destroy', destroyed
)
148 def got_response(widget
, response
):
149 if response
== g
.RESPONSE_OK
:
151 elif response
== REVERT
:
152 for o
in self
.options
:
153 o
._set
(self
.revert
[o
])
154 self
.update_widgets()
155 self
.options
.notify()
157 self
.connect('response', got_response
)
160 """Show the window, updating all the widgets at the same
161 time. Use this instead of show()."""
163 for option
in self
.options
:
164 self
.revert
[option
] = option
.value
165 self
.update_widgets()
169 def update_revert(self
):
170 "Shade/unshade the Revert button. Internal."
171 self
.set_response_sensitive(REVERT
, self
.changed())
174 """Check whether any options have different values (ie, whether Revert
175 will do anything)."""
176 for option
in self
.options
:
177 if option
.value
!= self
.revert
[option
]:
181 def update_widgets(self
):
182 "Make widgets show current values. Internal."
183 assert not self
.updating
187 for option
in self
.options
:
189 handler
= self
.handlers
[option
][1]
191 print "No widget for option '%s'!" % option
197 def build_window_frame(self
):
198 "Create the main structure of the window."
199 hbox
= g
.HBox(False, 4)
200 self
.vbox
.pack_start(hbox
, True, True, 0)
202 # scrolled window for the tree view
203 sw
= g
.ScrolledWindow()
204 sw
.set_shadow_type(g
.SHADOW_IN
)
205 sw
.set_policy(g
.POLICY_NEVER
, g
.POLICY_AUTOMATIC
)
206 hbox
.pack_start(sw
, False, True, 0)
207 self
.sections_swin
= sw
# Used to hide it...
210 model
= g
.TreeStore(gobject
.TYPE_STRING
, gobject
.TYPE_INT
)
211 tv
= g
.TreeView(model
)
212 sel
= tv
.get_selection()
213 sel
.set_mode(g
.SELECTION_BROWSE
)
214 tv
.set_headers_visible(False)
215 self
.sections
= model
217 tv
.unset_flags(g
.CAN_FOCUS
) # Stop irritating highlight
219 # Add a column to display column 0 of the store...
220 cell
= g
.CellRendererText()
221 column
= g
.TreeViewColumn('Section', cell
, text
= 0)
222 tv
.append_column(column
)
228 frame
.set_shadow_type(g
.SHADOW_IN
)
229 hbox
.pack_start(frame
, True, True, 0)
231 notebook
= g
.Notebook()
232 notebook
.set_show_tabs(False)
233 notebook
.set_show_border(False)
235 self
.notebook
= notebook
238 # (sel = sel; pygtk bug?)
239 def change_page(tv
, sel
= sel
, notebook
= notebook
):
240 selected
= sel
.get_selected()
243 model
, iter = selected
244 page
= model
.get_value(iter, 1)
246 notebook
.set_current_page(page
)
247 sel
.connect('changed', change_page
)
251 def check_widget(self
, option
):
252 "A widgets call this when the user changes its value."
256 assert isinstance(option
, options
.Option
)
258 new
= self
.handlers
[option
][0]()
260 if new
== option
.value
:
264 self
.options
.notify()
267 def build_section(self
, section
, parent
):
268 """Create a new page for the notebook and a new entry in the
269 sections tree, and build all the widgets inside the page."""
270 page
= g
.VBox(False, 4)
271 page
.set_border_width(4)
272 self
.notebook
.append_page(page
, g
.Label('unused'))
274 iter = self
.sections
.append(parent
)
275 self
.sections
.set(iter,
276 0, self
._(section
.getAttribute('title')),
277 1, self
.notebook
.page_num(page
))
278 for node
in section
.childNodes
:
279 if node
.nodeType
!= Node
.ELEMENT_NODE
:
281 name
= node
.localName
282 if name
== 'section':
283 self
.build_section(node
, iter)
285 self
.build_widget(node
, page
)
288 def build_widget(self
, node
, box
):
289 """Dispatches the job of dealing with a DOM Node to the
290 appropriate build_* function."""
291 label
= node
.getAttribute('label')
292 name
= node
.getAttribute('name')
294 label
= self
._(label
)
299 option
= self
.options
.options
[name
]
301 raise Exception("Unknown option '%s'" % name
)
303 # Check for a new-style function in the registry...
304 new_fn
= widget_registry
.get(node
.localName
, None)
306 # Wrap it up so it appears old-style
307 fn
= lambda *args
: new_fn(self
, *args
)
309 # Not in the registry... look in the class instead
311 name
= node
.localName
.replace('-', '_')
312 fn
= getattr(self
, 'build_' + name
)
313 except AttributeError:
314 print "Unknown widget type '%s'" \
319 widgets
= fn(node
, label
, option
)
321 widgets
= fn(node
, label
)
323 box
.pack_start(w
, False, True, 0)
325 def may_add_tip(self
, widget
, node
):
326 """If 'node' contains any text, use that as the tip for 'widget'."""
328 data
= ''.join([n
.nodeValue
for n
in node
.childNodes
]).strip()
332 self
.tips
.set_tip(widget
, self
._(data
))
334 # Each type of widget has a method called 'build_NAME' where name is
335 # the XML element name. This method is called as method(node, label,
336 # option) if it corresponds to an Option, or method(node, label)
337 # otherwise. It should return a list of widgets to add to the window
338 # and, if it's for an Option, set self.handlers[option] = (get, set).
340 def build_label(self
, node
, label
):
341 """<label>Text</label>"""
342 return [g
.Label(self
._(data(node
)))]
344 def build_spacer(self
, node
, label
):
347 eb
.set_size_request(8, 8)
350 def build_hbox(self
, node
, label
):
351 """<hbox>...</hbox> to layout child widgets horizontally."""
352 return self
.do_box(node
, label
, g
.HBox(False, 4))
353 def build_vbox(self
, node
, label
):
354 """<vbox>...</vbox> to layout child widgets vertically."""
355 return self
.do_box(node
, label
, g
.VBox(False, 0))
357 def do_box(self
, node
, label
, widget
):
358 "Helper function for building hbox, vbox and frame widgets."
360 widget
.pack_start(g
.Label(label
), False, True, 4)
362 for child
in node
.childNodes
:
363 if child
.nodeType
== Node
.ELEMENT_NODE
:
364 self
.build_widget(child
, widget
)
368 def build_frame(self
, node
, label
):
369 """<frame label='Title'>...</frame> to group options under a heading."""
370 frame
= g
.Frame(label
)
371 frame
.set_shadow_type(g
.SHADOW_NONE
)
373 # Make the label bold...
374 # (bug in pygtk => use set_markup)
375 label_widget
= frame
.get_label_widget()
376 label_widget
.set_markup('<b>' + label
+ '</b>')
377 #attr = pango.AttrWeight(pango.WEIGHT_BOLD)
378 #attr.start_index = 0
380 #list = pango.AttrList()
382 #label_widget.set_attributes(list)
384 vbox
= g
.VBox(False, 4)
385 vbox
.set_border_width(12)
388 self
.do_box(node
, None, vbox
)
392 def do_entry(self
, node
, label
, option
):
393 "Helper function for entry and secretentry widgets"
394 box
= g
.HBox(False, 4)
398 label_wid
= g
.Label(label
)
399 label_wid
.set_alignment(1.0, 0.5)
400 box
.pack_start(label_wid
, False, True, 0)
401 box
.pack_start(entry
, True, True, 0)
405 self
.may_add_tip(entry
, node
)
407 entry
.connect('changed', lambda e
: self
.check_widget(option
))
410 return entry
.get_chars(0, -1)
412 entry
.set_text(option
.value
)
413 self
.handlers
[option
] = (get
, set)
415 return (entry
, [box
or entry
])
417 def build_entry(self
, node
, label
, option
):
418 "<entry name='...' label='...'>Tooltip</entry>"
419 entry
, result
=self
.do_entry(node
, label
, option
)
422 def build_secretentry(self
, node
, label
, option
):
423 "<secretentry name='...' label='...' char='*'>Tooltip</secretentry>"
424 entry
, result
=self
.do_entry(node
, label
, option
)
426 ch
=node
.getAttribute('char')
434 entry
.set_visibility(g
.FALSE
)
435 entry
.set_invisible_char(ch
)
439 def build_font(self
, node
, label
, option
):
440 "<font name='...' label='...'>Tooltip</font>"
441 button
= FontButton(self
, option
, label
)
443 self
.may_add_tip(button
, node
)
445 hbox
= g
.HBox(False, 4)
446 hbox
.pack_start(g
.Label(label
), False, True, 0)
447 hbox
.pack_start(button
, False, True, 0)
449 self
.handlers
[option
] = (button
.get
, button
.set)
453 def build_colour(self
, node
, label
, option
):
454 "<colour name='...' label='...'>Tooltip</colour>"
455 button
= ColourButton(self
, option
, label
)
457 self
.may_add_tip(button
, node
)
459 hbox
= g
.HBox(False, 4)
460 hbox
.pack_start(g
.Label(label
), False, True, 0)
461 hbox
.pack_start(button
, False, True, 0)
463 self
.handlers
[option
] = (button
.get
, button
.set)
467 def build_numentry(self
, node
, label
, option
):
468 """<numentry name='...' label='...' min='0' max='100' step='1'>Tooltip</numentry>.
469 Lets the user choose a number from min to max."""
470 minv
= int(node
.getAttribute('min'))
471 maxv
= int(node
.getAttribute('max'))
472 step
= node
.getAttribute('step')
477 unit
= self
._(node
.getAttribute('unit'))
479 hbox
= g
.HBox(False, 4)
481 widget
= g
.Label(label
)
482 widget
.set_alignment(1.0, 0.5)
483 hbox
.pack_start(widget
, False, True, 0)
485 spin
= g
.SpinButton(g
.Adjustment(minv
, minv
, maxv
, step
))
486 spin
.set_width_chars(max(len(str(minv
)), len(str(maxv
))))
487 hbox
.pack_start(spin
, False, True, 0)
488 self
.may_add_tip(spin
, node
)
491 hbox
.pack_start(g
.Label(unit
), False, True, 0)
493 self
.handlers
[option
] = (
494 lambda: str(spin
.get_value()),
495 lambda: spin
.set_value(option
.int_value
))
497 spin
.connect('value-changed', lambda w
: self
.check_widget(option
))
501 def build_menu(self
, node
, label
, option
):
502 """Build an OptionMenu widget, only one item of which may be selected.
503 <menu name='...' label='...'>
504 <item value='...' label='...'/>
505 <item value='...' label='...'/>
510 option_menu
= g
.OptionMenu()
512 option_menu
.set_menu(menu
)
515 box
= g
.HBox(False, 4)
516 label_wid
= g
.Label(label
)
517 label_wid
.set_alignment(1.0, 0.5)
518 box
.pack_start(label_wid
, False, True, 0)
519 box
.pack_start(option_menu
, True, True, 0)
523 #self.may_add_tip(option_menu, node)
525 for item
in node
.getElementsByTagName('item'):
526 value
= item
.getAttribute('value')
528 label_item
= item
.getAttribute('label') or value
530 menu
.append(g
.MenuItem(label_item
))
533 option_menu
.connect('changed', lambda e
: self
.check_widget(option
))
536 return values
[option_menu
.get_history()]
540 option_menu
.set_history(values
.index(option
.value
))
542 print "Value '%s' not in combo list" % option
.value
544 self
.handlers
[option
] = (get
, set)
546 return [box
or option_menu
]
549 def build_radio_group(self
, node
, label
, option
):
550 """Build a list of radio buttons, only one of which may be selected.
551 <radio-group name='...'>
552 <radio value='...' label='...'>Tooltip</radio>
553 <radio value='...' label='...'>Tooltip</radio>
558 for radio
in node
.getElementsByTagName('radio'):
559 label
= self
._(radio
.getAttribute('label'))
560 button
= g
.RadioButton(button
, label
)
561 self
.may_add_tip(button
, radio
)
562 radios
.append(button
)
563 values
.append(radio
.getAttribute('value'))
564 button
.connect('toggled', lambda b
: self
.check_widget(option
))
568 i
= values
.index(option
.value
)
570 print "Value '%s' not in radio group!" % option
.value
572 radios
[i
].set_active(True)
574 for r
, v
in zip(radios
, values
):
577 raise Exception('Nothing selected!')
579 self
.handlers
[option
] = (get
, set)
583 def build_toggle(self
, node
, label
, option
):
584 "<toggle name='...' label='...'>Tooltip</toggle>"
585 toggle
= g
.CheckButton(label
)
586 self
.may_add_tip(toggle
, node
)
588 self
.handlers
[option
] = (
589 lambda: str(toggle
.get_active()),
590 lambda: toggle
.set_active(option
.int_value
))
592 toggle
.connect('toggled', lambda w
: self
.check_widget(option
))
596 class FontButton(g
.Button
):
597 def __init__(self
, option_box
, option
, title
):
598 g
.Button
.__init
__(self
)
599 self
.option_box
= option_box
602 self
.label
= g
.Label('<font>')
605 self
.connect('clicked', self
.clicked
)
608 self
.label
.set_text(self
.option
.value
)
610 self
.dialog
.destroy()
613 return self
.label
.get()
615 def clicked(self
, button
):
617 self
.dialog
.destroy()
622 def response(dialog
, resp
):
623 if resp
!= g
.RESPONSE_OK
:
626 self
.label
.set_text(dialog
.get_font_name())
628 self
.option_box
.check_widget(self
.option
)
630 self
.dialog
= g
.FontSelectionDialog(self
.title
)
631 self
.dialog
.set_position(g
.WIN_POS_MOUSE
)
632 self
.dialog
.connect('destroy', closed
)
633 self
.dialog
.connect('response', response
)
635 self
.dialog
.set_font_name(self
.get())
638 class ColourButton(g
.Button
):
639 def __init__(self
, option_box
, option
, title
):
640 g
.Button
.__init
__(self
)
641 self
.option_box
= option_box
644 self
.set_size_request(64, 12)
646 self
.connect('clicked', self
.clicked
)
648 def set(self
, c
= None):
650 c
= g
.gdk
.color_parse(self
.option
.value
)
651 self
.modify_bg(g
.STATE_NORMAL
, c
)
652 self
.modify_bg(g
.STATE_PRELIGHT
, c
)
653 self
.modify_bg(g
.STATE_ACTIVE
, c
)
656 c
= self
.get_style().bg
[g
.STATE_NORMAL
]
657 return '#%04x%04x%04x' % (c
.red
, c
.green
, c
.blue
)
659 def clicked(self
, button
):
661 self
.dialog
.destroy()
666 def response(dialog
, resp
):
667 if resp
!= g
.RESPONSE_OK
:
670 self
.set(dialog
.colorsel
.get_current_color())
672 self
.option_box
.check_widget(self
.option
)
674 self
.dialog
= g
.ColorSelectionDialog(self
.title
)
675 self
.dialog
.set_position(g
.WIN_POS_MOUSE
)
676 self
.dialog
.connect('destroy', closed
)
677 self
.dialog
.connect('response', response
)
679 c
= self
.get_style().bg
[g
.STATE_NORMAL
]
680 self
.dialog
.colorsel
.set_current_color(c
)
683 # Add your own options here... (maps element localName to build function)