Mercurial > libervia-backend
comparison sat_frontends/quick_frontend/quick_widgets.py @ 2562:26edcf3a30eb
core, setup: huge cleaning:
- moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention
- move twisted directory to root
- removed all hacks from setup.py, and added missing dependencies, it is now clean
- use https URL for website in setup.py
- removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed
- renamed sat.sh to sat and fixed its installation
- added python_requires to specify Python version needed
- replaced glib2reactor which use deprecated code by gtk3reactor
sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Apr 2018 19:44:50 +0200 |
parents | frontends/src/quick_frontend/quick_widgets.py@0046283a285d |
children | 56f94936df1e |
comparison
equal
deleted
inserted
replaced
2561:bd30dc3ffe5a | 2562:26edcf3a30eb |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # helper class for making a SAT frontend | |
5 # Copyright (C) 2009-2018 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 from sat_frontends.quick_frontend.constants import Const as C | |
24 | |
25 | |
26 NEW_INSTANCE_SUFF = '_new_instance_' | |
27 classes_map = {} | |
28 | |
29 | |
30 try: | |
31 # FIXME: to be removed when an acceptable solution is here | |
32 unicode('') # XXX: unicode doesn't exist in pyjamas | |
33 except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options | |
34 unicode = str | |
35 | |
36 | |
37 def register(base_cls, child_cls=None): | |
38 """Register a child class to use by default when a base class is needed | |
39 | |
40 @param base_cls: "Quick..." base class (like QuickChat or QuickContact), must inherit from QuickWidget | |
41 @param child_cls: inherited class to use when Quick... class is requested, must inherit from base_cls. | |
42 Can be None if it's the base_cls itself which register | |
43 """ | |
44 # FIXME: we use base_cls.__name__ instead of base_cls directly because pyjamas because | |
45 # in the second case | |
46 classes_map[base_cls.__name__] = child_cls | |
47 | |
48 | |
49 class WidgetAlreadyExistsError(Exception): | |
50 pass | |
51 | |
52 | |
53 class QuickWidgetsManager(object): | |
54 """This class is used to manage all the widgets of a frontend | |
55 A widget can be a window, a graphical thing, or someting else depending of the frontend""" | |
56 | |
57 def __init__(self, host): | |
58 self.host = host | |
59 self._widgets = {} | |
60 | |
61 def __iter__(self): | |
62 """Iterate throught all widgets""" | |
63 for widget_map in self._widgets.itervalues(): | |
64 for widget in widget_map.itervalues(): | |
65 yield widget | |
66 | |
67 def getRealClass(self, class_): | |
68 """Return class registered for given class_ | |
69 | |
70 @param class_: subclass of QuickWidget | |
71 @return: class actually used to create widget | |
72 """ | |
73 try: | |
74 # FIXME: we use base_cls.__name__ instead of base_cls directly because pyjamas bugs | |
75 # in the second case | |
76 cls = classes_map[class_.__name__] | |
77 except KeyError: | |
78 cls = class_ | |
79 if cls is None: | |
80 raise exceptions.InternalError("There is not class registered for {}".format(class_)) | |
81 return cls | |
82 | |
83 def getRootHash(self, hash_): | |
84 """Return root hash (i.e. hash without new instance suffix for recreated widgets | |
85 | |
86 @param hash_(immutable): hash of a widget | |
87 @return (unicode): root hash (transtyped to unicode) | |
88 """ | |
89 return unicode(hash_).split(NEW_INSTANCE_SUFF)[0] | |
90 | |
91 def getWidgets(self, class_, target=None, profiles=None): | |
92 """Get all subclassed widgets instances | |
93 | |
94 @param class_: subclass of QuickWidget, same parameter as used in [getOrCreateWidget] | |
95 @param target: if not None, construct a hash with this target and filter corresponding widgets | |
96 recreated widgets (with new instance suffix) are handled | |
97 @param profiles(iterable, None): if not None, filter on instances linked to these profiles | |
98 @return: iterator on widgets | |
99 """ | |
100 class_ = self.getRealClass(class_) | |
101 try: | |
102 widgets_map = self._widgets[class_.__name__] | |
103 except KeyError: | |
104 return | |
105 else: | |
106 if target is not None: | |
107 filter_hash = unicode(class_.getWidgetHash(target, profiles)) | |
108 else: | |
109 filter_hash = None | |
110 for w_hash, w in widgets_map.iteritems(): | |
111 if profiles is None or w.profiles.intersection(profiles): | |
112 if filter_hash is not None and self.getRootHash(w_hash) != filter_hash: | |
113 continue | |
114 yield w | |
115 | |
116 def getWidget(self, class_, target=None, profiles=None): | |
117 """Get a widget without creating it if it doesn't exist. | |
118 | |
119 @param class_(class): class of the widget to create | |
120 @param target: target depending of the widget, usually a JID instance | |
121 @param profiles (unicode, iterable[unicode], None): profile(s) to use (may or may not be | |
122 used, depending of the widget class) | |
123 @return: a class_ instance or None if the widget doesn't exist | |
124 """ | |
125 assert (target is not None) or (profiles is not None) | |
126 if profiles is not None and isinstance(profiles, unicode): | |
127 profiles = [profiles] | |
128 class_ = self.getRealClass(class_) | |
129 hash_ = class_.getWidgetHash(target, profiles) | |
130 try: | |
131 return self._widgets[class_.__name__][hash_] | |
132 except KeyError: | |
133 return None | |
134 | |
135 def getOrCreateWidget(self, class_, target, *args, **kwargs): | |
136 """Get an existing widget or create a new one when necessary | |
137 | |
138 If the widget is new, self.host.newWidget will be called with it. | |
139 @param class_(class): class of the widget to create | |
140 @param target: target depending of the widget, usually a JID instance | |
141 @param args(list): optional args to create a new instance of class_ | |
142 @param kwargs(dict): optional kwargs to create a new instance of class_ | |
143 if 'profile' key is present, it will be popped and put in 'profiles' | |
144 if there is neither 'profile' nor 'profiles', None will be used for 'profiles' | |
145 if 'on_new_widget' is present it can have the following values: | |
146 C.WIDGET_NEW [default]: self.host.newWidget will be called on widget creation | |
147 [callable]: this method will be called instead of self.host.newWidget | |
148 None: do nothing | |
149 if 'on_existing_widget' is present it can have the following values: | |
150 C.WIDGET_KEEP [default]: return the existing widget | |
151 C.WIDGET_RAISE: raise WidgetAlreadyExistsError | |
152 C.WIDGET_RECREATE: create a new widget *WITH A NEW HASH* | |
153 if the existing widget has a "recreateArgs" method, it will be called with args list and kwargs dict | |
154 so the values can be completed to create correctly the new instance | |
155 [callable]: this method will be called with existing widget as argument | |
156 if 'force_hash' is present, the hash given in value will be used instead of the one returned by class_.getWidgetHash | |
157 other keys will be used to instanciate class_ if the case happen (e.g. if type_ is present and class_ is a QuickChat subclass, | |
158 it will be used to create a new QuickChat instance). | |
159 @return: a class_ instance, either new or already existing | |
160 """ | |
161 cls = self.getRealClass(class_) | |
162 | |
163 ## arguments management ## | |
164 _args = [self.host, target] + list(args) or [] # FIXME: check if it's really necessary to use optional args | |
165 _kwargs = kwargs or {} | |
166 if 'profiles' in _kwargs and 'profile' in _kwargs: | |
167 raise ValueError("You can't have 'profile' and 'profiles' keys at the same time") | |
168 try: | |
169 _kwargs['profiles'] = [_kwargs.pop('profile')] | |
170 except KeyError: | |
171 if not 'profiles' in _kwargs: | |
172 _kwargs['profiles'] = None | |
173 | |
174 #on_new_widget tell what to do for the new widget creation | |
175 try: | |
176 on_new_widget = _kwargs.pop('on_new_widget') | |
177 except KeyError: | |
178 on_new_widget = C.WIDGET_NEW | |
179 | |
180 #on_existing_widget tell what to do when the widget already exists | |
181 try: | |
182 on_existing_widget = _kwargs.pop('on_existing_widget') | |
183 except KeyError: | |
184 on_existing_widget = C.WIDGET_KEEP | |
185 | |
186 ## we get the hash ## | |
187 try: | |
188 hash_ = _kwargs.pop('force_hash') | |
189 except KeyError: | |
190 hash_ = cls.getWidgetHash(target, _kwargs['profiles']) | |
191 | |
192 ## widget creation or retrieval ## | |
193 | |
194 widgets_map = self._widgets.setdefault(cls.__name__, {}) # we sorts widgets by classes | |
195 if not cls.SINGLE: | |
196 widget = None # if the class is not SINGLE, we always create a new widget | |
197 else: | |
198 try: | |
199 widget = widgets_map[hash_] | |
200 widget.addTarget(target) | |
201 except KeyError: | |
202 widget = None | |
203 | |
204 if widget is None: | |
205 # we need to create a new widget | |
206 log.debug(u"Creating new widget for target {} {}".format(target, cls)) | |
207 widget = cls(*_args, **_kwargs) | |
208 widgets_map[hash_] = widget | |
209 | |
210 if on_new_widget == C.WIDGET_NEW: | |
211 self.host.newWidget(widget) | |
212 elif callable(on_new_widget): | |
213 on_new_widget(widget) | |
214 else: | |
215 assert on_new_widget is None | |
216 else: | |
217 # the widget already exists | |
218 if on_existing_widget == C.WIDGET_KEEP: | |
219 pass | |
220 elif on_existing_widget == C.WIDGET_RAISE: | |
221 raise WidgetAlreadyExistsError(hash_) | |
222 elif on_existing_widget == C.WIDGET_RECREATE: | |
223 # we use getOrCreateWidget to recreate the new widget | |
224 # /!\ we use args and kwargs and not _args and _kwargs because we need the original args | |
225 # we need to get rid of kwargs special options | |
226 new_kwargs = kwargs.copy() | |
227 try: | |
228 new_kwargs.pop('force_hash') # FIXME: we use pop instead of del here because pyjamas doesn't raise error on del | |
229 except KeyError: | |
230 pass | |
231 else: | |
232 raise ValueError("force_hash option can't be used with on_existing_widget=RECREATE") | |
233 | |
234 new_kwargs['on_new_widget'] = on_new_widget | |
235 | |
236 # XXX: keep up-to-date if new special kwargs are added (i.e.: delete these keys here) | |
237 new_kwargs['on_existing_widget'] = C.WIDGET_RAISE | |
238 try: | |
239 recreateArgs = widget.recreateArgs | |
240 except AttributeError: | |
241 pass | |
242 else: | |
243 recreateArgs(args, new_kwargs) | |
244 hash_idx = 1 | |
245 while True: | |
246 new_kwargs['force_hash'] = "{}{}{}".format(hash_, NEW_INSTANCE_SUFF, hash_idx) | |
247 try: | |
248 widget = self.getOrCreateWidget(class_, target, *args, **new_kwargs) | |
249 except WidgetAlreadyExistsError: | |
250 hash_idx += 1 | |
251 else: | |
252 log.debug(u"Widget already exists, a new one has been recreated with hash {}".format(new_kwargs['force_hash'])) | |
253 break | |
254 elif callable(on_existing_widget): | |
255 on_existing_widget(widget) | |
256 else: | |
257 raise exceptions.InternalError("Unexpected on_existing_widget value ({})".format(on_existing_widget)) | |
258 | |
259 return widget | |
260 | |
261 def deleteWidget(self, widget_to_delete, *args, **kwargs): | |
262 """Delete a widget | |
263 | |
264 this method must be called by frontends when a widget is deleted | |
265 widget's onDelete method will be called before deletion | |
266 @param widget_to_delete(QuickWidget): widget which need to deleted | |
267 @param *args: extra arguments to pass to onDelete | |
268 @param *kwargs: extra keywords arguments to pass to onDelete | |
269 the extra arguments are not use by QuickFrontend, it's is up to | |
270 the frontend to use them or not | |
271 """ | |
272 if widget_to_delete.onDelete(*args, **kwargs) == False: | |
273 return | |
274 | |
275 if self.host.selected_widget == widget_to_delete: | |
276 self.host.selected_widget = None | |
277 | |
278 for widget_map in self._widgets.itervalues(): | |
279 to_delete = set() | |
280 for hash_, widget in widget_map.iteritems(): | |
281 if widget_to_delete is widget: | |
282 to_delete.add(hash_) | |
283 for hash_ in to_delete: | |
284 del widget_map[hash_] | |
285 | |
286 | |
287 class QuickWidget(object): | |
288 """generic widget base""" | |
289 SINGLE=True # if True, there can be only one widget per target(s) | |
290 PROFILES_MULTIPLE=False # If True, this widget can handle several profiles at once | |
291 PROFILES_ALLOW_NONE=False # If True, this widget can be used without profile | |
292 | |
293 def __init__(self, host, target, profiles=None): | |
294 """ | |
295 @param host: %(doc_host)s | |
296 @param target: target specific for this widget class | |
297 @param profiles: can be either: | |
298 - (unicode): used when widget class manage a unique profile | |
299 - (iterable): some widget class can manage several profiles, several at once can be specified here | |
300 - None: no profile is managed by this widget class (rare) | |
301 @raise: ValueError when (iterable) or None is given to profiles for a widget class which manage one unique profile. | |
302 """ | |
303 self.host = host | |
304 self.targets = set() | |
305 self.addTarget(target) | |
306 self.profiles = set() | |
307 if isinstance(profiles, basestring): | |
308 self.addProfile(profiles) | |
309 elif profiles is None: | |
310 if not self.PROFILES_ALLOW_NONE: | |
311 raise ValueError("profiles can't have a value of None") | |
312 else: | |
313 for profile in profiles: | |
314 self.addProfile(profile) | |
315 if not self.profiles: | |
316 raise ValueError("no profile found, use None for no profile classes") | |
317 | |
318 @property | |
319 def profile(self): | |
320 assert len(self.profiles) == 1 and not self.PROFILES_MULTIPLE and not self.PROFILES_ALLOW_NONE | |
321 return list(self.profiles)[0] | |
322 | |
323 def addTarget(self, target): | |
324 """Add a target if it doesn't already exists | |
325 | |
326 @param target: target to add | |
327 """ | |
328 self.targets.add(target) | |
329 | |
330 def addProfile(self, profile): | |
331 """Add a profile is if doesn't already exists | |
332 | |
333 @param profile: profile to add | |
334 """ | |
335 if self.profiles and not self.PROFILES_MULTIPLE: | |
336 raise ValueError("multiple profiles are not allowed") | |
337 self.profiles.add(profile) | |
338 | |
339 @staticmethod | |
340 def getWidgetHash(target, profiles): | |
341 """Return the hash associated with this target for this widget class | |
342 | |
343 some widget classes can manage several target on the same instance | |
344 (e.g.: a chat widget with multiple resources on the same bare jid), | |
345 this method allow to return a hash associated to one or several targets | |
346 to retrieve the good instance. For example, a widget managing JID targets, | |
347 and all resource of the same bare jid would return the bare jid as hash. | |
348 | |
349 @param target: target to check | |
350 @param profiles: profile(s) associated to target, see __init__ docstring | |
351 @return: a hash (can correspond to one or many targets or profiles, depending of widget class) | |
352 """ | |
353 return unicode(target) # by defaut, there is one hash for one target | |
354 | |
355 def onDelete(self, *args, **kwargs): | |
356 """Called when a widget is being deleted | |
357 | |
358 @return (boot, None): False to cancel deletion | |
359 all other value continue deletion | |
360 """ | |
361 log.debug(u"widget {} deleted".format(self)) | |
362 return True | |
363 |