comparison libervia/tui/xmlui.py @ 4076:b620a8e882e1

refactoring: rename `libervia.frontends.primitivus` to `libervia.tui`
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 16:25:25 +0200
parents libervia/frontends/primitivus/xmlui.py@26b7ed2817da
children 0d7bb4df2343
comparison
equal deleted inserted replaced
4075:47401850dec6 4076:b620a8e882e1
1 #!/usr/bin/env python3
2
3
4 # Libervia TUI
5 # Copyright (C) 2009-2021 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 libervia.backend.core.i18n import _
21 import urwid
22 import copy
23 from libervia.backend.core import exceptions
24 from urwid_satext import sat_widgets
25 from urwid_satext import files_management
26 from libervia.backend.core.log import getLogger
27
28 log = getLogger(__name__)
29 from libervia.tui.constants import Const as C
30 from libervia.tui.widget import LiberviaTUIWidget
31 from libervia.frontends.tools import xmlui
32
33
34 class LiberviaTUIEvents(object):
35 """ Used to manage change event of LiberviaTUI widgets """
36
37 def _event_callback(self, ctrl, *args, **kwargs):
38 """" Call xmlui callback and ignore any extra argument """
39 args[-1](ctrl)
40
41 def _xmlui_on_change(self, callback):
42 """ Call callback with widget as only argument """
43 urwid.connect_signal(self, "change", self._event_callback, callback)
44
45
46 class LiberviaTUIEmptyWidget(xmlui.EmptyWidget, urwid.Text):
47 def __init__(self, _xmlui_parent):
48 urwid.Text.__init__(self, "")
49
50
51 class LiberviaTUITextWidget(xmlui.TextWidget, urwid.Text):
52 def __init__(self, _xmlui_parent, value, read_only=False):
53 urwid.Text.__init__(self, value)
54
55
56 class LiberviaTUILabelWidget(xmlui.LabelWidget, LiberviaTUITextWidget):
57 def __init__(self, _xmlui_parent, value):
58 super(LiberviaTUILabelWidget, self).__init__(_xmlui_parent, value + ": ")
59
60
61 class LiberviaTUIJidWidget(xmlui.JidWidget, LiberviaTUITextWidget):
62 pass
63
64
65 class LiberviaTUIDividerWidget(xmlui.DividerWidget, urwid.Divider):
66 def __init__(self, _xmlui_parent, style="line"):
67 if style == "line":
68 div_char = "─"
69 elif style == "dot":
70 div_char = "·"
71 elif style == "dash":
72 div_char = "-"
73 elif style == "plain":
74 div_char = "█"
75 elif style == "blank":
76 div_char = " "
77 else:
78 log.warning(_("Unknown div_char"))
79 div_char = "─"
80
81 urwid.Divider.__init__(self, div_char)
82
83
84 class LiberviaTUIStringWidget(
85 xmlui.StringWidget, sat_widgets.AdvancedEdit, LiberviaTUIEvents
86 ):
87 def __init__(self, _xmlui_parent, value, read_only=False):
88 sat_widgets.AdvancedEdit.__init__(self, edit_text=value)
89 self.read_only = read_only
90
91 def selectable(self):
92 if self.read_only:
93 return False
94 return super(LiberviaTUIStringWidget, self).selectable()
95
96 def _xmlui_set_value(self, value):
97 self.set_edit_text(value)
98
99 def _xmlui_get_value(self):
100 return self.get_edit_text()
101
102
103 class LiberviaTUIJidInputWidget(xmlui.JidInputWidget, LiberviaTUIStringWidget):
104 pass
105
106
107 class LiberviaTUIPasswordWidget(
108 xmlui.PasswordWidget, sat_widgets.Password, LiberviaTUIEvents
109 ):
110 def __init__(self, _xmlui_parent, value, read_only=False):
111 sat_widgets.Password.__init__(self, edit_text=value)
112 self.read_only = read_only
113
114 def selectable(self):
115 if self.read_only:
116 return False
117 return super(LiberviaTUIPasswordWidget, self).selectable()
118
119 def _xmlui_set_value(self, value):
120 self.set_edit_text(value)
121
122 def _xmlui_get_value(self):
123 return self.get_edit_text()
124
125
126 class LiberviaTUITextBoxWidget(
127 xmlui.TextBoxWidget, sat_widgets.AdvancedEdit, LiberviaTUIEvents
128 ):
129 def __init__(self, _xmlui_parent, value, read_only=False):
130 sat_widgets.AdvancedEdit.__init__(self, edit_text=value, multiline=True)
131 self.read_only = read_only
132
133 def selectable(self):
134 if self.read_only:
135 return False
136 return super(LiberviaTUITextBoxWidget, self).selectable()
137
138 def _xmlui_set_value(self, value):
139 self.set_edit_text(value)
140
141 def _xmlui_get_value(self):
142 return self.get_edit_text()
143
144
145 class LiberviaTUIBoolWidget(xmlui.BoolWidget, urwid.CheckBox, LiberviaTUIEvents):
146 def __init__(self, _xmlui_parent, state, read_only=False):
147 urwid.CheckBox.__init__(self, "", state=state)
148 self.read_only = read_only
149
150 def selectable(self):
151 if self.read_only:
152 return False
153 return super(LiberviaTUIBoolWidget, self).selectable()
154
155 def _xmlui_set_value(self, value):
156 self.set_state(value == "true")
157
158 def _xmlui_get_value(self):
159 return C.BOOL_TRUE if self.get_state() else C.BOOL_FALSE
160
161
162 class LiberviaTUIIntWidget(xmlui.IntWidget, sat_widgets.AdvancedEdit, LiberviaTUIEvents):
163 def __init__(self, _xmlui_parent, value, read_only=False):
164 sat_widgets.AdvancedEdit.__init__(self, edit_text=value)
165 self.read_only = read_only
166
167 def selectable(self):
168 if self.read_only:
169 return False
170 return super(LiberviaTUIIntWidget, self).selectable()
171
172 def _xmlui_set_value(self, value):
173 self.set_edit_text(value)
174
175 def _xmlui_get_value(self):
176 return self.get_edit_text()
177
178
179 class LiberviaTUIButtonWidget(
180 xmlui.ButtonWidget, sat_widgets.CustomButton, LiberviaTUIEvents
181 ):
182 def __init__(self, _xmlui_parent, value, click_callback):
183 sat_widgets.CustomButton.__init__(self, value, on_press=click_callback)
184
185 def _xmlui_on_click(self, callback):
186 urwid.connect_signal(self, "click", callback)
187
188
189 class LiberviaTUIListWidget(xmlui.ListWidget, sat_widgets.List, LiberviaTUIEvents):
190 def __init__(self, _xmlui_parent, options, selected, flags):
191 sat_widgets.List.__init__(self, options=options, style=flags)
192 self._xmlui_select_values(selected)
193
194 def _xmlui_select_value(self, value):
195 return self.select_value(value)
196
197 def _xmlui_select_values(self, values):
198 return self.select_values(values)
199
200 def _xmlui_get_selected_values(self):
201 return [option.value for option in self.get_selected_values()]
202
203 def _xmlui_add_values(self, values, select=True):
204 current_values = self.get_all_values()
205 new_values = copy.deepcopy(current_values)
206 for value in values:
207 if value not in current_values:
208 new_values.append(value)
209 if select:
210 selected = self._xmlui_get_selected_values()
211 self.change_values(new_values)
212 if select:
213 for value in values:
214 if value not in selected:
215 selected.append(value)
216 self._xmlui_select_values(selected)
217
218
219 class LiberviaTUIJidsListWidget(xmlui.ListWidget, sat_widgets.List, LiberviaTUIEvents):
220 def __init__(self, _xmlui_parent, jids, styles):
221 sat_widgets.List.__init__(
222 self,
223 options=jids + [""], # the empty field is here to add new jids if needed
224 option_type=lambda txt, align: sat_widgets.AdvancedEdit(
225 edit_text=txt, align=align
226 ),
227 on_change=self._on_change,
228 )
229 self.delete = 0
230
231 def _on_change(self, list_widget, jid_widget=None, text=None):
232 if jid_widget is not None:
233 if jid_widget != list_widget.contents[-1] and not text:
234 # if a field is empty, we delete the line (except for the last line)
235 list_widget.contents.remove(jid_widget)
236 elif jid_widget == list_widget.contents[-1] and text:
237 # we always want an empty field as last value to be able to add jids
238 list_widget.contents.append(sat_widgets.AdvancedEdit())
239
240 def _xmlui_get_selected_values(self):
241 # XXX: there is not selection in this list, so we return all non empty values
242 return [jid_ for jid_ in self.get_all_values() if jid_]
243
244
245 class LiberviaTUIAdvancedListContainer(
246 xmlui.AdvancedListContainer, sat_widgets.TableContainer, LiberviaTUIEvents
247 ):
248 def __init__(self, _xmlui_parent, columns, selectable="no"):
249 options = {"ADAPT": ()}
250 if selectable != "no":
251 options["HIGHLIGHT"] = ()
252 sat_widgets.TableContainer.__init__(
253 self, columns=columns, options=options, row_selectable=selectable != "no"
254 )
255
256 def _xmlui_append(self, widget):
257 self.add_widget(widget)
258
259 def _xmlui_add_row(self, idx):
260 self.set_row_index(idx)
261
262 def _xmlui_get_selected_widgets(self):
263 return self.get_selected_widgets()
264
265 def _xmlui_get_selected_index(self):
266 return self.get_selected_index()
267
268 def _xmlui_on_select(self, callback):
269 """ Call callback with widget as only argument """
270 urwid.connect_signal(self, "click", self._event_callback, callback)
271
272
273 class LiberviaTUIPairsContainer(xmlui.PairsContainer, sat_widgets.TableContainer):
274 def __init__(self, _xmlui_parent):
275 options = {"ADAPT": (0,), "HIGHLIGHT": (0,)}
276 if self._xmlui_main.type == "param":
277 options["FOCUS_ATTR"] = "param_selected"
278 sat_widgets.TableContainer.__init__(self, columns=2, options=options)
279
280 def _xmlui_append(self, widget):
281 if isinstance(widget, LiberviaTUIEmptyWidget):
282 # we don't want highlight on empty widgets
283 widget = urwid.AttrMap(widget, "default")
284 self.add_widget(widget)
285
286
287 class LiberviaTUILabelContainer(LiberviaTUIPairsContainer, xmlui.LabelContainer):
288 pass
289
290
291 class LiberviaTUITabsContainer(xmlui.TabsContainer, sat_widgets.TabsContainer):
292 def __init__(self, _xmlui_parent):
293 sat_widgets.TabsContainer.__init__(self)
294
295 def _xmlui_append(self, widget):
296 self.body.append(widget)
297
298 def _xmlui_add_tab(self, label, selected):
299 tab = LiberviaTUIVerticalContainer(None)
300 self.add_tab(label, tab, selected)
301 return tab
302
303
304 class LiberviaTUIVerticalContainer(xmlui.VerticalContainer, urwid.ListBox):
305 BOX_HEIGHT = 5
306
307 def __init__(self, _xmlui_parent):
308 urwid.ListBox.__init__(self, urwid.SimpleListWalker([]))
309 self._last_size = None
310
311 def _xmlui_append(self, widget):
312 if "flow" not in widget.sizing():
313 widget = urwid.BoxAdapter(widget, self.BOX_HEIGHT)
314 self.body.append(widget)
315
316 def render(self, size, focus=False):
317 if size != self._last_size:
318 (maxcol, maxrow) = size
319 if self.body:
320 widget = self.body[0]
321 if isinstance(widget, urwid.BoxAdapter):
322 widget.height = maxrow
323 self._last_size = size
324 return super(LiberviaTUIVerticalContainer, self).render(size, focus)
325
326
327 ### Dialogs ###
328
329
330 class LiberviaTUIDialog(object):
331 def __init__(self, _xmlui_parent):
332 self.host = _xmlui_parent.host
333
334 def _xmlui_show(self):
335 self.host.show_pop_up(self)
336
337 def _xmlui_close(self):
338 self.host.remove_pop_up(self)
339
340
341 class LiberviaTUIMessageDialog(LiberviaTUIDialog, xmlui.MessageDialog, sat_widgets.Alert):
342 def __init__(self, _xmlui_parent, title, message, level):
343 LiberviaTUIDialog.__init__(self, _xmlui_parent)
344 xmlui.MessageDialog.__init__(self, _xmlui_parent)
345 sat_widgets.Alert.__init__(
346 self, title, message, ok_cb=lambda __: self._xmlui_close()
347 )
348
349
350 class LiberviaTUINoteDialog(xmlui.NoteDialog, LiberviaTUIMessageDialog):
351 # TODO: separate NoteDialog
352 pass
353
354
355 class LiberviaTUIConfirmDialog(
356 LiberviaTUIDialog, xmlui.ConfirmDialog, sat_widgets.ConfirmDialog
357 ):
358 def __init__(self, _xmlui_parent, title, message, level, buttons_set):
359 LiberviaTUIDialog.__init__(self, _xmlui_parent)
360 xmlui.ConfirmDialog.__init__(self, _xmlui_parent)
361 sat_widgets.ConfirmDialog.__init__(
362 self,
363 title,
364 message,
365 no_cb=lambda __: self._xmlui_cancelled(),
366 yes_cb=lambda __: self._xmlui_validated(),
367 )
368
369
370 class LiberviaTUIFileDialog(
371 LiberviaTUIDialog, xmlui.FileDialog, files_management.FileDialog
372 ):
373 def __init__(self, _xmlui_parent, title, message, level, filetype):
374 # TODO: message is not managed yet
375 LiberviaTUIDialog.__init__(self, _xmlui_parent)
376 xmlui.FileDialog.__init__(self, _xmlui_parent)
377 style = []
378 if filetype == C.XMLUI_DATA_FILETYPE_DIR:
379 style.append("dir")
380 files_management.FileDialog.__init__(
381 self,
382 ok_cb=lambda path: self._xmlui_validated({"path": path}),
383 cancel_cb=lambda __: self._xmlui_cancelled(),
384 message=message,
385 title=title,
386 style=style,
387 )
388
389
390 class GenericFactory(object):
391 def __getattr__(self, attr):
392 if attr.startswith("create"):
393 cls = globals()[
394 "LiberviaTUI" + attr[6:]
395 ] # XXX: we prefix with "LiberviaTUI" to work around an Urwid bug, WidgetMeta in Urwid don't manage multiple inheritance with same names
396 return cls
397
398
399 class WidgetFactory(GenericFactory):
400 def __getattr__(self, attr):
401 if attr.startswith("create"):
402 cls = GenericFactory.__getattr__(self, attr)
403 cls._xmlui_main = self._xmlui_main
404 return cls
405
406
407 class XMLUIPanel(xmlui.XMLUIPanel, LiberviaTUIWidget):
408 widget_factory = WidgetFactory()
409
410 def __init__(
411 self,
412 host,
413 parsed_xml,
414 title=None,
415 flags=None,
416 callback=None,
417 ignore=None,
418 whitelist=None,
419 profile=C.PROF_KEY_NONE,
420 ):
421 self.widget_factory._xmlui_main = self
422 self._dest = None
423 xmlui.XMLUIPanel.__init__(
424 self,
425 host,
426 parsed_xml,
427 title=title,
428 flags=flags,
429 callback=callback,
430 ignore=ignore,
431 profile=profile,
432 )
433 LiberviaTUIWidget.__init__(self, self.main_cont, self.xmlui_title)
434
435
436 def _parse_childs(self, _xmlui_parent, current_node, wanted=("container",), data=None):
437 # Small hack to always have a VerticalContainer as main container in LiberviaTUI.
438 # this used to be the default behaviour for all frontends, but now
439 # TabsContainer can also be the main container.
440 if _xmlui_parent is self:
441 node = current_node.childNodes[0]
442 if node.nodeName == "container" and node.getAttribute("type") == "tabs":
443 _xmlui_parent = self.widget_factory.createVerticalContainer(self)
444 self.main_cont = _xmlui_parent
445 return super(XMLUIPanel, self)._parse_childs(_xmlui_parent, current_node, wanted,
446 data)
447
448
449 def construct_ui(self, parsed_dom):
450 def post_treat():
451 assert self.main_cont.body
452
453 if self.type in ("form", "popup"):
454 buttons = []
455 if self.type == "form":
456 buttons.append(urwid.Button(_("Submit"), self.on_form_submitted))
457 if not "NO_CANCEL" in self.flags:
458 buttons.append(urwid.Button(_("Cancel"), self.on_form_cancelled))
459 else:
460 buttons.append(
461 urwid.Button(_("OK"), on_press=lambda __: self._xmlui_close())
462 )
463 max_len = max([len(button.get_label()) for button in buttons])
464 grid_wid = urwid.GridFlow(buttons, max_len + 4, 1, 0, "center")
465 self.main_cont.body.append(grid_wid)
466 elif self.type == "param":
467 tabs_cont = self.main_cont.body[0].base_widget
468 assert isinstance(tabs_cont, sat_widgets.TabsContainer)
469 buttons = []
470 buttons.append(sat_widgets.CustomButton(_("Save"), self.on_save_params))
471 buttons.append(
472 sat_widgets.CustomButton(
473 _("Cancel"), lambda x: self.host.remove_window()
474 )
475 )
476 max_len = max([button.get_size() for button in buttons])
477 grid_wid = urwid.GridFlow(buttons, max_len, 1, 0, "center")
478 tabs_cont.add_footer(grid_wid)
479
480 xmlui.XMLUIPanel.construct_ui(self, parsed_dom, post_treat)
481 urwid.WidgetWrap.__init__(self, self.main_cont)
482
483 def show(self, show_type=None, valign="middle"):
484 """Show the constructed UI
485 @param show_type: how to show the UI:
486 - None (follow XMLUI's recommendation)
487 - 'popup'
488 - 'window'
489 @param valign: vertical alignment when show_type is 'popup'.
490 Ignored when show_type is 'window'.
491
492 """
493 if show_type is None:
494 if self.type in ("window", "param"):
495 show_type = "window"
496 elif self.type in ("popup", "form"):
497 show_type = "popup"
498
499 if show_type not in ("popup", "window"):
500 raise ValueError("Invalid show_type [%s]" % show_type)
501
502 self._dest = show_type
503 if show_type == "popup":
504 self.host.show_pop_up(self, valign=valign)
505 elif show_type == "window":
506 self.host.new_widget(self, user_action=self.user_action)
507 else:
508 assert False
509 self.host.redraw()
510
511 def _xmlui_close(self):
512 if self._dest == "window":
513 self.host.remove_window()
514 elif self._dest == "popup":
515 self.host.remove_pop_up(self)
516 else:
517 raise exceptions.InternalError(
518 "self._dest unknown, are you sure you have called XMLUI.show ?"
519 )
520
521
522 class XMLUIDialog(xmlui.XMLUIDialog):
523 dialog_factory = GenericFactory()
524
525
526 xmlui.register_class(xmlui.CLASS_PANEL, XMLUIPanel)
527 xmlui.register_class(xmlui.CLASS_DIALOG, XMLUIDialog)
528 create = xmlui.create