chat.py 38.6 KB
Newer Older
1
##	chat.py
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
##
## Gajim Team:
##	- Yann Le Boulanger <asterix@lagaule.org>
##	- Vincent Hanquez <tab@snarc.org>
##	- Nikos Kouremenos <kourem@gmail.com>
##
##	Copyright (C) 2003-2005 Gajim Team
##
## This program 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 2 only.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##

import gtk
import gtk.glade
import pango
import gobject
import time
25
import math
26
import os
27

28
import dialogs
29
import history_window
30
import gtkgui_helpers
31
import tooltips
32
import conversation_textview
33
import message_textview
34

nkour's avatar
nkour committed
35
36
37
38
39
try:
	import gtkspell
except:
	pass

40
from common import gajim
41
from common import helpers
42
43
44
45
46
47
48
from common import i18n

_ = i18n._
APP = i18n.APP
gtk.glade.bindtextdomain(APP, i18n.DIR)
gtk.glade.textdomain(APP)

49
GTKGUI_GLADE = 'gtkgui.glade'
50

51
class Chat:
nkour's avatar
nkour committed
52
	'''Class for chat/groupchat windows'''
53
	def __init__(self, account, widget_name):
54
		self.xml = gtk.glade.XML(GTKGUI_GLADE, widget_name, APP)
55
		self.window = self.xml.get_widget(widget_name)
56

57
		self.widget_name = widget_name
58

59
60
61
		self.account = account
		self.change_cursor = None
		self.xmls = {}
62
63
		self.conversation_textviews = {} # holds per jid conversation textview
		self.message_textviews = {} # holds per jid message (where we write) textview
64
65
		self.nb_unread = {}
		self.print_time_timeout_id = {}
66
		self.names = {} # what is printed in the tab (eg. contact.name)
nkour's avatar
nkour committed
67
		self.childs = {} # holds the contents for every tab (VBox)
68

69
		# the following vars are used to keep history of user's messages
70
71
72
73
74
		self.sent_history = {}
		self.sent_history_pos = {}
		self.typing_new = {}
		self.orig_msg = {}

75
76
77
		# alignment before notebook (to control top padding for when showing tabs)
		self.alignment = self.xml.get_widget('alignment')
		
78
79
		# notebook customizations
		self.notebook = self.xml.get_widget('chat_notebook')
80
		self.notebook.remove_page(0) # FIXME why??
81
82
83
84
85
86
87
88
89
90
91
92
93
		pref_pos = gajim.config.get('tabs_position')
		if pref_pos != 'top':
			if pref_pos == 'bottom':
				nb_pos = gtk.POS_BOTTOM
			elif pref_pos == 'left':
				nb_pos = gtk.POS_LEFT
			elif pref_pos == 'right':
				nb_pos = gtk.POS_RIGHT
			else:
				nb_pos = gtk.POS_TOP
		else:
			nb_pos = gtk.POS_TOP
		self.notebook.set_tab_pos(nb_pos)
94
95
		if gajim.config.get('tabs_always_visible'):
			self.notebook.set_show_tabs(True)
nkour's avatar
nkour committed
96
			self.alignment.set_property('top-padding', 2)
97
98
		else:
			self.notebook.set_show_tabs(False)
99
100
		self.notebook.set_show_border(gajim.config.get('tabs_border'))

101
		if gajim.config.get('useemoticons'):
102
			self.emoticons_menu = self.prepare_emoticons_menu()
103

nicfit's avatar
nicfit committed
104
105
106
107
		# muc attention states (when we are mentioned in a muc)
		# if the room jid is in the list, the room has mentioned us
		self.muc_attentions = []

108
	def toggle_emoticons(self):
109
110
111
112
113
		'''hide show emoticons_button and make sure emoticons_menu is always there
		when needed'''
		if gajim.config.get('useemoticons'):
			self.emoticons_menu = self.prepare_emoticons_menu()
		
114
		for jid in self.xmls:
115
			emoticons_button = self.xmls[jid].get_widget('emoticons_button')
116
			if gajim.config.get('useemoticons'):
117
118
				emoticons_button.show()
				emoticons_button.set_no_show_all(False)
119
			else:
120
121
				emoticons_button.hide()
				emoticons_button.set_no_show_all(True)
122

123
124
	def update_font(self):
		font = pango.FontDescription(gajim.config.get('conversation_font'))
125
126
		for jid in self.xmls:
			self.conversation_textviews[jid].modify_font(font)
nkour's avatar
nkour committed
127
			msg_textview = self.message_textviews[jid]
128
			msg_textview.modify_font(font)
129

130
	def update_tags(self):
131
132
		for jid in self.conversation_textviews:
			self.conversation_textviews[jid].update_tags()
133
134

	def update_print_time(self):
135
		if gajim.config.get('print_time') != 'sometimes':
136
137
138
139
140
141
			list_jid = self.print_time_timeout_id.keys()
			for jid in list_jid:
				gobject.source_remove(self.print_time_timeout_id[jid])
				del self.print_time_timeout_id[jid]
		else:
			for jid in self.xmls:
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
				if not self.print_time_timeout_id.has_key(jid):
					self.print_time_timeout(jid)
					self.print_time_timeout_id[jid] = gobject.timeout_add(300000,
						self.print_time_timeout, jid)

	def print_time_timeout(self, jid):
		if not jid in self.xmls.keys():
			return False
		if gajim.config.get('print_time') == 'sometimes':
			conv_textview = self.conversation_textviews[jid]
			buffer = conv_textview.get_buffer()
			end_iter = buffer.get_end_iter()
			tim = time.localtime()
			tim_format = time.strftime('%H:%M', tim)
			buffer.insert_with_tags_by_name(end_iter, '\n' + tim_format,
				'time_sometimes')
			# scroll to the end of the textview
			if conv_textview.at_the_end():
				# we are at the end
				conv_textview.scroll_to_end()
			return True # loop again
		if self.print_time_timeout_id.has_key(jid):
			del self.print_time_timeout_id[jid]
		return False
