libervia-backend/frontends/src/primitivus/chat.py
souliane 2203e6aa52 quick_frontend, primitivus: fixes a couple of issues:
- chat: notification was raising an error when you just entered the room and self.nick is None
    - quick_widget: deleting a widget was modifying a dict while looping on it + unselect widget when it's being deleted
2015-04-16 17:17:28 +02:00

410 lines
18 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Primitivus: a SAT frontend
# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 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.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
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):
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 or '')))
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_=C.CHAT_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 behaviour with the type
if type_ == C.CHAT_ONE2ONE:
self.historyPrint(profile=self.profile)
elif type_ == C.CHAT_GROUP:
if len(self.chat_colums.contents) == 1:
self.occupants_list = sat_widgets.GenericList([], option_type=sat_widgets.ClickableText, on_click=self._occupantsClicked)
self.occupants_panel = sat_widgets.VerticalSeparator(self.occupants_list)
self._appendOccupantsPanel()
self.host.addListener('presence', self.presenceListener, [profiles])
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 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.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 == 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:
self.host.addMenus(menu, C.MENU_SINGLE, {'jid': self.target})
menu.addMenu(_("Action"), _("Send file"), self.onSendFileRequest)
return menu
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
"""
assert self.type == C.CHAT_GROUP
if entity.bare != self.target:
return
self.update(entity)
def update(self, entity=None):
"""Update one or all entities.
@param entity (jid.JID): entity to update
"""
contact_list = self.host.contact_lists[self.profile]
if self.type == C.CHAT_ONE2ONE: # only update the chat title
states = self.getEntityStates(self.target)
self.title_dynamic = ' '.join([u'({})'.format(state) for state in states.values()])
self.host.redraw()
return
nicks = list(self.occupants)
if entity is None: # rebuild all the occupants list
values = []
nicks.sort()
for nick in nicks:
values.append(self._buildOccupantMarkup(jid.newResource(self.target, nick)))
self.occupants_list.changeValues(values)
else: # add, remove or update only one occupant
nick = entity.resource
show = contact_list.getCache(entity, C.PRESENCE_SHOW)
if show == C.PRESENCE_UNAVAILABLE or show is None:
try:
self.occupants_list.deleteValue(nick)
except ValueError:
pass
else:
values = self.occupants_list.getAllValues()
markup = self._buildOccupantMarkup(entity)
if not values: # room has just been created
values = [markup]
else: # add or update the occupant, keep the list sorted
index = 0
for entry in values:
order = cmp(entry.value if hasattr(entry, 'value') else entry, nick)
if order < 0:
index += 1
continue
if order > 0: # insert the occupant
values.insert(index, markup)
else: # update an existing occupant
values[index] = markup
break
if index == len(values): # add to the end of the list
values.append(markup)
self.occupants_list.changeValues(values)
self.host.redraw()
def _buildOccupantMarkup(self, entity):
"""Return the option attributes for a MUC occupant.
@param nick (unicode): occupant nickname
"""
# TODO: for now it's not a markup but a simple text, the problem is that ListOption is unicode and not urwid.Text
contact_list = self.host.contact_lists[self.profile]
show = contact_list.getCache(entity, C.PRESENCE_SHOW)
states = self.getEntityStates(entity)
nick = entity.resource
show_icon, entity_attr = C.PRESENCE.get(show, (u'', u'default')) # TODO: use entity_attr and return (nick, markup)
text = "%s%s %s" % (u''.join(states.values()), show_icon, nick)
return (nick, text)
def _occupantsClicked(self, list_wid, clicked_wid):
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"""
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 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, extra=None, profile=C.PROF_KEY_NONE):
assert isinstance(from_jid, jid.JID)
if extra is None:
extra = {}
try:
timestamp = float(extra['timestamp'])
except KeyError:
timestamp = None
try:
nick, mymess = QuickChat.printMessage(self, from_jid, msg, extra, profile)
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', extra=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
"""
if extra is None:
extra = {}
try:
timestamp = float(extra['timestamp'])
except KeyError:
timestamp = None
_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 == C.CHAT_ONE2ONE:
self.host.x_notify.sendNotification(_("Primitivus: %s is talking to you") % from_jid)
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': from_jid, '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)
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.getMainResource(unicode(self.target.bare), self.profile)
if last_resource:
full_jid = 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"))
def onDelete(self):
QuickChat.onDelete(self)
if self.type == C.CHAT_GROUP:
self.host.removeListener('presence', self.presenceListener)
quick_widgets.register(QuickChat, Chat)
quick_widgets.register(quick_games.Tarot, game_tarot.TarotGame)