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