166

167
	def show_title(self, urgent = True):
nkour's avatar
nkour committed
168
		'''redraw the window's title'''
169
170
171
		unread = 0
		for jid in self.nb_unread:
			unread += self.nb_unread[jid]
172
		start = ''
173
		if unread > 1:
174
			start = '[' + unicode(unread) + '] '
175
		elif unread == 1:
176
			start = '* '
177
		chat = self.names[jid]
Vincent Hanquez's avatar
Vincent Hanquez committed
178
		if len(self.xmls) > 1: # if more than one tab in the same window
nkour's avatar
nkour committed
179
			if self.widget_name == 'tabbed_chat_window':
180
				add = _('Chat')
nkour's avatar
nkour committed
181
			elif self.widget_name == 'groupchat_window':
182
183
184
185
				add = _('Group Chat')
		elif len(self.xmls) == 1: # just one tab
			if self.widget_name == 'tabbed_chat_window':
				c = gajim.get_first_contact_instance_from_jid(self.account, jid)
nkour's avatar
nkour committed
186
187
188
189
				if c is None:
					add = ''
				else:
					add = c.name
190
191
192
193
194
			elif self.widget_name == 'groupchat_window':
				name = gajim.get_nick_from_jid(jid)
				add = name

		title = start + add
195
		if len(gajim.connections) >= 2: # if we have 2 or more accounts
196
			title += ' (' + _('account: ') + self.account + ')'
Vincent Hanquez's avatar
Vincent Hanquez committed
197

198
		self.window.set_title(title)
199
200
		if urgent:
			gtkgui_helpers.set_unset_urgency_hint(self.window, unread)
201

202
	def redraw_tab(self, jid, chatstate = None):
nicfit's avatar
nicfit committed
203
204
205
206
		'''redraw the label of the tab
		if chatstate is given that means we have HE SENT US a chatstate'''
		# Update status images
		self.set_state_image(jid)
207
			
208
		child = self.childs[jid]
209
		hb = self.notebook.get_tab_label(child).get_children()[0]
nkour's avatar
nkour committed
210
		if self.widget_name == 'tabbed_chat_window':
211
			nickname = hb.get_children()[1]
212
			close_button = hb.get_children()[2]
nicfit's avatar
nicfit committed
213

nicfit's avatar
nicfit committed
214
215
216
217
218
219
220
			unread = ''
			num_unread = self.nb_unread[jid]
			if num_unread == 1 and not gajim.config.get('show_unread_tab_icon'):
				unread = '* '
			elif num_unread > 1:
				unread = '[' + unicode(num_unread) + '] '

nicfit's avatar
nicfit committed
221
222
223
224
			# Draw tab label using chatstate 
			theme = gajim.config.get('roster_theme')
			color = None
			if unread and chatstate == 'active':
225
				color = gajim.config.get_per('themes', theme, 'state_unread_color')
nkour's avatar
nkour committed
226
227
			elif chatstate is not None:
				if chatstate == 'composing':
nicfit's avatar
nicfit committed
228
					color = gajim.config.get_per('themes', theme,
229
						'state_composing_color')
nkour's avatar
nkour committed
230
				elif chatstate == 'inactive':
nicfit's avatar
nicfit committed
231
					color = gajim.config.get_per('themes', theme,
232
						'state_inactive_color')
nkour's avatar
nkour committed
233
				elif chatstate == 'gone':
234
					color = gajim.config.get_per('themes', theme, 'state_gone_color')
nkour's avatar
nkour committed
235
				elif chatstate == 'paused':
236
					color = gajim.config.get_per('themes', theme, 'state_paused_color')
237
				elif unread and self.window.get_property('has-toplevel-focus'):
238
					color = gajim.config.get_per('themes', theme, 'state_active_color')
239
				elif unread:
240
					color = gajim.config.get_per('themes', theme, 'state_unread_color')
nicfit's avatar
nicfit committed
241
				else:
242
					color = gajim.config.get_per('themes', theme, 'state_active_color')
nicfit's avatar
nicfit committed
243
			if color:
244
				color = gtk.gdk.colormap_get_system().alloc_color(color)
245
246
				# We set the color for when it's the current tab or not
				nickname.modify_fg(gtk.STATE_NORMAL, color)
247
				if chatstate in ('inactive', 'gone'):
248
249
250
251
252
253
254
255
					# Adjust color to be lighter against the darker inactive
					# background
					p = 0.4
					mask = 0
					color.red = int((color.red * p) + (mask * (1 - p)))
					color.green = int((color.green * p) + (mask * (1 - p)))
					color.blue = int((color.blue * p) + (mask * (1 - p)))
				nickname.modify_fg(gtk.STATE_ACTIVE, color)
nkour's avatar
nkour committed
256
		elif self.widget_name == 'groupchat_window':
257
			nickname = hb.get_children()[0]
258
259
			close_button = hb.get_children()[1]

nicfit's avatar
nicfit committed
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
			unread = ''
			has_focus = self.window.get_property('has-toplevel-focus')
			current_tab = (self.notebook.page_num(child) == self.notebook.get_current_page())
			color = None
			theme = gajim.config.get('roster_theme')
			if chatstate == 'attention' and (not has_focus or not current_tab):
				if jid not in self.muc_attentions:
					self.muc_attentions.append(jid)
				color = gajim.config.get_per('themes', theme, 'state_muc_directed_msg')
			elif chatstate:
				if chatstate == 'active' or (current_tab and has_focus):
					if jid in self.muc_attentions:
						self.muc_attentions.remove(jid)
					color = gajim.config.get_per('themes', theme, 'state_active_color')
				elif chatstate == 'newmsg' and (not has_focus or not current_tab) and\
				     jid not in self.muc_attentions:
					color = gajim.config.get_per('themes', theme, 'state_muc_msg')
			if color:
				color = gtk.gdk.colormap_get_system().alloc_color(color)
				# The widget state depend on whether this tab is the "current" tab
				if current_tab:
					nickname.modify_fg(gtk.STATE_NORMAL, color)
				else:
					nickname.modify_fg(gtk.STATE_ACTIVE, color)

