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
115 self
.size_groups
= {} # Name -> GtkSizeGroup
116 self
.current_size_group
= None
118 self
.build_window_frame()
122 for section
in doc
.documentElement
.childNodes
:
123 if section
.nodeType
!= Node
.ELEMENT_NODE
:
125 if section
.localName
!= 'section':
126 print "Unknown section", section
128 self
.build_section(section
, None)
131 self
.tree_view
.expand_all()
133 self
.sections_swin
.hide()
137 def destroyed(widget
):
143 rox
.report_exception()
144 self
.connect('destroy', destroyed
)
146 def got_response(widget
, response
):
147 if response
== g
.RESPONSE_OK
:
149 elif response
== REVERT
:
150 for o
in self
.options
:
151 o
._set
(self
.revert
[o
])
152 self
.update_widgets()
153 self
.options
.notify()
155 self
.connect('response', got_response
)
158 """Show the window, updating all the widgets at the same
159 time. Use this instead of show()."""
161 for option
in self
.options
:
162 self
.revert
[option
] = option
.value
163 self
.update_widgets()
167 def update_revert(self
):
168 "Shade/unshade the Revert button. Internal."
169 self
.set_response_sensitive(REVERT
, self
.changed())
172 """Check whether any options have different values (ie, whether Revert
173 will do anything)."""
174 for option
in self
.options
:
175 if option
.value
!= self
.revert
[option
]:
179 def update_widgets(self
):
180 "Make widgets show current values. Internal."
181 assert not self
.updating
185 for option
in self
.options
:
187 handler
= self
.handlers
[option
][1]
189 print "No widget for option '%s'!" % option
195 def build_window_frame(self
):
196 "Create the main structure of the window."
197 hbox
= g
.HBox(False, 4)
198 self
.vbox
.pack_start(hbox
, True, True, 0)
200 # scrolled window for the tree view
201 sw
= g
.ScrolledWindow()
202 sw
.set_shadow_type(g
.SHADOW_IN
)
203 sw
.set_policy(g
.POLICY_NEVER
, g
.POLICY_AUTOMATIC
)
204 hbox
.pack_start(sw
, False, True, 0)
205 self
.sections_swin
= sw
# Used to hide it...
208 model
= g
.TreeStore(gobject
.TYPE_STRING
, gobject
.TYPE_INT
)
209 tv
= g
.TreeView(model
)
210 sel
= tv
.get_selection()
211 sel
.set_mode(g
.SELECTION_BROWSE
)
212 tv
.set_headers_visible(False)
213 self
.sections
= model
215 tv
.unset_flags(g
.CAN_FOCUS
) # Stop irritating highlight
217 # Add a column to display column 0 of the store...
218 cell
= g
.CellRendererText()
219 column
= g
.TreeViewColumn('Section', cell
, text
= 0)
220 tv
.append_column(column
)
226 frame
.set_shadow_type(g
.SHADOW_IN
)
227 hbox
.pack_start(frame
, True, True, 0)
229 notebook
= g
.Notebook()
230 notebook
.set_show_tabs(False)
231 notebook
.set_show_border(False)
233 self
.notebook
= notebook
236 # (sel = sel; pygtk bug?)
237 def change_page(tv
, sel
= sel
, notebook
= notebook
):
238 selected
= sel
.get_selected()
241 model
, titer
= selected
242 page
= model
.get_value(titer
, 1)
244 notebook
.set_current_page(page
)
245 sel
.connect('changed', change_page
)
249 def check_widget(self
, option
):
250 "A widgets call this when the user changes its value."
254 assert isinstance(option
, options
.Option
)
256 new
= self
.handlers
[option
][0]()
258 if new
== option
.value
:
262 self
.options
.notify()
265 def build_section(self
, section
, parent
):
266 """Create a new page for the notebook and a new entry in the
267 sections tree, and build all the widgets inside the page."""
268 page
= g
.VBox(False, 4)
269 page
.set_border_width(4)
270 self
.notebook
.append_page(page
, g
.Label('unused'))
272 titer
= self
.sections
.append(parent
)
273 self
.sections
.set(titer
,
274 0, self
._(section
.getAttribute('title')),
275 1, self
.notebook
.page_num(page
))
276 for node
in section
.childNodes
:
277 if node
.nodeType
!= Node
.ELEMENT_NODE
:
279 name
= node
.localName
280 if name
== 'section':
281 self
.build_section(node
, titer
)
283 self
.build_widget(node
, page
)
286 def build_widget(self
, node
, box
):
287 """Dispatches the job of dealing with a DOM Node to the
288 appropriate build_* function."""
289 label
= node
.getAttribute('label')
290 name
= node
.getAttribute('name')
292 label
= self
._(label
)
294 old_size_group
= self
.current_size_group
295 sg
= node
.getAttributeNode('size-group')
297 self
.current_size_group
= sg
.value
or None
302 option
= self
.options
.options
[name
]
304 raise Exception("Unknown option '%s'" % name
)
306 # Check for a new-style function in the registry...
307 new_fn
= widget_registry
.get(node
.localName
, None)
309 # Wrap it up so it appears old-style
310 fn
= lambda *args
: new_fn(self
, *args
)
312 # Not in the registry... look in the class instead
314 name
= node
.localName
.replace('-', '_')
315 fn
= getattr(self
, 'build_' + name
)
316 except AttributeError:
317 fn
= self
.build_unknown
320 widgets
= fn(node
, label
, option
)
322 widgets
= fn(node
, label
)
324 box
.pack_start(w
, False, True, 0)
326 self
.current_size_group
= old_size_group
328 def may_add_tip(self
, widget
, node
):
329 """If 'node' contains any text, use that as the tip for 'widget'."""
331 data
= ''.join([n
.nodeValue
for n
in node
.childNodes
]).strip()
335 self
.tips
.set_tip(widget
, self
._(data
))
337 def get_size_group(self
, name
):
338 """Return the GtkSizeGroup for this name, creating one
339 if it doesn't currently exist."""
341 return self
.size_groups
[name
]
343 group
= g
.SizeGroup(g
.SIZE_GROUP_HORIZONTAL
)
344 self
.size_groups
[name
] = group
347 def make_sized_label(self
, label
, suffix
= ""):
348 """Create a GtkLabel and add it to the current size-group, if any"""
349 widget
= g
.Label(label
)
350 if self
.current_size_group
:
351 widget
.set_alignment(1.0, 0.5)
352 group
= self
.get_size_group(self
.current_size_group
+ suffix
)
353 group
.add_widget(widget
)
356 # Each type of widget has a method called 'build_NAME' where name is
357 # the XML element name. This method is called as method(node, label,
358 # option) if it corresponds to an Option, or method(node, label)
359 # otherwise. It should return a list of widgets to add to the window
360 # and, if it's for an Option, set self.handlers[option] = (get, set).
362 def build_unknown(self
, node
, label
, option
= None):
363 return [g
.Label("Unknown widget type <%s>" % node
.localName
)]
365 def build_label(self
, node
, label
):
366 help_flag
= int(node
.getAttribute('help') or '0')
367 widget
= self
.make_sized_label(self
._(data(node
)))
369 widget
.set_alignment(0, 0.5)
371 widget
.set_alignment(0, 1)
372 widget
.set_justify(g
.JUSTIFY_LEFT
)
373 widget
.set_line_wrap(True)
376 hbox
= g
.HBox(False, 4)
378 image
.set_from_stock(g
.STOCK_DIALOG_INFO
,
380 align
= g
.Alignment(0, 0, 0, 0)
383 hbox
.pack_start(align
, False, True, 0)
384 hbox
.pack_start(widget
, False, True, 0)
386 spacer
= g
.EventBox()
387 spacer
.set_size_request(6, 6)
389 return [hbox
, spacer
]
392 def build_spacer(self
, node
, label
):
395 eb
.set_size_request(8, 8)
398 def build_hbox(self
, node
, label
):
399 """<hbox>...</hbox> to layout child widgets horizontally."""
400 return self
.do_box(node
, label
, g
.HBox(False, 4))
401 def build_vbox(self
, node
, label
):
402 """<vbox>...</vbox> to layout child widgets vertically."""
403 return self
.do_box(node
, label
, g
.VBox(False, 0))
405 def do_box(self
, node
, label
, widget
):
406 "Helper function for building hbox, vbox and frame widgets."
408 widget
.pack_start(self
.make_sized_label(label
),
411 for child
in node
.childNodes
:
412 if child
.nodeType
== Node
.ELEMENT_NODE
:
413 self
.build_widget(child
, widget
)
417 def build_frame(self
, node
, label
):
418 """<frame label='Title'>...</frame> to group options under a heading."""
419 frame
= g
.Frame(label
)
420 frame
.set_shadow_type(g
.SHADOW_NONE
)
422 # Make the label bold...
423 # (bug in pygtk => use set_markup)
424 label_widget
= frame
.get_label_widget()
425 label_widget
.set_markup('<b>' + label
+ '</b>')
426 #attr = pango.AttrWeight(pango.WEIGHT_BOLD)
427 #attr.start_index = 0
429 #list = pango.AttrList()
431 #label_widget.set_attributes(list)
433 vbox
= g
.VBox(False, 4)
434 vbox
.set_border_width(12)
437 self
.do_box(node
, None, vbox
)
441 def do_entry(self
, node
, label
, option
):
442 "Helper function for entry and secretentry widgets"
443 box
= g
.HBox(False, 4)
447 label_wid
= self
.make_sized_label(label
)
448 label_wid
.set_alignment(1.0, 0.5)
449 box
.pack_start(label_wid
, False, True, 0)
450 box
.pack_start(entry
, True, True, 0)
454 self
.may_add_tip(entry
, node
)
456 entry
.connect('changed', lambda e
: self
.check_widget(option
))
459 return entry
.get_chars(0, -1)
461 entry
.set_text(option
.value
)
462 self
.handlers
[option
] = (get
, set)
464 return (entry
, [box
or entry
])
466 def build_entry(self
, node
, label
, option
):
467 "<entry name='...' label='...'>Tooltip</entry>"
468 entry
, result
=self
.do_entry(node
, label
, option
)
471 def build_secretentry(self
, node
, label
, option
):
472 "<secretentry name='...' label='...' char='*'>Tooltip</secretentry>"
473 entry
, result
=self
.do_entry(node
, label
, option
)
475 ch
=node
.getAttribute('char')
483 entry
.set_visibility(g
.FALSE
)
484 entry
.set_invisible_char(ch
)
488 def build_font(self
, node
, label
, option
):
489 "<font name='...' label='...'>Tooltip</font>"
490 button
= FontButton(self
, option
, label
)
492 self
.may_add_tip(button
, node
)
494 hbox
= g
.HBox(False, 4)
495 hbox
.pack_start(self
.make_sized_label(label
), False, True, 0)
496 hbox
.pack_start(button
, False, True, 0)
498 self
.handlers
[option
] = (button
.get
, button
.set)
502 def build_colour(self
, node
, label
, option
):
503 "<colour name='...' label='...'>Tooltip</colour>"
504 button
= ColourButton(self
, option
, label
)
506 self
.may_add_tip(button
, node
)
508 hbox
= g
.HBox(False, 4)
509 hbox
.pack_start(self
.make_sized_label(label
), False, True, 0)
510 hbox
.pack_start(button
, False, True, 0)
512 self
.handlers
[option
] = (button
.get
, button
.set)
516 def build_numentry(self
, node
, label
, option
):
517 """<numentry name='...' label='...' min='0' max='100' step='1'>Tooltip</numentry>.
518 Lets the user choose a number from min to max."""
519 minv
= int(node
.getAttribute('min'))
520 maxv
= int(node
.getAttribute('max'))
521 step
= node
.getAttribute('step')
522 unit
= node
.getAttribute('unit')
530 hbox
= g
.HBox(False, 4)
532 widget
= self
.make_sized_label(label
)
533 widget
.set_alignment(1.0, 0.5)
534 hbox
.pack_start(widget
, False, True, 0)
536 spin
= g
.SpinButton(g
.Adjustment(minv
, minv
, maxv
, step
))
537 spin
.set_width_chars(max(len(str(minv
)), len(str(maxv
))))
538 hbox
.pack_start(spin
, False, True, 0)
539 self
.may_add_tip(spin
, node
)
542 hbox
.pack_start(g
.Label(unit
), False, True, 0)
544 self
.handlers
[option
] = (
545 lambda: str(spin
.get_value()),
546 lambda: spin
.set_value(option
.int_value
))
548 spin
.connect('value-changed', lambda w
: self
.check_widget(option
))
552 def build_menu(self
, node
, label
, option
):
553 """Build an OptionMenu widget, only one item of which may be selected.
554 <menu name='...' label='...'>
555 <item value='...' label='...'/>
556 <item value='...' label='...'/>
561 option_menu
= g
.OptionMenu()
563 option_menu
.set_menu(menu
)
566 box
= g
.HBox(False, 4)
567 label_wid
= self
.make_sized_label(label
)
568 label_wid
.set_alignment(1.0, 0.5)
569 box
.pack_start(label_wid
, False, True, 0)
570 box
.pack_start(option_menu
, True, True, 0)
574 #self.may_add_tip(option_menu, node)
576 for item
in node
.getElementsByTagName('item'):
577 assert item
.hasAttribute('value')
578 value
= item
.getAttribute('value')
579 label_item
= self
._(item
.getAttribute('label')) or value
581 menu
.append(g
.MenuItem(label_item
))
584 option_menu
.connect('changed', lambda e
: self
.check_widget(option
))
587 return values
[option_menu
.get_history()]
591 option_menu
.set_history(values
.index(option
.value
))
593 print "Value '%s' not in combo list" % option
.value
595 self
.handlers
[option
] = (get
, set)
597 return [box
or option_menu
]
600 def build_radio_group(self
, node
, label
, option
):
601 """Build a list of radio buttons, only one of which may be selected.
602 <radio-group name='...'>
603 <radio value='...' label='...'>Tooltip</radio>
604 <radio value='...' label='...'>Tooltip</radio>
609 for radio
in node
.getElementsByTagName('radio'):
610 label
= self
._(radio
.getAttribute('label'))
611 button
= g
.RadioButton(button
, label
)
612 self
.may_add_tip(button
, radio
)
613 radios
.append(button
)
614 values
.append(radio
.getAttribute('value'))
615 button
.connect('toggled', lambda b
: self
.check_widget(option
))
619 i
= values
.index(option
.value
)
621 print "Value '%s' not in radio group!" % option
.value
623 radios
[i
].set_active(True)
625 for r
, v
in zip(radios
, values
):
628 raise Exception('Nothing selected!')
630 self
.handlers
[option
] = (get
, set)
634 def build_toggle(self
, node
, label
, option
):
635 "<toggle name='...' label='...'>Tooltip</toggle>"
636 toggle
= g
.CheckButton(label
)
637 self
.may_add_tip(toggle
, node
)
639 self
.handlers
[option
] = (
640 lambda: str(toggle
.get_active()),
641 lambda: toggle
.set_active(option
.int_value
))
643 toggle
.connect('toggled', lambda w
: self
.check_widget(option
))
647 def build_slider(self
, node
, label
, option
):
648 minv
= int(node
.getAttribute('min'))
649 maxv
= int(node
.getAttribute('max'))
650 fixed
= int(node
.getAttribute('fixed') or "0")
651 showvalue
= int(node
.getAttribute('showvalue') or "0")
652 end
= node
.getAttribute('end')
654 hbox
= g
.HBox(False, 4)
656 widget
= self
.make_sized_label(label
)
657 hbox
.pack_start(widget
, False, True, 0)
660 hbox
.pack_end(self
.make_sized_label(_(end
),
664 adj
= g
.Adjustment(minv
, minv
, maxv
, 1, 10, 0)
665 slide
= g
.HScale(adj
)
668 slide
.set_size_request(adj
.upper
, 24)
670 slide
.set_size_request(120, -1)
672 slide
.draw_value(True)
673 slide
.set_value_pos(g
.POS_LEFT
)
676 slide
.set_draw_value(False)
678 self
.may_add_tip(slide
, node
)
679 hbox
.pack_start(slide
, not fixed
, True, 0)
681 self
.handlers
[option
] = (
682 lambda: str(adj
.get_value()),
683 lambda: adj
.set_value(option
.int_value
))
685 slide
.connect('value-changed',
686 lambda w
: self
.check_widget(option
))
690 class FontButton(g
.Button
):
691 """A button that opens a GtkFontSelectionDialog"""
692 def __init__(self
, option_box
, option
, title
):
693 g
.Button
.__init
__(self
)
694 self
.option_box
= option_box
697 self
.label
= g
.Label('<font>')
700 self
.connect('clicked', self
.clicked
)
703 self
.label
.set_text(self
.option
.value
)
705 self
.dialog
.destroy()
708 return self
.label
.get()
710 def clicked(self
, button
):
712 self
.dialog
.destroy()
717 def response(dialog
, resp
):
718 if resp
!= g
.RESPONSE_OK
:
721 self
.label
.set_text(dialog
.get_font_name())
723 self
.option_box
.check_widget(self
.option
)
725 self
.dialog
= g
.FontSelectionDialog(self
.title
)
726 self
.dialog
.set_position(g
.WIN_POS_MOUSE
)
727 self
.dialog
.connect('destroy', closed
)
728 self
.dialog
.connect('response', response
)
730 self
.dialog
.set_font_name(self
.get())
733 class ColourButton(g
.Button
):
734 """A button that opens a GtkColorSelectionDialog"""
735 def __init__(self
, option_box
, option
, title
):
736 g
.Button
.__init
__(self
)
737 self
.c_box
= g
.EventBox()
739 self
.option_box
= option_box
742 self
.set_size_request(64, 14)
744 self
.connect('clicked', self
.clicked
)
745 self
.connect('expose-event', self
.expose
)
747 def expose(self
, widget
, event
):
748 # Some themes draw images and stuff here, so we have to
749 # override it manually.
750 self
.c_box
.window
.draw_rectangle(
751 self
.c_box
.style
.bg_gc
[g
.STATE_NORMAL
], True,
753 self
.c_box
.allocation
.width
,
754 self
.c_box
.allocation
.height
)
756 def set(self
, c
= None):
758 c
= g
.gdk
.color_parse(self
.option
.value
)
759 self
.c_box
.modify_bg(g
.STATE_NORMAL
, c
)
762 c
= self
.c_box
.get_style().bg
[g
.STATE_NORMAL
]
763 return '#%04x%04x%04x' % (c
.red
, c
.green
, c
.blue
)
765 def clicked(self
, button
):
767 self
.dialog
.destroy()
772 def response(dialog
, resp
):
773 if resp
!= g
.RESPONSE_OK
:
776 self
.set(dialog
.colorsel
.get_current_color())
778 self
.option_box
.check_widget(self
.option
)
780 self
.dialog
= g
.ColorSelectionDialog(self
.title
)
781 self
.dialog
.set_position(g
.WIN_POS_MOUSE
)
782 self
.dialog
.connect('destroy', closed
)
783 self
.dialog
.connect('response', response
)
785 c
= self
.c_box
.get_style().bg
[g
.STATE_NORMAL
]
786 self
.dialog
.colorsel
.set_current_color(c
)
789 # Add your own options here... (maps element localName to build function)