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