nkour's avatar
nkour committed
285
286
287
		if gajim.config.get('tabs_close_button'):
			close_button.show()
		else:
288
			close_button.hide()
nkour's avatar
nkour committed
289

290
		nickname.set_max_width_chars(10)
nicfit's avatar
nicfit committed
291
		nickname.set_text(unread + self.names[jid])
292

293
	def get_message_type(self, jid):
294
295
296
297
298
		if self.widget_name == 'groupchat_window':
			return 'gc'
		if gajim.contacts[self.account].has_key(jid):
			return 'chat'
		return 'pm'
299

300
	def on_window_destroy(self, widget, kind): #kind is 'chats' or 'gc'
301
		'''clean gajim.interface.instances[self.account][kind]'''
302
		for jid in self.xmls:
303
			windows = gajim.interface.instances[self.account][kind]
304
305
			if kind == 'chats':
				# send 'gone' chatstate to every tabbed chat tab
306
				windows[jid].send_chatstate('gone', jid)
nkour's avatar
nkour committed
307
308
				gobject.source_remove(self.possible_paused_timeout_id[jid])
				gobject.source_remove(self.possible_inactive_timeout_id[jid])
309
310
			if gajim.interface.systray_enabled and self.nb_unread[jid] > 0:
				gajim.interface.systray.remove_jid(jid, self.account,
311
					self.get_message_type(jid))
312
			del windows[jid]
313
314
			if self.print_time_timeout_id.has_key(jid):
				gobject.source_remove(self.print_time_timeout_id[jid])
315
316
		if windows.has_key('tabbed'):
			del windows['tabbed']
317
318

	def get_active_jid(self):
Vincent Hanquez's avatar
Vincent Hanquez committed
319
320
		notebook = self.notebook
		active_child = notebook.get_nth_page(notebook.get_current_page())
321
322
		active_jid = ''
		for jid in self.xmls:
323
			if self.childs[jid] == active_child:
324
325
326
327
328
				active_jid = jid
				break
		return active_jid

	def on_close_button_clicked(self, button, jid):
nkour's avatar
nkour committed
329
		'''When close button is pressed: close a tab'''
330
331
		self.remove_tab(jid)

332
	def on_history_menuitem_clicked(self, widget = None, jid = None):
nkour's avatar
nkour committed
333
		'''When history menuitem is pressed: call history window'''
334
335
		if jid is None:
			jid = self.get_active_jid()
336
337
		if gajim.interface.instances['logs'].has_key(jid):
			gajim.interface.instances['logs'][jid].window.present()
338
		else:
339
			gajim.interface.instances['logs'][jid] = history_window.HistoryWindow(jid,
340
				self.account)
341

342
	def on_chat_window_focus_in_event(self, widget, event):
nkour's avatar
nkour committed
343
		'''When window gets focus'''
344
		jid = self.get_active_jid()
nicfit's avatar
nicfit committed
345
		
346
347
		textview = self.conversation_textviews[jid]
		if textview.at_the_end():
348
349
			#we are at the end
			if self.nb_unread[jid] > 0:
350
				self.nb_unread[jid] = 0 + self.get_specific_unread(jid)
351
				self.show_title()
352
353
				if gajim.interface.systray_enabled:
					gajim.interface.systray.remove_jid(jid, self.account,
354
						self.get_message_type(jid))
355
356
357
358
359
360
361
		
		'''TC/GC window received focus, so if we had urgency REMOVE IT
		NOTE: we do not have to read the message (it maybe in a bg tab)
		to remove urgency hint so this functions does that'''
		if gtk.gtk_version >= (2, 8, 0) and gtk.pygtk_version >= (2, 8, 0):
			if widget.props.urgency_hint:
				widget.props.urgency_hint = False
nkour's avatar
nkour committed
362
		# Undo "unread" state display, etc.
nicfit's avatar
nicfit committed
363
364
365
366
367
		if self.widget_name == 'groupchat_window':
			self.redraw_tab(jid, 'active')
		else:
			# NOTE: we do not send any chatstate to preserve inactive, gone, etc.
			self.redraw_tab(jid)
368
	
369
370
371
372
	def on_compact_view_menuitem_activate(self, widget):
		isactive = widget.get_active()
		self.set_compact_view(isactive)

nkour's avatar
nkour committed
373
374
	def on_actions_button_clicked(self, widget):
		'''popup action menu'''
375
		#FIXME: BUG http://bugs.gnome.org/show_bug.cgi?id=316786
376
		self.button_clicked = widget
377
		
nkour's avatar
nkour committed
378
379
		menu = self.prepare_context_menu()
		menu.show_all()
nkour's avatar
nkour committed
380
		menu.popup(None, None, self.position_menu_under_button, 1, 0)
nkour's avatar
nkour committed
381

382
383
	def on_emoticons_button_clicked(self, widget):
		'''popup emoticons menu'''
384
385
386
		#FIXME: BUG http://bugs.gnome.org/show_bug.cgi?id=316786
		self.button_clicked = widget
		self.emoticons_menu.popup(None, None, self.position_menu_under_button, 1, 0)
387

388
389
390
391
	def position_menu_under_button(self, menu):
		#FIXME: BUG http://bugs.gnome.org/show_bug.cgi?id=316786
		# pass btn instance when this bug is over
		button = self.button_clicked
nkour's avatar
nkour committed
392
393
		# here I get the coordinates of the button relative to
		# window (self.window)
394
		button_x, button_y = button.allocation.x, button.allocation.y
