conversation_textview.py 38.4 KB
Newer Older
1 2
##	conversation_textview.py
##
Yann Leboulanger's avatar
Yann Leboulanger committed
3
## Copyright (C) 2005-2006 Yann Leboulanger <asterix@lagaule.org>
4 5
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
## Copyright (C) 2005-2006 Travis Shirk <travis@pobox.com>
6
##
7 8 9
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
10
## it under the terms of the GNU General Public License as published
11
## by the Free Software Foundation; version 3 only.
12
##
13
## Gajim is distributed in the hope that it will be useful,
14
## but WITHOUT ANY WARRANTY; without even the implied warranty of
15
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the
16 17
## GNU General Public License for more details.
##
18 19 20
## You should have received a copy of the GNU General Public License
## along with Gajim.  If not, see <http://www.gnu.org/licenses/>.
##
21

22 23 24
import random
from tempfile import gettempdir
from subprocess import Popen
25
from threading import Timer # for smooth scrolling
26

27 28 29 30
import gtk
import pango
import gobject
import time
31
import os
32 33
import tooltips
import dialogs
Yann Leboulanger's avatar
Yann Leboulanger committed
34
import locale
35
import Queue
36

dkirov's avatar
fix TB  
dkirov committed
37
import gtkgui_helpers
38 39
from common import gajim
from common import helpers
40
from calendar import timegm
Yann Leboulanger's avatar
Yann Leboulanger committed
41
from common.fuzzyclock import FuzzyClock
42

43
from htmltextview import HtmlTextView
44
from common.exceptions import GajimGeneralException
45

46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

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

def has_focus(widget):
	return widget.flags() & gtk.HAS_FOCUS == gtk.HAS_FOCUS

class TextViewImage(gtk.Image):

	def __init__(self, anchor):
		super(TextViewImage, self).__init__()
		self.anchor = anchor
		self._selected = False
		self._disconnect_funcs = []
64 65
		self.connect('parent-set', self.on_parent_set)
		self.connect('expose-event', self.on_expose)
66 67 68 69 70 71 72 73 74 75 76

	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
77

78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
	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

113 114 115
		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)
116 117

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

121 122 123
	def do_queue_draw(self, *args):
		self.queue_draw()
		return False
124

125 126 127
	def on_mark_set(self, buf, iterat, mark):
		self.on_mark_modified(mark)
		return False
128

129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
	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
145

146

147
class ConversationTextview:
nkour's avatar
nkour committed
148 149
	'''Class for the conversation textview (where user reads already said messages)
	for chat/groupchat windows'''
150

151 152 153
	path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps', 'muc_separator.png')
	FOCUS_OUT_LINE_PIXBUF = gtk.gdk.pixbuf_new_from_file(path_to_file)

154 155 156 157
	# smooth scroll constants
	MAX_SCROLL_TIME = 0.4 # seconds
	SCROLL_DELAY = 33 # milliseconds

