Mercurial > libervia-desktop-kivy
comparison cagou/core/widgets_handler.py @ 353:19422bbd9c8e
core (widgets handler): refactoring:
- CagouWidget now has class properties (to be overridden when needed) which indicate how
if the widget handle must add a wrapping ScreenManager (global_screen_manager) or show
all instances of the class in a Carousel (collection_carousel). If none of those
options is used, a ScrollView will be wrapping the widget, to be sure that the widget
will be resized correctly when necessary (without it, the widget could still be
drawn in the backround when the size is too small and overflow on the WidgetWrapper,
this would be the case with WidgetSelector)
- some helper methods/properties have been added to CagouWidget. Check docstrings for
details
- better handling of (in)visible widget in WidgetsHandler
- thanks to the new wrapping ScrollView, WidgetSelect will show scroll bars if the
available space is too small.
- bugs fixes
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 17 Jan 2020 18:44:35 +0100 |
parents | 772c170b47a9 |
children | 8b6621cc142c |
comparison
equal
deleted
inserted
replaced
352:434f770fe55b | 353:19422bbd9c8e |
---|---|
1 #!/usr/bin/python | 1 #!/usr/bin/env python3 |
2 # -*- coding: utf-8 -*- | |
3 | 2 |
4 # Cagou: desktop/mobile frontend for Salut à Toi XMPP client | 3 # Cagou: desktop/mobile frontend for Salut à Toi XMPP client |
5 # Copyright (C) 2016-2019 Jérôme Poisson (goffi@goffi.org) | 4 # Copyright (C) 2016-2019 Jérôme Poisson (goffi@goffi.org) |
6 | 5 |
7 # This program is free software: you can redistribute it and/or modify | 6 # This program is free software: you can redistribute it and/or modify |
17 # You should have received a copy of the GNU Affero General Public License | 16 # 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/>. | 17 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 18 |
20 | 19 |
21 from sat.core import log as logging | 20 from sat.core import log as logging |
22 log = logging.getLogger(__name__) | |
23 from sat.core import exceptions | 21 from sat.core import exceptions |
24 from sat_frontends.quick_frontend import quick_widgets | 22 from sat_frontends.quick_frontend import quick_widgets |
25 from kivy.graphics import Color, Ellipse | 23 from kivy.graphics import Color, Ellipse |
26 from kivy.uix.layout import Layout | 24 from kivy.uix.layout import Layout |
27 from kivy.uix.boxlayout import BoxLayout | 25 from kivy.uix.boxlayout import BoxLayout |
28 from kivy.uix.stencilview import StencilView | 26 from kivy.uix.stencilview import StencilView |
27 from kivy.uix.carousel import Carousel | |
28 from kivy.uix.screenmanager import ScreenManager, Screen | |
29 from kivy.uix.scrollview import ScrollView | |
29 from kivy.metrics import dp | 30 from kivy.metrics import dp |
30 from kivy import properties | 31 from kivy import properties |
31 from cagou import G | 32 from cagou import G |
32 from cagou.core.constants import Const as C | 33 from .constants import Const as C |
34 from . import cagou_widget | |
35 | |
36 log = logging.getLogger(__name__) | |
33 | 37 |
34 | 38 |
35 REMOVE_WID_LIMIT = dp(50) | 39 REMOVE_WID_LIMIT = dp(50) |
36 MIN_WIDTH = MIN_HEIGHT = dp(70) | 40 MIN_WIDTH = MIN_HEIGHT = dp(70) |
37 | 41 |
38 | 42 |
39 class WHWrapper(BoxLayout): | 43 class WHWrapper(BoxLayout): |
40 carousel = properties.ObjectProperty(None) | 44 main_container = properties.ObjectProperty(None) |
45 screen_manager = properties.ObjectProperty(None, allownone=True) | |
46 carousel = properties.ObjectProperty(None, allownone=True) | |
41 split_size = properties.NumericProperty(dp(1)) | 47 split_size = properties.NumericProperty(dp(1)) |
42 split_margin = properties.NumericProperty(dp(2)) | 48 split_margin = properties.NumericProperty(dp(2)) |
43 split_color = properties.ListProperty([0.8, 0.8, 0.8, 1]) | 49 split_color = properties.ListProperty([0.8, 0.8, 0.8, 1]) |
44 split_color_move = C.COLOR_SEC_DARK | 50 split_color_move = C.COLOR_SEC_DARK |
45 split_color_del = properties.ListProperty([0.8, 0.0, 0.0, 1]) | 51 split_color_del = properties.ListProperty([0.8, 0.0, 0.0, 1]) |
52 | 58 |
53 def __init__(self, **kwargs): | 59 def __init__(self, **kwargs): |
54 idx = kwargs.pop('_wid_idx') | 60 idx = kwargs.pop('_wid_idx') |
55 self._wid_idx = idx | 61 self._wid_idx = idx |
56 super(WHWrapper, self).__init__(**kwargs) | 62 super(WHWrapper, self).__init__(**kwargs) |
57 self._former_slide = None | |
58 self.carousel.bind(current_slide=self.onSlideChange) | |
59 self._slides_update_lock = False | |
60 self._left_wids = set() | 63 self._left_wids = set() |
61 self._top_wids = set() | 64 self._top_wids = set() |
62 self._right_wids = set() | 65 self._right_wids = set() |
63 self._bottom_wids = set() | 66 self._bottom_wids = set() |
67 self._clear_attributes() | |
68 | |
69 def _clear_attributes(self): | |
70 self._former_slide = None | |
64 | 71 |
65 def __repr__(self): | 72 def __repr__(self): |
66 return "WHWrapper_{idx}".format(idx=self._wid_idx) | 73 return "WHWrapper_{idx}".format(idx=self._wid_idx) |
67 | 74 |
68 def _main_wid(self, wid_list): | 75 def _main_wid(self, wid_list): |
96 def _bottom_wid(self): | 103 def _bottom_wid(self): |
97 return self._main_wid(self._bottom_wids) | 104 return self._main_wid(self._bottom_wids) |
98 | 105 |
99 @property | 106 @property |
100 def current_slide(self): | 107 def current_slide(self): |
101 return self.carousel.current_slide | 108 if (self.carousel is not None |
109 and (self.screen_manager is None or self.screen_manager.current == '')): | |
110 return self.carousel.current_slide | |
111 elif self.screen_manager is not None: | |
112 # we should have exactly one children in current_screen, else there is a bug | |
113 return self.screen_manager.current_screen.children[0] | |
114 else: | |
115 try: | |
116 return self.main_container.children[0] | |
117 except IndexError: | |
118 log.error("No child found, this should not happen") | |
119 return None | |
120 | |
121 @property | |
122 def carousel_active(self): | |
123 """Return True if Carousel is used and active""" | |
124 if self.carousel is None: | |
125 return False | |
126 if self.screen_manager is not None and self.screen_manager.current != '': | |
127 return False | |
128 return True | |
129 | |
130 @property | |
131 def former_screen_wid(self): | |
132 """Return widget currently active for former screen""" | |
133 if self.screen_manager is None: | |
134 raise exceptions.InternalError( | |
135 "former_screen_wid can only be used if ScreenManager is used") | |
136 if self._former_screen_name is None: | |
137 return None | |
138 return self.getScreenWidget(self._former_screen_name) | |
139 | |
140 def getScreenWidget(self, screen_name): | |
141 """Return screen main widget, handling carousel if necessary""" | |
142 if self.carousel is not None and screen_name == '': | |
143 return self.carousel.current_slide | |
144 try: | |
145 return self.screen_manager.get_screen(screen_name).children[0] | |
146 except IndexError: | |
147 return None | |
102 | 148 |
103 def _draw_ellipse(self): | 149 def _draw_ellipse(self): |
104 """draw split ellipse""" | 150 """draw split ellipse""" |
105 color = self.split_color_del if self._split_del else self.split_color_move | 151 color = self.split_color_del if self._split_del else self.split_color_move |
106 try: | 152 try: |
156 # split area is touched, we activate left split mode | 202 # split area is touched, we activate left split mode |
157 self._split = "left" | 203 self._split = "left" |
158 touch.ud['ori_width'] = self.width | 204 touch.ud['ori_width'] = self.width |
159 self._draw_ellipse() | 205 self._draw_ellipse() |
160 else: | 206 else: |
161 if len(self.carousel.slides) == 1: | 207 if self.carousel_active and len(self.carousel.slides) <= 1: |
162 # we don't want swipe if there is only one slide | 208 # we don't want swipe of carousel if there is only one slide |
163 return StencilView.on_touch_down(self.carousel, touch) | 209 return StencilView.on_touch_down(self.carousel, touch) |
164 else: | 210 else: |
165 return super(WHWrapper, self).on_touch_down(touch) | 211 return super(WHWrapper, self).on_touch_down(touch) |
166 | 212 |
167 def on_touch_move(self, touch): | 213 def on_touch_move(self, touch): |
300 self.parent.remove_widget(left) | 346 self.parent.remove_widget(left) |
301 self._split = 'None' | 347 self._split = 'None' |
302 self.canvas.after.remove(self.ellipse) | 348 self.canvas.after.remove(self.ellipse) |
303 del self.ellipse | 349 del self.ellipse |
304 | 350 |
351 def clear_widgets(self): | |
352 current_slide = self.current_slide | |
353 if current_slide is not None: | |
354 G.host._removeVisibleWidget(current_slide) | |
355 | |
356 super().clear_widgets() | |
357 | |
358 self.screen_manager = None | |
359 self.carousel = None | |
360 self._clear_attributes() | |
361 | |
305 def set_widget(self, wid, index=0): | 362 def set_widget(self, wid, index=0): |
306 self.carousel.add_widget(wid, index) | 363 assert len(self.children) == 0 |
364 | |
365 if wid.collection_carousel or wid.global_screen_manager: | |
366 self.main_container = self | |
367 else: | |
368 self.main_container = ScrollView() | |
369 self.add_widget(self.main_container) | |
370 | |
371 if self.carousel is not None: | |
372 return self.carousel.add_widget(wid, index) | |
373 | |
374 if wid.global_screen_manager: | |
375 if self.screen_manager is None: | |
376 self.screen_manager = ScreenManager() | |
377 self.main_container.add_widget(self.screen_manager) | |
378 parent = Screen() | |
379 self.screen_manager.add_widget(parent) | |
380 self._former_screen_name = '' | |
381 self.screen_manager.bind(current=self.onScreenChange) | |
382 wid.screenManagerInit(self.screen_manager) | |
383 else: | |
384 parent = self.main_container | |
385 | |
386 if wid.collection_carousel: | |
387 # a Carousel is requested, and this is the first widget that we add | |
388 # so we need to create the carousel | |
389 self.carousel = Carousel( | |
390 direction = "right", | |
391 ignore_perpendicular_swipes = True, | |
392 loop = True, | |
393 ) | |
394 self._slides_update_lock = 0 | |
395 self.carousel.bind(current_slide=self.onSlideChange) | |
396 parent.add_widget(self.carousel) | |
397 self.carousel.add_widget(wid, index) | |
398 else: | |
399 # no Carousel requested, we add the widget as a direct child | |
400 parent.add_widget(wid) | |
401 G.host._addVisibleWidget(wid) | |
307 | 402 |
308 def changeWidget(self, new_widget): | 403 def changeWidget(self, new_widget): |
309 """Change currently displayed widget | 404 """Change currently displayed widget |
310 | 405 |
311 slides widgets will be updated | 406 slides widgets will be updated |
312 """ | 407 """ |
313 # slides update need to be blocked to avoid the update in onSlideChange | 408 if (self.carousel is not None |
314 # which would mess the removal of current widgets | 409 and self.carousel.current_slide.__class__ == new_widget.__class__): |
315 self._slides_update_lock = True | 410 # we have the same class, we reuse carousel and screen manager setting |
316 current = self.carousel.current_slide | 411 |
317 for w in self.carousel.slides: | 412 if self.carousel.current_slide != new_widget: |
318 if w == current or w == new_widget: | 413 # slides update need to be blocked to avoid the update in onSlideChange |
319 continue | 414 # which would mess the removal of current widgets |
320 if isinstance(w, quick_widgets.QuickWidget): | 415 self._slides_update_lock += 1 |
321 G.host.widgets.deleteWidget(w) | 416 new_wid = None |
322 self.carousel.clear_widgets() | 417 for w in self.carousel.slides[:]: |
323 self.carousel.add_widget(G.host.getOrClone(new_widget)) | 418 if w.widget_hash == new_widget.widget_hash: |
324 self._slides_update_lock = False | 419 new_wid = w |
325 self.updateHiddenSlides() | 420 continue |
421 self.carousel.remove_widget(w) | |
422 if isinstance(w, quick_widgets.QuickWidget): | |
423 G.host.widgets.deleteWidget(w) | |
424 if new_wid is None: | |
425 new_wid = G.host.getOrClone(new_widget) | |
426 self.carousel.add_widget(new_wid) | |
427 self._updateHiddenSlides() | |
428 self._slides_update_lock -= 1 | |
429 | |
430 if self.screen_manager is not None: | |
431 self.screen_manager.clear_widgets([ | |
432 s for s in self.screen_manager.screens if s.name != '']) | |
433 new_wid.screenManagerInit(self.screen_manager) | |
434 else: | |
435 # else, we restart fresh | |
436 self.clear_widgets() | |
437 self.set_widget(G.host.getOrClone(new_widget)) | |
438 | |
439 def onScreenChange(self, screen_manager, new_screen): | |
440 try: | |
441 new_screen_wid = self.current_slide | |
442 except IndexError: | |
443 new_screen_wid = None | |
444 log.warning("Switching to a screen without children") | |
445 if new_screen == '' and self.carousel is not None: | |
446 # carousel may have been changed in the background, so we update slides | |
447 self._updateHiddenSlides() | |
448 former_screen_wid = self.former_screen_wid | |
449 if isinstance(former_screen_wid, cagou_widget.CagouWidget): | |
450 G.host._removeVisibleWidget(former_screen_wid) | |
451 if isinstance(new_screen_wid, cagou_widget.CagouWidget): | |
452 G.host._addVisibleWidget(new_screen_wid) | |
453 self._former_screen_name = new_screen | |
454 G.host.selected_widget = new_screen_wid | |
326 | 455 |
327 def onSlideChange(self, handler, new_slide): | 456 def onSlideChange(self, handler, new_slide): |
457 if self._former_slide is new_slide: | |
458 # FIXME: workaround for Kivy a95d67f (and above?), Carousel.current_slide | |
459 # binding now calls onSlideChange twice with the same widget (here | |
460 # "new_slide"). To be checked with Kivy team. | |
461 return | |
462 log.debug(f"Slide change: new_slide = {new_slide}") | |
328 if self._former_slide is not None: | 463 if self._former_slide is not None: |
329 if self._former_slide is new_slide: | 464 G.host._removeVisibleWidget(self._former_slide, ignore_missing=True) |
330 # FIXME: workaround for Kivy a95d67f (and above?), Carousel.current_slide | |
331 # binding now calls onSlideChange twice with the same widget (here | |
332 # "new_slide"). To be checked with Kivy team. | |
333 return | |
334 G.host._removeVisibleWidget(self._former_slide) | |
335 self._former_slide = new_slide | 465 self._former_slide = new_slide |
336 if new_slide is not None: | 466 if self.carousel_active: |
337 G.host._addVisibleWidget(new_slide) | 467 G.host.selected_widget = new_slide |
338 self.updateHiddenSlides() | 468 if new_slide is not None: |
339 | 469 G.host._addVisibleWidget(new_slide) |
340 def hiddenList(self, visible_list): | 470 self._updateHiddenSlides() |
341 """return widgets of same class as holded one which are hidden | 471 |
472 def hiddenList(self, visible_list, ignore=None): | |
473 """return widgets of same class as carousel current one, if they are hidden | |
342 | 474 |
343 @param visible_list(list[QuickWidget]): widgets visible | 475 @param visible_list(list[QuickWidget]): widgets visible |
476 @param ignore(QuickWidget, None): do no return this widget | |
344 @return (iter[QuickWidget]): widgets hidden | 477 @return (iter[QuickWidget]): widgets hidden |
345 """ | 478 """ |
346 added = [(w.targets, w.profiles) for w in visible_list] # we want to avoid recreated widgets | 479 # we want to avoid recreated widgets |
347 for w in G.host.widgets.getWidgets(self.current_slide.__class__, profiles=self.current_slide.profiles): | 480 added = [w.widget_hash for w in visible_list] |
348 if w in visible_list or (w.targets, w.profiles) in added: | 481 current_slide = self.carousel.current_slide |
482 for w in G.host.widgets.getWidgets(current_slide.__class__, | |
483 profiles=current_slide.profiles): | |
484 wid_hash = w.widget_hash | |
485 if w in visible_list or wid_hash in added: | |
486 continue | |
487 if wid_hash == ignore.widget_hash: | |
349 continue | 488 continue |
350 yield w | 489 yield w |
351 | 490 |
352 def widgets_sort(self, widget): | 491 def widgets_sort(self, widget): |
353 """method used as key to sort the widgets | 492 """method used as key to sort the widgets |
359 try: | 498 try: |
360 return str(widget.target).lower() | 499 return str(widget.target).lower() |
361 except AttributeError: | 500 except AttributeError: |
362 return str(list(widget.targets)[0]).lower() | 501 return str(list(widget.targets)[0]).lower() |
363 | 502 |
364 def updateHiddenSlides(self): | 503 def _updateHiddenSlides(self): |
365 """adjust carousel slides according to visible widgets""" | 504 """adjust carousel slides according to visible widgets""" |
366 if self._slides_update_lock: | 505 if self._slides_update_lock or not self.carousel_active: |
367 return | 506 return |
368 if not isinstance(self.carousel.current_slide, quick_widgets.QuickWidget): | 507 current_slide = self.carousel.current_slide |
508 if not isinstance(current_slide, quick_widgets.QuickWidget): | |
369 return | 509 return |
370 # lock must be used here to avoid recursions | 510 # lock must be used here to avoid recursions |
371 self._slides_update_lock = True | 511 self._slides_update_lock += 1 |
372 visible_list = G.host.getVisibleList(self.current_slide.__class__) | 512 visible_list = G.host.getVisibleList(current_slide.__class__) |
373 hidden = list(self.hiddenList(visible_list)) | 513 # we ignore current_slide as it may not be visible yet (e.g. if an other |
374 slides_sorted = sorted(hidden + [self.carousel.current_slide], key=self.widgets_sort) | 514 # screen is shown |
375 to_remove = set(self.carousel.slides).difference({self.carousel.current_slide}) | 515 hidden = list(self.hiddenList(visible_list, ignore=current_slide)) |
516 slides_sorted = sorted(set(hidden + [current_slide]), key=self.widgets_sort) | |
517 to_remove = set(self.carousel.slides).difference({current_slide}) | |
376 for w in to_remove: | 518 for w in to_remove: |
377 self.carousel.remove_widget(w) | 519 self.carousel.remove_widget(w) |
378 if hidden: | 520 if hidden: |
379 # no need to add more than two widgets (next and previous), | 521 # no need to add more than two widgets (next and previous), |
380 # as the list will be updated on each new visible widget | 522 # as the list will be updated on each new visible widget |
381 current_idx = slides_sorted.index(self.current_slide) | 523 current_idx = slides_sorted.index(current_slide) |
382 try: | 524 try: |
383 next_slide = slides_sorted[current_idx+1] | 525 next_slide = slides_sorted[current_idx+1] |
384 except IndexError: | 526 except IndexError: |
385 next_slide = slides_sorted[0] | 527 next_slide = slides_sorted[0] |
386 self.carousel.add_widget(G.host.getOrClone(next_slide)) | 528 self.carousel.add_widget(G.host.getOrClone(next_slide)) |
387 if len(hidden)>1: | 529 if len(hidden)>1: |
388 previous_slide = slides_sorted[current_idx-1] | 530 previous_slide = slides_sorted[current_idx-1] |
389 self.carousel.add_widget(G.host.getOrClone(previous_slide)) | 531 self.carousel.add_widget(G.host.getOrClone(previous_slide)) |
390 | 532 |
391 self._slides_update_lock = False | 533 self._slides_update_lock -= 1 |
392 | 534 |
393 | 535 |
394 class WidgetsHandlerLayout(Layout): | 536 class WidgetsHandlerLayout(Layout): |
395 count = 0 | 537 count = 0 |
396 | 538 |
474 | 616 |
475 class WidgetsHandler(WidgetsHandlerLayout): | 617 class WidgetsHandler(WidgetsHandlerLayout): |
476 | 618 |
477 def __init__(self, **kw): | 619 def __init__(self, **kw): |
478 super(WidgetsHandler, self).__init__(**kw) | 620 super(WidgetsHandler, self).__init__(**kw) |
479 self.wrapper = self.add_widget() | 621 self.add_widget() |
480 | |
481 @property | |
482 def cagou_widget(self): | |
483 """get holded CagouWidget""" | |
484 return self.wrapper.current_slide |