Mercurial > libervia-backend
comparison frontends/src/quick_frontend/quick_widgets.py @ 1367:f71a0fc26886
merged branch frontends_multi_profiles
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 18 Mar 2015 10:52:28 +0100 |
parents | 273b044fde6d |
children | 069ad98b360d |
comparison
equal
deleted
inserted
replaced
1295:1e3b1f9ad6e2 | 1367:f71a0fc26886 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # helper class for making a SAT frontend | |
5 # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # 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/>. | |
19 | |
20 from sat.core.log import getLogger | |
21 log = getLogger(__name__) | |
22 from sat.core import exceptions | |
23 | |
24 from sat_frontends.quick_frontend.constants import Const as C | |
25 | |
26 classes_map = {} | |
27 | |
28 | |
29 try: | |
30 # FIXME: to be removed when an acceptable solution is here | |
31 unicode('') # XXX: unicode doesn't exist in pyjamas | |
32 except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options | |
33 unicode = str | |
34 | |
35 | |
36 def register(base_cls, child_cls=None): | |
37 """Register a child class to use by default when a base class is needed | |
38 | |
39 @param base_cls: "Quick..." base class (like QuickChat or QuickContact), must inherit from QuickWidget | |
40 @param child_cls: inherited class to use when Quick... class is requested, must inherit from base_cls. | |
41 Can be None if it's the base_cls itself which register | |
42 """ | |
43 # FIXME: we use base_cls.__name__ instead of base_cls directly because pyjamas because | |
44 # in the second case | |
45 classes_map[base_cls.__name__] = child_cls | |
46 | |
47 | |
48 class WidgetAlreadyExistsError(Exception): | |
49 pass | |
50 | |
51 | |
52 class QuickWidgetsManager(object): | |
53 """This class is used to manage all the widgets of a frontend | |
54 A widget can be a window, a graphical thing, or someting else depending of the frontend""" | |
55 | |
56 def __init__(self, host): | |
57 self.host = host | |
58 self._widgets = {} | |
59 | |
60 def __iter__(self): | |
61 """Iterate throught all widgets""" | |
62 for widget_map in self._widgets.itervalues(): | |
63 for widget in widget_map.itervalues(): | |
64 yield widget | |
65 | |
66 def getRealClass(self, class_): | |
67 """Return class registered for given class_ | |
68 | |
69 @param class_: subclass of QuickWidget | |
70 @return: class actually used to create widget | |
71 """ | |
72 try: | |
73 # FIXME: we use base_cls.__name__ instead of base_cls directly because pyjamas bugs | |
74 # in the second case | |
75 cls = classes_map[class_.__name__] | |
76 except KeyError: | |
77 cls = class_ | |
78 if cls is None: | |
79 raise exceptions.InternalError("There is not class registered for {}".format(class_)) | |
80 return cls | |
81 | |
82 def getWidgets(self, class_): | |
83 """Get all subclassed widgets | |
84 | |
85 @param class_: subclass of QuickWidget, same parameter as used in [getOrCreateWidget] | |
86 @return: iterator on widgets | |
87 """ | |
88 class_ = self.getRealClass(class_) | |
89 try: | |
90 widgets_map = self._widgets[class_.__name__] | |
91 except KeyError: | |
92 return iter([]) | |
93 else: | |
94 return widgets_map.itervalues() | |
95 | |
96 def getWidget(self, class_, target, profile): | |
97 """Get a widget without creating it if it doesn't exist. | |
98 | |
99 @param class_(class): class of the widget to create | |
100 @param target: target depending of the widget, usually a JID instance | |
101 @param profile (unicode): %(doc_profile)s | |
102 @return: a class_ instance or None if the widget doesn't exist | |
103 """ | |
104 class_ = self.getRealClass(class_) | |
105 hash_ = class_.getWidgetHash(target, profile) | |
106 try: | |
107 return self._widgets[class_.__name__][hash_] | |
108 except KeyError: | |
109 return None | |
110 | |
111 def getOrCreateWidget(self, class_, target, *args, **kwargs): | |
112 """Get an existing widget or create a new one when necessary | |
113 | |
114 If the widget is new, self.host.newWidget will be called with it. | |
115 @param class_(class): class of the widget to create | |
116 @param target: target depending of the widget, usually a JID instance | |
117 @param args(list): optional args to create a new instance of class_ | |
118 @param kwargs(dict): optional kwargs to create a new instance of class_ | |
119 if 'profile' key is present, it will be popped and put in 'profiles' | |
120 if there is neither 'profile' nor 'profiles', None will be used for 'profiles' | |
121 if 'on_new_widget' is present it can have the following values: | |
122 C.WIDGET_NEW [default]: self.host.newWidget will be called on widget creation | |
123 [callable]: this method will be called instead of self.host.newWidget | |
124 None: do nothing | |
125 if 'on_existing_widget' is present it can have the following values: | |
126 C.WIDGET_KEEP [default]: return the existing widget | |
127 C.WIDGET_RAISE: raise WidgetAlreadyExistsError | |
128 C.WIDGET_RECREATE: create a new widget *WITH A NEW HASH* | |
129 [callable]: this method will be called with existing widget as argument | |
130 if 'force_hash' is present, the hash given in value will be used instead of the one returned by class_.getWidgetHash | |
131 other keys will be used to instanciate class_ if the case happen (e.g. if type_ is present and class_ is a QuickChat subclass, | |
132 it will be used to create a new QuickChat instance). | |
133 @return: a class_ instance, either new or already existing | |
134 """ | |
135 cls = self.getRealClass(class_) | |
136 | |
137 ## arguments management ## | |
138 _args = [self.host, target] + list(args) or [] # FIXME: check if it's really necessary to use optional args | |
139 _kwargs = kwargs or {} | |
140 if 'profiles' in _kwargs and 'profile' in _kwargs: | |
141 raise ValueError("You can't have 'profile' and 'profiles' keys at the same time") | |
142 try: | |
143 _kwargs['profiles'] = _kwargs.pop('profile') | |
144 except KeyError: | |
145 if not 'profiles' in _kwargs: | |
146 _kwargs['profiles'] = None | |
147 | |
148 #on_new_widget tell what to do for the new widget creation | |
149 try: | |
150 on_new_widget = _kwargs.pop('on_new_widget') | |
151 except KeyError: | |
152 on_new_widget = C.WIDGET_NEW | |
153 | |
154 #on_existing_widget tell what to do when the widget already exists | |
155 try: | |
156 on_existing_widget = _kwargs.pop('on_existing_widget') | |
157 except KeyError: | |
158 on_existing_widget = C.WIDGET_KEEP | |
159 | |
160 ## we get the hash ## | |
161 try: | |
162 hash_ = _kwargs.pop('force_hash') | |
163 except KeyError: | |
164 hash_ = cls.getWidgetHash(target, _kwargs['profiles']) | |
165 | |
166 ## widget creation or retrieval ## | |
167 | |
168 widgets_map = self._widgets.setdefault(cls.__name__, {}) # we sorts widgets by classes | |
169 if not cls.SINGLE: | |
170 widget = None # if the class is not SINGLE, we always create a new widget | |
171 else: | |
172 try: | |
173 widget = widgets_map[hash_] | |
174 widget.addTarget(target) | |
175 except KeyError: | |
176 widget = None | |
177 | |
178 if widget is None: | |
179 # we need to create a new widget | |
180 log.debug(u"Creating new widget for target {} {}".format(target, cls)) | |
181 widget = cls(*_args, **_kwargs) | |
182 widgets_map[hash_] = widget | |
183 | |
184 if on_new_widget == C.WIDGET_NEW: | |
185 self.host.newWidget(widget) | |
186 elif callable(on_new_widget): | |
187 on_new_widget(widget) | |
188 else: | |
189 assert on_new_widget is None | |
190 else: | |
191 # the widget already exists | |
192 if on_existing_widget == C.WIDGET_KEEP: | |
193 pass | |
194 elif on_existing_widget == C.WIDGET_RAISE: | |
195 raise WidgetAlreadyExistsError(hash_) | |
196 elif on_existing_widget == C.WIDGET_RECREATE: | |
197 # we use getOrCreateWidget to recreate the new widget | |
198 # /!\ we use args and kwargs and not _args and _kwargs because we need the original args | |
199 # we need to get rid of kwargs special options | |
200 new_kwargs = kwargs.copy() | |
201 try: | |
202 new_kwargs.pop('force_hash') # FIXME: we use pop instead of del here because pyjamas doesn't raise error on del | |
203 except KeyError: | |
204 pass | |
205 else: | |
206 raise ValueError("force_hash option can't be used with on_existing_widget=RECREATE") | |
207 | |
208 # XXX: keep up-to-date if new special kwargs are added (i.e.: delete these keys here) | |
209 new_kwargs['on_existing_widget'] = C.WIDGET_RAISE | |
210 hash_idx = 1 | |
211 while True: | |
212 new_kwargs['force_hash'] = "{}_new_instance_{}".format(hash_, hash_idx) | |
213 try: | |
214 widget = self.getOrCreateWidget(class_, target, *args, **new_kwargs) | |
215 except WidgetAlreadyExistsError: | |
216 hash_idx += 1 | |
217 else: | |
218 log.debug(u"Widget already exists, a new one has been recreated with hash {}".format(new_kwargs['force_hash'])) | |
219 break | |
220 elif callable(on_existing_widget): | |
221 on_existing_widget(widget) | |
222 else: | |
223 raise exceptions.InternalError("Unexpected on_existing_widget value ({})".format(on_existing_widget)) | |
224 | |
225 return widget | |
226 | |
227 def deleteWidget(self, widget_to_delete): | |
228 """Delete a widget | |
229 | |
230 widget's onDelete method will be called before deletion | |
231 """ | |
232 widget_to_delete.onDelete() | |
233 | |
234 for widget_map in self._widgets.itervalues(): | |
235 for hash_, widget in widget_map.iteritems(): | |
236 if widget_to_delete is widget: | |
237 del widget_map[hash_] | |
238 | |
239 | |
240 class QuickWidget(object): | |
241 """generic widget base""" | |
242 SINGLE=True # if True, there can be only one widget per target(s) | |
243 PROFILES_MULTIPLE=False | |
244 PROFILES_ALLOW_NONE=False | |
245 | |
246 def __init__(self, host, target, profiles=None): | |
247 """ | |
248 @param host: %(doc_host)s | |
249 @param target: target specific for this widget class | |
250 @param profiles: can be either: | |
251 - (unicode): used when widget class manage a unique profile | |
252 - (iterable): some widget class can manage several profiles, several at once can be specified here | |
253 - None: no profile is managed by this widget class (rare) | |
254 @raise: ValueError when (iterable) or None is given to profiles for a widget class which manage one unique profile. | |
255 """ | |
256 self.host = host | |
257 self.targets = set() | |
258 self.addTarget(target) | |
259 self.profiles = set() | |
260 if isinstance(profiles, basestring): | |
261 self.addProfile(profiles) | |
262 elif profiles is None: | |
263 if not self.PROFILES_ALLOW_NONE: | |
264 raise ValueError("profiles can't have a value of None") | |
265 else: | |
266 if not self.PROFILES_MULTIPLE: | |
267 raise ValueError("multiple profiles are not allowed") | |
268 for profile in profiles: | |
269 self.addProfile(profile) | |
270 | |
271 @property | |
272 def profile(self): | |
273 assert len(self.profiles) == 1 and not self.PROFILES_MULTIPLE and not self.PROFILES_ALLOW_NONE | |
274 return list(self.profiles)[0] | |
275 | |
276 def addTarget(self, target): | |
277 """Add a target if it doesn't already exists | |
278 | |
279 @param target: target to add | |
280 """ | |
281 self.targets.add(target) | |
282 | |
283 def addProfile(self, profile): | |
284 """Add a profile is if doesn't already exists | |
285 | |
286 @param profile: profile to add | |
287 """ | |
288 if self.profiles and not self.PROFILES_MULTIPLE: | |
289 raise ValueError("multiple profiles are not allowed") | |
290 self.profiles.add(profile) | |
291 | |
292 @staticmethod | |
293 def getWidgetHash(target, profiles): | |
294 """Return the hash associated with this target for this widget class | |
295 | |
296 some widget classes can manage several target on the same instance | |
297 (e.g.: a chat widget with multiple resources on the same bare jid), | |
298 this method allow to return a hash associated to one or several targets | |
299 to retrieve the good instance. For example, a widget managing JID targets, | |
300 and all resource of the same bare jid would return the bare jid as hash. | |
301 | |
302 @param target: target to check | |
303 @param profiles: profile(s) associated to target, see __init__ docstring | |
304 @return: a hash (can correspond to one or many targets or profiles, depending of widget class) | |
305 """ | |
306 return unicode(target) # by defaut, there is one hash for one target | |
307 | |
308 def onDelete(self): | |
309 """Called when a widget is deleted""" | |
310 log.debug(u"deleting widget {}".format(self)) # Must be implemented by frontends |