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