158 159 160 161
	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'''
		self.used_in_history_window = used_in_history_window
162

163
		# no need to inherit TextView, use it as atrribute is safer
164 165
		self.tv = HtmlTextView()
		self.tv.html_hyperlink_handler = self.html_hyperlink_handler
nkour's avatar
nkour committed
166

167
		# set properties
168 169 170 171
		self.tv.set_border_width(1)
		self.tv.set_accepts_tab(True)
		self.tv.set_editable(False)
		self.tv.set_cursor_visible(False)
172
		self.tv.set_wrap_mode(gtk.WRAP_WORD_CHAR)
173 174 175
		self.tv.set_left_margin(2)
		self.tv.set_right_margin(2)
		self.handlers = {}
176 177
		self.images = []
		self.image_cache = {}
nkour's avatar
nkour committed
178

179
		# connect signals
180 181
		id = self.tv.connect('motion_notify_event',
			self.on_textview_motion_notify_event)
182 183 184
		self.handlers[id] = self.tv
		id = self.tv.connect('populate_popup', self.on_textview_populate_popup)
		self.handlers[id] = self.tv
185 186
		id = self.tv.connect('button_press_event',
			self.on_textview_button_press_event)
187
		self.handlers[id] = self.tv
188 189

		id = self.tv.connect('expose-event',
190 191 192
			self.on_textview_expose_event)
		self.handlers[id] = self.tv

nkour's avatar
nkour committed
193

194 195
		self.account = account
		self.change_cursor = None
196
		self.last_time_printout = 0
197 198

		font = pango.FontDescription(gajim.config.get('conversation_font'))
199 200
		self.tv.modify_font(font)
		buffer = self.tv.get_buffer()
201 202 203 204 205 206 207 208 209 210 211 212 213
		end_iter = buffer.get_end_iter()
		buffer.create_mark('end', end_iter, False)

		self.tagIn = buffer.create_tag('incoming')
		color = gajim.config.get('inmsgcolor')
		self.tagIn.set_property('foreground', color)
		self.tagOut = buffer.create_tag('outgoing')
		color = gajim.config.get('outmsgcolor')
		self.tagOut.set_property('foreground', color)
		self.tagStatus = buffer.create_tag('status')
		color = gajim.config.get('statusmsgcolor')
		self.tagStatus.set_property('foreground', color)

214 215
		colors = gajim.config.get('gc_nicknames_colors')
		colors = colors.split(':')
216 217
		for i,color in enumerate(colors):
			tagname = 'gc_nickname_color_' + str(i)
218 219 220
			tag = buffer.create_tag(tagname)
			tag.set_property('foreground', color)

221 222 223 224 225 226
		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')
227
		tag.set_property('foreground', 'darkgrey')
228 229 230 231 232 233
		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)

234 235 236
		tag = buffer.create_tag('restored_message')
		color = gajim.config.get('restored_messages_color')
		tag.set_property('foreground', color)
237

238
		self.tagURL = buffer.create_tag('url')
239
		color = gajim.config.get('urlmsgcolor')
240 241 242 243
		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
244

245 246 247 248 249
		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
250 251 252 253 254 255 256 257 258 259

		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)

260
		buffer.create_tag('focus-out-line', justification = gtk.JUSTIFY_CENTER)
261

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

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

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

276 277
	def del_handlers(self):
		for i in self.handlers.keys():
278 279
			if self.handlers[i].handler_is_connected(i):
				self.handlers[i].disconnect(i)
280 281
		del self.handlers
		self.tv.destroy()
282
		#FIXME:
283
		# self.line_tooltip.destroy()
284

285 286 287 288 289
	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'))
290 291
		self.tagURL.set_property('foreground', gajim.config.get('urlmsgcolor'))
		self.tagMail.set_property('foreground', gajim.config.get('urlmsgcolor'))
292 293

	def at_the_end(self):
294
		buffer = self.tv.get_buffer()
295
		end_iter = buffer.get_end_iter()
296 297
		end_rect = self.tv.get_iter_location(end_iter)
		visible_rect = self.tv.get_visible_rect()
298 299 300 301
		if end_rect.y <= (visible_rect.y + visible_rect.height):
			return True
		return False

302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
	# 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)
		vadj.set_value(onethird)
		if max_val - onethird < 0.01:
			self.smooth_id = None
			self.smooth_scroll_timer.cancel()
			return False
317
		return True
318 319

	def smooth_scroll_timeout(self):
320 321 322 323
		gobject.idle_add(self.do_smooth_scroll_timeout)
		return

	def do_smooth_scroll_timeout(self):
324 325 326
		if not self.smooth_id:
			# we finished scrolling
			return
327 328 329 330 331 332 333 334 335 336 337
		gobject.source_remove(self.smooth_id)
		self.smooth_id = None
		parent = self.tv.get_parent()
		if parent:
			vadj = parent.get_vadjustment()
			vadj.set_value(vadj.upper - vadj.page_size + 1)

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

344
	def scroll_to_end(self):
345 346
		parent = self.tv.get_parent()
		buffer = self.tv.get_buffer()
dkirov's avatar
dkirov committed
347 348 349 350
		end_mark = buffer.get_mark('end')
		if not end_mark:
			return False
		self.tv.scroll_to_mark(end_mark, 0, True, 0, 1)
351 352 353 354
		adjustment = parent.get_hadjustment()
		adjustment.set_value(0)
		return False # when called in an idle_add, just do it once

355 356
	def bring_scroll_to_end(self, diff_y = 0,
	use_smooth=gajim.config.get('use_smooth_scrolling')):
357
		''' scrolls to the end of textview if end is not visible '''
358
		buffer = self.tv.get_buffer()
359
		end_iter = buffer.get_end_iter()
360 361
		end_rect = self.tv.get_iter_location(end_iter)
		visible_rect = self.tv.get_visible_rect()
362 363
		# scroll only if expected end is not visible
		if end_rect.y >= (visible_rect.y + visible_rect.height + diff_y):
364 365 366 367
			if use_smooth:
				gobject.idle_add(self.smooth_scroll_to_end)
			else:
				gobject.idle_add(self.scroll_to_end_iter)
368 369

	def scroll_to_end_iter(self):
370
		buffer = self.tv.get_buffer()
371
		end_iter = buffer.get_end_iter()
dkirov's avatar
dkirov committed
372 373
		if not end_iter:
			return False
374
		self.tv.scroll_to_iter(end_iter, 0, False, 1, 1)
375 376
		return False # when called in an idle_add, just do it once

377 378 379 380 381 382 383 384 385
	def show_focus_out_line(self):
		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()

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

		else:
391 392 393
			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():
394 395 396 397 398 399 400 401 402 403
				# 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
404 405 406
			if self.focus_out_end_mark is not None:
				end_iter_for_previous_line = buffer.get_iter_at_mark(
					self.focus_out_end_mark)
407 408 409 410 411 412 413
				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)
414
				buffer.delete_mark(self.focus_out_end_mark)
415 416 417 418

			# add the new focus out line
			end_iter = buffer.get_end_iter()
			buffer.insert(end_iter, '\n')
419
			buffer.insert_pixbuf(end_iter,
420
				ConversationTextview.FOCUS_OUT_LINE_PIXBUF)
421 422 423

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

428 429
			self.allow_focus_out_line = False

430
			# update the iter we hold to make comparison the next time
431 432
			self.focus_out_end_mark = buffer.create_mark(None,
				buffer.get_end_iter(), left_gravity=True)
433 434 435 436 437 438

			buffer.end_user_action()

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

439
	def show_line_tooltip(self):
440 441
		pointer = self.tv.get_pointer()
		x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, pointer[0],
442
			pointer[1])
443 444
		tags = self.tv.get_iter_at_location(x, y).get_tags()
		tag_table = self.tv.get_buffer().get_tag_table()
445 446 447 448 449 450 451
		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
452
			position = self.tv.window.get_origin()
453
			self.line_tooltip.show_tooltip(_('Text below this line is what has '
454 455
			'been said since the last time you paid attention to this group chat'),
				8, position[1] + pointer[1])
456

457 458 459 460 461 462
	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
463

464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482
		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

483 484
	def on_textview_motion_notify_event(self, widget, event):
		'''change the cursor to a hand when we are over a mail or an url'''
485 486
		pointer_x, pointer_y, spam = self.tv.window.get_pointer()
		x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, pointer_x,
487
			pointer_y)
488
		tags = self.tv.get_iter_at_location(x, y).get_tags()
489
		if self.change_cursor:
490
			self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
491 492
				gtk.gdk.Cursor(gtk.gdk.XTERM))
			self.change_cursor = None
493
		tag_table = self.tv.get_buffer().get_tag_table()
494 495 496
		over_line = False
		for tag in tags:
			if tag in (tag_table.lookup('url'), tag_table.lookup('mail')):
497
				self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
498 499 500 501 502 503 504 505 506 507 508 509
					gtk.gdk.Cursor(gtk.gdk.HAND2))
				self.change_cursor = tag
			elif tag == tag_table.lookup('focus-out-line'):
				over_line = 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 over_line and not self.line_tooltip.win:
			self.line_tooltip.timeout = gobject.timeout_add(500,
				self.show_line_tooltip)
510
			self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
511 512 513 514 515
				gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
			self.change_cursor = tag

	def clear(self, tv = None):
		'''clear text in the textview'''
516
		buffer = self.tv.get_buffer()
517 518
		start, end = buffer.get_bounds()
		buffer.delete(start, end)
519 520 521
		size = gajim.config.get('max_conversation_lines')
		size = 2 * size - 1
		self.marks_queue = Queue.Queue(size)
522
		self.focus_out_end_mark = None
523 524 525 526 527 528 529

	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):
		'''we override the default context menu and we prepend Clear
