| 1 | # -*- coding:utf-8 -*- |
|---|
| 2 | ## src/conversation_textview.py |
|---|
| 3 | ## |
|---|
| 4 | ## Copyright (C) 2005 Norman Rasmussen <norman AT rasmussen.co.za> |
|---|
| 5 | ## Copyright (C) 2005-2006 Alex Mauer <hawke AT hawkesnest.net> |
|---|
| 6 | ## Travis Shirk <travis AT pobox.com> |
|---|
| 7 | ## Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com> |
|---|
| 8 | ## Copyright (C) 2005-2008 Yann Leboulanger <asterix AT lagaule.org> |
|---|
| 9 | ## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com> |
|---|
| 10 | ## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org> |
|---|
| 11 | ## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org> |
|---|
| 12 | ## Julien Pivotto <roidelapluie AT gmail.com> |
|---|
| 13 | ## Stephan Erb <steve-e AT h3c.de> |
|---|
| 14 | ## |
|---|
| 15 | ## This file is part of Gajim. |
|---|
| 16 | ## |
|---|
| 17 | ## Gajim is free software; you can redistribute it and/or modify |
|---|
| 18 | ## it under the terms of the GNU General Public License as published |
|---|
| 19 | ## by the Free Software Foundation; version 3 only. |
|---|
| 20 | ## |
|---|
| 21 | ## Gajim is distributed in the hope that it will be useful, |
|---|
| 22 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 23 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 24 | ## GNU General Public License for more details. |
|---|
| 25 | ## |
|---|
| 26 | ## You should have received a copy of the GNU General Public License |
|---|
| 27 | ## along with Gajim. If not, see <http://www.gnu.org/licenses/>. |
|---|
| 28 | ## |
|---|
| 29 | |
|---|
| 30 | import random |
|---|
| 31 | from tempfile import gettempdir |
|---|
| 32 | from subprocess import Popen |
|---|
| 33 | from threading import Timer # for smooth scrolling |
|---|
| 34 | |
|---|
| 35 | import gtk |
|---|
| 36 | import pango |
|---|
| 37 | import gobject |
|---|
| 38 | import time |
|---|
| 39 | import os |
|---|
| 40 | import tooltips |
|---|
| 41 | import dialogs |
|---|
| 42 | import locale |
|---|
| 43 | import Queue |
|---|
| 44 | |
|---|
| 45 | import gtkgui_helpers |
|---|
| 46 | from common import gajim |
|---|
| 47 | from common import helpers |
|---|
| 48 | from calendar import timegm |
|---|
| 49 | from common.fuzzyclock import FuzzyClock |
|---|
| 50 | |
|---|
| 51 | from htmltextview import HtmlTextView |
|---|
| 52 | from common.exceptions import GajimGeneralException |
|---|
| 53 | from common.exceptions import LatexError |
|---|
| 54 | |
|---|
| 55 | NOT_SHOWN = 0 |
|---|
| 56 | ALREADY_RECEIVED = 1 |
|---|
| 57 | SHOWN = 2 |
|---|
| 58 | |
|---|
| 59 | def is_selection_modified(mark): |
|---|
| 60 | name = mark.get_name() |
|---|
| 61 | if name and name in ('selection_bound', 'insert'): |
|---|
| 62 | return True |
|---|
| 63 | else: |
|---|
| 64 | return False |
|---|
| 65 | |
|---|
| 66 | def has_focus(widget): |
|---|
| 67 | return widget.flags() & gtk.HAS_FOCUS == gtk.HAS_FOCUS |
|---|
| 68 | |
|---|
| 69 | class TextViewImage(gtk.Image): |
|---|
| 70 | |
|---|
| 71 | def __init__(self, anchor): |
|---|
| 72 | super(TextViewImage, self).__init__() |
|---|
| 73 | self.anchor = anchor |
|---|
| 74 | self._selected = False |
|---|
| 75 | self._disconnect_funcs = [] |
|---|
| 76 | self.connect('parent-set', self.on_parent_set) |
|---|
| 77 | self.connect('expose-event', self.on_expose) |
|---|
| 78 | |
|---|
| 79 | def _get_selected(self): |
|---|
| 80 | parent = self.get_parent() |
|---|
| 81 | if not parent or not self.anchor: return False |
|---|
| 82 | buffer = parent.get_buffer() |
|---|
| 83 | position = buffer.get_iter_at_child_anchor(self.anchor) |
|---|
| 84 | bounds = buffer.get_selection_bounds() |
|---|
| 85 | if bounds and position.in_range(*bounds): |
|---|
| 86 | return True |
|---|
| 87 | else: |
|---|
| 88 | return False |
|---|
| 89 | |
|---|
| 90 | def get_state(self): |
|---|
| 91 | parent = self.get_parent() |
|---|
| 92 | if not parent: |
|---|
| 93 | return gtk.STATE_NORMAL |
|---|
| 94 | if self._selected: |
|---|
| 95 | if has_focus(parent): |
|---|
| 96 | return gtk.STATE_SELECTED |
|---|
| 97 | else: |
|---|
| 98 | return gtk.STATE_ACTIVE |
|---|
| 99 | else: |
|---|
| 100 | return gtk.STATE_NORMAL |
|---|
| 101 | |
|---|
| 102 | def _update_selected(self): |
|---|
| 103 | selected = self._get_selected() |
|---|
| 104 | if self._selected != selected: |
|---|
| 105 | self._selected = selected |
|---|
| 106 | self.queue_draw() |
|---|
| 107 | |
|---|
| 108 | def _do_connect(self, widget, signal, callback): |
|---|
| 109 | id = widget.connect(signal, callback) |
|---|
| 110 | def disconnect(): |
|---|
| 111 | widget.disconnect(id) |
|---|
| 112 | self._disconnect_funcs.append(disconnect) |
|---|
| 113 | |
|---|
| 114 | def _disconnect_signals(self): |
|---|
| 115 | for func in self._disconnect_funcs: |
|---|
| 116 | func() |
|---|
| 117 | self._disconnect_funcs = [] |
|---|
| 118 | |
|---|
| 119 | def on_parent_set(self, widget, old_parent): |
|---|
| 120 | parent = self.get_parent() |
|---|
| 121 | if not parent: |
|---|
| 122 | self._disconnect_signals() |
|---|
| 123 | return |
|---|
| 124 | |
|---|
| 125 | self._do_connect(parent, 'style-set', self.do_queue_draw) |
|---|
| 126 | self._do_connect(parent, 'focus-in-event', self.do_queue_draw) |
|---|
| 127 | self._do_connect(parent, 'focus-out-event', self.do_queue_draw) |
|---|
| 128 | |
|---|
| 129 | textbuf = parent.get_buffer() |
|---|
| 130 | self._do_connect(textbuf, 'mark-set', self.on_mark_set) |
|---|
| 131 | self._do_connect(textbuf, 'mark-deleted', self.on_mark_deleted) |
|---|
| 132 | |
|---|
| 133 | def do_queue_draw(self, *args): |
|---|
| 134 | self.queue_draw() |
|---|
| 135 | return False |
|---|
| 136 | |
|---|
| 137 | def on_mark_set(self, buf, iterat, mark): |
|---|
| 138 | self.on_mark_modified(mark) |
|---|
| 139 | return False |
|---|
| 140 | |
|---|
| 141 | def on_mark_deleted(self, buf, mark): |
|---|
| 142 | self.on_mark_modified(mark) |
|---|
| 143 | return False |
|---|
| 144 | |
|---|
| 145 | def on_mark_modified(self, mark): |
|---|
| 146 | if is_selection_modified(mark): |
|---|
| 147 | self._update_selected() |
|---|
| 148 | |
|---|
| 149 | def on_expose(self, widget, event): |
|---|
| 150 | state = self.get_state() |
|---|
| 151 | if state != gtk.STATE_NORMAL: |
|---|
| 152 | gc = widget.get_style().base_gc[state] |
|---|
| 153 | area = widget.allocation |
|---|
| 154 | widget.window.draw_rectangle(gc, True, area.x, area.y, |
|---|
| 155 | area.width, area.height) |
|---|
| 156 | return False |
|---|
| 157 | |
|---|
| 158 | |
|---|
| 159 | class ConversationTextview: |
|---|
| 160 | '''Class for the conversation textview (where user reads already said |
|---|
| 161 | messages) for chat/groupchat windows''' |
|---|
| 162 | |
|---|
| 163 | FOCUS_OUT_LINE_PIXBUF = gtk.gdk.pixbuf_new_from_file(os.path.join( |
|---|
| 164 | gajim.DATA_DIR, 'pixmaps', 'muc_separator.png')) |
|---|
| 165 | XEP0184_WARNING_PIXBUF = gtk.gdk.pixbuf_new_from_file(os.path.join( |
|---|
| 166 | gajim.DATA_DIR, 'pixmaps', 'receipt_missing.png')) |
|---|
| 167 | |
|---|
| 168 | # smooth scroll constants |
|---|
| 169 | MAX_SCROLL_TIME = 0.4 # seconds |
|---|
| 170 | SCROLL_DELAY = 33 # milliseconds |
|---|
| 171 | |
|---|
| 172 | def __init__(self, account, used_in_history_window = False): |
|---|
| 173 | '''if used_in_history_window is True, then we do not show |
|---|
| 174 | Clear menuitem in context menu''' |
|---|
| 175 | self.used_in_history_window = used_in_history_window |
|---|
| 176 | |
|---|
| 177 | # no need to inherit TextView, use it as atrribute is safer |
|---|
| 178 | self.tv = HtmlTextView() |
|---|
| 179 | self.tv.html_hyperlink_handler = self.html_hyperlink_handler |
|---|
| 180 | |
|---|
| 181 | # set properties |
|---|
| 182 | self.tv.set_border_width(1) |
|---|
| 183 | self.tv.set_accepts_tab(True) |
|---|
| 184 | self.tv.set_editable(False) |
|---|
| 185 | self.tv.set_cursor_visible(False) |
|---|
| 186 | self.tv.set_wrap_mode(gtk.WRAP_WORD_CHAR) |
|---|
| 187 | self.tv.set_left_margin(2) |
|---|
| 188 | self.tv.set_right_margin(2) |
|---|
| 189 | self.handlers = {} |
|---|
| 190 | self.images = [] |
|---|
| 191 | self.image_cache = {} |
|---|
| 192 | self.xep0184_marks = {} |
|---|
| 193 | self.xep0184_shown = {} |
|---|
| 194 | |
|---|
| 195 | # It's True when we scroll in the code, so we can detect scroll from user |
|---|
| 196 | self.auto_scrolling = False |
|---|
| 197 | |
|---|
| 198 | # connect signals |
|---|
| 199 | id = self.tv.connect('motion_notify_event', |
|---|
| 200 | self.on_textview_motion_notify_event) |
|---|
| 201 | self.handlers[id] = self.tv |
|---|
| 202 | id = self.tv.connect('populate_popup', self.on_textview_populate_popup) |
|---|
| 203 | self.handlers[id] = self.tv |
|---|
| 204 | id = self.tv.connect('button_press_event', |
|---|
| 205 | self.on_textview_button_press_event) |
|---|
| 206 | self.handlers[id] = self.tv |
|---|
| 207 | |
|---|
| 208 | id = self.tv.connect('expose-event', |
|---|
| 209 | self.on_textview_expose_event) |
|---|
| 210 | self.handlers[id] = self.tv |
|---|
| 211 | |
|---|
| 212 | |
|---|
| 213 | self.account = account |
|---|
| 214 | self.change_cursor = None |
|---|
| 215 | self.last_time_printout = 0 |
|---|
| 216 | |
|---|
| 217 | font = pango.FontDescription(gajim.config.get('conversation_font')) |
|---|
| 218 | self.tv.modify_font(font) |
|---|
| 219 | buffer = self.tv.get_buffer() |
|---|
| 220 | end_iter = buffer.get_end_iter() |
|---|
| 221 | buffer.create_mark('end', end_iter, False) |
|---|
| 222 | |
|---|
| 223 | self.tagIn = buffer.create_tag('incoming') |
|---|
| 224 | color = gajim.config.get('inmsgcolor') |
|---|
| 225 | self.tagIn.set_property('foreground', color) |
|---|
| 226 | self.tagOut = buffer.create_tag('outgoing') |
|---|
| 227 | color = gajim.config.get('outmsgcolor') |
|---|
| 228 | self.tagOut.set_property('foreground', color) |
|---|
| 229 | self.tagStatus = buffer.create_tag('status') |
|---|
| 230 | color = gajim.config.get('statusmsgcolor') |
|---|
| 231 | self.tagStatus.set_property('foreground', color) |
|---|
| 232 | |
|---|
| 233 | colors = gajim.config.get('gc_nicknames_colors') |
|---|
| 234 | colors = colors.split(':') |
|---|
| 235 | for i,color in enumerate(colors): |
|---|
| 236 | tagname = 'gc_nickname_color_' + str(i) |
|---|
| 237 | tag = buffer.create_tag(tagname) |
|---|
| 238 | tag.set_property('foreground', color) |
|---|
| 239 | |
|---|
| 240 | tag = buffer.create_tag('marked') |
|---|
| 241 | color = gajim.config.get('markedmsgcolor') |
|---|
| 242 | tag.set_property('foreground', color) |
|---|
| 243 | tag.set_property('weight', pango.WEIGHT_BOLD) |
|---|
| 244 | |
|---|
| 245 | tag = buffer.create_tag('time_sometimes') |
|---|
| 246 | tag.set_property('foreground', 'darkgrey') |
|---|
| 247 | tag.set_property('scale', pango.SCALE_SMALL) |
|---|
| 248 | tag.set_property('justification', gtk.JUSTIFY_CENTER) |
|---|
| 249 | |
|---|
| 250 | tag = buffer.create_tag('small') |
|---|
| 251 | tag.set_property('scale', pango.SCALE_SMALL) |
|---|
| 252 | |
|---|
| 253 | tag = buffer.create_tag('restored_message') |
|---|
| 254 | color = gajim.config.get('restored_messages_color') |
|---|
| 255 | tag.set_property('foreground', color) |
|---|
| 256 | |
|---|
| 257 | self.tagURL = buffer.create_tag('url') |
|---|
| 258 | color = gajim.config.get('urlmsgcolor') |
|---|
| 259 | self.tagURL.set_property('foreground', color) |
|---|
| 260 | self.tagURL.set_property('underline', pango.UNDERLINE_SINGLE) |
|---|
| 261 | id = self.tagURL.connect('event', self.hyperlink_handler, 'url') |
|---|
| 262 | self.handlers[id] = self.tagURL |
|---|
| 263 | |
|---|
| 264 | self.tagMail = buffer.create_tag('mail') |
|---|
| 265 | self.tagMail.set_property('foreground', color) |
|---|
| 266 | self.tagMail.set_property('underline', pango.UNDERLINE_SINGLE) |
|---|
| 267 | id = self.tagMail.connect('event', self.hyperlink_handler, 'mail') |
|---|
| 268 | self.handlers[id] = self.tagMail |
|---|
| 269 | |
|---|
| 270 | self.tagXMPP = buffer.create_tag('xmpp') |
|---|
| 271 | self.tagXMPP.set_property('foreground', color) |
|---|
| 272 | self.tagXMPP.set_property('underline', pango.UNDERLINE_SINGLE) |
|---|
| 273 | id = self.tagXMPP.connect('event', self.hyperlink_handler, 'xmpp') |
|---|
| 274 | self.handlers[id] = self.tagXMPP |
|---|
| 275 | |
|---|
| 276 | self.tagSthAtSth = buffer.create_tag('sth_at_sth') |
|---|
| 277 | self.tagSthAtSth.set_property('foreground', color) |
|---|
| 278 | self.tagSthAtSth.set_property('underline', pango.UNDERLINE_SINGLE) |
|---|
| 279 | id = self.tagSthAtSth.connect('event', self.hyperlink_handler, |
|---|
| 280 | 'sth_at_sth') |
|---|
| 281 | self.handlers[id] = self.tagSthAtSth |
|---|
| 282 | |
|---|
| 283 | tag = buffer.create_tag('bold') |
|---|
| 284 | tag.set_property('weight', pango.WEIGHT_BOLD) |
|---|
| 285 | |
|---|
| 286 | tag = buffer.create_tag('italic') |
|---|
| 287 | tag.set_property('style', pango.STYLE_ITALIC) |
|---|
| 288 | |
|---|
| 289 | tag = buffer.create_tag('underline') |
|---|
| 290 | tag.set_property('underline', pango.UNDERLINE_SINGLE) |
|---|
| 291 | |
|---|
| 292 | buffer.create_tag('focus-out-line', justification = gtk.JUSTIFY_CENTER) |
|---|
| 293 | |
|---|
| 294 | tag = buffer.create_tag('xep0184-warning') |
|---|
| 295 | |
|---|
| 296 | # One mark at the begining then 2 marks between each lines |
|---|
| 297 | size = gajim.config.get('max_conversation_lines') |
|---|
| 298 | size = 2 * size - 1 |
|---|
| 299 | self.marks_queue = Queue.Queue(size) |
|---|
| 300 | |
|---|
| 301 | self.allow_focus_out_line = True |
|---|
| 302 | # holds a mark at the end of --- line |
|---|
| 303 | self.focus_out_end_mark = None |
|---|
| 304 | |
|---|
| 305 | self.xep0184_warning_tooltip = tooltips.BaseTooltip() |
|---|
| 306 | |
|---|
| 307 | self.line_tooltip = tooltips.BaseTooltip() |
|---|
| 308 | # use it for hr too |
|---|
| 309 | self.tv.focus_out_line_pixbuf = ConversationTextview.FOCUS_OUT_LINE_PIXBUF |
|---|
| 310 | self.smooth_id = None |
|---|
| 311 | |
|---|
| 312 | def del_handlers(self): |
|---|
| 313 | for i in self.handlers.keys(): |
|---|
| 314 | if self.handlers[i].handler_is_connected(i): |
|---|
| 315 | self.handlers[i].disconnect(i) |
|---|
| 316 | del self.handlers |
|---|
| 317 | self.tv.destroy() |
|---|
| 318 | #FIXME: |
|---|
| 319 | # self.line_tooltip.destroy() |
|---|
| 320 | |
|---|
| 321 | def update_tags(self): |
|---|
| 322 | self.tagIn.set_property('foreground', gajim.config.get('inmsgcolor')) |
|---|
| 323 | self.tagOut.set_property('foreground', gajim.config.get('outmsgcolor')) |
|---|
| 324 | self.tagStatus.set_property('foreground', |
|---|
| 325 | gajim.config.get('statusmsgcolor')) |
|---|
| 326 | self.tagURL.set_property('foreground', gajim.config.get('urlmsgcolor')) |
|---|
| 327 | self.tagMail.set_property('foreground', gajim.config.get('urlmsgcolor')) |
|---|
| 328 | |
|---|
| 329 | def at_the_end(self): |
|---|
| 330 | buffer = self.tv.get_buffer() |
|---|
| 331 | end_iter = buffer.get_end_iter() |
|---|
| 332 | end_rect = self.tv.get_iter_location(end_iter) |
|---|
| 333 | visible_rect = self.tv.get_visible_rect() |
|---|
| 334 | if end_rect.y <= (visible_rect.y + visible_rect.height): |
|---|
| 335 | return True |
|---|
| 336 | return False |
|---|
| 337 | |
|---|
| 338 | # Smooth scrolling inspired by Pidgin code |
|---|
| 339 | def smooth_scroll(self): |
|---|
| 340 | parent = self.tv.get_parent() |
|---|
| 341 | if not parent: |
|---|
| 342 | return False |
|---|
| 343 | vadj = parent.get_vadjustment() |
|---|
| 344 | max_val = vadj.upper - vadj.page_size + 1 |
|---|
| 345 | cur_val = vadj.get_value() |
|---|
| 346 | # scroll by 1/3rd of remaining distance |
|---|
| 347 | onethird = cur_val + ((max_val - cur_val) / 3.0) |
|---|
| 348 | self.auto_scrolling = True |
|---|
| 349 | vadj.set_value(onethird) |
|---|
| 350 | self.auto_scrolling = False |
|---|
| 351 | if max_val - onethird < 0.01: |
|---|
| 352 | self.smooth_id = None |
|---|
| 353 | self.smooth_scroll_timer.cancel() |
|---|
| 354 | return False |
|---|
| 355 | return True |
|---|
| 356 | |
|---|
| 357 | def smooth_scroll_timeout(self): |
|---|
| 358 | gobject.idle_add(self.do_smooth_scroll_timeout) |
|---|
| 359 | return |
|---|
| 360 | |
|---|
| 361 | def do_smooth_scroll_timeout(self): |
|---|
| 362 | if not self.smooth_id: |
|---|
| 363 | # we finished scrolling |
|---|
| 364 | return |
|---|
| 365 | gobject.source_remove(self.smooth_id) |
|---|
| 366 | self.smooth_id = None |
|---|
| 367 | parent = self.tv.get_parent() |
|---|
| 368 | if parent: |
|---|
| 369 | vadj = parent.get_vadjustment() |
|---|
| 370 | self.auto_scrolling = True |
|---|
| 371 | vadj.set_value(vadj.upper - vadj.page_size + 1) |
|---|
| 372 | self.auto_scrolling = False |
|---|
| 373 | |
|---|
| 374 | def smooth_scroll_to_end(self): |
|---|
| 375 | if None != self.smooth_id: # already scrolling |
|---|
| 376 | return False |
|---|
| 377 | self.smooth_id = gobject.timeout_add(self.SCROLL_DELAY, |
|---|
| 378 | self.smooth_scroll) |
|---|
| 379 | self.smooth_scroll_timer = Timer(self.MAX_SCROLL_TIME, |
|---|
| 380 | self.smooth_scroll_timeout) |
|---|
| 381 | self.smooth_scroll_timer.start() |
|---|
| 382 | return False |
|---|
| 383 | |
|---|
| 384 | def scroll_to_end(self): |
|---|
| 385 | parent = self.tv.get_parent() |
|---|
| 386 | buffer = self.tv.get_buffer() |
|---|
| 387 | end_mark = buffer.get_mark('end') |
|---|
| 388 | if not end_mark: |
|---|
| 389 | return False |
|---|
| 390 | self.auto_scrolling = True |
|---|
| 391 | self.tv.scroll_to_mark(end_mark, 0, True, 0, 1) |
|---|
| 392 | adjustment = parent.get_hadjustment() |
|---|
| 393 | adjustment.set_value(0) |
|---|
| 394 | self.auto_scrolling = False |
|---|
| 395 | return False # when called in an idle_add, just do it once |
|---|
| 396 | |
|---|
| 397 | def bring_scroll_to_end(self, diff_y = 0, |
|---|
| 398 | use_smooth=gajim.config.get('use_smooth_scrolling')): |
|---|
| 399 | ''' scrolls to the end of textview if end is not visible ''' |
|---|
| 400 | buffer = self.tv.get_buffer() |
|---|
| 401 | end_iter = buffer.get_end_iter() |
|---|
| 402 | end_rect = self.tv.get_iter_location(end_iter) |
|---|
| 403 | visible_rect = self.tv.get_visible_rect() |
|---|
| 404 | # scroll only if expected end is not visible |
|---|
| 405 | if end_rect.y >= (visible_rect.y + visible_rect.height + diff_y): |
|---|
| 406 | if use_smooth: |
|---|
| 407 | gobject.idle_add(self.smooth_scroll_to_end) |
|---|
| 408 | else: |
|---|
| 409 | gobject.idle_add(self.scroll_to_end_iter) |
|---|
| 410 | |
|---|
| 411 | def scroll_to_end_iter(self): |
|---|
| 412 | buffer = self.tv.get_buffer() |
|---|
| 413 | end_iter = buffer.get_end_iter() |
|---|
| 414 | if not end_iter: |
|---|
| 415 | return False |
|---|
| 416 | self.tv.scroll_to_iter(end_iter, 0, False, 1, 1) |
|---|
| 417 | return False # when called in an idle_add, just do it once |
|---|
| 418 | |
|---|
| 419 | def stop_scrolling(self): |
|---|
| 420 | if self.smooth_id: |
|---|
| 421 | gobject.source_remove(self.smooth_id) |
|---|
| 422 | self.smooth_id = None |
|---|
| 423 | self.smooth_scroll_timer.cancel() |
|---|
| 424 | |
|---|
| 425 | def show_xep0184_warning(self, id_): |
|---|
| 426 | if id_ in self.xep0184_marks: |
|---|
| 427 | return |
|---|
| 428 | |
|---|
| 429 | buffer = self.tv.get_buffer() |
|---|
| 430 | buffer.begin_user_action() |
|---|
| 431 | |
|---|
| 432 | self.xep0184_marks[id_] = buffer.create_mark(None, |
|---|
| 433 | buffer.get_end_iter(), left_gravity=True) |
|---|
| 434 | self.xep0184_shown[id_] = NOT_SHOWN |
|---|
| 435 | |
|---|
| 436 | def show_it(): |
|---|
| 437 | if (not id_ in self.xep0184_shown) or \ |
|---|
| 438 | self.xep0184_shown[id_] == ALREADY_RECEIVED: |
|---|
| 439 | return False |
|---|
| 440 | |
|---|
| 441 | end_iter = buffer.get_iter_at_mark( |
|---|
| 442 | self.xep0184_marks[id_]) |
|---|
| 443 | buffer.insert(end_iter, ' ') |
|---|
| 444 | buffer.insert_pixbuf(end_iter, |
|---|
| 445 | ConversationTextview.XEP0184_WARNING_PIXBUF) |
|---|
| 446 | before_img_iter = buffer.get_iter_at_mark( |
|---|
| 447 | self.xep0184_marks[id_]) |
|---|
| 448 | before_img_iter.forward_char() |
|---|
| 449 | post_img_iter = before_img_iter.copy() |
|---|
| 450 | post_img_iter.forward_char() |
|---|
| 451 | buffer.apply_tag_by_name('xep0184-warning', before_img_iter, |
|---|
| 452 | post_img_iter) |
|---|
| 453 | |
|---|
| 454 | self.xep0184_shown[id_] = SHOWN |
|---|
| 455 | return False |
|---|
| 456 | gobject.timeout_add_seconds(3, show_it) |
|---|
| 457 | |
|---|
| 458 | buffer.end_user_action() |
|---|
| 459 | |
|---|
| 460 | def hide_xep0184_warning(self, id_): |
|---|
| 461 | if id_ not in self.xep0184_marks: |
|---|
| 462 | return |
|---|
| 463 | |
|---|
| 464 | if self.xep0184_shown[id_] == NOT_SHOWN: |
|---|
| 465 | self.xep0184_shown[id_] = ALREADY_RECEIVED |
|---|
| 466 | return |
|---|
| 467 | |
|---|
| 468 | buffer = self.tv.get_buffer() |
|---|
| 469 | buffer.begin_user_action() |
|---|
| 470 | |
|---|
| 471 | begin_iter = buffer.get_iter_at_mark(self.xep0184_marks[id_]) |
|---|
| 472 | |
|---|
| 473 | end_iter = begin_iter.copy() |
|---|
| 474 | # XXX: Is there a nicer way? |
|---|
| 475 | end_iter.forward_char() |
|---|
| 476 | end_iter.forward_char() |
|---|
| 477 | |
|---|
| 478 | buffer.delete(begin_iter, end_iter) |
|---|
| 479 | buffer.delete_mark(self.xep0184_marks[id_]) |
|---|
| 480 | |
|---|
| 481 | buffer.end_user_action() |
|---|
| 482 | |
|---|
| 483 | del self.xep0184_marks[id_] |
|---|
| 484 | del self.xep0184_shown[id_] |
|---|
| 485 | |
|---|
| 486 | def show_focus_out_line(self): |
|---|
| 487 | if not self.allow_focus_out_line: |
|---|
| 488 | # if room did not receive focus-in from the last time we added |
|---|
| 489 | # --- line then do not readd |
|---|
| 490 | return |
|---|
| 491 | |
|---|
| 492 | print_focus_out_line = False |
|---|
| 493 | buffer = self.tv.get_buffer() |
|---|
| 494 | |
|---|
| 495 | if self.focus_out_end_mark is None: |
|---|
| 496 | # this happens only first time we focus out on this room |
|---|
| 497 | print_focus_out_line = True |
|---|
| 498 | |
|---|
| 499 | else: |
|---|
| 500 | focus_out_end_iter = buffer.get_iter_at_mark(self.focus_out_end_mark) |
|---|
| 501 | focus_out_end_iter_offset = focus_out_end_iter.get_offset() |
|---|
| 502 | if focus_out_end_iter_offset != buffer.get_end_iter().get_offset(): |
|---|
| 503 | # this means after last-focus something was printed |
|---|
| 504 | # (else end_iter's offset is the same as before) |
|---|
| 505 | # only then print ---- line (eg. we avoid printing many following |
|---|
| 506 | # ---- lines) |
|---|
| 507 | print_focus_out_line = True |
|---|
| 508 | |
|---|
| 509 | if print_focus_out_line and buffer.get_char_count() > 0: |
|---|
| 510 | buffer.begin_user_action() |
|---|
| 511 | |
|---|
| 512 | # remove previous focus out line if such focus out line exists |
|---|
| 513 | if self.focus_out_end_mark is not None: |
|---|
| 514 | end_iter_for_previous_line = buffer.get_iter_at_mark( |
|---|
| 515 | self.focus_out_end_mark) |
|---|
| 516 | begin_iter_for_previous_line = end_iter_for_previous_line.copy() |
|---|
| 517 | # img_char+1 (the '\n') |
|---|
| 518 | begin_iter_for_previous_line.backward_chars(2) |
|---|
| 519 | |
|---|
| 520 | # remove focus out line |
|---|
| 521 | buffer.delete(begin_iter_for_previous_line, |
|---|
| 522 | end_iter_for_previous_line) |
|---|
| 523 | buffer.delete_mark(self.focus_out_end_mark) |
|---|
| 524 | |
|---|
| 525 | # add the new focus out line |
|---|
| 526 | end_iter = buffer.get_end_iter() |
|---|
| 527 | buffer.insert(end_iter, '\n') |
|---|
| 528 | buffer.insert_pixbuf(end_iter, |
|---|
| 529 | ConversationTextview.FOCUS_OUT_LINE_PIXBUF) |
|---|
| 530 | |
|---|
| 531 | end_iter = buffer.get_end_iter() |
|---|
| 532 | before_img_iter = end_iter.copy() |
|---|
| 533 | # one char back (an image also takes one char) |
|---|
| 534 | before_img_iter.backward_char() |
|---|
| 535 | buffer.apply_tag_by_name('focus-out-line', before_img_iter, end_iter) |
|---|
| 536 | |
|---|
| 537 | self.allow_focus_out_line = False |
|---|
| 538 | |
|---|
| 539 | # update the iter we hold to make comparison the next time |
|---|
| 540 | self.focus_out_end_mark = buffer.create_mark(None, |
|---|
| 541 | buffer.get_end_iter(), left_gravity=True) |
|---|
| 542 | |
|---|
| 543 | buffer.end_user_action() |
|---|
| 544 | |
|---|
| 545 | # scroll to the end (via idle in case the scrollbar has appeared) |
|---|
| 546 | gobject.idle_add(self.scroll_to_end) |
|---|
| 547 | |
|---|
| 548 | def show_xep0184_warning_tooltip(self): |
|---|
| 549 | pointer = self.tv.get_pointer() |
|---|
| 550 | x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, |
|---|
| 551 | pointer[0], pointer[1]) |
|---|
| 552 | tags = self.tv.get_iter_at_location(x, y).get_tags() |
|---|
| 553 | tag_table = self.tv.get_buffer().get_tag_table() |
|---|
| 554 | xep0184_warning = False |
|---|
| 555 | for tag in tags: |
|---|
| 556 | if tag == tag_table.lookup('xep0184-warning'): |
|---|
| 557 | xep0184_warning = True |
|---|
| 558 | break |
|---|
| 559 | if xep0184_warning and not self.xep0184_warning_tooltip.win: |
|---|
| 560 | # check if the current pointer is still over the line |
|---|
| 561 | position = self.tv.window.get_origin() |
|---|
| 562 | self.xep0184_warning_tooltip.show_tooltip(_('This icon indicates that ' |
|---|
| 563 | 'this message has not yet\nbeen received by the remote end. ' |
|---|
| 564 | "If this icon stays\nfor a long time, it's likely the message got " |
|---|
| 565 | 'lost.'), 8, position[1] + pointer[1]) |
|---|
| 566 | |
|---|
| 567 | def show_line_tooltip(self): |
|---|
| 568 | pointer = self.tv.get_pointer() |
|---|
| 569 | x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, |
|---|
| 570 | pointer[0], pointer[1]) |
|---|
| 571 | tags = self.tv.get_iter_at_location(x, y).get_tags() |
|---|
| 572 | tag_table = self.tv.get_buffer().get_tag_table() |
|---|
| 573 | over_line = False |
|---|
| 574 | for tag in tags: |
|---|
| 575 | if tag == tag_table.lookup('focus-out-line'): |
|---|
| 576 | over_line = True |
|---|
| 577 | break |
|---|
| 578 | if over_line and not self.line_tooltip.win: |
|---|
| 579 | # check if the current pointer is still over the line |
|---|
| 580 | position = self.tv.window.get_origin() |
|---|
| 581 | self.line_tooltip.show_tooltip(_('Text below this line is what has ' |
|---|
| 582 | 'been said since the\nlast time you paid attention to this group ' |
|---|
| 583 | 'chat'), 8, position[1] + pointer[1]) |
|---|
| 584 | |
|---|
| 585 | def on_textview_expose_event(self, widget, event): |
|---|
| 586 | expalloc = event.area |
|---|
| 587 | exp_x0 = expalloc.x |
|---|
| 588 | exp_y0 = expalloc.y |
|---|
| 589 | exp_x1 = exp_x0 + expalloc.width |
|---|
| 590 | exp_y1 = exp_y0 + expalloc.height |
|---|
| 591 | |
|---|
| 592 | try: |
|---|
| 593 | tryfirst = [self.image_cache[(exp_x0, exp_y0)]] |
|---|
| 594 | except KeyError: |
|---|
| 595 | tryfirst = [] |
|---|
| 596 | |
|---|
| 597 | for image in tryfirst + self.images: |
|---|
| 598 | imgalloc = image.allocation |
|---|
| 599 | img_x0 = imgalloc.x |
|---|
| 600 | img_y0 = imgalloc.y |
|---|
| 601 | img_x1 = img_x0 + imgalloc.width |
|---|
| 602 | img_y1 = img_y0 + imgalloc.height |
|---|
| 603 | |
|---|
| 604 | if img_x0 <= exp_x0 and img_y0 <= exp_y0 and \ |
|---|
| 605 | exp_x1 <= img_x1 and exp_y1 <= img_y1: |
|---|
| 606 | self.image_cache[(img_x0, img_y0)] = image |
|---|
| 607 | widget.propagate_expose(image, event) |
|---|
| 608 | return True |
|---|
| 609 | return False |
|---|
| 610 | |
|---|
| 611 | def on_textview_motion_notify_event(self, widget, event): |
|---|
| 612 | '''change the cursor to a hand when we are over a mail or an |
|---|
| 613 | url''' |
|---|
| 614 | pointer_x, pointer_y, spam = self.tv.window.get_pointer() |
|---|
| 615 | x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, |
|---|
| 616 | pointer_x, pointer_y) |
|---|
| 617 | tags = self.tv.get_iter_at_location(x, y).get_tags() |
|---|
| 618 | if self.change_cursor: |
|---|
| 619 | self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor( |
|---|
| 620 | gtk.gdk.Cursor(gtk.gdk.XTERM)) |
|---|
| 621 | self.change_cursor = None |
|---|
| 622 | tag_table = self.tv.get_buffer().get_tag_table() |
|---|
| 623 | over_line = False |
|---|
| 624 | xep0184_warning = False |
|---|
|
|---|