Skip to content
Snippets Groups Projects
conversation_textview.py 55.4 KiB
Newer Older
roidelapluie's avatar
roidelapluie committed
# -*- coding:utf-8 -*-
roidelapluie's avatar
roidelapluie committed
## src/conversation_textview.py
roidelapluie's avatar
roidelapluie committed
## Copyright (C) 2005 Norman Rasmussen <norman AT rasmussen.co.za>
## Copyright (C) 2005-2006 Alex Mauer <hawke AT hawkesnest.net>
##                         Travis Shirk <travis AT pobox.com>
## Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
Yann Leboulanger's avatar
Yann Leboulanger committed
## Copyright (C) 2005-2012 Yann Leboulanger <asterix AT lagaule.org>
roidelapluie's avatar
roidelapluie committed
## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
##                    Julien Pivotto <roidelapluie AT gmail.com>
##                    Stephan Erb <steve-e AT h3c.de>
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 3 only.
## Gajim is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
roidelapluie's avatar
roidelapluie committed
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## 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/>.
from threading import Timer # for smooth scrolling
import gtk
import pango
import gobject
import time
Yann Leboulanger's avatar
Yann Leboulanger committed
import locale
import urllib
dkirov's avatar
dkirov committed
import gtkgui_helpers
from common import gajim
from common import helpers
from common import i18n
Yann Leboulanger's avatar
Yann Leboulanger committed
from common.fuzzyclock import FuzzyClock
from htmltextview import HtmlTextView
from common.exceptions import GajimGeneralException
from encodings.punycode import punycode_encode as puny_encode
NOT_SHOWN = 0
ALREADY_RECEIVED = 1
SHOWN = 2

def is_selection_modified(mark):
    name = mark.get_name()
    if name and name in ('selection_bound', 'insert'):
        return True
    else:
        return False
    return widget.flags() & gtk.HAS_FOCUS == gtk.HAS_FOCUS

class TextViewImage(gtk.Image):

    def __init__(self, anchor, text):
        super(TextViewImage, self).__init__()
        self.anchor = anchor
        self._selected = False
        self._disconnect_funcs = []
        self.connect('parent-set', self.on_parent_set)
        self.connect('expose-event', self.on_expose)
        self.set_tooltip_text(text)
        self.anchor.set_data('plaintext', text)

    def _get_selected(self):
        parent = self.get_parent()
        if not parent or not self.anchor: return False
        buffer_ = parent.get_buffer()
        position = buffer_.get_iter_at_child_anchor(self.anchor)
        bounds = buffer_.get_selection_bounds()
        if bounds and position.in_range(*bounds):
            return True
        else:
            return False

    def get_state(self):
        parent = self.get_parent()
        if not parent:
            return gtk.STATE_NORMAL
        if self._selected:
            if has_focus(parent):
                return gtk.STATE_SELECTED
            else:
                return gtk.STATE_ACTIVE
        else:
            return gtk.STATE_NORMAL

    def _update_selected(self):
        selected = self._get_selected()
        if self._selected != selected:
            self._selected = selected
            self.queue_draw()

    def _do_connect(self, widget, signal, callback):
        id_ = widget.connect(signal, callback)
        def disconnect():
            widget.disconnect(id_)
        self._disconnect_funcs.append(disconnect)

    def _disconnect_signals(self):
        for func in self._disconnect_funcs:
            func()
        self._disconnect_funcs = []

    def on_parent_set(self, widget, old_parent):
        parent = self.get_parent()
        if not parent:
            self._disconnect_signals()
            return

        self._do_connect(parent, 'style-set', self.do_queue_draw)
        self._do_connect(parent, 'focus-in-event', self.do_queue_draw)
        self._do_connect(parent, 'focus-out-event', self.do_queue_draw)

        textbuf = parent.get_buffer()
        self._do_connect(textbuf, 'mark-set', self.on_mark_set)
        self._do_connect(textbuf, 'mark-deleted', self.on_mark_deleted)

    def do_queue_draw(self, *args):
        self.queue_draw()
        return False

    def on_mark_set(self, buf, iterat, mark):
        self.on_mark_modified(mark)
        return False

    def on_mark_deleted(self, buf, mark):
        self.on_mark_modified(mark)
        return False

    def on_mark_modified(self, mark):
        if is_selection_modified(mark):
            self._update_selected()

    def on_expose(self, widget, event):
        state = self.get_state()
        if state != gtk.STATE_NORMAL:
            gc = widget.get_style().base_gc[state]
            area = widget.allocation
            widget.window.draw_rectangle(gc, True, area.x, area.y,
                    area.width, area.height)
        return False