530
		(only if used_in_history_window is False)
531 532
		and if we have sth selected we show a submenu with actions on the phrase
		(see on_conversation_textview_button_press_event)'''
533 534 535 536 537 538 539 540 541 542 543 544

		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

545
		if self.selected_phrase:
546 547 548 549 550
			if not separator_menuitem_was_added:
				item = gtk.SeparatorMenuItem()
				menu.prepend(item)

			self.selected_phrase = helpers.reduce_chars_newlines(
nkour's avatar
nkour committed
551
				self.selected_phrase, 25, 2)
552
			item = gtk.MenuItem(_('_Actions for "%s"') % self.selected_phrase)
553 554 555
			menu.prepend(item)
			submenu = gtk.Menu()
			item.set_submenu(submenu)
nkour's avatar
nkour committed
556

557 558 559 560 561 562 563 564
			always_use_en = gajim.config.get('always_english_wikipedia')
			if always_use_en:
				link = 'http://en.wikipedia.org/wiki/Special:Search?search=%s'\
					% self.selected_phrase
			else:
				link = 'http://%s.wikipedia.org/wiki/Special:Search?search=%s'\
					% (gajim.LANG, self.selected_phrase)
			item = gtk.MenuItem(_('Read _Wikipedia Article'))
565 566
			id = item.connect('activate', self.visit_url_from_menuitem, link)
			self.handlers[id] = item
567 568 569 570 571 572 573 574 575 576 577 578 579
			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'\
						% self.selected_phrase
				else:
					link = 'http://%s.wiktionary.org/wiki/Special:Search?search=%s'\
						% (gajim.LANG, self.selected_phrase)
580 581
				id = item.connect('activate', self.visit_url_from_menuitem, link)
				self.handlers[id] = item
582 583
			else:
				if dict_link.find('%s') == -1:
584
					# we must have %s in the url if not WIKTIONARY
585 586
					item = gtk.MenuItem(_(
						'Dictionary URL is missing an "%s" and it is not WIKTIONARY'))
587 588 589
					item.set_property('sensitive', False)
				else:
					link = dict_link % self.selected_phrase
590 591
					id = item.connect('activate', self.visit_url_from_menuitem,
						link)
592
					self.handlers[id] = item
593
			submenu.append(item)
nkour's avatar
nkour committed
594 595


596 597
			search_link = gajim.config.get('search_engine')
			if search_link.find('%s') == -1:
598
				# we must have %s in the url
599 600 601 602
				item = gtk.MenuItem(_('Web Search URL is missing an "%s"'))
				item.set_property('sensitive', False)
			else:
				item = gtk.MenuItem(_('Web _Search for it'))
603
				link =	search_link % self.selected_phrase
604 605
				id = item.connect('activate', self.visit_url_from_menuitem, link)
				self.handlers[id] = item
606
			submenu.append(item)
607

nkour's avatar
hig  
nkour committed
608
			item = gtk.MenuItem(_('Open as _Link'))
609 610 611
			id = item.connect('activate', self.visit_url_from_menuitem, link)
			self.handlers[id] = item
			submenu.append(item)
nkour's avatar
nkour committed
612

613
		menu.show_all()
nkour's avatar
nkour committed
614

615 616 617
	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
618
		self.selected_phrase = '' # do not move belove event button check!
619 620 621 622

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

623
		x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
624
			int(event.x), int(event.y))
625
		iter = self.tv.get_iter_at_location(x, y)
626 627 628 629 630 631
		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')
nkour's avatar
nkour committed
632
				if tag_name in ('url', 'mail'):
633 634 635 636 637
					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
638
		buffer = self.tv.get_buffer()
639 640 641 642
		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]
643 644
			self.selected_phrase = buffer.get_text(start_sel, finish_sel).decode(
				'utf-8')
645 646 647 648 649 650 651 652 653 654 655

	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.roster.new_chat_from_jid(self.account, jid)

656
	def on_join_group_chat_menuitem_activate(self, widget, room_jid):
657
		if 'join_gc' in gajim.interface.instances[self.account]:
658
			instance = gajim.interface.instances[self.account]['join_gc']
659
			instance.xml.get_widget('room_jid_entry').set_text(room_jid)
nkour's avatar
nkour committed
660
			gajim.interface.instances[self.account]['join_gc'].window.present()
661 662
		else:
			try:
663
				gajim.interface.instances[self.account]['join_gc'] = \
664
				dialogs.JoinGroupchatWindow(self.account, room_jid)
665
			except GajimGeneralException:
666 667 668 669 670 671
				pass

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

	def make_link_menu(self, event, kind, text):
672
		xml = gtkgui_helpers.get_glade('chat_context_menu.glade')
673 674 675
		menu = xml.get_widget('chat_context_menu')
		childs = menu.get_children()
		if kind == 'url':
676 677
			id = childs[0].connect('activate', self.on_copy_link_activate, text)
			self.handlers[id] = childs[0]
678 679
			id = childs[1].connect('activate', self.on_open_link_activate, kind,
				text)
680
			self.handlers[id] = childs[1]
681 682
			childs[2].hide() # copy mail address
			childs[3].hide() # open mail composer
683
			childs[4].hide() # jid section separator
684 685 686 687
			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
688 689 690
			# load muc icon
			join_group_chat_menuitem = xml.get_widget('join_group_chat_menuitem')
			muc_icon = gajim.interface.roster.load_icon('muc_active')
691 692
			if muc_icon:
				join_group_chat_menuitem.set_image(muc_icon)
693

694
			text = text.lower()
695 696
			id = childs[2].connect('activate', self.on_copy_link_activate, text)
			self.handlers[id] = childs[2]
697 698
			id = childs[3].connect('activate', self.on_open_link_activate, kind,
				text)
699 700 701 702
			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',
703
				self.on_join_group_chat_menuitem_activate, text)
704
			self.handlers[id] = childs[6]
nkour's avatar
nkour committed
705

706
			allow_add = False
707 708
			c = gajim.contacts.get_first_contact_from_jid(self.account, text)
			if c and not gajim.contacts.is_pm_from_contact(self.account, c):
709
				if _('Not in Roster') in c.groups:
710
					allow_add = True
nkour's avatar
nkour committed
711
			else: # he or she's not at all in the account contacts
712
				allow_add = True
nkour's avatar
nkour committed
713

714
			if allow_add:
715 716
				id = childs[7].connect('activate', self.on_add_to_roster_activate,
					text)
717
				self.handlers[id] = childs[7]
718 719 720
				childs[7].show() # show add to roster menuitem
			else:
				childs[7].hide() # hide add to roster menuitem
nkour's avatar
nkour committed
721

722 723 724 725 726 727 728 729 730 731 732 733 734 735 736
			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()
737 738
			word = self.tv.get_buffer().get_text(begin_iter, end_iter).decode(
				'utf-8')
739 740 741 742 743 744
			if event.button == 3: # right click
				self.make_link_menu(event, kind, word)
			else:
				# we launch the correct application
				helpers.launch_browser_mailer(kind, word)

745 746 747 748
	def html_hyperlink_handler(self, texttag, widget, event, iter, kind, href):
		if event.type == gtk.gdk.BUTTON_PRESS:
			if event.button == 3: # right click
				self.make_link_menu(event, kind, href)
749
				return True
750 751 752 753 754
			else:
				# we launch the correct application
				helpers.launch_browser_mailer(kind, href)


755
	def detect_and_print_special_text(self, otext, other_tags):
nkour's avatar
nkour committed
756 757 758 759 760 761
		'''detects special text (emots & links & formatting)
		prints normal text before any special text it founts,
		then print special text (that happens many times until
		last special text is printed) and then returns the index
		after *last* special text, so we can print it in
		print_conversation_line()'''
nkour's avatar
nkour committed
762

763
		buffer = self.tv.get_buffer()
764 765 766 767 768 769

		start = 0
		end = 0
		index = 0

		# basic: links + mail + formatting is always checked (we like that)
770
		if gajim.config.get('emoticons_theme'): # search for emoticons & urls
771 772 773 774 775 776 777 778 779
			iterator = gajim.interface.emot_and_basic_re.finditer(otext)
		else: # search for just urls + mail + formatting
			iterator = gajim.interface.basic_pattern_re.finditer(otext)
		for match in iterator:
			start, end = match.span()
			special_text = otext[start:end]
			if start != 0:
				text_before_special_text = otext[index:start]
				end_iter = buffer.get_end_iter()
nkour's avatar
nkour committed
780
				# we insert normal text
781 782 783 784 785 786 787
				buffer.insert_with_tags_by_name(end_iter,
					text_before_special_text, *other_tags)
			index = end # update index

			# now print it
			self.print_special_text(special_text, other_tags)

nkour's avatar
nkour committed
788
		return index # the position after *last* special text
789

790 791
	def latex_to_image(self, str):
		result = None
792 793 794
		exitcode = 0

		# some latex commands are really bad
795 796 797 798 799 800 801 802 803 804
		blacklist = ['\\def', '\\let', '\\futurelet',
			'\\newcommand', '\\renewcomment', '\\else', '\\fi', '\\write',
			'\\input', '\\include', '\\chardef', '\\catcode', '\\makeatletter',
			'\\noexpand', '\\toksdef', '\\every', '\\errhelp', '\\errorstopmode',
			'\\scrollmode', '\\nonstopmode', '\\batchmode', '\\read', '\\csname',
			'\\newhelp', '\\relax', '\\afterground', '\\afterassignment',
			'\\expandafter', '\\noexpand', '\\special', '\\command', '\\loop',
			'\\repeat', '\\toks', '\\output', '\\line', '\\mathcode', '\\name',
			'\\item', '\\section', '\\mbox', '\\DeclareRobustCommand', '\\[',
			'\\]']
805

806
		str = str[2:len(str)-2]
807

808 809 810 811 812
		# filter latex code with bad commands
		for word in blacklist:
			if word in str:
				exitcode = 1
				break
813

814 815
		if exitcode == 0:
			random.seed()
816
			tmpfile = os.path.join(gettempdir(), 'gajimtex_' + random.randint(0,
817 818 819
				100).__str__())

			# build latex string
820 821 822 823
			texstr = '\\documentclass[12pt]{article}\\usepackage[dvips]{graphicx}'
			texstr += '\\usepackage{amsmath}\\usepackage{amssymb}'
			texstr += '\\pagestyle{empty}'
			texstr += '\\begin{document}\\begin{large}\\begin{gather*}'
824
			texstr += str
825
			texstr += '\\end{gather*}\\end{large}\\end{document}'
826

827
			file = open(os.path.join(tmpfile + '.tex'), 'w+')
828 829 830 831 832 833 834
			file.write(texstr)
			file.flush()
			file.close()

			p = Popen(['latex', '--interaction=nonstopmode', tmpfile + '.tex'],
				cwd=gettempdir())
			exitcode = p.wait()
835

836
		if exitcode == 0:
837 838
			p = Popen(['dvips', '-E', '-o', tmpfile + '.ps', tmpfile + '.dvi'],
				cwd=gettempdir())
839 840 841
			exitcode = p.wait()

		if exitcode == 0:
842 843
			p = Popen(['convert', '-alpha', 'off', tmpfile + '.ps',
				tmpfile + '.png'], cwd=gettempdir())
844 845
			exitcode = p.wait()

846
		extensions = ['.tex', '.log', '.aux', '.dvi', '.ps']
847 848 849 850 851
		for ext in extensions:
			try:
				os.remove(tmpfile + ext)
			except Exception:
				pass
852

853 854 855
		if exitcode == 0:
			result = tmpfile + '.png'

856
		return result
857

858
	def print_special_text(self, special_text, other_tags):
nkour's avatar
nkour committed
859 860
		'''is called by detect_and_print_special_text and prints
		special text (emots, links, formatting)'''
861 862 863 864
		tags = []
		use_other_tags = True
		show_ascii_formatting_chars = \
			gajim.config.get('show_ascii_formatting_chars')
865
		buffer = self.tv.get_buffer()
866 867

		possible_emot_ascii_caps = special_text.upper() # emoticons keys are CAPS
868
		if gajim.config.get('emoticons_theme') and \
869
		possible_emot_ascii_caps in gajim.interface.emoticons.keys():
870
			# it's an emoticon
871 872 873
			emot_ascii = possible_emot_ascii_caps
			end_iter = buffer.get_end_iter()
			anchor = buffer.create_child_anchor(end_iter)
874
			img = TextViewImage(anchor)
875 876 877 878 879
			animations = gajim.interface.emoticons_animations
			if not emot_ascii in animations:
				animations[emot_ascii] = gtk.gdk.PixbufAnimation(
					gajim.interface.emoticons[emot_ascii])
			img.set_from_animation(animations[emot_ascii])
880
			img.show()
881
			self.images.append(img)
882
			# add with possible animation
883
			self.tv.add_child_at_anchor(img, anchor)
nkour's avatar
nkour committed
884
		#FIXME: one day, somehow sync with regexp in gajim.py
885 886
		elif special_text.startswith('http://') or \
			special_text.startswith('www.') or \
887 888
			special_text.startswith('ftp://') or \
			special_text.startswith('ftp.') or \
889 890 891 892 893 894 895
			special_text.startswith('https://') or \
			special_text.startswith('gopher://') or \
			special_text.startswith('news://') or \
			special_text.startswith('ed2k://') or \
			special_text.startswith('irc://') or \
			special_text.startswith('sip:') or \
			special_text.startswith('magnet:'):
896
			# it's a url
897 898
			tags.append('url')
			use_other_tags = False
899 900 901
		elif special_text.startswith('mailto:') or \
		gajim.interface.sth_at_sth_dot_sth_re.match(special_text):
			# it's a mail
902 903 904 905
			tags.append('mail')
			use_other_tags = False
		elif special_text.startswith('*'): # it's a bold text
			tags.append('bold')
906 907
			if special_text[1] == '/' and special_text[-2] == '/' and\
			len(special_text) > 4: # it's also italic
908 909 910
				tags.append('italic')
				if not show_ascii_formatting_chars:
					special_text = special_text[2:-2] # remove */ /*
911 912
			elif special_text[1] == '_' and special_text[-2] == '_' and \
			len(special_text) > 4: # it's also underlined
913 914 915 916 917 918 919 920
				tags.append('underline')
				if not show_ascii_formatting_chars:
					special_text = special_text[2:-2] # remove *_ _*
			else:
				if not show_ascii_formatting_chars:
					special_text = special_text[1:-1] # remove * *
		elif special_text.startswith('/'): # it's an italic text
			tags.append('italic')
921 922
			if special_text[1] == '*' and special_text[-2] == '*' and \
			len(special_text) > 4: # it's also bold
923 924 925
				tags.append('bold')
				if not show_ascii_formatting_chars:
					special_text = special_text[2:-2] # remove /* */
926 927
			elif special_text[1] == '_' and special_text[-2] == '_' and \
			len(special_text) > 4: # it's also underlined
928 929 930 931 932 933 934 935
				tags.append('underline')
				if not show_ascii_formatting_chars:
					special_text = special_text[2:-2] # remove /_ _/
			else:
				if not show_ascii_formatting_chars:
					special_text = special_text[1:-1] # remove / /
		elif special_text.startswith('_'): # it's an underlined text
			tags.append('underline')
936 937
			if special_text[1] == '*' and special_text[-2] == '*' and \
			len(special_text) > 4: # it's also bold
938 939 940
				tags.append('bold')
				if not show_ascii_formatting_chars:
					special_text = special_text[2:-2] # remove _* *_
941 942
			elif special_text[1] == '/' and special_text[-2] == '/' and \
			len(special_text) > 4: # it's also italic
943 944 945 946 947 948
				tags.append('italic')
				if not show_ascii_formatting_chars:
					special_text = special_text[2:-2] # remove _/ /_
			else:
				if not show_ascii_formatting_chars:
					special_text = special_text[1:-1] # remove _ _
949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966
		elif special_text.startswith('$$') and special_text.endswith('$$'):
			imagepath = self.latex_to_image(special_text)
			end_iter = buffer.get_end_iter()
			anchor = buffer.create_child_anchor(end_iter)
			if imagepath != None:
				img = gtk.Image()
				img.set_from_file(imagepath)
				img.show()
				# add
				self.tv.add_child_at_anchor(img, anchor)
				# delete old file
				try:
					os.remove(imagepath)
				except Exception:
					pass
			else:
				buffer.insert(end_iter, special_text)
			use_other_tags = False
967 968 969 970 971 972 973 974 975 976 977 978 979
		else:
			#it's a url
			tags.append('url')
			use_other_tags = False

		if len(tags) > 0:
			end_iter = buffer.get_end_iter()
			all_tags = tags[:]
			if use_other_tags:
				all_tags += other_tags
			buffer.insert_with_tags_by_name(end_iter, special_text, *all_tags)

	def print_empty_line(self):
980
		buffer = self.tv.get_buffer()
981
		end_iter = buffer.get_end_iter()
982
		buffer.insert_with_tags_by_name(end_iter, '\n', 'eol')
983 984

	def print_conversation_line(self, text, jid, kind, name, tim,
985 986
	other_tags_for_name = [], other_tags_for_time = [], other_tags_for_text = [],
	subject = None, old_kind = None, xhtml = None):
987
		'''prints 'chat' type messages'''
988
		buffer = self.tv.get_buffer()
989
		buffer.begin_user_action()
990 991 992 993 994 995 996 997
		if self.marks_queue.full():
			# remove oldest line
			m1 = self.marks_queue.get()
			m2 = self.marks_queue.get()
			i1 = buffer.get_iter_at_mark(m1)
			i2 = buffer.get_iter_at_mark(m2)
			buffer.delete(i1, i2)
			buffer.delete_mark(m1)
998 999 1000 1001
		end_iter = buffer.get_end_iter()
		at_the_end = False
		if self.at_the_end():
			at_the_end = True
nkour's avatar
nkour committed
1002

1003 1004 1005
		# Create one mark and add it to queue once if it's the first line
		# else twice (one for end bound, one for start bound)
		mark = None
1006
		if buffer.get_char_count() > 0:
1007
			buffer.insert_with_tags_by_name(end_iter, '\n', 'eol')
1008 1009 1010 1011 1012
			mark = buffer.create_mark(None, end_iter, left_gravity=True)
			self.marks_queue.put(mark)
		if not mark:
			mark = buffer.create_mark(None, end_iter, left_gravity=True)
		self.marks_queue.put(mark)
1013 1014
		if kind == 'incoming_queue':
			kind = 'incoming'
1015 1016
		if old_kind == 'incoming_queue':
			old_kind = 'incoming'
1017
		# print the time stamp
1018
		if not tim:
nkour's avatar
nkour committed
1019
			# We don't have tim for outgoing messages...
1020
			tim = time.localtime()
1021
		current_print_time = gajim.config.get('print_time')
1022
		if current_print_time == 'always' and kind != 'info':
1023
			timestamp_str = self.get_time_to_show(tim)
1024 1025
			timestamp = time.strftime(timestamp_str, tim)
			buffer.insert_with_tags_by_name(end_iter, timestamp,
1026
				*other_tags_for_time)
1027
		elif current_print_time == 'sometimes' and kind != 'info':
1028 1029
			every_foo_seconds = 60 * gajim.config.get(
				'print_ichat_every_foo_minutes')
1030
			seconds_passed = time.mktime(tim) - self.last_time_printout
1031
			if seconds_passed > every_foo_seconds:
1032
				self.last_time_printout = time.mktime(tim)
1033
				end_iter = buffer.get_end_iter()
Yann Leboulanger's avatar
Yann Leboulanger committed
1034 1035
				if gajim.config.get('print_time_fuzzy') > 0:
					fc = FuzzyClock()
1036
					fc.setTime(time.strftime('%H:%M', tim))
Yann Leboulanger's avatar
Yann Leboulanger committed
1037 1038 1039
					ft = fc.getFuzzyTime(gajim.config.get('print_time_fuzzy'))
					tim_format = ft.decode(locale.getpreferredencoding())
				else:
jimpp's avatar
jimpp committed
1040
					tim_format = self.get_time_to_show(tim)
1041 1042
				buffer.insert_with_tags_by_name(end_iter, tim_format + '\n',
					'time_sometimes')
1043 1044 1045
		# kind = info, we print things as if it was a status: same color, ...
		if kind == 'info':
			kind = 'status'
1046
		other_text_tag = self.detect_other_text_tag(text, kind)
1047
		text_tags = other_tags_for_text[:] # create a new list
1048
		if other_text_tag:
1049
			# note that color of /me may be overwritten in gc_control
1050 1051
			text_tags.append(other_text_tag)
		else: # not status nor /me
1052 1053 1054 1055 1056 1057 1058 1059 1060
			if gajim.config.get(
				'chat_merge_consecutive_nickname'):
				if kind != old_kind:
					self.print_name(name, kind, other_tags_for_name)
				else:
					self.print_real_text(gajim.config.get(
						'chat_merge_consecutive_nickname_indent'))
			else:
				self.print_name(name, kind, other_tags_for_name)
1061
		self.print_subject(subject)
1062
		self.print_real_text(text, text_tags, name, xhtml)
1063 1064 1065 1066 1067

		# scroll to the end of the textview
		if at_the_end or kind == 'outgoing':
			# we are at the end or we are sending something
			# scroll to the end (via idle in case the scrollbar has appeared)
1068 1069 1070 1071
			if gajim.config.get('use_smooth_scrolling'):
				gobject.idle_add(self.smooth_scroll_to_end)
			else:
				gobject.idle_add(self.scroll_to_end)
1072 1073 1074

		buffer.end_user_action()

jimpp's avatar
jimpp committed
1075 1076 1077 1078 1079 1080 1081 1082
	def get_time_to_show(self, tim):
		'''Get the time, with the day before if needed and return it.
		It DOESN'T format a fuzzy time'''
		format = ''
		# get difference in days since epoch (86400 = 24*3600)
		# number of days since epoch for current time (in GMT) -
		# number of days since epoch for message (in GMT)
		diff_day = int(timegm(time.localtime())) / 86400 -\