395
396
397
398
399
		
		# now convert them to X11-relative
		window_x, window_y = self.window.window.get_origin()
		x = window_x + button_x
		y = window_y + button_y
nkour's avatar
nkour committed
400
401
402
403
404
405
406
407
408
409
410

		menu_width, menu_height = menu.size_request()

		## should we pop down or up?
		if (y + button.allocation.height + menu_height
		    < gtk.gdk.screen_height()):
			# now move the menu below the button
			y += button.allocation.height
		else:
			# now move the menu above the button
			y -= menu_height
411

412

nkour's avatar
nkour committed
413
		# push_in is True so all the menuitems are always inside screen
414
415
416
		push_in = True
		return (x, y, push_in)

417
418
419
	def remove_possible_switch_to_menuitems(self, menu):
		''' remove duplicate 'Switch to' if they exist and return clean menu'''
		childs = menu.get_children()
420
421
422
423
424
425
426
427
428
429

		if self.widget_name == 'tabbed_chat_window':
			jid = self.get_active_jid()
			c = gajim.get_first_contact_instance_from_jid(self.account, jid)
			if _('not in the roster') in c.groups: # for add_to_roster_menuitem
				childs[5].show()
				childs[5].set_no_show_all(False)
			else:
				childs[5].hide()
				childs[5].set_no_show_all(True)
430
431
432
433
434
			
			start_removing_from = 6 # this is from the seperator and after
			
		else:
			start_removing_from = 7 # # this is from the seperator and after
435
				
436
		for child in childs[start_removing_from:]:
nkour's avatar
nkour committed
437
			menu.remove(child)
438

439
440
		return menu
	
nkour's avatar
nkour committed
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
	def prepare_context_menu(self):
		'''sets compact view menuitem active state
		sets active and sensitivity state for toggle_gpg_menuitem
		and remove possible 'Switch to' menuitems'''
		if self.widget_name == 'groupchat_window':
			menu = self.gc_popup_menu
			childs = menu.get_children()
			# compact_view_menuitem
			childs[5].set_active(self.compact_view_current_state)
		elif self.widget_name == 'tabbed_chat_window':
			menu = self.tabbed_chat_popup_menu
			childs = menu.get_children()
			# check if gpg capabitlies or else make gpg toggle insensitive
			jid = self.get_active_jid()
			gpg_btn = self.xmls[jid].get_widget('gpg_togglebutton')
			isactive = gpg_btn.get_active()
			issensitive = gpg_btn.get_property('sensitive')
			childs[3].set_active(isactive)
			childs[3].set_property('sensitive', issensitive)
460
461
462
463
464
465
			# If we don't have resource, we can't do file transfert
			c = gajim.get_first_contact_instance_from_jid(self.account, jid)
			if not c.resource:
				childs[2].set_sensitive(False)
			else:
				childs[2].set_sensitive(True)
nkour's avatar
nkour committed
466
467
468
469
470
471
			# compact_view_menuitem
			childs[4].set_active(self.compact_view_current_state)
		menu = self.remove_possible_switch_to_menuitems(menu)
		
		return menu

472
	def prepare_emoticons_menu(self):
473
474
475
476
477
		menu = gtk.Menu()
	
		def append_emoticon(w, d):
			jid = self.get_active_jid()
			message_textview = self.message_textviews[jid]
478
479
480
			buffer = message_textview.get_buffer()
			if buffer.get_char_count():
				buffer.insert_at_cursor(' %s ' % d)
481
			else: # we are the beginning of buffer
482
				buffer.insert_at_cursor('%s ' % d)
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
			message_textview.grab_focus()
	
		counter = 0
		# Calculate the side lenght of the popup to make it a square
		size = int(round(math.sqrt(len(gajim.interface.emoticons_images))))
		for image in gajim.interface.emoticons_images:
			item = gtk.MenuItem()
			img = gtk.Image()
			if type(image[1]) == gtk.gdk.PixbufAnimation:
				img.set_from_animation(image[1])
			else:
				img.set_from_pixbuf(image[1])
			item.add(img)
			item.connect('activate', append_emoticon, image[0])
			#FIXME: add tooltip with ascii
			menu.attach(item,
					counter % size, counter % size + 1,
					counter / size, counter / size + 1)
			counter += 1
		menu.show_all()
		return menu

505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
	def popup_menu(self, event):
		menu = self.prepare_context_menu()
		# common menuitems (tab switches)
		if len(self.xmls) > 1: # if there is more than one tab
			menu.append(gtk.SeparatorMenuItem()) # seperator
			for jid in self.xmls:
				if jid != self.get_active_jid():
					item = gtk.ImageMenuItem(_('Switch to %s') % self.names[jid])
					img = gtk.image_new_from_stock(gtk.STOCK_JUMP_TO,
						gtk.ICON_SIZE_MENU)
					item.set_image(img)
					item.connect('activate', lambda obj, jid:self.set_active_tab(
						jid), jid)
					menu.append(item)

		# show the menu
		menu.popup(None, None, None, event.button, event.time)
		menu.show_all()

524
	def on_banner_eventbox_button_press_event(self, widget, event):
525
526
		'''If right-clicked, show popup'''
		if event.button == 3: # right click
527
			self.popup_menu(event)
528
529

	def on_chat_notebook_switch_page(self, notebook, page, page_num):
530
531
532
533
		# get the index of the page and then the page that we're leaving
		old_no = notebook.get_current_page()
		old_child = notebook.get_nth_page(old_no)
		
534
		new_child = notebook.get_nth_page(page_num)
535
536
		
		old_jid = ''
537
538
		new_jid = ''
		for jid in self.xmls:
539
			if self.childs[jid] == new_child:
540
				new_jid = jid
541
542
543
544
545
546
547
548
549
			elif self.childs[jid] == old_child:
				old_jid = jid
			
			if old_jid != '' and new_jid != '': # we found both jids
				break # so stop looping
		
		if self.widget_name == 'tabbed_chat_window':
			# send chatstate inactive to the one we're leaving
			# and active to the one we visit
