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):