Several incompatible changes to the experimental proxy API to make it simpler
[rox-lib.git] / python / rox / OptionsBox.py
blob22aef4e9482716fc27e8e2c5c588ae89faa625c3
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
115 self.size_groups = {} # Name -> GtkSizeGroup
116 self.current_size_group = None
118 self.build_window_frame()
120 # Add each section
121 n = 0
122 for section in doc.documentElement.childNodes:
123 if section.nodeType != Node.ELEMENT_NODE:
124 continue
125 if section.localName != 'section':
126 print "Unknown section", section
127 continue
128 self.build_section(section, None)
129 n += 1
130 if n > 1:
131 self.tree_view.expand_all()
132 else:
133 self.sections_swin.hide()
135 self.updating = 0
137 def destroyed(widget):
138 rox.toplevel_unref()
139 if self.changed():
140 try:
141 self.options.save()
142 except:
143 rox.report_exception()
144 self.connect('destroy', destroyed)
146 def got_response(widget, response):
147 if response == g.RESPONSE_OK:
148 self.destroy()
149 elif response == REVERT:
150 for o in self.options:
151 o._set(self.revert[o])
152 self.update_widgets()
153 self.options.notify()
154 self.update_revert()
155 self.connect('response', got_response)
157 def open(self):
158 """Show the window, updating all the widgets at the same
159 time. Use this instead of show()."""
160 rox.toplevel_ref()
161 for option in self.options:
162 self.revert[option] = option.value
163 self.update_widgets()
164 self.update_revert()
165 self.show()
167 def update_revert(self):
168 "Shade/unshade the Revert button. Internal."
169 self.set_response_sensitive(REVERT, self.changed())
171 def changed(self):
172 """Check whether any options have different values (ie, whether Revert
173 will do anything)."""
174 for option in self.options:
175 if option.value != self.revert[option]:
176 return True
177 return False
179 def update_widgets(self):
180 "Make widgets show current values. Internal."
181 assert not self.updating
182 self.updating = 1
184 try:
185 for option in self.options:
186 try:
187 handler = self.handlers[option][1]
188 except KeyError:
189 print "No widget for option '%s'!" % option
190 else:
191 handler()
192 finally:
193 self.updating = 0
195 def build_window_frame(self):
196 "Create the main structure of the window."
197 hbox = g.HBox(False, 4)
198 self.vbox.pack_start(hbox, True, True, 0)
200 # scrolled window for the tree view
201 sw = g.ScrolledWindow()
202 sw.set_shadow_type(g.SHADOW_IN)
203 sw.set_policy(g.POLICY_NEVER, g.POLICY_AUTOMATIC)
204 hbox.pack_start(sw, False, True, 0)
205 self.sections_swin = sw # Used to hide it...
207 # tree view
208 model = g.TreeStore(gobject.TYPE_STRING, gobject.TYPE_INT)
209 tv = g.TreeView(model)
210 sel = tv.get_selection()
211 sel.set_mode(g.SELECTION_BROWSE)
212 tv.set_headers_visible(False)
213 self.sections = model
214 self.tree_view = tv
215 tv.unset_flags(g.CAN_FOCUS) # Stop irritating highlight
217 # Add a column to display column 0 of the store...
218 cell = g.CellRendererText()
219 column = g.TreeViewColumn('Section', cell, text = 0)
220 tv.append_column(column)
222 sw.add(tv)
224 # main options area
225 frame = g.Frame()
226 frame.set_shadow_type(g.SHADOW_IN)
227 hbox.pack_start(frame, True, True, 0)
229 notebook = g.Notebook()
230 notebook.set_show_tabs(False)
231 notebook.set_show_border(False)
232 frame.add(notebook)
233 self.notebook = notebook
235 # Flip pages
236 # (sel = sel; pygtk bug?)
237 def change_page(tv, sel = sel, notebook = notebook):
238 selected = sel.get_selected()
239 if not selected:
240 return
241 model, titer = selected
242 page = model.get_value(titer, 1)
244 notebook.set_current_page(page)
245 sel.connect('changed', change_page)
247 self.vbox.show_all()
249 def check_widget(self, option):
250 "A widgets call this when the user changes its value."
251 if self.updating:
252 return
254 assert isinstance(option, options.Option)
256 new = self.handlers[option][0]()
258 if new == option.value:
259 return
261 option._set(new)
262 self.options.notify()
263 self.update_revert()
265 def build_section(self, section, parent):
266 """Create a new page for the notebook and a new entry in the
267 sections tree, and build all the widgets inside the page."""
268 page = g.VBox(False, 4)
269 page.set_border_width(4)
270 self.notebook.append_page(page, g.Label('unused'))
272 titer = self.sections.append(parent)
273 self.sections.set(titer,
274 0, self._(section.getAttribute('title')),
275 1, self.notebook.page_num(page))
276 for node in section.childNodes:
277 if node.nodeType != Node.ELEMENT_NODE:
278 continue
279 name = node.localName
280 if name == 'section':
281 self.build_section(node, titer)
282 else:
283 self.build_widget(node, page)
284 page.show_all()
286 def build_widget(self, node, box):
287 """Dispatches the job of dealing with a DOM Node to the
288 appropriate build_* function."""
289 label = node.getAttribute('label')
290 name = node.getAttribute('name')
291 if label:
292 label = self._(label)
294 old_size_group = self.current_size_group
295 sg = node.getAttributeNode('size-group')
296 if sg is not None:
297 self.current_size_group = sg.value or None
299 option = None
300 if name:
301 try:
302 option = self.options.options[name]
303 except KeyError:
304 raise Exception("Unknown option '%s'" % name)
306 # Check for a new-style function in the registry...
307 new_fn = widget_registry.get(node.localName, None)
308 if new_fn:
309 # Wrap it up so it appears old-style
310 fn = lambda *args: new_fn(self, *args)
311 else:
312 # Not in the registry... look in the class instead
313 try:
314 name = node.localName.replace('-', '_')
315 fn = getattr(self, 'build_' + name)
316 except AttributeError:
317 fn = self.build_unknown
319 if option:
320 widgets = fn(node, label, option)
321 else:
322 widgets = fn(node, label)
323 for w in widgets:
324 box.pack_start(w, False, True, 0)
326 self.current_size_group = old_size_group
328 def may_add_tip(self, widget, node):
329 """If 'node' contains any text, use that as the tip for 'widget'."""
330 if node.childNodes:
331 data = ''.join([n.nodeValue for n in node.childNodes]).strip()
332 else:
333 data = None
334 if data:
335 self.tips.set_tip(widget, self._(data))
337 def get_size_group(self, name):
338 """Return the GtkSizeGroup for this name, creating one
339 if it doesn't currently exist."""
340 try:
341 return self.size_groups[name]
342 except KeyError:
343 group = g.SizeGroup(g.SIZE_GROUP_HORIZONTAL)
344 self.size_groups[name] = group
345 return group
347 def make_sized_label(self, label, suffix = ""):
348 """Create a GtkLabel and add it to the current size-group, if any"""
349 widget = g.Label(label)
350 if self.current_size_group:
351 widget.set_alignment(1.0, 0.5)
352 group = self.get_size_group(self.current_size_group + suffix)
353 group.add_widget(widget)
354 return widget
356 # Each type of widget has a method called 'build_NAME' where name is
357 # the XML element name. This method is called as method(node, label,
358 # option) if it corresponds to an Option, or method(node, label)
359 # otherwise. It should return a list of widgets to add to the window
360 # and, if it's for an Option, set self.handlers[option] = (get, set).
362 def build_unknown(self, node, label, option = None):
363 return [g.Label("Unknown widget type <%s>" % node.localName)]
365 def build_label(self, node, label):
366 help_flag = int(node.getAttribute('help') or '0')
367 widget = self.make_sized_label(self._(data(node)))
368 if help_flag:
369 widget.set_alignment(0, 0.5)
370 else:
371 widget.set_alignment(0, 1)
372 widget.set_justify(g.JUSTIFY_LEFT)
373 widget.set_line_wrap(True)
375 if help_flag:
376 hbox = g.HBox(False, 4)
377 image = g.Image()
378 image.set_from_stock(g.STOCK_DIALOG_INFO,
379 g.ICON_SIZE_BUTTON)
380 align = g.Alignment(0, 0, 0, 0)
382 align.add(image)
383 hbox.pack_start(align, False, True, 0)
384 hbox.pack_start(widget, False, True, 0)
386 spacer = g.EventBox()
387 spacer.set_size_request(6, 6)
389 return [hbox, spacer]
390 return [widget]
392 def build_spacer(self, node, label):
393 """<spacer/>"""
394 eb = g.EventBox()
395 eb.set_size_request(8, 8)
396 return [eb]
398 def build_hbox(self, node, label):
399 """<hbox>...</hbox> to layout child widgets horizontally."""
400 return self.do_box(node, label, g.HBox(False, 4))
401 def build_vbox(self, node, label):
402 """<vbox>...</vbox> to layout child widgets vertically."""
403 return self.do_box(node, label, g.VBox(False, 0))
405 def do_box(self, node, label, widget):
406 "Helper function for building hbox, vbox and frame widgets."
407 if label:
408 widget.pack_start(self.make_sized_label(label),
409 False, True, 4)
411 for child in node.childNodes:
412 if child.nodeType == Node.ELEMENT_NODE:
413 self.build_widget(child, widget)
415 return [widget]
417 def build_frame(self, node, label):
418 """<frame label='Title'>...</frame> to group options under a heading."""
419 frame = g.Frame(label)
420 frame.set_shadow_type(g.SHADOW_NONE)
422 # Make the label bold...
423 # (bug in pygtk => use set_markup)
424 label_widget = frame.get_label_widget()
425 label_widget.set_markup('<b>' + label + '</b>')
426 #attr = pango.AttrWeight(pango.WEIGHT_BOLD)
427 #attr.start_index = 0
428 #attr.end_index = -1
429 #list = pango.AttrList()
430 #list.insert(attr)
431 #label_widget.set_attributes(list)
433 vbox = g.VBox(False, 4)
434 vbox.set_border_width(12)
435 frame.add(vbox)
437 self.do_box(node, None, vbox)
439 return [frame]
441 def do_entry(self, node, label, option):
442 "Helper function for entry and secretentry widgets"
443 box = g.HBox(False, 4)
444 entry = g.Entry()
446 if label:
447 label_wid = self.make_sized_label(label)
448 label_wid.set_alignment(1.0, 0.5)
449 box.pack_start(label_wid, False, True, 0)
450 box.pack_start(entry, True, True, 0)
451 else:
452 box = None
454 self.may_add_tip(entry, node)
456 entry.connect('changed', lambda e: self.check_widget(option))
458 def get():
459 return entry.get_chars(0, -1)
460 def set():
461 entry.set_text(option.value)
462 self.handlers[option] = (get, set)
464 return (entry, [box or entry])
466 def build_entry(self, node, label, option):
467 "<entry name='...' label='...'>Tooltip</entry>"
468 entry, result=self.do_entry(node, label, option)
469 return result
471 def build_secretentry(self, node, label, option):
472 "<secretentry name='...' label='...' char='*'>Tooltip</secretentry>"
473 entry, result=self.do_entry(node, label, option)
474 try:
475 ch=node.getAttribute('char')
476 if len(ch)>=1:
477 ch=ch[0]
478 else:
479 ch=u'\0'
480 except:
481 ch='*'
483 entry.set_visibility(g.FALSE)
484 entry.set_invisible_char(ch)
486 return result
488 def build_font(self, node, label, option):
489 "<font name='...' label='...'>Tooltip</font>"
490 button = FontButton(self, option, label)
492 self.may_add_tip(button, node)
494 hbox = g.HBox(False, 4)
495 hbox.pack_start(self.make_sized_label(label), False, True, 0)
496 hbox.pack_start(button, False, True, 0)
498 self.handlers[option] = (button.get, button.set)
500 return [hbox]
502 def build_colour(self, node, label, option):
503 "<colour name='...' label='...'>Tooltip</colour>"
504 button = ColourButton(self, option, label)
506 self.may_add_tip(button, node)
508 hbox = g.HBox(False, 4)
509 hbox.pack_start(self.make_sized_label(label), False, True, 0)
510 hbox.pack_start(button, False, True, 0)
512 self.handlers[option] = (button.get, button.set)
514 return [hbox]
516 def build_numentry(self, node, label, option):
517 """<numentry name='...' label='...' min='0' max='100' step='1'>Tooltip</numentry>.
518 Lets the user choose a number from min to max."""
519 minv = int(node.getAttribute('min'))
520 maxv = int(node.getAttribute('max'))
521 step = node.getAttribute('step')
522 unit = node.getAttribute('unit')
523 if step:
524 step = int(step)
525 else:
526 step = 1
527 if unit:
528 unit = self._(unit)
530 hbox = g.HBox(False, 4)
531 if label:
532 widget = self.make_sized_label(label)
533 widget.set_alignment(1.0, 0.5)
534 hbox.pack_start(widget, False, True, 0)
536 spin = g.SpinButton(g.Adjustment(minv, minv, maxv, step))
537 spin.set_width_chars(max(len(str(minv)), len(str(maxv))))
538 hbox.pack_start(spin, False, True, 0)
539 self.may_add_tip(spin, node)
541 if unit:
542 hbox.pack_start(g.Label(unit), False, True, 0)
544 self.handlers[option] = (
545 lambda: str(spin.get_value()),
546 lambda: spin.set_value(option.int_value))
548 spin.connect('value-changed', lambda w: self.check_widget(option))
550 return [hbox]
552 def build_menu(self, node, label, option):
553 """Build an OptionMenu widget, only one item of which may be selected.
554 <menu name='...' label='...'>
555 <item value='...' label='...'/>
556 <item value='...' label='...'/>
557 </menu>"""
559 values = []
561 option_menu = g.OptionMenu()
562 menu = g.Menu()
563 option_menu.set_menu(menu)
565 if label:
566 box = g.HBox(False, 4)
567 label_wid = self.make_sized_label(label)
568 label_wid.set_alignment(1.0, 0.5)
569 box.pack_start(label_wid, False, True, 0)
570 box.pack_start(option_menu, True, True, 0)
571 else:
572 box = None
574 #self.may_add_tip(option_menu, node)
576 for item in node.getElementsByTagName('item'):
577 assert item.hasAttribute('value')
578 value = item.getAttribute('value')
579 label_item = self._(item.getAttribute('label')) or value
581 menu.append(g.MenuItem(label_item))
582 values.append(value)
584 option_menu.connect('changed', lambda e: self.check_widget(option))
586 def get():
587 return values[option_menu.get_history()]
589 def set():
590 try:
591 option_menu.set_history(values.index(option.value))
592 except ValueError:
593 print "Value '%s' not in combo list" % option.value
595 self.handlers[option] = (get, set)
597 return [box or option_menu]
600 def build_radio_group(self, node, label, option):
601 """Build a list of radio buttons, only one of which may be selected.
602 <radio-group name='...'>
603 <radio value='...' label='...'>Tooltip</radio>
604 <radio value='...' label='...'>Tooltip</radio>
605 </radio-group>"""
606 radios = []
607 values = []
608 button = None
609 for radio in node.getElementsByTagName('radio'):
610 label = self._(radio.getAttribute('label'))
611 button = g.RadioButton(button, label)
612 self.may_add_tip(button, radio)
613 radios.append(button)
614 values.append(radio.getAttribute('value'))
615 button.connect('toggled', lambda b: self.check_widget(option))
617 def set():
618 try:
619 i = values.index(option.value)
620 except:
621 print "Value '%s' not in radio group!" % option.value
622 i = 0
623 radios[i].set_active(True)
624 def get():
625 for r, v in zip(radios, values):
626 if r.get_active():
627 return v
628 raise Exception('Nothing selected!')
630 self.handlers[option] = (get, set)
632 return radios
634 def build_toggle(self, node, label, option):
635 "<toggle name='...' label='...'>Tooltip</toggle>"
636 toggle = g.CheckButton(label)
637 self.may_add_tip(toggle, node)
639 self.handlers[option] = (
640 lambda: str(toggle.get_active()),
641 lambda: toggle.set_active(option.int_value))
643 toggle.connect('toggled', lambda w: self.check_widget(option))
645 return [toggle]
647 def build_slider(self, node, label, option):
648 minv = int(node.getAttribute('min'))
649 maxv = int(node.getAttribute('max'))
650 fixed = int(node.getAttribute('fixed') or "0")
651 showvalue = int(node.getAttribute('showvalue') or "0")
652 end = node.getAttribute('end')
654 hbox = g.HBox(False, 4)
655 if label:
656 widget = self.make_sized_label(label)
657 hbox.pack_start(widget, False, True, 0)
659 if end:
660 hbox.pack_end(self.make_sized_label(_(end),
661 suffix = '-unit'),
662 False, True, 0)
664 adj = g.Adjustment(minv, minv, maxv, 1, 10, 0)
665 slide = g.HScale(adj)
667 if fixed:
668 slide.set_size_request(adj.upper, 24)
669 else:
670 slide.set_size_request(120, -1)
671 if showvalue:
672 slide.draw_value(True)
673 slide.set_value_pos(g.POS_LEFT)
674 slide.set_digits(0)
675 else:
676 slide.set_draw_value(False)
678 self.may_add_tip(slide, node)
679 hbox.pack_start(slide, not fixed, True, 0)
681 self.handlers[option] = (
682 lambda: str(adj.get_value()),
683 lambda: adj.set_value(option.int_value))
685 slide.connect('value-changed',
686 lambda w: self.check_widget(option))
688 return [hbox]
690 class FontButton(g.Button):
691 """A button that opens a GtkFontSelectionDialog"""
692 def __init__(self, option_box, option, title):
693 g.Button.__init__(self)
694 self.option_box = option_box
695 self.option = option
696 self.title = title
697 self.label = g.Label('<font>')
698 self.add(self.label)
699 self.dialog = None
700 self.connect('clicked', self.clicked)
702 def set(self):
703 self.label.set_text(self.option.value)
704 if self.dialog:
705 self.dialog.destroy()
707 def get(self):
708 return self.label.get()
710 def clicked(self, button):
711 if self.dialog:
712 self.dialog.destroy()
714 def closed(dialog):
715 self.dialog = None
717 def response(dialog, resp):
718 if resp != g.RESPONSE_OK:
719 dialog.destroy()
720 return
721 self.label.set_text(dialog.get_font_name())
722 dialog.destroy()
723 self.option_box.check_widget(self.option)
725 self.dialog = g.FontSelectionDialog(self.title)
726 self.dialog.set_position(g.WIN_POS_MOUSE)
727 self.dialog.connect('destroy', closed)
728 self.dialog.connect('response', response)
730 self.dialog.set_font_name(self.get())
731 self.dialog.show()
733 class ColourButton(g.Button):
734 """A button that opens a GtkColorSelectionDialog"""
735 def __init__(self, option_box, option, title):
736 g.Button.__init__(self)
737 self.c_box = g.EventBox()
738 self.add(self.c_box)
739 self.option_box = option_box
740 self.option = option
741 self.title = title
742 self.set_size_request(64, 14)
743 self.dialog = None
744 self.connect('clicked', self.clicked)
745 self.connect('expose-event', self.expose)
747 def expose(self, widget, event):
748 # Some themes draw images and stuff here, so we have to
749 # override it manually.
750 self.c_box.window.draw_rectangle(
751 self.c_box.style.bg_gc[g.STATE_NORMAL], True,
752 0, 0,
753 self.c_box.allocation.width,
754 self.c_box.allocation.height)
756 def set(self, c = None):
757 if c is None:
758 c = g.gdk.color_parse(self.option.value)
759 self.c_box.modify_bg(g.STATE_NORMAL, c)
761 def get(self):
762 c = self.c_box.get_style().bg[g.STATE_NORMAL]
763 return '#%04x%04x%04x' % (c.red, c.green, c.blue)
765 def clicked(self, button):
766 if self.dialog:
767 self.dialog.destroy()
769 def closed(dialog):
770 self.dialog = None
772 def response(dialog, resp):
773 if resp != g.RESPONSE_OK:
774 dialog.destroy()
775 return
776 self.set(dialog.colorsel.get_current_color())
777 dialog.destroy()
778 self.option_box.check_widget(self.option)
780 self.dialog = g.ColorSelectionDialog(self.title)
781 self.dialog.set_position(g.WIN_POS_MOUSE)
782 self.dialog.connect('destroy', closed)
783 self.dialog.connect('response', response)
785 c = self.c_box.get_style().bg[g.STATE_NORMAL]
786 self.dialog.colorsel.set_current_color(c)
787 self.dialog.show()
789 # Add your own options here... (maps element localName to build function)
790 widget_registry = {