Use XMLRPC if the DBUS version is too old.
[rox-lib/lack.git] / python / rox / options.py
blobe6c129c55fa9d18a201088eca5eba6b2ea2d41ee
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)
11 icons = ListOption('icons', ('circle', 'square', 'triangle'), options)
13 3. Register any callbacks (notification of options changing):
14 def my_callback():
15 if colour.has_changed:
16 print "The colour is now", colour.value
17 options.add_notify(my_callback)
19 4. Notify any changes from defaults:
20 options.notify()
22 See OptionsBox for editing options. Do not change the value of options
23 yourself.
24 """
26 from __future__ import generators
27 import os
29 import rox
30 from rox import choices, basedir
32 from xml.dom import Node, minidom
34 def data(node):
35 """Return all the text directly inside this DOM Node."""
36 return ''.join([text.nodeValue for text in node.childNodes
37 if text.nodeType == Node.TEXT_NODE])
39 class Option:
40 """An Option stores a single value. Every option is part of exactly one OptionGroup.
42 The read-only attributes value and int_value can be used to get the current setting
43 for the Option. int_value will be -1 if the value is not a valid integer.
45 The has_changed attribute is used during notify() calls to indicate whether this
46 Option's value has changed since the last notify (or option creation).
47 You may set has_changed = 1 right after creating an option if you want to force
48 notification the first time even if the default is used.
49 """
50 def __init__(self, name, value, group = None):
51 """Create a new option with this name and default value.
52 Add to 'group', or to rox.app_options if no group is given.
53 The value cannot be used until the first notify() call to
54 the group."""
55 if not group:
56 assert rox.app_options
57 group = rox.app_options
58 self.name = name
59 self.has_changed = 0 # ... since last notify/default
60 self.default_value = str(value)
61 self.group = group
62 self.value = None
63 self.int_value = None
64 self.store = True
66 self.group._register(self)
68 def _set(self, value):
69 if self.value != value:
70 self.value = str(value)
71 self.has_changed = 1
72 try:
73 if self.value == 'True':
74 self.int_value = 1
75 elif self.value == 'False':
76 self.int_value = 0
77 else:
78 self.int_value = int(float(self.value))
79 except:
80 self.int_value = -1
82 def _to_xml(self, parent):
83 doc = parent.ownerDocument
84 node = doc.createElement('Option')
85 node.setAttribute('name', self.name)
86 node.appendChild(doc.createTextNode(self.value))
87 parent.appendChild(node)
89 def __str__(self):
90 return "<Option %s=%s>" % (self.name, self.value)
92 class ListOption(Option):
93 """A ListOption stores a list of values. Every option is part of exactly one OptionGroup.
95 The read-only attribute list_value can be used to get the current setting
96 for the ListOption. value will be str(list_value) and int_value wille be -1.
98 The has_changed attribute is used during notify() calls to indicate whether this
99 ListOption's value has changed since the last notify (or option creation).
100 You may set has_changed = 1 right after creating an option if you want to force
101 notification the first time even if the default is used.
103 def __init__(self, name, value, group = None):
104 self.list_value=None
105 rox.options.Option.__init__(self, name, value, group)
107 self.default_value=value
109 def _set(self, value):
110 try:
111 tmp=len(value)
112 except:
113 rox.options.Option._set(self, value)
114 return
115 if hasattr(value, 'upper'):
116 # Assume it's a string
117 rox.options.Option._set(self, value)
118 return
120 if self.list_value!=value:
121 self.list_value=list(value)
122 self.value=str(value)
123 self.int_value=-1
124 self.has_changed=1
126 def _to_xml(self, parent):
127 doc = parent.ownerDocument
128 node = doc.createElement('ListOption')
129 node.setAttribute('name', self.name)
130 if self.list_value:
131 for v in self.list_value:
132 snode=doc.createElement('Value')
133 snode.appendChild(doc.createTextNode(v))
134 node.appendChild(snode)
135 parent.appendChild(node)
137 def __str__(self):
138 return "<ListOption %s=%s>" % (self.name, self.list_value)
140 class OptionGroup:
141 def __init__(self, program, leaf, site = None):
142 """program/leaf is a Choices pair for the saved options. If site
143 is given, the basedir module is used for saving choices (the new system).
144 Otherwise, the deprecated choices module is used."""
145 self.site = site
146 self.program = program
147 self.leaf = leaf
148 self.pending = {} # Loaded, but not registered
149 self.options = {} # Name -> Option
150 self.callbacks = []
151 self.too_late_for_registrations = 0
153 if site:
154 path = basedir.load_first_config(site, program, leaf)
155 else:
156 path = choices.load(program, leaf)
157 if not path:
158 return
160 try:
161 doc = minidom.parse(path)
163 root = doc.documentElement
164 assert root.localName == 'Options'
165 for o in root.childNodes:
166 if o.nodeType != Node.ELEMENT_NODE:
167 continue
168 if o.localName == 'Option':
169 name = o.getAttribute('name')
170 self.pending[name] = data(o)
171 elif o.localName=='ListOption':
172 name = o.getAttribute('name')
173 v=[]
174 for s in o.getElementsByTagName('Value'):
175 v.append(data(s))
176 self.pending[name]=v
177 else:
178 print "Warning: Non Option element", o
179 except:
180 rox.report_exception()
182 def _register(self, option):
183 """Called by Option.__init__."""
184 assert option.name not in self.options
185 assert not self.too_late_for_registrations
187 name = option.name
189 self.options[name] = option
191 if name in self.pending:
192 option._set(self.pending[name])
193 del self.pending[name]
195 def save(self):
196 """Save all option values. Usually called by OptionsBox()."""
197 assert self.too_late_for_registrations
199 if self.site:
200 d = basedir.save_config_path(self.site, self.program)
201 path = os.path.join(d, self.leaf)
202 else:
203 path = choices.save(self.program, self.leaf)
204 if not path:
205 return # Saving is disabled
207 from xml.dom.minidom import Document
208 doc = Document()
209 root = doc.createElement('Options')
210 doc.appendChild(root)
212 for option in self:
213 if option.store:
214 option._to_xml(root)
216 stream = open(path, 'w')
217 doc.writexml(stream)
218 stream.close()
220 def add_notify(self, callback):
221 "Call callback() after one or more options have changed value."
222 assert callback not in self.callbacks
224 self.callbacks.append(callback)
226 def remove_notify(self, callback):
227 """Remove a callback added with add_notify()."""
228 self.callbacks.remove(callback)
230 def notify(self, warn_unused=True):
231 """Call this after creating any new options or changing their values."""
232 if not self.too_late_for_registrations:
233 self.too_late_for_registrations = 1
234 if self.pending and warn_unused:
235 print "Warning: Some options loaded but unused:"
236 for (key, value) in self.pending.iteritems():
237 print "%s=%s" % (key, value)
238 for o in self:
239 if o.value is None:
240 o._set(o.default_value)
241 map(apply, self.callbacks)
242 for option in self:
243 option.has_changed = 0
245 def __iter__(self):
246 return self.options.itervalues()