comparison sat_frontends/jp/xmlui_manager.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents frontends/src/jp/xmlui_manager.py@65695b9343d3
children 56f94936df1e
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # JP: a SàT frontend
5 # Copyright (C) 2009-2018 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 self.root.values_only:
239 for value in self.values:
240 self.disp(self.value)
241 return
242 if not self.options:
243 return
244
245 # list display
246 self.verboseName()
247
248 for idx, (value, label) in enumerate(self.options):
249 elems = []
250 if not self.root.read_only:
251 elems.extend([C.A_SUBHEADER, unicode(idx), A.RESET, u': '])
252 elems.append(label)
253 self.verboseName(elems, value)
254 self.disp(A.color(*elems))
255
256 if self.root.read_only:
257 return
258
259 if len(self.options) == 1:
260 # we have only one option, no need to ask
261 self.value = self.options[0][0]
262 return
263
264 # we ask use to choose an option
265 choice = None
266 limit_max = len(self.options)-1
267 while choice is None or choice<0 or choice>limit_max:
268 choice = raw_input(A.color(C.A_HEADER, _(u'your choice (0-{max}): ').format(max=limit_max)))
269 try:
270 choice = int(choice)
271 except ValueError:
272 choice = None
273 self.value = self.options[choice][0]
274 self.disp('')
275
276
277 class BoolWidget(xmlui_manager.BoolWidget, InputWidget):
278 type = u'bool'
279
280 def show(self):
281 disp_true = A.color(A.FG_GREEN, u'TRUE')
282 disp_false = A.color(A.FG_RED,u'FALSE')
283 if self.read_only:
284 self.disp(disp_true if self.value else disp_false)
285 else:
286 self.disp(A.color(C.A_HEADER, u'0: ', disp_false))
287 self.disp(A.color(C.A_HEADER, u'1: ', disp_true))
288 choice = None
289 while choice not in ('0', '1'):
290 elems = [C.A_HEADER, _(u'your choice (0,1): ')]
291 self.verboseName(elems)
292 choice = raw_input(A.color(*elems))
293 self.value = bool(int(choice))
294 self.disp('')
295
296 def _xmluiGetValue(self):
297 return C.boolConst(self.value)
298
299 ## Containers ##
300
301 class Container(Base):
302 category = u'container'
303
304 def __init__(self, xmlui_parent):
305 super(Container, self).__init__(xmlui_parent)
306 self.children = []
307
308 def __iter__(self):
309 return iter(self.children)
310
311 def _xmluiAppend(self, widget):
312 self.children.append(widget)
313
314 def _xmluiRemove(self, widget):
315 self.children.remove(widget)
316
317 def show(self):
318 for child in self.children:
319 child.show()
320
321
322 class VerticalContainer(xmlui_manager.VerticalContainer, Container):
323 type = u'vertical'
324
325
326 class PairsContainer(xmlui_manager.PairsContainer, Container):
327 type = u'pairs'
328
329
330 class LabelContainer(xmlui_manager.PairsContainer, Container):
331 type = u'label'
332
333 def show(self):
334 for child in self.children:
335 no_lf = False
336 # we check linked widget type
337 # to see if we want the label on the same line or not
338 if child.type == u'label':
339 for_name = child.for_name
340 if for_name is not None:
341 for_widget = self.root.widgets[for_name]
342 wid_type = for_widget.type
343 if self.root.values_only or wid_type in ('text', 'string', 'jid_input'):
344 no_lf = True
345 elif wid_type == 'bool' and for_widget.read_only:
346 no_lf = True
347 child.show(no_lf=no_lf, ansi=A.FG_CYAN)
348 else:
349 child.show()
350
351 ## Dialogs ##
352
353
354 class Dialog(object):
355
356 def __init__(self, xmlui_parent):
357 self.xmlui_parent = xmlui_parent
358 self.host = self.xmlui_parent.host
359
360 def disp(self, *args, **kwargs):
361 self.host.disp(*args, **kwargs)
362
363 def show(self):
364 """display current dialog
365
366 must be overriden by subclasses
367 """
368 raise NotImplementedError(self.__class__)
369
370
371 class NoteDialog(xmlui_manager.NoteDialog, Dialog):
372
373 def show(self):
374 # TODO: handle title and level
375 self.disp(self.message)
376
377 def __init__(self, _xmlui_parent, title, message, level):
378 Dialog.__init__(self, _xmlui_parent)
379 xmlui_manager.NoteDialog.__init__(self, _xmlui_parent)
380 self.title, self.message, self.level = title, message, level
381
382 ## Factory ##
383
384
385 class WidgetFactory(object):
386
387 def __getattr__(self, attr):
388 if attr.startswith("create"):
389 cls = globals()[attr[6:]]
390 return cls
391
392
393 class XMLUIPanel(xmlui_manager.XMLUIPanel):
394 widget_factory = WidgetFactory()
395 _actions = 0 # use to keep track of bridge's launchAction calls
396 read_only = False
397 values_only = False
398 workflow = None
399 _submit_cb = None
400
401 def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, ignore=None, whitelist=None, profile=None):
402 xmlui_manager.XMLUIPanel.__init__(self,
403 host,
404 parsed_dom,
405 title = title,
406 flags = flags,
407 ignore = ignore,
408 whitelist = whitelist,
409 profile=host.profile)
410 self.submitted = False
411
412 @property
413 def command(self):
414 return self.host.command
415
416 def show(self, workflow=None, read_only=False, values_only=False):
417 """display the panel
418
419 @param workflow(list, None): command to execute if not None
420 put here for convenience, the main workflow is the class attribute
421 (because workflow can continue in subclasses)
422 command are a list of consts or lists:
423 - SUBMIT is the only constant so far, it submits the XMLUI
424 - list must contain widget name/widget value to fill
425 @param read_only(bool): if True, don't request values
426 @param values_only(bool): if True, only show select values (imply read_only)
427 """
428 self.read_only = read_only
429 self.values_only = values_only
430 if self.values_only:
431 self.read_only = True
432 if workflow:
433 XMLUIPanel.workflow = workflow
434 if XMLUIPanel.workflow:
435 self.runWorkflow()
436 else:
437 self.main_cont.show()
438
439 def runWorkflow(self):
440 """loop into workflow commands and execute commands
441
442 SUBMIT will interrupt workflow (which will be continue on callback)
443 @param workflow(list): same as [show]
444 """
445 workflow = XMLUIPanel.workflow
446 while True:
447 try:
448 cmd = workflow.pop(0)
449 except IndexError:
450 break
451 if cmd == SUBMIT:
452 self.onFormSubmitted()
453 self.submit_id = None # avoid double submit
454 return
455 elif isinstance(cmd, list):
456 name, value = cmd
457 widget = self.widgets[name]
458 if widget.type == 'bool':
459 value = C.bool(value)
460 widget.value = value
461 self.show()
462
463 def submitForm(self, callback=None):
464 XMLUIPanel._submit_cb = callback
465 self.onFormSubmitted()
466
467 def onFormSubmitted(self, ignore=None):
468 # self.submitted is a Q&D workaround to avoid
469 # double submit when a workflow is set
470 if self.submitted:
471 return
472 self.submitted = True
473 super(XMLUIPanel, self).onFormSubmitted(ignore)
474
475 def _xmluiClose(self):
476 pass
477
478 def _launchActionCb(self, data):
479 XMLUIPanel._actions -= 1
480 assert XMLUIPanel._actions >= 0
481 if u'xmlui' in data:
482 xmlui_raw = data['xmlui']
483 xmlui = xmlui_manager.create(self.host, xmlui_raw)
484 xmlui.show()
485 if xmlui.submit_id:
486 xmlui.onFormSubmitted()
487 # TODO: handle data other than XMLUI
488 if not XMLUIPanel._actions:
489 if self._submit_cb is None:
490 self.host.quit()
491 else:
492 self._submit_cb()
493
494 def _xmluiLaunchAction(self, action_id, data):
495 XMLUIPanel._actions += 1
496 self.host.bridge.launchAction(
497 action_id,
498 data,
499 self.profile,
500 callback=self._launchActionCb,
501 errback=partial(self.command.errback,
502 msg=_(u"can't launch XMLUI action: {}"),
503 exit_code=C.EXIT_BRIDGE_ERRBACK))
504
505
506 class XMLUIDialog(xmlui_manager.XMLUIDialog):
507 type = 'dialog'
508 dialog_factory = WidgetFactory()
509 read_only = False
510
511 def show(self, dummy=None):
512 self.dlg.show()
513
514 def _xmluiClose(self):
515 pass
516
517
518 xmlui_manager.registerClass(xmlui_manager.CLASS_PANEL, XMLUIPanel)
519 xmlui_manager.registerClass(xmlui_manager.CLASS_DIALOG, XMLUIDialog)
520 create = xmlui_manager.create