Mercurial > libervia-backend
view 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 |
line wrap: on
line source
#!/usr/bin/python # -*- coding: utf-8 -*- # Primitivus: a SAT frontend # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org) # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. from sat.core.i18n import _ from sat.core import log as logging log = logging.getLogger(__name__) import urwid from urwid_satext import sat_widgets from urwid_satext.files_management import FileDialog from sat_frontends.quick_frontend import quick_widgets from sat_frontends.quick_frontend.quick_chat import QuickChat from sat_frontends.primitivus.card_game import CardGame from sat_frontends.primitivus.constants import Const as C from sat_frontends.primitivus.keys import action_key_map as a_key from sat_frontends.primitivus.widget import PrimitivusWidget import time from sat_frontends.tools.jid import JID class ChatText(urwid.FlowWidget): """Manage the printing of chat message""" def __init__(self, parent, timestamp, nick, my_mess, message, align='left', is_info=False): self.parent = parent self.timestamp = time.localtime(timestamp) self.nick = nick self.my_mess = my_mess self.message = unicode(message) self.align = align self.is_info = is_info def selectable(self): return True def keypress(self, size, key): return key def rows(self,size,focus=False): return self.display_widget(size, focus).rows(size, focus) def render(self, size, focus=False): canvas = urwid.CompositeCanvas(self.display_widget(size, focus).render(size, focus)) if focus: canvas.set_cursor(self.get_cursor_coords(size)) return canvas def get_cursor_coords(self, size): #(maxcol,) = size return 0, 0 def display_widget(self, size, focus): render_txt = [] if not self.is_info: if self.parent.show_timestamp: time_format = "%c" if self.timestamp < self.parent.day_change else "%H:%M" #if the message was sent before today, we print the full date render_txt.append(('date',"[%s]" % time.strftime(time_format, self.timestamp).decode('utf-8'))) if self.parent.show_short_nick: render_txt.append(('my_nick' if self.my_mess else 'other_nick',"**" if self.my_mess else "*")) else: render_txt.append(('my_nick' if self.my_mess else 'other_nick',"[%s] " % self.nick)) render_txt.append(self.message) txt_widget = urwid.Text(render_txt, align=self.align) if self.is_info: return urwid.AttrMap(txt_widget, 'info_msg') return txt_widget class Chat(PrimitivusWidget, QuickChat): def __init__(self, host, target, type_='one2one', profiles=None): QuickChat.__init__(self, host, target, type_, profiles=profiles) self.content = urwid.SimpleListWalker([]) self.text_list = urwid.ListBox(self.content) self.chat_widget = urwid.Frame(self.text_list) self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)]) self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)]) self.pile = urwid.Pile([self.chat_colums]) PrimitivusWidget.__init__(self, self.pile, self.target) # we must adapt the behavious with the type if type_ == 'one2one': self.historyPrint(profile=self.profile) elif type_ == 'group': if len(self.chat_colums.contents) == 1: present_widget = self._buildPresentList() self.present_panel = sat_widgets.VerticalSeparator(present_widget) self._appendPresentPanel() self.day_change = time.strptime(time.strftime("%a %b %d 00:00:00 %Y")) #struct_time of day changing time self.show_timestamp = True self.show_short_nick = False self.show_title = 1 #0: clip title; 1: full title; 2: no title self.subject = None def keypress(self, size, key): if key == a_key['OCCUPANTS_HIDE']: #user wants to (un)hide the presents panel if self.type == 'group': widgets = [widget for (widget, options) in self.chat_colums.contents] if self.present_panel in widgets: self._removePresentPanel() else: self._appendPresentPanel() elif key == a_key['TIMESTAMP_HIDE']: #user wants to (un)hide timestamp self.show_timestamp = not self.show_timestamp for wid in self.content: wid._invalidate() elif key == a_key['SHORT_NICKNAME']: #user wants to (not) use short nick self.show_short_nick = not self.show_short_nick for wid in self.content: wid._invalidate() elif key == a_key['SUBJECT_SWITCH']: #user wants to (un)hide group's subject or change its apperance if self.subject: self.show_title = (self.show_title + 1) % 3 if self.show_title == 0: self.setSubject(self.subject,'clip') elif self.show_title == 1: self.setSubject(self.subject,'space') elif self.show_title == 2: self.chat_widget.header = None self._invalidate() return super(Chat, self).keypress(size, key) def getMenu(self): """Return Menu bar""" menu = sat_widgets.Menu(self.host.loop) if self.type == 'group': self.host.addMenus(menu, C.MENU_ROOM, {'room_jid': self.target.bare}) game = _("Game") menu.addMenu(game, "Tarot", self.onTarotRequest) elif self.type == 'one2one': self.host.addMenus(menu, C.MENU_SINGLE, {'jid': self.target}) menu.addMenu(_("Action"), _("Send file"), self.onSendFileRequest) return menu def updateChatState(self, from_jid, state): if self.type == C.CHAT_GROUP: if from_jid == C.ENTITY_ALL: occupants = self.occupants else: nick = from_jid.resource if not nick: log.debug("no nick found for chatstate") return occupants = [nick] options = self.present_wid.getAllValues() for index in xrange(0, len(options)): nick = options[index].value if nick in occupants: options[index] = (nick, '%s %s' % (C.MUC_USER_STATES[state], nick)) self.present_wid.changeValues(options) self.host.redraw() else: self.title_dynamic = '({})'.format(state) def _presentClicked(self, list_wid, clicked_wid): assert self.type == 'group' nick = clicked_wid.getValue().value if nick == self.getUserNick(): #We ignore clicks on our own nick return contact_list = self.host.contact_lists[self.profile] full_jid = JID("%s/%s" % (self.target.bare, nick)) #we have a click on a nick, we need to create the widget if it doesn't exists self.getOrCreatePrivateWidget(full_jid) #now we select the new window contact_list.setFocus(full_jid, True) def _buildPresentList(self): self.present_wid = sat_widgets.GenericList([],option_type = sat_widgets.ClickableText, on_click=self._presentClicked) return self.present_wid def _appendPresentPanel(self): self.chat_colums.contents.append((self.present_panel,('weight', 2, False))) def _removePresentPanel(self): for widget, options in self.chat_colums.contents: if widget is self.present_panel: self.chat_colums.contents.remove((widget, options)) break def _appendGamePanel(self, widget): assert (len(self.pile.contents) == 1) self.pile.contents.insert(0,(widget,('weight', 1))) self.pile.contents.insert(1,(urwid.Filler(urwid.Divider('-'),('fixed', 1)))) self.host.redraw() def _removeGamePanel(self): assert (len(self.pile.contents) == 3) del self.pile.contents[0] self.host.redraw() def setSubject(self, subject, wrap='space'): """Set title for a group chat""" QuickChat.setSubject(self, subject) self.subject = subject self.subj_wid = urwid.Text(unicode(subject.replace('\n','|') if wrap == 'clip' else subject ), align='left' if wrap=='clip' else 'center',wrap=wrap) self.chat_widget.header = urwid.AttrMap(self.subj_wid,'title') self.host.redraw() def setPresents(self, param_nicks): """Set the users presents in the contact list for a group chat @param nicks: list of nicknames """ nicks = [unicode(nick) for nick in param_nicks] #FIXME: should be done in DBus bridge nicks.sort() QuickChat.setPresents(self, nicks) self.present_wid.changeValues(nicks) self.host.redraw() def replaceUser(self, param_nick, show_info=True): """Add user if it is not in the group list""" nick = unicode(param_nick) #FIXME: should be done in DBus bridge QuickChat.replaceUser(self, nick, show_info) presents = self.present_wid.getAllValues() if nick not in [present.value for present in presents]: presents.append(nick) presents.sort(cmp=lambda a, b: cmp(a.value if hasattr(a, 'value') else a, b.value if hasattr(b, 'value') else b)) self.present_wid.changeValues(presents) self.host.redraw() def removeUser(self, param_nick, show_info=True): """Remove a user from the group list""" nick = unicode(param_nick) #FIXME: should be done in DBus bridge QuickChat.removeUser(self, nick, show_info) self.present_wid.deleteValue(nick) self.host.redraw() def clearHistory(self): """Clear the content of this chat.""" del self.content[:] def afterHistoryPrint(self): """Refresh or scroll down the focus after the history is printed""" if len(self.content): self.text_list.focus_position = len(self.content) - 1 # scroll down self.host.redraw() def onPrivateCreated(self, widget): self.host.contact_lists[widget.profile].specialResourceVisible(widget.target) def printMessage(self, from_jid, msg, profile, timestamp=None): assert isinstance(from_jid, JID) try: jid, nick, mymess = QuickChat.printMessage(self, from_jid, msg, profile, timestamp) except TypeError: # None is returned, the message is managed return new_text = ChatText(self, timestamp, nick, mymess, msg) if timestamp and self.content: for idx in range(len(self.content) - 1, -1, -1): current_text = self.content[idx] older = new_text.timestamp < current_text.timestamp if older and idx > 0: continue # the new message is older, we need to insert it upper #we discard double messages, to avoid backlog / history conflict # FIXME: messages that have been sent several times will be displayed only once if ((idx and self.content[idx - 1].message == msg) or (self.content[idx].message == msg) or (idx < len(self.content) - 2 and self.content[idx + 1].message)): return self.content.insert(0 if older else idx + 1, new_text) break else: self.content.append(new_text) if not timestamp: # XXX: do not send notifications for each line of the history being displayed # FIXME: this must be changed in the future if the timestamp is passed with # all messages and not only with the messages coming from the history. self._notify(from_jid, msg) def printInfo(self, msg, type_='normal', timestamp=None): """Print general info @param msg: message to print @type_: one of: normal: general info like "toto has joined the room" me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist" @param timestamp (float): number of seconds since epoch """ _widget = ChatText(self, timestamp, None, False, msg, is_info=True) self.content.append(_widget) self._notify(msg=msg) def _notify(self, from_jid="somebody", msg=""): """Notify the user of a new message if primitivus doesn't have the focus. @param from_jid: contact who wrote to the users @param msg: the message that has been received """ if msg == "": return if self.text_list.get_focus()[1] == len(self.content) - 2: #we don't change focus if user is not at the bottom #as that mean that he is probably watching discussion history self.text_list.focus_position = len(self.content) - 1 self.host.redraw() if not self.host.x_notify.hasFocus(): if self.type == "one2one": self.host.x_notify.sendNotification(_("Primitivus: %s is talking to you") % from_jid) elif self.getUserNick().lower() in msg.lower(): self.host.x_notify.sendNotification(_("Primitivus: %(user)s mentioned you in room '%(room)s'") % {'user': from_jid, 'room': self.target}) def startGame(self, game_type, referee, players): """Configure the chat window to start a game""" if game_type=="Tarot": self.tarot_wid = CardGame(self, referee, players, self.nick) self._appendGamePanel(self.tarot_wid) def getGame(self, game_type): """Return class managing the game type""" #TODO: check that the game is launched, and manage errors if game_type=="Tarot": return self.tarot_wid #MENU EVENTS# def onTarotRequest(self, menu): # TODO: move this to plugin_misc_tarot with dynamic menu if len(self.occupants) != 4: 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)) else: self.host.bridge.tarotGameCreate(self.id, list(self.occupants), self.profile) def onSendFileRequest(self, menu): # TODO: move this to core with dynamic menus dialog = FileDialog(ok_cb=self.onFileSelected, cancel_cb=self.host.removePopUp) self.host.showPopUp(dialog, 80, 80) #MISC EVENTS# def onFileSelected(self, filepath): self.host.removePopUp() try: filepath = filepath.decode('utf-8') # FIXME: correctly manage unicode except UnicodeError: log.error("FIXME: filepath with unicode error are not managed yet") 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") return #FIXME: check last_resource: what if self.target.resource exists ? last_resource = self.host.bridge.getLastResource(unicode(self.target.bare), self.profile) if last_resource: full_jid = JID("%s/%s" % (self.target.bare, last_resource)) else: full_jid = self.target progress_id = self.host.bridge.sendFile(full_jid, filepath, {}, self.profile) self.host.addProgress(progress_id,filepath) self.host.showDialog(_(u"You file request has been sent, we are waiting for your contact answer"), title=_("File request sent")) quick_widgets.register(QuickChat, Chat)