comparison frontends/src/primitivus/chat.py @ 1265:e3a9ea76de35 frontends_multi_profiles

quick_frontend, primitivus: multi-profiles refactoring part 1 (big commit, sorry :p): This refactoring allow primitivus to manage correctly several profiles at once, with various other improvments: - profile_manager can now plug several profiles at once, requesting password when needed. No more profile plug specific method is used anymore in backend, instead a "validated" key is used in actions - Primitivus widget are now based on a common "PrimitivusWidget" classe which mainly manage the decoration so far - all widgets are treated in the same way (contactList, Chat, Progress, etc), no more chat_wins specific behaviour - widgets are created in a dedicated manager, with facilities to react on new widget creation or other events - quick_frontend introduce a new QuickWidget class, which aims to be as generic and flexible as possible. It can manage several targets (jids or something else), and several profiles - each widget class return a Hash according to its target. For example if given a target jid and a profile, a widget class return a hash like (target.bare, profile), the same widget will be used for all resources of the same jid - better management of CHAT_GROUP mode for Chat widgets - some code moved from Primitivus to QuickFrontend, the final goal is to have most non backend code in QuickFrontend, and just graphic code in subclasses - no more (un)escapePrivate/PRIVATE_PREFIX - contactList improved a lot: entities not in roster and special entities (private MUC conversations) are better managed - resources can be displayed in Primitivus, and their status messages - profiles are managed in QuickFrontend with dedicated managers This is work in progress, other frontends are broken. Urwid SàText need to be updated. Most of features of Primitivus should work as before (or in a better way ;))
author Goffi <goffi@goffi.org>
date Wed, 10 Dec 2014 19:00:09 +0100
parents 802b7e6bf098
children faa1129559b8
comparison
equal deleted inserted replaced
1264:60dfa2f5d61f 1265:e3a9ea76de35
21 from sat.core import log as logging 21 from sat.core import log as logging
22 log = logging.getLogger(__name__) 22 log = logging.getLogger(__name__)
23 import urwid 23 import urwid
24 from urwid_satext import sat_widgets 24 from urwid_satext import sat_widgets
25 from urwid_satext.files_management import FileDialog 25 from urwid_satext.files_management import FileDialog
26 from sat_frontends.quick_frontend import quick_widgets
26 from sat_frontends.quick_frontend.quick_chat import QuickChat 27 from sat_frontends.quick_frontend.quick_chat import QuickChat
27 from sat_frontends.primitivus.card_game import CardGame 28 from sat_frontends.primitivus.card_game import CardGame
28 from sat_frontends.quick_frontend.quick_utils import escapePrivate, unescapePrivate
29 from sat_frontends.primitivus.constants import Const as C 29 from sat_frontends.primitivus.constants import Const as C
30 from sat_frontends.primitivus.keys import action_key_map as a_key 30 from sat_frontends.primitivus.keys import action_key_map as a_key
31 from sat_frontends.primitivus.widget import PrimitivusWidget
31 import time 32 import time
32 from sat_frontends.tools.jid import JID 33 from sat_frontends.tools.jid import JID
33 34
34 35
35 class ChatText(urwid.FlowWidget): 36 class ChatText(urwid.FlowWidget):
78 if self.is_info: 79 if self.is_info:
79 return urwid.AttrMap(txt_widget, 'info_msg') 80 return urwid.AttrMap(txt_widget, 'info_msg')
80 return txt_widget 81 return txt_widget
81 82
82 83
83 class Chat(urwid.WidgetWrap, QuickChat): 84 class Chat(PrimitivusWidget, QuickChat):
84 85
85 def __init__(self, target, host, type_='one2one'): 86 def __init__(self, host, target, type_='one2one', profiles=None):
86 self.target = target 87 QuickChat.__init__(self, host, target, type_, profiles=profiles)
87 QuickChat.__init__(self, target, host, type_)
88 self.content = urwid.SimpleListWalker([]) 88 self.content = urwid.SimpleListWalker([])
89 self.text_list = urwid.ListBox(self.content) 89 self.text_list = urwid.ListBox(self.content)
90 self.chat_widget = urwid.Frame(self.text_list) 90 self.chat_widget = urwid.Frame(self.text_list)
91 self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)]) 91 self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)])
92 self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)]) 92 self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)])
93 self.pile = urwid.Pile([self.chat_colums]) 93 self.pile = urwid.Pile([self.chat_colums])
94 urwid.WidgetWrap.__init__(self, self.__getDecoration(self.pile)) 94 PrimitivusWidget.__init__(self, self.pile, self.target)
95 self.setType(type_) 95
96 # we must adapt the behavious with the type
97 if type_ == 'one2one':
98 self.historyPrint(profile=self.profile)
99 elif type_ == 'group':
100 if len(self.chat_colums.contents) == 1:
101 present_widget = self._buildPresentList()
102 self.present_panel = sat_widgets.VerticalSeparator(present_widget)
103 self._appendPresentPanel()
104
96 self.day_change = time.strptime(time.strftime("%a %b %d 00:00:00 %Y")) #struct_time of day changing time 105 self.day_change = time.strptime(time.strftime("%a %b %d 00:00:00 %Y")) #struct_time of day changing time
97 self.show_timestamp = True 106 self.show_timestamp = True
98 self.show_short_nick = False 107 self.show_short_nick = False
99 self.show_title = 1 #0: clip title; 1: full title; 2: no title 108 self.show_title = 1 #0: clip title; 1: full title; 2: no title
100 self.subject = None 109 self.subject = None
102 def keypress(self, size, key): 111 def keypress(self, size, key):
103 if key == a_key['OCCUPANTS_HIDE']: #user wants to (un)hide the presents panel 112 if key == a_key['OCCUPANTS_HIDE']: #user wants to (un)hide the presents panel
104 if self.type == 'group': 113 if self.type == 'group':
105 widgets = [widget for (widget, options) in self.chat_colums.contents] 114 widgets = [widget for (widget, options) in self.chat_colums.contents]
106 if self.present_panel in widgets: 115 if self.present_panel in widgets:
107 self.__removePresentPanel() 116 self._removePresentPanel()
108 else: 117 else:
109 self.__appendPresentPanel() 118 self._appendPresentPanel()
110 elif key == a_key['TIMESTAMP_HIDE']: #user wants to (un)hide timestamp 119 elif key == a_key['TIMESTAMP_HIDE']: #user wants to (un)hide timestamp
111 self.show_timestamp = not self.show_timestamp 120 self.show_timestamp = not self.show_timestamp
112 for wid in self.content: 121 for wid in self.content:
113 wid._invalidate() 122 wid._invalidate()
114 elif key == a_key['SHORT_NICKNAME']: #user wants to (not) use short nick 123 elif key == a_key['SHORT_NICKNAME']: #user wants to (not) use short nick
115 self.show_short_nick = not self.show_short_nick 124 self.show_short_nick = not self.show_short_nick
116 for wid in self.content: 125 for wid in self.content:
117 wid._invalidate() 126 wid._invalidate()
118 elif key == a_key['DECORATION_HIDE']: #user wants to (un)hide widget decoration
119 show = not isinstance(self._w, sat_widgets.LabelLine)
120 self.showDecoration(show)
121 self._invalidate()
122 elif key == a_key['SUBJECT_SWITCH']: #user wants to (un)hide group's subject or change its apperance 127 elif key == a_key['SUBJECT_SWITCH']: #user wants to (un)hide group's subject or change its apperance
123 if self.subject: 128 if self.subject:
124 self.show_title = (self.show_title + 1) % 3 129 self.show_title = (self.show_title + 1) % 3
125 if self.show_title == 0: 130 if self.show_title == 0:
126 self.setSubject(self.subject,'clip') 131 self.setSubject(self.subject,'clip')
128 self.setSubject(self.subject,'space') 133 self.setSubject(self.subject,'space')
129 elif self.show_title == 2: 134 elif self.show_title == 2:
130 self.chat_widget.header = None 135 self.chat_widget.header = None
131 self._invalidate() 136 self._invalidate()
132 137
133
134 return super(Chat, self).keypress(size, key) 138 return super(Chat, self).keypress(size, key)
135 139
136 def getMenu(self): 140 def getMenu(self):
137 """Return Menu bar""" 141 """Return Menu bar"""
138 menu = sat_widgets.Menu(self.host.loop) 142 menu = sat_widgets.Menu(self.host.loop)
139 if self.type == 'group': 143 if self.type == 'group':
140 self.host.addMenus(menu, C.MENU_ROOM, {'room_jid': self.target.bare}) 144 self.host.addMenus(menu, C.MENU_ROOM, {'room_jid': self.target.bare})
141 game = _("Game") 145 game = _("Game")
142 menu.addMenu(game, "Tarot", self.onTarotRequest) 146 menu.addMenu(game, "Tarot", self.onTarotRequest)
143 elif self.type == 'one2one': 147 elif self.type == 'one2one':
144 self.host.addMenus(menu, C.MENU_SINGLE, {'jid': unescapePrivate(self.target)}) 148 self.host.addMenus(menu, C.MENU_SINGLE, {'jid': self.target})
145 menu.addMenu(_("Action"), _("Send file"), self.onSendFileRequest) 149 menu.addMenu(_("Action"), _("Send file"), self.onSendFileRequest)
146 return menu 150 return menu
147 151
148 def setType(self, type_): 152 def updateChatState(self, from_jid, state):
149 QuickChat.setType(self, type_) 153 if self.type == C.CHAT_GROUP:
150 if type_ == 'one2one': 154 if from_jid == C.ENTITY_ALL:
151 self.historyPrint(profile=self.host.profile) 155 occupants = self.occupants
152 elif type_ == 'group': 156 else:
153 if len(self.chat_colums.contents) == 1: 157 nick = from_jid.resource
154 present_widget = self.__buildPresentList() 158 if not nick:
155 self.present_panel = sat_widgets.VerticalSeparator(present_widget) 159 log.debug("no nick found for chatstate")
156 self.__appendPresentPanel() 160 return
157 161 occupants = [nick]
158 def __getDecoration(self, widget):
159 return sat_widgets.LabelLine(widget, self.__getSurrendedText())
160
161 def __getSurrendedText(self):
162 """Get the text to be displayed as the dialog title."""
163 if not hasattr(self, "surrended_text"):
164 self.__setSurrendedText()
165 return self.surrended_text
166
167 def __setSurrendedText(self, state=None):
168 """Set the text to be displayed as the dialog title
169 @param stat: chat state of the contact
170 """
171 text = unicode(unescapePrivate(self.target))
172 if state:
173 text += " (" + state + ")"
174 self.surrended_text = sat_widgets.SurroundedText(text)
175
176 def showDecoration(self, show=True):
177 """Show/Hide the decoration around the chat window"""
178 if show:
179 main_widget = self.__getDecoration(self.pile)
180 else:
181 main_widget = self.pile
182 self._w = main_widget
183
184 def updateChatState(self, state, nick=None):
185 """Set the chat state (XEP-0085) of the contact. Leave nick to None
186 to set the state for a one2one conversation, or give a nickname or
187 C.ALL_OCCUPANTS to set the state of a participant within a MUC.
188 @param state: the new chat state
189 @param nick: None for one2one, the MUC user nick or C.ALL_OCCUPANTS
190 """
191 if nick:
192 assert(self.type == 'group')
193 occupants = self.occupants if nick == C.ALL_OCCUPANTS else [nick]
194 options = self.present_wid.getAllValues() 162 options = self.present_wid.getAllValues()
195 for index in xrange(0, len(options)): 163 for index in xrange(0, len(options)):
196 nick = options[index].value 164 nick = options[index].value
197 if nick in occupants: 165 if nick in occupants:
198 options[index] = (nick, '%s %s' % (C.MUC_USER_STATES[state], nick)) 166 options[index] = (nick, '%s %s' % (C.MUC_USER_STATES[state], nick))
199 self.present_wid.changeValues(options) 167 self.present_wid.changeValues(options)
200 self.host.redraw() 168 self.host.redraw()
201 else: 169 else:
202 assert(self.type == 'one2one') 170 self.title_dynamic = '({})'.format(state)
203 self.__setSurrendedText(state)
204 self.showDecoration()
205 self.host.redraw()
206 171
207 def _presentClicked(self, list_wid, clicked_wid): 172 def _presentClicked(self, list_wid, clicked_wid):
208 assert(self.type == 'group') 173 assert self.type == 'group'
209 nick = clicked_wid.getValue().value 174 nick = clicked_wid.getValue().value
210 if nick == self.getUserNick(): 175 if nick == self.getUserNick():
211 #We ignore click on our own nick 176 #We ignore clicks on our own nick
212 return 177 return
213 #we have a click on a nick, we add the private conversation to the contact_list 178 contact_list = self.host.contact_lists[self.profile]
214 full_jid = JID("%s/%s" % (self.target.bare, nick)) 179 full_jid = JID("%s/%s" % (self.target.bare, nick))
215 new_jid = escapePrivate(full_jid) 180
216 if new_jid not in self.host.contact_list: 181 #we have a click on a nick, we need to create the widget if it doesn't exists
217 self.host.contact_list.add(new_jid, [C.GROUP_NOT_IN_ROSTER]) 182 self.getOrCreatePrivateWidget(full_jid)
218 183
219 #now we select the new window 184 #now we select the new window
220 self.host.contact_list.setFocus(full_jid, True) 185 contact_list.setFocus(full_jid, True)
221 186
222 def __buildPresentList(self): 187 def _buildPresentList(self):
223 self.present_wid = sat_widgets.GenericList([],option_type = sat_widgets.ClickableText, on_click=self._presentClicked) 188 self.present_wid = sat_widgets.GenericList([],option_type = sat_widgets.ClickableText, on_click=self._presentClicked)
224 return self.present_wid 189 return self.present_wid
225 190
226 def __appendPresentPanel(self): 191 def _appendPresentPanel(self):
227 self.chat_colums.contents.append((self.present_panel,('weight', 2, False))) 192 self.chat_colums.contents.append((self.present_panel,('weight', 2, False)))
228 193
229 def __removePresentPanel(self): 194 def _removePresentPanel(self):
230 for widget, options in self.chat_colums.contents: 195 for widget, options in self.chat_colums.contents:
231 if widget is self.present_panel: 196 if widget is self.present_panel:
232 self.chat_colums.contents.remove((widget, options)) 197 self.chat_colums.contents.remove((widget, options))
233 break 198 break
234 199
235 def __appendGamePanel(self, widget): 200 def _appendGamePanel(self, widget):
236 assert (len(self.pile.contents) == 1) 201 assert (len(self.pile.contents) == 1)
237 self.pile.contents.insert(0,(widget,('weight', 1))) 202 self.pile.contents.insert(0,(widget,('weight', 1)))
238 self.pile.contents.insert(1,(urwid.Filler(urwid.Divider('-'),('fixed', 1)))) 203 self.pile.contents.insert(1,(urwid.Filler(urwid.Divider('-'),('fixed', 1))))
239 self.host.redraw() 204 self.host.redraw()
240 205
241 def __removeGamePanel(self): 206 def _removeGamePanel(self):
242 assert (len(self.pile.contents) == 3) 207 assert (len(self.pile.contents) == 3)
243 del self.pile.contents[0] 208 del self.pile.contents[0]
244 self.host.redraw() 209 self.host.redraw()
245 210
246 def setSubject(self, subject, wrap='space'): 211 def setSubject(self, subject, wrap='space'):
288 """Refresh or scroll down the focus after the history is printed""" 253 """Refresh or scroll down the focus after the history is printed"""
289 if len(self.content): 254 if len(self.content):
290 self.text_list.focus_position = len(self.content) - 1 # scroll down 255 self.text_list.focus_position = len(self.content) - 1 # scroll down
291 self.host.redraw() 256 self.host.redraw()
292 257
258 def onPrivateCreated(self, widget):
259 self.host.contact_lists[widget.profile].specialResourceVisible(widget.target)
260
293 def printMessage(self, from_jid, msg, profile, timestamp=None): 261 def printMessage(self, from_jid, msg, profile, timestamp=None):
294 assert isinstance(from_jid, JID) 262 assert isinstance(from_jid, JID)
295 try: 263 try:
296 jid, nick, mymess = QuickChat.printMessage(self, from_jid, msg, profile, timestamp) 264 jid, nick, mymess = QuickChat.printMessage(self, from_jid, msg, profile, timestamp)
297 except TypeError: 265 except TypeError:
266 # None is returned, the message is managed
298 return 267 return
299
300 new_text = ChatText(self, timestamp, nick, mymess, msg) 268 new_text = ChatText(self, timestamp, nick, mymess, msg)
301 269
302 if timestamp and self.content: 270 if timestamp and self.content:
303 for idx in range(len(self.content) - 1, -1, -1): 271 for idx in range(len(self.content) - 1, -1, -1):
304 current_text = self.content[idx] 272 current_text = self.content[idx]
355 323
356 def startGame(self, game_type, referee, players): 324 def startGame(self, game_type, referee, players):
357 """Configure the chat window to start a game""" 325 """Configure the chat window to start a game"""
358 if game_type=="Tarot": 326 if game_type=="Tarot":
359 self.tarot_wid = CardGame(self, referee, players, self.nick) 327 self.tarot_wid = CardGame(self, referee, players, self.nick)
360 self.__appendGamePanel(self.tarot_wid) 328 self._appendGamePanel(self.tarot_wid)
361 329
362 def getGame(self, game_type): 330 def getGame(self, game_type):
363 """Return class managing the game type""" 331 """Return class managing the game type"""
364 #TODO: check that the game is launched, and manage errors 332 #TODO: check that the game is launched, and manage errors
365 if game_type=="Tarot": 333 if game_type=="Tarot":
369 def onTarotRequest(self, menu): 337 def onTarotRequest(self, menu):
370 # TODO: move this to plugin_misc_tarot with dynamic menu 338 # TODO: move this to plugin_misc_tarot with dynamic menu
371 if len(self.occupants) != 4: 339 if len(self.occupants) != 4:
372 self.host.showPopUp(sat_widgets.Alert(_("Can't start game"), _("You need to be exactly 4 peoples in the room to start a Tarot game"), ok_cb=self.host.removePopUp)) 340 self.host.showPopUp(sat_widgets.Alert(_("Can't start game"), _("You need to be exactly 4 peoples in the room to start a Tarot game"), ok_cb=self.host.removePopUp))
373 else: 341 else:
374 self.host.bridge.tarotGameCreate(self.id, list(self.occupants), self.host.profile) 342 self.host.bridge.tarotGameCreate(self.id, list(self.occupants), self.profile)
375 343
376 def onSendFileRequest(self, menu): 344 def onSendFileRequest(self, menu):
377 # TODO: move this to core with dynamic menus 345 # TODO: move this to core with dynamic menus
378 dialog = FileDialog(ok_cb=self.onFileSelected, cancel_cb=self.host.removePopUp) 346 dialog = FileDialog(ok_cb=self.onFileSelected, cancel_cb=self.host.removePopUp)
379 self.host.showPopUp(dialog, 80, 80) 347 self.host.showPopUp(dialog, 80, 80)
386 except UnicodeError: 354 except UnicodeError:
387 log.error("FIXME: filepath with unicode error are not managed yet") 355 log.error("FIXME: filepath with unicode error are not managed yet")
388 self.host.showDialog(_(u"File has a unicode error in its name, it's not yet managed by SàT"), title=_("Can't send file"), type_="error") 356 self.host.showDialog(_(u"File has a unicode error in its name, it's not yet managed by SàT"), title=_("Can't send file"), type_="error")
389 return 357 return
390 #FIXME: check last_resource: what if self.target.resource exists ? 358 #FIXME: check last_resource: what if self.target.resource exists ?
391 last_resource = self.host.bridge.getLastResource(unicode(self.target.bare), self.host.profile) 359 last_resource = self.host.bridge.getLastResource(unicode(self.target.bare), self.profile)
392 if last_resource: 360 if last_resource:
393 full_jid = JID("%s/%s" % (self.target.bare, last_resource)) 361 full_jid = JID("%s/%s" % (self.target.bare, last_resource))
394 else: 362 else:
395 full_jid = self.target 363 full_jid = self.target
396 progress_id = self.host.bridge.sendFile(full_jid, filepath, {}, self.host.profile) 364 progress_id = self.host.bridge.sendFile(full_jid, filepath, {}, self.profile)
397 self.host.addProgress(progress_id,filepath) 365 self.host.addProgress(progress_id,filepath)
398 self.host.showDialog(_(u"You file request has been sent, we are waiting for your contact answer"), title=_("File request sent")) 366 self.host.showDialog(_(u"You file request has been sent, we are waiting for your contact answer"), title=_("File request sent"))
367
368
369 quick_widgets.register(QuickChat, Chat)