Changed 'Dismiss' to 'Close' (Chris Shaffer).
[rox-lib.git] / python / rox / OptionsBox.py
blob058160711edec3617a59a893f073da7175fe736c
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 sub-class OptionsBox to provide new types of
6 option widget.
7 """
9 from rox import g, options, _
10 import rox
11 from xml.dom import Node, minidom
12 import gobject
14 REVERT = 1
16 def data(node):
17 """Return all the text directly inside this DOM Node."""
18 return ''.join([text.nodeValue for text in node.childNodes
19 if text.nodeType == Node.TEXT_NODE])
21 class OptionsBox(g.Dialog):
22 """OptionsBox can be sub-classed to provide your own widgets, by
23 creating extra build_* functions. Each build funtion takes a DOM
24 Element from the <app_dir>/Options.xml file and returns a list of
25 GtkWidgets to add to the box. The function should be named after
26 the element (<foo> -> def build_foo()).
28 When creating the widget, self.handlers[option] should be set to
29 a pair of functions (get, set) called to get and set the value
30 shown in the widget.
32 When the widget is modified, call self.check_widget(option) to
33 update the stored values.
34 """
35 def __init__(self, options_group, options_xml):
36 """options_xml is an XML file, usually <app_dir>/Options.xml,
37 which defines the layout of the OptionsBox.
39 It contains an <options> root element containing (nested)
40 <section> elements. Each <section> contains a number of widgets,
41 some of which correspond to options. The build_* functions are
42 used to create them.
44 Example:
46 <?xml version='1.0'?>
47 <options>
48 <section title='First section'>
49 <label>Here are some options</label>
50 <entry name='default_name' label='Default file name'>
51 When saving an untitled file, use this name as the default.
52 </entry>
53 <section title='Nested section'>
54 ...
55 </section>
56 </section>
57 </options>
58 """
59 assert isinstance(options_group, options.OptionGroup)
61 g.Dialog.__init__(self)
62 self.tips = g.Tooltips()
63 self.set_has_separator(False)
65 self.options = options_group
66 self.set_title(('%s options') % options_group.program)
67 self.set_position(g.WIN_POS_CENTER)
69 button = rox.ButtonMixed(g.STOCK_UNDO, _('_Revert'))
70 self.add_action_widget(button, REVERT)
71 self.tips.set_tip(button, _('Restore all options to how they were '
72 'when the window was opened'))
74 self.add_button(g.STOCK_OK, g.RESPONSE_OK)
76 doc = minidom.parse(options_xml)
77 assert doc.documentElement.localName == 'options'
79 self.handlers = {} # Option -> (get, set)
80 self.revert = {} # Option -> old value
82 self.build_window_frame()
84 # Add each section
85 n = 0
86 for section in doc.documentElement.childNodes:
87 if section.nodeType != Node.ELEMENT_NODE:
88 continue
89 if section.localName != 'section':
90 print "Unknown section", section
91 continue
92 self.build_section(section, None)
93 n += 1
94 if n > 1:
95 self.tree_view.expand_all()
96 else:
97 self.sections_swin.hide()
99 self.updating = 0
101 def destroyed(widget):
102 rox.toplevel_unref()
103 if self.changed():
104 try:
105 self.options.save()
106 except:
107 rox.report_exception()
108 self.connect('destroy', destroyed)
110 def got_response(widget, response):
111 if response == g.RESPONSE_OK:
112 self.destroy()
113 elif response == REVERT:
114 for o in self.options:
115 o._set(self.revert[o])
116 self.update_widgets()
117 self.options.notify()
118 self.update_revert()
119 self.connect('response', got_response)
121 def open(self):
122 """Show the window, updating all the widgets at the same
123 time. Use this instead of show()."""
124 rox.toplevel_ref()
125 for option in self.options:
126 self.revert[option] = option.value
127 self.update_widgets()
128 self.update_revert()
129 self.show()
131 def update_revert(self):
132 "Shade/unshade the Revert button. Internal."
133 self.set_response_sensitive(REVERT, self.changed())
135 def changed(self):
136 """Check whether any options have different values (ie, whether Revert
137 will do anything)."""
138 for option in self.options:
139 if option.value != self.revert[option]:
140 return True
141 return False
143 def update_widgets(self):
144 "Make widgets show current values. Internal."
145 assert not self.updating
146 self.updating = 1
148 try:
149 for option in self.options:
150 try:
151 handler = self.handlers[option][1]
152 except KeyError:
153 print "No widget for option '%s'!" % option
154 else:
155 handler()
156 finally:
157 self.updating = 0
159 def build_window_frame(self):
160 "Create the main structure of the window."
161 hbox = g.HBox(False, 4)
162 self.vbox.pack_start(hbox, True, True, 0)
164 # scrolled window for the tree view
165 sw = g.ScrolledWindow()
166 sw.set_shadow_type(g.SHADOW_IN)
167 sw.set_policy(g.POLICY_NEVER, g.POLICY_AUTOMATIC)
168 hbox.pack_start(sw, False, True, 0)
169 self.sections_swin = sw # Used to hide it...
171 # tree view
172 model = g.TreeStore(gobject.TYPE_STRING, gobject.TYPE_INT)
173 tv = g.TreeView(model)
174 sel = tv.get_selection()
175 sel.set_mode(g.SELECTION_BROWSE)
176 tv.set_headers_visible(False)
177 self.sections = model
178 self.tree_view = tv
179 tv.unset_flags(g.CAN_FOCUS) # Stop irritating highlight
181 # Add a column to display column 0 of the store...
182 cell = g.CellRendererText()
183 column = g.TreeViewColumn('Section', cell, text = 0)
184 tv.append_column(column)
186 sw.add(tv)
188 # main options area
189 frame = g.Frame()
190 frame.set_shadow_type(g.SHADOW_IN)
191 hbox.pack_start(frame, True, True, 0)
193 notebook = g.Notebook()
194 notebook.set_show_tabs(False)
195 notebook.set_show_border(False)
196 frame.add(notebook)
197 self.notebook = notebook
199 # Flip pages
200 # (sel = sel; pygtk bug?)
201 def change_page(tv, sel = sel, notebook = notebook):
202 selected = sel.get_selected()
203 if not selected:
204 return
205 model, iter = selected
206 page = model.get_value(iter, 1)
208 notebook.set_current_page(page)
209 sel.connect('changed', change_page)
211 self.vbox.show_all()
213 def check_widget(self, option):
214 "A widgets call this when the user changes its value."
215 if self.updating:
216 return
218 assert isinstance(option, options.Option)
220 new = self.handlers[option][0]()
222 if new == option.value:
223 return
225 option._set(new)
226 self.options.notify()
227 self.update_revert()
229 def build_section(self, section, parent):
230 """Create a new page for the notebook and a new entry in the
231 sections tree, and build all the widgets inside the page."""
232 page = g.VBox(False, 4)
233 page.set_border_width(4)
234 self.notebook.append_page(page, g.Label('unused'))
236 iter = self.sections.append(parent)
237 self.sections.set(iter, 0, section.getAttribute('title'),
238 1, self.notebook.page_num(page))
239 for node in section.childNodes:
240 if node.nodeType != Node.ELEMENT_NODE:
241 continue
242 name = node.localName
243 if name == 'section':
244 self.build_section(node, iter)
245 else:
246 self.build_widget(node, page)
247 page.show_all()
249 def build_widget(self, node, box):
250 """Dispatches the job of dealing with a DOM Node to the
251 appropriate build_* function."""
252 label = node.getAttribute('label')
253 name = node.getAttribute('name')
255 option = None
256 if name:
257 try:
258 option = self.options.options[name]
259 except KeyError:
260 raise Exception("Unknown option '%s'" % name)
262 try:
263 name = node.localName.replace('-', '_')
264 fn = getattr(self, 'build_' + name)
265 except AttributeError:
266 print "Unknown widget type '%s'" % node.localName
267 else:
268 if option:
269 widgets = fn(node, label, option)
270 else:
271 widgets = fn(node, label)
272 for w in widgets:
273 box.pack_start(w, False, True, 0)
275 def may_add_tip(self, widget, node):
276 """If 'node' contains any text, use that as the tip for 'widget'."""
277 if node.childNodes:
278 data = ''.join([n.nodeValue for n in node.childNodes]).strip()
279 else:
280 data = None
281 if data:
282 self.tips.set_tip(widget, data)
284 # Each type of widget has a method called 'build_NAME' where name is
285 # the XML element name. This method is called as method(node, label,
286 # option) if it corresponds to an Option, or method(node, label)
287 # otherwise. It should return a list of widgets to add to the window
288 # and, if it's for an Option, set self.handlers[option] = (get, set).
290 def build_label(self, node, label):
291 """<label>Text</label>"""
292 return [g.Label(data(node))]
294 def build_spacer(self, node, label):
295 """<spacer/>"""
296 eb = g.EventBox()
297 eb.set_size_request(8, 8)
298 return [eb]
300 def build_hbox(self, node, label):
301 """<hbox>...</hbox> to layout child widgets horizontally."""
302 return self.do_box(node, label, g.HBox(False, 4))
303 def build_vbox(self, node, label):
304 """<vbox>...</vbox> to layout child widgets vertically."""
305 return self.do_box(node, label, g.VBox(False, 0))
307 def do_box(self, node, label, widget):
308 "Helper function for building hbox, vbox and frame widgets."
309 if label:
310 widget.pack_start(g.Label(label), False, True, 4)
312 for child in node.childNodes:
313 if child.nodeType == Node.ELEMENT_NODE:
314 self.build_widget(child, widget)
316 return [widget]
318 def build_frame(self, node, label):
319 """<frame label='Title'>...</frame> to group options under a heading."""
320 frame = g.Frame(label)
321 frame.set_shadow_type(g.SHADOW_NONE)
323 # Make the label bold...
324 # (bug in pygtk => use set_markup)
325 label_widget = frame.get_label_widget()
326 label_widget.set_markup('<b>' + label + '</b>')
327 #attr = pango.AttrWeight(pango.WEIGHT_BOLD)
328 #attr.start_index = 0
329 #attr.end_index = -1
330 #list = pango.AttrList()
331 #list.insert(attr)
332 #label_widget.set_attributes(list)
334 vbox = g.VBox(False, 4)
335 vbox.set_border_width(12)
336 frame.add(vbox)
338 self.do_box(node, None, vbox)
340 return [frame]
342 def build_entry(self, node, label, option):
343 "<entry name='...' label='...'>Tooltip</entry>"
344 box = g.HBox(False, 4)
345 entry = g.Entry()
347 if label:
348 label_wid = g.Label(label)
349 label_wid.set_alignment(1.0, 0.5)
350 box.pack_start(label_wid, False, True, 0)
351 box.pack_start(entry, True, True, 0)
352 else:
353 box = None
355 self.may_add_tip(entry, node)
357 entry.connect('changed', lambda e: self.check_widget(option))
359 def get():
360 return entry.get_chars(0, -1)
361 def set():
362 entry.set_text(option.value)
363 self.handlers[option] = (get, set)
365 return [box or entry]
367 def build_font(self, node, label, option):
368 "<font name='...' label='...'>Tooltip</font>"
369 button = FontButton(self, option, label)
371 self.may_add_tip(button, node)
373 hbox = g.HBox(False, 4)
374 hbox.pack_start(g.Label(label), False, True, 0)
375 hbox.pack_start(button, False, True, 0)
377 self.handlers[option] = (button.get, button.set)
379 return [hbox]
381 def build_colour(self, node, label, option):
382 "<colour name='...' label='...'>Tooltip</colour>"
383 button = ColourButton(self, option, label)
385 self.may_add_tip(button, node)
387 hbox = g.HBox(False, 4)
388 hbox.pack_start(g.Label(label), False, True, 0)
389 hbox.pack_start(button, False, True, 0)
391 self.handlers[option] = (button.get, button.set)
393 return [hbox]
395 def build_numentry(self, node, label, option):
396 """<numentry name='...' label='...' min='0' max='100' step='1'>Tooltip</numentry>.
397 Lets the user choose a number from min to max."""
398 minv = int(node.getAttribute('min'))
399 maxv = int(node.getAttribute('max'))
400 step = node.getAttribute('step')
401 if step:
402 step = int(step)
403 else:
404 step = 1
405 unit = node.getAttribute('unit')
407 hbox = g.HBox(False, 4)
408 if label:
409 widget = g.Label(label)
410 widget.set_alignment(1.0, 0.5)
411 hbox.pack_start(widget, False, True, 0)
413 spin = g.SpinButton(g.Adjustment(minv, minv, maxv, step))
414 spin.set_width_chars(max(len(str(minv)), len(str(maxv))))
415 hbox.pack_start(spin, False, True, 0)
416 self.may_add_tip(spin, node)
418 if unit:
419 hbox.pack_start(g.Label(unit), False, True, 0)
421 self.handlers[option] = (
422 lambda: str(spin.get_value()),
423 lambda: spin.set_value(option.int_value))
425 spin.connect('value-changed', lambda w: self.check_widget(option))
427 return [hbox]
429 def build_radio_group(self, node, label, option):
430 """Build a list of radio buttons, only one of which may be selected.
431 <radio-group name='...'>
432 <radio value='...' label='...'>Tooltip</radio>
433 <radio value='...' label='...'>Tooltip</radio>
434 </radio-group>"""
435 radios = []
436 values = []
437 button = None
438 for radio in node.getElementsByTagName('radio'):
439 label = radio.getAttribute('label')
440 button = g.RadioButton(button, label)
441 self.may_add_tip(button, radio)
442 radios.append(button)
443 values.append(radio.getAttribute('value'))
444 button.connect('toggled', lambda b: self.check_widget(option))
446 def set():
447 try:
448 i = values.index(option.value)
449 except:
450 print "Value '%s' not in radio group!" % option.value
451 i = 0
452 radios[i].set_active(True)
453 def get():
454 for r, v in zip(radios, values):
455 if r.get_active():
456 return v
457 raise Exception('Nothing selected!')
459 self.handlers[option] = (get, set)
461 return radios
463 def build_toggle(self, node, label, option):
464 "<toggle name='...' label='...'>Tooltip</toggle>"
465 toggle = g.CheckButton(label)
466 self.may_add_tip(toggle, node)
468 self.handlers[option] = (
469 lambda: str(toggle.get_active()),
470 lambda: toggle.set_active(option.int_value))
472 toggle.connect('toggled', lambda w: self.check_widget(option))
474 return [toggle]
476 class FontButton(g.Button):
477 def __init__(self, option_box, option, title):
478 g.Button.__init__(self)
479 self.option_box = option_box
480 self.option = option
481 self.title = title
482 self.label = g.Label('<font>')
483 self.add(self.label)
484 self.dialog = None
485 self.connect('clicked', self.clicked)
487 def set(self):
488 self.label.set_text(self.option.value)
489 if self.dialog:
490 self.dialog.destroy()
492 def get(self):
493 return self.label.get()
495 def clicked(self, button):
496 if self.dialog:
497 self.dialog.destroy()
499 def closed(dialog):
500 self.dialog = None
502 def response(dialog, resp):
503 if resp != g.RESPONSE_OK:
504 dialog.destroy()
505 return
506 self.label.set_text(dialog.get_font_name())
507 dialog.destroy()
508 self.option_box.check_widget(self.option)
510 self.dialog = g.FontSelectionDialog(self.title)
511 self.dialog.set_position(g.WIN_POS_MOUSE)
512 self.dialog.connect('destroy', closed)
513 self.dialog.connect('response', response)
515 self.dialog.set_font_name(self.get())
516 self.dialog.show()
518 class ColourButton(g.Button):
519 def __init__(self, option_box, option, title):
520 g.Button.__init__(self)
521 self.option_box = option_box
522 self.option = option
523 self.title = title
524 self.set_size_request(64, 12)
525 self.dialog = None
526 self.connect('clicked', self.clicked)
528 def set(self, c = None):
529 if c is None:
530 c = g.gdk.color_parse(self.option.value)
531 self.modify_bg(g.STATE_NORMAL, c)
532 self.modify_bg(g.STATE_PRELIGHT, c)
533 self.modify_bg(g.STATE_ACTIVE, c)
535 def get(self):
536 c = self.get_style().bg[g.STATE_NORMAL]
537 return '#%04x%04x%04x' % (c.red, c.green, c.blue)
539 def clicked(self, button):
540 if self.dialog:
541 self.dialog.destroy()
543 def closed(dialog):
544 self.dialog = None
546 def response(dialog, resp):
547 if resp != g.RESPONSE_OK:
548 dialog.destroy()
549 return
550 self.set(dialog.colorsel.get_current_color())
551 dialog.destroy()
552 self.option_box.check_widget(self.option)
554 self.dialog = g.ColorSelectionDialog(self.title)
555 self.dialog.set_position(g.WIN_POS_MOUSE)
556 self.dialog.connect('destroy', closed)
557 self.dialog.connect('response', response)
559 c = self.get_style().bg[g.STATE_NORMAL]
560 self.dialog.colorsel.set_current_color(c)
561 self.dialog.show()