2408
|
1 #!/usr/bin/env python2 |
|
2 # -*- coding: utf-8 -*- |
|
3 |
|
4 # JP: a SàT frontend |
|
5 # Copyright (C) 2009-2016 Jérôme Poisson (goffi@goffi.org) |
|
6 |
|
7 # This program is free software: you can redistribute it and/or modify |
|
8 # it under the terms of the GNU Affero General Public License as published by |
|
9 # the Free Software Foundation, either version 3 of the License, or |
|
10 # (at your option) any later version. |
|
11 |
|
12 # This program is distributed in the hope that it will be useful, |
|
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
15 # GNU Affero General Public License for more details. |
|
16 |
|
17 # You should have received a copy of the GNU Affero General Public License |
|
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
19 |
|
20 from sat.core.log import getLogger |
|
21 log = getLogger(__name__) |
|
22 from sat_frontends.tools import xmlui as xmlui_manager |
|
23 from sat_frontends.jp.constants import Const as C |
|
24 from sat.tools.common.ansi import ANSI as A |
|
25 from sat.core.i18n import _ |
|
26 from functools import partial |
|
27 |
|
28 # workflow constants |
|
29 |
|
30 SUBMIT = 'SUBMIT' # submit form |
|
31 |
|
32 |
|
33 |
|
34 ## Widgets ## |
|
35 |
|
36 class Base(object): |
|
37 """Base for Widget and Container""" |
|
38 type = None |
|
39 _root = None |
|
40 |
|
41 def __init__(self, xmlui_parent): |
|
42 self.xmlui_parent = xmlui_parent |
|
43 self.host = self.xmlui_parent.host |
|
44 |
|
45 @property |
|
46 def root(self): |
|
47 """retrieve main XMLUI parent class""" |
|
48 if self._root is not None: |
|
49 return self._root |
|
50 root = self |
|
51 while not isinstance(root, xmlui_manager.XMLUIBase): |
|
52 root = root.xmlui_parent |
|
53 self._root = root |
|
54 return root |
|
55 |
|
56 def disp(self, *args, **kwargs): |
|
57 self.host.disp(*args, **kwargs) |
|
58 |
|
59 |
|
60 class Widget(Base): |
|
61 category = u'widget' |
|
62 enabled = True |
|
63 |
|
64 @property |
|
65 def name(self): |
|
66 return self._xmlui_name |
|
67 |
|
68 def show(self): |
|
69 """display current widget |
|
70 |
|
71 must be overriden by subclasses |
|
72 """ |
|
73 raise NotImplementedError(self.__class__) |
|
74 |
|
75 def verboseName(self, elems=None, value=None): |
|
76 """add name in color to the elements |
|
77 |
|
78 helper method to display name which can then be used to automate commands |
|
79 elems is only modified if verbosity is > 0 |
|
80 @param elems(list[unicode], None): elements to display |
|
81 None to display name directly |
|
82 @param value(unicode, None): value to show |
|
83 use self.name if None |
|
84 """ |
|
85 if value is None: |
|
86 value = self.name |
|
87 if self.host.verbosity: |
|
88 to_disp = [A.FG_MAGENTA, |
|
89 u' ' if elems else u'', |
|
90 u'({})'.format(value), A.RESET] |
|
91 if elems is None: |
|
92 self.host.disp(A.color(*to_disp)) |
|
93 else: |
|
94 elems.extend(to_disp) |
|
95 |
|
96 class ValueWidget(Widget): |
|
97 |
|
98 def __init__(self, xmlui_parent, value): |
|
99 super(ValueWidget, self).__init__(xmlui_parent) |
|
100 self.value = value |
|
101 |
|
102 @property |
|
103 def values(self): |
|
104 return [self.value] |
|
105 |
|
106 |
|
107 class InputWidget(ValueWidget): |
|
108 |
|
109 def __init__(self, xmlui_parent, value, read_only=False): |
|
110 super(InputWidget, self).__init__(xmlui_parent, value) |
|
111 self.read_only = read_only |
|
112 |
|
113 def _xmluiGetValue(self): |
|
114 return self.value |
|
115 |
|
116 |
|
117 class OptionsWidget(Widget): |
|
118 |
|
119 def __init__(self, xmlui_parent, options, selected, style): |
|
120 super(OptionsWidget, self).__init__(xmlui_parent) |
|
121 self.options = options |
|
122 self.selected = selected |
|
123 self.style = style |
|
124 |
|
125 @property |
|
126 def values(self): |
|
127 return self.selected |
|
128 |
|
129 @values.setter |
|
130 def values(self, values): |
|
131 self.selected = values |
|
132 |
|
133 @property |
|
134 def value(self): |
|
135 return self.selected[0] |
|
136 |
|
137 @value.setter |
|
138 def value(self, value): |
|
139 self.selected = [value] |
|
140 |
|
141 def _xmluiSelectValue(self, value): |
|
142 self.value = value |
|
143 |
|
144 def _xmluiSelectValues(self, values): |
|
145 self.values = values |
|
146 |
|
147 def _xmluiGetSelectedValues(self): |
|
148 return self.values |
|
149 |
|
150 @property |
|
151 def labels(self): |
|
152 """return only labels from self.items""" |
|
153 for value, label in self.items: |
|
154 yield label |
|
155 |
|
156 @property |
|
157 def items(self): |
|
158 """return suitable items, according to style""" |
|
159 no_select = self.no_select |
|
160 for value,label in self.options: |
|
161 if no_select or value in self.selected: |
|
162 yield value,label |
|
163 |
|
164 @property |
|
165 def inline(self): |
|
166 return u'inline' in self.style |
|
167 |
|
168 @property |
|
169 def no_select(self): |
|
170 return u'noselect' in self.style |
|
171 |
|
172 |
|
173 class EmptyWidget(xmlui_manager.EmptyWidget, Widget): |
|
174 |
|
175 def __init__(self, _xmlui_parent): |
|
176 Widget.__init__(self) |
|
177 |
|
178 |
|
179 class TextWidget(xmlui_manager.TextWidget, ValueWidget): |
|
180 type = u"text" |
|
181 |
|
182 def show(self): |
|
183 self.host.disp(self.value) |
|
184 |
|
185 |
|
186 class LabelWidget(xmlui_manager.LabelWidget, ValueWidget): |
|
187 type = u"label" |
|
188 |
|
189 @property |
|
190 def for_name(self): |
|
191 try: |
|
192 return self._xmlui_for_name |
|
193 except AttributeError: |
|
194 return None |
|
195 |
|
196 def show(self, no_lf=False, ansi=u''): |
|
197 """show label |
|
198 |
|
199 @param no_lf(bool): same as for [JP.disp] |
|
200 @param ansi(unicode): ansi escape code to print before label |
|
201 """ |
|
202 self.disp(A.color(ansi, self.value), no_lf=no_lf) |
|
203 |
|
204 |
|
205 class StringWidget(xmlui_manager.StringWidget, InputWidget): |
|
206 type = u"string" |
|
207 |
|
208 def show(self): |
|
209 if self.read_only: |
|
210 self.disp(self.value) |
|
211 else: |
|
212 elems = [] |
|
213 self.verboseName(elems) |
|
214 if self.value: |
|
215 elems.append(_(u'(enter: {default})').format(default=self.value)) |
|
216 elems.extend([C.A_HEADER, u'> ']) |
|
217 value = raw_input(A.color(*elems)) |
|
218 if value: |
|
219 # TODO: empty value should be possible |
|
220 # an escape key should be used for default instead of enter with empty value |
|
221 self.value = value |
|
222 |
|
223 |
|
224 |
|
225 class JidInputWidget(xmlui_manager.JidInputWidget, StringWidget): |
|
226 type = u'jid_input' |
|
227 |
|
228 |
|
229 class TextBoxWidget(xmlui_manager.TextWidget, StringWidget): |
|
230 type = u"textbox" |
|
231 |
|
232 |
|
233 class ListWidget(xmlui_manager.ListWidget, OptionsWidget): |
|
234 type = u'list' |
|
235 # TODO: handle flags, notably multi |
|
236 |
|
237 def show(self): |
|
238 if not self.options: |
|
239 return |
|
240 |
|
241 # list display |
|
242 self.verboseName() |
|
243 |
|
244 for idx, (value, label) in enumerate(self.options): |
|
245 elems = [] |
|
246 if not self.root.readonly: |
|
247 elems.extend([C.A_SUBHEADER, unicode(idx), A.RESET, u': ']) |
|
248 elems.append(label) |
|
249 self.verboseName(elems, value) |
|
250 self.disp(A.color(*elems)) |
|
251 |
|
252 if self.root.readonly: |
|
253 return |
|
254 |
|
255 if len(self.options) == 1: |
|
256 # we have only one option, no need to ask |
|
257 self.value = self.options[0][0] |
|
258 return |
|
259 |
|
260 # we ask use to choose an option |
|
261 choice = None |
|
262 limit_max = len(self.options)-1 |
|
263 while choice is None or choice<0 or choice>limit_max: |
|
264 choice = raw_input(A.color(C.A_HEADER, _(u'your choice (0-{max}): ').format(max=limit_max))) |
|
265 try: |
|
266 choice = int(choice) |
|
267 except ValueError: |
|
268 choice = None |
|
269 self.value = self.options[choice][0] |
|
270 self.disp('') |
|
271 |
|
272 |
|
273 class BoolWidget(xmlui_manager.BoolWidget, InputWidget): |
|
274 type = u'bool' |
|
275 |
|
276 def show(self): |
|
277 disp_true = A.color(A.FG_GREEN, u'TRUE') |
|
278 disp_false = A.color(A.FG_RED,u'FALSE') |
|
279 if self.read_only: |
|
280 self.disp(disp_true if self.value else disp_false) |
|
281 else: |
|
282 self.disp(A.color(C.A_HEADER, u'0: ', disp_false)) |
|
283 self.disp(A.color(C.A_HEADER, u'1: ', disp_true)) |
|
284 choice = None |
|
285 while choice not in ('0', '1'): |
|
286 elems = [C.A_HEADER, _(u'your choice (0,1): ')] |
|
287 self.verboseName(elems) |
|
288 choice = raw_input(A.color(*elems)) |
|
289 self.value = bool(int(choice)) |
|
290 self.disp('') |
|
291 |
|
292 def _xmluiGetValue(self): |
|
293 return C.boolConst(self.value) |
|
294 |
|
295 ## Containers ## |
|
296 |
|
297 class Container(Base): |
|
298 category = u'container' |
|
299 |
|
300 def __init__(self, xmlui_parent): |
|
301 super(Container, self).__init__(xmlui_parent) |
|
302 self.children = [] |
|
303 |
|
304 def __iter__(self): |
|
305 return iter(self.children) |
|
306 |
|
307 def _xmluiAppend(self, widget): |
|
308 self.children.append(widget) |
|
309 |
|
310 def show(self): |
|
311 for child in self.children: |
|
312 child.show() |
|
313 |
|
314 |
|
315 class VerticalContainer(xmlui_manager.VerticalContainer, Container): |
|
316 type = u'vertical' |
|
317 |
|
318 |
|
319 class PairsContainer(xmlui_manager.PairsContainer, Container): |
|
320 type = u'pairs' |
|
321 |
|
322 |
|
323 class LabelContainer(xmlui_manager.PairsContainer, Container): |
|
324 type = u'label' |
|
325 |
|
326 def show(self): |
|
327 for child in self.children: |
|
328 no_lf = False |
|
329 # we check linked widget type |
|
330 # to see if we want the label on the same line or not |
|
331 if child.type == u'label': |
|
332 for_name = child.for_name |
|
333 if for_name is not None: |
|
334 for_widget = self.root.widgets[for_name] |
|
335 wid_type = for_widget.type |
|
336 if wid_type in ('text', 'string', 'jid_input'): |
|
337 no_lf = True |
|
338 elif wid_type == 'bool' and for_widget.read_only: |
|
339 no_lf = True |
|
340 child.show(no_lf=no_lf, ansi=A.FG_CYAN) |
|
341 else: |
|
342 child.show() |
|
343 |
|
344 ## Dialogs ## |
|
345 |
|
346 |
|
347 class Dialog(object): |
|
348 |
|
349 def __init__(self, xmlui_parent): |
|
350 self.xmlui_parent = xmlui_parent |
|
351 self.host = self.xmlui_parent.host |
|
352 |
|
353 def disp(self, *args, **kwargs): |
|
354 self.host.disp(*args, **kwargs) |
|
355 |
|
356 def show(self): |
|
357 """display current dialog |
|
358 |
|
359 must be overriden by subclasses |
|
360 """ |
|
361 raise NotImplementedError(self.__class__) |
|
362 |
|
363 |
|
364 class NoteDialog(xmlui_manager.NoteDialog, Dialog): |
|
365 |
|
366 def show(self): |
|
367 # TODO: handle title and level |
|
368 self.disp(self.message) |
|
369 |
|
370 def __init__(self, _xmlui_parent, title, message, level): |
|
371 Dialog.__init__(self, _xmlui_parent) |
|
372 xmlui_manager.NoteDialog.__init__(self, _xmlui_parent) |
|
373 self.title, self.message, self.level = title, message, level |
|
374 |
|
375 ## Factory ## |
|
376 |
|
377 |
|
378 class WidgetFactory(object): |
|
379 |
|
380 def __getattr__(self, attr): |
|
381 if attr.startswith("create"): |
|
382 cls = globals()[attr[6:]] |
|
383 return cls |
|
384 |
|
385 |
|
386 class XMLUIPanel(xmlui_manager.XMLUIPanel): |
|
387 widget_factory = WidgetFactory() |
|
388 _actions = 0 # use to keep track of bridge's launchAction calls |
|
389 readonly = False |
|
390 workflow = None |
|
391 _submit_cb = None |
|
392 |
|
393 def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, profile=None): |
|
394 xmlui_manager.XMLUIPanel.__init__(self, host, parsed_dom, title, flags, profile=host.profile) |
|
395 self.submitted = False |
|
396 |
|
397 @property |
|
398 def command(self): |
|
399 return self.host.command |
|
400 |
|
401 def show(self, workflow=None): |
|
402 """display the panel |
|
403 |
|
404 @param workflow(list, None): command to execute if not None |
|
405 put here for convenience, the main workflow is the class attribute |
|
406 (because workflow can continue in subclasses) |
|
407 command are a list of consts or lists: |
|
408 - SUBMIT is the only constant so far, it submits the XMLUI |
|
409 - list must contain widget name/widget value to fill |
|
410 """ |
|
411 if workflow: |
|
412 XMLUIPanel.workflow = workflow |
|
413 if XMLUIPanel.workflow: |
|
414 self.runWorkflow() |
|
415 else: |
|
416 self.main_cont.show() |
|
417 |
|
418 def runWorkflow(self): |
|
419 """loop into workflow commands and execute commands |
|
420 |
|
421 SUBMIT will interrupt workflow (which will be continue on callback) |
|
422 @param workflow(list): same as [show] |
|
423 """ |
|
424 workflow = XMLUIPanel.workflow |
|
425 while True: |
|
426 try: |
|
427 cmd = workflow.pop(0) |
|
428 except IndexError: |
|
429 break |
|
430 if cmd == SUBMIT: |
|
431 self.onFormSubmitted() |
|
432 self.submit_id = None # avoid double submit |
|
433 return |
|
434 elif isinstance(cmd, list): |
|
435 name, value = cmd |
|
436 self.widgets[name].value = value |
|
437 self.show() |
|
438 |
|
439 def submitForm(self, callback=None): |
|
440 XMLUIPanel._submit_cb = callback |
|
441 self.onFormSubmitted() |
|
442 |
|
443 def onFormSubmitted(self, ignore=None): |
|
444 # self.submitted is a Q&D workaround to avoid |
|
445 # double submit when a workflow is set |
|
446 if self.submitted: |
|
447 return |
|
448 self.submitted = True |
|
449 super(XMLUIPanel, self).onFormSubmitted(ignore) |
|
450 |
|
451 def _xmluiClose(self): |
|
452 pass |
|
453 |
|
454 def _launchActionCb(self, data): |
|
455 XMLUIPanel._actions -= 1 |
|
456 assert XMLUIPanel._actions >= 0 |
|
457 if u'xmlui' in data: |
|
458 xmlui_raw = data['xmlui'] |
|
459 xmlui = xmlui_manager.create(self.host, xmlui_raw) |
|
460 xmlui.show() |
|
461 if xmlui.submit_id: |
|
462 xmlui.onFormSubmitted() |
|
463 # TODO: handle data other than XMLUI |
|
464 if not XMLUIPanel._actions: |
|
465 if self._submit_cb is None: |
|
466 self.host.quit() |
|
467 else: |
|
468 self._submit_cb() |
|
469 |
|
470 def _xmluiLaunchAction(self, action_id, data): |
|
471 XMLUIPanel._actions += 1 |
|
472 self.host.bridge.launchAction( |
|
473 action_id, |
|
474 data, |
|
475 self.profile, |
|
476 callback=self._launchActionCb, |
|
477 errback=partial(self.command.errback, |
|
478 msg=_(u"can't launch XMLUI action: {}"), |
|
479 exit_code=C.EXIT_BRIDGE_ERRBACK)) |
|
480 |
|
481 |
|
482 class XMLUIDialog(xmlui_manager.XMLUIDialog): |
|
483 type = 'dialog' |
|
484 dialog_factory = WidgetFactory() |
|
485 readonly = False |
|
486 |
|
487 def show(self): |
|
488 self.dlg.show() |
|
489 |
|
490 def _xmluiClose(self): |
|
491 pass |
|
492 |
|
493 |
|
494 xmlui_manager.registerClass(xmlui_manager.CLASS_PANEL, XMLUIPanel) |
|
495 xmlui_manager.registerClass(xmlui_manager.CLASS_DIALOG, XMLUIDialog) |
|
496 create = xmlui_manager.create |