Mercurial > libervia-backend
view frontends/src/primitivus/chat.py @ 1967:de6faf9be715 refacoring_messages
primitivus (chat): fixed filters clearing when changing filter while one already exists
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 20 Jun 2016 23:07:53 +0200 |
parents | d727aab9a80e |
children | 200cd707a46d |
line wrap: on
line source
#!/usr/bin/env python2 # -*- coding: utf-8 -*- # Primitivus: a SAT frontend # Copyright (C) 2009-2016 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 sat_frontends.quick_frontend import quick_widgets from sat_frontends.quick_frontend import quick_chat from sat_frontends.quick_frontend import quick_games from sat_frontends.primitivus import game_tarot 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 import jid from functools import total_ordering import bisect class MessageWidget(urwid.WidgetWrap): def __init__(self, mess_data): """ @param mess_data(quick_chat.Message, None): message data None: used only for non text widgets (e.g.: focus separator) """ self.mess_data = mess_data mess_data.widgets.add(self) self.timestamp = time.localtime(mess_data.timestamp) super(MessageWidget, self).__init__(urwid.Text(self.markup)) @property def markup(self): return self._generateInfoMarkup() if self.mess_data.type == C.MESS_TYPE_INFO else self._generateMarkup() @property def info_type(self): return self.mess_data.info_type @property def parent(self): return self.mess_data.parent @property def message(self): """Return currently displayed message""" message = self.mess_data.message if self.parent.lang in message: self.selected_lang = self.parent.lang return message[self.parent.lang] try: self.selected_lang = '' return message[''] except KeyError: try: lang, mess = message.iteritems().next() self.selected_lang = lang return mess except StopIteration: log.error(u"Can't find message for uid {}".format(self.mess_data.uid)) @message.setter def message(self, value): self.mess_data.message = {'':value} self._w.set_text(self.markup) @property def type(self): try: return self.mess_data.type except AttributeError: return C.MESS_TYPE_INFO def selectable(self): return True def keypress(self, size, key): return key def get_cursor_coords(self, size): return 0, 0 def render(self, size, focus=False): # Text widget doesn't render cursor, but we want one # so we add it here canvas = urwid.CompositeCanvas(self._w.render(size, focus)) if focus: canvas.set_cursor(self.get_cursor_coords(size)) return canvas def _generateInfoMarkup(self): return ('info_msg', self.message) def _generateMarkup(self): """Generate text markup according to message data and Widget options""" markup = [] d = self.mess_data # timestamp if self.parent.show_timestamp: # if the message was sent before today, we print the full date time_format = "%c" if self.timestamp < self.parent.day_change else "%H:%M" markup.append(('date', "[{}]".format(time.strftime(time_format, self.timestamp).decode('utf-8')))) # nickname if self.parent.show_short_nick: markup.append(('my_nick' if d.own_mess else 'other_nick', "**" if d.own_mess else "*")) else: markup.append(('my_nick' if d.own_mess else 'other_nick', u"[{}] ".format(d.nick or ''))) msg = self.message # needed to generate self.selected_lang if self.selected_lang: markup.append(("msg_lang", u"[{}] ".format(self.selected_lang))) # message body markup.append(msg) return markup @total_ordering class OccupantWidget(urwid.WidgetWrap): def __init__(self, occupant_data): self.occupant_data = occupant_data occupant_data.widgets.add(self) markup = self._generateMarkup() super(OccupantWidget, self).__init__(urwid.Text(markup)) def __eq__(self, other): return self.occupant_data.nick == other.occupant_data.nick def __lt__(self, other): return self.occupant_data.nick.lower() < other.occupant_data.nick.lower() @property def parent(self): return self.mess_data.parent def selectable(self): return True def keypress(self, size, key): return key def get_cursor_coords(self, size): return 0, 0 def render(self, size, focus=False): # Text widget doesn't render cursor, but we want one # so we add it here canvas = urwid.CompositeCanvas(self._w.render(size, focus)) if focus: canvas.set_cursor(self.get_cursor_coords(size)) return canvas def _generateMarkup(self): # TODO: role and affiliation are shown in a Q&D way # should be more intuitive and themable o = self.occupant_data markup = [] markup.append(('info_msg', '{}{} '.format( o.role[0].upper(), o.affiliation[0].upper(), ))) markup.append(o.nick) return markup class Chat(PrimitivusWidget, quick_chat.QuickChat): def __init__(self, host, target, type_=C.CHAT_ONE2ONE, occupants=None, subject=None, profiles=None): quick_chat.QuickChat.__init__(self, host, target, type_, occupants, subject, profiles=profiles) self.filters = [] # list of filter callbacks to apply self.mess_walker = urwid.SimpleListWalker([]) self.mess_widgets = urwid.ListBox(self.mess_walker) self.chat_widget = urwid.Frame(self.mess_widgets) 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 behaviour with the type if type_ == C.CHAT_GROUP: if len(self.chat_colums.contents) == 1: self.occupants_walker = urwid.SimpleListWalker([]) # TODO: put a real ContactPanel class here, based on FocusWidget ? self.occupants_widgets = urwid.ListBox(self.occupants_walker) # FIXME # , option_type=sat_widgets.ClickableText, on_click=self._occupantsClicked) self.occupants_panel = sat_widgets.VerticalSeparator(self.occupants_widgets) self._appendOccupantsPanel() occupants_list = sorted(self.occupants.keys(), key=lambda o:o.lower()) for occupant in occupants_list: occupant_data = self.occupants[occupant] self.occupants_walker.append(OccupantWidget(occupant_data)) self.host.addListener('presence', self.presenceListener, [profiles]) # focus marker is a separator indicated last visible message before focus was lost self.focus_marker = None # link to current marker self.focus_marker_set = None # True if a new marker has been inserted 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.postInit() def keypress(self, size, key): if key == a_key['OCCUPANTS_HIDE']: # user wants to (un)hide the occupants panel if self.type == C.CHAT_GROUP: widgets = [widget for (widget, options) in self.chat_colums.contents] if self.occupants_panel in widgets: self._removeOccupantsPanel() else: self._appendOccupantsPanel() elif key == a_key['TIMESTAMP_HIDE']: # user wants to (un)hide timestamp self.show_timestamp = not self.show_timestamp for wid in self.mess_walker: 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.mess_walker: 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 == C.CHAT_GROUP: self.host.addMenus(menu, C.MENU_ROOM, {'room_jid': self.target.bare}) game = _("Game") menu.addMenu(game, "Tarot", self.onTarotRequest) elif self.type == C.CHAT_ONE2ONE: # FIXME: self.target is a bare jid, we need to check that contact_list = self.host.contact_lists[self.profile] if not self.target.resource: full_jid = contact_list.getFullJid(self.target) else: full_jid = self.target self.host.addMenus(menu, C.MENU_SINGLE, {'jid': full_jid}) return menu def setFilter(self, args): """set filtering of messages @param args(list[unicode]): filters following syntax "[filter]=[value]" empty list to clear all filters only lang=XX is handled for now """ del self.filters[:] if args: if args[0].startswith("lang="): lang = args[0][5:].strip() self.filters.append(lambda mess_data: lang in mess_data.message) del self.mess_walker[:] for message in self.messages.itervalues(): self.appendMessage(message) def presenceListener(self, entity, show, priority, statuses, profile): """Update entity's presence status @param entity (jid.JID): entity updated @param show: availability @param priority: resource's priority @param statuses: dict of statuses @param profile: %(doc_profile)s """ # FIXME: disable for refactoring, need to be checked and re-enabled return # assert self.type == C.CHAT_GROUP # if entity.bare != self.target: # return # self.update(entity) def createMessage(self, message): self.appendMessage(message) def _user_moved(self, message): """return true if message is a user left/joined message @param message(quick_chat.Message): message to add """ if message.type != C.MESS_TYPE_INFO: return False try: info_type = message.extra['info_type'] except KeyError: return False else: return info_type in quick_chat.ROOM_USER_MOVED def appendMessage(self, message): """Create a MessageWidget and append it Can merge messages together is desirable (e.g.: multiple joined/leave) @param message(quick_chat.Message): message to add """ if self.filters: if not all([f(message) for f in self.filters]): return if self._user_moved(message): for wid in reversed(self.mess_walker): # we merge in/out messages if no message was sent meanwhile if not isinstance(wid, MessageWidget): continue if wid.mess_data.type != C.MESS_TYPE_INFO: break if wid.info_type in quick_chat.ROOM_USER_MOVED and wid.mess_data.nick == message.nick: try: count = wid.reentered_count except AttributeError: count = wid.reentered_count = 1 nick = wid.mess_data.nick if message.info_type == quick_chat.ROOM_USER_LEFT: wid.message = _(u"<= {nick} has left the room ({count})").format(nick=nick, count=count) else: wid.message = _(u"<=> {nick} re-entered the room ({count})") .format(nick=nick, count=count) wid.reentered_count+=1 return if ((self.host.selected_widget != self or not self.host.x_notify.hasFocus()) and self.focus_marker_set is not None): if not self.focus_marker_set and not self._locked and self.mess_walker: if self.focus_marker is not None: self.mess_walker.remove(self.focus_marker) self.focus_marker = urwid.Divider('—') self.mess_walker.append(self.focus_marker) self.focus_marker_set = True else: if self.focus_marker_set: self.focus_marker_set = False if not message.message: log.error(u"Received an empty message for uid {}".format(message.uid)) else: self.mess_walker.append(MessageWidget(message)) self.mess_widgets.focus_position = len(self.mess_walker) - 1 # scroll down self.host.redraw() # FIXME: should not be necessary def addUser(self, nick): occupant = super(Chat, self).addUser(nick) bisect.insort(self.occupants_walker, OccupantWidget(occupant)) def removeUser(self, occupant_data): occupant = super(Chat, self).removeUser(occupant_data) if occupant is not None: for widget in occupant.widgets: self.occupants_walker.remove(widget) def _occupantsClicked(self, list_wid, clicked_wid): # FIXME: not called anymore after refactoring assert self.type == C.CHAT_GROUP nick = clicked_wid.getValue().value if nick == self.nick: # We ignore clicks on our own nick return contact_list = self.host.contact_lists[self.profile] full_jid = 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 _appendOccupantsPanel(self): self.chat_colums.contents.append((self.occupants_panel, ('weight', 2, False))) def _removeOccupantsPanel(self): for widget, options in self.chat_colums.contents: if widget is self.occupants_panel: self.chat_colums.contents.remove((widget, options)) break def addGamePanel(self, widget): """Insert a game panel to this Chat dialog. @param widget (Widget): the game panel """ 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, widget): """Remove the game panel from this Chat dialog. @param widget (Widget): the game panel """ 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""" quick_chat.QuickChat.setSubject(self, 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() ## Messages def updateHistory(self, size=C.HISTORY_LIMIT_DEFAULT, search='', profile='@NONE@'): del self.mess_walker[:] if search: self.mess_walker.append(urwid.Text(_(u"Results for searching the globbing pattern: {}").format(search))) self.mess_walker.append(urwid.Text(_(u"Type ':history <lines>' to reset the chat history").format(search))) super(Chat, self).updateHistory(size, search, profile) def _onHistoryPrinted(self): """Refresh or scroll down the focus after the history is printed""" for message in self.messages.itervalues(): self.appendMessage(message) super(Chat, self)._onHistoryPrinted() def onPrivateCreated(self, widget): self.host.contact_lists[widget.profile].specialResourceVisible(widget.target) def onSelected(self): self.focus_marker_set = False def notify(self, contact="somebody", msg=""): """Notify the user of a new message if primitivus doesn't have the focus. @param contact (unicode): contact who wrote to the users @param msg (unicode): the message that has been received """ # FIXME: not called anymore after refactoring if msg == "": return if self.mess_widgets.get_focus()[1] == len(self.mess_walker) - 2: # we don't change focus if user is not at the bottom # as that mean that he is probably watching discussion history self.mess_widgets.focus_position = len(self.mess_walker) - 1 self.host.redraw() if not self.host.x_notify.hasFocus(): if self.type == C.CHAT_ONE2ONE: self.host.x_notify.sendNotification(_("Primitivus: %s is talking to you") % contact) elif self.nick is not None and self.nick.lower() in msg.lower(): self.host.x_notify.sendNotification(_("Primitivus: %(user)s mentioned you in room '%(room)s'") % {'user': contact, 'room': self.target}) # 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.target, list(self.occupants), self.profile) # MISC EVENTS # def onDelete(self): # FIXME: to be checked after refactoring quick_chat.QuickChat.onDelete(self) if self.type == C.CHAT_GROUP: self.host.removeListener('presence', self.presenceListener) quick_widgets.register(quick_chat.QuickChat, Chat) quick_widgets.register(quick_games.Tarot, game_tarot.TarotGame)