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
):
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 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()
86 for section
in doc
.documentElement
.childNodes
:
87 if section
.nodeType
!= Node
.ELEMENT_NODE
:
89 if section
.localName
!= 'section':
90 print "Unknown section", section
92 self
.build_section(section
, None)
95 self
.tree_view
.expand_all()
97 self
.sections_swin
.hide()
101 def destroyed(widget
):
107 rox
.report_exception()
108 self
.connect('destroy', destroyed
)
110 def got_response(widget
, response
):
111 if response
== g
.RESPONSE_OK
:
113 elif response
== REVERT
:
114 for o
in self
.options
:
115 o
._set
(self
.revert
[o
])
116 self
.update_widgets()
117 self
.options
.notify()
119 self
.connect('response', got_response
)
122 """Show the window, updating all the widgets at the same
123 time. Use this instead of show()."""
125 for option
in self
.options
:
126 self
.revert
[option
] = option
.value
127 self
.update_widgets()
131 def update_revert(self
):
132 "Shade/unshade the Revert button. Internal."
133 self
.set_response_sensitive(REVERT
, self
.changed())
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
]:
143 def update_widgets(self
):
144 "Make widgets show current values. Internal."
145 assert not self
.updating
149 for option
in self
.options
:
151 handler
= self
.handlers
[option
][1]
153 print "No widget for option '%s'!" % option
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...
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
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
)
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)
197 self
.notebook
= notebook
200 # (sel = sel; pygtk bug?)
201 def change_page(tv
, sel
= sel
, notebook
= notebook
):
202 selected
= sel
.get_selected()
205 model
, iter = selected
206 page
= model
.get_value(iter, 1)
208 notebook
.set_current_page(page
)
209 sel
.connect('changed', change_page
)
213 def check_widget(self
, option
):
214 "A widgets call this when the user changes its value."
218 assert isinstance(option
, options
.Option
)
220 new
= self
.handlers
[option
][0]()
222 if new
== option
.value
:
226 self
.options
.notify()
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
:
242 name
= node
.localName
243 if name
== 'section':
244 self
.build_section(node
, iter)
246 self
.build_widget(node
, page
)
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')
258 option
= self
.options
.options
[name
]
260 raise Exception("Unknown option '%s'" % name
)
263 name
= node
.localName
.replace('-', '_')
264 fn
= getattr(self
, 'build_' + name
)
265 except AttributeError:
266 print "Unknown widget type '%s'" % node
.localName
269 widgets
= fn(node
, label
, option
)
271 widgets
= fn(node
, label
)
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'."""
278 data
= ''.join([n
.nodeValue
for n
in node
.childNodes
]).strip()
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
):
297 eb
.set_size_request(8, 8)
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."
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
)
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
330 #list = pango.AttrList()
332 #label_widget.set_attributes(list)
334 vbox
= g
.VBox(False, 4)
335 vbox
.set_border_width(12)
338 self
.do_box(node
, None, vbox
)
342 def build_entry(self
, node
, label
, option
):
343 "<entry name='...' label='...'>Tooltip</entry>"
344 box
= g
.HBox(False, 4)
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)
355 self
.may_add_tip(entry
, node
)
357 entry
.connect('changed', lambda e
: self
.check_widget(option
))
360 return entry
.get_chars(0, -1)
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)
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)
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')
405 unit
= node
.getAttribute('unit')
407 hbox
= g
.HBox(False, 4)
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
)
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
))
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>
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
))
448 i
= values
.index(option
.value
)
450 print "Value '%s' not in radio group!" % option
.value
452 radios
[i
].set_active(True)
454 for r
, v
in zip(radios
, values
):
457 raise Exception('Nothing selected!')
459 self
.handlers
[option
] = (get
, set)
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
))
476 class FontButton(g
.Button
):
477 def __init__(self
, option_box
, option
, title
):
478 g
.Button
.__init
__(self
)
479 self
.option_box
= option_box
482 self
.label
= g
.Label('<font>')
485 self
.connect('clicked', self
.clicked
)
488 self
.label
.set_text(self
.option
.value
)
490 self
.dialog
.destroy()
493 return self
.label
.get()
495 def clicked(self
, button
):
497 self
.dialog
.destroy()
502 def response(dialog
, resp
):
503 if resp
!= g
.RESPONSE_OK
:
506 self
.label
.set_text(dialog
.get_font_name())
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())
518 class ColourButton(g
.Button
):
519 def __init__(self
, option_box
, option
, title
):
520 g
.Button
.__init
__(self
)
521 self
.option_box
= option_box
524 self
.set_size_request(64, 12)
526 self
.connect('clicked', self
.clicked
)
528 def set(self
, c
= 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
)
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
):
541 self
.dialog
.destroy()
546 def response(dialog
, resp
):
547 if resp
!= g
.RESPONSE_OK
:
550 self
.set(dialog
.colorsel
.get_current_color())
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
)