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
9 from rox
import g
, options
, _
11 from xml
.dom
import Node
, minidom
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
32 When the widget is modified, call self.check_widget(option) to
33 update the stored values.
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
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.
53 <section title='Nested section'>
59 assert isinstance(options_group
, options
.OptionGroup
)
61 if translation
is None:
63 if hasattr(__main__
.__builtins
__, '_'):
64 translation
= __main__
.__builtins
__._
66 translation
= lambda x
: x
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()
94 for section
in doc
.documentElement
.childNodes
:
95 if section
.nodeType
!= Node
.ELEMENT_NODE
:
97 if section
.localName
!= 'section':
98 print "Unknown section", section
100 self
.build_section(section
, None)
103 self
.tree_view
.expand_all()
105 self
.sections_swin
.hide()
109 def destroyed(widget
):
115 rox
.report_exception()
116 self
.connect('destroy', destroyed
)
118 def got_response(widget
, response
):
119 if response
== g
.RESPONSE_OK
:
121 elif response
== REVERT
:
122 for o
in self
.options
:
123 o
._set
(self
.revert
[o
])
124 self
.update_widgets()
125 self
.options
.notify()
127 self
.connect('response', got_response
)
130 """Show the window, updating all the widgets at the same
131 time. Use this instead of show()."""
133 for option
in self
.options
:
134 self
.revert
[option
] = option
.value
135 self
.update_widgets()
139 def update_revert(self
):
140 "Shade/unshade the Revert button. Internal."
141 self
.set_response_sensitive(REVERT
, self
.changed())
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
]:
151 def update_widgets(self
):
152 "Make widgets show current values. Internal."
153 assert not self
.updating
157 for option
in self
.options
:
159 handler
= self
.handlers
[option
][1]
161 print "No widget for option '%s'!" % option
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...
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
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
)
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)
205 self
.notebook
= notebook
208 # (sel = sel; pygtk bug?)
209 def change_page(tv
, sel
= sel
, notebook
= notebook
):
210 selected
= sel
.get_selected()
213 model
, iter = selected
214 page
= model
.get_value(iter, 1)
216 notebook
.set_current_page(page
)
217 sel
.connect('changed', change_page
)
221 def check_widget(self
, option
):
222 "A widgets call this when the user changes its value."
226 assert isinstance(option
, options
.Option
)
228 new
= self
.handlers
[option
][0]()
230 if new
== option
.value
:
234 self
.options
.notify()
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
:
250 name
= node
.localName
251 if name
== 'section':
252 self
.build_section(node
, iter)
254 self
.build_widget(node
, page
)
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')
263 label
= self
._(label
)
268 option
= self
.options
.options
[name
]
270 raise Exception("Unknown option '%s'" % name
)
273 name
= node
.localName
.replace('-', '_')
274 fn
= getattr(self
, 'build_' + name
)
275 except AttributeError:
276 print "Unknown widget type '%s'" % node
.localName
279 widgets
= fn(node
, label
, option
)
281 widgets
= fn(node
, label
)
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'."""
288 data
= ''.join([n
.nodeValue
for n
in node
.childNodes
]).strip()
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
):
307 eb
.set_size_request(8, 8)
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."
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
)
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
340 #list = pango.AttrList()
342 #label_widget.set_attributes(list)
344 vbox
= g
.VBox(False, 4)
345 vbox
.set_border_width(12)
348 self
.do_box(node
, None, vbox
)
352 def build_entry(self
, node
, label
, option
):
353 "<entry name='...' label='...'>Tooltip</entry>"
354 box
= g
.HBox(False, 4)
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)
365 self
.may_add_tip(entry
, node
)
367 entry
.connect('changed', lambda e
: self
.check_widget(option
))
370 return entry
.get_chars(0, -1)
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)
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)
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')
415 unit
= self
._(node
.getAttribute('unit'))
417 hbox
= g
.HBox(False, 4)
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
)
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
))
439 def build_combo_group(self
, node
, label
, option
):
440 """Build a list combo entry widget, only one of which may be
442 <combo-group name='...' label='...'>
443 <combo value='...' label='...'/>
444 <combo value='...' label='...'/>
447 # List to be displayed in widget
449 # Dictionary to equate labels to values
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)
463 #self.may_add_tip(combo, node)
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
))
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
:
489 #print combos[option.value]
491 saved_value_label
= combos
[option
.value
]
492 combo
.entry
.set_text(saved_value_label
)
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>
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
))
520 i
= values
.index(option
.value
)
522 print "Value '%s' not in radio group!" % option
.value
524 radios
[i
].set_active(True)
526 for r
, v
in zip(radios
, values
):
529 raise Exception('Nothing selected!')
531 self
.handlers
[option
] = (get
, set)
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
))
548 class FontButton(g
.Button
):
549 def __init__(self
, option_box
, option
, title
):
550 g
.Button
.__init
__(self
)
551 self
.option_box
= option_box
554 self
.label
= g
.Label('<font>')
557 self
.connect('clicked', self
.clicked
)
560 self
.label
.set_text(self
.option
.value
)
562 self
.dialog
.destroy()
565 return self
.label
.get()
567 def clicked(self
, button
):
569 self
.dialog
.destroy()
574 def response(dialog
, resp
):
575 if resp
!= g
.RESPONSE_OK
:
578 self
.label
.set_text(dialog
.get_font_name())
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())
590 class ColourButton(g
.Button
):
591 def __init__(self
, option_box
, option
, title
):
592 g
.Button
.__init
__(self
)
593 self
.option_box
= option_box
596 self
.set_size_request(64, 12)
598 self
.connect('clicked', self
.clicked
)
600 def set(self
, c
= 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
)
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
):
613 self
.dialog
.destroy()
618 def response(dialog
, resp
):
619 if resp
!= g
.RESPONSE_OK
:
622 self
.set(dialog
.colorsel
.get_current_color())
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
)