550
551
			if old_jid != '':
				self.send_chatstate('inactive', old_jid)
552
			self.send_chatstate('active', new_jid)
Vincent Hanquez's avatar
Vincent Hanquez committed
553

554
555
		conv_textview = self.conversation_textviews[new_jid]
		if conv_textview.at_the_end():
556
557
			#we are at the end
			if self.nb_unread[new_jid] > 0:
558
				self.nb_unread[new_jid] = 0 + self.get_specific_unread(new_jid)
559
560
				self.redraw_tab(new_jid)
				self.show_title()
561
562
				if gajim.interface.systray_enabled:
					gajim.interface.systray.remove_jid(new_jid, self.account,
563
						self.get_message_type(new_jid))
564

565
		conv_textview.grab_focus()
566

567
	def set_active_tab(self, jid):
568
		self.notebook.set_current_page(self.notebook.page_num(self.childs[jid]))
569
570

	def remove_tab(self, jid, kind): #kind is 'chats' or 'gc'
571
572
		if len(self.xmls) == 1: # only one tab when we asked to remove
			# so destroy window
573

574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
			# we check and possibly save positions here, because Ctrl+W, Escape
			# etc.. call remove_tab so similar code in delete_event callbacks
			# is not enough
			if gajim.config.get('saveposition'):
				if kind == 'chats':
					x, y = self.window.get_position()
					gajim.config.set('chat-x-position', x)
					gajim.config.set('chat-y-position', y)
					width, height = self.window.get_size()
					gajim.config.set('chat-width', width)
					gajim.config.set('chat-height', height)
				elif kind == 'gc':
					gajim.config.set('gc-hpaned-position', self.hpaned_position)
					x, y = self.window.get_position()
					gajim.config.set('gc-x-position', x)
					gajim.config.set('gc-y-position', y)
					width, height = self.window.get_size()
					gajim.config.set('gc-width', width)
					gajim.config.set('gc-height', height)

594
			self.window.destroy()
595
596
597
		else:
			if self.nb_unread[jid] > 0:
				self.nb_unread[jid] = 0
598
599
				if gajim.interface.systray_enabled:
					gajim.interface.systray.remove_jid(jid, self.account,
600
						self.get_message_type(jid))
601
602
603
604
605
			if self.print_time_timeout_id.has_key(jid):
				gobject.source_remove(self.print_time_timeout_id[jid])
				del self.print_time_timeout_id[jid]

			self.notebook.remove_page(self.notebook.page_num(self.childs[jid]))
606

607
608
		if gajim.interface.instances[self.account][kind].has_key(jid):
			del gajim.interface.instances[self.account][kind][jid]
Vincent Hanquez's avatar
Vincent Hanquez committed
609
		del self.nb_unread[jid]
610
		del gajim.last_message_time[self.account][jid]
Vincent Hanquez's avatar
Vincent Hanquez committed
611
		del self.xmls[jid]
612
		del self.childs[jid]
613
614
615
		del self.sent_history[jid]
		del self.sent_history_pos[jid]
		del self.typing_new[jid]
Alex Mauer's avatar
Alex Mauer committed
616
		del self.orig_msg[jid]
617

618
		if len(self.xmls) == 1: # we now have only one tab
nkour's avatar
nkour committed
619
620
			show_tabs_if_one_tab = gajim.config.get('tabs_always_visible')
			self.notebook.set_show_tabs(show_tabs_if_one_tab)
621
622
623
624
			
			if not show_tabs_if_one_tab:
				self.alignment.set_property('top-padding', 0)
			
625
			self.show_title()
626

627
628
629
630
631
632
633
634
635
	def bring_scroll_to_end(self, textview, diff_y = 0):
		''' scrolls to the end of textview if end is not visible '''
		buffer = textview.get_buffer()
		end_iter = buffer.get_end_iter()
		end_rect = textview.get_iter_location(end_iter)
		visible_rect = textview.get_visible_rect()
		# scroll only if expected end is not visible
		if end_rect.y >= (visible_rect.y + visible_rect.height + diff_y):
			gobject.idle_add(self.scroll_to_end_iter, textview)
636

637
638
639
640
641
	def scroll_to_end_iter(self, textview):
		buffer = textview.get_buffer()
		end_iter = buffer.get_end_iter()
		textview.scroll_to_iter(end_iter, 0, False, 1, 1)
		return False
642

643
	def size_request(self, msg_textview , requisition, xml_top):
dkirov's avatar
dkirov committed
644
645
		''' When message_textview changes its size. If the new height
		will enlarge the window, enable the scrollbar automatic policy'''
646
		if msg_textview.window is None:
dkirov's avatar
dkirov committed
647
			return
dkirov's avatar
dkirov committed
648
		message_scrolledwindow = xml_top.get_widget('message_scrolledwindow')
649
650
651

		conversation_scrolledwindow = xml_top.get_widget(
			'conversation_scrolledwindow')
652
		conv_textview = conversation_scrolledwindow.get_children()[0]
dkirov's avatar
dkirov committed
653
654

		min_height = conversation_scrolledwindow.get_property('height-request')
655
		conversation_height = conv_textview.window.get_size()[1]
656
		message_height = msg_textview.window.get_size()[1]
dkirov's avatar
dkirov committed
657
658
659
		# new tab is not exposed yet
		if conversation_height < 2:
			return
660

dkirov's avatar
dkirov committed
661
662
		if conversation_height < min_height:
			min_height = conversation_height
663

dkirov's avatar
dkirov committed
664
		diff_y =  message_height - requisition.height
665
		if diff_y is not 0:
dkirov's avatar
dkirov committed
666
			if  conversation_height + diff_y < min_height:
