Mercurial > libervia-desktop-kivy
comparison libervia/desktop_kivy/core/xmlui.py @ 493:b3cedbee561d
refactoring: rename `cagou` to `libervia.desktop_kivy` + update imports and names following backend changes
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 18:26:16 +0200 |
parents | cagou/core/xmlui.py@203755bbe0fe |
children |
comparison
equal
deleted
inserted
replaced
492:5114bbb5daa3 | 493:b3cedbee561d |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 | |
4 # Libervia Desktop-Kivy | |
5 # Copyright (C) 2016-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 from .constants import Const as C | |
22 from libervia.backend.core.log import getLogger | |
23 from libervia.frontends.tools import xmlui | |
24 from kivy.uix.scrollview import ScrollView | |
25 from kivy.uix.boxlayout import BoxLayout | |
26 from kivy.uix.gridlayout import GridLayout | |
27 from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem | |
28 from kivy.uix.textinput import TextInput | |
29 from kivy.uix.label import Label | |
30 from kivy.uix.button import Button | |
31 from kivy.uix.togglebutton import ToggleButton | |
32 from kivy.uix.widget import Widget | |
33 from kivy.uix.switch import Switch | |
34 from kivy import properties | |
35 from libervia.desktop_kivy import G | |
36 from libervia.desktop_kivy.core import dialog | |
37 from functools import partial | |
38 | |
39 log = getLogger(__name__) | |
40 | |
41 ## Widgets ## | |
42 | |
43 | |
44 class TextInputOnChange(object): | |
45 | |
46 def __init__(self): | |
47 self._xmlui_onchange_cb = None | |
48 self._got_focus = False | |
49 | |
50 def _xmlui_on_change(self, callback): | |
51 self._xmlui_onchange_cb = callback | |
52 | |
53 def on_focus(self, instance, focus): | |
54 # we need to wait for first focus, else initial value | |
55 # will trigger a on_text | |
56 if not self._got_focus and focus: | |
57 self._got_focus = True | |
58 | |
59 def on_text(self, instance, new_text): | |
60 if self._xmlui_onchange_cb is not None and self._got_focus: | |
61 self._xmlui_onchange_cb(self) | |
62 | |
63 | |
64 class EmptyWidget(xmlui.EmptyWidget, Widget): | |
65 | |
66 def __init__(self, _xmlui_parent): | |
67 Widget.__init__(self) | |
68 | |
69 | |
70 class TextWidget(xmlui.TextWidget, Label): | |
71 | |
72 def __init__(self, xmlui_parent, value): | |
73 Label.__init__(self, text=value) | |
74 | |
75 | |
76 class LabelWidget(xmlui.LabelWidget, TextWidget): | |
77 pass | |
78 | |
79 | |
80 class JidWidget(xmlui.JidWidget, TextWidget): | |
81 pass | |
82 | |
83 | |
84 class StringWidget(xmlui.StringWidget, TextInput, TextInputOnChange): | |
85 | |
86 def __init__(self, xmlui_parent, value, read_only=False): | |
87 TextInput.__init__(self, text=value) | |
88 TextInputOnChange.__init__(self) | |
89 self.readonly = read_only | |
90 | |
91 def _xmlui_set_value(self, value): | |
92 self.text = value | |
93 | |
94 def _xmlui_get_value(self): | |
95 return self.text | |
96 | |
97 | |
98 class TextBoxWidget(xmlui.TextBoxWidget, StringWidget): | |
99 pass | |
100 | |
101 | |
102 class JidInputWidget(xmlui.JidInputWidget, StringWidget): | |
103 pass | |
104 | |
105 | |
106 class ButtonWidget(xmlui.ButtonWidget, Button): | |
107 | |
108 def __init__(self, _xmlui_parent, value, click_callback): | |
109 Button.__init__(self) | |
110 self.text = value | |
111 self.callback = click_callback | |
112 | |
113 def _xmlui_on_click(self, callback): | |
114 self.callback = callback | |
115 | |
116 def on_release(self): | |
117 self.callback(self) | |
118 | |
119 | |
120 class DividerWidget(xmlui.DividerWidget, Widget): | |
121 # FIXME: not working properly + only 'line' is handled | |
122 style = properties.OptionProperty('line', | |
123 options=['line', 'dot', 'dash', 'plain', 'blank']) | |
124 | |
125 def __init__(self, _xmlui_parent, style="line"): | |
126 Widget.__init__(self, style=style) | |
127 | |
128 | |
129 class ListWidgetItem(ToggleButton): | |
130 value = properties.StringProperty() | |
131 | |
132 def on_release(self): | |
133 parent = self.parent | |
134 while parent is not None and not isinstance(parent, ListWidget): | |
135 parent = parent.parent | |
136 | |
137 if parent is not None: | |
138 parent.select(self) | |
139 return super(ListWidgetItem, self).on_release() | |
140 | |
141 @property | |
142 def selected(self): | |
143 return self.state == 'down' | |
144 | |
145 @selected.setter | |
146 def selected(self, value): | |
147 self.state = 'down' if value else 'normal' | |
148 | |
149 | |
150 class ListWidget(xmlui.ListWidget, ScrollView): | |
151 layout = properties.ObjectProperty() | |
152 | |
153 def __init__(self, _xmlui_parent, options, selected, flags): | |
154 ScrollView.__init__(self) | |
155 self.multi = 'single' not in flags | |
156 self._values = [] | |
157 for option in options: | |
158 self.add_value(option) | |
159 self._xmlui_select_values(selected) | |
160 self._on_change = None | |
161 | |
162 @property | |
163 def items(self): | |
164 return self.layout.children | |
165 | |
166 def select(self, item): | |
167 if not self.multi: | |
168 self._xmlui_select_values([item.value]) | |
169 if self._on_change is not None: | |
170 self._on_change(self) | |
171 | |
172 def add_value(self, option, selected=False): | |
173 """add a value in the list | |
174 | |
175 @param option(tuple): value, label in a tuple | |
176 """ | |
177 self._values.append(option) | |
178 item = ListWidgetItem() | |
179 item.value, item.text = option | |
180 item.selected = selected | |
181 self.layout.add_widget(item) | |
182 | |
183 def _xmlui_select_value(self, value): | |
184 self._xmlui_select_values([value]) | |
185 | |
186 def _xmlui_select_values(self, values): | |
187 for item in self.items: | |
188 item.selected = item.value in values | |
189 if item.selected and not self.multi: | |
190 self.text = item.text | |
191 | |
192 def _xmlui_get_selected_values(self): | |
193 return [item.value for item in self.items if item.selected] | |
194 | |
195 def _xmlui_add_values(self, values, select=True): | |
196 values = set(values).difference([c.value for c in self.items]) | |
197 for v in values: | |
198 self.add_value(v, select) | |
199 | |
200 def _xmlui_on_change(self, callback): | |
201 self._on_change = callback | |
202 | |
203 | |
204 class JidsListWidget(ListWidget): | |
205 # TODO: real list dedicated to jids | |
206 | |
207 def __init__(self, _xmlui_parent, jids, flags): | |
208 ListWidget.__init__(self, _xmlui_parent, [(j,j) for j in jids], [], flags) | |
209 | |
210 | |
211 class PasswordWidget(xmlui.PasswordWidget, TextInput, TextInputOnChange): | |
212 | |
213 def __init__(self, _xmlui_parent, value, read_only=False): | |
214 TextInput.__init__(self, password=True, multiline=False, | |
215 text=value, readonly=read_only, size=(100,25), size_hint=(1,None)) | |
216 TextInputOnChange.__init__(self) | |
217 | |
218 def _xmlui_set_value(self, value): | |
219 self.text = value | |
220 | |
221 def _xmlui_get_value(self): | |
222 return self.text | |
223 | |
224 | |
225 class BoolWidget(xmlui.BoolWidget, Switch): | |
226 | |
227 def __init__(self, _xmlui_parent, state, read_only=False): | |
228 Switch.__init__(self, active=state) | |
229 if read_only: | |
230 self.disabled = True | |
231 | |
232 def _xmlui_set_value(self, value): | |
233 self.active = value | |
234 | |
235 def _xmlui_get_value(self): | |
236 return C.BOOL_TRUE if self.active else C.BOOL_FALSE | |
237 | |
238 def _xmlui_on_change(self, callback): | |
239 self.bind(active=lambda instance, value: callback(instance)) | |
240 | |
241 | |
242 class IntWidget(xmlui.IntWidget, TextInput, TextInputOnChange): | |
243 | |
244 def __init__(self, _xmlui_parent, value, read_only=False): | |
245 TextInput.__init__(self, text=value, input_filter='int', multiline=False) | |
246 TextInputOnChange.__init__(self) | |
247 if read_only: | |
248 self.disabled = True | |
249 | |
250 def _xmlui_set_value(self, value): | |
251 self.text = value | |
252 | |
253 def _xmlui_get_value(self): | |
254 return self.text | |
255 | |
256 | |
257 ## Containers ## | |
258 | |
259 | |
260 class VerticalContainer(xmlui.VerticalContainer, BoxLayout): | |
261 | |
262 def __init__(self, xmlui_parent): | |
263 self.xmlui_parent = xmlui_parent | |
264 BoxLayout.__init__(self) | |
265 | |
266 def _xmlui_append(self, widget): | |
267 self.add_widget(widget) | |
268 | |
269 | |
270 class PairsContainer(xmlui.PairsContainer, GridLayout): | |
271 | |
272 def __init__(self, xmlui_parent): | |
273 self.xmlui_parent = xmlui_parent | |
274 GridLayout.__init__(self) | |
275 | |
276 def _xmlui_append(self, widget): | |
277 self.add_widget(widget) | |
278 | |
279 | |
280 class LabelContainer(PairsContainer, xmlui.LabelContainer): | |
281 pass | |
282 | |
283 | |
284 class TabsPanelContainer(TabbedPanelItem): | |
285 layout = properties.ObjectProperty(None) | |
286 | |
287 def _xmlui_append(self, widget): | |
288 self.layout.add_widget(widget) | |
289 | |
290 | |
291 class TabsContainer(xmlui.TabsContainer, TabbedPanel): | |
292 | |
293 def __init__(self, xmlui_parent): | |
294 self.xmlui_parent = xmlui_parent | |
295 TabbedPanel.__init__(self, do_default_tab=False) | |
296 | |
297 def _xmlui_add_tab(self, label, selected): | |
298 tab = TabsPanelContainer(text=label) | |
299 self.add_widget(tab) | |
300 return tab | |
301 | |
302 | |
303 class AdvancedListRow(BoxLayout): | |
304 global_index = 0 | |
305 index = properties.ObjectProperty() | |
306 selected = properties.BooleanProperty(False) | |
307 | |
308 def __init__(self, **kwargs): | |
309 self.global_index = AdvancedListRow.global_index | |
310 AdvancedListRow.global_index += 1 | |
311 super(AdvancedListRow, self).__init__(**kwargs) | |
312 | |
313 def on_touch_down(self, touch): | |
314 if self.collide_point(*touch.pos): | |
315 parent = self.parent | |
316 while parent is not None and not isinstance(parent, AdvancedListContainer): | |
317 parent = parent.parent | |
318 if parent is None: | |
319 log.error("Can't find parent AdvancedListContainer") | |
320 else: | |
321 if parent.selectable: | |
322 self.selected = parent._xmlui_toggle_selected(self) | |
323 | |
324 return super(AdvancedListRow, self).on_touch_down(touch) | |
325 | |
326 | |
327 class AdvancedListContainer(xmlui.AdvancedListContainer, BoxLayout): | |
328 | |
329 def __init__(self, xmlui_parent, columns, selectable='no'): | |
330 self.xmlui_parent = xmlui_parent | |
331 BoxLayout.__init__(self) | |
332 self._columns = columns | |
333 self.selectable = selectable != 'no' | |
334 self._current_row = None | |
335 self._selected = [] | |
336 self._xmlui_select_cb = None | |
337 | |
338 def _xmlui_toggle_selected(self, row): | |
339 """inverse selection status of an AdvancedListRow | |
340 | |
341 @param row(AdvancedListRow): row to (un)select | |
342 @return (bool): True if row is selected | |
343 """ | |
344 try: | |
345 self._selected.remove(row) | |
346 except ValueError: | |
347 self._selected.append(row) | |
348 if self._xmlui_select_cb is not None: | |
349 self._xmlui_select_cb(self) | |
350 return True | |
351 else: | |
352 return False | |
353 | |
354 def _xmlui_append(self, widget): | |
355 if self._current_row is None: | |
356 log.error("No row set, ignoring append") | |
357 return | |
358 self._current_row.add_widget(widget) | |
359 | |
360 def _xmlui_add_row(self, idx): | |
361 self._current_row = AdvancedListRow() | |
362 self._current_row.cols = self._columns | |
363 self._current_row.index = idx | |
364 self.add_widget(self._current_row) | |
365 | |
366 def _xmlui_get_selected_widgets(self): | |
367 return self._selected | |
368 | |
369 def _xmlui_get_selected_index(self): | |
370 if not self._selected: | |
371 return None | |
372 return self._selected[0].index | |
373 | |
374 def _xmlui_on_select(self, callback): | |
375 """ Call callback with widget as only argument """ | |
376 self._xmlui_select_cb = callback | |
377 | |
378 | |
379 ## Dialogs ## | |
380 | |
381 | |
382 class NoteDialog(xmlui.NoteDialog): | |
383 | |
384 def __init__(self, _xmlui_parent, title, message, level): | |
385 xmlui.NoteDialog.__init__(self, _xmlui_parent) | |
386 self.title, self.message, self.level = title, message, level | |
387 | |
388 def _xmlui_show(self): | |
389 G.host.add_note(self.title, self.message, self.level) | |
390 | |
391 | |
392 class MessageDialog(xmlui.MessageDialog, dialog.MessageDialog): | |
393 | |
394 def __init__(self, _xmlui_parent, title, message, level): | |
395 dialog.MessageDialog.__init__(self, | |
396 title=title, | |
397 message=message, | |
398 level=level, | |
399 close_cb = self.close_cb) | |
400 xmlui.MessageDialog.__init__(self, _xmlui_parent) | |
401 | |
402 def close_cb(self): | |
403 self._xmlui_close() | |
404 | |
405 def _xmlui_show(self): | |
406 G.host.add_notif_ui(self) | |
407 | |
408 def _xmlui_close(self, reason=None): | |
409 G.host.close_ui() | |
410 | |
411 def show(self, *args, **kwargs): | |
412 G.host.show_ui(self) | |
413 | |
414 | |
415 class ConfirmDialog(xmlui.ConfirmDialog, dialog.ConfirmDialog): | |
416 | |
417 def __init__(self, _xmlui_parent, title, message, level, buttons_set): | |
418 dialog.ConfirmDialog.__init__(self) | |
419 xmlui.ConfirmDialog.__init__(self, _xmlui_parent) | |
420 self.title=title | |
421 self.message=message | |
422 self.no_cb = self.no_cb | |
423 self.yes_cb = self.yes_cb | |
424 | |
425 def no_cb(self): | |
426 G.host.close_ui() | |
427 self._xmlui_cancelled() | |
428 | |
429 def yes_cb(self): | |
430 G.host.close_ui() | |
431 self._xmlui_validated() | |
432 | |
433 def _xmlui_show(self): | |
434 G.host.add_notif_ui(self) | |
435 | |
436 def _xmlui_close(self, reason=None): | |
437 G.host.close_ui() | |
438 | |
439 def show(self, *args, **kwargs): | |
440 assert kwargs["force"] | |
441 G.host.show_ui(self) | |
442 | |
443 | |
444 class FileDialog(xmlui.FileDialog, BoxLayout): | |
445 message = properties.ObjectProperty() | |
446 | |
447 def __init__(self, _xmlui_parent, title, message, level, filetype): | |
448 xmlui.FileDialog.__init__(self, _xmlui_parent) | |
449 BoxLayout.__init__(self) | |
450 self.message.text = message | |
451 if filetype == C.XMLUI_DATA_FILETYPE_DIR: | |
452 self.file_chooser.dirselect = True | |
453 | |
454 def _xmlui_show(self): | |
455 G.host.add_notif_ui(self) | |
456 | |
457 def _xmlui_close(self, reason=None): | |
458 # FIXME: notif UI is not removed if dialog is not shown yet | |
459 G.host.close_ui() | |
460 | |
461 def on_select(self, path): | |
462 try: | |
463 path = path[0] | |
464 except IndexError: | |
465 path = None | |
466 if not path: | |
467 self._xmlui_cancelled() | |
468 else: | |
469 self._xmlui_validated({'path': path}) | |
470 | |
471 def show(self, *args, **kwargs): | |
472 assert kwargs["force"] | |
473 G.host.show_ui(self) | |
474 | |
475 | |
476 ## Factory ## | |
477 | |
478 | |
479 class WidgetFactory(object): | |
480 | |
481 def __getattr__(self, attr): | |
482 if attr.startswith("create"): | |
483 cls = globals()[attr[6:]] | |
484 return cls | |
485 | |
486 | |
487 ## Core ## | |
488 | |
489 | |
490 class Title(Label): | |
491 | |
492 def __init__(self, *args, **kwargs): | |
493 kwargs['size'] = (100, 25) | |
494 kwargs['size_hint'] = (1,None) | |
495 super(Title, self).__init__(*args, **kwargs) | |
496 | |
497 | |
498 class FormButton(Button): | |
499 pass | |
500 | |
501 class SubmitButton(FormButton): | |
502 pass | |
503 | |
504 class CancelButton(FormButton): | |
505 pass | |
506 | |
507 class SaveButton(FormButton): | |
508 pass | |
509 | |
510 | |
511 class XMLUIPanel(xmlui.XMLUIPanel, ScrollView): | |
512 widget_factory = WidgetFactory() | |
513 layout = properties.ObjectProperty() | |
514 | |
515 def __init__(self, host, parsed_xml, title=None, flags=None, callback=None, | |
516 ignore=None, whitelist=None, profile=C.PROF_KEY_NONE): | |
517 ScrollView.__init__(self) | |
518 self.close_cb = None | |
519 self._post_treats = [] # list of callback to call after UI is constructed | |
520 | |
521 # used to workaround touch issues when a ScrollView is used inside this | |
522 # one. This happens notably when a TabsContainer is used as main container | |
523 # (this is the case with settings). | |
524 self._skip_scroll_events = False | |
525 xmlui.XMLUIPanel.__init__(self, | |
526 host, | |
527 parsed_xml, | |
528 title=title, | |
529 flags=flags, | |
530 callback=callback, | |
531 ignore=ignore, | |
532 whitelist=whitelist, | |
533 profile=profile) | |
534 self.bind(height=self.on_height) | |
535 | |
536 def on_touch_down(self, touch, after=False): | |
537 if self._skip_scroll_events: | |
538 return super(ScrollView, self).on_touch_down(touch) | |
539 else: | |
540 return super(XMLUIPanel, self).on_touch_down(touch) | |
541 | |
542 def on_touch_up(self, touch, after=False): | |
543 if self._skip_scroll_events: | |
544 return super(ScrollView, self).on_touch_up(touch) | |
545 else: | |
546 return super(XMLUIPanel, self).on_touch_up(touch) | |
547 | |
548 def on_touch_move(self, touch, after=False): | |
549 if self._skip_scroll_events: | |
550 return super(ScrollView, self).on_touch_move(touch) | |
551 else: | |
552 return super(XMLUIPanel, self).on_touch_move(touch) | |
553 | |
554 def set_close_cb(self, close_cb): | |
555 self.close_cb = close_cb | |
556 | |
557 def _xmlui_close(self, __=None, reason=None): | |
558 if self.close_cb is not None: | |
559 self.close_cb(self, reason) | |
560 else: | |
561 G.host.close_ui() | |
562 | |
563 def on_param_change(self, ctrl): | |
564 super(XMLUIPanel, self).on_param_change(ctrl) | |
565 self.save_btn.disabled = False | |
566 | |
567 def add_post_treat(self, callback): | |
568 self._post_treats.append(callback) | |
569 | |
570 def _post_treat_cb(self): | |
571 for cb in self._post_treats: | |
572 cb() | |
573 del self._post_treats | |
574 | |
575 def _save_button_cb(self, button): | |
576 button.disabled = True | |
577 self.on_save_params(button) | |
578 | |
579 def construct_ui(self, parsed_dom): | |
580 xmlui.XMLUIPanel.construct_ui(self, parsed_dom, self._post_treat_cb) | |
581 if self.xmlui_title: | |
582 self.layout.add_widget(Title(text=self.xmlui_title)) | |
583 if isinstance(self.main_cont, TabsContainer): | |
584 # cf. comments above | |
585 self._skip_scroll_events = True | |
586 self.layout.add_widget(self.main_cont) | |
587 if self.type == 'form': | |
588 submit_btn = SubmitButton() | |
589 submit_btn.bind(on_press=self.on_form_submitted) | |
590 self.layout.add_widget(submit_btn) | |
591 if not 'NO_CANCEL' in self.flags: | |
592 cancel_btn = CancelButton(text=_("Cancel")) | |
593 cancel_btn.bind(on_press=self.on_form_cancelled) | |
594 self.layout.add_widget(cancel_btn) | |
595 elif self.type == 'param': | |
596 self.save_btn = SaveButton(text=_("Save"), disabled=True) | |
597 self.save_btn.bind(on_press=self._save_button_cb) | |
598 self.layout.add_widget(self.save_btn) | |
599 elif self.type == 'window': | |
600 cancel_btn = CancelButton(text=_("Cancel")) | |
601 cancel_btn.bind( | |
602 on_press=partial(self._xmlui_close, reason=C.XMLUI_DATA_CANCELLED)) | |
603 self.layout.add_widget(cancel_btn) | |
604 | |
605 def on_height(self, __, height): | |
606 if isinstance(self.main_cont, TabsContainer): | |
607 other_children_height = sum([c.height for c in self.layout.children | |
608 if c is not self.main_cont]) | |
609 self.main_cont.height = height - other_children_height | |
610 | |
611 def show(self, *args, **kwargs): | |
612 if not self.user_action and not kwargs.get("force", False): | |
613 G.host.add_notif_ui(self) | |
614 else: | |
615 G.host.show_ui(self) | |
616 | |
617 | |
618 class XMLUIDialog(xmlui.XMLUIDialog): | |
619 dialog_factory = WidgetFactory() | |
620 | |
621 | |
622 create = partial(xmlui.create, class_map={ | |
623 xmlui.CLASS_PANEL: XMLUIPanel, | |
624 xmlui.CLASS_DIALOG: XMLUIDialog}) |