comparison frontends/src/primitivus/xmlui.py @ 796:46aa5ada61bf

core, frontends: XMLUI refactoring: - now there is a base XMLUI class. Frontends inherits from this class, and add their specific widgets/containers/behaviour - wix: param.py has been removed, as the same behaviour has been reimplemented through XMLUI - removed "misc" argument in XMLUI.__init__ - severals things are broken (gateway, directory search), following patches will fix them - core: elements names in xml_tools.dataForm2XMLUI now use a construction with category_name/param_name to avoid name conflicts (could happen with 2 parameters of the same name in differents categories).
author Goffi <goffi@goffi.org>
date Tue, 04 Feb 2014 18:02:35 +0100
parents bfabeedbf32e
children 9007bb133009
comparison
equal deleted inserted replaced
795:6625558371db 796:46aa5ada61bf
20 from sat.core.i18n import _ 20 from sat.core.i18n import _
21 import urwid 21 import urwid
22 from urwid_satext import sat_widgets 22 from urwid_satext import sat_widgets
23 from logging import debug, info, warning, error 23 from logging import debug, info, warning, error
24 from xml.dom import minidom 24 from xml.dom import minidom
25 25 from sat_frontends.tools import xmlui
26 26
27 SAT_FORM_PREFIX = "SAT_FORM_" 27
28 28 class PrimitivusEvents(object):
29 def getText(node): 29 """ Used to manage change event of Primitivus widgets """
30 """Get child text nodes 30
31 @param node: dom Node 31 def _change_callback(self, ctrl, *args, **kwargs):
32 @return: joined unicode text of all nodes 32 """" Call xmlui callback and ignore any extra argument """
33 33 args[-1](ctrl)
34 """ 34
35 data = [] 35 def _xmluiOnChange(self, callback):
36 for child in node.childNodes: 36 """ Call callback with widget as only argument """
37 if child.nodeType == child.TEXT_NODE: 37 urwid.connect_signal(self, 'change', self._change_callback, callback)
38 data.append(child.wholeText) 38
39 return u"".join(data) 39
40 40 class PrimitivusEmptyWidget(xmlui.EmptyWidget, urwid.Text):
41 41
42 class Pairs(urwid.WidgetWrap): 42 def __init__(self, parent):
43 43 urwid.Text.__init__(self, '')
44 def __init__(self, weight_0='1', weight_1='1'): 44
45
46 class PrimitivusTextWidget(xmlui.TextWidget, urwid.Text):
47
48 def __init__(self, parent, value):
49 urwid.Text.__init__(self, value)
50
51
52 class PrimitivusStringWidget(xmlui.StringWidget, sat_widgets.AdvancedEdit, PrimitivusEvents):
53
54 def __init__(self, parent, value):
55 sat_widgets.AdvancedEdit.__init__(self, edit_text=value)
56
57 def _xmluiGetValue(self):
58 return self.get_edit_text()
59
60
61 class PrimitivusPasswordWidget(xmlui.PasswordWidget, sat_widgets.Password, PrimitivusEvents):
62
63 def __init__(self, parent, value):
64 sat_widgets.Password.__init__(self, edit_text=value)
65
66 def _xmluiGetValue(self):
67 return self.get_edit_text()
68
69
70 class PrimitivusTextBoxWidget(xmlui.TextBoxWidget, sat_widgets.AdvancedEdit, PrimitivusEvents):
71
72 def __init__(self, parent, value):
73 sat_widgets.AdvancedEdit.__init__(self, edit_text=value, multiline=True)
74
75 def _xmluiGetValue(self):
76 return self.getValue()
77
78
79 class PrimitivusBoolWidget(xmlui.BoolWidget, urwid.CheckBox, PrimitivusEvents):
80
81 def __init__(self, parent, state):
82 urwid.CheckBox.__init__(self, '', state = state)
83
84 def _xmluiGetValue(self):
85 return "true" if self.get_state() else "false"
86
87
88 class PrimitivusButtonWidget(xmlui.ButtonWidget, sat_widgets.CustomButton, PrimitivusEvents):
89
90 def __init__(self, parent, value, click_callback):
91 sat_widgets.CustomButton.__init__(self, value, on_press=click_callback)
92
93
94 class PrimitivusListWidget(xmlui.ListWidget, sat_widgets.List, PrimitivusEvents):
95
96 def __init__(self, parent, options, flags):
97 sat_widgets.List.__init__(self, options=options, style=flags)
98
99 def _xmluiSelectValue(self, value):
100 return self.selectValue(value)
101
102 def _xmluiGetSelectedValues(self):
103 return [option.value for option in self.getSelectedValues()]
104
105
106 class PrimitivusAdvancedListWidget(PrimitivusListWidget, PrimitivusEvents):
107
108 def __init__(self, parent, options, flags):
109 sat_widgets.List.__init__(self, options=options, style=flags, max_height=20)
110
111
112 class PrimitivusPairsContainer(xmlui.PairsContainer, urwid.WidgetWrap):
113
114 def __init__(self, parent, weight_0='1', weight_1='1'):
45 self.idx = 0 115 self.idx = 0
46 self.weight_0 = weight_0 116 self.weight_0 = weight_0
47 self.weight_1 = weight_1 117 self.weight_1 = weight_1
48 columns = urwid.Columns([urwid.Text(''), urwid.Text('')]) 118 columns = urwid.Columns([urwid.Text(''), urwid.Text('')])
49 #XXX: empty Text hack needed because Pile doesn't support empty list 119 #XXX: empty Text hack needed because Pile doesn't support empty list
50 urwid.WidgetWrap.__init__(self,columns) 120 urwid.WidgetWrap.__init__(self,columns)
51 121
52 def append(self, widget): 122 def _xmluiAppend(self, widget):
53 pile = self._w.widget_list[self.idx] 123 pile = self._w.widget_list[self.idx]
54 if isinstance(pile, urwid.Text): 124 if isinstance(pile, urwid.Text):
55 self._w.widget_list[self.idx] = urwid.Pile([widget]) 125 self._w.widget_list[self.idx] = urwid.Pile([widget])
56 if self.idx == 1: 126 if self.idx == 1:
57 self._w.set_focus(1) 127 self._w.set_focus(1)
58 else: 128 else:
59 pile.contents.append((widget,('weight',getattr(self,'weight_'+str(self.idx))))) 129 pile.contents.append((widget,('weight',getattr(self,'weight_'+str(self.idx)))))
60 self.idx = (self.idx + 1) % 2 130 self.idx = (self.idx + 1) % 2
61 131
62 class InvalidXMLUI(Exception): 132
63 pass 133 class PrimitivusTabsContainer(xmlui.TabsContainer, sat_widgets.TabsContainer):
64 134
65 class XMLUI(urwid.WidgetWrap): 135 def __init__(self, parent):
66 136 sat_widgets.TabsContainer.__init__(self)
67 def __init__(self, host, xml_data, title = None, options = None, misc = None): 137
68 self.host = host 138 def _xmluiAppend(self, widget):
69 self.title = title 139 self.body.append(widget)
70 self.options = options or [] 140
71 self.misc = misc or {} 141 def _xmluiAddTab(self, label):
72 self.__dest = "window" 142 list_box = super(PrimitivusTabsContainer, self).addTab(label)
73 self.ctrl_list = {} # usefull to access ctrl 143 if hasattr(PrimitivusVerticalContainer, "_PrimitivusVerticalContainer__super"): # workaround for Urwid's metaclass baviour
74 widget = self.constructUI(xml_data) 144 del PrimitivusVerticalContainer._PrimitivusVerticalContainer__super
75 urwid.WidgetWrap.__init__(self,widget) 145 PrimitivusVerticalContainer._xmluiAdapt(list_box)
76 146 return list_box
77 def __parseElems(self, node, parent): 147
78 """Parse elements inside a <layout> tags, and add them to the parent""" 148
79 for elem in node.childNodes: 149 class PrimitivusVerticalContainer(xmlui.VerticalContainer, urwid.ListBox):
80 if elem.nodeName != "elem": 150
81 message=_("Unmanaged tag") 151
82 error(message) 152 def __init__(self, parent):
83 raise Exception(message) 153 urwid.ListBox.__init__(self, urwid.SimpleListWalker([]))
84 id_ = elem.getAttribute("id") 154
85 name = elem.getAttribute("name") 155 def _xmluiAppend(self, widget):
86 type_ = elem.getAttribute("type") 156 self.body.append(widget)
87 value = elem.getAttribute("value") if elem.hasAttribute('value') else u'' 157
88 if type_=="empty": 158
89 ctrl = urwid.Text('') 159 class WidgetFactory(object):
90 elif type_=="text": 160
91 try: 161 def __getattr__(self, attr):
92 value = elem.childNodes[0].wholeText 162 if attr.startswith("create"):
93 except IndexError: 163 return globals()["Primitivus" + attr[6:]] # XXX: we prefix with "Primitivus" to work around an Urwid bug, WidgetMeta in Urwid don't manage multiple inheritance with same names
94 warning (_("text node has no child !")) 164
95 ctrl = urwid.Text(value) 165
96 elif type_=="label": 166 class XMLUI(xmlui.XMLUI, urwid.WidgetWrap):
97 ctrl = urwid.Text(value+": ") 167 widget_factory = WidgetFactory()
98 elif type_=="string": 168
99 ctrl = sat_widgets.AdvancedEdit(edit_text = value) 169 def __init__(self, host, xml_data, title = None, flags = None):
100 self.ctrl_list[name] = ({'type':type_, 'control':ctrl}) 170 self._dest = "window"
101 elif type_=="password": 171 xmlui.XMLUI.__init__(self, host, xml_data, title, flags)
102 ctrl = sat_widgets.Password(edit_text = value)
103 self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
104 elif type_=="textbox":
105 ctrl = sat_widgets.AdvancedEdit(edit_text = value, multiline=True)
106 self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
107 elif type_=="bool":
108 ctrl = urwid.CheckBox('', state = value=="true")
109 self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
110 elif type_=="list":
111 style=[] if elem.getAttribute("multi")=='yes' else ['single']
112 ctrl = sat_widgets.List(options=[(option.getAttribute("value"), option.getAttribute("label")) for option in elem.getElementsByTagName("option")], style=style, on_change=self.onParamChange if self.type == "param" else None)
113 ctrl.selectValue(elem.getAttribute("value"))
114 self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
115 elif type_=="button":
116 callback_id = elem.getAttribute("callback")
117 ctrl = sat_widgets.CustomButton(value, on_press=self.onButtonPress)
118 ctrl.param_id = (callback_id,[field.getAttribute('name') for field in elem.getElementsByTagName("field_back")])
119 elif type_=="advanced_list":
120 ctrl = sat_widgets.List(options=[getText(txt_elt) for txt_elt in elem.getElementsByTagName("text")], style=['can_select_none'], max_height=20, on_change=self.onParamChange)
121 ctrl.selectValue(elem.getAttribute("value"))
122 self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
123 else:
124 error(_("FIXME FIXME FIXME: type [%s] is not implemented") % type_) #FIXME !
125 raise NotImplementedError
126 if self.type == 'param':
127 if isinstance(ctrl, urwid.Edit) or isinstance(ctrl, urwid.CheckBox):
128 urwid.connect_signal(ctrl,'change',self.onParamChange)
129 elif isinstance(ctrl, sat_widgets.List):
130 # the GenericList member triggers the events, not List itself
131 # TODO: create a method GenericList.setParamData() for that,
132 # or later in onSaveParams do something like ctrl.getParent()
133 ctrl.genericList._param_category = self._current_category
134 ctrl.genericList._param_name = name
135 ctrl._param_category = self._current_category
136 ctrl._param_name = name
137 parent.append(ctrl)
138
139 def __parseChilds(self, current, elem, wanted = ['layout'], data = None):
140 """Recursively parse childNodes of an elemen
141 @param current: widget container with 'append' method
142 @param elem: element from which childs will be parsed
143 @param wanted: list of tag names that can be present in the childs to be SàT XMLUI compliant"""
144 for node in elem.childNodes:
145 if wanted and not node.nodeName in wanted:
146 raise InvalidXMLUI
147 if node.nodeName == "layout":
148 type = node.getAttribute('type')
149 if type == "tabs":
150 tab_cont = sat_widgets.TabsContainer()
151 self.__parseChilds(current, node, ['category'], tab_cont)
152 current.append(tab_cont)
153 elif type == "vertical":
154 self.__parseElems(node, current)
155 elif type == "pairs":
156 pairs = Pairs()
157 self.__parseElems(node, pairs)
158 current.append(pairs)
159 else:
160 warning(_("Unknown layout, using default one"))
161 self.__parseElems(node, current)
162 elif node.nodeName == "category":
163 name = node.getAttribute('name')
164 label = node.getAttribute('label')
165 if not name or not isinstance(data,sat_widgets.TabsContainer):
166 raise InvalidXMLUI
167 if self.type == 'param':
168 self._current_category = name #XXX: awful hack because params need category and we don't keep parent
169 tab_cont = data
170 listbox = tab_cont.addTab(label or name)
171 self.__parseChilds(listbox.body, node, ['layout'])
172 else:
173 message=_("Unknown tag")
174 error(message)
175 raise NotImplementedError
176 172
177 def constructUI(self, xml_data): 173 def constructUI(self, xml_data):
178 174 def postTreat(ret_wid):
179 ret_wid = urwid.ListBox(urwid.SimpleListWalker([])) 175 assert ret_wid.body
180 176
181 cat_dom = minidom.parseString(xml_data.encode('utf-8')) 177 if isinstance(ret_wid.body[0],sat_widgets.TabsContainer):
182 top=cat_dom.documentElement 178 ret_wid = ret_wid.body[0] #xxx: awfull hack cause TabsContainer is a BoxWidget, can't be inside a ListBox
183 self.type = top.getAttribute("type") 179
184 self.title = top.getAttribute("title") or self.title 180 if self.type == 'form':
185 self.session_id = top.getAttribute("session_id") or None 181 buttons = []
186 self.submit_id = top.getAttribute("submit") or None 182 buttons.append(urwid.Button(_('Submit'),self.onFormSubmitted))
187 if top.nodeName != "sat_xmlui" or not self.type in ['form', 'param', 'window']: 183 if not 'NO_CANCEL' in self.flags:
188 raise InvalidXMLUI 184 buttons.append(urwid.Button(_('Cancel'),self.onFormCancelled))
189 185 max_len = max([len(button.get_label()) for button in buttons])
190 if self.type == 'param': 186 grid_wid = urwid.GridFlow(buttons,max_len+4,1,0,'center')
191 self.param_changed = set() 187 ret_wid.body.append(grid_wid)
192 188 elif self.type == 'param':
193 self.__parseChilds(ret_wid.body, cat_dom.documentElement) 189 assert(isinstance(ret_wid,sat_widgets.TabsContainer))
194 190 buttons = []
195 assert ret_wid.body 191 buttons.append(sat_widgets.CustomButton(_('Save'),self.onSaveParams))
196 192 buttons.append(sat_widgets.CustomButton(_('Cancel'),lambda x:self.host.removeWindow()))
197 if isinstance(ret_wid.body[0],sat_widgets.TabsContainer): 193 max_len = max([button.getSize() for button in buttons])
198 ret_wid = ret_wid.body[0] #xxx: awfull hack cause TabsContainer is a BoxWidget, can't be inside a ListBox 194 grid_wid = urwid.GridFlow(buttons,max_len,1,0,'center')
199 195 ret_wid.addFooter(grid_wid)
200 196 return ret_wid
201 if self.type == 'form': 197
202 buttons = [] 198 widget = super(XMLUI, self).constructUI(xml_data, postTreat)
203 buttons.append(urwid.Button(_('Submit'),self.onFormSubmitted)) 199 urwid.WidgetWrap.__init__(self, widget)
204 if not 'NO_CANCEL' in self.options:
205 buttons.append(urwid.Button(_('Cancel'),self.onFormCancelled))
206 max_len = max([len(button.get_label()) for button in buttons])
207 grid_wid = urwid.GridFlow(buttons,max_len+4,1,0,'center')
208 ret_wid.body.append(grid_wid)
209 elif self.type == 'param':
210 assert(isinstance(ret_wid,sat_widgets.TabsContainer))
211 buttons = []
212 buttons.append(sat_widgets.CustomButton(_('Save'),self.onSaveParams))
213 buttons.append(sat_widgets.CustomButton(_('Cancel'),lambda x:self.host.removeWindow()))
214 max_len = max([button.getSize() for button in buttons])
215 grid_wid = urwid.GridFlow(buttons,max_len,1,0,'center')
216 ret_wid.addFooter(grid_wid)
217 return ret_wid
218 200
219 def show(self, show_type='popup', valign='middle'): 201 def show(self, show_type='popup', valign='middle'):
220 """Show the constructed UI 202 """Show the constructed UI
221 @param show_type: how to show the UI: 203 @param show_type: how to show the UI:
222 - popup 204 - popup
223 - window 205 - window
224 @param valign: vertical alignment when show_type is 'popup'. 206 @param valign: vertical alignment when show_type is 'popup'.
225 Ignored when show_type is 'window'. 207 Ignored when show_type is 'window'.
208
226 """ 209 """
227 self.__dest = "popup" 210 self._dest = "popup"
228 decorated = sat_widgets.LabelLine(self, sat_widgets.SurroundedText(self.title or '')) 211 decorated = sat_widgets.LabelLine(self, sat_widgets.SurroundedText(self.title or ''))
229 if show_type == 'popup': 212 if show_type == 'popup':
230 self.host.showPopUp(decorated, valign=valign) 213 self.host.showPopUp(decorated, valign=valign)
231 elif show_type == 'window': 214 elif show_type == 'window':
232 self.host.addWindow(decorated) 215 self.host.addWindow(decorated)
234 error(_('INTERNAL ERROR: Unmanaged show_type (%s)') % show_type) 217 error(_('INTERNAL ERROR: Unmanaged show_type (%s)') % show_type)
235 assert(False) 218 assert(False)
236 self.host.redraw() 219 self.host.redraw()
237 220
238 221
239 ##EVENTS## 222 def _xmluiClose(self):
240 223 if self._dest == 'window':
241 def onButtonPress(self, button):
242 callback_id, fields = button.param_id
243 data = {}
244 for field in fields:
245 ctrl = self.ctrl_list[field]
246 if isinstance(ctrl['control'],sat_widgets.List):
247 data[field] = u'\t'.join(ctrl['control'].getSelectedValues())
248 else:
249 data[field] = ctrl['control'].getValue()
250
251 self.host.launchAction(callback_id, data, profile_key = self.host.profile)
252
253 def onParamChange(self, widget, extra=None):
254 """Called when type is param and a widget to save is modified"""
255 assert(self.type == "param")
256 self.param_changed.add(widget)
257
258 def onFormSubmitted(self, button):
259 selected_values = []
260 for ctrl_name in self.ctrl_list:
261 escaped = u"%s%s" % (SAT_FORM_PREFIX, ctrl_name)
262 ctrl = self.ctrl_list[ctrl_name]
263 if isinstance(ctrl['control'], sat_widgets.List):
264 selected_values.append((escaped, u'\t'.join([option.value for option in ctrl['control'].getSelectedValues()])))
265 elif isinstance(ctrl['control'], urwid.CheckBox):
266 selected_values.append((escaped, "true" if ctrl['control'].get_state() else "false"))
267 else:
268 selected_values.append((escaped, ctrl['control'].get_edit_text()))
269 if self.misc.has_key('action_back'): #FIXME FIXME FIXME: WTF ! Must be cleaned
270 raise NotImplementedError
271 elif 'callback' in self.misc: # FIXME: this part is not needed anymore
272 try:
273 self.misc['callback'](selected_values, submit_id=self.submit_id, *self.misc['callback_args'])
274 except KeyError:
275 self.misc['callback'](selected_values, submit_id=self.submit_id)
276 elif self.submit_id is not None:
277 data = dict(selected_values)
278 if self.session_id is not None:
279 data["session_id"] = self.session_id
280 self.host.launchAction(self.submit_id, data, profile_key=self.host.profile)
281
282 else:
283 warning (_("The form data is not sent back, the type is not managed properly"))
284 self.host.removePopUp()
285
286 def onFormCancelled(self, button):
287 if self.__dest == 'window':
288 self.host.removeWindow() 224 self.host.removeWindow()
289 else: 225 else:
290 self.host.removePopUp() 226 self.host.removePopUp()
291
292 def onSaveParams(self, button):
293 for ctrl in self.param_changed:
294 if isinstance(ctrl, urwid.CheckBox):
295 value = "true" if ctrl.get_state() else "false"
296 elif isinstance(ctrl, sat_widgets.GenericList):
297 value = u'\t'.join(ctrl.getSelectedValues())
298 else:
299 value = ctrl.get_edit_text()
300 self.host.bridge.setParam(ctrl._param_name, value, ctrl._param_category,
301 profile_key=self.host.profile)
302 self.host.removeWindow()