Allow "" as a valid value in a OptionsBox menu (reported by Guido Schimmels).
[rox-lib.git] / python / rox / options.py
blobd7ab3483021c52eb71eb71133fc31b819dba496d
1 """
2 To use the Options system:
4 1. Create an OptionGroup:
5 options = OptionGroup('MyProg', 'Options')
6 You can also use the handy rox.setup_app_options() in most applications.
8 2. Create the options:
9 colour = Option('colour', 'red', options)
10 size = Option('size', 3, options)
12 3. Register any callbacks (notification of options changing):
13 def my_callback():
14 if colour.has_changed:
15 print "The colour is now", colour.value
16 options.add_notify(my_callback)
18 4. Notify any changes from defaults:
19 options.notify()
21 See OptionsBox for editing options. Do not change the value of options
22 yourself.
23 """
25 from __future__ import generators
27 import choices
28 import rox
30 from xml.dom import Node, minidom
32 def data(node):
33 """Return all the text directly inside this DOM Node."""
34 return ''.join([text.nodeValue for text in node.childNodes
35 if text.nodeType == Node.TEXT_NODE])
37 class Option:
38 """An Option stores a single value. Every option is part of exactly one OptionGroup.
40 The read-only attributes value and int_value can be used to get the current setting
41 for the Option. int_value will be -1 if the value is not a valid integer.
43 The has_changed attribute is used during notify() calls to indicate whether this
44 Option's value has changed since the last notify (or option creation).
45 You may set has_changed = 1 right after creating an option if you want to force
46 notification the first time even if the default is used.
47 """
48 def __init__(self, name, value, group = None):
49 """Create a new option with this name and default value.
50 Add to 'group', or to rox.app_options if no group is given.
51 The value cannot be used until the first notify() call to
52 the group."""
53 if not group:
54 assert rox.app_options
55 group = rox.app_options
56 self.name = name
57 self.has_changed = 0 # ... since last notify/default
58 self.default_value = str(value)
59 self.group = group
60 self.value = None
61 self.int_value = None
63 self.group._register(self)
65 def _set(self, value):
66 if self.value != value:
67 self.value = str(value)
68 self.has_changed = 1
69 try:
70 if self.value == 'True':
71 self.int_value = 1
72 elif self.value == 'False':
73 self.int_value = 0
74 else:
75 self.int_value = int(float(self.value))
76 except:
77 self.int_value = -1
79 def _to_xml(self, parent):
80 doc = parent.ownerDocument
81 node = doc.createElement('Option')
82 node.setAttribute('name', self.name)
83 node.appendChild(doc.createTextNode(self.value))
84 parent.appendChild(node)
86 def __str__(self):
87 return "<Option %s=%s>" % (self.name, self.value)
89 class OptionGroup:
90 def __init__(self, program, leaf):
91 "program/leaf is a Choices pair for the saved options."
92 self.program = program
93 self.leaf = leaf
94 self.pending = {} # Loaded, but not registered
95 self.options = {} # Name -> Option
96 self.callbacks = []
97 self.too_late_for_registrations = 0
99 path = choices.load(program, leaf)
100 if not path:
101 return
103 try:
104 doc = minidom.parse(path)
106 root = doc.documentElement
107 assert root.localName == 'Options'
108 for o in root.childNodes:
109 if o.nodeType != Node.ELEMENT_NODE:
110 continue
111 if o.localName != 'Option':
112 print "Warning: Non Option element", o
113 continue
114 name = o.getAttribute('name')
115 self.pending[name] = data(o)
116 except:
117 rox.report_exception()
119 def _register(self, option):
120 """Called by Option.__init__."""
121 assert option.name not in self.options
122 assert not self.too_late_for_registrations
124 name = option.name
126 self.options[name] = option
128 if name in self.pending:
129 option._set(self.pending[name])
130 del self.pending[name]
132 def save(self):
133 """Save all option values. Usually called by OptionsBox()."""
134 assert self.too_late_for_registrations
136 path = choices.save(self.program, self.leaf)
137 if not path:
138 return # Saving is disabled
140 from xml.dom.minidom import Document
141 doc = Document()
142 root = doc.createElement('Options')
143 doc.appendChild(root)
145 for option in self:
146 option._to_xml(root)
148 stream = open(path, 'w')
149 doc.writexml(stream)
150 stream.close()
152 def add_notify(self, callback):
153 "Call callback() after one or more options have changed value."
154 assert callback not in self.callbacks
156 self.callbacks.append(callback)
158 def remove_notify(self, callback):
159 """Remove a callback added with add_notify()."""
160 self.callbacks.remove(callback)
162 def notify(self):
163 """Call this after creating any new options or changing their values."""
164 if not self.too_late_for_registrations:
165 self.too_late_for_registrations = 1
166 if self.pending:
167 print "Warning: Some options loaded but unused:"
168 for (key, value) in self.pending.iteritems():
169 print "%s=%s" % (key, value)
170 for o in self:
171 if o.value is None:
172 o._set(o.default_value)
173 map(apply, self.callbacks)
174 for option in self:
175 option.has_changed = 0
177 def __iter__(self):
178 return self.options.itervalues()