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})