Translate text in Options box.
[rox-lib.git] / python / rox / OptionsBox.py
blob1a697ee616b343a7ce7011ffe28df2c826d648e6
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, translation = None):
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 if translation is None:
62 import __main__
63 if hasattr(__main__.__builtins__, '_'):
64 translation = __main__.__builtins__._
65 else:
66 translation = lambda x: x
67 self._ = translation
69 g.Dialog.__init__(self)
70 self.tips = g.Tooltips()
71 self.set_has_separator(False)
73 self.options = options_group
74 self.set_title(('%s options') % options_group.program)
75 self.set_position(g.WIN_POS_CENTER)
77 button = rox.ButtonMixed(g.STOCK_UNDO, _('_Revert'))
78 self.add_action_widget(button, REVERT)
79 self.tips.set_tip(button, _('Restore all options to how they were '
80 'when the window was opened'))
82 self.add_button(g.STOCK_OK, g.RESPONSE_OK)
84 doc = minidom.parse(options_xml)
85 assert doc.documentElement.localName == 'options'
87 self.handlers = {} # Option -> (get, set)
88 self.revert = {} # Option -> old value
90 self.build_window_frame()
92 # Add each section
93 n = 0
94 for section in doc.documentElement.childNodes:
95 if section.nodeType != Node.ELEMENT_NODE:
96 continue
97 if section.localName != 'section':
98 print "Unknown section", section
99 continue
100 self.build_section(section, None)
101 n += 1
102 if n > 1:
103 self.tree_view.expand_all()
104 else:
105 self.sections_swin.hide()
107 self.updating = 0
109 def destroyed(widget):
110 rox.toplevel_unref()
111 if self.changed():
112 try:
113 self.options.save()
114 except:
115 rox.report_exception()
116 self.connect('destroy', destroyed)
118 def got_response(widget, response):
119 if response == g.RESPONSE_OK:
120 self.destroy()
121 elif response == REVERT:
122 for o in self.options:
123 o._set(self.revert[o])
124 self.update_widgets()
125 self.options.notify()
126 self.update_revert()
127 self.connect('response', got_response)
129 def open(self):
130 """Show the window, updating all the widgets at the same
131 time. Use this instead of show()."""
132 rox.toplevel_ref()
133 for option in self.options:
134 self.revert[option] = option.value
135 self.update_widgets()
136 self.update_revert()
137 self.show()
139 def update_revert(self):
140 "Shade/unshade the Revert button. Internal."
141 self.set_response_sensitive(REVERT, self.changed())
143 def changed(self):
144 """Check whether any options have different values (ie, whether Revert
145 will do anything)."""
146 for option in self.options:
147 if option.value != self.revert[option]:
148 return True
149 return False
151 def update_widgets(self):
152 "Make widgets show current values. Internal."
153 assert not self.updating
154 self.updating = 1
156 try:
157 for option in self.options:
158 try:
159 handler = self.handlers[option][1]
160 except KeyError:
161 print "No widget for option '%s'!" % option
162 else:
163 handler()
164 finally:
165 self.updating = 0
167 def build_window_frame(self):
168 "Create the main structure of the window."
169 hbox = g.HBox(False, 4)
170 self.vbox.pack_start(hbox, True, True, 0)
172 # scrolled window for the tree view
173 sw = g.ScrolledWindow()
174 sw.set_shadow_type(g.SHADOW_IN)
175 sw.set_policy(g.POLICY_NEVER, g.POLICY_AUTOMATIC)
176 hbox.pack_start(sw, False, True, 0)
177 self.sections_swin = sw # Used to hide it...
179 # tree view
180 model = g.TreeStore(gobject.TYPE_STRING, gobject.TYPE_INT)
181 tv = g.TreeView(model)
182 sel = tv.get_selection()
183 sel.set_mode(g.SELECTION_BROWSE)
184 tv.set_headers_visible(False)
185 self.sections = model
186 self.tree_view = tv
187 tv.unset_flags(g.CAN_FOCUS) # Stop irritating highlight
189 # Add a column to display column 0 of the store...
190 cell = g.CellRendererText()
191 column = g.TreeViewColumn('Section', cell, text = 0)
192 tv.append_column(column)
194 sw.add(tv)
196 # main options area
197 frame = g.Frame()
198 frame.set_shadow_type(g.SHADOW_IN)
199 hbox.pack_start(frame, True, True, 0)
201 notebook = g.Notebook()
202 notebook.set_show_tabs(False)
203 notebook.set_show_border(False)
204 frame.add(notebook)
205 self.notebook = notebook
207 # Flip pages
208 # (sel = sel; pygtk bug?)
209 def change_page(tv, sel = sel, notebook = notebook):
210 selected = sel.get_selected()
211 if not selected:
212 return
213 model, iter = selected
214 page = model.get_value(iter, 1)
216 notebook.set_current_page(page)
217 sel.connect('changed', change_page)
219 self.vbox.show_all()
221 def check_widget(self, option):
222 "A widgets call this when the user changes its value."
223 if self.updating:
224 return
226 assert isinstance(option, options.Option)
228 new = self.handlers[option][0]()
230 if new == option.value:
231 return
233 option._set(new)
234 self.options.notify()
235 self.update_revert()
237 def build_section(self, section, parent):
238 """Create a new page for the notebook and a new entry in the
239 sections tree, and build all the widgets inside the page."""
240 page = g.VBox(False, 4)
241 page.set_border_width(4)
242 self.notebook.append_page(page, g.Label('unused'))
244 iter = self.sections.append(parent)
245 self.sections.set(iter, 0, section.getAttribute('title'),
246 1, self.notebook.page_num(page))
247 for node in section.childNodes:
248 if node.nodeType != Node.ELEMENT_NODE:
249 continue
250 name = node.localName
251 if name == 'section':
252 self.build_section(node, iter)
253 else:
254 self.build_widget(node, page)
255 page.show_all()
257 def build_widget(self, node, box):
258 """Dispatches the job of dealing with a DOM Node to the
259 appropriate build_* function."""
260 label = node.getAttribute('label')
261 name = node.getAttribute('name')
262 if label:
263 label = self._(label)
265 option = None
266 if name:
267 try:
268 option = self.options.options[name]
269 except KeyError:
270 raise Exception("Unknown option '%s'" % name)
272 try:
273 name = node.localName.replace('-', '_')
274 fn = getattr(self, 'build_' + name)
275 except AttributeError:
276 print "Unknown widget type '%s'" % node.localName
277 else:
278 if option:
279 widgets = fn(node, label, option)
280 else:
281 widgets = fn(node, label)
282 for w in widgets:
283 box.pack_start(w, False, True, 0)
285 def may_add_tip(self, widget, node):
286 """If 'node' contains any text, use that as the tip for 'widget'."""
287 if node.childNodes:
288 data = ''.join([n.nodeValue for n in node.childNodes]).strip()
289 else:
290 data = None
291 if data:
292 self.tips.set_tip(widget, self._(data))
294 # Each type of widget has a method called 'build_NAME' where name is
295 # the XML element name. This method is called as method(node, label,
296 # option) if it corresponds to an Option, or method(node, label)
297 # otherwise. It should return a list of widgets to add to the window
298 # and, if it's for an Option, set self.handlers[option] = (get, set).
300 def build_label(self, node, label):
301 """<label>Text</label>"""
302 return [g.Label(data(node))]
304 def build_spacer(self, node, label):
305 """<spacer/>"""
306 eb = g.EventBox()
307 eb.set_size_request(8, 8)
308 return [eb]
310 def build_hbox(self, node, label):
311 """<hbox>...</hbox> to layout child widgets horizontally."""
312 return self.do_box(node, label, g.HBox(False, 4))
313 def build_vbox(self, node, label):
314 """<vbox>...</vbox> to layout child widgets vertically."""
315 return self.do_box(node, label, g.VBox(False, 0))
317 def do_box(self, node, label, widget):
318 "Helper function for building hbox, vbox and frame widgets."
319 if label:
320 widget.pack_start(g.Label(label), False, True, 4)
322 for child in node.childNodes:
323 if child.nodeType == Node.ELEMENT_NODE:
324 self.build_widget(child, widget)
326 return [widget]
328 def build_frame(self, node, label):
329 """<frame label='Title'>...</frame> to group options under a heading."""
330 frame = g.Frame(label)
331 frame.set_shadow_type(g.SHADOW_NONE)
333 # Make the label bold...
334 # (bug in pygtk => use set_markup)
335 label_widget = frame.get_label_widget()
336 label_widget.set_markup('<b>' + label + '</b>')
337 #attr = pango.AttrWeight(pango.WEIGHT_BOLD)
338 #attr.start_index = 0
339 #attr.end_index = -1
340 #list = pango.AttrList()
341 #list.insert(attr)
342 #label_widget.set_attributes(list)
344 vbox = g.VBox(False, 4)
345 vbox.set_border_width(12)
346 frame.add(vbox)
348 self.do_box(node, None, vbox)
350 return [frame]
352 def build_entry(self, node, label, option):
353 "<entry name='...' label='...'>Tooltip</entry>"
354 box = g.HBox(False, 4)
355 entry = g.Entry()
357 if label:
358 label_wid = g.Label(label)
359 label_wid.set_alignment(1.0, 0.5)
360 box.pack_start(label_wid, False, True, 0)
361 box.pack_start(entry, True, True, 0)
362 else:
363 box = None
365 self.may_add_tip(entry, node)
367 entry.connect('changed', lambda e: self.check_widget(option))
369 def get():
370 return entry.get_chars(0, -1)
371 def set():
372 entry.set_text(option.value)
373 self.handlers[option] = (get, set)
375 return [box or entry]
377 def build_font(self, node, label, option):
378 "<font name='...' label='...'>Tooltip</font>"
379 button = FontButton(self, option, label)
381 self.may_add_tip(button, node)
383 hbox = g.HBox(False, 4)
384 hbox.pack_start(g.Label(label), False, True, 0)
385 hbox.pack_start(button, False, True, 0)
387 self.handlers[option] = (button.get, button.set)
389 return [hbox]
391 def build_colour(self, node, label, option):
392 "<colour name='...' label='...'>Tooltip</colour>"
393 button = ColourButton(self, option, label)
395 self.may_add_tip(button, node)
397 hbox = g.HBox(False, 4)
398 hbox.pack_start(g.Label(label), False, True, 0)
399 hbox.pack_start(button, False, True, 0)
401 self.handlers[option] = (button.get, button.set)
403 return [hbox]
405 def build_numentry(self, node, label, option):
406 """<numentry name='...' label='...' min='0' max='100' step='1'>Tooltip</numentry>.
407 Lets the user choose a number from min to max."""
408 minv = int(node.getAttribute('min'))
409 maxv = int(node.getAttribute('max'))
410 step = node.getAttribute('step')
411 if step:
412 step = int(step)
413 else:
414 step = 1
415 unit = self._(node.getAttribute('unit'))
417 hbox = g.HBox(False, 4)
418 if label:
419 widget = g.Label(label)
420 widget.set_alignment(1.0, 0.5)
421 hbox.pack_start(widget, False, True, 0)
423 spin = g.SpinButton(g.Adjustment(minv, minv, maxv, step))
424 spin.set_width_chars(max(len(str(minv)), len(str(maxv))))
425 hbox.pack_start(spin, False, True, 0)
426 self.may_add_tip(spin, node)
428 if unit:
429 hbox.pack_start(g.Label(unit), False, True, 0)
431 self.handlers[option] = (
432 lambda: str(spin.get_value()),
433 lambda: spin.set_value(option.int_value))
435 spin.connect('value-changed', lambda w: self.check_widget(option))
437 return [hbox]
439 def build_combo_group(self, node, label, option):
440 """Build a list combo entry widget, only one of which may be
441 selected.
442 <combo-group name='...' label='...'>
443 <combo value='...' label='...'/>
444 <combo value='...' label='...'/>
445 </combo-group>"""
447 # List to be displayed in widget
448 labels = []
449 # Dictionary to equate labels to values
450 combos = {}
452 combo = g.Combo()
454 if label:
455 box = g.HBox(False, 4)
456 label_wid = g.Label(label)
457 label_wid.set_alignment(1.0, 0.5)
458 box.pack_start(label_wid, False, True, 0)
459 box.pack_start(combo, True, True, 0)
460 else:
461 box = None
463 #self.may_add_tip(combo, node)
465 # Build combo list
466 for cmb_option in node.getElementsByTagName('combo'):
467 value = cmb_option.getAttribute('value')
468 label_item = cmb_option.getAttribute('label') or value
470 labels.append(label_item)
471 combos[value] = label_item
473 # Now, add the list to the combo box
474 combo.set_popdown_strings(labels)
476 combo.entry.connect('changed',
477 lambda e: self.check_widget(option))
479 def get():
480 cmb_label = combo.entry.get_text()
481 """Look for where the label that is selected equals a
482 value in the dictionary, then return the key"""
483 for key, item in combos.items():
484 if item == cmb_label:
485 return key
487 def set():
488 """Paranoia check"""
489 #print combos[option.value]
490 try:
491 saved_value_label = combos[option.value]
492 combo.entry.set_text(saved_value_label)
493 except:
494 print "Value '%s' not in combo list" % option.value
496 self.handlers[option] = (get, set)
498 return [box or combo]
501 def build_radio_group(self, node, label, option):
502 """Build a list of radio buttons, only one of which may be selected.
503 <radio-group name='...'>
504 <radio value='...' label='...'>Tooltip</radio>
505 <radio value='...' label='...'>Tooltip</radio>
506 </radio-group>"""
507 radios = []
508 values = []
509 button = None
510 for radio in node.getElementsByTagName('radio'):
511 label = self._(radio.getAttribute('label'))
512 button = g.RadioButton(button, label)
513 self.may_add_tip(button, radio)
514 radios.append(button)
515 values.append(radio.getAttribute('value'))
516 button.connect('toggled', lambda b: self.check_widget(option))
518 def set():
519 try:
520 i = values.index(option.value)
521 except:
522 print "Value '%s' not in radio group!" % option.value
523 i = 0
524 radios[i].set_active(True)
525 def get():
526 for r, v in zip(radios, values):
527 if r.get_active():
528 return v
529 raise Exception('Nothing selected!')
531 self.handlers[option] = (get, set)
533 return radios
535 def build_toggle(self, node, label, option):
536 "<toggle name='...' label='...'>Tooltip</toggle>"
537 toggle = g.CheckButton(label)
538 self.may_add_tip(toggle, node)
540 self.handlers[option] = (
541 lambda: str(toggle.get_active()),
542 lambda: toggle.set_active(option.int_value))
544 toggle.connect('toggled', lambda w: self.check_widget(option))
546 return [toggle]
548 class FontButton(g.Button):
549 def __init__(self, option_box, option, title):
550 g.Button.__init__(self)
551 self.option_box = option_box
552 self.option = option
553 self.title = title
554 self.label = g.Label('<font>')
555 self.add(self.label)
556 self.dialog = None
557 self.connect('clicked', self.clicked)
559 def set(self):
560 self.label.set_text(self.option.value)
561 if self.dialog:
562 self.dialog.destroy()
564 def get(self):
565 return self.label.get()
567 def clicked(self, button):
568 if self.dialog:
569 self.dialog.destroy()
571 def closed(dialog):
572 self.dialog = None
574 def response(dialog, resp):
575 if resp != g.RESPONSE_OK:
576 dialog.destroy()
577 return
578 self.label.set_text(dialog.get_font_name())
579 dialog.destroy()
580 self.option_box.check_widget(self.option)
582 self.dialog = g.FontSelectionDialog(self.title)
583 self.dialog.set_position(g.WIN_POS_MOUSE)
584 self.dialog.connect('destroy', closed)
585 self.dialog.connect('response', response)
587 self.dialog.set_font_name(self.get())
588 self.dialog.show()
590 class ColourButton(g.Button):
591 def __init__(self, option_box, option, title):
592 g.Button.__init__(self)
593 self.option_box = option_box
594 self.option = option
595 self.title = title
596 self.set_size_request(64, 12)
597 self.dialog = None
598 self.connect('clicked', self.clicked)
600 def set(self, c = None):
601 if c is None:
602 c = g.gdk.color_parse(self.option.value)
603 self.modify_bg(g.STATE_NORMAL, c)
604 self.modify_bg(g.STATE_PRELIGHT, c)
605 self.modify_bg(g.STATE_ACTIVE, c)
607 def get(self):
608 c = self.get_style().bg[g.STATE_NORMAL]
609 return '#%04x%04x%04x' % (c.red, c.green, c.blue)
611 def clicked(self, button):
612 if self.dialog:
613 self.dialog.destroy()
615 def closed(dialog):
616 self.dialog = None
618 def response(dialog, resp):
619 if resp != g.RESPONSE_OK:
620 dialog.destroy()
621 return
622 self.set(dialog.colorsel.get_current_color())
623 dialog.destroy()
624 self.option_box.check_widget(self.option)
626 self.dialog = g.ColorSelectionDialog(self.title)
627 self.dialog.set_position(g.WIN_POS_MOUSE)
628 self.dialog.connect('destroy', closed)
629 self.dialog.connect('response', response)
631 c = self.get_style().bg[g.STATE_NORMAL]
632 self.dialog.colorsel.set_current_color(c)
633 self.dialog.show()