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