comparison frontends/src/quick_frontend/quick_app.py @ 1290:faa1129559b8 frontends_multi_profiles

core, frontends: refactoring to base Libervia on QuickFrontend (big mixed commit): /!\ not finished, everything is still instable ! - bridge: DBus bridge has been modified to allow blocking call to be called in the same way as asynchronous calls - bridge: calls with a callback and no errback are now possible, default errback log the error - constants: removed hack to manage presence without OrderedDict, as an OrderedDict like class has been implemented in Libervia - core: getLastResource has been removed and replaced by getMainResource (there is a global better management of resources) - various style improvments: use of constants when possible, fixed variable overlaps, import of module instead of direct class import - frontends: printInfo and printMessage methods in (Quick)Chat are more generic (use of extra instead of timestamp) - frontends: bridge creation and option parsing (command line arguments) are now specified by the frontend in QuickApp __init__ - frontends: ProfileManager manage a more complete plug sequence (some stuff formerly manage in contact_list have moved to ProfileManager) - quick_frontend (quick_widgets): QuickWidgetsManager is now iterable (all widgets are then returned), or can return an iterator on a specific class (return all widgets of this class) with getWidgets - frontends: tools.jid can now be used in Pyjamas, with some care - frontends (XMLUI): profile is now managed - core (memory): big improvment on entities cache management (and specially resource management) - core (params/exceptions): added PermissionError - various fixes and improvments, check diff for more details
author Goffi <goffi@goffi.org>
date Sat, 24 Jan 2015 01:00:29 +0100
parents e3a9ea76de35
children 6c7d89843f1b
comparison
equal deleted inserted replaced
1289:653f2e2eea31 1290:faa1129559b8
20 from sat.core.i18n import _ 20 from sat.core.i18n import _
21 import sys 21 import sys
22 from sat.core.log import getLogger 22 from sat.core.log import getLogger
23 log = getLogger(__name__) 23 log = getLogger(__name__)
24 from sat.core import exceptions 24 from sat.core import exceptions
25 from sat_frontends.bridge.DBus import DBusBridgeFrontend
26 from sat_frontends.tools import jid 25 from sat_frontends.tools import jid
27 from sat_frontends.quick_frontend.quick_widgets import QuickWidgetsManager 26 from sat_frontends.quick_frontend.quick_widgets import QuickWidgetsManager
28 from sat_frontends.quick_frontend import quick_chat 27 from sat_frontends.quick_frontend import quick_chat
29 from optparse import OptionParser
30 28
31 from sat_frontends.quick_frontend.constants import Const as C 29 from sat_frontends.quick_frontend.constants import Const as C
32 30
33 31
34 class ProfileManager(object): 32 class ProfileManager(object):
76 self.host.setStatusOnline(True, profile=self.profile) 74 self.host.setStatusOnline(True, profile=self.profile)
77 75
78 contact_list.fill() 76 contact_list.fill()
79 77
80 #The waiting subscription requests 78 #The waiting subscription requests
81 waitingSub = self.bridge.getWaitingSub(self.profile) 79 self.bridge.getWaitingSub(self.profile, callback=self._plug_profile_gotWaitingSub)
82 for sub in waitingSub: 80
83 self.host.subscribeHandler(waitingSub[sub], sub, self.profile) 81 def _plug_profile_gotWaitingSub(self, waiting_sub):
84 82 for sub in waiting_sub:
85 #Now we open the MUC window where we already are: 83 self.host.subscribeHandler(waiting_sub[sub], sub, self.profile)
86 for room_args in self.bridge.getRoomsJoined(self.profile): 84
87 self.host.roomJoinedHandler(*room_args, profile=self.profile) 85 self.bridge.getRoomsJoined(self.profile, callback=self._plug_profile_gotRoomsJoined)
88 86
89 for subject_args in self.bridge.getRoomsSubjects(self.profile): 87 def _plug_profile_gotRoomsJoined(self, rooms_args):
90 self.host.roomNewSubjectHandler(*subject_args, profile=self.profile) 88 #Now we open the MUC window where we already are:
91 89 for room_args in rooms_args:
92 #Finaly, we get the waiting confirmation requests 90 self.host.roomJoinedHandler(*room_args, profile=self.profile)
93 for confirm_id, confirm_type, data in self.bridge.getWaitingConf(self.profile): 91
94 self.host.askConfirmationHandler(confirm_id, confirm_type, data, self.profile) 92 self.bridge.getRoomsSubjects(self.profile, callback=self._plug_profile_gotRoomsSubjects)
93
94 def _plug_profile_gotRoomsSubjects(self, subjects_args):
95 for subject_args in subjects_args:
96 self.host.roomNewSubjectHandler(*subject_args, profile=self.profile)
97
98 #Presence must be requested after rooms are filled
99 self.host.bridge.getPresenceStatuses(self.profile, callback=self._plug_profile_gotPresences)
100
101
102 def _plug_profile_gotPresences(self, presences):
103 def gotEntityData(data, contact):
104 for key in ('avatar', 'nick'):
105 if key in data:
106 self.host.entityDataUpdatedHandler(contact, key, data[key], self.profile)
107
108 for contact in presences:
109 for res in presences[contact]:
110 jabber_id = ('%s/%s' % (jid.JID(contact).bare, res)) if res else contact
111 show = presences[contact][res][0]
112 priority = presences[contact][res][1]
113 statuses = presences[contact][res][2]
114 self.host.presenceUpdateHandler(jabber_id, show, priority, statuses, self.profile)
115 self.host.bridge.getEntityData(contact, ['avatar', 'nick'], self.profile, callback=lambda data, contact=contact: gotEntityData(data, contact), errback=lambda failure, contact=contact: log.debug("No cache data for {}".format(contact)))
116
117 #Finaly, we get the waiting confirmation requests
118 self.bridge.getWaitingConf(self.profile, callback=self._plug_profile_gotWaitingConf)
119
120 def _plug_profile_gotWaitingConf(self, waiting_confs):
121 for confirm_id, confirm_type, data in waiting_confs:
122 self.host.askConfirmationHandler(confirm_id, confirm_type, data, self.profile)
123
124 # At this point, profile should be fully plugged
125 # and we launch frontend specific method
126 self.host.profilePlugged(self.profile)
95 127
96 def _getParamError(self, ignore): 128 def _getParamError(self, ignore):
97 log.error(_("Can't get profile parameter")) 129 log.error(_("Can't get profile parameter"))
98 130
99 131
130 return self._profiles.keys()[0] 162 return self._profiles.keys()[0]
131 163
132 class QuickApp(object): 164 class QuickApp(object):
133 """This class contain the main methods needed for the frontend""" 165 """This class contain the main methods needed for the frontend"""
134 166
135 def __init__(self): 167 def __init__(self, create_bridge, check_options=None):
168 """Create a frontend application
169
170 @param create_bridge: method to use to create the Bridge
171 @param check_options: method to call to check options (usually command line arguments)
172 """
136 ProfileManager.host = self 173 ProfileManager.host = self
137 self.profiles = ProfilesManager() 174 self.profiles = ProfilesManager()
138 self.contact_lists = {} 175 self.contact_lists = {}
139 self.widgets = QuickWidgetsManager(self) 176 self.widgets = QuickWidgetsManager(self)
140 self.check_options() 177 if check_options is not None:
178 self.options = check_options()
179 else:
180 self.options = None
141 181
142 # widgets 182 # widgets
143 self.visible_widgets = set() # widgets currently visible (must be filled by frontend)
144 self.selected_widget = None # widget currently selected (must be filled by frontend) 183 self.selected_widget = None # widget currently selected (must be filled by frontend)
145 184
146 ## bridge ## 185 ## bridge ##
147 try: 186 try:
148 self.bridge = DBusBridgeFrontend() 187 self.bridge = create_bridge()
149 except exceptions.BridgeExceptionNoService: 188 except exceptions.BridgeExceptionNoService:
150 print(_(u"Can't connect to SàT backend, are you sure it's launched ?")) 189 print(_(u"Can't connect to SàT backend, are you sure it's launched ?"))
151 sys.exit(1) 190 sys.exit(1)
152 except exceptions.BridgeInitError: 191 except exceptions.BridgeInitError:
153 print(_(u"Can't init bridge")) 192 print(_(u"Can't init bridge"))
200 try: 239 try:
201 return self.selected_widget.profile 240 return self.selected_widget.profile
202 except (TypeError, AttributeError): 241 except (TypeError, AttributeError):
203 return self.profiles.chooseOneProfile() 242 return self.profiles.chooseOneProfile()
204 243
244 @property
245 def visible_widgets(self):
246 """widgets currently visible (must be implemented by frontend)"""
247 raise NotImplementedError
248
205 def registerSignal(self, functionName, handler=None, iface="core", with_profile=True): 249 def registerSignal(self, functionName, handler=None, iface="core", with_profile=True):
206 """Register a handler for a signal 250 """Register a handler for a signal
207 251
208 @param functionName (str): name of the signal to handle 252 @param functionName (str): name of the signal to handle
209 @param handler (instancemethod): method to call when the signal arrive, None for calling an automatically named handler (functionName + 'Handler') 253 @param handler (instancemethod): method to call when the signal arrive, None for calling an automatically named handler (functionName + 'Handler')
210 @param iface (str): interface of the bridge to use ('core' or 'plugin') 254 @param iface (str): interface of the bridge to use ('core' or 'plugin')
211 @param with_profile (boolean): True if the signal concerns a specific profile, in that case the profile name has to be passed by the caller 255 @param with_profile (boolean): True if the signal concerns a specific profile, in that case the profile name has to be passed by the caller
212 """ 256 """
213 if handler is None: 257 if handler is None:
214 handler = getattr(self, "%s%s" % (functionName, 'Handler')) 258 handler = getattr(self, "{}{}".format(functionName, 'Handler'))
215 if not with_profile: 259 if not with_profile:
216 self.bridge.register(functionName, handler, iface) 260 self.bridge.register(functionName, handler, iface)
217 return 261 return
218 262
219 def signalReceived(*args, **kwargs): 263 def signalReceived(*args, **kwargs):
234 def postInit(self, profile_manager): 278 def postInit(self, profile_manager):
235 """Must be called after initialization is done, do all automatic task (auto plug profile) 279 """Must be called after initialization is done, do all automatic task (auto plug profile)
236 280
237 @param profile_manager: instance of a subclass of Quick_frontend.QuickProfileManager 281 @param profile_manager: instance of a subclass of Quick_frontend.QuickProfileManager
238 """ 282 """
239 if self.options.profile: 283 if self.options and self.options.profile:
240 profile_manager.autoconnect([self.options.profile]) 284 profile_manager.autoconnect([self.options.profile])
241 285
242 def check_options(self): 286 def profilePlugged(self, profile):
243 """Check command line options""" 287 """Method called when the profile is fully plugged, to launch frontend specific workflow
244 usage = _(""" 288
245 %prog [options] 289 @param profile(unicode): %(doc_profile)s
246 290 """
247 %prog --help for options list 291 pass
248 """)
249 parser = OptionParser(usage=usage) # TODO: use argparse
250
251 parser.add_option("-p", "--profile", help=_("Select the profile to use"))
252
253 (self.options, args) = parser.parse_args()
254 if self.options.profile:
255 self.options.profile = self.options.profile.decode('utf-8')
256 return args
257 292
258 def asyncConnect(self, profile, callback=None, errback=None): 293 def asyncConnect(self, profile, callback=None, errback=None):
259 if not callback: 294 if not callback:
260 callback = lambda dummy: None 295 callback = lambda dummy: None
261 if not errback: 296 if not errback:
279 def plugging_profiles(self): 314 def plugging_profiles(self):
280 """Method to subclass to manage frontend specific things to do 315 """Method to subclass to manage frontend specific things to do
281 316
282 will be called when profiles are choosen and are to be plugged soon 317 will be called when profiles are choosen and are to be plugged soon
283 """ 318 """
284 raise NotImplementedError 319 pass
285 320
286 def unplug_profile(self, profile): 321 def unplug_profile(self, profile):
287 """Tell the application to not follow anymore the profile""" 322 """Tell the application to not follow anymore the profile"""
288 if not profile in self.profiles: 323 if not profile in self.profiles:
289 raise ValueError("The profile [{}] is not plugged".format(profile)) 324 raise ValueError("The profile [{}] is not plugged".format(profile))
290 self.profiles.remove(profile) 325 self.profiles.remove(profile)
291 326
292 def clear_profile(self): 327 def clear_profile(self):
293 self.profiles.clear() 328 self.profiles.clear()
294 329
330 def addContactList(self, profile):
331 """Method to subclass to add a contact list widget
332
333 will be called on each profile session build
334 @return: a ContactList widget
335 """
336 return NotImplementedError
337
295 def newWidget(self, widget): 338 def newWidget(self, widget):
296 raise NotImplementedError 339 raise NotImplementedError
297 340
298 def connectedHandler(self, profile): 341 def connectedHandler(self, profile):
299 """called when the connection is made""" 342 """called when the connection is made"""
329 372
330 chat_widget.newMessage(from_jid, target, msg, type_, extra, profile) 373 chat_widget.newMessage(from_jid, target, msg, type_, extra, profile)
331 374
332 def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, callback=None, errback=None, profile_key=C.PROF_KEY_NONE): 375 def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, callback=None, errback=None, profile_key=C.PROF_KEY_NONE):
333 if callback is None: 376 if callback is None:
334 callback = lambda: None 377 callback = lambda dummy=None: None # FIXME: optional argument is here because pyjamas doesn't support callback without arg with json proxy
335 if errback is None: 378 if errback is None:
336 errback = lambda failure: self.showDialog(failure.fullname, failure.message, "error") 379 errback = lambda failure: self.showDialog(failure.fullname, failure.message, "error")
337 self.bridge.sendMessage(to_jid, message, subject, mess_type, extra, profile_key, callback=callback, errback=errback) 380 self.bridge.sendMessage(to_jid, message, subject, mess_type, extra, profile_key, callback=callback, errback=errback)
338 381
339 def newAlertHandler(self, msg, title, alert_type, profile): 382 def newAlertHandler(self, msg, title, alert_type, profile):
498 @param from_jid_s: JID of the contact who sent his state, or '@ALL@' 541 @param from_jid_s: JID of the contact who sent his state, or '@ALL@'
499 @param state: new state (string) 542 @param state: new state (string)
500 @profile: current profile 543 @profile: current profile
501 """ 544 """
502 from_jid = jid.JID(from_jid_s) if from_jid_s != C.ENTITY_ALL else C.ENTITY_ALL 545 from_jid = jid.JID(from_jid_s) if from_jid_s != C.ENTITY_ALL else C.ENTITY_ALL
503 for widget in self.visible_widgets: 546 for widget in self.widgets.getWidgets(quick_chat.QuickChat):
504 if isinstance(widget, quick_chat.QuickChat): 547 if from_jid == C.ENTITY_ALL or from_jid.bare == widget.target.bare:
505 if from_jid == C.ENTITY_ALL or from_jid.bare == widget.target.bare: 548 widget.updateChatState(from_jid, state)
506 widget.updateChatState(from_jid, state)
507 549
508 def _subscribe_cb(self, answer, data): 550 def _subscribe_cb(self, answer, data):
509 entity, profile = data 551 entity, profile = data
510 if answer: 552 if answer:
511 self.bridge.subscription("subscribed", entity.bare, profile_key=profile) 553 self.bridge.subscription("subscribed", entity.bare, profile_key=profile)
552 if key == "nick": 594 if key == "nick":
553 if entity in self.contact_lists[profile]: 595 if entity in self.contact_lists[profile]:
554 self.contact_lists[profile].setCache(entity, 'nick', value) 596 self.contact_lists[profile].setCache(entity, 'nick', value)
555 elif key == "avatar": 597 elif key == "avatar":
556 if entity in self.contact_lists[profile]: 598 if entity in self.contact_lists[profile]:
557 filename = self.bridge.getAvatarFile(value) 599 def gotFilename(filename):
558 self.contact_lists[profile].setCache(entity, 'avatar', filename) 600 self.contact_lists[profile].setCache(entity, 'avatar', filename)
601 self.bridge.getAvatarFile(value, callback=gotFilename)
559 602
560 def askConfirmationHandler(self, confirm_id, confirm_type, data, profile): 603 def askConfirmationHandler(self, confirm_id, confirm_type, data, profile):
561 raise NotImplementedError 604 raise NotImplementedError
562 605
563 def actionResultHandler(self, type, id, data, profile): 606 def actionResultHandler(self, type, id, data, profile):