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