Mercurial > libervia-desktop-kivy
comparison cagou/core/xmlui.py @ 126:cd99f70ea592
global file reorganisation:
- follow common convention by puttin cagou in "cagou" instead of "src/cagou"
- added VERSION in cagou with current version
- updated dates
- moved main executable in /bin
- moved buildozer files in root directory
- temporary moved platform to assets/platform
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 05 Apr 2018 17:11:21 +0200 |
parents | src/cagou/core/xmlui.py@b6e6afb0dc46 |
children | 0704f3be65cb |
comparison
equal
deleted
inserted
replaced
125:b6e6afb0dc46 | 126:cd99f70ea592 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # Cagou: a SàT frontend | |
5 # Copyright (C) 2016-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.i18n import _ | |
21 from .constants import Const as C | |
22 from sat.core.log import getLogger | |
23 log = getLogger(__name__) | |
24 from sat_frontends.tools import xmlui | |
25 from kivy.uix.scrollview import ScrollView | |
26 from kivy.uix.boxlayout import BoxLayout | |
27 from kivy.uix.gridlayout import GridLayout | |
28 from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem | |
29 from kivy.uix.textinput import TextInput | |
30 from kivy.uix.label import Label | |
31 from kivy.uix.button import Button | |
32 from kivy.uix.togglebutton import ToggleButton | |
33 from kivy.uix.widget import Widget | |
34 from kivy.uix.dropdown import DropDown | |
35 from kivy.uix.switch import Switch | |
36 from kivy import properties | |
37 from cagou import G | |
38 | |
39 | |
40 ## Widgets ## | |
41 | |
42 | |
43 class TextInputOnChange(object): | |
44 | |
45 def __init__(self): | |
46 self._xmlui_onchange_cb = None | |
47 self._got_focus = False | |
48 | |
49 def _xmluiOnChange(self, callback): | |
50 self._xmlui_onchange_cb = callback | |
51 | |
52 def on_focus(self, instance, focus): | |
53 # we need to wait for first focus, else initial value | |
54 # will trigger a on_text | |
55 if not self._got_focus and focus: | |
56 self._got_focus = True | |
57 | |
58 def on_text(self, instance, new_text): | |
59 log.debug("on_text: %s" % 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, multiline=False) | |
88 TextInputOnChange.__init__(self) | |
89 self.readonly = read_only | |
90 | |
91 def _xmluiSetValue(self, value): | |
92 self.text = value | |
93 | |
94 def _xmluiGetValue(self): | |
95 return self.text | |
96 | |
97 | |
98 class JidInputWidget(xmlui.JidInputWidget, StringWidget): | |
99 pass | |
100 | |
101 | |
102 class ButtonWidget(xmlui.ButtonWidget, Button): | |
103 | |
104 def __init__(self, _xmlui_parent, value, click_callback): | |
105 Button.__init__(self) | |
106 self.text = value | |
107 self.callback = click_callback | |
108 | |
109 def _xmluiOnClick(self, callback): | |
110 self.callback = callback | |
111 | |
112 def on_release(self): | |
113 self.callback(self) | |
114 | |
115 | |
116 class DividerWidget(xmlui.DividerWidget, Widget): | |
117 # FIXME: not working properly + only 'line' is handled | |
118 style = properties.OptionProperty('line', | |
119 options=['line', 'dot', 'dash', 'plain', 'blank']) | |
120 | |
121 def __init__(self, _xmlui_parent, style="line"): | |
122 Widget.__init__(self, style=style) | |
123 | |
124 | |
125 class ListWidgetItem(ToggleButton): | |
126 value = properties.StringProperty() | |
127 | |
128 def on_release(self): | |
129 super(ListWidgetItem, self).on_release() | |
130 parent = self.parent | |
131 while parent is not None and not isinstance(parent, DropDown): | |
132 parent = parent.parent | |
133 | |
134 if parent is not None and parent.attach_to is not None: | |
135 parent.select(self) | |
136 | |
137 @property | |
138 def selected(self): | |
139 return self.state == 'down' | |
140 | |
141 @selected.setter | |
142 def selected(self, value): | |
143 self.state = 'down' if value else 'normal' | |
144 | |
145 | |
146 class ListWidget(xmlui.ListWidget, Button): | |
147 | |
148 def __init__(self, _xmlui_parent, options, selected, flags): | |
149 Button.__init__(self) | |
150 self.text = _(u"open list") | |
151 self._dropdown = DropDown() | |
152 self._dropdown.auto_dismiss = False | |
153 self._dropdown.bind(on_select = self.on_select) | |
154 self.multi = 'single' not in flags | |
155 self._dropdown.dismiss_on_select = not self.multi | |
156 self._values = [] | |
157 for option in options: | |
158 self.addValue(option) | |
159 self._xmluiSelectValues(selected) | |
160 self._on_change = None | |
161 | |
162 @property | |
163 def items(self): | |
164 return self._dropdown.children[0].children | |
165 | |
166 def on_touch_down(self, touch): | |
167 # we simulate auto-dismiss ourself because dropdown | |
168 # will dismiss even if attached button is touched | |
169 # resulting in a dismiss just before a toggle in on_release | |
170 # so the dropbox would always be opened, we don't want that! | |
171 if super(ListWidget, self).on_touch_down(touch): | |
172 return True | |
173 if self._dropdown.parent: | |
174 self._dropdown.dismiss() | |
175 | |
176 def on_release(self): | |
177 if self._dropdown.parent is not None: | |
178 # we want to close a list already opened | |
179 self._dropdown.dismiss() | |
180 else: | |
181 self._dropdown.open(self) | |
182 | |
183 def on_select(self, drop_down, item): | |
184 if not self.multi: | |
185 self._xmluiSelectValues([item.value]) | |
186 if self._on_change is not None: | |
187 self._on_change(self) | |
188 | |
189 def addValue(self, option, selected=False): | |
190 """add a value in the list | |
191 | |
192 @param option(tuple): value, label in a tuple | |
193 """ | |
194 self._values.append(option) | |
195 item = ListWidgetItem() | |
196 item.value, item.text = option | |
197 item.selected = selected | |
198 self._dropdown.add_widget(item) | |
199 | |
200 def _xmluiSelectValue(self, value): | |
201 self._xmluiSelectValues([value]) | |
202 | |
203 def _xmluiSelectValues(self, values): | |
204 for item in self.items: | |
205 item.selected = item.value in values | |
206 if item.selected and not self.multi: | |
207 self.text = item.text | |
208 | |
209 def _xmluiGetSelectedValues(self): | |
210 return [item.value for item in self.items if item.selected] | |
211 | |
212 def _xmluiAddValues(self, values, select=True): | |
213 values = set(values).difference([c.value for c in self.items]) | |
214 for v in values: | |
215 self.addValue(v, select) | |
216 | |
217 def _xmluiOnChange(self, callback): | |
218 self._on_change = callback | |
219 | |
220 | |
221 class JidsListWidget(ListWidget): | |
222 # TODO: real list dedicated to jids | |
223 | |
224 def __init__(self, _xmlui_parent, jids, flags): | |
225 ListWidget.__init__(self, _xmlui_parent, [(j,j) for j in jids], [], flags) | |
226 | |
227 | |
228 class PasswordWidget(xmlui.PasswordWidget, TextInput, TextInputOnChange): | |
229 | |
230 def __init__(self, _xmlui_parent, value, read_only=False): | |
231 TextInput.__init__(self, password=True, multiline=False, | |
232 text=value, readonly=read_only, size=(100,25), size_hint=(1,None)) | |
233 TextInputOnChange.__init__(self) | |
234 | |
235 def _xmluiSetValue(self, value): | |
236 self.text = value | |
237 | |
238 def _xmluiGetValue(self): | |
239 return self.text | |
240 | |
241 | |
242 class BoolWidget(xmlui.BoolWidget, Switch): | |
243 | |
244 def __init__(self, _xmlui_parent, state, read_only=False): | |
245 Switch.__init__(self, active=state) | |
246 if read_only: | |
247 self.disabled = True | |
248 | |
249 def _xmluiSetValue(self, value): | |
250 self.active = value | |
251 | |
252 def _xmluiGetValue(self): | |
253 return C.BOOL_TRUE if self.active else C.BOOL_FALSE | |
254 | |
255 def _xmluiOnChange(self, callback): | |
256 self.bind(active=lambda instance, value: callback(instance)) | |
257 | |
258 | |
259 class IntWidget(xmlui.IntWidget, TextInput, TextInputOnChange): | |
260 | |
261 def __init__(self, _xmlui_parent, value, read_only=False): | |
262 TextInput.__init__(self, text=value, input_filter='int', multiline=False) | |
263 TextInputOnChange.__init__(self) | |
264 if read_only: | |
265 self.disabled = True | |
266 | |
267 def _xmluiSetValue(self, value): | |
268 self.text = value | |
269 | |
270 def _xmluiGetValue(self): | |
271 return self.text | |
272 | |
273 | |
274 ## Containers ## | |
275 | |
276 | |
277 class VerticalContainer(xmlui.VerticalContainer, GridLayout): | |
278 | |
279 def __init__(self, xmlui_parent): | |
280 self.xmlui_parent = xmlui_parent | |
281 GridLayout.__init__(self) | |
282 | |
283 def _xmluiAppend(self, widget): | |
284 self.add_widget(widget) | |
285 | |
286 | |
287 class PairsContainer(xmlui.PairsContainer, GridLayout): | |
288 | |
289 def __init__(self, xmlui_parent): | |
290 self.xmlui_parent = xmlui_parent | |
291 GridLayout.__init__(self) | |
292 | |
293 def _xmluiAppend(self, widget): | |
294 self.add_widget(widget) | |
295 | |
296 | |
297 class LabelContainer(PairsContainer, xmlui.LabelContainer): | |
298 pass | |
299 | |
300 | |
301 class TabsPanelContainer(TabbedPanelItem): | |
302 | |
303 def _xmluiAppend(self, widget): | |
304 self.add_widget(widget) | |
305 | |
306 | |
307 class TabsContainer(xmlui.TabsContainer, TabbedPanel): | |
308 | |
309 def __init__(self, xmlui_parent): | |
310 self.xmlui_parent = xmlui_parent | |
311 xmlui_panel = xmlui_parent | |
312 while not isinstance(xmlui_panel, XMLUIPanel): | |
313 xmlui_panel = xmlui_panel.xmlui_parent | |
314 xmlui_panel.addPostTreat(self._postTreat) | |
315 TabbedPanel.__init__(self, do_default_tab=False) | |
316 | |
317 def _xmluiAddTab(self, label, selected): | |
318 tab = TabsPanelContainer(text=label) | |
319 self.add_widget(tab) | |
320 return tab | |
321 | |
322 def _postTreat(self): | |
323 """bind minimum height of tabs' content so self.height is adapted""" | |
324 # we need to do this in postTreat because contents exists after UI construction | |
325 for t in self.tab_list: | |
326 t.content.bind(minimum_height=self._updateHeight) | |
327 | |
328 def _updateHeight(self, instance, height): | |
329 """Called after UI is constructed (so height can be calculated)""" | |
330 # needed because TabbedPanel doesn't have a minimum_height property | |
331 self.height = max([t.content.minimum_height for t in self.tab_list]) + self.tab_height + 5 | |
332 | |
333 | |
334 class AdvancedListRow(GridLayout): | |
335 global_index = 0 | |
336 index = properties.ObjectProperty() | |
337 selected = properties.BooleanProperty(False) | |
338 | |
339 def __init__(self, **kwargs): | |
340 self.global_index = AdvancedListRow.global_index | |
341 AdvancedListRow.global_index += 1 | |
342 super(AdvancedListRow, self).__init__(**kwargs) | |
343 | |
344 def on_touch_down(self, touch): | |
345 if self.collide_point(*touch.pos): | |
346 parent = self.parent | |
347 while parent is not None and not isinstance(parent, AdvancedListContainer): | |
348 parent = parent.parent | |
349 if parent is None: | |
350 log.error(u"Can't find parent AdvancedListContainer") | |
351 else: | |
352 if parent.selectable: | |
353 self.selected = parent._xmluiToggleSelected(self) | |
354 | |
355 return super(AdvancedListRow, self).on_touch_down(touch) | |
356 | |
357 | |
358 class AdvancedListContainer(xmlui.AdvancedListContainer, GridLayout): | |
359 | |
360 def __init__(self, xmlui_parent, columns, selectable='no'): | |
361 self.xmlui_parent = xmlui_parent | |
362 GridLayout.__init__(self) | |
363 self._columns = columns | |
364 self.selectable = selectable != 'no' | |
365 self._current_row = None | |
366 self._selected = [] | |
367 self._xmlui_select_cb = None | |
368 | |
369 def _xmluiToggleSelected(self, row): | |
370 """inverse selection status of an AdvancedListRow | |
371 | |
372 @param row(AdvancedListRow): row to (un)select | |
373 @return (bool): True if row is selected | |
374 """ | |
375 try: | |
376 self._selected.remove(row) | |
377 except ValueError: | |
378 self._selected.append(row) | |
379 if self._xmlui_select_cb is not None: | |
380 self._xmlui_select_cb(self) | |
381 return True | |
382 else: | |
383 return False | |
384 | |
385 def _xmluiAppend(self, widget): | |
386 if self._current_row is None: | |
387 log.error(u"No row set, ignoring append") | |
388 return | |
389 self._current_row.add_widget(widget) | |
390 | |
391 def _xmluiAddRow(self, idx): | |
392 self._current_row = AdvancedListRow() | |
393 self._current_row.cols = self._columns | |
394 self._current_row.index = idx | |
395 self.add_widget(self._current_row) | |
396 | |
397 def _xmluiGetSelectedWidgets(self): | |
398 return self._selected | |
399 | |
400 def _xmluiGetSelectedIndex(self): | |
401 if not self._selected: | |
402 return None | |
403 return self._selected[0].index | |
404 | |
405 def _xmluiOnSelect(self, callback): | |
406 """ Call callback with widget as only argument """ | |
407 self._xmlui_select_cb = callback | |
408 | |
409 ## Dialogs ## | |
410 | |
411 | |
412 class NoteDialog(xmlui.NoteDialog): | |
413 | |
414 def __init__(self, _xmlui_parent, title, message, level): | |
415 xmlui.NoteDialog.__init__(self, _xmlui_parent) | |
416 self.title, self.message, self.level = title, message, level | |
417 | |
418 def _xmluiShow(self): | |
419 G.host.addNote(self.title, self.message, self.level) | |
420 | |
421 | |
422 class FileDialog(xmlui.FileDialog, BoxLayout): | |
423 message = properties.ObjectProperty() | |
424 | |
425 def __init__(self, _xmlui_parent, title, message, level, filetype): | |
426 xmlui.FileDialog.__init__(self, _xmlui_parent) | |
427 BoxLayout.__init__(self) | |
428 self.message.text = message | |
429 if filetype == C.XMLUI_DATA_FILETYPE_DIR: | |
430 self.file_chooser.dirselect = True | |
431 | |
432 def _xmluiShow(self): | |
433 G.host.addNotifUI(self) | |
434 | |
435 def _xmluiClose(self): | |
436 # FIXME: notif UI is not removed if dialog is not shown yet | |
437 G.host.closeUI() | |
438 | |
439 def onSelect(self, path): | |
440 try: | |
441 path = path[0] | |
442 except IndexError: | |
443 path = None | |
444 if not path: | |
445 self._xmluiCancelled() | |
446 else: | |
447 self._xmluiValidated({'path': path}) | |
448 | |
449 def show(self, *args, **kwargs): | |
450 assert kwargs["force"] | |
451 G.host.showUI(self) | |
452 | |
453 | |
454 ## Factory ## | |
455 | |
456 | |
457 class WidgetFactory(object): | |
458 | |
459 def __getattr__(self, attr): | |
460 if attr.startswith("create"): | |
461 cls = globals()[attr[6:]] | |
462 return cls | |
463 | |
464 | |
465 ## Core ## | |
466 | |
467 | |
468 class Title(Label): | |
469 | |
470 def __init__(self, *args, **kwargs): | |
471 kwargs['size'] = (100, 25) | |
472 kwargs['size_hint'] = (1,None) | |
473 super(Title, self).__init__(*args, **kwargs) | |
474 | |
475 | |
476 class FormButton(Button): | |
477 pass | |
478 | |
479 | |
480 class XMLUIPanelGrid(GridLayout): | |
481 pass | |
482 | |
483 class XMLUIPanel(xmlui.XMLUIPanel, ScrollView): | |
484 widget_factory = WidgetFactory() | |
485 | |
486 def __init__(self, host, parsed_xml, title=None, flags=None, callback=None, ignore=None, profile=C.PROF_KEY_NONE): | |
487 ScrollView.__init__(self) | |
488 self.close_cb = None | |
489 self._grid = XMLUIPanelGrid() | |
490 self._post_treats = [] # list of callback to call after UI is constructed | |
491 ScrollView.add_widget(self, self._grid) | |
492 xmlui.XMLUIPanel.__init__(self, | |
493 host, | |
494 parsed_xml, | |
495 title=title, | |
496 flags=flags, | |
497 callback=callback, | |
498 ignore=ignore, | |
499 profile=profile) | |
500 | |
501 def add_widget(self, wid): | |
502 self._grid.add_widget(wid) | |
503 | |
504 def setCloseCb(self, close_cb): | |
505 self.close_cb = close_cb | |
506 | |
507 def _xmluiClose(self): | |
508 if self.close_cb is not None: | |
509 self.close_cb(self) | |
510 else: | |
511 G.host.closeUI() | |
512 | |
513 def onParamChange(self, ctrl): | |
514 super(XMLUIPanel, self).onParamChange(ctrl) | |
515 self.save_btn.disabled = False | |
516 | |
517 def addPostTreat(self, callback): | |
518 self._post_treats.append(callback) | |
519 | |
520 def _postTreatCb(self): | |
521 for cb in self._post_treats: | |
522 cb() | |
523 del self._post_treats | |
524 | |
525 def _saveButtonCb(self, button): | |
526 button.disabled = True | |
527 self.onSaveParams(button) | |
528 | |
529 def constructUI(self, parsed_dom): | |
530 xmlui.XMLUIPanel.constructUI(self, parsed_dom, self._postTreatCb) | |
531 if self.xmlui_title: | |
532 self.add_widget(Title(text=self.xmlui_title)) | |
533 self.add_widget(self.main_cont) | |
534 if self.type == 'form': | |
535 submit_btn = FormButton(text=_(u"Submit")) | |
536 submit_btn.bind(on_press=self.onFormSubmitted) | |
537 self.add_widget(submit_btn) | |
538 if not 'NO_CANCEL' in self.flags: | |
539 cancel_btn = FormButton(text=_(u"Cancel")) | |
540 cancel_btn.bind(on_press=self.onFormCancelled) | |
541 self.add_widget(cancel_btn) | |
542 elif self.type == 'param': | |
543 self.save_btn = FormButton(text=_(u"Save"), disabled=True) | |
544 self.save_btn.bind(on_press=self._saveButtonCb) | |
545 self.add_widget(self.save_btn) | |
546 self.add_widget(Widget()) # to have elements on the top | |
547 | |
548 def show(self, *args, **kwargs): | |
549 if not self.user_action and not kwargs.get("force", False): | |
550 G.host.addNotifUI(self) | |
551 else: | |
552 G.host.showUI(self) | |
553 | |
554 | |
555 class XMLUIDialog(xmlui.XMLUIDialog): | |
556 dialog_factory = WidgetFactory() | |
557 | |
558 | |
559 xmlui.registerClass(xmlui.CLASS_PANEL, XMLUIPanel) | |
560 xmlui.registerClass(xmlui.CLASS_DIALOG, XMLUIDialog) | |
561 create = xmlui.create |