gajim-plural/src/chat_control.py

2855 lines
97 KiB
Python
Raw Normal View History

# -*- coding:utf-8 -*-
## src/chat_control.py
2005-12-29 04:21:43 +01:00
##
## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
## Copyright (C) 2006-2008 Yann Leboulanger <asterix AT lagaule.org>
## Jean-Marie Traissard <jim AT lapin.org>
## Nikos Kouremenos <kourem AT gmail.com>
2008-08-22 05:15:54 +02:00
## Travis Shirk <travis AT pobox.com>
## Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
## Julien Pivotto <roidelapluie AT gmail.com>
## Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
## Stephan Erb <steve-e AT h3c.de>
## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
2005-12-29 04:21:43 +01:00
##
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
2005-12-29 04:21:43 +01:00
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 3 only.
2005-12-29 04:21:43 +01:00
##
## Gajim is distributed in the hope that it will be useful,
2005-12-29 04:21:43 +01:00
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2005-12-29 04:21:43 +01:00
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
##
2005-12-29 04:21:43 +01:00
2006-01-12 00:17:16 +01:00
import os
2006-01-01 20:40:05 +01:00
import time
2005-12-29 04:21:43 +01:00
import gtk
import pango
import gobject
import gtkgui_helpers
import gui_menu_builder
import message_control
2006-01-01 20:40:05 +01:00
import dialogs
import history_window
import notify
import re
2005-12-29 04:21:43 +01:00
from common import gajim
2005-12-31 08:19:43 +01:00
from common import helpers
from common import exceptions
from message_control import MessageControl
2005-12-29 04:21:43 +01:00
from conversation_textview import ConversationTextview
from message_textview import MessageTextView
from common.contacts import GC_Contact
2008-12-02 16:10:31 +01:00
from common.logger import constants
2008-07-29 00:33:20 +02:00
from common.pep import MOODS, ACTIVITIES
from common.xmpp.protocol import NS_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC
from common.xmpp.protocol import NS_RECEIPTS, NS_ESESSION
from common.xmpp.protocol import NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO, NS_JINGLE_ICE_UDP
2005-12-29 04:21:43 +01:00
from command_system.implementation.middleware import ChatCommandProcessor
from command_system.implementation.middleware import CommandTools
from command_system.implementation.hosts import ChatCommands
# Here we load the module with the standard commands, so they are being detected
# and dispatched.
import command_system.implementation.standard
2005-12-31 08:19:43 +01:00
try:
import gtkspell
HAS_GTK_SPELL = True
except ImportError:
2005-12-31 08:19:43 +01:00
HAS_GTK_SPELL = False
# the next script, executed in the "po" directory,
# generates the following list.
##!/bin/sh
2007-10-07 22:36:43 +02:00
#LANG=$(for i in *.po; do j=${i/.po/}; echo -n "_('"$j"')":" '"$j"', " ; done)
#echo "{_('en'):'en'",$LANG"}"
2007-02-12 19:33:33 +01:00
langs = {_('English'): 'en', _('Belarusian'): 'be', _('Bulgarian'): 'bg', _('Breton'): 'br', _('Czech'): 'cs', _('German'): 'de', _('Greek'): 'el', _('British'): 'en_GB', _('Esperanto'): 'eo', _('Spanish'): 'es', _('Basque'): 'eu', _('French'): 'fr', _('Croatian'): 'hr', _('Italian'): 'it', _('Norwegian (b)'): 'nb', _('Dutch'): 'nl', _('Norwegian'): 'no', _('Polish'): 'pl', _('Portuguese'): 'pt', _('Brazilian Portuguese'): 'pt_BR', _('Russian'): 'ru', _('Serbian'): 'sr', _('Slovak'): 'sk', _('Swedish'): 'sv', _('Chinese (Ch)'): 'zh_CN'}
if gajim.config.get('use_speller') and HAS_GTK_SPELL:
# loop removing non-existent dictionaries
# iterating on a copy
tv = gtk.TextView()
spell = gtkspell.Spell(tv)
for lang in dict(langs):
try:
spell.set_language(langs[lang])
except OSError:
del langs[lang]
if spell:
spell.detach()
del tv
################################################################################
class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
2009-11-25 14:01:40 +01:00
"""
A base class containing a banner, ConversationTextview, MessageTextView
"""
def make_href(self, match):
url_color = gajim.config.get('urlmsgcolor')
url = match.group()
if not '://' in url:
url = 'http://' + url
return '<a href="%s"><span color="%s">%s</span></a>' % (url,
url_color, match.group())
2006-03-20 17:22:34 +01:00
def get_font_attrs(self):
2009-11-25 14:01:40 +01:00
"""
Get pango font attributes for banner from theme settings
"""
2006-03-20 17:22:34 +01:00
theme = gajim.config.get('roster_theme')
bannerfont = gajim.config.get_per('themes', theme, 'bannerfont')
bannerfontattrs = gajim.config.get_per('themes', theme, 'bannerfontattrs')
2006-03-20 17:22:34 +01:00
if bannerfont:
font = pango.FontDescription(bannerfont)
else:
font = pango.FontDescription('Normal')
if bannerfontattrs:
# B attribute is set by default
if 'B' in bannerfontattrs:
font.set_weight(pango.WEIGHT_HEAVY)
if 'I' in bannerfontattrs:
font.set_style(pango.STYLE_ITALIC)
2006-03-20 17:22:34 +01:00
font_attrs = 'font_desc="%s"' % font.to_string()
2006-03-20 17:22:34 +01:00
# in case there is no font specified we use x-large font size
if font.get_size() == 0:
font_attrs = '%s size="x-large"' % font_attrs
font.set_weight(pango.WEIGHT_NORMAL)
font_attrs_small = 'font_desc="%s" size="small"' % font.to_string()
2006-03-20 17:22:34 +01:00
return (font_attrs, font_attrs_small)
def get_nb_unread(self):
jid = self.contact.jid
if self.resource:
jid += '/' + self.resource
type_ = self.type_id
return len(gajim.events.get_events(self.account, jid, ['printed_' + type_,
type_]))
def draw_banner(self):
2009-11-25 14:01:40 +01:00
"""
Draw the fat line at the top of the window that houses the icon, jid, etc
Derived types MAY implement this.
"""
self.draw_banner_text()
self._update_banner_state_image()
def draw_banner_text(self):
2009-11-25 14:01:40 +01:00
"""
Derived types SHOULD implement this
"""
pass
def update_ui(self):
2009-11-25 14:01:40 +01:00
"""
Derived types SHOULD implement this
"""
self.draw_banner()
def repaint_themed_widgets(self):
2009-11-25 14:01:40 +01:00
"""
Derived types MAY implement this
"""
self._paint_banner()
self.draw_banner()
def _update_banner_state_image(self):
2009-11-25 14:01:40 +01:00
"""
Derived types MAY implement this
"""
pass
def handle_message_textview_mykey_press(self, widget, event_keyval,
2009-11-25 14:01:40 +01:00
event_keymod):
"""
Derives types SHOULD implement this, rather than connection to the even
itself
"""
event = gtk.gdk.Event(gtk.gdk.KEY_PRESS)
event.keyval = event_keyval
event.state = event_keymod
event.time = 0
_buffer = widget.get_buffer()
start, end = _buffer.get_bounds()
if event.keyval -- gtk.keysyms.Tab:
position = _buffer.get_insert()
end = _buffer.get_iter_at_mark(position)
text = _buffer.get_text(start, end, False)
text = text.decode('utf8')
splitted = text.split()
if (text.startswith(self.COMMAND_PREFIX) and not
text.startswith(self.COMMAND_PREFIX * 2) and len(splitted) == 1):
text = splitted[0]
bare = text.lstrip(self.COMMAND_PREFIX)
if len(text) == 1:
self.command_hits = []
for command in self.list_commands():
for name in command.names:
self.command_hits.append(name)
else:
if (self.last_key_tabs and self.command_hits and
self.command_hits[0].startswith(bare)):
self.command_hits.append(self.command_hits.pop(0))
else:
self.command_hits = []
for command in self.list_commands():
for name in command.names:
if name.startswith(bare):
self.command_hits.append(name)
if self.command_hits:
_buffer.delete(start, end)
_buffer.insert_at_cursor(self.COMMAND_PREFIX + self.command_hits[0] + ' ')
self.last_key_tabs = True
return True
self.last_key_tabs = False
2006-01-07 23:53:46 +01:00
def status_url_clicked(self, widget, url):
helpers.launch_browser_mailer('url', url)
def __init__(self, type_id, parent_win, widget_name, contact, acct,
resource=None):
# Undo needs this variable to know if space has been pressed.
# Initialize it to True so empty textview is saved in undo list
self.space_pressed = True
if resource is None:
# We very likely got a contact with a random resource.
# This is bad, we need the highest for caps etc.
2008-07-23 20:34:26 +02:00
c = gajim.contacts.get_contact_with_highest_priority(
acct, contact.jid)
if c and not isinstance(c, GC_Contact):
2008-07-23 20:34:26 +02:00
contact = c
MessageControl.__init__(self, type_id, parent_win, widget_name,
contact, acct, resource=resource)
widget = self.xml.get_object('history_button')
2008-12-03 18:16:04 +01:00
id_ = widget.connect('clicked', self._on_history_menuitem_activate)
self.handlers[id_] = widget
# when/if we do XHTML we will put formatting buttons back
widget = self.xml.get_object('emoticons_button')
2008-12-03 18:16:04 +01:00
id_ = widget.connect('clicked', self.on_emoticons_button_clicked)
self.handlers[id_] = widget
# Create banner and connect signals
widget = self.xml.get_object('banner_eventbox')
2008-12-03 18:16:04 +01:00
id_ = widget.connect('button-press-event',
self._on_banner_eventbox_button_press_event)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = widget
self.urlfinder = re.compile(
r"(www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s<>'\"]+[^!,\.\s<>\)'\"\]]")
self.banner_status_label = self.xml.get_object('banner_label')
id_ = self.banner_status_label.connect('populate_popup',
self.on_banner_label_populate_popup)
self.handlers[id_] = self.banner_status_label
# Init DND
self.TARGET_TYPE_URI_LIST = 80
self.dnd_list = [ ( 'text/uri-list', 0, self.TARGET_TYPE_URI_LIST ),
('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_APP, 0)]
2008-12-03 18:16:04 +01:00
id_ = self.widget.connect('drag_data_received',
self._on_drag_data_received)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = self.widget
self.widget.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
gtk.DEST_DEFAULT_HIGHLIGHT |
gtk.DEST_DEFAULT_DROP,
self.dnd_list, gtk.gdk.ACTION_COPY)
# Create textviews and connect signals
2006-01-08 05:31:02 +01:00
self.conv_textview = ConversationTextview(self.account)
id_ = self.conv_textview.connect('quote', self.on_quote)
self.handlers[id_] = self.conv_textview.tv
2008-12-03 18:16:04 +01:00
id_ = self.conv_textview.tv.connect('key_press_event',
self._conv_textview_key_press_event)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = self.conv_textview.tv
# FIXME: DND on non editable TextView, find a better way
self.drag_entered = False
2008-12-03 18:16:04 +01:00
id_ = self.conv_textview.tv.connect('drag_data_received',
self._on_drag_data_received)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = self.conv_textview.tv
id_ = self.conv_textview.tv.connect('drag_motion', self._on_drag_motion)
self.handlers[id_] = self.conv_textview.tv
id_ = self.conv_textview.tv.connect('drag_leave', self._on_drag_leave)
self.handlers[id_] = self.conv_textview.tv
self.conv_textview.tv.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
gtk.DEST_DEFAULT_HIGHLIGHT |
gtk.DEST_DEFAULT_DROP,
self.dnd_list, gtk.gdk.ACTION_COPY)
self.conv_scrolledwindow = self.xml.get_object(
2006-01-25 15:05:31 +01:00
'conversation_scrolledwindow')
self.conv_scrolledwindow.add(self.conv_textview.tv)
widget = self.conv_scrolledwindow.get_vadjustment()
2008-12-03 18:16:04 +01:00
id_ = widget.connect('value-changed',
2006-01-02 10:04:30 +01:00
self.on_conversation_vadjustment_value_changed)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = widget
id_ = widget.connect('changed',
self.on_conversation_vadjustment_changed)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = widget
self.scroll_to_end_id = None
self.was_at_the_end = True
# add MessageTextView to UI and connect signals
self.msg_scrolledwindow = self.xml.get_object('message_scrolledwindow')
self.msg_textview = MessageTextView()
2008-12-03 18:16:04 +01:00
id_ = self.msg_textview.connect('mykeypress',
self._on_message_textview_mykeypress_event)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = self.msg_textview
2006-01-02 10:04:30 +01:00
self.msg_scrolledwindow.add(self.msg_textview)
2008-12-03 18:16:04 +01:00
id_ = self.msg_textview.connect('key_press_event',
self._on_message_textview_key_press_event)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = self.msg_textview
id_ = self.msg_textview.connect('size-request', self.size_request)
self.handlers[id_] = self.msg_textview
id_ = self.msg_textview.connect('populate_popup',
self.on_msg_textview_populate_popup)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = self.msg_textview
# Setup DND
2008-12-03 18:16:04 +01:00
id_ = self.msg_textview.connect('drag_data_received',
self._on_drag_data_received)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = self.msg_textview
self.msg_textview.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
gtk.DEST_DEFAULT_HIGHLIGHT,
self.dnd_list, gtk.gdk.ACTION_COPY)
2006-01-02 10:04:30 +01:00
self.update_font()
2006-01-03 05:05:28 +01:00
# Hook up send button
widget = self.xml.get_object('send_button')
2008-12-03 18:16:04 +01:00
id_ = widget.connect('clicked', self._on_send_button_clicked)
self.handlers[id_] = widget
widget = self.xml.get_object('formattings_button')
2008-12-03 18:16:04 +01:00
id_ = widget.connect('clicked', self.on_formattings_button_clicked)
self.handlers[id_] = widget
2008-11-14 12:13:15 +01:00
2005-12-31 04:53:48 +01:00
# the following vars are used to keep history of user's messages
self.sent_history = []
self.sent_history_pos = 0
self.orig_msg = None
2005-12-31 04:53:48 +01:00
# Emoticons menu
# set image no matter if user wants at this time emoticons or not
# (so toggle works ok)
img = self.xml.get_object('emoticons_button_image')
img.set_from_file(os.path.join(gajim.DATA_DIR, 'emoticons', 'static',
'smile.png'))
self.toggle_emoticons()
2005-12-31 08:19:43 +01:00
# Attach speller
if gajim.config.get('use_speller') and HAS_GTK_SPELL:
self.set_speller()
self.conv_textview.tv.show()
self._paint_banner()
2008-02-06 15:59:28 +01:00
# For XEP-0172
self.user_nick = None
self.smooth = True
self.msg_textview.grab_focus()
self.command_hits = []
self.last_key_tabs = False
def set_speller(self):
# now set the one the user selected
per_type = 'contacts'
if self.type_id == message_control.TYPE_GC:
per_type = 'rooms'
lang = gajim.config.get_per(per_type, self.contact.jid,
'speller_language')
if not lang:
# use the default one
lang = gajim.config.get('speller_language')
if not lang:
lang = gajim.LANG
if lang:
try:
gtkspell.Spell(self.msg_textview, lang)
self.msg_textview.lang = lang
except (gobject.GError, RuntimeError, TypeError, OSError):
dialogs.AspellDictError(lang)
def on_banner_label_populate_popup(self, label, menu):
2009-11-25 14:01:40 +01:00
"""
Override the default context menu and add our own menutiems
"""
item = gtk.SeparatorMenuItem()
menu.prepend(item)
menu2 = self.prepare_context_menu()
i = 0
for item in menu2:
menu2.remove(item)
menu.prepend(item)
menu.reorder_child(item, i)
i += 1
menu.show_all()
def on_msg_textview_populate_popup(self, textview, menu):
2009-11-25 14:01:40 +01:00
"""
Override the default context menu and we prepend an option to switch
languages
"""
def _on_select_dictionary(widget, lang):
per_type = 'contacts'
if self.type_id == message_control.TYPE_GC:
per_type = 'rooms'
if not gajim.config.get_per(per_type, self.contact.jid):
gajim.config.add_per(per_type, self.contact.jid)
gajim.config.set_per(per_type, self.contact.jid, 'speller_language',
lang)
spell = gtkspell.get_from_text_view(self.msg_textview)
self.msg_textview.lang = lang
spell.set_language(lang)
widget.set_active(True)
item = gtk.ImageMenuItem(gtk.STOCK_UNDO)
menu.prepend(item)
id_ = item.connect('activate', self.msg_textview.undo)
self.handlers[id_] = item
item = gtk.SeparatorMenuItem()
menu.prepend(item)
item = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
menu.prepend(item)
2008-12-03 18:16:04 +01:00
id_ = item.connect('activate', self.msg_textview.clear)
self.handlers[id_] = item
if gajim.config.get('use_speller') and HAS_GTK_SPELL:
item = gtk.MenuItem(_('Spelling language'))
menu.prepend(item)
submenu = gtk.Menu()
item.set_submenu(submenu)
for lang in sorted(langs):
item = gtk.CheckMenuItem(lang)
if langs[lang] == self.msg_textview.lang:
item.set_active(True)
submenu.append(item)
2008-12-03 18:16:04 +01:00
id_ = item.connect('activate', _on_select_dictionary, langs[lang])
self.handlers[id_] = item
menu.show_all()
def on_quote(self, widget, text):
text = '>' + text.replace('\n', '\n>') + '\n'
message_buffer = self.msg_textview.get_buffer()
message_buffer.insert_at_cursor(text)
# moved from ChatControl
def _on_banner_eventbox_button_press_event(self, widget, event):
2009-11-25 14:01:40 +01:00
"""
If right-clicked, show popup
"""
if event.button == 3: # right click
self.parent_win.popup_menu(event)
2006-01-03 05:05:28 +01:00
def _on_send_button_clicked(self, widget):
2009-11-25 14:01:40 +01:00
"""
When send button is pressed: send the current message
"""
if gajim.connections[self.account].connected < 2: # we are not connected
dialogs.ErrorDialog(_('A connection is not available'),
_('Your message can not be sent until you are connected.'))
return
2006-01-03 05:05:28 +01:00
message_buffer = self.msg_textview.get_buffer()
start_iter = message_buffer.get_start_iter()
end_iter = message_buffer.get_end_iter()
message = message_buffer.get_text(start_iter, end_iter, 0).decode('utf-8')
2008-11-14 12:13:15 +01:00
xhtml = self.msg_textview.get_xhtml()
2006-01-03 05:05:28 +01:00
# send the message
2008-11-14 12:13:15 +01:00
self.send_message(message, xhtml=xhtml)
2006-01-03 05:05:28 +01:00
def _paint_banner(self):
2009-11-25 14:01:40 +01:00
"""
Repaint banner with theme color
"""
theme = gajim.config.get('roster_theme')
bgcolor = gajim.config.get_per('themes', theme, 'bannerbgcolor')
textcolor = gajim.config.get_per('themes', theme, 'bannertextcolor')
# the backgrounds are colored by using an eventbox by
# setting the bg color of the eventbox and the fg of the name_label
banner_eventbox = self.xml.get_object('banner_eventbox')
banner_name_label = self.xml.get_object('banner_name_label')
self.disconnect_style_event(banner_name_label)
self.disconnect_style_event(self.banner_status_label)
if bgcolor:
banner_eventbox.modify_bg(gtk.STATE_NORMAL,
gtk.gdk.color_parse(bgcolor))
default_bg = False
else:
default_bg = True
if textcolor:
banner_name_label.modify_fg(gtk.STATE_NORMAL,
gtk.gdk.color_parse(textcolor))
self.banner_status_label.modify_fg(gtk.STATE_NORMAL,
gtk.gdk.color_parse(textcolor))
default_fg = False
else:
default_fg = True
if default_bg or default_fg:
2006-03-28 14:39:47 +02:00
self._on_style_set_event(banner_name_label, None, default_fg,
default_bg)
2009-08-07 23:43:19 +02:00
if self.banner_status_label.flags() & gtk.REALIZED:
# Widget is realized
self._on_style_set_event(self.banner_status_label, None, default_fg,
default_bg)
def disconnect_style_event(self, widget):
# Try to find the event_id
for id_ in self.handlers.keys():
2008-12-03 18:16:04 +01:00
if self.handlers[id_] == widget:
widget.disconnect(id_)
del self.handlers[id_]
break
def connect_style_event(self, widget, set_fg = False, set_bg = False):
self.disconnect_style_event(widget)
2008-12-03 21:45:26 +01:00
id_ = widget.connect('style-set', self._on_style_set_event, set_fg,
set_bg)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = widget
def _on_style_set_event(self, widget, style, *opts):
2009-11-25 14:01:40 +01:00
"""
Set style of widget from style class *.Frame.Eventbox
opts[0] == True -> set fg color
2009-11-25 14:01:40 +01:00
opts[1] == True -> set bg color
"""
banner_eventbox = self.xml.get_object('banner_eventbox')
self.disconnect_style_event(widget)
if opts[1]:
bg_color = widget.style.bg[gtk.STATE_SELECTED]
banner_eventbox.modify_bg(gtk.STATE_NORMAL, bg_color)
if opts[0]:
fg_color = widget.style.fg[gtk.STATE_SELECTED]
widget.modify_fg(gtk.STATE_NORMAL, fg_color)
self.connect_style_event(widget, opts[0], opts[1])
def _conv_textview_key_press_event(self, widget, event):
if (event.state & gtk.gdk.CONTROL_MASK and event.keyval in (gtk.keysyms.c,
2008-12-13 15:32:37 +01:00
gtk.keysyms.Insert)) or (event.state & gtk.gdk.SHIFT_MASK and \
event.keyval in (gtk.keysyms.Page_Down, gtk.keysyms.Page_Up)):
return False
self.parent_win.notebook.emit('key_press_event', event)
2008-12-05 15:12:31 +01:00
return True
def show_emoticons_menu(self):
if not gajim.config.get('emoticons_theme'):
return
def set_emoticons_menu_position(w, msg_tv = self.msg_textview):
window = msg_tv.get_window(gtk.TEXT_WINDOW_WIDGET)
# get the window position
origin = window.get_origin()
size = window.get_size()
buf = msg_tv.get_buffer()
# get the cursor position
cursor = msg_tv.get_iter_location(buf.get_iter_at_mark(
buf.get_insert()))
cursor = msg_tv.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT,
cursor.x, cursor.y)
x = origin[0] + cursor[0]
y = origin[1] + size[1]
2008-12-02 16:53:23 +01:00
menu_height = gajim.interface.emoticons_menu.size_request()[1]
#FIXME: get_line_count is not so good
#get the iter of cursor, then tv.get_line_yrange
# so we know in which y we are typing (not how many lines we have
# then go show just above the current cursor line for up
# or just below the current cursor line for down
#TEST with having 3 lines and writing in the 2nd
if y + menu_height > gtk.gdk.screen_height():
# move menu just above cursor
y -= menu_height + (msg_tv.allocation.height / buf.get_line_count())
#else: # move menu just below cursor
# y -= (msg_tv.allocation.height / buf.get_line_count())
return (x, y, True) # push_in True
gajim.interface.emoticon_menuitem_clicked = self.append_emoticon
gajim.interface.emoticons_menu.popup(None, None,
set_emoticons_menu_position, 1, 0)
def _on_message_textview_key_press_event(self, widget, event):
if event.keyval == gtk.keysyms.space:
self.space_pressed = True
elif (self.space_pressed or self.msg_textview.undo_pressed) and \
event.keyval not in (gtk.keysyms.Control_L, gtk.keysyms.Control_R) and \
not (event.keyval == gtk.keysyms.z and event.state & gtk.gdk.CONTROL_MASK):
# If the space key has been pressed and now it hasnt,
# we save the buffer into the undo list. But be carefull we're not
# pressiong Control again (as in ctrl+z)
_buffer = widget.get_buffer()
start_iter, end_iter = _buffer.get_bounds()
self.msg_textview.save_undo(_buffer.get_text(start_iter, end_iter))
self.space_pressed = False
2008-12-05 15:12:31 +01:00
# Ctrl [+ Shift] + Tab are not forwarded to notebook. We handle it here
if self.widget_name == 'groupchat_control':
if event.keyval not in (gtk.keysyms.ISO_Left_Tab, gtk.keysyms.Tab):
self.last_key_tabs = False
if event.state & gtk.gdk.SHIFT_MASK:
# CTRL + SHIFT + TAB
if event.state & gtk.gdk.CONTROL_MASK and \
event.keyval == gtk.keysyms.ISO_Left_Tab:
self.parent_win.move_to_next_unread_tab(False)
return True
# SHIFT + PAGE_[UP|DOWN]: send to conv_textview
elif event.keyval == gtk.keysyms.Page_Down or \
2006-01-19 02:30:18 +01:00
event.keyval == gtk.keysyms.Page_Up:
self.conv_textview.tv.emit('key_press_event', event)
return True
elif event.state & gtk.gdk.CONTROL_MASK:
if event.keyval == gtk.keysyms.Tab: # CTRL + TAB
self.parent_win.move_to_next_unread_tab(True)
return True
return False
def _on_message_textview_mykeypress_event(self, widget, event_keyval,
event_keymod):
2009-11-25 14:01:40 +01:00
"""
When a key is pressed: if enter is pressed without the shift key, message
(if not empty) is sent and printed in the conversation
"""
# NOTE: handles mykeypress which is custom signal connected to this
# CB in new_tab(). for this singal see message_textview.py
message_textview = widget
message_buffer = message_textview.get_buffer()
start_iter, end_iter = message_buffer.get_bounds()
message = message_buffer.get_text(start_iter, end_iter, False).decode(
'utf-8')
2008-12-03 22:56:12 +01:00
xhtml = self.msg_textview.get_xhtml()
# construct event instance from binding
event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) # it's always a key-press here
event.keyval = event_keyval
event.state = event_keymod
event.time = 0 # assign current time
if event.keyval == gtk.keysyms.Up:
if event.state & gtk.gdk.CONTROL_MASK: # Ctrl+UP
2006-01-02 23:08:50 +01:00
self.sent_messages_scroll('up', widget.get_buffer())
elif event.keyval == gtk.keysyms.Down:
if event.state & gtk.gdk.CONTROL_MASK: # Ctrl+Down
2006-01-02 23:08:50 +01:00
self.sent_messages_scroll('down', widget.get_buffer())
elif event.keyval == gtk.keysyms.Return or \
event.keyval == gtk.keysyms.KP_Enter: # ENTER
# NOTE: SHIFT + ENTER is not needed to be emulated as it is not
# binding at all (textview's default action is newline)
if gajim.config.get('send_on_ctrl_enter'):
# here, we emulate GTK default action on ENTER (add new line)
# normally I would add in keypress but it gets way to complex
# to get instant result on changing this advanced setting
if event.state == 0: # no ctrl, no shift just ENTER add newline
end_iter = message_buffer.get_end_iter()
message_buffer.insert_at_cursor('\n')
send_message = False
elif event.state & gtk.gdk.CONTROL_MASK: # CTRL + ENTER
send_message = True
else: # send on Enter, do newline on Ctrl Enter
if event.state & gtk.gdk.CONTROL_MASK: # Ctrl + ENTER
end_iter = message_buffer.get_end_iter()
message_buffer.insert_at_cursor('\n')
send_message = False
else: # ENTER
send_message = True
if gajim.connections[self.account].connected < 2 and send_message:
# we are not connected
dialogs.ErrorDialog(_('A connection is not available'),
2006-01-07 23:53:46 +01:00
_('Your message can not be sent until you are connected.'))
send_message = False
if send_message:
2008-11-14 12:13:15 +01:00
self.send_message(message, xhtml=xhtml) # send the message
elif event.keyval == gtk.keysyms.z: # CTRL+z
if event.state & gtk.gdk.CONTROL_MASK:
self.msg_textview.undo()
2006-01-07 23:53:46 +01:00
else:
# Give the control itself a chance to process
self.handle_message_textview_mykey_press(widget, event_keyval,
event_keymod)
2005-12-31 04:53:48 +01:00
def _on_drag_data_received(self, widget, context, x, y, selection,
2009-11-25 14:01:40 +01:00
target_type, timestamp):
"""
Derived types SHOULD implement this
"""
pass
def _on_drag_leave(self, widget, context, time):
# FIXME: DND on non editable TextView, find a better way
self.drag_entered = False
self.conv_textview.tv.set_editable(False)
def _on_drag_motion(self, widget, context, x, y, time):
# FIXME: DND on non editable TextView, find a better way
if not self.drag_entered:
# We drag new data over the TextView, make it editable to catch dnd
self.drag_entered_conv = True
self.conv_textview.tv.set_editable(True)
def send_message(self, message, keyID='', type_='chat', chatstate=None,
2009-11-25 14:01:40 +01:00
msg_id=None, composing_xep=None, resource=None, xhtml=None,
callback=None, callback_args=[], process_commands=True):
"""
Send the given message to the active tab. Doesn't return None if error
"""
2005-12-31 04:53:48 +01:00
if not message or message == '\n':
return None
2005-12-31 04:53:48 +01:00
if process_commands and self.process_as_command(message):
return
2009-09-11 03:54:26 +02:00
MessageControl.send_message(self, message, keyID, type_=type_,
chatstate=chatstate, msg_id=msg_id, composing_xep=composing_xep,
resource=resource, user_nick=self.user_nick, xhtml=xhtml,
callback=callback, callback_args=callback_args)
2009-09-11 03:54:26 +02:00
# Record message history
self.save_sent_message(message)
2005-12-31 04:53:48 +01:00
2009-09-11 03:54:26 +02:00
# Be sure to send user nickname only once according to JEP-0172
self.user_nick = None
2005-12-31 04:53:48 +01:00
# Clear msg input
message_buffer = self.msg_textview.get_buffer()
message_buffer.set_text('') # clear message buffer (and tv of course)
def save_sent_message(self, message):
# save the message, so user can scroll though the list with key up/down
2005-12-31 04:53:48 +01:00
size = len(self.sent_history)
# we don't want size of the buffer to grow indefinately
2005-12-31 04:53:48 +01:00
max_size = gajim.config.get('key_up_lines')
if size >= max_size:
for i in xrange(0, size - 1):
2005-12-31 04:53:48 +01:00
self.sent_history[i] = self.sent_history[i + 1]
self.sent_history[max_size - 1] = message
# self.sent_history_pos has changed if we browsed sent_history,
# reset to real value
self.sent_history_pos = max_size
2005-12-31 04:53:48 +01:00
else:
self.sent_history.append(message)
self.sent_history_pos = size + 1
2007-02-09 00:48:52 +01:00
self.orig_msg = None
2005-12-31 04:53:48 +01:00
def print_conversation_line(self, text, kind, name, tim,
2009-11-25 14:01:40 +01:00
other_tags_for_name=[], other_tags_for_time=[],
other_tags_for_text=[], count_as_new=True, subject=None,
old_kind=None, xhtml=None, simple=False, xep0184_id=None,
graphics=True):
"""
Print 'chat' type messages
"""
2005-12-31 04:53:48 +01:00
jid = self.contact.jid
full_jid = self.get_full_jid()
2005-12-31 04:53:48 +01:00
textview = self.conv_textview
end = False
if self.was_at_the_end or kind == 'outgoing':
2005-12-31 04:53:48 +01:00
end = True
textview.print_conversation_line(text, jid, kind, name, tim,
other_tags_for_name, other_tags_for_time, other_tags_for_text,
subject, old_kind, xhtml, simple=simple, graphics=graphics)
2005-12-31 04:53:48 +01:00
if xep0184_id is not None:
textview.show_xep0184_warning(xep0184_id)
2005-12-31 04:53:48 +01:00
if not count_as_new:
return
if kind == 'incoming':
if not self.type_id == message_control.TYPE_GC or \
gajim.config.get('notify_on_all_muc_messages') or \
'marked' in other_tags_for_text:
# it's a normal message, or a muc message with want to be
# notified about if quitting just after
# other_tags_for_text == ['marked'] --> highlighted gc message
gajim.last_message_time[self.account][full_jid] = time.time()
if kind in ('incoming', 'incoming_queue', 'error'):
gc_message = False
2007-10-07 22:36:43 +02:00
if self.type_id == message_control.TYPE_GC:
gc_message = True
2007-05-08 17:38:23 +02:00
if ((self.parent_win and (not self.parent_win.get_active_control() or \
self != self.parent_win.get_active_control() or \
2007-05-08 17:38:23 +02:00
not self.parent_win.is_active() or not end)) or \
(gc_message and \
jid in gajim.interface.minimized_controls[self.account])) and \
kind in ('incoming', 'incoming_queue', 'error'):
2007-05-08 17:38:23 +02:00
# we want to have save this message in events list
# other_tags_for_text == ['marked'] --> highlighted gc message
if gc_message:
if 'marked' in other_tags_for_text:
type_ = 'printed_marked_gc_msg'
else:
type_ = 'printed_gc_msg'
event = 'gc_message_received'
2008-02-09 07:57:07 +01:00
else:
type_ = 'printed_' + self.type_id
event = 'message_received'
show_in_roster = notify.get_show_in_roster(event,
self.account, self.contact, self.session)
show_in_systray = notify.get_show_in_systray(event,
self.account, self.contact, type_)
2008-02-09 07:57:07 +01:00
event = gajim.events.create_event(type_, (self,),
show_in_roster = show_in_roster,
show_in_systray = show_in_systray)
gajim.events.add_event(self.account, full_jid, event)
# We need to redraw contact if we show in roster
if show_in_roster:
gajim.interface.roster.draw_contact(self.contact.jid,
self.account)
if not self.parent_win:
return
if (not self.parent_win.get_active_control() or \
self != self.parent_win.get_active_control() or \
not self.parent_win.is_active() or not end) and \
kind in ('incoming', 'incoming_queue', 'error'):
self.parent_win.redraw_tab(self)
2006-01-22 23:20:00 +01:00
if not self.parent_win.is_active():
self.parent_win.show_title(True, self) # Enabled Urgent hint
else:
self.parent_win.show_title(False, self) # Disabled Urgent hint
def toggle_emoticons(self):
2009-11-25 14:01:40 +01:00
"""
Hide show emoticons_button and make sure emoticons_menu is always there
when needed
"""
emoticons_button = self.xml.get_object('emoticons_button')
if gajim.config.get('emoticons_theme'):
emoticons_button.show()
emoticons_button.set_no_show_all(False)
else:
emoticons_button.hide()
emoticons_button.set_no_show_all(True)
def append_emoticon(self, str_):
2008-12-03 18:16:04 +01:00
buffer_ = self.msg_textview.get_buffer()
if buffer_.get_char_count():
buffer_.insert_at_cursor(' %s ' % str_)
else: # we are the beginning of buffer
2008-12-03 18:16:04 +01:00
buffer_.insert_at_cursor('%s ' % str_)
self.msg_textview.grab_focus()
def on_emoticons_button_clicked(self, widget):
2009-11-25 14:01:40 +01:00
"""
Popup emoticons menu
"""
gajim.interface.emoticon_menuitem_clicked = self.append_emoticon
gajim.interface.popup_emoticons_under_button(widget, self.parent_win)
2008-11-14 12:13:15 +01:00
def on_formattings_button_clicked(self, widget):
2009-11-25 14:01:40 +01:00
"""
Popup formattings menu
"""
2008-11-20 16:47:20 +01:00
menu = gtk.Menu()
menuitems = ((_('Bold'), 'bold'),
(_('Italic'), 'italic'),
(_('Underline'), 'underline'),
(_('Strike'), 'strike'))
2008-11-14 12:13:15 +01:00
active_tags = self.msg_textview.get_active_tags()
for menuitem in menuitems:
item = gtk.CheckMenuItem(menuitem[0])
if menuitem[1] in active_tags:
item.set_active(True)
else:
item.set_active(False)
item.connect('activate', self.msg_textview.set_tag,
menuitem[1])
menu.append(item)
item = gtk.SeparatorMenuItem() # separator
menu.append(item)
item = gtk.ImageMenuItem(_('Color'))
icon = gtk.image_new_from_stock(gtk.STOCK_SELECT_COLOR, gtk.ICON_SIZE_MENU)
item.set_image(icon)
item.connect('activate', self.on_color_menuitem_activale)
menu.append(item)
item = gtk.ImageMenuItem(_('Font'))
icon = gtk.image_new_from_stock(gtk.STOCK_SELECT_FONT, gtk.ICON_SIZE_MENU)
item.set_image(icon)
item.connect('activate', self.on_font_menuitem_activale)
menu.append(item)
item = gtk.SeparatorMenuItem() # separator
menu.append(item)
item = gtk.ImageMenuItem(_('Clear formating'))
icon = gtk.image_new_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_MENU)
item.set_image(icon)
item.connect('activate', self.msg_textview.clear_tags)
menu.append(item)
menu.show_all()
gtkgui_helpers.popup_emoticons_under_button(menu, widget,
self.parent_win)
def on_color_menuitem_activale(self, widget):
color_dialog = gtk.ColorSelectionDialog('Select a color')
2008-12-03 22:56:12 +01:00
color_dialog.connect('response', self.msg_textview.color_set,
2008-11-14 12:13:15 +01:00
color_dialog.colorsel)
color_dialog.show_all()
def on_font_menuitem_activale(self, widget):
font_dialog = gtk.FontSelectionDialog('Select a font')
2008-12-03 22:56:12 +01:00
font_dialog.connect('response', self.msg_textview.font_set,
2008-11-14 12:13:15 +01:00
font_dialog.fontsel)
font_dialog.show_all()
2008-12-03 22:56:12 +01:00
2008-11-14 12:13:15 +01:00
2005-12-31 08:19:43 +01:00
def on_actions_button_clicked(self, widget):
2009-11-25 14:01:40 +01:00
"""
Popup action menu
"""
menu = self.prepare_context_menu(hide_buttonbar_items=True)
2005-12-31 08:19:43 +01:00
menu.show_all()
2006-07-13 15:07:31 +02:00
gtkgui_helpers.popup_emoticons_under_button(menu, widget,
self.parent_win)
2005-12-31 08:19:43 +01:00
def update_font(self):
font = pango.FontDescription(gajim.config.get('conversation_font'))
self.conv_textview.tv.modify_font(font)
self.msg_textview.modify_font(font)
def update_tags(self):
self.conv_textview.update_tags()
2005-12-31 08:19:43 +01:00
def clear(self, tv):
2008-12-03 18:16:04 +01:00
buffer_ = tv.get_buffer()
start, end = buffer_.get_bounds()
buffer_.delete(start, end)
def _on_history_menuitem_activate(self, widget = None, jid = None):
2009-11-25 14:01:40 +01:00
"""
When history menuitem is pressed: call history window
"""
if not jid:
jid = self.contact.jid
if 'logs' in gajim.interface.instances:
gajim.interface.instances['logs'].window.present()
gajim.interface.instances['logs'].open_history(jid, self.account)
else:
gajim.interface.instances['logs'] = \
history_window.HistoryWindow(jid, self.account)
def _on_send_file(self, gc_contact=None):
2009-11-25 14:01:40 +01:00
"""
gc_contact can be set when we are in a groupchat control
"""
def _on_ok(c):
gajim.interface.instances['file_transfers'].show_file_send_request(
self.account, c)
if self.TYPE_ID == message_control.TYPE_PM:
gc_contact = self.gc_contact
if gc_contact:
# gc or pm
gc_control = gajim.interface.msg_win_mgr.get_gc_control(
gc_contact.room_jid, self.account)
self_contact = gajim.contacts.get_gc_contact(self.account,
gc_control.room_jid, gc_control.nick)
if gc_control.is_anonymous and gc_contact.affiliation not in ['admin',
'owner'] and self_contact.affiliation in ['admin', 'owner']:
contact = gajim.contacts.get_contact(self.account, gc_contact.jid)
if not contact or contact.sub not in ('both', 'to'):
prim_text = _('Really send file?')
sec_text = _('If you send a file to %s, he/she will know your '
'real Jabber ID.') % gc_contact.name
dialog = dialogs.NonModalConfirmationDialog(prim_text, sec_text,
on_response_ok = (_on_ok, gc_contact))
dialog.popup()
return
_on_ok(gc_contact)
return
_on_ok(self.contact)
def on_minimize_menuitem_toggled(self, widget):
2009-11-25 14:01:40 +01:00
"""
When a grouchat is minimized, unparent the tab, put it in roster etc
"""
old_value = False
minimized_gc = gajim.config.get_per('accounts', self.account,
'minimized_gc').split()
if self.contact.jid in minimized_gc:
old_value = True
minimize = widget.get_active()
if minimize and not self.contact.jid in minimized_gc:
minimized_gc.append(self.contact.jid)
if not minimize and self.contact.jid in minimized_gc:
minimized_gc.remove(self.contact.jid)
if old_value != minimize:
gajim.config.set_per('accounts', self.account, 'minimized_gc',
' '.join(minimized_gc))
2006-01-02 10:04:30 +01:00
def set_control_active(self, state):
if state:
jid = self.contact.jid
if self.was_at_the_end:
# we are at the end
type_ = ['printed_' + self.type_id]
if self.type_id == message_control.TYPE_GC:
type_ = ['printed_gc_msg', 'printed_marked_gc_msg']
if not gajim.events.remove_events(self.account, self.get_full_jid(),
types = type_):
# There were events to remove
self.redraw_after_event_removed(jid)
2006-01-02 10:04:30 +01:00
def bring_scroll_to_end(self, textview, diff_y = 0):
2009-11-25 14:01:40 +01:00
"""
Scroll to the end of textview if end is not visible
"""
if self.scroll_to_end_id:
# a scroll is already planned
return
2008-12-03 18:16:04 +01:00
buffer_ = textview.get_buffer()
end_iter = buffer_.get_end_iter()
2006-01-02 10:04:30 +01:00
end_rect = textview.get_iter_location(end_iter)
visible_rect = textview.get_visible_rect()
# scroll only if expected end is not visible
if end_rect.y >= (visible_rect.y + visible_rect.height + diff_y):
self.scroll_to_end_id = gobject.idle_add(self.scroll_to_end_iter,
textview)
2006-01-02 10:04:30 +01:00
def scroll_to_end_iter(self, textview):
2008-12-03 18:16:04 +01:00
buffer_ = textview.get_buffer()
end_iter = buffer_.get_end_iter()
2006-01-02 10:04:30 +01:00
textview.scroll_to_iter(end_iter, 0, False, 1, 1)
self.scroll_to_end_id = None
2006-01-02 10:04:30 +01:00
return False
2006-04-09 21:16:27 +02:00
def size_request(self, msg_textview , requisition):
2009-11-25 14:01:40 +01:00
"""
When message_textview changes its size: if the new height will enlarge
the window, enable the scrollbar automatic policy. Also enable scrollbar
automatic policy for horizontal scrollbar if message we have in
message_textview is too big
"""
2006-01-02 10:04:30 +01:00
if msg_textview.window is None:
return
min_height = self.conv_scrolledwindow.get_property('height-request')
conversation_height = self.conv_textview.tv.window.get_size()[1]
2006-01-02 10:04:30 +01:00
message_height = msg_textview.window.get_size()[1]
message_width = msg_textview.window.get_size()[0]
# new tab is not exposed yet
if conversation_height < 2:
return
if conversation_height < min_height:
min_height = conversation_height
# we don't want to always resize in height the message_textview
# so we have minimum on conversation_textview's scrolled window
# but we also want to avoid window resizing so if we reach that
2006-01-02 10:04:30 +01:00
# minimum for conversation_textview and maximum for message_textview
# we set to automatic the scrollbar policy
2007-10-07 22:36:43 +02:00
diff_y = message_height - requisition.height
2006-01-02 10:04:30 +01:00
if diff_y != 0:
if conversation_height + diff_y < min_height:
if message_height + conversation_height - min_height > min_height:
policy = self.msg_scrolledwindow.get_property(
'vscrollbar-policy')
# scroll only when scrollbar appear
if policy != gtk.POLICY_AUTOMATIC:
self.msg_scrolledwindow.set_property('vscrollbar-policy',
gtk.POLICY_AUTOMATIC)
self.msg_scrolledwindow.set_property('height-request',
message_height + conversation_height - min_height)
self.bring_scroll_to_end(msg_textview)
2006-01-02 10:04:30 +01:00
else:
self.msg_scrolledwindow.set_property('vscrollbar-policy',
2006-01-02 10:04:30 +01:00
gtk.POLICY_NEVER)
self.msg_scrolledwindow.set_property('height-request', -1)
self.conv_textview.bring_scroll_to_end(diff_y - 18, False)
else:
self.conv_textview.bring_scroll_to_end(diff_y - 18, self.smooth)
self.smooth = True # reinit the flag
2006-01-02 10:04:30 +01:00
# enable scrollbar automatic policy for horizontal scrollbar
# if message we have in message_textview is too big
if requisition.width > message_width:
self.msg_scrolledwindow.set_property('hscrollbar-policy',
2006-01-02 10:04:30 +01:00
gtk.POLICY_AUTOMATIC)
else:
self.msg_scrolledwindow.set_property('hscrollbar-policy',
2006-01-02 10:04:30 +01:00
gtk.POLICY_NEVER)
return True
def on_conversation_vadjustment_changed(self, adjustment):
# used to stay at the end of the textview when we shrink conversation
# textview.
if self.was_at_the_end:
self.conv_textview.bring_scroll_to_end(-18)
self.was_at_the_end = (adjustment.upper - adjustment.value - adjustment.page_size) < 18
def on_conversation_vadjustment_value_changed(self, adjustment):
# stop automatic scroll when we manually scroll
if not self.conv_textview.auto_scrolling:
self.conv_textview.stop_scrolling()
self.was_at_the_end = (adjustment.upper - adjustment.value - adjustment.page_size) < 18
if self.resource:
jid = self.contact.get_full_jid()
else:
jid = self.contact.jid
types_list = []
type_ = self.type_id
if type_ == message_control.TYPE_GC:
type_ = 'gc_msg'
types_list = ['printed_' + type_, type_, 'printed_marked_gc_msg']
else: # Not a GC
types_list = ['printed_' + type_, type_]
if not len(gajim.events.get_events(self.account, jid, types_list)):
return
if not self.parent_win:
return
2006-01-27 04:43:00 +01:00
if self.conv_textview.at_the_end() and \
self.parent_win.get_active_control() == self and \
self.parent_win.window.is_active():
# we are at the end
if self.type_id == message_control.TYPE_GC:
if not gajim.events.remove_events(self.account, jid,
types=types_list):
self.redraw_after_event_removed(jid)
elif self.session and self.session.remove_events(types_list):
# There were events to remove
self.redraw_after_event_removed(jid)
def redraw_after_event_removed(self, jid):
2009-11-25 14:01:40 +01:00
"""
We just removed a 'printed_*' event, redraw contact in roster or
gc_roster and titles in roster and msg_win
"""
self.parent_win.redraw_tab(self)
self.parent_win.show_title()
# TODO : get the contact and check notify.get_show_in_roster()
if self.type_id == message_control.TYPE_PM:
room_jid, nick = gajim.get_room_and_nick_from_fjid(jid)
2008-05-02 04:32:28 +02:00
groupchat_control = gajim.interface.msg_win_mgr.get_gc_control(
room_jid, self.account)
if room_jid in gajim.interface.minimized_controls[self.account]:
groupchat_control = \
gajim.interface.minimized_controls[self.account][room_jid]
contact = \
gajim.contacts.get_contact_with_highest_priority(self.account, \
room_jid)
if contact:
gajim.interface.roster.draw_contact(room_jid, self.account)
if groupchat_control:
groupchat_control.draw_contact(nick)
if groupchat_control.parent_win:
groupchat_control.parent_win.redraw_tab(groupchat_control)
else:
gajim.interface.roster.draw_contact(jid, self.account)
gajim.interface.roster.show_title()
2006-01-02 10:04:30 +01:00
2006-01-02 23:08:50 +01:00
def sent_messages_scroll(self, direction, conv_buf):
size = len(self.sent_history)
if self.orig_msg is None:
# user was typing something and then went into history, so save
# whatever is already typed
2006-01-02 23:08:50 +01:00
start_iter = conv_buf.get_start_iter()
end_iter = conv_buf.get_end_iter()
self.orig_msg = conv_buf.get_text(start_iter, end_iter, 0).decode(
'utf-8')
2006-01-02 23:08:50 +01:00
if direction == 'up':
if self.sent_history_pos == 0:
return
self.sent_history_pos = self.sent_history_pos - 1
self.smooth = False
2006-01-02 23:08:50 +01:00
conv_buf.set_text(self.sent_history[self.sent_history_pos])
elif direction == 'down':
if self.sent_history_pos >= size - 1:
2008-12-02 14:58:54 +01:00
conv_buf.set_text(self.orig_msg)
self.orig_msg = None
2006-01-02 23:08:50 +01:00
self.sent_history_pos = size
return
self.sent_history_pos = self.sent_history_pos + 1
self.smooth = False
2006-01-02 23:08:50 +01:00
conv_buf.set_text(self.sent_history[self.sent_history_pos])
2006-01-05 06:51:28 +01:00
def lighten_color(self, color):
p = 0.4
mask = 0
color.red = int((color.red * p) + (mask * (1 - p)))
color.green = int((color.green * p) + (mask * (1 - p)))
color.blue = int((color.blue * p) + (mask * (1 - p)))
return color
def widget_set_visible(self, widget, state):
2009-11-25 14:01:40 +01:00
"""
Show or hide a widget
"""
2006-01-06 07:59:55 +01:00
# make the last message visible, when changing to "full view"
if not state:
gobject.idle_add(self.conv_textview.scroll_to_end_iter)
widget.set_no_show_all(state)
if state:
widget.hide()
2006-01-06 07:59:55 +01:00
else:
widget.show_all()
def chat_buttons_set_visible(self, state):
2009-11-25 14:01:40 +01:00
"""
Toggle chat buttons
"""
MessageControl.chat_buttons_set_visible(self, state)
self.widget_set_visible(self.xml.get_object('actions_hbox'), state)
2006-01-06 07:59:55 +01:00
2006-02-04 03:52:36 +01:00
def got_connected(self):
self.msg_textview.set_sensitive(True)
self.msg_textview.set_editable(True)
2008-11-20 16:44:06 +01:00
# FIXME: Set sensitivity for toolbar
2006-02-04 03:52:36 +01:00
def got_disconnected(self):
self.msg_textview.set_sensitive(False)
self.msg_textview.set_editable(False)
self.conv_textview.tv.grab_focus()
self.no_autonegotiation = False
2008-11-20 16:44:06 +01:00
# FIXME: Set sensitivity for toolbar
2006-02-04 03:52:36 +01:00
################################################################################
class ChatControl(ChatControlBase):
2009-11-25 14:01:40 +01:00
"""
A control for standard 1-1 chat
"""
2009-09-17 14:48:15 +02:00
(
JINGLE_STATE_NOT_AVAILABLE,
JINGLE_STATE_AVAILABLE,
JINGLE_STATE_CONNECTING,
JINGLE_STATE_CONNECTION_RECEIVED,
JINGLE_STATE_CONNECTED,
JINGLE_STATE_ERROR
2009-09-17 14:48:15 +02:00
) = range(6)
TYPE_ID = message_control.TYPE_CHAT
old_msg_kind = None # last kind of the printed message
# Set a command host to bound to. Every command given through a chat will be
# processed with this command host.
COMMAND_HOST = ChatCommands
def __init__(self, parent_win, contact, acct, session, resource = None):
ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
'chat_control', contact, acct, resource)
self.gpg_is_active = False
# for muc use:
# widget = self.xml.get_object('muc_window_actions_button')
self.actions_button = self.xml.get_object('message_window_actions_button')
2008-12-03 18:16:04 +01:00
id_ = self.actions_button.connect('clicked',
self.on_actions_button_clicked)
self.handlers[id_] = self.actions_button
self._formattings_button = self.xml.get_object('formattings_button')
self._add_to_roster_button = self.xml.get_object(
'add_to_roster_button')
2008-12-03 18:16:04 +01:00
id_ = self._add_to_roster_button.connect('clicked',
self._on_add_to_roster_menuitem_activate)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = self._add_to_roster_button
self._audio_button = self.xml.get_object('audio_togglebutton')
id_ = self._audio_button.connect('toggled', self.on_audio_button_toggled)
self.handlers[id_] = self._audio_button
# add a special img
2009-12-01 15:50:58 +01:00
gtkgui_helpers.add_image_to_button(self._audio_button,
'gajim-mic_inactive')
2009-09-17 22:21:06 +02:00
self._video_button = self.xml.get_object('video_togglebutton')
id_ = self._video_button.connect('toggled', self.on_video_button_toggled)
self.handlers[id_] = self._video_button
# add a special img
2009-12-01 15:50:58 +01:00
gtkgui_helpers.add_image_to_button(self._video_button,
'gajim-cam_inactive')
2009-09-17 22:21:06 +02:00
self._send_file_button = self.xml.get_object('send_file_button')
# add a special img for send file button
2009-11-27 16:42:32 +01:00
path_to_upload_img = gtkgui_helpers.get_icon_path('gajim-upload')
img = gtk.Image()
img.set_from_file(path_to_upload_img)
self._send_file_button.set_image(img)
2008-12-03 18:16:04 +01:00
id_ = self._send_file_button.connect('clicked',
self._on_send_file_menuitem_activate)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = self._send_file_button
self._convert_to_gc_button = self.xml.get_object(
'convert_to_gc_button')
2008-12-03 18:16:04 +01:00
id_ = self._convert_to_gc_button.connect('clicked',
self._on_convert_to_gc_menuitem_activate)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = self._convert_to_gc_button
contact_information_button = self.xml.get_object(
'contact_information_button')
2008-12-03 18:16:04 +01:00
id_ = contact_information_button.connect('clicked',
self._on_contact_information_menuitem_activate)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = contact_information_button
compact_view = gajim.config.get('compact_view')
self.chat_buttons_set_visible(compact_view)
self.widget_set_visible(self.xml.get_object('banner_eventbox'),
gajim.config.get('hide_chat_banner'))
self.authentication_button = self.xml.get_object(
'authentication_button')
2008-12-03 18:16:04 +01:00
id_ = self.authentication_button.connect('clicked',
self._on_authentication_button_clicked)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = self.authentication_button
# Add lock image to show chat encryption
self.lock_image = self.xml.get_object('lock_image')
# Convert to GC icon
img = self.xml.get_object('convert_to_gc_button_image')
img.set_from_pixbuf(gtkgui_helpers.load_icon(
'muc_active').get_pixbuf())
self._audio_banner_image = self.xml.get_object('audio_banner_image')
self._video_banner_image = self.xml.get_object('video_banner_image')
2009-09-25 15:47:43 +02:00
self.audio_sid = None
self.audio_state = self.JINGLE_STATE_NOT_AVAILABLE
self.video_sid = None
self.video_state = self.JINGLE_STATE_NOT_AVAILABLE
2009-09-17 22:21:06 +02:00
self.update_toolbar()
self._pep_images = {}
self._pep_images['mood'] = self.xml.get_object('mood_image')
self._pep_images['activity'] = self.xml.get_object('activity_image')
self._pep_images['tune'] = self.xml.get_object('tune_image')
self._pep_images['location'] = self.xml.get_object('location_image')
self.update_all_pep_types()
2008-07-22 23:08:52 +02:00
2006-01-02 23:08:50 +01:00
# keep timeout id and window obj for possible big avatar
# it is on enter-notify and leave-notify so no need to be
# per jid
2006-01-02 23:08:50 +01:00
self.show_bigger_avatar_timeout_id = None
self.bigger_avatar_window = None
self.show_avatar()
2006-01-02 23:08:50 +01:00
# chatstate timers and state
self.reset_kbd_mouse_timeout_vars()
self._schedule_activity_timers()
# Hook up signals
2008-12-03 18:16:04 +01:00
id_ = self.parent_win.window.connect('motion-notify-event',
2006-03-28 14:39:47 +02:00
self._on_window_motion_notify)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = self.parent_win.window
message_tv_buffer = self.msg_textview.get_buffer()
2008-12-03 18:16:04 +01:00
id_ = message_tv_buffer.connect('changed',
self._on_message_tv_buffer_changed)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = message_tv_buffer
widget = self.xml.get_object('avatar_eventbox')
widget.set_property('height-request', gajim.config.get(
'chat_avatar_height'))
2008-12-03 18:16:04 +01:00
id_ = widget.connect('enter-notify-event',
self.on_avatar_eventbox_enter_notify_event)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = widget
2006-03-28 14:39:47 +02:00
2008-12-03 18:16:04 +01:00
id_ = widget.connect('leave-notify-event',
self.on_avatar_eventbox_leave_notify_event)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = widget
2008-12-03 18:16:04 +01:00
id_ = widget.connect('button-press-event',
self.on_avatar_eventbox_button_press_event)
2008-12-03 18:16:04 +01:00
self.handlers[id_] = widget
widget = self.xml.get_object('location_eventbox')
id_ = widget.connect('button-release-event',
self.on_location_eventbox_button_release_event)
self.handlers[id_] = widget
for key in ('1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'):
widget = self.xml.get_object(key + '_button')
id_ = widget.connect('pressed', self.on_num_button_pressed, key)
self.handlers[id_] = widget
id_ = widget.connect('released', self.on_num_button_released)
self.handlers[id_] = widget
widget = self.xml.get_object('mic_hscale')
id_ = widget.connect('value_changed', self.on_mic_hscale_value_changed)
self.handlers[id_] = widget
widget = self.xml.get_object('sound_hscale')
id_ = widget.connect('value_changed', self.on_sound_hscale_value_changed)
self.handlers[id_] = widget
if not session:
# Don't use previous session if we want to a specific resource
# and it's not the same
if not resource:
resource = contact.resource
session = gajim.connections[self.account].find_controlless_session(
self.contact.jid, resource)
if session:
session.control = self
self.session = session
if session.enable_encryption:
self.print_esession_details()
2007-08-20 10:16:48 +02:00
# Enable encryption if needed
self.no_autonegotiation = False
e2e_is_active = self.session and self.session.enable_encryption
gpg_pref = gajim.config.get_per('contacts', contact.jid,
'gpg_enabled')
# try GPG first
if not e2e_is_active and gpg_pref and \
gajim.config.get_per('accounts', self.account, 'keyid') and \
gajim.connections[self.account].USE_GPG:
self.gpg_is_active = True
gajim.encrypted_chats[self.account].append(contact.jid)
msg = _('GPG encryption enabled')
ChatControlBase.print_conversation_line(self, msg,
'status', '', None)
if self.session:
self.session.loggable = gajim.config.get_per('accounts',
self.account, 'log_encrypted_sessions')
# GPG is always authenticated as we use GPG's WoT
self._show_lock_image(self.gpg_is_active, 'GPG', self.gpg_is_active,
self.session and self.session.is_loggable(), True)
self.update_ui()
# restore previous conversation
self.restore_conversation()
self.msg_textview.grab_focus()
def update_toolbar(self):
# Formatting
if self.contact.supports(NS_XHTML_IM) and not self.gpg_is_active:
self._formattings_button.set_sensitive(True)
else:
self._formattings_button.set_sensitive(False)
# Add to roster
if not isinstance(self.contact, GC_Contact) \
and _('Not in Roster') in self.contact.groups:
self._add_to_roster_button.show()
else:
self._add_to_roster_button.hide()
# Jingle detection
if self.contact.supports(NS_JINGLE_ICE_UDP) and \
gajim.HAVE_FARSIGHT and self.contact.resource:
if self.contact.supports(NS_JINGLE_RTP_AUDIO):
if self.audio_state == self.JINGLE_STATE_NOT_AVAILABLE:
self.set_audio_state('available')
else:
self.set_audio_state('not_available')
if self.contact.supports(NS_JINGLE_RTP_VIDEO):
if self.video_state == self.JINGLE_STATE_NOT_AVAILABLE:
self.set_video_state('available')
else:
self.set_video_state('not_available')
else:
if self.audio_state != self.JINGLE_STATE_NOT_AVAILABLE:
self.set_audio_state('not_available')
if self.video_state != self.JINGLE_STATE_NOT_AVAILABLE:
self.set_video_state('not_available')
2009-09-17 22:21:06 +02:00
# Audio buttons
if self.audio_state == self.JINGLE_STATE_NOT_AVAILABLE:
self._audio_button.set_sensitive(False)
else:
self._audio_button.set_sensitive(True)
# Video buttons
if self.video_state == self.JINGLE_STATE_NOT_AVAILABLE:
self._video_button.set_sensitive(False)
2009-09-17 22:21:06 +02:00
else:
self._video_button.set_sensitive(True)
2009-09-17 22:21:06 +02:00
# Send file
if self.contact.supports(NS_FILE) and self.contact.resource:
self._send_file_button.set_sensitive(True)
else:
self._send_file_button.set_sensitive(False)
if not self.contact.supports(NS_FILE):
self._send_file_button.set_tooltip_text(_(
"This contact does not support file transfer."))
else:
self._send_file_button.set_tooltip_text(
_("You need to know the real JID of the contact to send him or "
"her a file."))
# Convert to GC
if self.contact.supports(NS_MUC):
self._convert_to_gc_button.set_sensitive(True)
else:
self._convert_to_gc_button.set_sensitive(False)
def update_all_pep_types(self):
for pep_type in self._pep_images:
self.update_pep(pep_type)
def update_pep(self, pep_type):
2008-07-29 00:33:20 +02:00
if isinstance(self.contact, GC_Contact):
return
if pep_type not in self._pep_images:
return
pep = self.contact.pep
img = self._pep_images[pep_type]
if pep_type in pep:
img.set_from_pixbuf(pep[pep_type].asPixbufIcon())
img.set_tooltip_markup(pep[pep_type].asMarkupText())
img.show()
2008-07-23 01:06:58 +02:00
else:
img.hide()
2008-07-23 01:06:58 +02:00
def _update_jingle(self, jingle_type):
if jingle_type not in ('audio', 'video'):
return
banner_image = getattr(self, '_' + jingle_type + '_banner_image')
state = getattr(self, jingle_type + '_state')
if state in (self.JINGLE_STATE_NOT_AVAILABLE,
self.JINGLE_STATE_AVAILABLE):
banner_image.hide()
else:
banner_image.show()
if state == self.JINGLE_STATE_CONNECTING:
banner_image.set_from_stock(
gtk.STOCK_CONVERT, 1)
elif state == self.JINGLE_STATE_CONNECTION_RECEIVED:
banner_image.set_from_stock(
gtk.STOCK_NETWORK, 1)
elif state == self.JINGLE_STATE_CONNECTED:
banner_image.set_from_stock(
gtk.STOCK_CONNECT, 1)
elif state == self.JINGLE_STATE_ERROR:
banner_image.set_from_stock(
gtk.STOCK_DIALOG_WARNING, 1)
self.update_toolbar()
def update_audio(self):
self._update_jingle('audio')
vbox = self.xml.get_object('audio_vbox')
if self.audio_state == self.JINGLE_STATE_CONNECTED:
# Set volume from config
input_vol = gajim.config.get('audio_input_volume')
output_vol = gajim.config.get('audio_output_volume')
input_vol = max(min(input_vol, 100), 0)
output_vol = max(min(output_vol, 100), 0)
self.xml.get_object('mic_hscale').set_value(input_vol)
self.xml.get_object('sound_hscale').set_value(output_vol)
# Show vbox
vbox.set_no_show_all(False)
vbox.show_all()
elif not self.audio_sid:
vbox.set_no_show_all(True)
vbox.hide()
def update_video(self):
self._update_jingle('video')
2009-09-17 14:48:15 +02:00
def change_resource(self, resource):
old_full_jid = self.get_full_jid()
self.resource = resource
new_full_jid = self.get_full_jid()
# update gajim.last_message_time
if old_full_jid in gajim.last_message_time[self.account]:
gajim.last_message_time[self.account][new_full_jid] = \
gajim.last_message_time[self.account][old_full_jid]
# update events
gajim.events.change_jid(self.account, old_full_jid, new_full_jid)
# update MessageWindow._controls
self.parent_win.change_jid(self.account, old_full_jid, new_full_jid)
def _set_jingle_state(self, jingle_type, state, sid=None, reason=None):
if jingle_type not in ('audio', 'video'):
return
if state in ('connecting', 'connected', 'stop') and reason:
str = _('%(type)s state : %(state)s, reason: %(reason)s') % {
'type': jingle_type.capitalize(), 'state': state, 'reason': reason}
2009-09-17 22:21:06 +02:00
self.print_conversation(str, 'info')
2009-10-16 19:04:04 +02:00
states = {'not_available': self.JINGLE_STATE_NOT_AVAILABLE,
'available': self.JINGLE_STATE_AVAILABLE,
'connecting': self.JINGLE_STATE_CONNECTING,
'connection_received': self.JINGLE_STATE_CONNECTION_RECEIVED,
'connected': self.JINGLE_STATE_CONNECTED,
'stop': self.JINGLE_STATE_AVAILABLE,
'error': self.JINGLE_STATE_ERROR}
if state in states:
2009-10-29 10:08:22 +01:00
jingle_state = states[state]
if getattr(self, jingle_type + '_state') == jingle_state:
2009-10-29 10:08:22 +01:00
return
setattr(self, jingle_type + '_state', jingle_state)
# Destroy existing session with the user when he signs off
# We need to do that before modifying the sid
if state == 'not_available':
gajim.connections[self.account].delete_jingle_session(
self.contact.get_full_jid(), getattr(self, jingle_type + '_sid'))
if state in ('not_available', 'available', 'stop'):
setattr(self, jingle_type + '_sid', None)
if state in ('connection_received', 'connecting'):
setattr(self, jingle_type + '_sid', sid)
2009-10-16 19:04:04 +02:00
if state in ('connecting', 'connected', 'connection_received'):
getattr(self, '_' + jingle_type + '_button').set_active(True)
2009-10-16 19:04:04 +02:00
elif state in ('not_available', 'stop'):
getattr(self, '_' + jingle_type + '_button').set_active(False)
2009-10-16 19:04:04 +02:00
getattr(self, 'update_' + jingle_type)()
2009-09-17 14:48:15 +02:00
def set_audio_state(self, state, sid=None, reason=None):
self._set_jingle_state('audio', state, sid=sid, reason=reason)
2009-10-16 19:04:04 +02:00
def set_video_state(self, state, sid=None, reason=None):
self._set_jingle_state('video', state, sid=sid, reason=reason)
def _get_audio_content(self):
session = gajim.connections[self.account].get_jingle_session(
self.contact.get_full_jid(), self.audio_sid)
return session.get_content('audio')
def on_num_button_pressed(self, widget, num):
self._get_audio_content()._start_dtmf(num)
def on_num_button_released(self, released):
self._get_audio_content()._stop_dtmf()
def on_mic_hscale_value_changed(self, widget):
value = widget.get_value()
self._get_audio_content().set_mic_volume(value / 100)
# Save volume to config
# FIXME: Putting it here is maybe not the right thing to do?
gajim.config.set('audio_input_volume', value)
def on_sound_hscale_value_changed(self, widget):
value = widget.get_value()
self._get_audio_content().set_out_volume(value / 100)
# Save volume to config
# FIXME: Putting it here is maybe not the right thing to do?
gajim.config.set('audio_output_volume', value)
2006-01-05 04:09:54 +01:00
def on_avatar_eventbox_enter_notify_event(self, widget, event):
2009-11-25 14:01:40 +01:00
"""
Enter the eventbox area so we under conditions add a timeout to show a
bigger avatar after 0.5 sec
"""
jid = self.contact.jid
avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid)
if avatar_pixbuf in ('ask', None):
return
avatar_w = avatar_pixbuf.get_width()
avatar_h = avatar_pixbuf.get_height()
scaled_buf = self.xml.get_object('avatar_image').get_pixbuf()
scaled_buf_w = scaled_buf.get_width()
scaled_buf_h = scaled_buf.get_height()
# do we have something bigger to show?
if avatar_w > scaled_buf_w or avatar_h > scaled_buf_h:
# wait for 0.5 sec in case we leave earlier
2008-08-25 22:13:16 +02:00
self.show_bigger_avatar_timeout_id = gobject.timeout_add(500,
self.show_bigger_avatar, widget)
2006-01-05 04:09:54 +01:00
def on_avatar_eventbox_leave_notify_event(self, widget, event):
2009-11-25 14:01:40 +01:00
"""
Left the eventbox area that holds the avatar img
"""
# did we add a timeout? if yes remove it
if self.show_bigger_avatar_timeout_id is not None:
2008-08-25 22:13:16 +02:00
gobject.source_remove(self.show_bigger_avatar_timeout_id)
2006-01-02 23:08:50 +01:00
def on_avatar_eventbox_button_press_event(self, widget, event):
2009-11-25 14:01:40 +01:00
"""
If right-clicked, show popup
"""
if event.button == 3: # right click
menu = gtk.Menu()
menuitem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS)
2008-12-03 18:16:04 +01:00
id_ = menuitem.connect('activate',
gtkgui_helpers.on_avatar_save_as_menuitem_activate,
self.contact.jid, self.account, self.contact.get_shown_name())
2008-12-03 18:16:04 +01:00
self.handlers[id_] = menuitem
menu.append(menuitem)
menu.show_all()
menu.connect('selection-done', lambda w:w.destroy())
# show the menu
menu.show_all()
menu.popup(None, None, None, event.button, event.time)
return True
def on_location_eventbox_button_release_event(self, widget, event):
if 'location' in self.contact.pep:
location = self.contact.pep['location']._pep_specific_data
if ('lat' in location) and ('lon' in location):
uri = 'http://www.openstreetmap.org/?' + \
'mlat=%(lat)s&mlon=%(lon)s&zoom=16' % {'lat': location['lat'],
'lon': location['lon']}
helpers.launch_browser_mailer('url', uri)
2006-01-02 23:08:50 +01:00
def _on_window_motion_notify(self, widget, event):
2009-11-25 14:01:40 +01:00
"""
It gets called no matter if it is the active window or not
"""
2006-01-07 23:53:46 +01:00
if self.parent_win.get_active_jid() == self.contact.jid:
# if window is the active one, change vars assisting chatstate
2006-01-02 23:08:50 +01:00
self.mouse_over_in_last_5_secs = True
self.mouse_over_in_last_30_secs = True
2005-12-31 04:53:48 +01:00
def _schedule_activity_timers(self):
self.possible_paused_timeout_id = gobject.timeout_add_seconds(5,
self.check_for_possible_paused_chatstate, None)
self.possible_inactive_timeout_id = gobject.timeout_add_seconds(30,
self.check_for_possible_inactive_chatstate, None)
2005-12-31 04:53:48 +01:00
def update_ui(self):
# The name banner is drawn here
ChatControlBase.update_ui(self)
2008-07-23 19:40:02 +02:00
self.update_toolbar()
def _update_banner_state_image(self):
contact = gajim.contacts.get_contact_with_highest_priority(self.account,
self.contact.jid)
if not contact or self.resource:
2006-01-09 00:14:50 +01:00
# For transient contacts
contact = self.contact
show = contact.show
jid = contact.jid
# Set banner image
img_32 = gajim.interface.roster.get_appropriate_state_images(jid,
size = '32', icon_name = show)
img_16 = gajim.interface.roster.get_appropriate_state_images(jid,
icon_name = show)
if show in img_32 and img_32[show].get_pixbuf():
# we have 32x32! use it!
banner_image = img_32[show]
use_size_32 = True
else:
banner_image = img_16[show]
use_size_32 = False
banner_status_img = self.xml.get_object('banner_status_image')
if banner_image.get_storage_type() == gtk.IMAGE_ANIMATION:
banner_status_img.set_from_animation(banner_image.get_animation())
else:
pix = banner_image.get_pixbuf()
if pix is not None:
if use_size_32:
banner_status_img.set_from_pixbuf(pix)
else: # we need to scale 16x16 to 32x32
scaled_pix = pix.scale_simple(32, 32,
gtk.gdk.INTERP_BILINEAR)
banner_status_img.set_from_pixbuf(scaled_pix)
def draw_banner_text(self):
2009-11-25 14:01:40 +01:00
"""
Draw the text in the fat line at the top of the window that houses the
name, jid
"""
contact = self.contact
jid = contact.jid
banner_name_label = self.xml.get_object('banner_name_label')
name = contact.get_shown_name()
if self.resource:
name += '/' + self.resource
if self.TYPE_ID == message_control.TYPE_PM:
name = _('%(nickname)s from group chat %(room_name)s') %\
{'nickname': name, 'room_name': self.room_name}
name = gobject.markup_escape_text(name)
2006-02-08 05:11:42 +01:00
# We know our contacts nick, but if another contact has the same nick
# in another account we need to also display the account.
# except if we are talking to two different resources of the same contact
2006-02-08 05:11:42 +01:00
acct_info = ''
for account in gajim.contacts.get_accounts():
if account == self.account:
2006-02-08 05:11:42 +01:00
continue
if acct_info: # We already found a contact with same nick
2006-02-08 05:11:42 +01:00
break
for jid in gajim.contacts.get_jid_list(account):
other_contact_ = \
gajim.contacts.get_first_contact_from_jid(account, jid)
if other_contact_.get_shown_name() == self.contact.get_shown_name():
acct_info = ' (%s)' % \
gobject.markup_escape_text(self.account)
break
status = contact.status
if status is not None:
banner_name_label.set_ellipsize(pango.ELLIPSIZE_END)
self.banner_status_label.set_ellipsize(pango.ELLIPSIZE_END)
status_reduced = helpers.reduce_chars_newlines(status, max_lines = 1)
status_escaped = gobject.markup_escape_text(status_reduced)
2006-03-20 17:22:34 +01:00
font_attrs, font_attrs_small = self.get_font_attrs()
st = gajim.config.get('displayed_chat_state_notifications')
cs = contact.chatstate
if cs and st in ('composing_only', 'all'):
if contact.show == 'offline':
chatstate = ''
elif contact.composing_xep == 'XEP-0085':
if st == 'all' or cs == 'composing':
chatstate = helpers.get_uf_chatstate(cs)
else:
chatstate = ''
elif contact.composing_xep == 'XEP-0022':
if cs in ('composing', 'paused'):
# only print composing, paused
chatstate = helpers.get_uf_chatstate(cs)
else:
chatstate = ''
else:
# When does that happen ? See [7797] and [7804]
chatstate = helpers.get_uf_chatstate(cs)
2008-07-25 00:11:48 +02:00
label_text = '<span %s>%s</span><span %s>%s %s</span>' \
% (font_attrs, name, font_attrs_small,
acct_info, chatstate)
2008-07-29 09:28:07 +02:00
if acct_info:
acct_info = ' ' + acct_info
label_tooltip = '%s%s %s' % (name, acct_info, chatstate)
else:
2006-03-01 12:03:56 +01:00
# weight="heavy" size="x-large"
2006-03-20 17:22:34 +01:00
label_text = '<span %s>%s</span><span %s>%s</span>' % \
(font_attrs, name, font_attrs_small, acct_info)
2008-07-29 09:28:07 +02:00
if acct_info:
acct_info = ' ' + acct_info
label_tooltip = '%s%s' % (name, acct_info)
if status_escaped:
status_text = self.urlfinder.sub(self.make_href, status_escaped)
status_text = '<span %s>%s</span>' % (font_attrs_small, status_escaped)
self.banner_status_label.set_tooltip_text(status)
self.banner_status_label.set_no_show_all(False)
self.banner_status_label.show()
else:
status_text = ''
self.banner_status_label.hide()
self.banner_status_label.set_no_show_all(True)
self.banner_status_label.set_markup(status_text)
# setup the label that holds name and jid
banner_name_label.set_markup(label_text)
banner_name_label.set_tooltip_text(label_tooltip)
def close_jingle_content(self, jingle_type):
sid = getattr(self, jingle_type + '_sid')
if not sid:
return
session = gajim.connections[self.account].get_jingle_session(
self.contact.get_full_jid(), sid)
if session:
content = session.get_content(jingle_type)
if content:
session.remove_content(content.creator, content.name)
def on_jingle_button_toggled(self, widget, jingle_type):
img_name = 'gajim-%s_%s' % ({'audio': 'mic', 'video': 'cam'}[jingle_type],
2009-12-01 15:50:58 +01:00
{True: 'active', False: 'inactive'}[widget.get_active()])
path_to_img = gtkgui_helpers.get_icon_path(img_name)
if widget.get_active():
2009-12-01 15:50:58 +01:00
if getattr(self, jingle_type + '_state') == \
self.JINGLE_STATE_AVAILABLE:
sid = getattr(gajim.connections[self.account],
'start_' + jingle_type)(self.contact.get_full_jid())
getattr(self, 'set_' + jingle_type + '_state')('connecting', sid)
else:
self.close_jingle_content(jingle_type)
img = getattr(self, '_' + jingle_type + '_button').get_property('image')
img.set_from_file(path_to_img)
def on_audio_button_toggled(self, widget):
self.on_jingle_button_toggled(widget, 'audio')
def on_video_button_toggled(self, widget):
self.on_jingle_button_toggled(widget, 'video')
def _toggle_gpg(self):
if not self.gpg_is_active and not self.contact.keyID:
dialogs.ErrorDialog(_('No GPG key assigned'),
_('No GPG key is assigned to this contact. So you cannot '
'encrypt messages with GPG.'))
return
ec = gajim.encrypted_chats[self.account]
if self.gpg_is_active:
# Disable encryption
ec.remove(self.contact.jid)
self.gpg_is_active = False
loggable = False
msg = _('GPG encryption disabled')
2008-07-15 19:04:36 +02:00
ChatControlBase.print_conversation_line(self, msg,
'status', '', None)
if self.session:
self.session.loggable = True
else:
# Enable encryption
ec.append(self.contact.jid)
self.gpg_is_active = True
msg = _('GPG encryption enabled')
2008-07-15 19:04:36 +02:00
ChatControlBase.print_conversation_line(self, msg,
'status', '', None)
loggable = gajim.config.get_per('accounts', self.account,
'log_encrypted_sessions')
if self.session:
self.session.loggable = loggable
loggable = self.session.is_loggable()
else:
loggable = loggable and gajim.config.should_log(self.account,
self.contact.jid)
if loggable:
msg = _('Session WILL be logged')
else:
msg = _('Session WILL NOT be logged')
2008-07-15 19:04:36 +02:00
ChatControlBase.print_conversation_line(self, msg,
'status', '', None)
2008-07-15 19:04:36 +02:00
gajim.config.set_per('contacts', self.contact.jid,
'gpg_enabled', self.gpg_is_active)
2008-07-15 19:04:36 +02:00
self._show_lock_image(self.gpg_is_active, 'GPG',
self.gpg_is_active, loggable, True)
2009-11-25 14:01:40 +01:00
def _show_lock_image(self, visible, enc_type = '', enc_enabled = False,
chat_logged = False, authenticated = False):
"""
Set lock icon visibility and create tooltip
"""
#encryption %s active
status_string = enc_enabled and _('is') or _('is NOT')
#chat session %s be logged
logged_string = chat_logged and _('will') or _('will NOT')
if authenticated:
#About encrypted chat session
authenticated_string = _('and authenticated')
2009-12-01 15:50:58 +01:00
img_path = gtkgui_helpers.get_icon_path('gajim-security_high')
else:
#About encrypted chat session
authenticated_string = _('and NOT authenticated')
2009-12-01 15:50:58 +01:00
img_path = gtkgui_helpers.get_icon_path('gajim-security_low')
self.lock_image.set_from_file(img_path)
#status will become 'is' or 'is not', authentificaed will become
#'and authentificated' or 'and not authentificated', logged will become
#'will' or 'will not'
tooltip = _('%(type)s encryption %(status)s active %(authenticated)s.\n'
'Your chat session %(logged)s be logged.') % {'type': enc_type,
'status': status_string, 'authenticated': authenticated_string,
'logged': logged_string}
self.authentication_button.set_tooltip_text(tooltip)
self.widget_set_visible(self.authentication_button, not visible)
self.lock_image.set_sensitive(enc_enabled)
def _on_authentication_button_clicked(self, widget):
if self.gpg_is_active:
dialogs.GPGInfoWindow(self)
elif self.session and self.session.enable_encryption:
dialogs.ESessionInfoWindow(self.session)
def send_message(self, message, keyID='', chatstate=None, xhtml=None,
process_commands=True):
2009-11-25 14:01:40 +01:00
"""
Send a message to contact
"""
2009-09-11 03:54:26 +02:00
if message in ('', None, '\n'):
return None
2005-12-31 04:53:48 +01:00
2006-01-05 06:51:28 +01:00
# refresh timers
self.reset_kbd_mouse_timeout_vars()
2005-12-31 04:53:48 +01:00
contact = self.contact
2007-08-20 10:16:48 +02:00
encrypted = bool(self.session) and self.session.enable_encryption
2005-12-31 04:53:48 +01:00
keyID = ''
if self.gpg_is_active:
2005-12-31 04:53:48 +01:00
keyID = contact.keyID
encrypted = True
if not keyID:
keyID = 'UNKNOWN'
2005-12-31 04:53:48 +01:00
chatstates_on = gajim.config.get('outgoing_chat_state_notifications') != \
'disabled'
composing_xep = contact.composing_xep
2005-12-31 04:53:48 +01:00
chatstate_to_send = None
if chatstates_on and contact is not None:
if composing_xep is None:
2005-12-31 04:53:48 +01:00
# no info about peer
# send active to discover chat state capabilities
# this is here (and not in send_chatstate)
# because we want it sent with REAL message
# (not standlone) eg. one that has body
2007-06-08 21:42:02 +02:00
if contact.our_chatstate:
# We already asked for xep 85, don't ask it twice
composing_xep = 'asked_once'
2005-12-31 04:53:48 +01:00
chatstate_to_send = 'active'
contact.our_chatstate = 'ask' # pseudo state
# if peer supports jep85 and we are not 'ask', send 'active'
# NOTE: first active and 'ask' is set in gajim.py
elif composing_xep is not False:
# send active chatstate on every message (as XEP says)
2005-12-31 04:53:48 +01:00
chatstate_to_send = 'active'
contact.our_chatstate = 'active'
gobject.source_remove(self.possible_paused_timeout_id)
gobject.source_remove(self.possible_inactive_timeout_id)
self._schedule_activity_timers()
def _on_sent(id_, contact, message, encrypted, xhtml):
if contact.supports(NS_RECEIPTS) and gajim.config.get_per('accounts',
self.account, 'request_receipt'):
2008-12-03 18:16:04 +01:00
xep0184_id = id_
else:
xep0184_id = None
2009-06-26 19:33:46 +02:00
self.print_conversation(message, self.contact.jid, encrypted=encrypted,
xep0184_id=xep0184_id, xhtml=xhtml)
2005-12-31 04:53:48 +01:00
ChatControlBase.send_message(self, message, keyID, type_='chat',
chatstate=chatstate_to_send, composing_xep=composing_xep,
2009-09-11 03:54:26 +02:00
xhtml=xhtml, callback=_on_sent,
callback_args=[contact, message, encrypted, xhtml],
process_commands=process_commands)
2005-12-31 04:53:48 +01:00
def check_for_possible_paused_chatstate(self, arg):
2009-11-25 14:01:40 +01:00
"""
Did we move mouse of that window or write something in message textview
in the last 5 seconds? If yes - we go active for mouse, composing for
kbd. If not - we go paused if we were previously composing
"""
2005-12-31 04:53:48 +01:00
contact = self.contact
jid = contact.jid
current_state = contact.our_chatstate
if current_state is False: # jid doesn't support chatstates
return False # stop looping
message_buffer = self.msg_textview.get_buffer()
if self.kbd_activity_in_last_5_secs and message_buffer.get_char_count():
# Only composing if the keyboard activity was in text entry
self.send_chatstate('composing')
elif self.mouse_over_in_last_5_secs and\
jid == self.parent_win.get_active_jid():
self.send_chatstate('active')
else:
if current_state == 'composing':
self.send_chatstate('paused') # pause composing
# assume no activity and let the motion-notify or 'insert-text' make them
# True refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds!
2005-12-31 04:53:48 +01:00
self.reset_kbd_mouse_timeout_vars()
return True # loop forever
2005-12-31 04:53:48 +01:00
def check_for_possible_inactive_chatstate(self, arg):
2009-11-25 14:01:40 +01:00
"""
Did we move mouse over that window or wrote something in message textview
in the last 30 seconds? if yes - we go active. If no - we go inactive
"""
2005-12-31 04:53:48 +01:00
contact = self.contact
current_state = contact.our_chatstate
if current_state is False: # jid doesn't support chatstates
return False # stop looping
if self.mouse_over_in_last_5_secs or self.kbd_activity_in_last_5_secs:
return True # loop forever
if not self.mouse_over_in_last_30_secs or \
self.kbd_activity_in_last_30_secs:
self.send_chatstate('inactive', contact)
2005-12-31 04:53:48 +01:00
# assume no activity and let the motion-notify or 'insert-text' make them
# True refresh 30 seconds too or else it's 30 - 5 = 25 seconds!
2005-12-31 04:53:48 +01:00
self.reset_kbd_mouse_timeout_vars()
return True # loop forever
def reset_kbd_mouse_timeout_vars(self):
self.kbd_activity_in_last_5_secs = False
self.mouse_over_in_last_5_secs = False
self.mouse_over_in_last_30_secs = False
self.kbd_activity_in_last_30_secs = False
def on_cancel_session_negotiation(self):
msg = _('Session negotiation cancelled')
ChatControlBase.print_conversation_line(self, msg, 'status', '', None)
def print_esession_details(self):
2009-11-25 14:01:40 +01:00
"""
Print esession settings to textview
"""
e2e_is_active = bool(self.session) and self.session.enable_encryption
if e2e_is_active:
2008-09-08 17:51:13 +02:00
msg = _('This session is encrypted')
if self.session.is_loggable():
2008-09-08 17:51:13 +02:00
msg += _(' and WILL be logged')
else:
2008-09-08 17:51:13 +02:00
msg += _(' and WILL NOT be logged')
ChatControlBase.print_conversation_line(self, msg, 'status', '', None)
if not self.session.verified_identity:
2008-07-09 05:10:50 +02:00
ChatControlBase.print_conversation_line(self, _("Remote contact's identity not verified. Click the shield button for more details."), 'status', '', None)
else:
msg = _('E2E encryption disabled')
ChatControlBase.print_conversation_line(self, msg, 'status', '', None)
self._show_lock_image(e2e_is_active, 'E2E', e2e_is_active, self.session and \
self.session.is_loggable(), self.session and self.session.verified_identity)
def print_conversation(self, text, frm='', tim=None, encrypted=False,
2009-11-25 14:01:40 +01:00
subject=None, xhtml=None, simple=False, xep0184_id=None):
"""
Print a line in the conversation
If frm is set to status: it's a status message.
if frm is set to error: it's an error message. The difference between
status and error is mainly that with error, msg count as a new message
(in systray and in control).
If frm is set to info: it's a information message.
If frm is set to print_queue: it is incomming from queue.
If frm is set to another value: it's an outgoing message.
If frm is not set: it's an incomming message.
"""
2005-12-31 04:53:48 +01:00
contact = self.contact
if frm == 'status':
if not gajim.config.get('print_status_in_chats'):
return
2005-12-31 04:53:48 +01:00
kind = 'status'
name = ''
elif frm == 'error':
kind = 'error'
name = ''
elif frm == 'info':
kind = 'info'
name = ''
2005-12-31 04:53:48 +01:00
else:
if self.session and self.session.enable_encryption:
# ESessions
if not encrypted:
msg = _('The following message was NOT encrypted')
ChatControlBase.print_conversation_line(self, msg, 'status', '',
tim)
2007-08-20 10:16:48 +02:00
else:
# GPG encryption
if encrypted and not self.gpg_is_active:
msg = _('The following message was encrypted')
ChatControlBase.print_conversation_line(self, msg, 'status', '',
tim)
# turn on OpenPGP if this was in fact a XEP-0027 encrypted message
if encrypted == 'xep27':
self._toggle_gpg()
elif not encrypted and self.gpg_is_active:
msg = _('The following message was NOT encrypted')
ChatControlBase.print_conversation_line(self, msg, 'status', '',
tim)
2005-12-31 04:53:48 +01:00
if not frm:
kind = 'incoming'
name = contact.get_shown_name()
2005-12-31 04:53:48 +01:00
elif frm == 'print_queue': # incoming message, but do not update time
kind = 'incoming_queue'
name = contact.get_shown_name()
2005-12-31 04:53:48 +01:00
else:
kind = 'outgoing'
name = gajim.nicks[self.account]
if not xhtml and not (encrypted and self.gpg_is_active) and \
gajim.config.get('rst_formatting_outgoing_messages'):
from common.rst_xhtml_generator import create_xhtml
xhtml = create_xhtml(text)
if xhtml:
xhtml = '<body xmlns="%s">%s</body>' % (NS_XHTML, xhtml)
2005-12-31 04:53:48 +01:00
ChatControlBase.print_conversation_line(self, text, kind, name, tim,
subject=subject, old_kind=self.old_msg_kind, xhtml=xhtml,
simple=simple, xep0184_id=xep0184_id)
if text.startswith('/me ') or text.startswith('/me\n'):
self.old_msg_kind = None
else:
self.old_msg_kind = kind
2005-12-29 04:21:43 +01:00
2006-01-05 03:58:59 +01:00
def get_tab_label(self, chatstate):
unread = ''
if self.resource:
jid = self.contact.get_full_jid()
else:
jid = self.contact.jid
num_unread = len(gajim.events.get_events(self.account, jid,
['printed_' + self.type_id, self.type_id]))
if num_unread == 1 and not gajim.config.get('show_unread_tab_icon'):
unread = '*'
elif num_unread > 1:
unread = '[' + unicode(num_unread) + ']'
2008-05-04 02:24:27 +02:00
# Draw tab label using chatstate
theme = gajim.config.get('roster_theme')
color = None
if not chatstate:
chatstate = self.contact.chatstate
if chatstate is not None:
if chatstate == 'composing':
color = gajim.config.get_per('themes', theme,
'state_composing_color')
elif chatstate == 'inactive':
color = gajim.config.get_per('themes', theme,
'state_inactive_color')
elif chatstate == 'gone':
color = gajim.config.get_per('themes', theme,
'state_gone_color')
elif chatstate == 'paused':
color = gajim.config.get_per('themes', theme,
'state_paused_color')
if color:
# We set the color for when it's the current tab or not
color = gtk.gdk.colormap_get_system().alloc_color(color)
# In inactive tab color to be lighter against the darker inactive
# background
if chatstate in ('inactive', 'gone') and\
self.parent_win.get_active_control() != self:
2006-01-05 06:51:28 +01:00
color = self.lighten_color(color)
else: # active or not chatstate, get color from gtk
color = self.parent_win.notebook.style.fg[gtk.STATE_ACTIVE]
name = self.contact.get_shown_name()
if self.resource:
name += '/' + self.resource
label_str = gobject.markup_escape_text(name)
if num_unread: # if unread, text in the label becomes bold
label_str = '<b>' + unread + label_str + '</b>'
return (label_str, color)
2005-12-31 08:19:43 +01:00
def get_tab_image(self, count_unread=True):
if self.resource:
jid = self.contact.get_full_jid()
else:
jid = self.contact.jid
if count_unread:
num_unread = len(gajim.events.get_events(self.account, jid,
['printed_' + self.type_id, self.type_id]))
else:
num_unread = 0
# Set tab image (always 16x16); unread messages show the 'event' image
2006-01-05 03:58:59 +01:00
tab_img = None
2006-01-05 03:58:59 +01:00
if num_unread and gajim.config.get('show_unread_tab_icon'):
img_16 = gajim.interface.roster.get_appropriate_state_images(
self.contact.jid, icon_name = 'event')
tab_img = img_16['event']
2006-01-05 03:58:59 +01:00
else:
contact = gajim.contacts.get_contact_with_highest_priority(
self.account, self.contact.jid)
if not contact or self.resource:
2006-01-09 00:14:50 +01:00
# For transient contacts
contact = self.contact
img_16 = gajim.interface.roster.get_appropriate_state_images(
self.contact.jid, icon_name=contact.show)
tab_img = img_16[contact.show]
2006-01-05 03:58:59 +01:00
return tab_img
def prepare_context_menu(self, hide_buttonbar_items=False):
2009-11-25 14:01:40 +01:00
"""
Set compact view menuitem active state sets active and sensitivity state
for toggle_gpg_menuitem sets sensitivity for history_menuitem (False for
tranasports) and file_transfer_menuitem and hide()/show() for
add_to_roster_menuitem
"""
menu = gui_menu_builder.get_contact_menu(self.contact, self.account,
use_multiple_contacts=False, show_start_chat=False,
show_encryption=True, control=self,
show_buttonbar_items=not hide_buttonbar_items)
2005-12-31 08:19:43 +01:00
return menu
def send_chatstate(self, state, contact = None):
2009-11-25 14:01:40 +01:00
"""
Send OUR chatstate as STANDLONE chat state message (eg. no body)
to contact only if new chatstate is different from the previous one
2009-11-25 14:01:40 +01:00
if jid is not specified, send to active tab
"""
# JEP 85 does not allow resending the same chatstate
# this function checks for that and just returns so it's safe to call it
# with same state.
# This functions also checks for violation in state transitions
# and raises RuntimeException with appropriate message
# more on that http://www.jabber.org/jeps/jep-0085.html#statechart
# do not send nothing if we have chat state notifications disabled
# that means we won't reply to the <active/> from other peer
# so we do not broadcast jep85 capabalities
chatstate_setting = gajim.config.get('outgoing_chat_state_notifications')
if chatstate_setting == 'disabled':
return
elif chatstate_setting == 'composing_only' and state != 'active' and\
state != 'composing':
return
if contact is None:
contact = self.parent_win.get_active_contact()
if contact is None:
# contact was from pm in MUC, and left the room so contact is None
# so we cannot send chatstate anymore
return
# Don't send chatstates to offline contacts
if contact.show == 'offline':
return
if contact.composing_xep is False: # jid cannot do xep85 nor xep22
return
# if the new state we wanna send (state) equals
# the current state (contact.our_chatstate) then return
if contact.our_chatstate == state:
return
if contact.composing_xep is None:
# we don't know anything about jid, so return
# NOTE:
# send 'active', set current state to 'ask' and return is done
# in self.send_message() because we need REAL message (with <body>)
# for that procedure so return to make sure we send only once
# 'active' until we know peer supports jep85
return
if contact.our_chatstate == 'ask':
return
# in JEP22, when we already sent stop composing
# notification on paused, don't resend it
if contact.composing_xep == 'XEP-0022' and \
contact.our_chatstate in ('paused', 'active', 'inactive') and \
state is not 'composing': # not composing == in (active, inactive, gone)
contact.our_chatstate = 'active'
self.reset_kbd_mouse_timeout_vars()
return
# prevent going paused if we we were not composing (JEP violation)
if state == 'paused' and not contact.our_chatstate == 'composing':
# go active before
MessageControl.send_message(self, None, chatstate = 'active')
contact.our_chatstate = 'active'
self.reset_kbd_mouse_timeout_vars()
# if we're inactive prevent composing (JEP violation)
2006-02-27 00:29:49 +01:00
elif contact.our_chatstate == 'inactive' and state == 'composing':
# go active before
MessageControl.send_message(self, None, chatstate = 'active')
contact.our_chatstate = 'active'
self.reset_kbd_mouse_timeout_vars()
MessageControl.send_message(self, None, chatstate = state,
msg_id = contact.msg_id, composing_xep = contact.composing_xep)
contact.our_chatstate = state
if contact.our_chatstate == 'active':
self.reset_kbd_mouse_timeout_vars()
2006-01-01 20:40:05 +01:00
def shutdown(self):
# Send 'gone' chatstate
self.send_chatstate('gone', self.contact)
2006-01-01 20:40:05 +01:00
self.contact.chatstate = None
self.contact.our_chatstate = None
2008-02-06 06:24:07 +01:00
for jingle_type in ('audio', 'video'):
self.close_jingle_content(jingle_type)
# disconnect self from session
if self.session:
self.session.control = None
2008-02-06 06:24:07 +01:00
2006-01-01 20:40:05 +01:00
# Disconnect timer callbacks
gobject.source_remove(self.possible_paused_timeout_id)
gobject.source_remove(self.possible_inactive_timeout_id)
# Remove bigger avatar window
if self.bigger_avatar_window:
self.bigger_avatar_window.destroy()
# Clean events
gajim.events.remove_events(self.account, self.get_full_jid(),
types = ['printed_' + self.type_id, self.type_id])
# Remove contact instance if contact has been removed
key = (self.contact.jid, self.account)
roster = gajim.interface.roster
if key in roster.contacts_to_be_removed.keys() and \
not roster.contact_has_pending_roster_events(self.contact, self.account):
backend = roster.contacts_to_be_removed[key]['backend']
del roster.contacts_to_be_removed[key]
roster.remove_contact(self.contact.jid, self.account, force=True,
backend=backend)
# remove all register handlers on widgets, created by self.xml
# to prevent circular references among objects
for i in self.handlers.keys():
if self.handlers[i].handler_is_connected(i):
self.handlers[i].disconnect(i)
del self.handlers[i]
self.conv_textview.del_handlers()
if gajim.config.get('use_speller') and HAS_GTK_SPELL:
spell_obj = gtkspell.get_from_text_view(self.msg_textview)
if spell_obj:
spell_obj.detach()
self.msg_textview.destroy()
2006-01-01 20:40:05 +01:00
def minimizable(self):
return False
def safe_shutdown(self):
return False
def allow_shutdown(self, method, on_yes, on_no, on_minimize):
if time.time() - gajim.last_message_time[self.account]\
[self.get_full_jid()] < 2:
2006-01-01 20:40:05 +01:00
# 2 seconds
def on_ok():
on_yes(self)
def on_cancel():
on_no(self)
2008-12-02 16:53:23 +01:00
dialogs.ConfirmationDialog(
2007-06-15 20:30:48 +02:00
# %s is being replaced in the code with JID
_('You just received a new message from "%s"') % self.contact.jid,
2006-01-01 20:40:05 +01:00
_('If you close this tab and you have history disabled, '\
'this message will be lost.'), on_response_ok=on_ok,
on_response_cancel=on_cancel)
return
on_yes(self)
2006-01-01 20:40:05 +01:00
def handle_incoming_chatstate(self):
2009-11-25 14:01:40 +01:00
"""
Handle incoming chatstate that jid SENT TO us
"""
self.draw_banner_text()
# update chatstate in tab for this chat
self.parent_win.redraw_tab(self, self.contact.chatstate)
2006-01-02 10:04:30 +01:00
def set_control_active(self, state):
ChatControlBase.set_control_active(self, state)
# send chatstate inactive to the one we're leaving
# and active to the one we visit
if state:
self.send_chatstate('active', self.contact)
2006-01-02 10:04:30 +01:00
else:
self.send_chatstate('inactive', self.contact)
2009-06-23 16:42:32 +02:00
# Hide bigger avatar window
if self.bigger_avatar_window:
self.bigger_avatar_window.destroy()
self.bigger_avatar_window = None
# Re-show the small avatar
self.show_avatar()
2006-01-02 23:08:50 +01:00
def show_avatar(self):
if not gajim.config.get('show_avatar_in_chat'):
return
jid_with_resource = self.contact.get_full_jid()
pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid_with_resource)
if pixbuf == 'ask':
# we don't have the vcard
if self.TYPE_ID == message_control.TYPE_PM:
if self.gc_contact.jid:
# We know the real jid of this contact
real_jid = self.gc_contact.jid
if self.gc_contact.resource:
real_jid += '/' + self.gc_contact.resource
else:
real_jid = jid_with_resource
gajim.connections[self.account].request_vcard(real_jid,
jid_with_resource)
else:
gajim.connections[self.account].request_vcard(jid_with_resource)
2006-01-02 23:08:50 +01:00
return
elif pixbuf:
2006-01-02 23:08:50 +01:00
scaled_pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'chat')
else:
scaled_pixbuf = None
2006-01-02 23:08:50 +01:00
image = self.xml.get_object('avatar_image')
2006-01-02 23:08:50 +01:00
image.set_from_pixbuf(scaled_pixbuf)
image.show_all()
def _on_drag_data_received(self, widget, context, x, y, selection,
target_type, timestamp):
if not selection.data:
return
if self.TYPE_ID == message_control.TYPE_PM:
c = self.gc_contact
else:
c = self.contact
2006-01-02 23:08:50 +01:00
if target_type == self.TARGET_TYPE_URI_LIST:
if not c.resource: # If no resource is known, we can't send a file
2007-07-25 19:30:43 +02:00
return
2006-01-02 23:08:50 +01:00
uri = selection.data.strip()
uri_splitted = uri.split() # we may have more than one file dropped
for uri in uri_splitted:
path = helpers.get_file_path_from_dnd_dropped_uri(uri)
if os.path.isfile(path): # is it file?
ft = gajim.interface.instances['file_transfers']
ft.send_file(self.account, c, path)
return
# chat2muc
treeview = gajim.interface.roster.tree
model = treeview.get_model()
data = selection.data
path = treeview.get_selection().get_selected_rows()[1][0]
2008-12-03 18:16:04 +01:00
iter_ = model.get_iter(path)
type_ = model[iter_][2]
if type_ != 'contact': # source is not a contact
return
dropped_jid = data.decode('utf-8')
dropped_transport = gajim.get_transport_name_from_jid(dropped_jid)
c_transport = gajim.get_transport_name_from_jid(c.jid)
if dropped_transport or c_transport:
return # transport contacts cannot be invited
dialogs.TransformChatToMUC(self.account, [c.jid], [dropped_jid])
def _on_message_tv_buffer_changed(self, textbuffer):
self.kbd_activity_in_last_5_secs = True
self.kbd_activity_in_last_30_secs = True
if textbuffer.get_char_count():
self.send_chatstate('composing', self.contact)
e2e_is_active = self.session and \
self.session.enable_encryption
e2e_pref = gajim.config.get_per('accounts', self.account,
'enable_esessions') and gajim.config.get_per('accounts',
self.account, 'autonegotiate_esessions') and gajim.config.get_per(
'contacts', self.contact.jid, 'autonegotiate_esessions')
want_e2e = not e2e_is_active and not self.gpg_is_active \
and e2e_pref
if want_e2e and not self.no_autonegotiation \
and gajim.HAVE_PYCRYPTO and self.contact.supports(NS_ESESSION):
self.begin_e2e_negotiation()
else:
self.send_chatstate('active', self.contact)
def restore_conversation(self):
jid = self.contact.jid
# don't restore lines if it's a transport
if gajim.jid_is_transport(jid):
return
2006-03-24 20:20:05 +01:00
# How many lines to restore and when to time them out
restore_how_many = gajim.config.get('restore_lines')
if restore_how_many <= 0:
return
timeout = gajim.config.get('restore_timeout') # in minutes
2006-03-24 20:20:05 +01:00
# number of messages that are in queue and are already logged, we want
# to avoid duplication
pending_how_many = len(gajim.events.get_events(self.account, jid,
['chat', 'pm']))
if self.resource:
pending_how_many += len(gajim.events.get_events(self.account,
self.contact.get_full_jid(), ['chat', 'pm']))
try:
rows = gajim.logger.get_last_conversation_lines(jid, restore_how_many,
pending_how_many, timeout, self.account)
except exceptions.DatabaseMalformed:
import common.logger
dialogs.ErrorDialog(_('Database Error'),
_('The database file (%s) cannot be read. Try to repair it or remove it (all history will be lost).') % common.logger.LOG_DB_PATH)
rows = []
local_old_kind = None
for row in rows: # row[0] time, row[1] has kind, row[2] the message
if not row[2]: # message is empty, we don't print it
continue
if row[1] in (constants.KIND_CHAT_MSG_SENT,
constants.KIND_SINGLE_MSG_SENT):
kind = 'outgoing'
name = gajim.nicks[self.account]
elif row[1] in (constants.KIND_SINGLE_MSG_RECV,
constants.KIND_CHAT_MSG_RECV):
kind = 'incoming'
name = self.contact.get_shown_name()
elif row[1] == constants.KIND_ERROR:
kind = 'status'
2007-01-13 22:42:28 +01:00
name = self.contact.get_shown_name()
tim = time.localtime(float(row[0]))
if gajim.config.get('restored_messages_small'):
small_attr = ['small']
else:
small_attr = []
ChatControlBase.print_conversation_line(self, row[2], kind, name, tim,
small_attr,
small_attr + ['restored_message'],
small_attr + ['restored_message'],
False, old_kind = local_old_kind)
if row[2].startswith('/me ') or row[2].startswith('/me\n'):
local_old_kind = None
else:
local_old_kind = kind
if len(rows):
self.conv_textview.print_empty_line()
def read_queue(self):
2009-11-25 14:01:40 +01:00
"""
Read queue and print messages containted in it
"""
jid = self.contact.jid
jid_with_resource = jid
if self.resource:
jid_with_resource += '/' + self.resource
events = gajim.events.get_events(self.account, jid_with_resource)
# list of message ids which should be marked as read
message_ids = []
for event in events:
2006-09-07 15:19:43 +02:00
if event.type_ != self.type_id:
continue
data = event.parameters
kind = data[2]
if kind == 'error':
kind = 'info'
else:
kind = 'print_queue'
self.print_conversation(data[0], kind, tim = data[3],
encrypted = data[4], subject = data[1], xhtml = data[7])
if len(data) > 6 and isinstance(data[6], int):
message_ids.append(data[6])
if len(data) > 8:
self.set_session(data[8])
if message_ids:
gajim.logger.set_read_messages(message_ids)
gajim.events.remove_events(self.account, jid_with_resource,
2006-09-07 15:19:43 +02:00
types = [self.type_id])
typ = 'chat' # Is it a normal chat or a pm ?
2006-01-06 07:59:55 +01:00
# reset to status image in gc if it is a pm
# Is it a pm ?
room_jid, nick = gajim.get_room_and_nick_from_fjid(jid)
control = gajim.interface.msg_win_mgr.get_gc_control(room_jid,
self.account)
2008-05-04 02:24:27 +02:00
if control and control.type_id == message_control.TYPE_GC:
control.update_ui()
control.parent_win.show_title()
2006-01-06 07:59:55 +01:00
typ = 'pm'
self.redraw_after_event_removed(jid)
if (self.contact.show in ('offline', 'error')):
show_offline = gajim.config.get('showoffline')
show_transports = gajim.config.get('show_transports_group')
if (not show_transports and gajim.jid_is_transport(jid)) or \
(not show_offline and typ == 'chat' and \
len(gajim.contacts.get_contacts(self.account, jid)) < 2):
gajim.interface.roster.remove_to_be_removed(self.contact.jid,
self.account)
elif typ == 'pm':
2006-01-06 07:59:55 +01:00
control.remove_contact(nick)
2006-01-03 05:05:28 +01:00
def show_bigger_avatar(self, small_avatar):
2009-11-25 14:01:40 +01:00
"""
Resize the avatar, if needed, so it has at max half the screen size and
shows it
"""
2006-07-17 14:48:07 +02:00
if not small_avatar.window:
# Tab has been closed since we hovered the avatar
return
2006-03-18 10:27:41 +01:00
avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(
self.contact.jid)
2006-03-18 10:22:27 +01:00
if avatar_pixbuf in ('ask', None):
2006-01-03 05:05:28 +01:00
return
# Hide the small avatar
2006-03-10 14:54:04 +01:00
# this code hides the small avatar when we show a bigger one in case
# the avatar has a transparency hole in the middle
# so when we show the big one we avoid seeing the small one behind.
# It's why I set it transparent.
image = self.xml.get_object('avatar_image')
pixbuf = image.get_pixbuf()
2006-07-18 23:14:07 +02:00
pixbuf.fill(0xffffff00L) # RGBA
image.queue_draw()
2006-01-03 05:05:28 +01:00
screen_w = gtk.gdk.screen_width()
screen_h = gtk.gdk.screen_height()
avatar_w = avatar_pixbuf.get_width()
avatar_h = avatar_pixbuf.get_height()
half_scr_w = screen_w / 2
half_scr_h = screen_h / 2
if avatar_w > half_scr_w:
avatar_w = half_scr_w
if avatar_h > half_scr_h:
avatar_h = half_scr_h
window = gtk.Window(gtk.WINDOW_POPUP)
self.bigger_avatar_window = window
pixmap, mask = avatar_pixbuf.render_pixmap_and_mask()
window.set_size_request(avatar_w, avatar_h)
# we should make the cursor visible
# gtk+ doesn't make use of the motion notify on gtkwindow by default
# so this line adds that
window.set_events(gtk.gdk.POINTER_MOTION_MASK)
window.set_app_paintable(True)
window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_TOOLTIP)
2008-05-04 02:24:27 +02:00
2006-01-03 05:05:28 +01:00
window.realize()
window.window.set_back_pixmap(pixmap, False) # make it transparent
window.window.shape_combine_mask(mask, 0, 0)
2008-05-04 02:24:27 +02:00
# make the bigger avatar window show up centered
2006-01-03 05:05:28 +01:00
x0, y0 = small_avatar.window.get_origin()
x0 += small_avatar.allocation.x
y0 += small_avatar.allocation.y
center_x= x0 + (small_avatar.allocation.width / 2)
center_y = y0 + (small_avatar.allocation.height / 2)
2008-05-04 02:24:27 +02:00
pos_x, pos_y = center_x - (avatar_w / 2), center_y - (avatar_h / 2)
2006-01-03 05:05:28 +01:00
window.move(pos_x, pos_y)
# make the cursor invisible so we can see the image
invisible_cursor = gtkgui_helpers.get_invisible_cursor()
window.window.set_cursor(invisible_cursor)
# we should hide the window
window.connect('leave_notify_event',
self._on_window_avatar_leave_notify_event)
window.connect('motion-notify-event',
self._on_window_motion_notify_event)
window.show_all()
def _on_window_avatar_leave_notify_event(self, widget, event):
2009-11-25 14:01:40 +01:00
"""
Just left the popup window that holds avatar
"""
2006-01-03 05:05:28 +01:00
self.bigger_avatar_window.destroy()
self.bigger_avatar_window = None
# Re-show the small avatar
self.show_avatar()
2006-01-03 05:05:28 +01:00
def _on_window_motion_notify_event(self, widget, event):
2009-11-25 14:01:40 +01:00
"""
Just moved the mouse so show the cursor
"""
2006-01-03 05:05:28 +01:00
cursor = gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)
self.bigger_avatar_window.window.set_cursor(cursor)
def _on_send_file_menuitem_activate(self, widget):
self._on_send_file()
def _on_add_to_roster_menuitem_activate(self, widget):
dialogs.AddNewContactWindow(self.account, self.contact.jid)
def _on_contact_information_menuitem_activate(self, widget):
gajim.interface.roster.on_info(widget, self.contact, self.account)
def _on_toggle_gpg_menuitem_activate(self, widget):
self._toggle_gpg()
def _on_convert_to_gc_menuitem_activate(self, widget):
2009-11-25 14:01:40 +01:00
"""
User wants to invite some friends to chat
"""
dialogs.TransformChatToMUC(self.account, [self.contact.jid])
2007-06-08 21:42:02 +02:00
def _on_toggle_e2e_menuitem_activate(self, widget):
2007-07-03 20:00:09 +02:00
if self.session and self.session.enable_encryption:
# e2e was enabled, disable it
2007-06-29 06:12:08 +02:00
jid = str(self.session.jid)
2008-06-29 17:57:07 +02:00
thread_id = self.session.thread_id
2007-06-29 06:12:08 +02:00
2008-06-29 17:57:07 +02:00
self.session.terminate_e2e()
2007-08-20 10:16:48 +02:00
2008-06-29 17:57:07 +02:00
gajim.connections[self.account].delete_session(jid, thread_id)
# presumably the user had a good reason to shut it off, so
# disable autonegotiation too
self.no_autonegotiation = True
2007-06-08 21:42:02 +02:00
else:
self.begin_e2e_negotiation()
def begin_e2e_negotiation(self):
self.no_autonegotiation = True
if not self.session:
fjid = self.contact.get_full_jid()
new_sess = gajim.connections[self.account].make_new_session(fjid, type_=self.type_id)
self.set_session(new_sess)
2007-07-03 20:00:09 +02:00
self.session.negotiate_e2e(False)
2006-02-04 03:52:36 +01:00
def got_connected(self):
ChatControlBase.got_connected(self)
# Refreshing contact
contact = gajim.contacts.get_contact_with_highest_priority(
self.account, self.contact.jid)
if isinstance(contact, GC_Contact):
contact = contact.as_contact()
if contact:
self.contact = contact
2006-02-04 03:52:36 +01:00
self.draw_banner()
def update_status_display(self, name, uf_show, status):
2009-11-25 14:01:40 +01:00
"""
Print the contact's status and update the status/GPG image
"""
self.update_ui()
self.parent_win.redraw_tab(self)
2008-08-01 11:30:36 +02:00
self.print_conversation(_('%(name)s is now %(status)s') % {'name': name,
'status': uf_show}, 'status')
if status:
self.print_conversation(' (', 'status', simple=True)
self.print_conversation('%s' % (status), 'status', simple=True)
self.print_conversation(')', 'status', simple=True)
2008-07-30 16:00:21 +02:00
# vim: se ts=3: