Skip to content
Snippets Groups Projects
chat_control.py 135 KiB
Newer Older
roidelapluie's avatar
roidelapluie committed
# -*- coding:utf-8 -*-
roidelapluie's avatar
roidelapluie committed
## src/chat_control.py
nicfit's avatar
nicfit committed
##
roidelapluie's avatar
roidelapluie committed
## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
Yann Leboulanger's avatar
Yann Leboulanger committed
## Copyright (C) 2006-2012 Yann Leboulanger <asterix AT lagaule.org>
## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
roidelapluie's avatar
roidelapluie committed
##                         Nikos Kouremenos <kourem AT gmail.com>
##                         Travis Shirk <travis AT pobox.com>
roidelapluie's avatar
roidelapluie committed
## 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>
nicfit's avatar
nicfit committed
##
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
nicfit's avatar
nicfit committed
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 3 only.
nicfit's avatar
nicfit committed
##
## Gajim is distributed in the hope that it will be useful,
nicfit's avatar
nicfit committed
## but WITHOUT ANY WARRANTY; without even the implied warranty of
roidelapluie's avatar
roidelapluie committed
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
nicfit's avatar
nicfit committed
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
roidelapluie's avatar
roidelapluie committed
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
nicfit's avatar
nicfit committed

nkour's avatar
nkour committed
import os
nicfit's avatar
nicfit committed
import time
nicfit's avatar
nicfit committed
import gtk
import pango
import gobject
import gtkgui_helpers
nicfit's avatar
nicfit committed
import dialogs
import history_window
nicfit's avatar
nicfit committed

from common import gajim
nicfit's avatar
nicfit committed
from common import helpers
from common import ged
from message_control import MessageControl
nicfit's avatar
nicfit committed
from conversation_textview import ConversationTextview
from message_textview import MessageTextView
from common.stanza_session import EncryptedStanzaSession, ArchivingStanzaSession
from common.contacts import GC_Contact
from common.logger import constants
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, NS_JINGLE_FILE_TRANSFER
from common.connection_handlers_events import MessageOutgoingEvent
from common.exceptions import GajimGeneralException
nicfit's avatar
nicfit committed

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
Yann Leboulanger's avatar
Yann Leboulanger committed
import command_system.implementation.execute
nicfit's avatar
nicfit committed
try:
    import gtkspell
    HAS_GTK_SPELL = True
