New release.
[rox-lib/lack.git] / python / rox / OptionsBox.py
blob37281bf2fc23113411f64a6f29c61dc7ce840b16
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)
13 return [button]
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
23 value:
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))
35 return [toggle]
36 OptionsBox.widget_registry['mytoggle'] = build_toggle
37 """
39 from rox import g, options, _
40 import rox
41 from xml.dom import Node, minidom
42 import gobject
44 REVERT = 1
46 def data(node):
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
60 shown in the widget.
62 When the widget is modified, call self.check_widget(option) to
63 update the stored values.
64 """
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
72 used to create them.
74 Example:
76 <?xml version='1.0'?>
77 <options>
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.
82 </entry>
83 <section title='Nested section'>
84 ...
85 </section>
86 </section>
87 </options>
88 """
89 assert isinstance(options_group, options.OptionGroup)
91 if translation is None:
92 import __main__
93 if hasattr(__main__.__builtins__, '_'):
94 translation = __main__.__builtins__._
95 else:
96 translation = lambda x: x
97 self._ = translation
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()
122 # Add each section
123 n = 0
124 for section in doc.documentElement.childNodes:
125 if section.nodeType != Node.ELEMENT_NODE:
126 continue
127 if section.localName != 'section':
128 print "Unknown section", section
129 continue
130 self.build_section(section, None)
131 n += 1
132 if n > 1:
133 self.tree_view.expand_all()
134 else:
135 self.sections_swin.hide()
137 self.updating = 0
139 def destroyed(widget):
140 rox.toplevel_unref()
141 if self.changed():
142 try:
143 self.options.save()
144 except:
145 rox.report_exception()
146 self.connect('destroy', destroyed)
148 def got_response(widget, response):
149 if response == g.RESPONSE_OK:
150 self.destroy()
151 elif response == REVERT:
152 for o in self.options:
153 o._set(self.revert[o])
154 self.update_widgets()
155 self.options.notify()
156 self.update_revert()
157 self.connect('response', got_response)
159 def open(self):
160 """Show the window, updating all the widgets at the same
161 time. Use this instead of show()."""
162 rox.toplevel_ref()
163 for option in self.options:
164 self.revert[option] = option.value
165 self.update_widgets()
166 self.update_revert()
167 self.show()
169 def update_revert(self):
170 "Shade/unshade the Revert button. Internal."
171 self.set_response_sensitive(REVERT, self.changed())
173 def changed(self):
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]:
178 return True
179 return False
181 def update_widgets(self):
182 "Make widgets show current values. Internal."
183 assert not self.updating
184 self.updating = 1
186 try:
187 for option in self.options:
188 try:
189 handler = self.handlers[option][1]
190 except KeyError:
191 print "No widget for option '%s'!" % option
192 else:
193 handler()
194 finally:
195 self.updating = 0
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...
209 # tree view
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
216 self.tree_view = tv
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)
224 sw.add(tv)
226 # main options area
227 frame = g.Frame()
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)
234 frame.add(notebook)
235 self.notebook = notebook
237 # Flip pages
238 # (sel = sel; pygtk bug?)
239 def change_page(tv, sel = sel, notebook = notebook):
240 selected = sel.get_selected()
241 if not selected:
242 return
243 model, iter = selected
244 page = model.get_value(iter, 1)
246 notebook.set_current_page(page)
247 sel.connect('changed', change_page)
249 self.vbox.show_all()
251 def check_widget(self, option):
252 "A widgets call this when the user changes its value."
253 if self.updating:
254 return
256 assert isinstance(option, options.Option)
258 new = self.handlers[option][0]()
260 if new == option.value:
261 return
263 option._set(new)
264 self.options.notify()
265 self.update_revert()
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:
280 continue
281 name = node.localName
282 if name == 'section':
283 self.build_section(node, iter)
284 else:
285 self.build_widget(node, page)
286 page.show_all()
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')
293 if label:
294 label = self._(label)
296 option = None
297 if name:
298 try:
299 option = self.options.options[name]
300 except KeyError:
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)
305 if new_fn:
306 # Wrap it up so it appears old-style
307 fn = lambda *args: new_fn(self, *args)
308 else:
309 # Not in the registry... look in the class instead
310 try:
311 name = node.localName.replace('-', '_')
312 fn = getattr(self, 'build_' + name)
313 except AttributeError:
314 print "Unknown widget type '%s'" \
315 % node.localName
316 return
318 if option:
319 widgets = fn(node, label, option)
320 else:
321 widgets = fn(node, label)
322 for w in widgets:
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'."""
327 if node.childNodes:
328 data = ''.join([n.nodeValue for n in node.childNodes]).strip()
329 else:
330 data = None
331 if data:
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):
345 """<spacer/>"""
346 eb = g.EventBox()
347 eb.set_size_request(8, 8)
348 return [eb]
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."
359 if label:
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)
366 return [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
379 #attr.end_index = -1
380 #list = pango.AttrList()
381 #list.insert(attr)
382 #label_widget.set_attributes(list)
384 vbox = g.VBox(False, 4)
385 vbox.set_border_width(12)
386 frame.add(vbox)
388 self.do_box(node, None, vbox)
390 return [frame]
392 def do_entry(self, node, label, option):
393 "Helper function for entry and secretentry widgets"
394 box = g.HBox(False, 4)
395 entry = g.Entry()
397 if label:
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)
402 else:
403 box = None
405 self.may_add_tip(entry, node)
407 entry.connect('changed', lambda e: self.check_widget(option))
409 def get():
410 return entry.get_chars(0, -1)
411 def set():
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)
420 return result
422 def build_secretentry(self, node, label, option):
423 "<secretentry name='...' label='...' char='*'>Tooltip</secretentry>"
424 entry, result=self.do_entry(node, label, option)
425 try:
426 ch=node.getAttribute('char')
427 if len(ch)>=1:
428 ch=ch[0]
429 else:
430 ch=u'\0'
431 except:
432 ch='*'
434 entry.set_visibility(g.FALSE)
435 entry.set_invisible_char(ch)
437 return result
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)
451 return [hbox]
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)
465 return [hbox]
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')
473 if step:
474 step = int(step)
475 else:
476 step = 1
477 unit = self._(node.getAttribute('unit'))
479 hbox = g.HBox(False, 4)
480 if label:
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)
490 if unit:
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))
499 return [hbox]
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='...'/>
506 </menu>"""
508 values = []
510 option_menu = g.OptionMenu()
511 menu = g.Menu()
512 option_menu.set_menu(menu)
514 if label:
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)
520 else:
521 box = None
523 #self.may_add_tip(option_menu, node)
525 for item in node.getElementsByTagName('item'):
526 value = item.getAttribute('value')
527 assert value
528 label_item = item.getAttribute('label') or value
530 menu.append(g.MenuItem(label_item))
531 values.append(value)
533 option_menu.connect('changed', lambda e: self.check_widget(option))
535 def get():
536 return values[option_menu.get_history()]
538 def set():
539 try:
540 option_menu.set_history(values.index(option.value))
541 except ValueError:
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>
554 </radio-group>"""
555 radios = []
556 values = []
557 button = None
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))
566 def set():
567 try:
568 i = values.index(option.value)
569 except:
570 print "Value '%s' not in radio group!" % option.value
571 i = 0
572 radios[i].set_active(True)
573 def get():
574 for r, v in zip(radios, values):
575 if r.get_active():
576 return v
577 raise Exception('Nothing selected!')
579 self.handlers[option] = (get, set)
581 return radios
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))
594 return [toggle]
596 class FontButton(g.Button):
597 def __init__(self, option_box, option, title):
598 g.Button.__init__(self)
599 self.option_box = option_box
600 self.option = option
601 self.title = title
602 self.label = g.Label('<font>')
603 self.add(self.label)
604 self.dialog = None
605 self.connect('clicked', self.clicked)
607 def set(self):
608 self.label.set_text(self.option.value)
609 if self.dialog:
610 self.dialog.destroy()
612 def get(self):
613 return self.label.get()
615 def clicked(self, button):
616 if self.dialog:
617 self.dialog.destroy()
619 def closed(dialog):
620 self.dialog = None
622 def response(dialog, resp):
623 if resp != g.RESPONSE_OK:
624 dialog.destroy()
625 return
626 self.label.set_text(dialog.get_font_name())
627 dialog.destroy()
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())
636 self.dialog.show()
638 class ColourButton(g.Button):
639 def __init__(self, option_box, option, title):
640 g.Button.__init__(self)
641 self.option_box = option_box
642 self.option = option
643 self.title = title
644 self.set_size_request(64, 12)
645 self.dialog = None
646 self.connect('clicked', self.clicked)
648 def set(self, c = None):
649 if c is 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)
655 def get(self):
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):
660 if self.dialog:
661 self.dialog.destroy()
663 def closed(dialog):
664 self.dialog = None
666 def response(dialog, resp):
667 if resp != g.RESPONSE_OK:
668 dialog.destroy()
669 return
670 self.set(dialog.colorsel.get_current_color())
671 dialog.destroy()
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)
681 self.dialog.show()
683 # Add your own options here... (maps element localName to build function)
684 widget_registry = {