Added missing docstrings and made pychecker happier.
[rox-lib.git] / python / rox / OptionsBox.py
blob334826c58e54bd7046638951d71c29f423ac2105
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 """A dialog box which lets the user edit the options. The file
53 Options.xml specifies the layout of this box."""
55 tips = None # GtkTooltips
56 options = None # The OptionGroup we are editing
57 revert = None # Option -> old value
58 handlers = None # Option -> (get, set)
59 _ = None # Translation function (application's, not ROX-Lib's)
61 def __init__(self, options_group, options_xml, translation = None):
62 """options_xml is an XML file, usually <app_dir>/Options.xml,
63 which defines the layout of the OptionsBox.
65 It contains an <options> root element containing (nested)
66 <section> elements. Each <section> contains a number of widgets,
67 some of which correspond to options. The build_* functions are
68 used to create them.
70 Example:
72 <?xml version='1.0'?>
73 <options>
74 <section title='First section'>
75 <label>Here are some options</label>
76 <entry name='default_name' label='Default file name'>
77 When saving an untitled file, use this name as the default.
78 </entry>
79 <section title='Nested section'>
80 ...
81 </section>
82 </section>
83 </options>
84 """
85 assert isinstance(options_group, options.OptionGroup)
87 if translation is None:
88 import __main__
89 if hasattr(__main__.__builtins__, '_'):
90 translation = __main__.__builtins__._
91 else:
92 translation = lambda x: x
93 self._ = translation
95 g.Dialog.__init__(self)
96 self.tips = g.Tooltips()
97 self.set_has_separator(False)
99 self.options = options_group
100 self.set_title(('%s options') % options_group.program)
101 self.set_position(g.WIN_POS_CENTER)
103 button = rox.ButtonMixed(g.STOCK_UNDO, _('_Revert'))
104 self.add_action_widget(button, REVERT)
105 self.tips.set_tip(button, _('Restore all options to how they were '
106 'when the window was opened'))
108 self.add_button(g.STOCK_OK, g.RESPONSE_OK)
110 doc = minidom.parse(options_xml)
111 assert doc.documentElement.localName == 'options'
113 self.handlers = {} # Option -> (get, set)
114 self.revert = {} # Option -> old value
116 self.build_window_frame()
118 # Add each section
119 n = 0
120 for section in doc.documentElement.childNodes:
121 if section.nodeType != Node.ELEMENT_NODE:
122 continue
123 if section.localName != 'section':
124 print "Unknown section", section
125 continue
126 self.build_section(section, None)
127 n += 1
128 if n > 1:
129 self.tree_view.expand_all()
130 else:
131 self.sections_swin.hide()
133 self.updating = 0
135 def destroyed(widget):
136 rox.toplevel_unref()
137 if self.changed():
138 try:
139 self.options.save()
140 except:
141 rox.report_exception()
142 self.connect('destroy', destroyed)
144 def got_response(widget, response):
145 if response == g.RESPONSE_OK:
146 self.destroy()
147 elif response == REVERT:
148 for o in self.options:
149 o._set(self.revert[o])
150 self.update_widgets()
151 self.options.notify()
152 self.update_revert()
153 self.connect('response', got_response)
155 def open(self):
156 """Show the window, updating all the widgets at the same
157 time. Use this instead of show()."""
158 rox.toplevel_ref()
159 for option in self.options:
160 self.revert[option] = option.value
161 self.update_widgets()
162 self.update_revert()
163 self.show()
165 def update_revert(self):
166 "Shade/unshade the Revert button. Internal."
167 self.set_response_sensitive(REVERT, self.changed())
169 def changed(self):
170 """Check whether any options have different values (ie, whether Revert
171 will do anything)."""
172 for option in self.options:
173 if option.value != self.revert[option]:
174 return True
175 return False
177 def update_widgets(self):
178 "Make widgets show current values. Internal."
179 assert not self.updating
180 self.updating = 1
182 try:
183 for option in self.options:
184 try:
185 handler = self.handlers[option][1]
186 except KeyError:
187 print "No widget for option '%s'!" % option
188 else:
189 handler()
190 finally:
191 self.updating = 0
193 def build_window_frame(self):
194 "Create the main structure of the window."
195 hbox = g.HBox(False, 4)
196 self.vbox.pack_start(hbox, True, True, 0)
198 # scrolled window for the tree view
199 sw = g.ScrolledWindow()
200 sw.set_shadow_type(g.SHADOW_IN)
201 sw.set_policy(g.POLICY_NEVER, g.POLICY_AUTOMATIC)
202 hbox.pack_start(sw, False, True, 0)
203 self.sections_swin = sw # Used to hide it...
205 # tree view
206 model = g.TreeStore(gobject.TYPE_STRING, gobject.TYPE_INT)
207 tv = g.TreeView(model)
208 sel = tv.get_selection()
209 sel.set_mode(g.SELECTION_BROWSE)
210 tv.set_headers_visible(False)
211 self.sections = model
212 self.tree_view = tv
213 tv.unset_flags(g.CAN_FOCUS) # Stop irritating highlight
215 # Add a column to display column 0 of the store...
216 cell = g.CellRendererText()
217 column = g.TreeViewColumn('Section', cell, text = 0)
218 tv.append_column(column)
220 sw.add(tv)
222 # main options area
223 frame = g.Frame()
224 frame.set_shadow_type(g.SHADOW_IN)
225 hbox.pack_start(frame, True, True, 0)
227 notebook = g.Notebook()
228 notebook.set_show_tabs(False)
229 notebook.set_show_border(False)
230 frame.add(notebook)
231 self.notebook = notebook
233 # Flip pages
234 # (sel = sel; pygtk bug?)
235 def change_page(tv, sel = sel, notebook = notebook):
236 selected = sel.get_selected()
237 if not selected:
238 return
239 model, titer = selected
240 page = model.get_value(titer, 1)
242 notebook.set_current_page(page)
243 sel.connect('changed', change_page)
245 self.vbox.show_all()
247 def check_widget(self, option):
248 "A widgets call this when the user changes its value."
249 if self.updating:
250 return
252 assert isinstance(option, options.Option)
254 new = self.handlers[option][0]()
256 if new == option.value:
257 return
259 option._set(new)
260 self.options.notify()
261 self.update_revert()
263 def build_section(self, section, parent):
264 """Create a new page for the notebook and a new entry in the
265 sections tree, and build all the widgets inside the page."""
266 page = g.VBox(False, 4)
267 page.set_border_width(4)
268 self.notebook.append_page(page, g.Label('unused'))
270 titer = self.sections.append(parent)
271 self.sections.set(titer,
272 0, self._(section.getAttribute('title')),
273 1, self.notebook.page_num(page))
274 for node in section.childNodes:
275 if node.nodeType != Node.ELEMENT_NODE:
276 continue
277 name = node.localName
278 if name == 'section':
279 self.build_section(node, titer)
280 else:
281 self.build_widget(node, page)
282 page.show_all()
284 def build_widget(self, node, box):
285 """Dispatches the job of dealing with a DOM Node to the
286 appropriate build_* function."""
287 label = node.getAttribute('label')
288 name = node.getAttribute('name')
289 if label:
290 label = self._(label)
292 option = None
293 if name:
294 try:
295 option = self.options.options[name]
296 except KeyError:
297 raise Exception("Unknown option '%s'" % name)
299 # Check for a new-style function in the registry...
300 new_fn = widget_registry.get(node.localName, None)
301 if new_fn:
302 # Wrap it up so it appears old-style
303 fn = lambda *args: new_fn(self, *args)
304 else:
305 # Not in the registry... look in the class instead
306 try:
307 name = node.localName.replace('-', '_')
308 fn = getattr(self, 'build_' + name)
309 except AttributeError:
310 print "Unknown widget type '%s'" \
311 % node.localName
312 return
314 if option:
315 widgets = fn(node, label, option)
316 else:
317 widgets = fn(node, label)
318 for w in widgets:
319 box.pack_start(w, False, True, 0)
321 def may_add_tip(self, widget, node):
322 """If 'node' contains any text, use that as the tip for 'widget'."""
323 if node.childNodes:
324 data = ''.join([n.nodeValue for n in node.childNodes]).strip()
325 else:
326 data = None
327 if data:
328 self.tips.set_tip(widget, self._(data))
330 # Each type of widget has a method called 'build_NAME' where name is
331 # the XML element name. This method is called as method(node, label,
332 # option) if it corresponds to an Option, or method(node, label)
333 # otherwise. It should return a list of widgets to add to the window
334 # and, if it's for an Option, set self.handlers[option] = (get, set).
336 def build_label(self, node, label):
337 widget = g.Label(self._(data(node)))
338 help_flag = int(node.getAttribute('help') or '0')
339 if help_flag:
340 widget.set_alignment(0, 0.5)
341 else:
342 widget.set_alignment(0, 1)
343 widget.set_justify(g.JUSTIFY_LEFT)
344 widget.set_line_wrap(True)
346 if help_flag:
347 hbox = g.HBox(False, 4)
348 image = g.Image()
349 image.set_from_stock(g.STOCK_DIALOG_INFO,
350 g.ICON_SIZE_BUTTON)
351 align = g.Alignment(0, 0, 0, 0)
353 align.add(image)
354 hbox.pack_start(align, False, True, 0)
355 hbox.pack_start(widget, False, True, 0)
357 spacer = g.EventBox()
358 spacer.set_size_request(6, 6)
360 return [hbox, spacer]
361 return [widget]
363 def build_spacer(self, node, label):
364 """<spacer/>"""
365 eb = g.EventBox()
366 eb.set_size_request(8, 8)
367 return [eb]
369 def build_hbox(self, node, label):
370 """<hbox>...</hbox> to layout child widgets horizontally."""
371 return self.do_box(node, label, g.HBox(False, 4))
372 def build_vbox(self, node, label):
373 """<vbox>...</vbox> to layout child widgets vertically."""
374 return self.do_box(node, label, g.VBox(False, 0))
376 def do_box(self, node, label, widget):
377 "Helper function for building hbox, vbox and frame widgets."
378 if label:
379 widget.pack_start(g.Label(label), False, True, 4)
381 for child in node.childNodes:
382 if child.nodeType == Node.ELEMENT_NODE:
383 self.build_widget(child, widget)
385 return [widget]
387 def build_frame(self, node, label):
388 """<frame label='Title'>...</frame> to group options under a heading."""
389 frame = g.Frame(label)
390 frame.set_shadow_type(g.SHADOW_NONE)
392 # Make the label bold...
393 # (bug in pygtk => use set_markup)
394 label_widget = frame.get_label_widget()
395 label_widget.set_markup('<b>' + label + '</b>')
396 #attr = pango.AttrWeight(pango.WEIGHT_BOLD)
397 #attr.start_index = 0
398 #attr.end_index = -1
399 #list = pango.AttrList()
400 #list.insert(attr)
401 #label_widget.set_attributes(list)
403 vbox = g.VBox(False, 4)
404 vbox.set_border_width(12)
405 frame.add(vbox)
407 self.do_box(node, None, vbox)
409 return [frame]
411 def do_entry(self, node, label, option):
412 "Helper function for entry and secretentry widgets"
413 box = g.HBox(False, 4)
414 entry = g.Entry()
416 if label:
417 label_wid = g.Label(label)
418 label_wid.set_alignment(1.0, 0.5)
419 box.pack_start(label_wid, False, True, 0)
420 box.pack_start(entry, True, True, 0)
421 else:
422 box = None
424 self.may_add_tip(entry, node)
426 entry.connect('changed', lambda e: self.check_widget(option))
428 def get():
429 return entry.get_chars(0, -1)
430 def set():
431 entry.set_text(option.value)
432 self.handlers[option] = (get, set)
434 return (entry, [box or entry])
436 def build_entry(self, node, label, option):
437 "<entry name='...' label='...'>Tooltip</entry>"
438 entry, result=self.do_entry(node, label, option)
439 return result
441 def build_secretentry(self, node, label, option):
442 "<secretentry name='...' label='...' char='*'>Tooltip</secretentry>"
443 entry, result=self.do_entry(node, label, option)
444 try:
445 ch=node.getAttribute('char')
446 if len(ch)>=1:
447 ch=ch[0]
448 else:
449 ch=u'\0'
450 except:
451 ch='*'
453 entry.set_visibility(g.FALSE)
454 entry.set_invisible_char(ch)
456 return result
458 def build_font(self, node, label, option):
459 "<font name='...' label='...'>Tooltip</font>"
460 button = FontButton(self, option, label)
462 self.may_add_tip(button, node)
464 hbox = g.HBox(False, 4)
465 hbox.pack_start(g.Label(label), False, True, 0)
466 hbox.pack_start(button, False, True, 0)
468 self.handlers[option] = (button.get, button.set)
470 return [hbox]
472 def build_colour(self, node, label, option):
473 "<colour name='...' label='...'>Tooltip</colour>"
474 button = ColourButton(self, option, label)
476 self.may_add_tip(button, node)
478 hbox = g.HBox(False, 4)
479 hbox.pack_start(g.Label(label), False, True, 0)
480 hbox.pack_start(button, False, True, 0)
482 self.handlers[option] = (button.get, button.set)
484 return [hbox]
486 def build_numentry(self, node, label, option):
487 """<numentry name='...' label='...' min='0' max='100' step='1'>Tooltip</numentry>.
488 Lets the user choose a number from min to max."""
489 minv = int(node.getAttribute('min'))
490 maxv = int(node.getAttribute('max'))
491 step = node.getAttribute('step')
492 if step:
493 step = int(step)
494 else:
495 step = 1
496 unit = self._(node.getAttribute('unit'))
498 hbox = g.HBox(False, 4)
499 if label:
500 widget = g.Label(label)
501 widget.set_alignment(1.0, 0.5)
502 hbox.pack_start(widget, False, True, 0)
504 spin = g.SpinButton(g.Adjustment(minv, minv, maxv, step))
505 spin.set_width_chars(max(len(str(minv)), len(str(maxv))))
506 hbox.pack_start(spin, False, True, 0)
507 self.may_add_tip(spin, node)
509 if unit:
510 hbox.pack_start(g.Label(unit), False, True, 0)
512 self.handlers[option] = (
513 lambda: str(spin.get_value()),
514 lambda: spin.set_value(option.int_value))
516 spin.connect('value-changed', lambda w: self.check_widget(option))
518 return [hbox]
520 def build_menu(self, node, label, option):
521 """Build an OptionMenu widget, only one item of which may be selected.
522 <menu name='...' label='...'>
523 <item value='...' label='...'/>
524 <item value='...' label='...'/>
525 </menu>"""
527 values = []
529 option_menu = g.OptionMenu()
530 menu = g.Menu()
531 option_menu.set_menu(menu)
533 if label:
534 box = g.HBox(False, 4)
535 label_wid = g.Label(label)
536 label_wid.set_alignment(1.0, 0.5)
537 box.pack_start(label_wid, False, True, 0)
538 box.pack_start(option_menu, True, True, 0)
539 else:
540 box = None
542 #self.may_add_tip(option_menu, node)
544 for item in node.getElementsByTagName('item'):
545 value = item.getAttribute('value')
546 assert value
547 label_item = item.getAttribute('label') or value
549 menu.append(g.MenuItem(label_item))
550 values.append(value)
552 option_menu.connect('changed', lambda e: self.check_widget(option))
554 def get():
555 return values[option_menu.get_history()]
557 def set():
558 try:
559 option_menu.set_history(values.index(option.value))
560 except ValueError:
561 print "Value '%s' not in combo list" % option.value
563 self.handlers[option] = (get, set)
565 return [box or option_menu]
568 def build_radio_group(self, node, label, option):
569 """Build a list of radio buttons, only one of which may be selected.
570 <radio-group name='...'>
571 <radio value='...' label='...'>Tooltip</radio>
572 <radio value='...' label='...'>Tooltip</radio>
573 </radio-group>"""
574 radios = []
575 values = []
576 button = None
577 for radio in node.getElementsByTagName('radio'):
578 label = self._(radio.getAttribute('label'))
579 button = g.RadioButton(button, label)
580 self.may_add_tip(button, radio)
581 radios.append(button)
582 values.append(radio.getAttribute('value'))
583 button.connect('toggled', lambda b: self.check_widget(option))
585 def set():
586 try:
587 i = values.index(option.value)
588 except:
589 print "Value '%s' not in radio group!" % option.value
590 i = 0
591 radios[i].set_active(True)
592 def get():
593 for r, v in zip(radios, values):
594 if r.get_active():
595 return v
596 raise Exception('Nothing selected!')
598 self.handlers[option] = (get, set)
600 return radios
602 def build_toggle(self, node, label, option):
603 "<toggle name='...' label='...'>Tooltip</toggle>"
604 toggle = g.CheckButton(label)
605 self.may_add_tip(toggle, node)
607 self.handlers[option] = (
608 lambda: str(toggle.get_active()),
609 lambda: toggle.set_active(option.int_value))
611 toggle.connect('toggled', lambda w: self.check_widget(option))
613 return [toggle]
615 class FontButton(g.Button):
616 """A button that opens a GtkFontSelectionDialog"""
617 def __init__(self, option_box, option, title):
618 g.Button.__init__(self)
619 self.option_box = option_box
620 self.option = option
621 self.title = title
622 self.label = g.Label('<font>')
623 self.add(self.label)
624 self.dialog = None
625 self.connect('clicked', self.clicked)
627 def set(self):
628 self.label.set_text(self.option.value)
629 if self.dialog:
630 self.dialog.destroy()
632 def get(self):
633 return self.label.get()
635 def clicked(self, button):
636 if self.dialog:
637 self.dialog.destroy()
639 def closed(dialog):
640 self.dialog = None
642 def response(dialog, resp):
643 if resp != g.RESPONSE_OK:
644 dialog.destroy()
645 return
646 self.label.set_text(dialog.get_font_name())
647 dialog.destroy()
648 self.option_box.check_widget(self.option)
650 self.dialog = g.FontSelectionDialog(self.title)
651 self.dialog.set_position(g.WIN_POS_MOUSE)
652 self.dialog.connect('destroy', closed)
653 self.dialog.connect('response', response)
655 self.dialog.set_font_name(self.get())
656 self.dialog.show()
658 class ColourButton(g.Button):
659 """A button that opens a GtkColorSelectionDialog"""
660 def __init__(self, option_box, option, title):
661 g.Button.__init__(self)
662 self.c_box = g.EventBox()
663 self.add(self.c_box)
664 self.option_box = option_box
665 self.option = option
666 self.title = title
667 self.set_size_request(64, 14)
668 self.dialog = None
669 self.connect('clicked', self.clicked)
670 self.connect('expose-event', self.expose)
672 def expose(self, widget, event):
673 # Some themes draw images and stuff here, so we have to
674 # override it manually.
675 self.c_box.window.draw_rectangle(
676 self.c_box.style.bg_gc[g.STATE_NORMAL], True,
677 0, 0,
678 self.c_box.allocation.width,
679 self.c_box.allocation.height)
681 def set(self, c = None):
682 if c is None:
683 c = g.gdk.color_parse(self.option.value)
684 self.c_box.modify_bg(g.STATE_NORMAL, c)
686 def get(self):
687 c = self.c_box.get_style().bg[g.STATE_NORMAL]
688 return '#%04x%04x%04x' % (c.red, c.green, c.blue)
690 def clicked(self, button):
691 if self.dialog:
692 self.dialog.destroy()
694 def closed(dialog):
695 self.dialog = None
697 def response(dialog, resp):
698 if resp != g.RESPONSE_OK:
699 dialog.destroy()
700 return
701 self.set(dialog.colorsel.get_current_color())
702 dialog.destroy()
703 self.option_box.check_widget(self.option)
705 self.dialog = g.ColorSelectionDialog(self.title)
706 self.dialog.set_position(g.WIN_POS_MOUSE)
707 self.dialog.connect('destroy', closed)
708 self.dialog.connect('response', response)
710 c = self.get_style().bg[g.STATE_NORMAL]
711 self.dialog.colorsel.set_current_color(c)
712 self.dialog.show()
714 # Add your own options here... (maps element localName to build function)
715 widget_registry = {