Dicson's avatar
Dicson committed
if dbus_support.supported:
# the next script, executed in the "po" directory,
# generates the following list.
##!/bin/sh
#LANG=$(for i in *.po; do j=${i/.po/}; echo -n "_('"$j"')":" '"$j"', " ; done)
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):
    """
    A base class containing a banner, ConversationTextview, MessageTextView
    """

    keymap = gtk.gdk.keymap_get_default()
    try:
        keycode_c = keymap.get_entries_for_keyval(gtk.keysyms.c)[0][0]
    except TypeError:
        keycode_c = 54
    try:
        keycode_ins = keymap.get_entries_for_keyval(gtk.keysyms.Insert)[0][0]
    except TypeError:
        keycode_ins = 118
    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())

    def get_font_attrs(self):
        """
        Get pango font attributes for banner from theme settings
        """
        theme = gajim.config.get('roster_theme')
        bannerfont = gajim.config.get_per('themes', theme, 'bannerfont')
        bannerfontattrs = gajim.config.get_per('themes', theme, 'bannerfontattrs')

        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)

        font_attrs = 'font_desc="%s"' % font.to_string()

        # 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()
        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):
        """
        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()
        gajim.plugin_manager.gui_extension_point('chat_control_base_draw_banner',
Yann Leboulanger's avatar
Yann Leboulanger committed
            self)
    def update_toolbar(self):
        """
        update state of buttons in toolbar
        """
        self._update_toolbar()
        gajim.plugin_manager.gui_extension_point(
            'chat_control_base_update_toolbar', self)

    def draw_banner_text(self):
        """
        Derived types SHOULD implement this
        """
        pass

    def update_ui(self):
        """
        Derived types SHOULD implement this
        """
        self.draw_banner()

    def repaint_themed_widgets(self):
        """
        Derived types MAY implement this
        """
        self._paint_banner()
        self.draw_banner()

    def _update_banner_state_image(self):
        """
        Derived types MAY implement this
        """
        pass

    def _update_toolbar(self):
        """
        Derived types MAY implement this
        """
        pass

    def _nec_our_status(self, obj):
        if self.account != obj.conn.name:
            return
        if obj.show == 'offline' or (obj.show == 'invisible' and \
        obj.conn.is_zeroconf):
            self.got_disconnected()
        else:
            # Other code rejoins all GCs, so we don't do it here
            if not self.type_id == message_control.TYPE_GC:
                self.got_connected()
        if self.parent_win:
            self.parent_win.redraw_tab(self)

    def _nec_ping_sent(self, obj):
        if self.contact != obj.contact:
            return
        self.print_conversation(_('Ping?'), 'status')

    def _nec_ping_reply(self, obj):
        if self.contact != obj.contact:
            return
        self.print_conversation(_('Pong! (%s s.)') % obj.seconds, 'status')

    def _nec_ping_error(self, obj):
        if self.contact != obj.contact:
            return
        self.print_conversation(_('Error.'), 'status')

    def handle_message_textview_mykey_press(self, widget, event_keyval,
        """
        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

    def status_url_clicked(self, widget, url):
        helpers.launch_browser_mailer('url', url)

    def setup_seclabel(self, combo):
        self.seclabel_combo = combo
        self.seclabel_combo.hide()
        self.seclabel_combo.set_no_show_all(True)
        lb = gtk.ListStore(str)
        self.seclabel_combo.set_model(lb)
        cell = gtk.CellRendererText()
        cell.set_property('xpad', 5)  # padding for status text
        self.seclabel_combo.pack_start(cell, True)
        # text to show is in in first column of liststore
        self.seclabel_combo.add_attribute(cell, 'text', 0)
        if gajim.connections[self.account].seclabel_supported:
            gajim.connections[self.account].seclabel_catalogue(self.contact.jid, self.on_seclabels_ready)

    def on_seclabels_ready(self):
        lb = self.seclabel_combo.get_model()
        lb.clear()
        i = 0
        sel = 0
        catalogue = gajim.connections[self.account].seclabel_catalogues[
            self.contact.jid]
        for label in catalogue[2]:
            if label == catalogue[3]:
                sel = i
            i += 1
        self.seclabel_combo.set_active(sel)
        self.seclabel_combo.set_no_show_all(False)
        self.seclabel_combo.show_all()

    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.
            c = gajim.contacts.get_contact_with_highest_priority(
                    acct, contact.jid)
            if c and not isinstance(c, GC_Contact):
                contact = c

        MessageControl.__init__(self, type_id, parent_win, widget_name,
                contact, acct, resource=resource)

        widget = self.xml.get_object('history_button')
        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')
        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')
        id_ = widget.connect('button-press-event',
                self._on_banner_eventbox_button_press_event)
        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)]
        id_ = self.widget.connect('drag_data_received',
                self._on_drag_data_received)
        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
        self.conv_textview = ConversationTextview(self.account)
        id_ = self.conv_textview.connect('quote', self.on_quote)
        self.handlers[id_] = self.conv_textview.tv
        id_ = self.conv_textview.tv.connect('key_press_event',
                self._conv_textview_key_press_event)
        self.handlers[id_] = self.conv_textview.tv
        # FIXME: DND on non editable TextView, find a better way
        self.drag_entered = False
        id_ = self.conv_textview.tv.connect('drag_data_received',
                self._on_drag_data_received)
        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(
                'conversation_scrolledwindow')
        self.conv_scrolledwindow.add(self.conv_textview.tv)
        widget = self.conv_scrolledwindow.get_vadjustment()
        id_ = widget.connect('value-changed',
                self.on_conversation_vadjustment_value_changed)
        self.handlers[id_] = widget
        id_ = widget.connect('changed',
                self.on_conversation_vadjustment_changed)
        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()
        id_ = self.msg_textview.connect('mykeypress',
                self._on_message_textview_mykeypress_event)
        self.handlers[id_] = self.msg_textview
        self.msg_scrolledwindow.add(self.msg_textview)
        id_ = self.msg_textview.connect('key_press_event',
                self._on_message_textview_key_press_event)
        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)
        self.handlers[id_] = self.msg_textview
        # Setup DND
        id_ = self.msg_textview.connect('drag_data_received',
                self._on_drag_data_received)
        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)

        self.update_font()

        # Hook up send button
        widget = self.xml.get_object('send_button')
        id_ = widget.connect('clicked', self._on_send_button_clicked)
        self.handlers[id_] = widget

        widget = self.xml.get_object('formattings_button')
        id_ = widget.connect('clicked', self.on_formattings_button_clicked)
        self.handlers[id_] = widget

        # the following vars are used to keep history of user's messages
        self.sent_history = []
        self.sent_history_pos = 0
        self.received_history = []
        self.received_history_pos = 0
        self.orig_msg = None

        # 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()

        # Attach speller
        if gajim.config.get('use_speller') and HAS_GTK_SPELL:
            self.set_speller()
        self.conv_textview.tv.show()
        self._paint_banner()

        # For XEP-0172
        self.user_nick = None

        self.smooth = True

        self.command_hits = []
        self.last_key_tabs = False

        # PluginSystem: adding GUI extension point for ChatControlBase
        # instance object (also subclasses, eg. ChatControl or GroupchatControl)
        gajim.plugin_manager.gui_extension_point('chat_control_base', self)

        gajim.ged.register_event_handler('our-show', ged.GUI1,
            self._nec_our_status)
        gajim.ged.register_event_handler('ping-sent', ged.GUI1,
            self._nec_ping_sent)
        gajim.ged.register_event_handler('ping-reply', ged.GUI1,
            self._nec_ping_reply)
        gajim.ged.register_event_handler('ping-error', ged.GUI1,
            self._nec_ping_error)
        # This is bascially a very nasty hack to surpass the inability
        # to properly use the super, because of the old code.
        CommandTools.__init__(self)

    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):
        """
        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()

        super(ChatControlBase, self).shutdown()
        # PluginSystem: removing GUI extension points connected with ChatControlBase
        # instance object
        gajim.plugin_manager.remove_gui_extension_point('chat_control_base',
            self)
        gajim.plugin_manager.remove_gui_extension_point(
            'chat_control_base_draw_banner', self)
        gajim.plugin_manager.remove_gui_extension_point('print_special_text',
            self)
        gajim.ged.remove_event_handler('our-show', ged.GUI1,
            self._nec_our_status)
    def on_msg_textview_populate_popup(self, textview, menu):
        """
        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)
        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)
                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):
        """
        If right-clicked, show popup
        """
        if event.button == 3:  # right click
            self.parent_win.popup_menu(event)

    def _on_send_button_clicked(self, widget):
        """
        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
        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')
        xhtml = self.msg_textview.get_xhtml()

        # send the message
        self.send_message(message, xhtml=xhtml)

    def _paint_banner(self):
        """
        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:
            self._on_style_set_event(banner_name_label, None, default_fg,
                    default_bg)
            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():
            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)
        id_ = widget.connect('style-set', self._on_style_set_event, set_fg,
                set_bg)
        self.handlers[id_] = widget

    def _on_style_set_event(self, widget, style, *opts):
        """
        Set style of widget from style class *.Frame.Eventbox
                opts[0] == True -> set fg color
                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):
        # translate any layout to latin_layout
        keymap = gtk.gdk.keymap_get_default()
        keycode = keymap.get_entries_for_keyval(event.keyval)[0][0]
        if (event.state & gtk.gdk.CONTROL_MASK and keycode in (self.keycode_c,
        self.keycode_ins)) 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)
        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]
            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

        # 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 \
                            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):
        """
        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')
        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
                self.scroll_messages('up', message_buffer, 'sent')
            # Ctrl+Shift+UP
            elif event.state == (gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK):
                self.scroll_messages('up', message_buffer, 'received')
        elif event.keyval == gtk.keysyms.Down:
            if event.state == gtk.gdk.CONTROL_MASK:  # Ctrl+Down
                self.scroll_messages('down', message_buffer, 'sent')
            # Ctrl+Shift+Down
            elif event.state == (gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK):
                self.scroll_messages('down', message_buffer, 'received')
        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'),
                        _('Your message can not be sent until you are connected.'))
                send_message = False

            if send_message:
                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()
        else:
            # Give the control itself a chance to process
            self.handle_message_textview_mykey_press(widget, event_keyval,
                    event_keymod)

    def _on_drag_data_received(self, widget, context, x, y, selection,
                    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 get_seclabel(self):
        label = None
        if self.seclabel_combo is not None:
            idx = self.seclabel_combo.get_active()
            if idx != -1:
                cat = gajim.connections[self.account].seclabel_catalogues[self.contact.jid]
                lname = cat[2][idx]
                label = cat[1][lname]
        return label

    def send_message(self, message, keyID='', type_='chat', chatstate=None,
    msg_id=None, resource=None, xhtml=None, callback=None, callback_args=[],
        """
        Send the given message to the active tab. Doesn't return None if error
        """
        if not message or message == '\n':
            return None

        if process_commands and self.process_as_command(message):
            return

        gajim.nec.push_outgoing_event(MessageOutgoingEvent(None,
            account=self.account, jid=self.contact.jid, message=message,
            keyID=keyID, type_=type_, chatstate=chatstate, msg_id=msg_id,
            resource=resource, user_nick=self.user_nick, xhtml=xhtml,
            label=label, callback=callback, callback_args=callback_args,
        # Record the history of sent messages
        self.save_message(message, 'sent')

        # Be sure to send user nickname only once according to JEP-0172
        self.user_nick = None

        # Clear msg input
        message_buffer = self.msg_textview.get_buffer()
        message_buffer.set_text('') # clear message buffer (and tv of course)

    def save_message(self, message, msg_type):
        # save the message, so user can scroll though the list with key up/down
        if msg_type == 'sent':
            history = self.sent_history
            pos = self.received_history_pos
        size = len(history)
        scroll = False if pos == size else True # are we scrolling?
        # we don't want size of the buffer to grow indefinately
        max_size = gajim.config.get('key_up_lines')
        for i in xrange(size - max_size + 1):
        if not scroll or msg_type == 'sent':
            pos = len(history)
            self.sent_history_pos = pos
            self.orig_msg = None

    def print_conversation_line(self, text, kind, name, tim,
    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, displaymarking=None, msg_id=None):
        """
        Print 'chat' type messages
        """
        jid = self.contact.jid
        full_jid = self.get_full_jid()
        textview = self.conv_textview
        end = False
        if self.was_at_the_end or kind == 'outgoing':
            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,
            displaymarking=displaymarking)

        if xep0184_id is not None:
            textview.show_xep0184_warning(xep0184_id)

        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'):
            # Record the history of received messages
            self.save_message(text, 'received')

        if kind in ('incoming', 'incoming_queue', 'error'):
            gc_message = False
            if self.type_id == message_control.TYPE_GC:
                gc_message = True

            if ((self.parent_win and (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)) or \
            (gc_message and \
            jid in gajim.interface.minimized_controls[self.account])) and \
            kind in ('incoming', 'incoming_queue', 'error'):
                # 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'
                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,
                event = gajim.events.create_event(type_, (text, subject, self,
                    msg_id), show_in_roster=show_in_roster,
                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,

        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'):