comparison frontends/src/primitivus/chat.py @ 1367:f71a0fc26886

merged branch frontends_multi_profiles
author Goffi <goffi@goffi.org>
date Wed, 18 Mar 2015 10:52:28 +0100
parents d3e9848b9574
children 017270e6eea4
comparison
equal deleted inserted replaced
1295:1e3b1f9ad6e2 1367:f71a0fc26886
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.quick_frontend import quick_games
28 from sat_frontends.quick_frontend.quick_utils import escapePrivate, unescapePrivate 29 from sat_frontends.primitivus import game_tarot
29 from sat_frontends.primitivus.constants import Const as C 30 from sat_frontends.primitivus.constants import Const as C
30 from sat_frontends.primitivus.keys import action_key_map as a_key 31 from sat_frontends.primitivus.keys import action_key_map as a_key
32 from sat_frontends.primitivus.widget import PrimitivusWidget
31 import time 33 import time
32 from sat_frontends.tools.jid import JID 34 from sat_frontends.tools import jid
33 35
34 36
35 class ChatText(urwid.FlowWidget): 37 class ChatText(urwid.FlowWidget):
36 """Manage the printing of chat message""" 38 """Manage the printing of chat message"""
37 39
78 if self.is_info: 80 if self.is_info:
79 return urwid.AttrMap(txt_widget, 'info_msg') 81 return urwid.AttrMap(txt_widget, 'info_msg')
80 return txt_widget 82 return txt_widget
81 83
82 84
83 class Chat(urwid.WidgetWrap, QuickChat): 85 class Chat(PrimitivusWidget, QuickChat):
84 86
85 def __init__(self, target, host, type_='one2one'): 87 def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None):
86 self.target = target 88 QuickChat.__init__(self, host, target, type_, profiles=profiles)
87 QuickChat.__init__(self, target, host, type_)
88 self.content = urwid.SimpleListWalker([]) 89 self.content = urwid.SimpleListWalker([])
89 self.text_list = urwid.ListBox(self.content) 90 self.text_list = urwid.ListBox(self.content)
90 self.chat_widget = urwid.Frame(self.text_list) 91 self.chat_widget = urwid.Frame(self.text_list)
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.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)])
93 self.pile = urwid.Pile([self.chat_colums]) 94 self.pile = urwid.Pile([self.chat_colums])
94 urwid.WidgetWrap.__init__(self, self.__getDecoration(self.pile)) 95 PrimitivusWidget.__init__(self, self.pile, self.target)
95 self.setType(type_) 96
97 # we must adapt the behaviour with the type
98 if type_ == C.CHAT_ONE2ONE:
99 self.historyPrint(profile=self.profile)
100 elif type_ == C.CHAT_GROUP:
101 if len(self.chat_colums.contents) == 1:
102 present_widget = self._buildPresentList()
103 self.present_panel = sat_widgets.VerticalSeparator(present_widget)
104 self._appendPresentPanel()
105
96 self.day_change = time.strptime(time.strftime("%a %b %d 00:00:00 %Y")) #struct_time of day changing time 106 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 107 self.show_timestamp = True
98 self.show_short_nick = False 108 self.show_short_nick = False
99 self.show_title = 1 #0: clip title; 1: full title; 2: no title 109 self.show_title = 1 #0: clip title; 1: full title; 2: no title
100 self.subject = None 110 self.subject = None
101 111
102 def keypress(self, size, key): 112 def keypress(self, size, key):
103 if key == a_key['OCCUPANTS_HIDE']: #user wants to (un)hide the presents panel 113 if key == a_key['OCCUPANTS_HIDE']: #user wants to (un)hide the presents panel
104 if self.type == 'group': 114 if self.type == C.CHAT_GROUP:
105 widgets = [widget for (widget, options) in self.chat_colums.contents] 115 widgets = [widget for (widget, options) in self.chat_colums.contents]
106 if self.present_panel in widgets: 116 if self.present_panel in widgets:
107 self.__removePresentPanel() 117 self._removePresentPanel()
108 else: 118 else:
109 self.__appendPresentPanel() 119 self._appendPresentPanel()
110 elif key == a_key['TIMESTAMP_HIDE']: #user wants to (un)hide timestamp 120 elif key == a_key['TIMESTAMP_HIDE']: #user wants to (un)hide timestamp
111 self.show_timestamp = not self.show_timestamp 121 self.show_timestamp = not self.show_timestamp
112 for wid in self.content: 122 for wid in self.content:
113 wid._invalidate() 123 wid._invalidate()
114 elif key == a_key['SHORT_NICKNAME']: #user wants to (not) use short nick 124 elif key == a_key['SHORT_NICKNAME']: #user wants to (not) use short nick
115 self.show_short_nick = not self.show_short_nick 125 self.show_short_nick = not self.show_short_nick
116 for wid in self.content: 126 for wid in self.content:
117 wid._invalidate() 127 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 128 elif key == a_key['SUBJECT_SWITCH']: #user wants to (un)hide group's subject or change its apperance
123 if self.subject: 129 if self.subject:
124 self.show_title = (self.show_title + 1) % 3 130 self.show_title = (self.show_title + 1) % 3
125 if self.show_title == 0: 131 if self.show_title == 0:
126 self.setSubject(self.subject,'clip') 132 self.setSubject(self.subject,'clip')
128 self.setSubject(self.subject,'space') 134 self.setSubject(self.subject,'space')
129 elif self.show_title == 2: 135 elif self.show_title == 2:
130 self.chat_widget.header = None 136 self.chat_widget.header = None
131 self._invalidate() 137 self._invalidate()
132 138
133
134 return super(Chat, self).keypress(size, key) 139 return super(Chat, self).keypress(size, key)
135 140
136 def getMenu(self): 141 def getMenu(self):
137 """Return Menu bar""" 142 """Return Menu bar"""
138 menu = sat_widgets.Menu(self.host.loop) 143 menu = sat_widgets.Menu(self.host.loop)
139 if self.type == 'group': 144 if self.type == C.CHAT_GROUP:
140 self.host.addMenus(menu, C.MENU_ROOM, {'room_jid': self.target.bare}) 145 self.host.addMenus(menu, C.MENU_ROOM, {'room_jid': self.target.bare})
141 game = _("Game") 146 game = _("Game")
142 menu.addMenu(game, "Tarot", self.onTarotRequest) 147 menu.addMenu(game, "Tarot", self.onTarotRequest)
143 elif self.type == 'one2one': 148 elif self.type == C.CHAT_ONE2ONE:
144 self.host.addMenus(menu, C.MENU_SINGLE, {'jid': unescapePrivate(self.target)}) 149 self.host.addMenus(menu, C.MENU_SINGLE, {'jid': self.target})
145 menu.addMenu(_("Action"), _("Send file"), self.onSendFileRequest) 150 menu.addMenu(_("Action"), _("Send file"), self.onSendFileRequest)
146 return menu 151 return menu
147 152
148 def setType(self, type_): 153 def updateChatState(self, from_jid, state):
149 QuickChat.setType(self, type_) 154 if self.type == C.CHAT_GROUP:
150 if type_ == 'one2one': 155 if from_jid == C.ENTITY_ALL:
151 self.historyPrint(profile=self.host.profile) 156 occupants = self.occupants
152 elif type_ == 'group': 157 else:
153 if len(self.chat_colums.contents) == 1: 158 nick = from_jid.resource
154 present_widget = self.__buildPresentList() 159 if not nick:
155 self.present_panel = sat_widgets.VerticalSeparator(present_widget) 160 log.debug("no nick found for chatstate")
156 self.__appendPresentPanel() 161 return
157 162 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() 163 options = self.present_wid.getAllValues()
195 for index in xrange(0, len(options)): 164 for index in xrange(0, len(options)):
196 nick = options[index].value 165 nick = options[index].value
197 if nick in occupants: 166 if nick in occupants:
198 options[index] = (nick, '%s %s' % (C.MUC_USER_STATES[state], nick)) 167 options[index] = (nick, '%s %s' % (C.MUC_USER_STATES[state], nick))
199 self.present_wid.changeValues(options) 168 self.present_wid.changeValues(options)
200 self.host.redraw() 169 self.host.redraw()
201 else: 170 else:
202 assert(self.type == 'one2one') 171 self.title_dynamic = '({})'.format(state)
203 self.__setSurrendedText(state)
204 self.showDecoration()
205 self.host.redraw()
206 172
207 def _presentClicked(self, list_wid, clicked_wid): 173 def _presentClicked(self, list_wid, clicked_wid):
208 assert(self.type == 'group') 174 assert self.type == C.CHAT_GROUP
209 nick = clicked_wid.getValue().value 175 nick = clicked_wid.getValue().value
210 if nick == self.getUserNick(): 176 if nick == self.getUserNick():
211 #We ignore click on our own nick 177 #We ignore clicks on our own nick
212 return 178 return
213 #we have a click on a nick, we add the private conversation to the contact_list 179 contact_list = self.host.contact_lists[self.profile]
214 full_jid = JID("%s/%s" % (self.target.bare, nick)) 180 full_jid = jid.JID("%s/%s" % (self.target.bare, nick))
215 new_jid = escapePrivate(full_jid) 181
216 if new_jid not in self.host.contact_list: 182 #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]) 183 self.getOrCreatePrivateWidget(full_jid)
218 184
219 #now we select the new window 185 #now we select the new window
220 self.host.contact_list.setFocus(full_jid, True) 186 contact_list.setFocus(full_jid, True)
221 187
222 def __buildPresentList(self): 188 def _buildPresentList(self):
223 self.present_wid = sat_widgets.GenericList([],option_type = sat_widgets.ClickableText, on_click=self._presentClicked) 189 self.present_wid = sat_widgets.GenericList([],option_type = sat_widgets.ClickableText, on_click=self._presentClicked)
224 return self.present_wid 190 return self.present_wid
225 191
226 def __appendPresentPanel(self): 192 def _appendPresentPanel(self):
227 self.chat_colums.contents.append((self.present_panel,('weight', 2, False))) 193 self.chat_colums.contents.append((self.present_panel,('weight', 2, False)))
228 194
229 def __removePresentPanel(self): 195 def _removePresentPanel(self):
230 for widget, options in self.chat_colums.contents: 196 for widget, options in self.chat_colums.contents:
231 if widget is self.present_panel: 197 if widget is self.present_panel:
232 self.chat_colums.contents.remove((widget, options)) 198 self.chat_colums.contents.remove((widget, options))
233 break 199 break
234 200
235 def __appendGamePanel(self, widget): 201 def addGamePanel(self, widget):
202 """Insert a game panel to this Chat dialog.
203
204 @param widget (Widget): the game panel
205 """
236 assert (len(self.pile.contents) == 1) 206 assert (len(self.pile.contents) == 1)
237 self.pile.contents.insert(0,(widget,('weight', 1))) 207 self.pile.contents.insert(0, (widget, ('weight', 1)))
238 self.pile.contents.insert(1,(urwid.Filler(urwid.Divider('-'),('fixed', 1)))) 208 self.pile.contents.insert(1, (urwid.Filler(urwid.Divider('-'), ('fixed', 1))))
239 self.host.redraw() 209 self.host.redraw()
240 210
241 def __removeGamePanel(self): 211 def removeGamePanel(self, widget):
212 """Remove the game panel from this Chat dialog.
213
214 @param widget (Widget): the game panel
215 """
242 assert (len(self.pile.contents) == 3) 216 assert (len(self.pile.contents) == 3)
243 del self.pile.contents[0] 217 del self.pile.contents[0]
244 self.host.redraw() 218 self.host.redraw()
245 219
246 def setSubject(self, subject, wrap='space'): 220 def setSubject(self, subject, wrap='space'):
250 self.subj_wid = urwid.Text(unicode(subject.replace('\n','|') if wrap == 'clip' else subject ), 224 self.subj_wid = urwid.Text(unicode(subject.replace('\n','|') if wrap == 'clip' else subject ),
251 align='left' if wrap=='clip' else 'center',wrap=wrap) 225 align='left' if wrap=='clip' else 'center',wrap=wrap)
252 self.chat_widget.header = urwid.AttrMap(self.subj_wid,'title') 226 self.chat_widget.header = urwid.AttrMap(self.subj_wid,'title')
253 self.host.redraw() 227 self.host.redraw()
254 228
255 def setPresents(self, param_nicks): 229 def setPresents(self, nicks):
256 """Set the users presents in the contact list for a group chat 230 """Set the occupants of a group chat.
257 @param nicks: list of nicknames 231
258 """ 232 @param nicks (list[unicode]): sorted list of nicknames
259 nicks = [unicode(nick) for nick in param_nicks] #FIXME: should be done in DBus bridge 233 """
260 nicks.sort()
261 QuickChat.setPresents(self, nicks) 234 QuickChat.setPresents(self, nicks)
262 self.present_wid.changeValues(nicks) 235 self.present_wid.changeValues(nicks)
263 self.host.redraw() 236 self.host.redraw()
264 237
265 def replaceUser(self, param_nick, show_info=True): 238 def replaceUser(self, param_nick, show_info=True):
288 """Refresh or scroll down the focus after the history is printed""" 261 """Refresh or scroll down the focus after the history is printed"""
289 if len(self.content): 262 if len(self.content):
290 self.text_list.focus_position = len(self.content) - 1 # scroll down 263 self.text_list.focus_position = len(self.content) - 1 # scroll down
291 self.host.redraw() 264 self.host.redraw()
292 265
293 def printMessage(self, from_jid, msg, profile, timestamp=None): 266 def onPrivateCreated(self, widget):
294 assert isinstance(from_jid, JID) 267 self.host.contact_lists[widget.profile].specialResourceVisible(widget.target)
268
269 def printMessage(self, from_jid, msg, extra=None, profile=C.PROF_KEY_NONE):
270 assert isinstance(from_jid, jid.JID)
271 if extra is None:
272 extra = {}
295 try: 273 try:
296 jid, nick, mymess = QuickChat.printMessage(self, from_jid, msg, profile, timestamp) 274 timestamp = float(extra['timestamp'])
275 except KeyError:
276 timestamp=None
277 try:
278 nick, mymess = QuickChat.printMessage(self, from_jid, msg, extra, profile)
297 except TypeError: 279 except TypeError:
280 # None is returned, the message is managed
298 return 281 return
299
300 new_text = ChatText(self, timestamp, nick, mymess, msg) 282 new_text = ChatText(self, timestamp, nick, mymess, msg)
301 283
302 if timestamp and self.content: 284 if timestamp and self.content:
303 for idx in range(len(self.content) - 1, -1, -1): 285 for idx in range(len(self.content) - 1, -1, -1):
304 current_text = self.content[idx] 286 current_text = self.content[idx]
321 # XXX: do not send notifications for each line of the history being displayed 303 # XXX: do not send notifications for each line of the history being displayed
322 # FIXME: this must be changed in the future if the timestamp is passed with 304 # FIXME: this must be changed in the future if the timestamp is passed with
323 # all messages and not only with the messages coming from the history. 305 # all messages and not only with the messages coming from the history.
324 self._notify(from_jid, msg) 306 self._notify(from_jid, msg)
325 307
326 def printInfo(self, msg, type_='normal', timestamp=None): 308 def printInfo(self, msg, type_='normal', extra=None):
327 """Print general info 309 """Print general info
328 @param msg: message to print 310 @param msg: message to print
329 @type_: one of: 311 @type_: one of:
330 normal: general info like "toto has joined the room" 312 normal: general info like "toto has joined the room"
331 me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist" 313 me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist"
332 @param timestamp (float): number of seconds since epoch 314 @param timestamp (float): number of seconds since epoch
333 """ 315 """
316 if extra is None:
317 extra = {}
318 try:
319 timestamp = float(extra['timestamp'])
320 except KeyError:
321 timestamp=None
334 _widget = ChatText(self, timestamp, None, False, msg, is_info=True) 322 _widget = ChatText(self, timestamp, None, False, msg, is_info=True)
335 self.content.append(_widget) 323 self.content.append(_widget)
336 self._notify(msg=msg) 324 self._notify(msg=msg)
337 325
338 def _notify(self, from_jid="somebody", msg=""): 326 def _notify(self, from_jid="somebody", msg=""):
346 #we don't change focus if user is not at the bottom 334 #we don't change focus if user is not at the bottom
347 #as that mean that he is probably watching discussion history 335 #as that mean that he is probably watching discussion history
348 self.text_list.focus_position = len(self.content) - 1 336 self.text_list.focus_position = len(self.content) - 1
349 self.host.redraw() 337 self.host.redraw()
350 if not self.host.x_notify.hasFocus(): 338 if not self.host.x_notify.hasFocus():
351 if self.type == "one2one": 339 if self.type == C.CHAT_ONE2ONE:
352 self.host.x_notify.sendNotification(_("Primitivus: %s is talking to you") % from_jid) 340 self.host.x_notify.sendNotification(_("Primitivus: %s is talking to you") % from_jid)
353 elif self.getUserNick().lower() in msg.lower(): 341 elif self.getUserNick().lower() in msg.lower():
354 self.host.x_notify.sendNotification(_("Primitivus: %(user)s mentioned you in room '%(room)s'") % {'user': from_jid, 'room': self.target}) 342 self.host.x_notify.sendNotification(_("Primitivus: %(user)s mentioned you in room '%(room)s'") % {'user': from_jid, 'room': self.target})
355
356 def startGame(self, game_type, referee, players):
357 """Configure the chat window to start a game"""
358 if game_type=="Tarot":
359 self.tarot_wid = CardGame(self, referee, players, self.nick)
360 self.__appendGamePanel(self.tarot_wid)
361
362 def getGame(self, game_type):
363 """Return class managing the game type"""
364 #TODO: check that the game is launched, and manage errors
365 if game_type=="Tarot":
366 return self.tarot_wid
367 343
368 #MENU EVENTS# 344 #MENU EVENTS#
369 def onTarotRequest(self, menu): 345 def onTarotRequest(self, menu):
370 # TODO: move this to plugin_misc_tarot with dynamic menu 346 # TODO: move this to plugin_misc_tarot with dynamic menu
371 if len(self.occupants) != 4: 347 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)) 348 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: 349 else:
374 self.host.bridge.tarotGameCreate(self.id, list(self.occupants), self.host.profile) 350 self.host.bridge.tarotGameCreate(self.id, list(self.occupants), self.profile)
375 351
376 def onSendFileRequest(self, menu): 352 def onSendFileRequest(self, menu):
377 # TODO: move this to core with dynamic menus 353 # TODO: move this to core with dynamic menus
378 dialog = FileDialog(ok_cb=self.onFileSelected, cancel_cb=self.host.removePopUp) 354 dialog = FileDialog(ok_cb=self.onFileSelected, cancel_cb=self.host.removePopUp)
379 self.host.showPopUp(dialog, 80, 80) 355 self.host.showPopUp(dialog, 80, 80)
386 except UnicodeError: 362 except UnicodeError:
387 log.error("FIXME: filepath with unicode error are not managed yet") 363 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") 364 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 365 return
390 #FIXME: check last_resource: what if self.target.resource exists ? 366 #FIXME: check last_resource: what if self.target.resource exists ?
391 last_resource = self.host.bridge.getLastResource(unicode(self.target.bare), self.host.profile) 367 last_resource = self.host.bridge.getMainResource(unicode(self.target.bare), self.profile)
392 if last_resource: 368 if last_resource:
393 full_jid = JID("%s/%s" % (self.target.bare, last_resource)) 369 full_jid = jid.JID("%s/%s" % (self.target.bare, last_resource))
394 else: 370 else:
395 full_jid = self.target 371 full_jid = self.target
396 progress_id = self.host.bridge.sendFile(full_jid, filepath, {}, self.host.profile) 372 progress_id = self.host.bridge.sendFile(full_jid, filepath, {}, self.profile)
397 self.host.addProgress(progress_id,filepath) 373 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")) 374 self.host.showDialog(_(u"You file request has been sent, we are waiting for your contact answer"), title=_("File request sent"))
375
376
377 quick_widgets.register(QuickChat, Chat)
378 quick_widgets.register(quick_games.Tarot, game_tarot.TarotGame)