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)
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
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))
36 OptionsBox.widget_registry['mytoggle'] = build_toggle
39 from rox
import g
, options
, _
41 from xml
.dom
import Node
, minidom
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
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.
79 <section title='Nested section'>
85 assert isinstance(options_group
, options
.OptionGroup
)
87 if translation
is None:
89 if hasattr(__main__
.__builtins
__, '_'):
90 translation
= __main__
.__builtins
__._
92 translation
= lambda x
: x
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
116 self
.build_window_frame()
120 for section
in doc
.documentElement
.childNodes
:
121 if section
.nodeType
!= Node
.ELEMENT_NODE
:
123 if section
.localName
!= 'section':
124 print "Unknown section", section
126 self
.build_section(section
, None)
129 self
.tree_view
.expand_all()
131 self
.sections_swin
.hide()
135 def destroyed(widget
):
141 rox
.report_exception()
142 self
.connect('destroy', destroyed
)
144 def got_response(widget
, response
):
145 if response
== g
.RESPONSE_OK
:
147 elif response
== REVERT
:
148 for o
in self
.options
:
149 o
._set
(self
.revert
[o
])
150 self
.update_widgets()
151 self
.options
.notify()
153 self
.connect('response', got_response
)
156 """Show the window, updating all the widgets at the same
157 time. Use this instead of show()."""
159 for option
in self
.options
:
160 self
.revert
[option
] = option
.value
161 self
.update_widgets()
165 def update_revert(self
):
166 "Shade/unshade the Revert button. Internal."
167 self
.set_response_sensitive(REVERT
, self
.changed())
170 """Check whether any options have different values (ie, whether Revert
171 will do anything)."""
172 for option
in self
.options
:
173 if option
.value
!= self
.revert
[option
]:
177 def update_widgets(self
):
178 "Make widgets show current values. Internal."
179 assert not self
.updating
183 for option
in self
.options
:
185 handler
= self
.handlers
[option
][1]
187 print "No widget for option '%s'!" % option
193 def build_window_frame(self
):
194 "Create the main structure of the window."
195 hbox
= g
.HBox(False, 4)
196 self
.vbox
.pack_start(hbox
, True, True, 0)
198 # scrolled window for the tree view
199 sw
= g
.ScrolledWindow()
200 sw
.set_shadow_type(g
.SHADOW_IN
)
201 sw
.set_policy(g
.POLICY_NEVER
, g
.POLICY_AUTOMATIC
)
202 hbox
.pack_start(sw
, False, True, 0)
203 self
.sections_swin
= sw
# Used to hide it...
206 model
= g
.TreeStore(gobject
.TYPE_STRING
, gobject
.TYPE_INT
)
207 tv
= g
.TreeView(model
)
208 sel
= tv
.get_selection()
209 sel
.set_mode(g
.SELECTION_BROWSE
)
210 tv
.set_headers_visible(False)
211 self
.sections
= model
213 tv
.unset_flags(g
.CAN_FOCUS
) # Stop irritating highlight
215 # Add a column to display column 0 of the store...
216 cell
= g
.CellRendererText()
217 column
= g
.TreeViewColumn('Section', cell
, text
= 0)
218 tv
.append_column(column
)
224 frame
.set_shadow_type(g
.SHADOW_IN
)
225 hbox
.pack_start(frame
, True, True, 0)
227 notebook
= g
.Notebook()
228 notebook
.set_show_tabs(False)
229 notebook
.set_show_border(False)
231 self
.notebook
= notebook
234 # (sel = sel; pygtk bug?)
235 def change_page(tv
, sel
= sel
, notebook
= notebook
):
236 selected
= sel
.get_selected()
239 model
, titer
= selected
240 page
= model
.get_value(titer
, 1)
242 notebook
.set_current_page(page
)
243 sel
.connect('changed', change_page
)
247 def check_widget(self
, option
):
248 "A widgets call this when the user changes its value."
252 assert isinstance(option
, options
.Option
)
254 new
= self
.handlers
[option
][0]()
256 if new
== option
.value
:
260 self
.options
.notify()
263 def build_section(self
, section
, parent
):
264 """Create a new page for the notebook and a new entry in the
265 sections tree, and build all the widgets inside the page."""
266 page
= g
.VBox(False, 4)
267 page
.set_border_width(4)
268 self
.notebook
.append_page(page
, g
.Label('unused'))
270 titer
= self
.sections
.append(parent
)
271 self
.sections
.set(titer
,
272 0, self
._(section
.getAttribute('title')),
273 1, self
.notebook
.page_num(page
))
274 for node
in section
.childNodes
:
275 if node
.nodeType
!= Node
.ELEMENT_NODE
:
277 name
= node
.localName
278 if name
== 'section':
279 self
.build_section(node
, titer
)
281 self
.build_widget(node
, page
)
284 def build_widget(self
, node
, box
):
285 """Dispatches the job of dealing with a DOM Node to the
286 appropriate build_* function."""
287 label
= node
.getAttribute('label')
288 name
= node
.getAttribute('name')
290 label
= self
._(label
)
295 option
= self
.options
.options
[name
]
297 raise Exception("Unknown option '%s'" % name
)
299 # Check for a new-style function in the registry...
300 new_fn
= widget_registry
.get(node
.localName
, None)
302 # Wrap it up so it appears old-style
303 fn
= lambda *args
: new_fn(self
, *args
)
305 # Not in the registry... look in the class instead
307 name
= node
.localName
.replace('-', '_')
308 fn
= getattr(self
, 'build_' + name
)
309 except AttributeError:
310 print "Unknown widget type '%s'" \
315 widgets
= fn(node
, label
, option
)
317 widgets
= fn(node
, label
)
319 box
.pack_start(w
, False, True, 0)
321 def may_add_tip(self
, widget
, node
):
322 """If 'node' contains any text, use that as the tip for 'widget'."""
324 data
= ''.join([n
.nodeValue
for n
in node
.childNodes
]).strip()
328 self
.tips
.set_tip(widget
, self
._(data
))
330 # Each type of widget has a method called 'build_NAME' where name is
331 # the XML element name. This method is called as method(node, label,
332 # option) if it corresponds to an Option, or method(node, label)
333 # otherwise. It should return a list of widgets to add to the window
334 # and, if it's for an Option, set self.handlers[option] = (get, set).
336 def build_label(self
, node
, label
):
337 widget
= g
.Label(self
._(data(node
)))
338 help_flag
= int(node
.getAttribute('help') or '0')
340 widget
.set_alignment(0, 0.5)
342 widget
.set_alignment(0, 1)
343 widget
.set_justify(g
.JUSTIFY_LEFT
)
344 widget
.set_line_wrap(True)
347 hbox
= g
.HBox(False, 4)
349 image
.set_from_stock(g
.STOCK_DIALOG_INFO
,
351 align
= g
.Alignment(0, 0, 0, 0)
354 hbox
.pack_start(align
, False, True, 0)
355 hbox
.pack_start(widget
, False, True, 0)
357 spacer
= g
.EventBox()
358 spacer
.set_size_request(6, 6)
360 return [hbox
, spacer
]
363 def build_spacer(self
, node
, label
):
366 eb
.set_size_request(8, 8)
369 def build_hbox(self
, node
, label
):
370 """<hbox>...</hbox> to layout child widgets horizontally."""
371 return self
.do_box(node
, label
, g
.HBox(False, 4))
372 def build_vbox(self
, node
, label
):
373 """<vbox>...</vbox> to layout child widgets vertically."""
374 return self
.do_box(node
, label
, g
.VBox(False, 0))
376 def do_box(self
, node
, label
, widget
):
377 "Helper function for building hbox, vbox and frame widgets."
379 widget
.pack_start(g
.Label(label
), False, True, 4)
381 for child
in node
.childNodes
:
382 if child
.nodeType
== Node
.ELEMENT_NODE
:
383 self
.build_widget(child
, widget
)
387 def build_frame(self
, node
, label
):
388 """<frame label='Title'>...</frame> to group options under a heading."""
389 frame
= g
.Frame(label
)
390 frame
.set_shadow_type(g
.SHADOW_NONE
)
392 # Make the label bold...
393 # (bug in pygtk => use set_markup)
394 label_widget
= frame
.get_label_widget()
395 label_widget
.set_markup('<b>' + label
+ '</b>')
396 #attr = pango.AttrWeight(pango.WEIGHT_BOLD)
397 #attr.start_index = 0
399 #list = pango.AttrList()
401 #label_widget.set_attributes(list)
403 vbox
= g
.VBox(False, 4)
404 vbox
.set_border_width(12)
407 self
.do_box(node
, None, vbox
)
411 def do_entry(self
, node
, label
, option
):
412 "Helper function for entry and secretentry widgets"
413 box
= g
.HBox(False, 4)
417 label_wid
= g
.Label(label
)
418 label_wid
.set_alignment(1.0, 0.5)
419 box
.pack_start(label_wid
, False, True, 0)
420 box
.pack_start(entry
, True, True, 0)
424 self
.may_add_tip(entry
, node
)
426 entry
.connect('changed', lambda e
: self
.check_widget(option
))
429 return entry
.get_chars(0, -1)
431 entry
.set_text(option
.value
)
432 self
.handlers
[option
] = (get
, set)
434 return (entry
, [box
or entry
])
436 def build_entry(self
, node
, label
, option
):
437 "<entry name='...' label='...'>Tooltip</entry>"
438 entry
, result
=self
.do_entry(node
, label
, option
)
441 def build_secretentry(self
, node
, label
, option
):
442 "<secretentry name='...' label='...' char='*'>Tooltip</secretentry>"
443 entry
, result
=self
.do_entry(node
, label
, option
)
445 ch
=node
.getAttribute('char')
453 entry
.set_visibility(g
.FALSE
)
454 entry
.set_invisible_char(ch
)
458 def build_font(self
, node
, label
, option
):
459 "<font name='...' label='...'>Tooltip</font>"
460 button
= FontButton(self
, option
, label
)
462 self
.may_add_tip(button
, node
)
464 hbox
= g
.HBox(False, 4)
465 hbox
.pack_start(g
.Label(label
), False, True, 0)
466 hbox
.pack_start(button
, False, True, 0)
468 self
.handlers
[option
] = (button
.get
, button
.set)
472 def build_colour(self
, node
, label
, option
):
473 "<colour name='...' label='...'>Tooltip</colour>"
474 button
= ColourButton(self
, option
, label
)
476 self
.may_add_tip(button
, node
)
478 hbox
= g
.HBox(False, 4)
479 hbox
.pack_start(g
.Label(label
), False, True, 0)
480 hbox
.pack_start(button
, False, True, 0)
482 self
.handlers
[option
] = (button
.get
, button
.set)
486 def build_numentry(self
, node
, label
, option
):
487 """<numentry name='...' label='...' min='0' max='100' step='1'>Tooltip</numentry>.
488 Lets the user choose a number from min to max."""
489 minv
= int(node
.getAttribute('min'))
490 maxv
= int(node
.getAttribute('max'))
491 step
= node
.getAttribute('step')
496 unit
= self
._(node
.getAttribute('unit'))
498 hbox
= g
.HBox(False, 4)
500 widget
= g
.Label(label
)
501 widget
.set_alignment(1.0, 0.5)
502 hbox
.pack_start(widget
, False, True, 0)
504 spin
= g
.SpinButton(g
.Adjustment(minv
, minv
, maxv
, step
))
505 spin
.set_width_chars(max(len(str(minv
)), len(str(maxv
))))
506 hbox
.pack_start(spin
, False, True, 0)
507 self
.may_add_tip(spin
, node
)
510 hbox
.pack_start(g
.Label(unit
), False, True, 0)
512 self
.handlers
[option
] = (
513 lambda: str(spin
.get_value()),
514 lambda: spin
.set_value(option
.int_value
))
516 spin
.connect('value-changed', lambda w
: self
.check_widget(option
))
520 def build_menu(self
, node
, label
, option
):
521 """Build an OptionMenu widget, only one item of which may be selected.
522 <menu name='...' label='...'>
523 <item value='...' label='...'/>
524 <item value='...' label='...'/>
529 option_menu
= g
.OptionMenu()
531 option_menu
.set_menu(menu
)
534 box
= g
.HBox(False, 4)
535 label_wid
= g
.Label(label
)
536 label_wid
.set_alignment(1.0, 0.5)
537 box
.pack_start(label_wid
, False, True, 0)
538 box
.pack_start(option_menu
, True, True, 0)
542 #self.may_add_tip(option_menu, node)
544 for item
in node
.getElementsByTagName('item'):
545 value
= item
.getAttribute('value')
547 label_item
= item
.getAttribute('label') or value
549 menu
.append(g
.MenuItem(label_item
))
552 option_menu
.connect('changed', lambda e
: self
.check_widget(option
))
555 return values
[option_menu
.get_history()]
559 option_menu
.set_history(values
.index(option
.value
))
561 print "Value '%s' not in combo list" % option
.value
563 self
.handlers
[option
] = (get
, set)
565 return [box
or option_menu
]
568 def build_radio_group(self
, node
, label
, option
):
569 """Build a list of radio buttons, only one of which may be selected.
570 <radio-group name='...'>
571 <radio value='...' label='...'>Tooltip</radio>
572 <radio value='...' label='...'>Tooltip</radio>
577 for radio
in node
.getElementsByTagName('radio'):
578 label
= self
._(radio
.getAttribute('label'))
579 button
= g
.RadioButton(button
, label
)
580 self
.may_add_tip(button
, radio
)
581 radios
.append(button
)
582 values
.append(radio
.getAttribute('value'))
583 button
.connect('toggled', lambda b
: self
.check_widget(option
))
587 i
= values
.index(option
.value
)
589 print "Value '%s' not in radio group!" % option
.value
591 radios
[i
].set_active(True)
593 for r
, v
in zip(radios
, values
):
596 raise Exception('Nothing selected!')
598 self
.handlers
[option
] = (get
, set)
602 def build_toggle(self
, node
, label
, option
):
603 "<toggle name='...' label='...'>Tooltip</toggle>"
604 toggle
= g
.CheckButton(label
)
605 self
.may_add_tip(toggle
, node
)
607 self
.handlers
[option
] = (
608 lambda: str(toggle
.get_active()),
609 lambda: toggle
.set_active(option
.int_value
))
611 toggle
.connect('toggled', lambda w
: self
.check_widget(option
))
615 class FontButton(g
.Button
):
616 """A button that opens a GtkFontSelectionDialog"""
617 def __init__(self
, option_box
, option
, title
):
618 g
.Button
.__init
__(self
)
619 self
.option_box
= option_box
622 self
.label
= g
.Label('<font>')
625 self
.connect('clicked', self
.clicked
)
628 self
.label
.set_text(self
.option
.value
)
630 self
.dialog
.destroy()
633 return self
.label
.get()
635 def clicked(self
, button
):
637 self
.dialog
.destroy()
642 def response(dialog
, resp
):
643 if resp
!= g
.RESPONSE_OK
:
646 self
.label
.set_text(dialog
.get_font_name())
648 self
.option_box
.check_widget(self
.option
)
650 self
.dialog
= g
.FontSelectionDialog(self
.title
)
651 self
.dialog
.set_position(g
.WIN_POS_MOUSE
)
652 self
.dialog
.connect('destroy', closed
)
653 self
.dialog
.connect('response', response
)
655 self
.dialog
.set_font_name(self
.get())
658 class ColourButton(g
.Button
):
659 """A button that opens a GtkColorSelectionDialog"""
660 def __init__(self
, option_box
, option
, title
):
661 g
.Button
.__init
__(self
)
662 self
.c_box
= g
.EventBox()
664 self
.option_box
= option_box
667 self
.set_size_request(64, 14)
669 self
.connect('clicked', self
.clicked
)
670 self
.connect('expose-event', self
.expose
)
672 def expose(self
, widget
, event
):
673 # Some themes draw images and stuff here, so we have to
674 # override it manually.
675 self
.c_box
.window
.draw_rectangle(
676 self
.c_box
.style
.bg_gc
[g
.STATE_NORMAL
], True,
678 self
.c_box
.allocation
.width
,
679 self
.c_box
.allocation
.height
)
681 def set(self
, c
= None):
683 c
= g
.gdk
.color_parse(self
.option
.value
)
684 self
.c_box
.modify_bg(g
.STATE_NORMAL
, c
)
687 c
= self
.c_box
.get_style().bg
[g
.STATE_NORMAL
]
688 return '#%04x%04x%04x' % (c
.red
, c
.green
, c
.blue
)
690 def clicked(self
, button
):
692 self
.dialog
.destroy()
697 def response(dialog
, resp
):
698 if resp
!= g
.RESPONSE_OK
:
701 self
.set(dialog
.colorsel
.get_current_color())
703 self
.option_box
.check_widget(self
.option
)
705 self
.dialog
= g
.ColorSelectionDialog(self
.title
)
706 self
.dialog
.set_position(g
.WIN_POS_MOUSE
)
707 self
.dialog
.connect('destroy', closed
)
708 self
.dialog
.connect('response', response
)
710 c
= self
.get_style().bg
[g
.STATE_NORMAL
]
711 self
.dialog
.colorsel
.set_current_color(c
)
714 # Add your own options here... (maps element localName to build function)