1083
			int(timegm(tim)) / 86400
jimpp's avatar
jimpp committed
1084 1085 1086 1087 1088 1089 1090 1091 1092 1093
		if diff_day == 0:
			day_str = ''
		elif diff_day == 1:
			day_str = _('Yesterday')
		else:
			#the number is >= 2
			# %i is day in year (1-365), %d (1-31) we want %i
			day_str = _('%i days ago') % diff_day
		if day_str:
			format += day_str + ' '
1094 1095 1096
		timestamp_str = gajim.config.get('time_stamp')
		timestamp_str = helpers.from_one_line(timestamp_str)
		format += timestamp_str
jimpp's avatar
jimpp committed
1097
		tim_format = time.strftime(format, tim)
1098
		if locale.getpreferredencoding() != 'KOI8-R':
jimpp's avatar
jimpp committed
1099 1100 1101 1102 1103
			# if tim_format comes as unicode because of day_str.
			# we convert it to the encoding that we want (and that is utf-8)
			tim_format = helpers.ensure_utf8_string(tim_format)
		return tim_format

1104
	def detect_other_text_tag(self, text, kind):
1105
		if kind == 'status':
1106
			return kind
1107
		elif text.startswith('/me ') or text.startswith('/me\n'):
1108
			return kind
1109

1110 1111
	def print_name(self, name, kind, other_tags_for_name):