Mercurial > libervia-backend
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() |