667
668
669
670
671
672
673
				if message_height + conversation_height - min_height > min_height:
					message_scrolledwindow.set_property('vscrollbar-policy', 
						gtk.POLICY_AUTOMATIC)
					message_scrolledwindow.set_property('hscrollbar-policy', 
						gtk.POLICY_AUTOMATIC)
					message_scrolledwindow.set_property('height-request', 
						message_height + conversation_height - min_height)
674
					self.bring_scroll_to_end(msg_textview)
675
			else:
dkirov's avatar
dkirov committed
676
677
678
679
				message_scrolledwindow.set_property('vscrollbar-policy', 
					gtk.POLICY_NEVER)
				message_scrolledwindow.set_property('hscrollbar-policy', 
					gtk.POLICY_NEVER)
680
				message_scrolledwindow.set_property('height-request', -1)
681
		conv_textview.bring_scroll_to_end(diff_y - 18)
dkirov's avatar
dkirov committed
682
		return True
683
684
685
686
687

	def on_tab_eventbox_button_press_event(self, widget, event, child):
		if event.button == 3:
			n = self.notebook.page_num(child)
			self.notebook.set_current_page(n)
688
			self.popup_menu(event)
689

690
	def new_tab(self, jid):
691
		#FIXME: text formating buttons will be hidden in 0.8 release
692
693
		for w in ('bold_togglebutton', 'italic_togglebutton',
			'underline_togglebutton'):
694
695
			self.xmls[jid].get_widget(w).set_no_show_all(True)

696
		# add ConversationTextView to UI and connect signals
697
698
699
700
701
702
703
		conv_textview = self.conversation_textviews[jid] = \
			conversation_textview.ConversationTextview(self.account)
		conv_textview.show_all()
		conversation_scrolledwindow = self.xmls[jid].get_widget(
			'conversation_scrolledwindow')
		conversation_scrolledwindow.add(conv_textview)
		conv_textview.connect('key_press_event', self.on_conversation_textview_key_press_event)
704
705
706
707
708
709
710
711
712
713
714
715
		
		# add MessageTextView to UI and connect signals
		message_scrolledwindow = self.xmls[jid].get_widget(
			'message_scrolledwindow')
		msg_textview = self.message_textviews[jid] = \
			message_textview.MessageTextView()
		msg_textview.connect('mykeypress',
			self.on_message_textview_mykeypress_event)
		message_scrolledwindow.add(msg_textview)
		msg_textview.connect('key_press_event',
			self.on_message_textview_key_press_event)
		
716
		self.set_compact_view(self.always_compact_view)
717
		self.nb_unread[jid] = 0
718
		gajim.last_message_time[self.account][jid] = 0
719
		font = pango.FontDescription(gajim.config.get('conversation_font'))
720
		
nkour's avatar
nkour committed
721
		if gajim.config.get('use_speller') and 'gtkspell' in globals():
722
			try:
723
				gtkspell.Spell(msg_textview)
724
			except gobject.GError, msg:
nkour's avatar
nkour committed
725
				#FIXME: add a ui for this use spell.set_language()
726
				dialogs.ErrorDialog(unicode(msg), _('If that is not your language for which you want to highlight misspelled words, then please set your $LANG as appropriate. Eg. for French do export LANG=fr_FR or export LANG=fr_FR.UTF-8 in ~/.bash_profile or to make it global in /etc/profile.\n\nHighlighting misspelled words feature will not be used')).get_response()
727
				gajim.config.set('use_speller', False)
nkour's avatar
nkour committed
728
		
729
730
731
732
733
		emoticons_button = self.xmls[jid].get_widget('emoticons_button')
		# set image no matter if user wants at this time emoticons or not
		# (so toggle works ok)
		img = self.xmls[jid].get_widget('emoticons_button_image')
		img.set_from_file(os.path.join(gajim.DATA_DIR, 'emoticons', 'smile.png'))
734
		if gajim.config.get('useemoticons'):
735
736
			emoticons_button.show()
			emoticons_button.set_no_show_all(False)
737
		else:
738
739
			emoticons_button.hide()
			emoticons_button.set_no_show_all(True)
740

741
742
743
744
		conv_textview.modify_font(font)
		conv_buffer = conv_textview.get_buffer()
		end_iter = conv_buffer.get_end_iter()

745
		self.xmls[jid].signal_autoconnect(self)
746
		conversation_scrolledwindow.get_vadjustment().connect('value-changed',
747
			self.on_conversation_vadjustment_value_changed)
nkour's avatar
nkour committed
748

749
750
		if len(self.xmls) > 1:
			self.notebook.set_show_tabs(True)
nkour's avatar
nkour committed
751
			self.alignment.set_property('top-padding', 2)
752

nkour's avatar
nkour committed
753
		if self.widget_name == 'tabbed_chat_window':
754
755
			xm = gtk.glade.XML(GTKGUI_GLADE, 'chats_eventbox', APP)
			tab_hbox = xm.get_widget('chats_eventbox')
nkour's avatar
nkour committed
756
		elif self.widget_name == 'groupchat_window':
757
758
759
760
761
			xm = gtk.glade.XML(GTKGUI_GLADE, 'gc_eventbox', APP)
			tab_hbox = xm.get_widget('gc_eventbox')

		child = self.childs[jid]

762
763
		xm.signal_connect('on_close_button_clicked',
			self.on_close_button_clicked, jid)
764
765
		xm.signal_connect('on_tab_eventbox_button_press_event',
			self.on_tab_eventbox_button_press_event, child)
766

nkour's avatar
nkour committed
767
		self.notebook.append_page(child, tab_hbox)
768
		
769
770
		msg_textview.modify_font(font)
		msg_textview.connect('size-request', self.size_request,
dkirov's avatar
dkirov committed
771
			self.xmls[jid])
772
		# init new sent history for this conversation
773
774
775
776
777
		self.sent_history[jid] = []
		self.sent_history_pos[jid] = 0
		self.typing_new[jid] = True
		self.orig_msg[jid] = ''

778
779
		self.show_title()

