Mercurial > libervia-backend
comparison sat_frontends/quick_frontend/quick_widgets.py @ 2852:e2595c81eb6d
quick frontend(widgets): improved handling of multiple instances of widgets:
when a widget was recreated, a new hash was created with a suffix, making hash matching difficult and bug prone.
This has been changed to use a list of instances instead, this simplify the code and avoid issues while looking for existing instances of a widget.
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 10 Mar 2019 18:03:14 +0100 |
parents | 7764383a968c |
children | 6901a425d882 |
comparison
equal
deleted
inserted
replaced
2851:7764383a968c | 2852:e2595c81eb6d |
---|---|
22 log = getLogger(__name__) | 22 log = getLogger(__name__) |
23 from sat.core import exceptions | 23 from sat.core import exceptions |
24 from sat_frontends.quick_frontend.constants import Const as C | 24 from sat_frontends.quick_frontend.constants import Const as C |
25 | 25 |
26 | 26 |
27 NEW_INSTANCE_SUFF = "_new_instance_" | |
28 classes_map = {} | 27 classes_map = {} |
29 | 28 |
30 | 29 |
31 try: | 30 try: |
32 # FIXME: to be removed when an acceptable solution is here | 31 # FIXME: to be removed when an acceptable solution is here |
63 self._widgets = {} | 62 self._widgets = {} |
64 | 63 |
65 def __iter__(self): | 64 def __iter__(self): |
66 """Iterate throught all widgets""" | 65 """Iterate throught all widgets""" |
67 for widget_map in self._widgets.itervalues(): | 66 for widget_map in self._widgets.itervalues(): |
68 for widget in widget_map.itervalues(): | 67 for widget_instances in widget_map.itervalues(): |
69 yield widget | 68 for widget in widget_instances: |
69 yield widget | |
70 | 70 |
71 def getRealClass(self, class_): | 71 def getRealClass(self, class_): |
72 """Return class registered for given class_ | 72 """Return class registered for given class_ |
73 | 73 |
74 @param class_: subclass of QuickWidget | 74 @param class_: subclass of QuickWidget |
84 raise exceptions.InternalError( | 84 raise exceptions.InternalError( |
85 "There is not class registered for {}".format(class_) | 85 "There is not class registered for {}".format(class_) |
86 ) | 86 ) |
87 return cls | 87 return cls |
88 | 88 |
89 def getRootHash(self, hash_): | 89 def getWidgetInstances(self, widget): |
90 """Return root hash (i.e. hash without new instance suffix for recreated widgets | 90 """Get all instance of a widget |
91 | 91 |
92 @param hash_(immutable): hash of a widget | 92 This is a helper method which call getWidgets |
93 @return (unicode): root hash (transtyped to unicode) | 93 @param widget(QuickWidget): retrieve instances of this widget |
94 """ | 94 @return: iterator on widgets |
95 return unicode(hash_).split(NEW_INSTANCE_SUFF)[0] | 95 """ |
96 try: | |
97 target = widget.target | |
98 except AttributeError: | |
99 target = next(iter(widget.targets)) | |
100 return self.getWidgets(widget.__class__, target, widget.profiles) | |
96 | 101 |
97 def getWidgets(self, class_, target=None, profiles=None): | 102 def getWidgets(self, class_, target=None, profiles=None): |
98 """Get all subclassed widgets instances | 103 """Get all subclassed widgets instances |
99 | 104 |
100 @param class_: subclass of QuickWidget, same parameter as used in [getOrCreateWidget] | 105 @param class_: subclass of QuickWidget, same parameter as used in |
101 @param target: if not None, construct a hash with this target and filter corresponding widgets | 106 [getOrCreateWidget] |
102 recreated widgets (with new instance suffix) are handled | 107 @param target: if not None, construct a hash with this target and filter |
103 @param profiles(iterable, None): if not None, filter on instances linked to these profiles | 108 corresponding widgets |
109 recreated widgets are handled | |
110 @param profiles(iterable, None): if not None, filter on instances linked to these | |
111 profiles | |
104 @return: iterator on widgets | 112 @return: iterator on widgets |
105 """ | 113 """ |
106 class_ = self.getRealClass(class_) | 114 class_ = self.getRealClass(class_) |
107 try: | 115 try: |
108 widgets_map = self._widgets[class_.__name__] | 116 widgets_map = self._widgets[class_.__name__] |
111 else: | 119 else: |
112 if target is not None: | 120 if target is not None: |
113 filter_hash = unicode(class_.getWidgetHash(target, profiles)) | 121 filter_hash = unicode(class_.getWidgetHash(target, profiles)) |
114 else: | 122 else: |
115 filter_hash = None | 123 filter_hash = None |
116 for w_hash, w in widgets_map.iteritems(): | 124 if filter_hash is not None: |
117 if profiles is None or w.profiles.intersection(profiles): | 125 for widget in widgets_map[filter_hash]: |
118 if ( | 126 yield widget |
119 filter_hash is not None | 127 else: |
120 and self.getRootHash(w_hash) != filter_hash | 128 for widget_instances in widgets_map.itervalues(): |
121 ): | 129 for widget in widget_instances: |
122 continue | 130 yield widget |
123 yield w | |
124 | 131 |
125 def getWidget(self, class_, target=None, profiles=None): | 132 def getWidget(self, class_, target=None, profiles=None): |
126 """Get a widget without creating it if it doesn't exist. | 133 """Get a widget without creating it if it doesn't exist. |
127 | 134 |
135 if several instances of widgets with this hash exist, the first one is returned | |
128 @param class_: subclass of QuickWidget, same parameter as used in [getOrCreateWidget] | 136 @param class_: subclass of QuickWidget, same parameter as used in [getOrCreateWidget] |
129 @param target: target depending of the widget, usually a JID instance | 137 @param target: target depending of the widget, usually a JID instance |
130 @param profiles (unicode, iterable[unicode], None): profile(s) to use (may or may not be | 138 @param profiles (unicode, iterable[unicode], None): profile(s) to use (may or may not be |
131 used, depending of the widget class) | 139 used, depending of the widget class) |
132 @return: a class_ instance or None if the widget doesn't exist | 140 @return: a class_ instance or None if the widget doesn't exist |
135 if profiles is not None and isinstance(profiles, unicode): | 143 if profiles is not None and isinstance(profiles, unicode): |
136 profiles = [profiles] | 144 profiles = [profiles] |
137 class_ = self.getRealClass(class_) | 145 class_ = self.getRealClass(class_) |
138 hash_ = class_.getWidgetHash(target, profiles) | 146 hash_ = class_.getWidgetHash(target, profiles) |
139 try: | 147 try: |
140 return self._widgets[class_.__name__][hash_] | 148 return self._widgets[class_.__name__][hash_][0] |
141 except KeyError: | 149 except KeyError: |
142 return None | 150 return None |
143 | 151 |
144 def getOrCreateWidget(self, class_, target, *args, **kwargs): | 152 def getOrCreateWidget(self, class_, target, *args, **kwargs): |
145 """Get an existing widget or create a new one when necessary | 153 """Get an existing widget or create a new one when necessary |
156 [callable]: this method will be called instead of self.host.newWidget | 164 [callable]: this method will be called instead of self.host.newWidget |
157 None: do nothing | 165 None: do nothing |
158 if 'on_existing_widget' is present it can have the following values: | 166 if 'on_existing_widget' is present it can have the following values: |
159 C.WIDGET_KEEP [default]: return the existing widget | 167 C.WIDGET_KEEP [default]: return the existing widget |
160 C.WIDGET_RAISE: raise WidgetAlreadyExistsError | 168 C.WIDGET_RAISE: raise WidgetAlreadyExistsError |
161 C.WIDGET_RECREATE: create a new widget *WITH A NEW HASH* | 169 C.WIDGET_RECREATE: create a new widget |
162 if the existing widget has a "recreateArgs" method, it will be called with args list and kwargs dict | 170 if the existing widget has a "recreateArgs" method, it will be called with args list and kwargs dict |
163 so the values can be completed to create correctly the new instance | 171 so the values can be completed to create correctly the new instance |
164 [callable]: this method will be called with existing widget as argument | 172 [callable]: this method will be called with existing widget as argument, the widget to use must be returned |
165 if 'force_hash' is present, the hash given in value will be used instead of the one returned by class_.getWidgetHash | 173 if 'force_hash' is present, the hash given in value will be used instead of the one returned by class_.getWidgetHash |
166 other keys will be used to instanciate class_ if the case happen (e.g. if type_ is present and class_ is a QuickChat subclass, | 174 other keys will be used to instanciate class_ if the case happen (e.g. if type_ is present and class_ is a QuickChat subclass, |
167 it will be used to create a new QuickChat instance). | 175 it will be used to create a new QuickChat instance). |
168 @return: a class_ instance, either new or already existing | 176 @return: a class_ instance, either new or already existing |
169 """ | 177 """ |
209 ) # we sorts widgets by classes | 217 ) # we sorts widgets by classes |
210 if not cls.SINGLE: | 218 if not cls.SINGLE: |
211 widget = None # if the class is not SINGLE, we always create a new widget | 219 widget = None # if the class is not SINGLE, we always create a new widget |
212 else: | 220 else: |
213 try: | 221 try: |
214 widget = widgets_map[hash_] | 222 widget = widgets_map[hash_][0] |
215 widget.addTarget(target) | |
216 except KeyError: | 223 except KeyError: |
217 widget = None | 224 widget = None |
225 else: | |
226 widget.addTarget(target) | |
218 | 227 |
219 if widget is None: | 228 if widget is None: |
220 # we need to create a new widget | 229 # we need to create a new widget |
221 log.debug(u"Creating new widget for target {} {}".format(target, cls)) | 230 log.debug(u"Creating new widget for target {} {}".format(target, cls)) |
222 widget = cls(*_args, **_kwargs) | 231 widget = cls(*_args, **_kwargs) |
223 widgets_map[hash_] = widget | 232 widgets_map[hash_] = [widget] |
224 | 233 |
225 if on_new_widget == C.WIDGET_NEW: | 234 if on_new_widget == C.WIDGET_NEW: |
226 self.host.newWidget(widget) | 235 self.host.newWidget(widget) |
227 elif callable(on_new_widget): | 236 elif callable(on_new_widget): |
228 on_new_widget(widget) | 237 on_new_widget(widget) |
233 if on_existing_widget == C.WIDGET_KEEP: | 242 if on_existing_widget == C.WIDGET_KEEP: |
234 pass | 243 pass |
235 elif on_existing_widget == C.WIDGET_RAISE: | 244 elif on_existing_widget == C.WIDGET_RAISE: |
236 raise WidgetAlreadyExistsError(hash_) | 245 raise WidgetAlreadyExistsError(hash_) |
237 elif on_existing_widget == C.WIDGET_RECREATE: | 246 elif on_existing_widget == C.WIDGET_RECREATE: |
238 # we use getOrCreateWidget to recreate the new widget | |
239 # /!\ we use args and kwargs and not _args and _kwargs because we need the original args | |
240 # we need to get rid of kwargs special options | |
241 new_kwargs = kwargs.copy() | |
242 try: | |
243 new_kwargs.pop( | |
244 "force_hash" | |
245 ) # FIXME: we use pop instead of del here because pyjamas doesn't raise error on del | |
246 except KeyError: | |
247 pass | |
248 else: | |
249 raise ValueError( | |
250 "force_hash option can't be used with on_existing_widget=RECREATE" | |
251 ) | |
252 | |
253 new_kwargs["on_new_widget"] = on_new_widget | |
254 | |
255 # XXX: keep up-to-date if new special kwargs are added (i.e.: delete these keys here) | |
256 new_kwargs["on_existing_widget"] = C.WIDGET_RAISE | |
257 try: | 247 try: |
258 recreateArgs = widget.recreateArgs | 248 recreateArgs = widget.recreateArgs |
259 except AttributeError: | 249 except AttributeError: |
260 pass | 250 pass |
261 else: | 251 else: |
262 recreateArgs(args, new_kwargs) | 252 recreateArgs(_args, _kwargs) |
263 hash_idx = 1 | 253 widget = cls(*_args, **_kwargs) |
264 while True: | 254 widgets_map[hash_].append(widget) |
265 new_kwargs["force_hash"] = "{}{}{}".format( | 255 log.debug(u"widget <{wid}> already exists, a new one has been recreated" |
266 hash_, NEW_INSTANCE_SUFF, hash_idx | 256 .format(wid=widget)) |
267 ) | |
268 try: | |
269 widget = self.getOrCreateWidget( | |
270 class_, target, *args, **new_kwargs | |
271 ) | |
272 except WidgetAlreadyExistsError: | |
273 hash_idx += 1 | |
274 else: | |
275 log.debug( | |
276 u"Widget already exists, a new one has been recreated with hash {}".format( | |
277 new_kwargs["force_hash"] | |
278 ) | |
279 ) | |
280 break | |
281 elif callable(on_existing_widget): | 257 elif callable(on_existing_widget): |
282 on_existing_widget(widget) | 258 widget = on_existing_widget(widget) |
259 if widget is None: | |
260 raise exceptions.InternalError( | |
261 u"on_existing_widget method must return the widget to use") | |
283 else: | 262 else: |
284 raise exceptions.InternalError( | 263 raise exceptions.InternalError( |
285 "Unexpected on_existing_widget value ({})".format(on_existing_widget) | 264 "Unexpected on_existing_widget value ({})".format(on_existing_widget)) |
286 ) | |
287 | 265 |
288 return widget | 266 return widget |
289 | 267 |
290 def deleteWidget(self, widget_to_delete, *args, **kwargs): | 268 def deleteWidget(self, widget_to_delete, *args, **kwargs): |
291 """Delete a widget | 269 """Delete a widget |
304 if self.host.selected_widget == widget_to_delete: | 282 if self.host.selected_widget == widget_to_delete: |
305 self.host.selected_widget = None | 283 self.host.selected_widget = None |
306 | 284 |
307 for widget_map in self._widgets.itervalues(): | 285 for widget_map in self._widgets.itervalues(): |
308 to_delete = set() | 286 to_delete = set() |
309 for hash_, widget in widget_map.iteritems(): | 287 for hash_, widget_instances in widget_map.iteritems(): |
310 if widget_to_delete is widget: | 288 if widget_to_delete in widget_instances: |
311 to_delete.add(hash_) | 289 widget_instances.remove(widget_to_delete) |
290 if not widget_instances: | |
291 to_delete.add(hash_) | |
312 for hash_ in to_delete: | 292 for hash_ in to_delete: |
313 del widget_map[hash_] | 293 del widget_map[hash_] |
314 | 294 |
315 | 295 |
316 class QuickWidget(object): | 296 class QuickWidget(object): |