comparison frontends/src/tools/xmlui.py @ 1106:e2e1e27a3680

frontends: XMLUI refactoring + dialogs: - there are now XMLUIPanel and XMLUIDialog both inheriting from XMLUIBase - following dialogs are managed: - MessageDialog - NoteDialog - ConfirmDialog - FileDialog - XMLUI creation is now made using xmlui.create(...) instead of instanciating directly XMLUI - classes must be registed in frontends - "parent" attribute renamed to "_xmlui_parent" to avoid name conflicts with frontends toolkits
author Goffi <goffi@goffi.org>
date Wed, 13 Aug 2014 14:48:49 +0200
parents b3b7a2863060
children 0a448c947038
comparison
equal deleted inserted replaced
1105:018bdd687747 1106:e2e1e27a3680
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 from sat.core.i18n import _ 20 from sat.core.i18n import _
21 from sat.core.log import getLogger 21 from sat.core.log import getLogger
22 log = getLogger(__name__) 22 log = getLogger(__name__)
23 from sat_frontends.constants import Const 23 from sat_frontends.constants import Const as C
24 from sat.core.exceptions import DataError 24 from sat.core.exceptions import DataError
25 25
26
27 class_map = {}
28 CLASS_PANEL = 'panel'
29 CLASS_DIALOG = 'dialog'
26 30
27 class InvalidXMLUI(Exception): 31 class InvalidXMLUI(Exception):
28 pass 32 pass
29 33
34
35 class ClassNotRegistedError(Exception):
36 pass
30 37
31 def getText(node): 38 def getText(node):
32 """Get child text nodes 39 """Get child text nodes
33 @param node: dom Node 40 @param node: dom Node
34 @return: joined unicode text of all nodes 41 @return: joined unicode text of all nodes
40 data.append(child.wholeText) 47 data.append(child.wholeText)
41 return u"".join(data) 48 return u"".join(data)
42 49
43 50
44 class Widget(object): 51 class Widget(object):
45 """ base Widget """ 52 """base Widget"""
46 pass 53 pass
47 54
48 55
49 class EmptyWidget(Widget): 56 class EmptyWidget(Widget):
50 """ Just a placeholder widget """ 57 """Just a placeholder widget"""
51 pass 58 pass
52 59
53 60
54 class TextWidget(Widget): 61 class TextWidget(Widget):
55 """ Non interactive text """ 62 """Non interactive text"""
56 pass 63 pass
57 64
58 65
59 class LabelWidget(Widget): 66 class LabelWidget(Widget):
60 """ Non interactive text """ 67 """Non interactive text"""
61 pass 68 pass
62 69
63 70
64 class JidWidget(Widget): 71 class JidWidget(Widget):
65 """ Jabber ID """ 72 """Jabber ID"""
66 pass 73 pass
67 74
68 75
69 class DividerWidget(Widget): 76 class DividerWidget(Widget):
70 """ Separator """ 77 """Separator"""
71 pass 78 pass
72 79
73 80
74 class StringWidget(Widget): 81 class StringWidget(Widget):
75 """ Input widget with require a string 82 """Input widget wich require a string
83
76 often called Edit in toolkits 84 often called Edit in toolkits
77
78 """ 85 """
79 86
80 87
81 class PasswordWidget(Widget): 88 class PasswordWidget(Widget):
82 """ Input widget with require a masked string 89 """Input widget with require a masked string"""
83
84 """
85 90
86 91
87 class TextBoxWidget(Widget): 92 class TextBoxWidget(Widget):
88 """ Input widget with require a long, possibly multilines string 93 """Input widget with require a long, possibly multilines string
89 often called TextArea in toolkits 94 often called TextArea in toolkits
90
91 """ 95 """
92 96
93 97
94 class BoolWidget(Widget): 98 class BoolWidget(Widget):
95 """ Input widget with require a boolean value 99 """Input widget with require a boolean value
96 often called CheckBox in toolkits 100 often called CheckBox in toolkits
97
98 """ 101 """
99 102
100 103
101 class ButtonWidget(Widget): 104 class ButtonWidget(Widget):
102 """ A clickable widget """ 105 """A clickable widget"""
103 106
104 107
105 class ListWidget(Widget): 108 class ListWidget(Widget):
106 """ A widget able to show/choose one or several strings in a list """ 109 """A widget able to show/choose one or several strings in a list"""
107 110
108 111
109 class Container(Widget): 112 class Container(Widget):
110 """ Widget which can contain other ones with a specific layout """ 113 """Widget which can contain other ones with a specific layout"""
111 114
112 @classmethod 115 @classmethod
113 def _xmluiAdapt(cls, instance): 116 def _xmluiAdapt(cls, instance):
114 """ Make cls as instance.__class__ 117 """Make cls as instance.__class__
118
115 cls must inherit from original instance class 119 cls must inherit from original instance class
116 Usefull when you get a class from UI toolkit 120 Usefull when you get a class from UI toolkit
117
118 """ 121 """
119 assert instance.__class__ in cls.__bases__ 122 assert instance.__class__ in cls.__bases__
120 instance.__class__ = type(cls.__name__, cls.__bases__, dict(cls.__dict__)) 123 instance.__class__ = type(cls.__name__, cls.__bases__, dict(cls.__dict__))
121 124
122 125
123 class PairsContainer(Container): 126 class PairsContainer(Container):
124 """ Widgets are disposed in rows of two (usually label/input) """ 127 """Widgets are disposed in rows of two (usually label/input) """
125 pass
126 128
127 129
128 class TabsContainer(Container): 130 class TabsContainer(Container):
129 """ A container which several other containers in tabs 131 """A container which several other containers in tabs
132
130 Often called Notebook in toolkits 133 Often called Notebook in toolkits
131 134 """
132 """
133
134 135
135 class VerticalContainer(Container): 136 class VerticalContainer(Container):
136 """ Widgets are disposed vertically """ 137 """Widgets are disposed vertically"""
137 pass
138 138
139 139
140 class AdvancedListContainer(Container): 140 class AdvancedListContainer(Container):
141 """ Widgets are disposed in rows with advaned features """ 141 """Widgets are disposed in rows with advaned features"""
142 pass 142
143 143
144 144 class Dialog(object):
145 class XMLUI(object): 145 """base dialog"""
146 """ Base class to construct SàT XML User Interface 146
147 def __init__(self, _xmlui_parent):
148 self._xmlui_parent = _xmlui_parent
149
150 def _xmluiValidated(self, data=None):
151 if data is None:
152 data = {}
153 self._xmluiSetData(C.XMLUI_STATUS_VALIDATED, data)
154 self._xmluiSubmit(data)
155 self._xmluiClose()
156
157 def _xmluiCancelled(self):
158 data = {C.XMLUI_DATA_CANCELLED: C.BOOL_TRUE}
159 self._xmluiSetData(C.XMLUI_STATUS_CANCELLED, data)
160 self._xmluiSubmit(data)
161 self._xmluiClose()
162
163 def _xmluiSubmit(self, data):
164 self._xmlui_parent.submit(data)
165
166 def _xmluiSetData(self, status, data):
167 pass
168
169
170 class MessageDialog(Dialog):
171 """Dialog with a OK/Cancel type configuration"""
172
173
174 class NoteDialog(Dialog):
175 """Dialog with a OK/Cancel type configuration"""
176
177
178 class ConfirmDialog(Dialog):
179 """Dialog with a OK/Cancel type configuration"""
180
181 def _xmluiSetData(self, status, data):
182 if status == C.XMLUI_STATUS_VALIDATED:
183 data[C.XMLUI_DATA_ANSWER] = C.BOOL_TRUE
184 elif status == C.XMLUI_STATUS_CANCELLED:
185 data[C.XMLUI_DATA_ANSWER] = C.BOOL_FALSE
186
187
188 class FileDialog(Dialog):
189 """Dialog with a OK/Cancel type configuration"""
190
191
192 class XMLUIBase(object):
193 """Base class to construct SàT XML User Interface
194
195 This class must not be instancied directly
196 """
197
198 def __init__(self, host, parsed_dom, title = None, flags = None):
199 """Initialise the XMLUI instance
200
201 @param host: %(doc_host)s
202 @param parsed_dom: main parsed dom
203 @param title: force the title, or use XMLUI one if None
204 @param flags: list of string which can be:
205 - NO_CANCEL: the UI can't be cancelled
206 """
207 self.host = host
208 top=parsed_dom.documentElement
209 self.session_id = top.getAttribute("session_id") or None
210 self.submit_id = top.getAttribute("submit") or None
211 self.title = title or top.getAttribute("title") or u""
212 if flags is None:
213 flags = []
214 self.flags = flags
215
216 def _isAttrSet(self, name, node):
217 """Returnw widget boolean attribute status
218
219 @param name: name of the attribute (e.g. "read_only")
220 @param node: Node instance
221 @return (bool): True if widget's attribute is set (C.BOOL_TRUE)
222 """
223 read_only = node.getAttribute(name) or C.BOOL_FALSE
224 return read_only.lower().strip() == C.BOOL_TRUE
225
226 def _getChildNode(self, node, name):
227 """Return the first child node with the given name
228
229 @param node: Node instance
230 @param name: name of the wanted node
231
232 @return: The found element or None
233 """
234 for child in node.childNodes:
235 if child.nodeName == name:
236 return child
237 return None
238
239 def submit(self, data):
240 if self.submit_id is None:
241 raise ValueError("Can't submit is self.submit_id is not set")
242 if "session_id" in data:
243 raise ValueError("session_id must no be used in data, it is automaticaly filled with self.session_id if present")
244 if self.session_id is not None:
245 data["session_id"] = self.session_id
246 self._xmluiLaunchAction(self.submit_id, data)
247
248 def _xmluiLaunchAction(self, action_id, data):
249 self.host.launchAction(action_id, data, profile_key = self.host.profile)
250
251
252 class XMLUIPanel(XMLUIBase):
253 """XMLUI Panel
254
147 New frontends can inherite this class to easily implement XMLUI 255 New frontends can inherite this class to easily implement XMLUI
148 @property widget_factory: factory to create frontend-specific widgets 256 @property widget_factory: factory to create frontend-specific widgets
149 @proporety dialog_factory: factory to create frontend-specific dialogs 257 @proporety dialog_factory: factory to create frontend-specific dialogs
150
151 """ 258 """
152 widget_factory = None 259 widget_factory = None
153 dialog_factory = None # TODO 260
154 261 def __init__(self, host, parsed_dom, title = None, flags = None):
155 def __init__(self, host, xml_data, title = None, flags = None, dom_parse=None, dom_free=None): 262 super(XMLUIPanel, self).__init__(host, parsed_dom, title = None, flags = None)
156 """ Initialise the XMLUI instance
157 @param host: %(doc_host)s
158 @param xml_data: the raw XML containing the UI
159 @param title: force the title, or use XMLUI one if None
160 @param flags: list of string which can be:
161 - NO_CANCEL: the UI can't be cancelled
162 @param dom_parse: methode equivalent to minidom.parseString (but which must manage unicode), or None to use default one
163 @param dom_free: method used to free the parsed DOM
164
165 """
166 if dom_parse is None:
167 from xml.dom import minidom
168 self.dom_parse = lambda xml_data: minidom.parseString(xml_data.encode('utf-8'))
169 self.dom_free = lambda cat_dom: cat_dom.unlink()
170 else:
171 self.dom_parse = dom_parse
172 self.dom_free = dom_free or (lambda cat_dom: None)
173 self.host = host
174 self.title = title or ""
175 if flags is None:
176 flags = []
177 self.flags = flags
178 self.ctrl_list = {} # usefull to access ctrl 263 self.ctrl_list = {} # usefull to access ctrl
179 self._main_cont = None 264 self._main_cont = None
180 self.constructUI(xml_data) 265 self.constructUI(parsed_dom)
181 266
182 def escape(self, name): 267 def escape(self, name):
183 """ return escaped name for forms """ 268 """Return escaped name for forms"""
184 return u"%s%s" % (Const.SAT_FORM_PREFIX, name) 269 return u"%s%s" % (C.SAT_FORM_PREFIX, name)
185 270
186 @property 271 @property
187 def main_cont(self): 272 def main_cont(self):
188 return self._main_cont 273 return self._main_cont
189 274
191 def main_cont(self, value): 276 def main_cont(self, value):
192 if self._main_cont is not None: 277 if self._main_cont is not None:
193 raise ValueError(_("XMLUI can have only one main container")) 278 raise ValueError(_("XMLUI can have only one main container"))
194 self._main_cont = value 279 self._main_cont = value
195 280
196 def _isAttrSet(self, name, node): 281 def _parseChilds(self, _xmlui_parent, current_node, wanted = ('container',), data = None):
197 """Returnw widget boolean attribute status
198
199 @param name: name of the attribute (e.g. "read_only")
200 @param node: Node instance
201 @return (bool): True if widget's attribute is set ("true")
202 """
203 read_only = node.getAttribute(name) or "false"
204 return read_only.lower().strip() == "true"
205
206 def _getChildNode(self, node, name):
207 """Return the first child node with the given name
208
209 @param node: Node instance
210 @param name: name of the wanted node
211
212 @return: The found element or None
213 """
214 for child in node.childNodes:
215 if child.nodeName == name:
216 return child
217 return None
218
219 def _parseChilds(self, parent, current_node, wanted = ('container',), data = None):
220 """Recursively parse childNodes of an elemen 282 """Recursively parse childNodes of an elemen
221 @param parent: widget container with '_xmluiAppend' method 283
284 @param _xmlui_parent: widget container with '_xmluiAppend' method
222 @param current_node: element from which childs will be parsed 285 @param current_node: element from which childs will be parsed
223 @param wanted: list of tag names that can be present in the childs to be SàT XMLUI compliant 286 @param wanted: list of tag names that can be present in the childs to be SàT XMLUI compliant
224 @param data: additionnal data which are needed in some cases 287 @param data: additionnal data which are needed in some cases
225
226 """ 288 """
227 for node in current_node.childNodes: 289 for node in current_node.childNodes:
228 if wanted and not node.nodeName in wanted: 290 if wanted and not node.nodeName in wanted:
229 raise InvalidXMLUI('Unexpected node: [%s]' % node.nodeName) 291 raise InvalidXMLUI('Unexpected node: [%s]' % node.nodeName)
230 292
231 if node.nodeName == "container": 293 if node.nodeName == "container":
232 type_ = node.getAttribute('type') 294 type_ = node.getAttribute('type')
233 if parent is self and type_ != 'vertical': 295 if _xmlui_parent is self and type_ != 'vertical':
234 # main container is not a VerticalContainer and we want one, so we create one to wrap it 296 # main container is not a VerticalContainer and we want one, so we create one to wrap it
235 parent = self.widget_factory.createVerticalContainer(self) 297 _xmlui_parent = self.widget_factory.createVerticalContainer(self)
236 self.main_cont = parent 298 self.main_cont = _xmlui_parent
237 if type_ == "tabs": 299 if type_ == "tabs":
238 cont = self.widget_factory.createTabsContainer(parent) 300 cont = self.widget_factory.createTabsContainer(_xmlui_parent)
239 self._parseChilds(parent, node, ('tab',), cont) 301 self._parseChilds(_xmlui_parent, node, ('tab',), cont)
240 elif type_ == "vertical": 302 elif type_ == "vertical":
241 cont = self.widget_factory.createVerticalContainer(parent) 303 cont = self.widget_factory.createVerticalContainer(_xmlui_parent)
242 self._parseChilds(cont, node, ('widget', 'container')) 304 self._parseChilds(cont, node, ('widget', 'container'))
243 elif type_ == "pairs": 305 elif type_ == "pairs":
244 cont = self.widget_factory.createPairsContainer(parent) 306 cont = self.widget_factory.createPairsContainer(_xmlui_parent)
245 self._parseChilds(cont, node, ('widget', 'container')) 307 self._parseChilds(cont, node, ('widget', 'container'))
246 elif type_ == "advanced_list": 308 elif type_ == "advanced_list":
247 try: 309 try:
248 columns = int(node.getAttribute('columns')) 310 columns = int(node.getAttribute('columns'))
249 except (TypeError, ValueError): 311 except (TypeError, ValueError):
250 raise DataError("Invalid columns") 312 raise DataError("Invalid columns")
251 selectable = node.getAttribute('selectable') or 'no' 313 selectable = node.getAttribute('selectable') or 'no'
252 auto_index = node.getAttribute('auto_index') == 'true' 314 auto_index = node.getAttribute('auto_index') == C.BOOL_TRUE
253 data = {'index': 0} if auto_index else None 315 data = {'index': 0} if auto_index else None
254 cont = self.widget_factory.createAdvancedListContainer(parent, columns, selectable) 316 cont = self.widget_factory.createAdvancedListContainer(_xmlui_parent, columns, selectable)
255 callback_id = node.getAttribute("callback") or None 317 callback_id = node.getAttribute("callback") or None
256 if callback_id is not None: 318 if callback_id is not None:
257 if selectable == 'no': 319 if selectable == 'no':
258 raise ValueError("can't have selectable=='no' and callback_id at the same time") 320 raise ValueError("can't have selectable=='no' and callback_id at the same time")
259 cont._xmlui_callback_id = callback_id 321 cont._xmlui_callback_id = callback_id
260 cont._xmluiOnSelect(self.onAdvListSelect) 322 cont._xmluiOnSelect(self.onAdvListSelect)
261 323
262 self._parseChilds(cont, node, ('row',), data) 324 self._parseChilds(cont, node, ('row',), data)
263 else: 325 else:
264 log.warning(_("Unknown container [%s], using default one") % type_) 326 log.warning(_("Unknown container [%s], using default one") % type_)
265 cont = self.widget_factory.createVerticalContainer(parent) 327 cont = self.widget_factory.createVerticalContainer(_xmlui_parent)
266 self._parseChilds(cont, node, ('widget', 'container')) 328 self._parseChilds(cont, node, ('widget', 'container'))
267 try: 329 try:
268 parent._xmluiAppend(cont) 330 _xmlui_parent._xmluiAppend(cont)
269 except (AttributeError, TypeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError 331 except (AttributeError, TypeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
270 if parent is self: 332 if _xmlui_parent is self:
271 self.main_cont = cont 333 self.main_cont = cont
272 else: 334 else:
273 raise Exception(_("Internal Error, container has not _xmluiAppend method")) 335 raise Exception(_("Internal Error, container has not _xmluiAppend method"))
274 336
275 elif node.nodeName == 'tab': 337 elif node.nodeName == 'tab':
287 try: 349 try:
288 index = str(data['index']) 350 index = str(data['index'])
289 data['index'] += 1 351 data['index'] += 1
290 except TypeError: 352 except TypeError:
291 index = node.getAttribute('index') or None 353 index = node.getAttribute('index') or None
292 parent._xmluiAddRow(index) 354 _xmlui_parent._xmluiAddRow(index)
293 self._parseChilds(parent, node, ('widget', 'container')) 355 self._parseChilds(_xmlui_parent, node, ('widget', 'container'))
294 356
295 elif node.nodeName == "widget": 357 elif node.nodeName == "widget":
296 id_ = node.getAttribute("id")
297 name = node.getAttribute("name") 358 name = node.getAttribute("name")
298 type_ = node.getAttribute("type") 359 type_ = node.getAttribute("type")
299 value_elt = self._getChildNode(node, "value") 360 value_elt = self._getChildNode(node, "value")
300 if value_elt is not None: 361 if value_elt is not None:
301 value = getText(value_elt) 362 value = getText(value_elt)
302 else: 363 else:
303 value = node.getAttribute("value") if node.hasAttribute('value') else u'' 364 value = node.getAttribute("value") if node.hasAttribute('value') else u''
304 if type_=="empty": 365 if type_=="empty":
305 ctrl = self.widget_factory.createEmptyWidget(parent) 366 ctrl = self.widget_factory.createEmptyWidget(_xmlui_parent)
306 elif type_=="text": 367 elif type_=="text":
307 ctrl = self.widget_factory.createTextWidget(parent, value) 368 ctrl = self.widget_factory.createTextWidget(_xmlui_parent, value)
308 elif type_=="label": 369 elif type_=="label":
309 ctrl = self.widget_factory.createLabelWidget(parent, value) 370 ctrl = self.widget_factory.createLabelWidget(_xmlui_parent, value)
310 elif type_=="jid": 371 elif type_=="jid":
311 ctrl = self.widget_factory.createJidWidget(parent, value) 372 ctrl = self.widget_factory.createJidWidget(_xmlui_parent, value)
312 elif type_=="divider": 373 elif type_=="divider":
313 style = node.getAttribute("style") or 'line' 374 style = node.getAttribute("style") or 'line'
314 ctrl = self.widget_factory.createDividerWidget(parent, style) 375 ctrl = self.widget_factory.createDividerWidget(_xmlui_parent, style)
315 elif type_=="string": 376 elif type_=="string":
316 ctrl = self.widget_factory.createStringWidget(parent, value, self._isAttrSet("read_only", node)) 377 ctrl = self.widget_factory.createStringWidget(_xmlui_parent, value, self._isAttrSet("read_only", node))
317 self.ctrl_list[name] = ({'type':type_, 'control':ctrl}) 378 self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
318 elif type_=="password": 379 elif type_=="password":
319 ctrl = self.widget_factory.createPasswordWidget(parent, value, self._isAttrSet("read_only", node)) 380 ctrl = self.widget_factory.createPasswordWidget(_xmlui_parent, value, self._isAttrSet("read_only", node))
320 self.ctrl_list[name] = ({'type':type_, 'control':ctrl}) 381 self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
321 elif type_=="textbox": 382 elif type_=="textbox":
322 ctrl = self.widget_factory.createTextBoxWidget(parent, value, self._isAttrSet("read_only", node)) 383 ctrl = self.widget_factory.createTextBoxWidget(_xmlui_parent, value, self._isAttrSet("read_only", node))
323 self.ctrl_list[name] = ({'type':type_, 'control':ctrl}) 384 self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
324 elif type_=="bool": 385 elif type_=="bool":
325 ctrl = self.widget_factory.createBoolWidget(parent, value=='true', self._isAttrSet("read_only", node)) 386 ctrl = self.widget_factory.createBoolWidget(_xmlui_parent, value==C.BOOL_TRUE, self._isAttrSet("read_only", node))
326 self.ctrl_list[name] = ({'type':type_, 'control':ctrl}) 387 self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
327 elif type_ == "list": 388 elif type_ == "list":
328 style = [] if node.getAttribute("multi") == 'yes' else ['single'] 389 style = [] if node.getAttribute("multi") == 'yes' else ['single']
329 _options = [(option.getAttribute("value"), option.getAttribute("label")) for option in node.getElementsByTagName("option")] 390 _options = [(option.getAttribute("value"), option.getAttribute("label")) for option in node.getElementsByTagName("option")]
330 _selected = [option.getAttribute("value") for option in node.getElementsByTagName("option") if option.getAttribute('selected') == 'true'] 391 _selected = [option.getAttribute("value") for option in node.getElementsByTagName("option") if option.getAttribute('selected') == C.BOOL_TRUE]
331 ctrl = self.widget_factory.createListWidget(parent, _options, _selected, style) 392 ctrl = self.widget_factory.createListWidget(_xmlui_parent, _options, _selected, style)
332 self.ctrl_list[name] = ({'type': type_, 'control': ctrl}) 393 self.ctrl_list[name] = ({'type': type_, 'control': ctrl})
333 elif type_=="button": 394 elif type_=="button":
334 callback_id = node.getAttribute("callback") 395 callback_id = node.getAttribute("callback")
335 ctrl = self.widget_factory.createButtonWidget(parent, value, self.onButtonPress) 396 ctrl = self.widget_factory.createButtonWidget(_xmlui_parent, value, self.onButtonPress)
336 ctrl._xmlui_param_id = (callback_id, [field.getAttribute('name') for field in node.getElementsByTagName("field_back")]) 397 ctrl._xmlui_param_id = (callback_id, [field.getAttribute('name') for field in node.getElementsByTagName("field_back")])
337 else: 398 else:
338 log.error(_("FIXME FIXME FIXME: widget type [%s] is not implemented") % type_) 399 log.error(_("FIXME FIXME FIXME: widget type [%s] is not implemented") % type_)
339 raise NotImplementedError(_("FIXME FIXME FIXME: type [%s] is not implemented") % type_) 400 raise NotImplementedError(_("FIXME FIXME FIXME: type [%s] is not implemented") % type_)
340 401
356 ctrl._xmluiOnClick(self.onChangeInternal) 417 ctrl._xmluiOnClick(self.onChangeInternal)
357 else: 418 else:
358 ctrl._xmluiOnChange(self.onChangeInternal) 419 ctrl._xmluiOnChange(self.onChangeInternal)
359 420
360 ctrl._xmlui_name = name 421 ctrl._xmlui_name = name
361 parent._xmluiAppend(ctrl) 422 _xmlui_parent._xmluiAppend(ctrl)
362 423
363 else: 424 else:
364 raise NotImplementedError(_('Unknown tag [%s]') % node.nodeName) 425 raise NotImplementedError(_('Unknown tag [%s]') % node.nodeName)
365 426
366 def constructUI(self, xml_data, post_treat=None): 427 def constructUI(self, parsed_dom, post_treat=None):
367 """ Actually construct the UI 428 """Actually construct the UI
368 @param xml_data: raw XMLUI 429
430 @param parsed_dom: main parsed dom
369 @param post_treat: frontend specific treatments to do once the UI is constructed 431 @param post_treat: frontend specific treatments to do once the UI is constructed
370 @return: constructed widget 432 @return: constructed widget
371 """ 433 """
372 cat_dom = self.dom_parse(xml_data) 434 top=parsed_dom.documentElement
373 top=cat_dom.documentElement
374 self.type = top.getAttribute("type") 435 self.type = top.getAttribute("type")
375 self.title = self.title or top.getAttribute("title") or u""
376 self.session_id = top.getAttribute("session_id") or None
377 self.submit_id = top.getAttribute("submit") or None
378 if top.nodeName != "sat_xmlui" or not self.type in ['form', 'param', 'window', 'popup']: 436 if top.nodeName != "sat_xmlui" or not self.type in ['form', 'param', 'window', 'popup']:
379 raise InvalidXMLUI 437 raise InvalidXMLUI
380 438
381 if self.type == 'param': 439 if self.type == 'param':
382 self.param_changed = set() 440 self.param_changed = set()
383 441
384 self._parseChilds(self, cat_dom.documentElement) 442 self._parseChilds(self, parsed_dom.documentElement)
385 443
386 if post_treat is not None: 444 if post_treat is not None:
387 post_treat() 445 post_treat()
388 446
389 self.dom_free(cat_dom)
390
391 def _xmluiClose(self): 447 def _xmluiClose(self):
392 """ Close the window/popup/... where the constructeur XMLUI is 448 """Close the window/popup/... where the constructeur XMLUI is
449
393 this method must be overrided 450 this method must be overrided
394
395 """ 451 """
396 raise NotImplementedError 452 raise NotImplementedError
397
398 def _xmluiLaunchAction(self, action_id, data):
399 self.host.launchAction(action_id, data, profile_key = self.host.profile)
400 453
401 def _xmluiSetParam(self, name, value, category): 454 def _xmluiSetParam(self, name, value, category):
402 self.host.bridge.setParam(name, value, category, profile_key=self.host.profile) 455 self.host.bridge.setParam(name, value, category, profile_key=self.host.profile)
403 456
404 ##EVENTS## 457 ##EVENTS##
405 458
406 def onParamChange(self, ctrl): 459 def onParamChange(self, ctrl):
407 """ Called when type is param and a widget to save is modified 460 """Called when type is param and a widget to save is modified
461
408 @param ctrl: widget modified 462 @param ctrl: widget modified
409
410 """ 463 """
411 assert(self.type == "param") 464 assert(self.type == "param")
412 self.param_changed.add(ctrl) 465 self.param_changed.add(ctrl)
413 466
414 def onAdvListSelect(self, ctrl): 467 def onAdvListSelect(self, ctrl):
429 log.info(_("No callback_id found")) 482 log.info(_("No callback_id found"))
430 return 483 return
431 self._xmluiLaunchAction(callback_id, data) 484 self._xmluiLaunchAction(callback_id, data)
432 485
433 def onButtonPress(self, button): 486 def onButtonPress(self, button):
434 """ Called when an XMLUI button is clicked 487 """Called when an XMLUI button is clicked
488
435 Launch the action associated to the button 489 Launch the action associated to the button
436 @param button: the button clicked 490 @param button: the button clicked
437
438 """ 491 """
439 callback_id, fields = button._xmlui_param_id 492 callback_id, fields = button._xmlui_param_id
440 if not callback_id: # the button is probably bound to an internal action 493 if not callback_id: # the button is probably bound to an internal action
441 return 494 return
442 data = {} 495 data = {}
448 else: 501 else:
449 data[escaped] = ctrl['control']._xmluiGetValue() 502 data[escaped] = ctrl['control']._xmluiGetValue()
450 self._xmluiLaunchAction(callback_id, data) 503 self._xmluiLaunchAction(callback_id, data)
451 504
452 def onChangeInternal(self, ctrl): 505 def onChangeInternal(self, ctrl):
453 """ Called when a widget that has been bound to an internal callback is changed. 506 """Called when a widget that has been bound to an internal callback is changed.
454 507
455 This is used to perform some UI actions without communicating with the backend. 508 This is used to perform some UI actions without communicating with the backend.
456 See sat.tools.xml_tools.Widget.setInternalCallback for more details. 509 See sat.tools.xml_tools.Widget.setInternalCallback for more details.
457 @param ctrl: widget modified 510 @param ctrl: widget modified
458 """ 511 """
505 source = None 558 source = None
506 559
507 def getInternalCallbackData(self, action, node): 560 def getInternalCallbackData(self, action, node):
508 """Retrieve from node the data needed to perform given action. 561 """Retrieve from node the data needed to perform given action.
509 562
510 TODO: it would be better to not have a specific way to retrieve
511 data for each action, but instead to have a generic method to
512 extract any kind of data structure from the 'internal_data' element.
513
514 @param action (string): a value from the one that can be passed to the 563 @param action (string): a value from the one that can be passed to the
515 'callback' parameter of sat.tools.xml_tools.Widget.setInternalCallback 564 'callback' parameter of sat.tools.xml_tools.Widget.setInternalCallback
516 @param node (DOM Element): the node of the widget that triggers the callback 565 @param node (DOM Element): the node of the widget that triggers the callback
517 """ 566 """
567 # TODO: it would be better to not have a specific way to retrieve
568 # data for each action, but instead to have a generic method to
569 # extract any kind of data structure from the 'internal_data' element.
570
518 try: # data is stored in the first 'internal_data' element of the node 571 try: # data is stored in the first 'internal_data' element of the node
519 data_elts = node.getElementsByTagName('internal_data')[0].childNodes 572 data_elts = node.getElementsByTagName('internal_data')[0].childNodes
520 except IndexError: 573 except IndexError:
521 return None 574 return None
522 data = {} 575 data = {}
527 for value_elt in elt.childNodes: 580 for value_elt in elt.childNodes:
528 data[jid_s].append(value_elt.getAttribute('name')) 581 data[jid_s].append(value_elt.getAttribute('name'))
529 return data 582 return data
530 583
531 def onFormSubmitted(self, ignore=None): 584 def onFormSubmitted(self, ignore=None):
532 """ An XMLUI form has been submited 585 """An XMLUI form has been submited
586
533 call the submit action associated with this form 587 call the submit action associated with this form
534
535 """ 588 """
536 selected_values = [] 589 selected_values = []
537 for ctrl_name in self.ctrl_list: 590 for ctrl_name in self.ctrl_list:
538 escaped = self.escape(ctrl_name) 591 escaped = self.escape(ctrl_name)
539 ctrl = self.ctrl_list[ctrl_name] 592 ctrl = self.ctrl_list[ctrl_name]
541 selected_values.append((escaped, u'\t'.join(ctrl['control']._xmluiGetSelectedValues()))) 594 selected_values.append((escaped, u'\t'.join(ctrl['control']._xmluiGetSelectedValues())))
542 else: 595 else:
543 selected_values.append((escaped, ctrl['control']._xmluiGetValue())) 596 selected_values.append((escaped, ctrl['control']._xmluiGetValue()))
544 if self.submit_id is not None: 597 if self.submit_id is not None:
545 data = dict(selected_values) 598 data = dict(selected_values)
546 if self.session_id is not None: 599 self.submit(data)
547 data["session_id"] = self.session_id
548 self._xmluiLaunchAction(self.submit_id, data)
549
550 else: 600 else:
551 log.warning(_("The form data is not sent back, the type is not managed properly")) 601 log.warning(_("The form data is not sent back, the type is not managed properly"))
552 self._xmluiClose() 602 self._xmluiClose()
553 603
554 def onFormCancelled(self, ignore=None): 604 def onFormCancelled(self, ignore=None):
555 """ Called when a form is cancelled """ 605 """Called when a form is cancelled"""
556 log.debug(_("Cancelling form")) 606 log.debug(_("Cancelling form"))
557 self._xmluiClose() 607 self._xmluiClose()
558 608
559 def onSaveParams(self, ignore=None): 609 def onSaveParams(self, ignore=None):
560 """ Params are saved, we send them to backend 610 """Params are saved, we send them to backend
611
561 self.type must be param 612 self.type must be param
562
563 """ 613 """
564 assert(self.type == 'param') 614 assert(self.type == 'param')
565 for ctrl in self.param_changed: 615 for ctrl in self.param_changed:
566 if isinstance(ctrl, ListWidget): 616 if isinstance(ctrl, ListWidget):
567 value = u'\t'.join(ctrl._xmluiGetSelectedValues()) 617 value = u'\t'.join(ctrl._xmluiGetSelectedValues())
568 else: 618 else:
569 value = ctrl._xmluiGetValue() 619 value = ctrl._xmluiGetValue()
570 param_name = ctrl._xmlui_name.split(Const.SAT_PARAM_SEPARATOR)[1] 620 param_name = ctrl._xmlui_name.split(C.SAT_PARAM_SEPARATOR)[1]
571 self._xmluiSetParam(param_name, value, ctrl._param_category) 621 self._xmluiSetParam(param_name, value, ctrl._param_category)
572 622
573 self._xmluiClose() 623 self._xmluiClose()
624
625 def show(self, *args, **kwargs):
626 pass
627
628
629 class XMLUIDialog(XMLUIBase):
630 dialog_factory = None
631
632 def __init__(self, host, parsed_dom, title = None, flags = None):
633 super(XMLUIDialog, self).__init__(host, parsed_dom, title = None, flags = None)
634 top=parsed_dom.documentElement
635 dlg_elt = self._getChildNode(top, "dialog")
636 if dlg_elt is None:
637 raise ValueError("Invalid XMLUI: no Dialog element found !")
638 dlg_type = dlg_elt.getAttribute("type") or C.XMLUI_DIALOG_MESSAGE
639 try:
640 mess_elt = self._getChildNode(dlg_elt, C.XMLUI_DATA_MESS)
641 message = getText(mess_elt)
642 except (TypeError, AttributeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
643 message = ""
644 level = dlg_elt.getAttribute(C.XMLUI_DATA_LVL) or C.XMLUI_DATA_LVL_INFO
645
646 if dlg_type == C.XMLUI_DIALOG_MESSAGE:
647 self.dlg = self.dialog_factory.createMessageDialog(self, self.title, message, level)
648 elif dlg_type == C.XMLUI_DIALOG_NOTE:
649 self.dlg = self.dialog_factory.createNoteDialog(self, self.title, message, level)
650 elif dlg_type == C.XMLUI_DIALOG_CONFIRM:
651 try:
652 buttons_elt = self._getChildNode(dlg_elt, "buttons")
653 buttons_set = buttons_elt.getAttribute("set") or C.XMLUI_DATA_BTNS_SET_DEFAULT
654 except (TypeError, AttributeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
655 buttons_set = C.XMLUI_DATA_BTNS_SET_DEFAULT
656 self.dlg = self.dialog_factory.createConfirmDialog(self, self.title, message, level, buttons_set)
657 elif dlg_type == C.XMLUI_DIALOG_FILE:
658 try:
659 file_elt = self._getChildNode(dlg_elt, "file")
660 filetype = file_elt.getAttribute("type") or C.XMLUI_DATA_FILETYPE_DEFAULT
661 except (TypeError, AttributeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
662 filetype = C.XMLUI_DATA_FILETYPE_DEFAULT
663 self.dlg = self.dialog_factory.createFileDialog(self, self.title, message, level, filetype)
664 else:
665 raise ValueError("Unknown dialog type [%s]" % dlg_type)
666
667 def show(self):
668 self.dlg._xmluiShow()
669
670
671 def registerClass(type_, class_):
672 """Register the class to use with the factory
673
674 @param type_: one of:
675 CLASS_PANEL: classical XMLUI interface
676 CLASS_DIALOG: XMLUI dialog
677 @param class_: the class to use to instanciate given type
678 """
679 assert type_ in (CLASS_PANEL, CLASS_DIALOG)
680 class_map[type_] = class_
681
682
683 def create(host, xml_data, title = None, flags = None, dom_parse=None, dom_free=None):
684 """
685 @param dom_parse: methode equivalent to minidom.parseString (but which must manage unicode), or None to use default one
686 @param dom_free: method used to free the parsed DOM
687 """
688 if dom_parse is None:
689 from xml.dom import minidom
690 dom_parse = lambda xml_data: minidom.parseString(xml_data.encode('utf-8'))
691 dom_free = lambda parsed_dom: parsed_dom.unlink()
692 else:
693 dom_parse = dom_parse
694 dom_free = dom_free or (lambda parsed_dom: None)
695 parsed_dom = dom_parse(xml_data)
696 top=parsed_dom.documentElement
697 ui_type = top.getAttribute("type")
698 try:
699 if ui_type != C.XMLUI_DIALOG:
700 cls = class_map[CLASS_PANEL]
701 else:
702 cls = class_map[CLASS_DIALOG]
703 except KeyError:
704 raise ClassNotRegistedError(_("You must register classes with registerClass before creating a XMLUI"))
705
706 xmlui = cls(host, parsed_dom, title, flags)
707 dom_free(parsed_dom)
708 return xmlui