Mercurial > libervia-desktop-kivy
comparison cagou/core/widgets_handler.py @ 154:a5e8833184c6
widget handler: refactoring:
- replaced proof of concept implementation with cleaner one based on custom layout
- removed proof of concept big bars in favor of thin line to separate widgets, with a 3 dots area in the center where user can touch/click more easily
- when in delete zone, the line + half circle become red, so user knows that she's about to delete a widget
- carousel is now created in kv
- ignore perpendicular swipes. This was not working before but is know working well, and the swipe is far more easy to do on desktop or mobile
- each new widget of the handler has an id (its creation number), which is displayed in debug logs on touch
- handler's widgets keep track of which widgets are on sides (left, top, right, bottom)
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 27 Apr 2018 16:45:09 +0200 |
parents | cd99f70ea592 |
children | a0e486074d91 |
comparison
equal
deleted
inserted
replaced
153:e0985834f8eb | 154:a5e8833184c6 |
---|---|
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
20 | 20 |
21 from sat.core import log as logging | 21 from sat.core import log as logging |
22 log = logging.getLogger(__name__) | 22 log = logging.getLogger(__name__) |
23 from sat.core import exceptions | |
23 from sat_frontends.quick_frontend import quick_widgets | 24 from sat_frontends.quick_frontend import quick_widgets |
25 from kivy.graphics import Color, Ellipse | |
26 from kivy.uix.layout import Layout | |
24 from kivy.uix.boxlayout import BoxLayout | 27 from kivy.uix.boxlayout import BoxLayout |
25 from kivy.uix.button import Button | |
26 from kivy.uix.carousel import Carousel | |
27 from kivy.metrics import dp | 28 from kivy.metrics import dp |
28 from kivy import properties | 29 from kivy import properties |
29 from cagou import G | 30 from cagou import G |
30 | 31 |
31 | |
32 CAROUSEL_SCROLL_DISTANCE = dp(50) | 32 CAROUSEL_SCROLL_DISTANCE = dp(50) |
33 CAROUSEL_SCROLL_TIMEOUT = 80 | 33 CAROUSEL_SCROLL_TIMEOUT = 80 |
34 NEW_WIDGET_DIST = 10 | 34 REMOVE_WID_LIMIT = dp(10) |
35 REMOVE_WIDGET_DIST = NEW_WIDGET_DIST | 35 MIN_WIDTH = MIN_HEIGHT = dp(50) |
36 | 36 |
37 | 37 |
38 class WHSplitter(Button): | 38 class WHWrapper(BoxLayout): |
39 horizontal=properties.BooleanProperty(True) | 39 carousel = properties.ObjectProperty(None) |
40 thickness=properties.NumericProperty(dp(20)) | 40 split_size = properties.NumericProperty(dp(1)) |
41 split_move = None # we handle one split at a time, so we use a class attribute | 41 split_margin = properties.NumericProperty(dp(2)) |
42 | 42 split_color = properties.ListProperty([0.8, 0.8, 0.8, 1]) |
43 def __init__(self, handler, **kwargs): | 43 split_color_move = properties.ListProperty([0.0, 0.8, 0.8, 1]) |
44 super(WHSplitter, self).__init__(**kwargs) | 44 split_color_del = properties.ListProperty([0.8, 0.0, 0.0, 1]) |
45 self.handler = handler | 45 # sp stands for "split point" |
46 | 46 sp_size = properties.NumericProperty(dp(1)) |
47 def getPos(self, touch): | 47 sp_space = properties.NumericProperty(dp(4)) |
48 if self.horizontal: | 48 sp_zone = properties.NumericProperty(dp(30)) |
49 relative_y = self.handler.to_local(*touch.pos, relative=True)[1] | 49 _split = properties.OptionProperty('None', options=['None', 'left', 'top']) |
50 return self.handler.height - relative_y | 50 _split_del = properties.BooleanProperty(False) |
51 | |
52 def __init__(self, **kwargs): | |
53 idx = kwargs.pop('_wid_idx') | |
54 self._wid_idx = idx | |
55 super(WHWrapper, self).__init__(**kwargs) | |
56 self._former_slide = None | |
57 self.carousel.bind(current_slide=self.onSlideChange) | |
58 self._slides_update_lock = False | |
59 self._left_wids = set() | |
60 self._top_wids = set() | |
61 self._right_wids = set() | |
62 self._bottom_wids = set() | |
63 | |
64 def __repr__(self): | |
65 return "WHWrapper_{idx}".format(idx=self._wid_idx) | |
66 | |
67 def _main_wid(self, wid_list): | |
68 """return main widget of a side list | |
69 | |
70 main widget is either the widget currently splitter | |
71 or any widget if none is split | |
72 @return (WHWrapper, None): main widget or None | |
73 if there is not widget | |
74 """ | |
75 if not wid_list: | |
76 return None | |
77 for wid in wid_list: | |
78 if wid._split != 'None': | |
79 return wid | |
80 return next(iter(wid_list)) | |
81 | |
82 @property | |
83 def _left_wid(self): | |
84 return self._main_wid(self._left_wids) | |
85 | |
86 @property | |
87 def _top_wid(self): | |
88 return self._main_wid(self._top_wids) | |
89 | |
90 @property | |
91 def _right_wid(self): | |
92 return self._main_wid(self._right_wids) | |
93 | |
94 @property | |
95 def _bottom_wid(self): | |
96 return self._main_wid(self._bottom_wids) | |
97 | |
98 @property | |
99 def current_slide(self): | |
100 return self.carousel.current_slide | |
101 | |
102 def _draw_ellipse(self): | |
103 """draw split ellipse""" | |
104 color = self.split_color_del if self._split_del else self.split_color_move | |
105 try: | |
106 self.canvas.after.remove(self.ellipse) | |
107 except AttributeError: | |
108 pass | |
109 if self._split == "top": | |
110 with self.canvas.after: | |
111 Color(*color) | |
112 self.ellipse = Ellipse(angle_start=90, angle_end=270, | |
113 pos=(self.x + self.width/2 - self.sp_zone/2, | |
114 self.y + self.height - self.sp_zone/2), | |
115 size=(self.sp_zone, self.sp_zone)) | |
116 elif self._split == "left": | |
117 with self.canvas.after: | |
118 Color(*color) | |
119 self.ellipse = Ellipse(angle_end=180, | |
120 pos=(self.x + -self.sp_zone/2, | |
121 self.y + self.height/2 - self.sp_zone/2), | |
122 size = (self.sp_zone, self.sp_zone)) | |
51 else: | 123 else: |
52 return touch.x | 124 raise exceptions.InternalError('unexpected split value') |
125 | |
126 def on_touch_down(self, touch): | |
127 """activate split if touch is on a split zone""" | |
128 if not self.collide_point(*touch.pos): | |
129 return | |
130 log.debug("WIDGET IDX: {} (left: {}, top: {}, right: {}, bottom: {}), pos: {}, size: {}".format( | |
131 self._wid_idx, | |
132 'None' if not self._left_wids else [w._wid_idx for w in self._left_wids], | |
133 'None' if not self._top_wids else [w._wid_idx for w in self._top_wids], | |
134 'None' if not self._right_wids else [w._wid_idx for w in self._right_wids], | |
135 'None' if not self._bottom_wids else [w._wid_idx for w in self._bottom_wids], | |
136 self.pos, | |
137 self.size, | |
138 )) | |
139 touch_rx, touch_ry = self.to_widget(*touch.pos, relative=True) | |
140 if (touch_ry <= self.height and | |
141 touch_ry >= self.height - self.split_size - self.split_margin or | |
142 touch_ry <= self.height and | |
143 touch_ry >= self.height - self.sp_zone and | |
144 touch_rx >= self.width/2 - self.sp_zone/2 and | |
145 touch_rx <= self.width/2 + self.sp_zone/2): | |
146 # split area is touched, we activate top split mode | |
147 self._split = "top" | |
148 self._draw_ellipse() | |
149 elif (touch_rx >= 0 and | |
150 touch_rx <= self.split_size + self.split_margin or | |
151 touch_rx >= 0 and | |
152 touch_rx <= self.sp_zone and | |
153 touch_ry >= self.height/2 - self.sp_zone/2 and | |
154 touch_ry <= self.height/2 + self.sp_zone/2): | |
155 # split area is touched, we activate left split mode | |
156 self._split = "left" | |
157 touch.ud['ori_width'] = self.width | |
158 self._draw_ellipse() | |
159 else: | |
160 return super(WHWrapper, self).on_touch_down(touch) | |
53 | 161 |
54 def on_touch_move(self, touch): | 162 def on_touch_move(self, touch): |
55 if self.split_move is None and self.collide_point(*touch.opos): | 163 """handle size change and widget creation on split""" |
56 WHSplitter.split_move = self | 164 if self._split == 'None': |
57 | 165 return super(WHWrapper, self).on_touch_move(touch) |
58 if self.split_move is self: | 166 |
59 pos = self.getPos(touch) | 167 elif self._split == 'top': |
60 if pos > NEW_WIDGET_DIST: | 168 new_height = touch.y - self.y |
61 # we are above minimal distance, we resize the widget | 169 |
62 self.handler.setWidgetSize(self.horizontal, pos) | 170 if new_height < MIN_HEIGHT: |
171 return | |
172 | |
173 # we must not pass the top widget/border | |
174 if self._top_wids: | |
175 top = next(iter(self._top_wids)) | |
176 y_limit = top.y + top.height | |
177 | |
178 if top.height <= REMOVE_WID_LIMIT: | |
179 # we are in remove zone, we add visual hint for that | |
180 if not self._split_del: | |
181 self._split_del = True | |
182 self._draw_ellipse() | |
183 else: | |
184 if self._split_del: | |
185 self._split_del = False | |
186 self._draw_ellipse() | |
187 else: | |
188 y_limit = self.y + self.height | |
189 | |
190 if touch.y >= y_limit: | |
191 return | |
192 | |
193 # all right, we can change size | |
194 self.height = new_height | |
195 self.ellipse.pos = (self.ellipse.pos[0], touch.y - self.sp_zone/2) | |
196 | |
197 if not self._top_wids: | |
198 # we are the last widget on the top | |
199 # so we create a new widget | |
200 new_wid = self.parent.add_widget() | |
201 self._top_wids.add(new_wid) | |
202 new_wid._bottom_wids.add(self) | |
203 for w in self._right_wids: | |
204 new_wid._right_wids.add(w) | |
205 w._left_wids.add(new_wid) | |
206 for w in self._left_wids: | |
207 new_wid._left_wids.add(w) | |
208 w._right_wids.add(new_wid) | |
209 | |
210 elif self._split == 'left': | |
211 ori_width = touch.ud['ori_width'] | |
212 new_x = touch.x | |
213 new_width = ori_width - (touch.x - touch.ox) | |
214 | |
215 if new_width < MIN_WIDTH: | |
216 return | |
217 | |
218 # we must not pass the left widget/border | |
219 if self._left_wids: | |
220 left = next(iter(self._left_wids)) | |
221 x_limit = left.x | |
222 | |
223 if left.width <= REMOVE_WID_LIMIT: | |
224 # we are in remove zone, we add visual hint for that | |
225 if not self._split_del: | |
226 self._split_del = True | |
227 self._draw_ellipse() | |
228 else: | |
229 if self._split_del: | |
230 self._split_del = False | |
231 self._draw_ellipse() | |
232 else: | |
233 x_limit = self.x | |
234 | |
235 if new_x <= x_limit: | |
236 return | |
237 | |
238 # all right, we can change position/size | |
239 self.x = new_x | |
240 self.width = new_width | |
241 self.ellipse.pos = (touch.x - self.sp_zone/2, self.ellipse.pos[1]) | |
242 | |
243 if not self._left_wids: | |
244 # we are the last widget on the left | |
245 # so we create a new widget | |
246 new_wid = self.parent.add_widget() | |
247 self._left_wids.add(new_wid) | |
248 new_wid._right_wids.add(self) | |
249 for w in self._top_wids: | |
250 new_wid._top_wids.add(w) | |
251 w._bottom_wids.add(new_wid) | |
252 for w in self._bottom_wids: | |
253 new_wid._bottom_wids.add(w) | |
254 w._top_wids.add(new_wid) | |
255 | |
256 else: | |
257 raise Exception.InternalError('invalid _split value') | |
63 | 258 |
64 def on_touch_up(self, touch): | 259 def on_touch_up(self, touch): |
65 if self.split_move is self: | 260 if self._split == 'None': |
66 pos = self.getPos(touch) | 261 return super(WHWrapper, self).on_touch_up(touch) |
67 if pos <= REMOVE_WIDGET_DIST: | 262 if self._split == 'top': |
68 # if we go under minimal distance, the widget is not wanted anymore | 263 # we remove all top widgets in delete zone, |
69 self.handler.removeWidget(self.horizontal) | 264 # and update there side widgets list |
70 WHSplitter.split_move=None | 265 for top in self._top_wids.copy(): |
71 return super(WHSplitter, self).on_touch_up(touch) | 266 if top.height <= REMOVE_WID_LIMIT: |
72 | 267 for w in top._top_wids: |
73 | 268 w._bottom_wids.remove(top) |
74 class HandlerCarousel(Carousel): | 269 w._bottom_wids.update(top._bottom_wids) |
75 | 270 for w in top._bottom_wids: |
76 def __init__(self, *args, **kwargs): | 271 w._top_wids.remove(top) |
77 super(HandlerCarousel, self).__init__( | 272 w._top_wids.update(top._top_wids) |
78 *args, | 273 for w in top._left_wids: |
79 direction='right', | 274 w._right_wids.remove(top) |
80 loop=True, | 275 for w in top._right_wids: |
81 **kwargs) | 276 w._left_wids.remove(top) |
82 self._former_slide = None | 277 self.parent.remove_widget(top) |
83 self.bind(current_slide=self.onSlideChange) | 278 elif self._split == 'left': |
84 self._slides_update_lock = False | 279 # we remove all left widgets in delete zone, |
280 # and update there side widgets list | |
281 for left in self._left_wids.copy(): | |
282 if left.width <= REMOVE_WID_LIMIT: | |
283 for w in left._left_wids: | |
284 w._right_wids.remove(left) | |
285 w._right_wids.update(left._right_wids) | |
286 for w in left._right_wids: | |
287 w._left_wids.remove(left) | |
288 w._left_wids.update(left._left_wids) | |
289 for w in left._top_wids: | |
290 w._bottom_wids.remove(left) | |
291 for w in left._bottom_wids: | |
292 w._top_wids.remove(left) | |
293 self.parent.remove_widget(left) | |
294 self._split = 'None' | |
295 self.canvas.after.remove(self.ellipse) | |
296 del self.ellipse | |
297 | |
298 def set_widget(self, wid, index=0): | |
299 self.carousel.add_widget(wid, index) | |
85 | 300 |
86 def changeWidget(self, new_widget): | 301 def changeWidget(self, new_widget): |
87 """Change currently displayed widget | 302 """Change currently displayed widget |
88 | 303 |
89 slides widgets will be updated | 304 slides widgets will be updated |
90 """ | 305 """ |
91 # slides update need to be blocked to avoid the update in onSlideChange | 306 # slides update need to be blocked to avoid the update in onSlideChange |
92 # which would mess the removal of current widgets | 307 # which would mess the removal of current widgets |
93 self._slides_update_lock = True | 308 self._slides_update_lock = True |
94 current = self.current_slide | 309 current = self.carousel.current_slide |
95 for w in self.slides: | 310 for w in self.carousel.slides: |
96 if w == current or w == new_widget: | 311 if w == current or w == new_widget: |
97 continue | 312 continue |
98 if isinstance(w, quick_widgets.QuickWidget): | 313 if isinstance(w, quick_widgets.QuickWidget): |
99 G.host.widgets.deleteWidget(w) | 314 G.host.widgets.deleteWidget(w) |
100 self.clear_widgets() | 315 self.carousel.clear_widgets() |
101 self.add_widget(new_widget) | 316 self.carousel.add_widget(new_widget) |
102 self._slides_update_lock = False | 317 self._slides_update_lock = False |
103 self.updateHiddenSlides() | 318 self.updateHiddenSlides() |
104 | 319 |
105 def onSlideChange(self, handler, new_slide): | 320 def onSlideChange(self, handler, new_slide): |
106 if isinstance(self._former_slide, quick_widgets.QuickWidget): | 321 if isinstance(self._former_slide, quick_widgets.QuickWidget): |
136 | 351 |
137 def updateHiddenSlides(self): | 352 def updateHiddenSlides(self): |
138 """adjust carousel slides according to visible widgets""" | 353 """adjust carousel slides according to visible widgets""" |
139 if self._slides_update_lock: | 354 if self._slides_update_lock: |
140 return | 355 return |
141 if not isinstance(self.current_slide, quick_widgets.QuickWidget): | 356 if not isinstance(self.carousel.current_slide, quick_widgets.QuickWidget): |
142 return | 357 return |
143 # lock must be used here to avoid recursions | 358 # lock must be used here to avoid recursions |
144 self._slides_update_lock = True | 359 self._slides_update_lock = True |
145 visible_list = G.host.getVisibleList(self.current_slide.__class__) | 360 visible_list = G.host.getVisibleList(self.current_slide.__class__) |
146 hidden = list(self.hiddenList(visible_list)) | 361 hidden = list(self.hiddenList(visible_list)) |
147 slides_sorted = sorted(hidden + [self.current_slide], key=self.widgets_sort) | 362 slides_sorted = sorted(hidden + [self.carousel.current_slide], key=self.widgets_sort) |
148 to_remove = set(self.slides).difference({self.current_slide}) | 363 to_remove = set(self.carousel.slides).difference({self.carousel.current_slide}) |
149 for w in to_remove: | 364 for w in to_remove: |
150 self.remove_widget(w) | 365 self.carousel.remove_widget(w) |
151 if hidden: | 366 if hidden: |
152 # no need to add more than two widgets (next and previous), | 367 # no need to add more than two widgets (next and previous), |
153 # as the list will be updated on each new visible widget | 368 # as the list will be updated on each new visible widget |
154 current_idx = slides_sorted.index(self.current_slide) | 369 current_idx = slides_sorted.index(self.current_slide) |
155 try: | 370 try: |
156 next_slide = slides_sorted[current_idx+1] | 371 next_slide = slides_sorted[current_idx+1] |
157 except IndexError: | 372 except IndexError: |
158 next_slide = slides_sorted[0] | 373 next_slide = slides_sorted[0] |
159 self.add_widget(G.host.getOrClone(next_slide)) | 374 self.carousel.add_widget(G.host.getOrClone(next_slide)) |
160 if len(hidden)>1: | 375 if len(hidden)>1: |
161 previous_slide = slides_sorted[current_idx-1] | 376 previous_slide = slides_sorted[current_idx-1] |
162 self.add_widget(G.host.getOrClone(previous_slide)) | 377 self.carousel.add_widget(G.host.getOrClone(previous_slide)) |
163 | 378 |
164 if len(self.slides) == 1: | |
165 # we block carousel with high scroll_distance to avoid swiping | |
166 # when the is not other instance of the widget | |
167 self.scroll_distance=2**32 | |
168 self.scroll_timeout=0 | |
169 else: | |
170 self.scroll_distance = CAROUSEL_SCROLL_DISTANCE | |
171 self.scroll_timeout=CAROUSEL_SCROLL_TIMEOUT | |
172 self._slides_update_lock = False | 379 self._slides_update_lock = False |
173 | 380 |
174 | 381 |
175 class WidgetsHandler(BoxLayout): | 382 class WidgetsHandlerLayout(Layout): |
176 | 383 count = 0 |
177 def __init__(self, wid=None, **kw): | 384 |
178 if wid is None: | 385 def __init__(self, **kwargs): |
179 wid=self.default_widget | 386 super(WidgetsHandlerLayout, self).__init__(**kwargs) |
180 self.vert_wid = self.hor_wid = None | 387 fbind = self.fbind |
181 BoxLayout.__init__(self, orientation="vertical", **kw) | 388 update = self._trigger_layout |
182 self.blh = BoxLayout(orientation="horizontal") | 389 fbind('children', update) |
183 self.blv = BoxLayout(orientation="vertical") | 390 fbind('parent', update) |
184 self.blv.add_widget(WHSplitter(self)) | 391 fbind('size', update) |
185 self.carousel = HandlerCarousel() | 392 fbind('pos', update) |
186 self.blv.add_widget(self.carousel) | |
187 self.blh.add_widget(WHSplitter(self, horizontal=False)) | |
188 self.blh.add_widget(self.blv) | |
189 self.add_widget(self.blh) | |
190 self.changeWidget(wid) | |
191 | 393 |
192 @property | 394 @property |
193 def default_widget(self): | 395 def default_widget(self): |
194 return G.host.default_wid['factory'](G.host.default_wid, None, None) | 396 return G.host.default_wid['factory'](G.host.default_wid, None, None) |
195 | 397 |
398 def do_layout(self, *args): | |
399 x, y = self.pos | |
400 width, height = self.width, self.height | |
401 end_x, end_y = x + width, y + height | |
402 for child in self.children: | |
403 # XXX: left must be calculated before right and bottom before top | |
404 # because they are the pos, and are used to caculate size (right and top) | |
405 # left | |
406 left = child._left_wid | |
407 left_end_x = x-1 if left is None else left.x + left.width | |
408 if child.x != left_end_x + 1 and child._split == "None": | |
409 child.x = left_end_x + 1 | |
410 # right | |
411 right = child._right_wid | |
412 right_x = end_x + 1 if right is None else right.x | |
413 if child.x + child.width != right_x - 1: | |
414 child.width = right_x - child.x - 1 | |
415 # bottom | |
416 bottom = child._bottom_wid | |
417 if bottom is None: | |
418 if child.y != y: | |
419 child.y = y | |
420 else: | |
421 bottom_end_y = bottom.y + bottom.height | |
422 if child.y != bottom_end_y + 1: | |
423 child.y = bottom_end_y + 1 | |
424 # top | |
425 top = child._top_wid | |
426 top_y = end_y+1 if top is None else top.y | |
427 if child.y + child.height != top_y - 1: | |
428 if child._split == "None": | |
429 child.height = top_y - child.y - 1 | |
430 | |
431 def remove_widget(self, wid): | |
432 super(WidgetsHandlerLayout, self).remove_widget(wid) | |
433 log.debug("widget deleted ({})".format(wid._wid_idx)) | |
434 | |
435 def add_widget(self, wid=None, index=0): | |
436 WidgetsHandlerLayout.count += 1 | |
437 if wid is None: | |
438 wid = self.default_widget | |
439 wrapper = WHWrapper(_wid_idx=WidgetsHandlerLayout.count) | |
440 log.debug("WHWrapper created ({})".format(wrapper._wid_idx)) | |
441 wrapper.set_widget(wid) | |
442 super(WidgetsHandlerLayout, self).add_widget(wrapper, index) | |
443 return wrapper | |
444 | |
445 | |
446 class WidgetsHandler(WidgetsHandlerLayout): | |
447 | |
448 def __init__(self, **kw): | |
449 super(WidgetsHandler, self).__init__(**kw) | |
450 self.wrapper = self.add_widget() | |
451 | |
196 @property | 452 @property |
197 def cagou_widget(self): | 453 def cagou_widget(self): |
198 """get holded CagouWidget""" | 454 """get holded CagouWidget""" |
199 return self.carousel.current_slide | 455 return self.wrapper.current_slide |
200 | |
201 def changeWidget(self, new_widget): | |
202 self.carousel.changeWidget(new_widget) | |
203 | |
204 def removeWidget(self, vertical): | |
205 if vertical and self.vert_wid is not None: | |
206 self.remove_widget(self.vert_wid) | |
207 self.vert_wid.onDelete() | |
208 self.vert_wid = None | |
209 elif self.hor_wid is not None: | |
210 self.blh.remove_widget(self.hor_wid) | |
211 self.hor_wid.onDelete() | |
212 self.hor_wid = None | |
213 | |
214 def setWidgetSize(self, vertical, size): | |
215 if vertical: | |
216 if self.vert_wid is None: | |
217 self.vert_wid = WidgetsHandler(self.default_widget, size_hint=(1, None)) | |
218 self.add_widget(self.vert_wid, len(self.children)) | |
219 self.vert_wid.height=size | |
220 else: | |
221 if self.hor_wid is None: | |
222 self.hor_wid = WidgetsHandler(self.default_widget, size_hint=(None, 1)) | |
223 self.blh.add_widget(self.hor_wid, len(self.blh.children)) | |
224 self.hor_wid.width=size | |
225 | |
226 def onDelete(self): | |
227 # when this handler is deleted, we need to delete the holded CagouWidget | |
228 cagou_widget = self.cagou_widget | |
229 if isinstance(cagou_widget, quick_widgets.QuickWidget): | |
230 G.host.removeVisibleWidget(cagou_widget) |