780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
	def on_message_textview_key_press_event(self, widget, event):
		jid = self.get_active_jid()
		conv_textview = self.conversation_textviews[jid]
		
		if self.widget_name == 'groupchat_window':
			if event.keyval not in (gtk.keysyms.ISO_Left_Tab, gtk.keysyms.Tab):
				room_jid = self.get_active_jid()
				self.last_key_tabs[room_jid] = False
		
		if event.keyval == gtk.keysyms.Page_Down: # PAGE DOWN
			if event.state & gtk.gdk.CONTROL_MASK: # CTRL + PAGE DOWN
				self.notebook.emit('key_press_event', event)
			elif event.state & gtk.gdk.SHIFT_MASK: # SHIFT + PAGE DOWN
				conv_textview.emit('key_press_event', event)
		elif event.keyval == gtk.keysyms.Page_Up: # PAGE UP
			if event.state & gtk.gdk.CONTROL_MASK: # CTRL + PAGE UP
				self.notebook.emit('key_press_event', event)
			elif event.state & gtk.gdk.SHIFT_MASK: # SHIFT + PAGE UP
				conv_textview.emit('key_press_event', event)
	
800
	def on_conversation_textview_key_press_event(self, widget, event):
nkour's avatar
nkour committed
801
		'''Do not block these events and send them to the notebook'''
Yann Leboulanger's avatar
use    
Yann Leboulanger committed
802
		if event.state & gtk.gdk.CONTROL_MASK:
Yann Leboulanger's avatar
Yann Leboulanger committed
803
			if event.keyval == gtk.keysyms.Tab: # CTRL + TAB
804
				self.notebook.emit('key_press_event', event)
Yann Leboulanger's avatar
use    
Yann Leboulanger committed
805
806
			elif event.keyval == gtk.keysyms.ISO_Left_Tab: # CTRL + SHIFT + TAB
				self.notebook.emit('key_press_event', event)
Yann Leboulanger's avatar
Yann Leboulanger committed
807
808
809
810
			elif event.keyval == gtk.keysyms.Page_Down: # CTRL + PAGE DOWN
				self.notebook.emit('key_press_event', event)
			elif event.keyval == gtk.keysyms.Page_Up: # CTRL + PAGE UP
				self.notebook.emit('key_press_event', event)
811
			elif event.keyval == gtk.keysyms.l or \
812
				event.keyval == gtk.keysyms.L: # CTRL + L
nkour's avatar
nkour committed
813
				jid = self.get_active_jid()
814
815
				conv_textview = self.conversation_textviews[jid]
				conv_textview.get_buffer().set_text('')
816
			elif event.keyval == gtk.keysyms.v: # CTRL + V
817
				jid = self.get_active_jid()
nkour's avatar
nkour committed
818
				msg_textview = self.message_textviews[jid]
819
820
821
				if not msg_textview.is_focus():
					msg_textview.grab_focus()
				msg_textview.emit('key_press_event', event)
822

823
	def on_chat_notebook_key_press_event(self, widget, event):
824
		st = '1234567890' # alt+1 means the first tab (tab 0)
825
		jid = self.get_active_jid()
826
827
		if event.keyval == gtk.keysyms.Escape: # ESCAPE
			if self.widget_name == 'tabbed_chat_window':
828
				self.remove_tab(jid)
Yann Leboulanger's avatar
Yann Leboulanger committed
829
		elif event.keyval == gtk.keysyms.F4 and \
830
			(event.state & gtk.gdk.CONTROL_MASK): # CTRL + F4
Yann Leboulanger's avatar
Yann Leboulanger committed
831
				self.remove_tab(jid)
nkour's avatar
nkour committed
832
833
834
		elif event.keyval == gtk.keysyms.w and \
			(event.state & gtk.gdk.CONTROL_MASK): # CTRL + W
				self.remove_tab(jid)
Vincent Hanquez's avatar
Vincent Hanquez committed
835
		elif event.string and event.string in st and \
836
			(event.state & gtk.gdk.MOD1_MASK): # alt + 1,2,3..
837
			self.notebook.set_current_page(st.index(event.string))
838
		elif event.keyval == gtk.keysyms.c and \
839
840
			(event.state & gtk.gdk.MOD1_MASK): # alt + C toggles compact view
			self.set_compact_view(not self.compact_view_current_state)
841
842
843
		elif event.keyval == gtk.keysyms.e and \
			(event.state & gtk.gdk.MOD1_MASK): # alt + E opens emoticons menu
			if gajim.config.get('useemoticons'):
nkour's avatar
nkour committed
844
845
846
				msg_tv = self.message_textviews[jid]
				def set_emoticons_menu_position(w, msg_tv = msg_tv):
					window = msg_tv.get_window(gtk.TEXT_WINDOW_WIDGET)
847
					# get the window position
848
849
					origin = window.get_origin()
					size = window.get_size()
nkour's avatar
nkour committed
850
					buf = msg_tv.get_buffer()
851
					# get the cursor position
nkour's avatar
nkour committed
852
					cursor = msg_tv.get_iter_location(buf.get_iter_at_mark(
853
						buf.get_insert()))
nkour's avatar
nkour committed
854
					cursor =  msg_tv.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT,
855
856
857
858
						cursor.x, cursor.y)
					x = origin[0] + cursor[0]
					y = origin[1] + size[1]
					menu_width, menu_height = self.emoticons_menu.size_request()
nkour's avatar
nkour committed
859
860
861
862
863
864
					#FIXME: get_line_count is not so good
					#get the iter of cursor, then tv.get_line_yrange
					# so we know in which y we are typing (not how many lines we have
					# then go show just above the current cursor line for up
					# or just below the current cursor line for down
					#TEST with having 3 lines and writing in the 2nd
865
					if y + menu_height > gtk.gdk.screen_height():
nkour's avatar
nkour committed
866
867
						# move menu just above cursor
						y -= menu_height + (msg_tv.allocation.height / buf.get_line_count())