class ConversationTextview(gobject.GObject):
    """
    Class for the conversation textview (where user reads already said messages)
    for chat/groupchat windows
    """
    __gsignals__ = dict(
            quote = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION,
                    None, # return value
                    (str, ) # arguments
            )
    )

    FOCUS_OUT_LINE_PIXBUF = gtkgui_helpers.get_icon_pixmap('gajim-muc_separator')
    XEP0184_WARNING_PIXBUF = gtkgui_helpers.get_icon_pixmap(
            'gajim-receipt_missing')

    # smooth scroll constants
    MAX_SCROLL_TIME = 0.4 # seconds
    SCROLL_DELAY = 33 # milliseconds

    def __init__(self, account, used_in_history_window = False):
        """
        If used_in_history_window is True, then we do not show Clear menuitem in
        context menu
        """
        gobject.GObject.__init__(self)
        self.used_in_history_window = used_in_history_window

        self.fc = FuzzyClock()


        # no need to inherit TextView, use it as atrribute is safer
        self.tv = HtmlTextView()
        self.tv.hyperlink_handler = self.hyperlink_handler

        # set properties
        self.tv.set_border_width(1)
        self.tv.set_accepts_tab(True)
        self.tv.set_editable(False)
        self.tv.set_cursor_visible(False)
        self.tv.set_wrap_mode(gtk.WRAP_WORD_CHAR)
        self.tv.set_left_margin(2)
        self.tv.set_right_margin(2)
        self.handlers = {}
        self.images = []
        self.image_cache = {}
        self.xep0184_marks = {}
        self.xep0184_shown = {}

        # It's True when we scroll in the code, so we can detect scroll from user
        self.auto_scrolling = False

        # connect signals
        id_ = self.tv.connect('motion_notify_event',
                self.on_textview_motion_notify_event)
        self.handlers[id_] = self.tv
        id_ = self.tv.connect('populate_popup', self.on_textview_populate_popup)
        self.handlers[id_] = self.tv
        id_ = self.tv.connect('button_press_event',
                self.on_textview_button_press_event)
        self.handlers[id_] = self.tv

        id_ = self.tv.connect('expose-event',
                self.on_textview_expose_event)
        self.handlers[id_] = self.tv


        self.account = account
        self.change_cursor = False
        self.last_time_printout = 0

        font = pango.FontDescription(gajim.config.get('conversation_font'))
        self.tv.modify_font(font)
        buffer_ = self.tv.get_buffer()
        end_iter = buffer_.get_end_iter()
        buffer_.create_mark('end', end_iter, False)

        self.tagIn = buffer_.create_tag('incoming')
        color = gajim.config.get('inmsgcolor')
        font = pango.FontDescription(gajim.config.get('inmsgfont'))
        self.tagIn.set_property('foreground', color)
        self.tagIn.set_property('font-desc', font)

        self.tagOut = buffer_.create_tag('outgoing')
        color = gajim.config.get('outmsgcolor')
        font = pango.FontDescription(gajim.config.get('outmsgfont'))
        self.tagOut.set_property('foreground', color)
        self.tagOut.set_property('font-desc', font)

        self.tagStatus = buffer_.create_tag('status')
        color = gajim.config.get('statusmsgcolor')
        font = pango.FontDescription(gajim.config.get('satusmsgfont'))
        self.tagStatus.set_property('foreground', color)
        self.tagStatus.set_property('font-desc', font)

        self.tagInText = buffer_.create_tag('incomingtxt')
        color = gajim.config.get('inmsgtxtcolor')
        font = pango.FontDescription(gajim.config.get('inmsgtxtfont'))
        if color:
            self.tagInText.set_property('foreground', color)
        self.tagInText.set_property('font-desc', font)

        self.tagOutText = buffer_.create_tag('outgoingtxt')
        color = gajim.config.get('outmsgtxtcolor')
        if color:
            font = pango.FontDescription(gajim.config.get('outmsgtxtfont'))
        self.tagOutText.set_property('foreground', color)
        self.tagOutText.set_property('font-desc', font)

        colors = gajim.config.get('gc_nicknames_colors')
        colors = colors.split(':')
        for i, color in enumerate(colors):
            tagname = 'gc_nickname_color_' + str(i)
            tag = buffer_.create_tag(tagname)
            tag.set_property('foreground', color)

        tag = buffer_.create_tag('marked')
        color = gajim.config.get('markedmsgcolor')
        tag.set_property('foreground', color)
        tag.set_property('weight', pango.WEIGHT_BOLD)

        tag = buffer_.create_tag('time_sometimes')
        tag.set_property('foreground', 'darkgrey')
        tag.set_property('scale', pango.SCALE_SMALL)
        tag.set_property('justification', gtk.JUSTIFY_CENTER)

        tag = buffer_.create_tag('small')
        tag.set_property('scale', pango.SCALE_SMALL)

        tag = buffer_.create_tag('restored_message')
        color = gajim.config.get('restored_messages_color')
        tag.set_property('foreground', color)

        self.tagURL = buffer_.create_tag('url')
        color = gajim.config.get('urlmsgcolor')
        self.tagURL.set_property('foreground', color)
        self.tagURL.set_property('underline', pango.UNDERLINE_SINGLE)
        id_ = self.tagURL.connect('event', self.hyperlink_handler, 'url')
        self.handlers[id_] = self.tagURL

        self.tagMail = buffer_.create_tag('mail')
        self.tagMail.set_property('foreground', color)
        self.tagMail.set_property('underline', pango.UNDERLINE_SINGLE)
        id_ = self.tagMail.connect('event', self.hyperlink_handler, 'mail')
        self.handlers[id_] = self.tagMail

        self.tagXMPP = buffer_.create_tag('xmpp')
        self.tagXMPP.set_property('foreground', color)
        self.tagXMPP.set_property('underline', pango.UNDERLINE_SINGLE)
        id_ = self.tagXMPP.connect('event', self.hyperlink_handler, 'xmpp')
        self.handlers[id_] = self.tagXMPP

        self.tagSthAtSth = buffer_.create_tag('sth_at_sth')
        self.tagSthAtSth.set_property('foreground', color)
        self.tagSthAtSth.set_property('underline', pango.UNDERLINE_SINGLE)
        id_ = self.tagSthAtSth.connect('event', self.hyperlink_handler,
                'sth_at_sth')
        self.handlers[id_] = self.tagSthAtSth

        tag = buffer_.create_tag('bold')
        tag.set_property('weight', pango.WEIGHT_BOLD)

        tag = buffer_.create_tag('italic')
        tag.set_property('style', pango.STYLE_ITALIC)

        tag = buffer_.create_tag('underline')
        tag.set_property('underline', pango.UNDERLINE_SINGLE)

        buffer_.create_tag('focus-out-line', justification = gtk.JUSTIFY_CENTER)

        tag = buffer_.create_tag('xep0184-warning')

        # One mark at the begining then 2 marks between each lines
        size = gajim.config.get('max_conversation_lines')
        size = 2 * size - 1
        self.marks_queue = Queue.Queue(size)

        self.allow_focus_out_line = True
        # holds a mark at the end of --- line
        self.focus_out_end_mark = None

        self.xep0184_warning_tooltip = tooltips.BaseTooltip()

        self.line_tooltip = tooltips.BaseTooltip()
        # use it for hr too
        self.tv.focus_out_line_pixbuf = ConversationTextview.FOCUS_OUT_LINE_PIXBUF
        self.smooth_id = None

    def del_handlers(self):
        for i in self.handlers.keys():
            if self.handlers[i].handler_is_connected(i):
                self.handlers[i].disconnect(i)
        del self.handlers
        self.tv.destroy()
        #FIXME:
        # self.line_tooltip.destroy()

    def update_tags(self):
        self.tagIn.set_property('foreground', gajim.config.get('inmsgcolor'))
        self.tagOut.set_property('foreground', gajim.config.get('outmsgcolor'))
        self.tagStatus.set_property('foreground',
                gajim.config.get('statusmsgcolor'))
        self.tagURL.set_property('foreground', gajim.config.get('urlmsgcolor'))
        self.tagMail.set_property('foreground', gajim.config.get('urlmsgcolor'))

    def at_the_end(self):
        buffer_ = self.tv.get_buffer()
        end_iter = buffer_.get_end_iter()
        end_rect = self.tv.get_iter_location(end_iter)
        visible_rect = self.tv.get_visible_rect()
        if end_rect.y <= (visible_rect.y + visible_rect.height):
            return True
        return False

    # Smooth scrolling inspired by Pidgin code
    def smooth_scroll(self):
        parent = self.tv.get_parent()
        if not parent:
            return False
        vadj = parent.get_vadjustment()
        max_val = vadj.upper - vadj.page_size + 1
        cur_val = vadj.get_value()
        # scroll by 1/3rd of remaining distance
        onethird = cur_val + ((max_val - cur_val) / 3.0)
        self.auto_scrolling = True
        vadj.set_value(onethird)
        self.auto_scrolling = False
        if max_val - onethird < 0.01:
            self.smooth_id = None
            self.smooth_scroll_timer.cancel()
            return False
        return True

    def smooth_scroll_timeout(self):
        gobject.idle_add(self.do_smooth_scroll_timeout)
        return

    def do_smooth_scroll_timeout(self):
        if not self.smooth_id:
            # we finished scrolling
            return
        gobject.source_remove(self.smooth_id)
        self.smooth_id = None
        parent = self.tv.get_parent()
        if parent:
            vadj = parent.get_vadjustment()
            self.auto_scrolling = True
            vadj.set_value(vadj.upper - vadj.page_size + 1)
            self.auto_scrolling = False

    def smooth_scroll_to_end(self):
        if None != self.smooth_id: # already scrolling
            return False
        self.smooth_id = gobject.timeout_add(self.SCROLL_DELAY,
                self.smooth_scroll)
        self.smooth_scroll_timer = Timer(self.MAX_SCROLL_TIME,
                self.smooth_scroll_timeout)
        self.smooth_scroll_timer.start()
        return False

    def scroll_to_end(self):
        parent = self.tv.get_parent()
        buffer_ = self.tv.get_buffer()
        end_mark = buffer_.get_mark('end')
        if not end_mark:
            return False
        self.auto_scrolling = True
        self.tv.scroll_to_mark(end_mark, 0, True, 0, 1)
        adjustment = parent.get_hadjustment()
        adjustment.set_value(0)
        self.auto_scrolling = False
        return False # when called in an idle_add, just do it once

    def bring_scroll_to_end(self, diff_y = 0,
    use_smooth=gajim.config.get('use_smooth_scrolling')):
        ''' scrolls to the end of textview if end is not visible '''
        buffer_ = self.tv.get_buffer()
        end_iter = buffer_.get_end_iter()
        end_rect = self.tv.get_iter_location(end_iter)
        visible_rect = self.tv.get_visible_rect()
        # scroll only if expected end is not visible
        if end_rect.y >= (visible_rect.y + visible_rect.height + diff_y):
            if use_smooth:
                gobject.idle_add(self.smooth_scroll_to_end)
            else:
                gobject.idle_add(self.scroll_to_end_iter)

    def scroll_to_end_iter(self):
        buffer_ = self.tv.get_buffer()
        end_iter = buffer_.get_end_iter()
        if not end_iter:
            return False
        self.tv.scroll_to_iter(end_iter, 0, False, 1, 1)
        return False # when called in an idle_add, just do it once

    def stop_scrolling(self):
        if self.smooth_id:
            gobject.source_remove(self.smooth_id)
            self.smooth_id = None
            self.smooth_scroll_timer.cancel()

    def show_xep0184_warning(self, id_):
        if id_ in self.xep0184_marks:
            return

        buffer_ = self.tv.get_buffer()
        buffer_.begin_user_action()

        self.xep0184_marks[id_] = buffer_.create_mark(None,
                buffer_.get_end_iter(), left_gravity=True)
        self.xep0184_shown[id_] = NOT_SHOWN

        def show_it():
            if (not id_ in self.xep0184_shown) or \
            self.xep0184_shown[id_] == ALREADY_RECEIVED:
                return False

            end_iter = buffer_.get_iter_at_mark(self.xep0184_marks[id_])
            buffer_.insert(end_iter, ' ')
            anchor = buffer_.create_child_anchor(end_iter)
            img = TextViewImage(anchor, '')
            img.set_from_pixbuf(ConversationTextview.XEP0184_WARNING_PIXBUF)
            img.show()
            self.tv.add_child_at_anchor(img, anchor)
            before_img_iter = buffer_.get_iter_at_mark(self.xep0184_marks[id_])
            before_img_iter.forward_char()
            post_img_iter = before_img_iter.copy()
            post_img_iter.forward_char()
            buffer_.apply_tag_by_name('xep0184-warning', before_img_iter,
                    post_img_iter)

            self.xep0184_shown[id_] = SHOWN
            return False
        gobject.timeout_add_seconds(3, show_it)

        buffer_.end_user_action()

    def hide_xep0184_warning(self, id_):
        if id_ not in self.xep0184_marks:
            return

        if self.xep0184_shown[id_] == NOT_SHOWN:
            self.xep0184_shown[id_] = ALREADY_RECEIVED
            return

        buffer_ = self.tv.get_buffer()
        buffer_.begin_user_action()

        begin_iter = buffer_.get_iter_at_mark(self.xep0184_marks[id_])

        end_iter = begin_iter.copy()
        # XXX: Is there a nicer way?
        end_iter.forward_char()
        end_iter.forward_char()

        buffer_.delete(begin_iter, end_iter)
        buffer_.delete_mark(self.xep0184_marks[id_])

        buffer_.end_user_action()

        del self.xep0184_marks[id_]
        del self.xep0184_shown[id_]

    def show_focus_out_line(self, scroll=True):
        if not self.allow_focus_out_line:
            # if room did not receive focus-in from the last time we added
            # --- line then do not readd
            return

        print_focus_out_line = False
        buffer_ = self.tv.get_buffer()

        if self.focus_out_end_mark is None:
            # this happens only first time we focus out on this room
            print_focus_out_line = True

        else:
            focus_out_end_iter = buffer_.get_iter_at_mark(self.focus_out_end_mark)
            focus_out_end_iter_offset = focus_out_end_iter.get_offset()
            if focus_out_end_iter_offset != buffer_.get_end_iter().get_offset():
                # this means after last-focus something was printed
                # (else end_iter's offset is the same as before)
                # only then print ---- line (eg. we avoid printing many following
                # ---- lines)
                print_focus_out_line = True

        if print_focus_out_line and buffer_.get_char_count() > 0:
            buffer_.begin_user_action()

            # remove previous focus out line if such focus out line exists
            if self.focus_out_end_mark is not None:
                end_iter_for_previous_line = buffer_.get_iter_at_mark(
                        self.focus_out_end_mark)
                begin_iter_for_previous_line = end_iter_for_previous_line.copy()
                # img_char+1 (the '\n')
                begin_iter_for_previous_line.backward_chars(2)

                # remove focus out line
                buffer_.delete(begin_iter_for_previous_line,
                        end_iter_for_previous_line)
                buffer_.delete_mark(self.focus_out_end_mark)

            # add the new focus out line
            end_iter = buffer_.get_end_iter()
            buffer_.insert(end_iter, '\n')
            buffer_.insert_pixbuf(end_iter,
                    ConversationTextview.FOCUS_OUT_LINE_PIXBUF)

            end_iter = buffer_.get_end_iter()
            before_img_iter = end_iter.copy()
            # one char back (an image also takes one char)
            before_img_iter.backward_char()
            buffer_.apply_tag_by_name('focus-out-line', before_img_iter, end_iter)

            self.allow_focus_out_line = False

            # update the iter we hold to make comparison the next time
            self.focus_out_end_mark = buffer_.create_mark(None,
                    buffer_.get_end_iter(), left_gravity=True)

            buffer_.end_user_action()

            if scroll:
                # scroll to the end (via idle in case the scrollbar has
                # appeared)
                gobject.idle_add(self.scroll_to_end)

    def show_xep0184_warning_tooltip(self):
        pointer = self.tv.get_pointer()
        x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
                pointer[0], pointer[1])
        tags = self.tv.get_iter_at_location(x, y).get_tags()
        tag_table = self.tv.get_buffer().get_tag_table()
        xep0184_warning = False
        for tag in tags:
            if tag == tag_table.lookup('xep0184-warning'):
                xep0184_warning = True
                break
        if xep0184_warning and not self.xep0184_warning_tooltip.win:
            # check if the current pointer is still over the line
            position = self.tv.window.get_origin()
            self.xep0184_warning_tooltip.show_tooltip(_('This icon indicates that '
                    'this message has not yet\nbeen received by the remote end. '
                    "If this icon stays\nfor a long time, it's likely the message got "
                    'lost.'), 8, position[1] + pointer[1])

    def show_line_tooltip(self):
        pointer = self.tv.get_pointer()
        x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
                pointer[0], pointer[1])
        tags = self.tv.get_iter_at_location(x, y).get_tags()
        tag_table = self.tv.get_buffer().get_tag_table()
        over_line = False
        for tag in tags:
            if tag == tag_table.lookup('focus-out-line'):
                over_line = True
                break
        if over_line and not self.line_tooltip.win:
            # check if the current pointer is still over the line
            position = self.tv.window.get_origin()
            self.line_tooltip.show_tooltip(_('Text below this line is what has '
                    'been said since the\nlast time you paid attention to this group '
                    'chat'), 8, position[1] + pointer[1])

    def on_textview_expose_event(self, widget, event):
        expalloc = event.area
        exp_x0 = expalloc.x
        exp_y0 = expalloc.y
        exp_x1 = exp_x0 + expalloc.width
        exp_y1 = exp_y0 + expalloc.height

        try:
            tryfirst = [self.image_cache[(exp_x0, exp_y0)]]
        except KeyError:
            tryfirst = []

        for image in tryfirst + self.images:
            imgalloc = image.allocation
            img_x0 = imgalloc.x
            img_y0 = imgalloc.y
            img_x1 = img_x0 + imgalloc.width
            img_y1 = img_y0 + imgalloc.height

            if img_x0 <= exp_x0 and img_y0 <= exp_y0 and \
            exp_x1 <= img_x1 and exp_y1 <= img_y1:
                self.image_cache[(img_x0, img_y0)] = image
                widget.propagate_expose(image, event)
                return True
        return False

    def on_textview_motion_notify_event(self, widget, event):
        """
        Change the cursor to a hand when we are over a mail or an url
        """
        pointer_x, pointer_y = self.tv.window.get_pointer()[0:2]
        x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
                pointer_x, pointer_y)
        tags = self.tv.get_iter_at_location(x, y).get_tags()
        if self.change_cursor:
            self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
                    gtk.gdk.Cursor(gtk.gdk.XTERM))
            self.change_cursor = False
        tag_table = self.tv.get_buffer().get_tag_table()
        over_line = False
        xep0184_warning = False
        for tag in tags:
            if tag in (tag_table.lookup('url'), tag_table.lookup('mail'), \
            tag_table.lookup('xmpp'), tag_table.lookup('sth_at_sth')):
                self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
                        gtk.gdk.Cursor(gtk.gdk.HAND2))
                self.change_cursor = True
            elif tag == tag_table.lookup('focus-out-line'):
                over_line = True
            elif tag == tag_table.lookup('xep0184-warning'):
                xep0184_warning = True

        if self.line_tooltip.timeout != 0:
            # Check if we should hide the line tooltip
            if not over_line:
                self.line_tooltip.hide_tooltip()
        if self.xep0184_warning_tooltip.timeout != 0:
            # Check if we should hide the XEP-184 warning tooltip
            if not xep0184_warning:
                self.xep0184_warning_tooltip.hide_tooltip()
        if over_line and not self.line_tooltip.win:
            self.line_tooltip.timeout = gobject.timeout_add(500,
                    self.show_line_tooltip)
            self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
                    gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
            self.change_cursor = True
        if xep0184_warning and not self.xep0184_warning_tooltip.win:
            self.xep0184_warning_tooltip.timeout = gobject.timeout_add(500,
                    self.show_xep0184_warning_tooltip)
            self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
                    gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
            self.change_cursor = True

    def clear(self, tv = None):
        """
        Clear text in the textview
        """
        buffer_ = self.tv.get_buffer()
        start, end = buffer_.get_bounds()
        buffer_.delete(start, end)
        size = gajim.config.get('max_conversation_lines')
        size = 2 * size - 1
        self.marks_queue = Queue.Queue(size)
        self.focus_out_end_mark = None

    def visit_url_from_menuitem(self, widget, link):
        """
        Basically it filters out the widget instance
        """
        helpers.launch_browser_mailer('url', link)

    def on_textview_populate_popup(self, textview, menu):
        """
        Override the default context menu and we prepend Clear (only if
        used_in_history_window is False) and if we have sth selected we show a
        submenu with actions on the phrase (see
        on_conversation_textview_button_press_event)
        """
        separator_menuitem_was_added = False
        if not self.used_in_history_window:
            item = gtk.SeparatorMenuItem()
            menu.prepend(item)
            separator_menuitem_was_added = True

            item = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
            menu.prepend(item)
            id_ = item.connect('activate', self.clear)
            self.handlers[id_] = item

        if self.selected_phrase:
            if not separator_menuitem_was_added:
                item = gtk.SeparatorMenuItem()
                menu.prepend(item)

            if not self.used_in_history_window:
                item = gtk.MenuItem(_('_Quote'))
                id_ = item.connect('activate', self.on_quote)
                self.handlers[id_] = item
                menu.prepend(item)

            _selected_phrase = helpers.reduce_chars_newlines(
                    self.selected_phrase, 25, 2)
            item = gtk.MenuItem(_('_Actions for "%s"') % _selected_phrase)
            menu.prepend(item)
            submenu = gtk.Menu()
            item.set_submenu(submenu)
            phrase_for_url = urllib.quote(self.selected_phrase.encode('utf-8'))

            always_use_en = gajim.config.get('always_english_wikipedia')
            if always_use_en:
                link = 'http://en.wikipedia.org/wiki/Special:Search?search=%s'\
                        % phrase_for_url
            else:
                link = 'http://%s.wikipedia.org/wiki/Special:Search?search=%s'\
                        % (gajim.LANG, phrase_for_url)
            item = gtk.MenuItem(_('Read _Wikipedia Article'))
            id_ = item.connect('activate', self.visit_url_from_menuitem, link)
            self.handlers[id_] = item
            submenu.append(item)

            item = gtk.MenuItem(_('Look it up in _Dictionary'))
            dict_link = gajim.config.get('dictionary_url')
            if dict_link == 'WIKTIONARY':
                # special link (yeah undocumented but default)
                always_use_en = gajim.config.get('always_english_wiktionary')
                if always_use_en:
                    link = 'http://en.wiktionary.org/wiki/Special:Search?search=%s'\
                            % phrase_for_url
                else:
                    link = 'http://%s.wiktionary.org/wiki/Special:Search?search=%s'\
                            % (gajim.LANG, phrase_for_url)
                id_ = item.connect('activate', self.visit_url_from_menuitem, link)
                self.handlers[id_] = item
            else:
                if dict_link.find('%s') == -1:
                    # we must have %s in the url if not WIKTIONARY
                    item = gtk.MenuItem(_(
                            'Dictionary URL is missing an "%s" and it is not WIKTIONARY'))
                    item.set_property('sensitive', False)
                else:
                    link = dict_link % phrase_for_url
                    id_ = item.connect('activate', self.visit_url_from_menuitem,
                            link)
                    self.handlers[id_] = item
            submenu.append(item)


            search_link = gajim.config.get('search_engine')
            if search_link.find('%s') == -1:
                # we must have %s in the url
                item = gtk.MenuItem(_('Web Search URL is missing an "%s"'))
                item.set_property('sensitive', False)
            else:
                item = gtk.MenuItem(_('Web _Search for it'))
                link =  search_link % phrase_for_url
                id_ = item.connect('activate', self.visit_url_from_menuitem, link)
                self.handlers[id_] = item
            submenu.append(item)

            item = gtk.MenuItem(_('Open as _Link'))
            id_ = item.connect('activate', self.visit_url_from_menuitem, link)
            self.handlers[id_] = item
            submenu.append(item)

        menu.show_all()

    def on_quote(self, widget):
        self.emit('quote', self.selected_phrase)

    def on_textview_button_press_event(self, widget, event):
        # If we clicked on a taged text do NOT open the standard popup menu
        # if normal text check if we have sth selected
        self.selected_phrase = '' # do not move belove event button check!

        if event.button != 3: # if not right click
            return False

        x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
                int(event.x), int(event.y))
        iter_ = self.tv.get_iter_at_location(x, y)
        tags = iter_.get_tags()


        if tags: # we clicked on sth special (it can be status message too)
            for tag in tags:
                tag_name = tag.get_property('name')
                if tag_name in ('url', 'mail', 'xmpp', 'sth_at_sth'):
                    return True # we block normal context menu

        # we check if sth was selected and if it was we assign
        # selected_phrase variable
        # so on_conversation_textview_populate_popup can use it
        buffer_ = self.tv.get_buffer()
        return_val = buffer_.get_selection_bounds()
        if return_val: # if sth was selected when we right-clicked
            # get the selected text
            start_sel, finish_sel = return_val[0], return_val[1]
            self.selected_phrase = buffer_.get_text(start_sel, finish_sel).decode(
                    'utf-8')
        elif ord(iter_.get_char()) > 31:
            # we clicked on a word, do as if it's selected for context menu
            start_sel = iter_.copy()
            if not start_sel.starts_word():
                start_sel.backward_word_start()
            finish_sel = iter_.copy()
            if not finish_sel.ends_word():
                finish_sel.forward_word_end()
            self.selected_phrase = buffer_.get_text(start_sel, finish_sel).decode(
                    'utf-8')

    def on_open_link_activate(self, widget, kind, text):
        helpers.launch_browser_mailer(kind, text)

    def on_copy_link_activate(self, widget, text):
        clip = gtk.clipboard_get()
        clip.set_text(text)

    def on_start_chat_activate(self, widget, jid):
        gajim.interface.new_chat_from_jid(self.account, jid)

    def on_join_group_chat_menuitem_activate(self, widget, room_jid):
        if 'join_gc' in gajim.interface.instances[self.account]:
            instance = gajim.interface.instances[self.account]['join_gc']
            instance.xml.get_object('room_jid_entry').set_text(room_jid)
            gajim.interface.instances[self.account]['join_gc'].window.present()
        else:
            try:
                dialogs.JoinGroupchatWindow(account=self.account, room_jid=room_jid)
            except GajimGeneralException:
                pass

    def on_add_to_roster_activate(self, widget, jid):
        dialogs.AddNewContactWindow(self.account, jid)

    def make_link_menu(self, event, kind, text):
        xml = gtkgui_helpers.get_gtk_builder('chat_context_menu.ui')
        menu = xml.get_object('chat_context_menu')
        childs = menu.get_children()
        if kind == 'url':
            id_ = childs[0].connect('activate', self.on_copy_link_activate, text)
            self.handlers[id_] = childs[0]
            id_ = childs[1].connect('activate', self.on_open_link_activate, kind,
                    text)
            self.handlers[id_] = childs[1]
            childs[2].hide() # copy mail address
            childs[3].hide() # open mail composer
            childs[4].hide() # jid section separator
            childs[5].hide() # start chat
            childs[6].hide() # join group chat
            childs[7].hide() # add to roster
        else: # It's a mail or a JID
            # load muc icon
            join_group_chat_menuitem = xml.get_object('join_group_chat_menuitem')
            muc_icon = gtkgui_helpers.load_icon('muc_active')
            if muc_icon:
                join_group_chat_menuitem.set_image(muc_icon)

            text = text.lower()
            if text.startswith('xmpp:'):
                text = text[5:]
            id_ = childs[2].connect('activate', self.on_copy_link_activate, text)
            self.handlers[id_] = childs[2]
            id_ = childs[3].connect('activate', self.on_open_link_activate, kind,
                    text)
            self.handlers[id_] = childs[3]
            id_ = childs[5].connect('activate', self.on_start_chat_activate, text)
            self.handlers[id_] = childs[5]
            id_ = childs[6].connect('activate',
                    self.on_join_group_chat_menuitem_activate, text)
            self.handlers[id_] = childs[6]

            if self.account and gajim.connections[self.account].\
            roster_supported:
                id_ = childs[7].connect('activate',
                    self.on_add_to_roster_activate, text)
                self.handlers[id_] = childs[7]
                childs[7].show() # show add to roster menuitem
            else:
                childs[7].hide() # hide add to roster menuitem

            if kind == 'xmpp':
                id_ = childs[0].connect('activate', self.on_copy_link_activate,
                    'xmpp:' + text)
                childs[2].hide() # copy mail address
                childs[3].hide() # open mail composer
                childs[4].hide() # jid section separator
            elif kind == 'mail':
                childs[4].hide() # jid section separator
                childs[5].hide() # start chat
                childs[6].hide() # join group chat
                childs[7].hide() # add to roster

            if kind != 'xmpp':
                childs[0].hide() # copy link location
            childs[1].hide() # open link in browser

        menu.popup(None, None, None, event.button, event.time)

    def hyperlink_handler(self, texttag, widget, event, iter_, kind):
        if event.type == gtk.gdk.BUTTON_PRESS:
            begin_iter = iter_.copy()
            # we get the begining of the tag
            while not begin_iter.begins_tag(texttag):
                begin_iter.backward_char()
            end_iter = iter_.copy()
            # we get the end of the tag
            while not end_iter.ends_tag(texttag):
                end_iter.forward_char()

            # Detect XHTML-IM link
            word = getattr(texttag, 'href', None)
            if word:
                if word.startswith('xmpp'):
                    kind = 'xmpp'
                elif word.startswith('mailto:'):
                    kind = 'mail'
                elif gajim.interface.sth_at_sth_dot_sth_re.match(word):
                    # it's a JID or mail
                    kind = 'sth_at_sth'
            else:
                word = self.tv.get_buffer().get_text(begin_iter, end_iter).decode(
            if event.button == 3: # right click
                self.make_link_menu(event, kind, word)
            else:
                # we launch the correct application
                if kind == 'xmpp':
                    word = word[5:]
                    if '?' in word:
                        (jid, action) = word.split('?')
                        if action == 'join':
                            self.on_join_group_chat_menuitem_activate(None, jid)
                        else:
                            self.on_start_chat_activate(None, jid)
                    else:
                        self.on_start_chat_activate(None, word)
                else:
                    helpers.launch_browser_mailer(kind, word)

    def detect_and_print_special_text(self, otext, other_tags, graphics=True):
        """
        Detect special text (emots & links & formatting), print normal text
        before any special text it founds, then print special text (that happens
        many times until last special text is printed) and then return the index
        after *last* special text, so we can print it in
        print_conversation_line()
        """
        buffer_ = self.tv.get_buffer()

        insert_tags_func = buffer_.insert_with_tags_by_name
        # detect_and_print_special_text() is also used by
        # HtmlHandler.handle_specials() and there tags is gtk.TextTag objects,