nkour's avatar
nkour committed
868
869
					#else: # move menu just below cursor
					#	y -= (msg_tv.allocation.height / buf.get_line_count())
870
					return (x, y, True) # push_in True
871
				self.emoticons_menu.popup(None, None, set_emoticons_menu_position, 1, 0)
Yann Leboulanger's avatar
Yann Leboulanger committed
872
		elif event.keyval == gtk.keysyms.Page_Down:
873
			if event.state & gtk.gdk.SHIFT_MASK: # SHIFT + PAGE DOWN
874
875
				conv_textview = self.conversation_textviews[jid]
				rect = conv_textview.get_visible_rect()
nkour's avatar
nkour committed
876
				iter = conv_textview.get_iter_at_location(rect.x,
877
					rect.y + rect.height)
878
				conv_textview.scroll_to_iter(iter, 0.1, True, 0, 0)
Yann Leboulanger's avatar
Yann Leboulanger committed
879
		elif event.keyval == gtk.keysyms.Page_Up: 
880
			if event.state & gtk.gdk.SHIFT_MASK: # SHIFT + PAGE UP
881
882
883
884
				conv_textview = self.conversation_textviews[jid]
				rect = conv_textview.get_visible_rect()
				iter = conv_textview.get_iter_at_location(rect.x, rect.y)
				conv_textview.scroll_to_iter(iter, 0.1, True, 0, 1)
Yann Leboulanger's avatar
Yann Leboulanger committed
885
886
		elif event.keyval == gtk.keysyms.Up: 
			if event.state & gtk.gdk.SHIFT_MASK: # SHIFT + UP
Vincent Hanquez's avatar
Vincent Hanquez committed
887
888
				conversation_scrolledwindow = self.xml.get_widget('conversation_scrolledwindow')
				conversation_scrolledwindow.emit('scroll-child',
Yann Leboulanger's avatar
Yann Leboulanger committed
889
					gtk.SCROLL_PAGE_BACKWARD, False)
Yann Leboulanger's avatar
use    
Yann Leboulanger committed
890
		elif event.keyval == gtk.keysyms.ISO_Left_Tab: # SHIFT + TAB
891
			if event.state & gtk.gdk.CONTROL_MASK: # CTRL + SHIFT + TAB
Yann Leboulanger's avatar
Yann Leboulanger committed
892
893
				current = self.notebook.get_current_page()
				if current > 0:
894
895
					self.notebook.prev_page()
				else: # traverse for ever (eg. don't stop at first tab)
Yann Leboulanger's avatar
Yann Leboulanger committed
896
					self.notebook.set_current_page(self.notebook.get_n_pages()-1)
Yann Leboulanger's avatar
use    
Yann Leboulanger committed
897
898
		elif event.keyval == gtk.keysyms.Tab: # TAB
			if event.state & gtk.gdk.CONTROL_MASK: # CTRL + TAB
Yann Leboulanger's avatar
Yann Leboulanger committed
899
900
				current = self.notebook.get_current_page()
				if current < (self.notebook.get_n_pages()-1):
901
902
					self.notebook.next_page()
				else: # traverse for ever (eg. don't stop at last tab)
Yann Leboulanger's avatar
Yann Leboulanger committed
903
					self.notebook.set_current_page(0)
904
905
		elif (event.keyval == gtk.keysyms.l or event.keyval == gtk.keysyms.L) \
				and event.state & gtk.gdk.CONTROL_MASK: # CTRL + L
906
907
			conv_textview = self.conversation_textviews[jid]
			conv_textview.get_buffer().set_text('')
908
909
		elif event.keyval == gtk.keysyms.v and event.state & gtk.gdk.CONTROL_MASK:
			# CTRL + V
nkour's avatar
nkour committed
910
			msg_textview = self.message_textviews[jid]
911
912
913
			if not msg_textview.is_focus():
				msg_textview.grab_focus()
			msg_textview.emit('key_press_event', event)
914
915
916
		elif event.state & gtk.gdk.CONTROL_MASK or \
			  (event.keyval == gtk.keysyms.Control_L) or \
			  (event.keyval == gtk.keysyms.Control_R):
Vincent Hanquez's avatar
Vincent Hanquez committed
917
918
919
			# we pressed a control key or ctrl+sth: we don't block
			# the event in order to let ctrl+c (copy text) and
			# others do their default work
920
			pass
921
		else: # it's a normal key press make sure message_textview has focus
nkour's avatar
nkour committed
922
			msg_textview = self.message_textviews[jid]
923
924
925
926
			if msg_textview.get_property('sensitive'):
				if not msg_textview.is_focus():
					msg_textview.grab_focus()
				msg_textview.emit('key_press_event', event)
927
928
929
930
931

	def on_conversation_vadjustment_value_changed(self, widget):
		jid = self.get_active_jid()
		if not self.nb_unread[jid]:
			return
932
933
		conv_textview = self.conversation_textviews[jid]
		if conv_textview.at_the_end() and self.window.is_active():
934
			#we are at the end
935
			self.nb_unread[jid] = self.get_specific_unread(jid)
936
937
			self.redraw_tab(jid)
			self.show_title()
938
939
			if gajim.interface.systray_enabled:
				gajim.interface.systray.remove_jid(jid, self.account,
940
					self.get_message_type(jid))
941
942
943

	def clear(self, tv):
		buffer = tv.get_buffer()
944
945
946
		start, end = buffer.get_bounds()
		buffer.delete(start, end)

Vincent Hanquez's avatar
Vincent Hanquez committed
947
	def print_conversation_line(self, text, jid, kind, name, tim,
948
			other_tags_for_name = [], other_tags_for_time = [], 
949
			other_tags_for_text = [], count_as_new = True, subject = None):
950
		'''prints 'chat' type messages'''
951
		textview = self.conversation_textviews[jid