filetransfers_window.py 32.8 KB
Newer Older
1 2
##	filetransfers_window.py
##
3 4 5
## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
## Copyright (C) 2005
6 7
##                    Dimitur Kirov <dkirov@gmail.com>
##                    Travis Shirk <travis@pobox.com>
8
## Copyright (C) 2004-2005 Vincent Hanquez <tab@snarc.org>
9 10 11 12 13 14 15 16 17 18 19 20 21
##
## 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 gobject
dkirov's avatar
dkirov committed
22
import pango
23
import os
dkirov's avatar
dkirov committed
24
import time
25 26 27 28 29 30 31 32

import gtkgui_helpers
import tooltips
import dialogs

from common import gajim
from common import helpers

dkirov's avatar
dkirov committed
33 34 35
C_IMAGE = 0
C_LABELS = 1
C_FILE = 2
36 37 38
C_TIME = 3
C_PROGRESS = 4
C_PERCENT = 5
dkirov's avatar
dkirov committed
39 40 41
C_SID = 6


42
class FileTransfersWindow:
43
	def __init__(self):
44 45
		self.files_props = {'r' : {}, 's': {}}
		self.height_diff = 0
46
		self.xml = gtkgui_helpers.get_glade('filetransfers.glade')
47 48 49 50
		self.window = self.xml.get_widget('file_transfers_window')
		self.tree = self.xml.get_widget('transfers_list')
		self.cancel_button = self.xml.get_widget('cancel_button')
		self.pause_button = self.xml.get_widget('pause_restore_button')
nkour's avatar
nkour committed
51
		self.cleanup_button = self.xml.get_widget('cleanup_button')
52 53 54 55 56 57 58
		self.notify_ft_checkbox = self.xml.get_widget(
			'notify_ft_complete_checkbox')
		notify = gajim.config.get('notify_on_file_complete')
		if notify:
			self.notify_ft_checkbox.set_active(True)
		else:
			self.notify_ft_checkbox.set_active(False)
59
		self.model = gtk.ListStore(gtk.gdk.Pixbuf, str, str, str, str, int, str)
60 61
		self.tree.set_model(self.model)
		col = gtk.TreeViewColumn()
62

63
		render_pixbuf = gtk.CellRendererPixbuf()
64

65 66 67 68 69 70
		col.pack_start(render_pixbuf, expand = True)
		render_pixbuf.set_property('xpad', 3)
		render_pixbuf.set_property('ypad', 3)
		render_pixbuf.set_property('yalign', .0)
		col.add_attribute(render_pixbuf, 'pixbuf', 0)
		self.tree.append_column(col)
71

72 73 74
		col = gtk.TreeViewColumn(_('File'))
		renderer = gtk.CellRendererText()
		col.pack_start(renderer, expand=False)
dkirov's avatar
dkirov committed
75
		col.add_attribute(renderer, 'markup' , C_LABELS)
76 77 78
		renderer.set_property('yalign', 0.)
		renderer = gtk.CellRendererText()
		col.pack_start(renderer, expand=True)
dkirov's avatar
dkirov committed
79
		col.add_attribute(renderer, 'markup' , C_FILE)
80 81
		renderer.set_property('xalign', 0.)
		renderer.set_property('yalign', 0.)
dkirov's avatar
dkirov committed
82
		renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
83 84 85
		col.set_resizable(True)
		col.set_expand(True)
		self.tree.append_column(col)
86

dkirov's avatar
dkirov committed
87 88 89 90 91 92 93 94 95 96
		col = gtk.TreeViewColumn(_('Time'))
		renderer = gtk.CellRendererText()
		col.pack_start(renderer, expand=False)
		col.add_attribute(renderer, 'markup' , C_TIME)
		renderer.set_property('yalign', 0.5)
		renderer.set_property('xalign', 0.5)
		renderer = gtk.CellRendererText()
		renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
		col.set_resizable(True)
		col.set_expand(False)
97
		self.tree.append_column(col)
98

99 100 101 102 103 104 105 106
		col = gtk.TreeViewColumn(_('Progress'))
		renderer = gtk.CellRendererProgress()
		renderer.set_property('yalign', 0.5)
		renderer.set_property('xalign', 0.5)
		col.pack_start(renderer, expand = False)
		col.add_attribute(renderer, 'text' , C_PROGRESS)
		col.add_attribute(renderer, 'value' , C_PERCENT)
		col.set_resizable(True)
107
		col.set_expand(False)
108
		self.tree.append_column(col)
109

110 111 112 113
		self.set_images()
		self.tree.get_selection().set_mode(gtk.SELECTION_SINGLE)
		self.tree.get_selection().connect('changed', self.selection_changed)
		self.tooltip = tooltips.FileTransfersTooltip()
114 115 116 117 118
		self.file_transfers_menu = self.xml.get_widget('file_transfers_menu')
		self.open_folder_menuitem = self.xml.get_widget('open_folder_menuitem')
		self.cancel_menuitem = self.xml.get_widget('cancel_menuitem')
		self.pause_menuitem = self.xml.get_widget('pause_menuitem')
		self.continue_menuitem = self.xml.get_widget('continue_menuitem')
119 120
		self.continue_menuitem.hide()
		self.continue_menuitem.set_no_show_all(True)
121 122
		self.remove_menuitem = self.xml.get_widget('remove_menuitem')
		self.xml.signal_autoconnect(self)
123

124 125 126
	def find_transfer_by_jid(self, account, jid):
		''' find all transfers with peer 'jid' that belong to 'account' '''
		active_transfers = [[],[]] # ['senders', 'receivers']
127

128 129 130
		# 'account' is the sender
		for file_props in self.files_props['s'].values():
			if file_props['tt_account'] == account:
131
				receiver_jid = unicode(file_props['receiver']).split('/')[0]
132 133 134
				if jid == receiver_jid:
					if not self.is_transfer_stoped(file_props):
						active_transfers[0].append(file_props)
135

136 137 138
		# 'account' is the recipient
		for file_props in self.files_props['r'].values():
			if file_props['tt_account'] == account:
139
				sender_jid = unicode(file_props['sender']).split('/')[0]
140 141 142 143
				if jid == sender_jid:
					if not self.is_transfer_stoped(file_props):
						active_transfers[1].append(file_props)
		return active_transfers
144

145
	def show_completed(self, jid, file_props):
dkirov's avatar
dkirov committed
146
		''' show a dialog saying that file (file_props) has been transferred'''
147
		def on_open(widget, file_props):
dkirov's avatar
dkirov committed
148
			dialog.destroy()
149 150 151 152 153 154 155
			if not file_props.has_key('file-name'):
				return
			(path, file) = os.path.split(file_props['file-name'])
			if os.path.exists(path) and os.path.isdir(path):
				helpers.launch_file_manager(path)
			self.tree.get_selection().unselect_all()

156
		if file_props['type'] == 'r':
dkirov's avatar
dkirov committed
157
			# file path is used below in 'Save in'
158 159 160
			(file_path, file_name) = os.path.split(file_props['file-name'])
		else:
			file_name = file_props['name']
161
		sectext = '\t' + _('Filename: %s') % file_name
162 163
		sectext += '\n\t' + _('Size: %s') % \
		helpers.convert_bytes(file_props['size'])
164
		if file_props['type'] == 'r':
165
			jid = unicode(file_props['sender']).split('/')[0]
166
			sender_name = gajim.contacts.get_first_contact_from_jid( 
167
				file_props['tt_account'], jid).get_shown_name()
168
			sender = sender_name
169
		else:
170
			#You is a reply of who sent a file
171
			sender = _('You')
172 173 174
		sectext += '\n\t' +_('Sender: %s') % sender
		sectext += '\n\t' +_('Recipient: ')
		if file_props['type'] == 's':
175
			jid = unicode(file_props['receiver']).split('/')[0]
176
			receiver_name = gajim.contacts.get_first_contact_from_jid( 
177
				file_props['tt_account'], jid).get_shown_name()
178
			recipient = receiver_name
179
		else:
180
			#You is a reply of who received a file
181
			recipient = _('You')
182
		sectext += recipient
183
		if file_props['type'] == 'r':
184
			sectext += '\n\t' +_('Saved in: %s') % file_path
dkirov's avatar
dkirov committed
185
		dialog = dialogs.HigDialog(None, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE, 
dkirov's avatar
dkirov committed
186
				_('File transfer completed'), sectext)
187
		if file_props['type'] == 'r':
188 189
			button = gtk.Button(_('_Open Containing Folder'))
			button.connect('clicked', on_open, file_props)
dkirov's avatar
dkirov committed
190 191
			dialog.action_area.pack_start(button)
		ok_button = dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
192
		def on_ok(widget):
dkirov's avatar
dkirov committed
193
			dialog.destroy()
194
		ok_button.connect('clicked', on_ok)
dkirov's avatar
dkirov committed
195
		dialog.show_all()
196

197
	def show_request_error(self, file_props):
dkirov's avatar
dkirov committed
198 199
		''' show error dialog to the recipient saying that transfer 
		has been canceled'''
200
		dialogs.InformationDialog(_('File transfer cancelled'), _('Connection with peer cannot be established.'))
201
		self.tree.get_selection().unselect_all()
202

203
	def show_send_error(self, file_props):
dkirov's avatar
dkirov committed
204 205
		''' show error dialog to the sender saying that transfer 
		has been canceled'''
206
		dialogs.InformationDialog(_('File transfer cancelled'),
207 208
_('Connection with peer cannot be established.'))
		self.tree.get_selection().unselect_all()
209

210
	def show_stopped(self, jid, file_props, error_msg = ''):
211
		if file_props['type'] == 'r':
dkirov's avatar
dkirov committed
212
			file_name = os.path.basename(file_props['file-name'])
213 214
		else:
			file_name = file_props['name']
215
		sectext = '\t' + _('Filename: %s') % file_name
216
		sectext += '\n\t' + _('Recipient: %s') % jid
217 218
		if error_msg:
			sectext += '\n\t' + _('Error message: %s') % error_msg
219 220
		dialogs.ErrorDialog(_('File transfer stopped by the contact at the other '
			'end'), sectext)
221
		self.tree.get_selection().unselect_all()
222

223
	def show_file_send_request(self, account, contact):
224 225
		def on_ok(widget):
			file_dir = None
dkirov's avatar
dkirov committed
226
			files_path_list = dialog.get_filenames()
227 228 229 230 231 232 233
			files_path_list = gtkgui_helpers.decode_filechooser_file_paths(
				files_path_list)
			for file_path in files_path_list:
				if self.send_file(account, contact, file_path) and file_dir is None:
					file_dir = os.path.dirname(file_path)
			if file_dir:
				gajim.config.set('last_send_dir', file_dir)
dkirov's avatar
dkirov committed
234
				dialog.destroy()
235

dkirov's avatar
dkirov committed
236
		dialog = dialogs.FileChooserDialog(_('Choose File to Send...'), 
237 238 239 240
			gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL),
			gtk.RESPONSE_OK,
			True, # select multiple true as we can select many files to send
			gajim.config.get('last_send_dir'),
241 242
			on_response_ok = on_ok,
			on_response_cancel = lambda e:dialog.destroy()
243 244
			)

245 246 247 248 249 250
		btn = gtk.Button(_('_Send'))
		btn.set_property('can-default', True)
		# FIXME: add send icon to this button (JUMP_TO)
		dialog.add_action_widget(btn, gtk.RESPONSE_OK)
		dialog.set_default_response(gtk.RESPONSE_OK)
		btn.show()
251

252
	def send_file(self, account, contact, file_path):
dkirov's avatar
dkirov committed
253
		''' start the real transfer(upload) of the file '''
254 255 256
		if gtkgui_helpers.file_is_locked(file_path):
			pritext = _('Gajim cannot access this file')
			sextext = _('This file is being used by another process.')
257
			dialogs.ErrorDialog(pritext, sextext)
258
			return
259

260
		if isinstance(contact, str):
dkirov's avatar
dkirov committed
261 262
			if contact.find('/') == -1:
				return
263
			(jid, resource) = contact.split('/', 1)
264 265
			contact = gajim.contacts.create_contact(jid = jid,
				resource = resource)
266 267 268
		(file_dir, file_name) = os.path.split(file_path)
		file_props = self.get_send_file_props(account, contact, 
				file_path, file_name)
269 270
		if file_props is None:
			return False
271 272
		self.add_transfer(account, contact, file_props)
		gajim.connections[account].send_file_request(file_props)
273
		return True
274

275 276 277 278 279 280 281 282
	def _start_receive(self, file_path, account, contact, file_props):
		file_dir = os.path.dirname(file_path)
		if file_dir:
			gajim.config.set('last_save_dir', file_dir)
		file_props['file-name'] = file_path
		self.add_transfer(account, contact, file_props)
		gajim.connections[account].send_file_approval(file_props)

283
	def show_file_request(self, account, contact, file_props):
dkirov's avatar
dkirov committed
284 285
		''' show dialog asking for comfirmation and store location of new
		file requested by a contact'''
286 287
		if file_props is None or not file_props.has_key('name'):
			return
288
		sec_text = '\t' + _('File: %s') % file_props['name']
289 290 291 292
		if file_props.has_key('size'):
			sec_text += '\n\t' + _('Size: %s') % \
				helpers.convert_bytes(file_props['size'])
		if file_props.has_key('mime-type'):
293
			sec_text += '\n\t' + _('Type: %s') % file_props['mime-type']
294
		if file_props.has_key('desc'):
295
			sec_text += '\n\t' + _('Description: %s') % file_props['desc']
296
		prim_text = _('%s wants to send you a file:') % contact.jid
dkirov's avatar
dkirov committed
297
		dialog, dialog2 = None, None
Yann Leboulanger's avatar
typo  
Yann Leboulanger committed
298

299
		def on_response_ok(widget, account, contact, file_props):
dkirov's avatar
dkirov committed
300
			dialog.destroy()
301 302

			def on_ok(widget, account, contact, file_props):
dkirov's avatar
dkirov committed
303
				file_path = dialog2.get_filename()
304 305
				file_path = gtkgui_helpers.decode_filechooser_file_paths(
					(file_path,))[0]
306
				if os.path.exists(file_path):
307 308 309 310 311 312
					# check if we have write permissions
					if not os.access(file_path, os.W_OK):
						file_name = os.path.basename(file_path)
						dialogs.ErrorDialog(_('Cannot overwrite existing file "%s"' % file_name),
						_('A file with this name already exists and you do not have permission to overwrite it.'))
						return
313 314 315 316 317 318 319
					stat = os.stat(file_path)
					dl_size = stat.st_size
					file_size = file_props['size']
					dl_finished = dl_size >= file_size
					dialog = dialogs.FTOverwriteConfirmationDialog(
						_('This file already exists'), _('What do you want to do?'),
						not dl_finished)
dkirov's avatar
dkirov committed
320 321
					dialog.set_transient_for(dialog2)
					dialog.set_destroy_with_parent(True)
322
					response = dialog.get_response()
dkirov's avatar
dkirov committed
323
					if response < 0:
324
						return
325 326
					elif response == 100:
						file_props['offset'] = dl_size
327 328
				else:
					dirname = os.path.dirname(file_path)
329 330 331
					if not os.access(dirname, os.W_OK) and os.name != 'nt':
						# read-only bit is used to mark special folder under windows,
						# not to mark that a folder is read-only. See ticket #3587
332
						dialogs.ErrorDialog(_('Directory "%s" is not writable') % dirname, _('You do not have permission to create files in this directory.'))
333
						return
dkirov's avatar
dkirov committed
334
				dialog2.destroy()
335 336 337
				self._start_receive(file_path, account, contact, file_props)

			def on_cancel(widget, account, contact, file_props):
dkirov's avatar
dkirov committed
338
				dialog2.destroy()
339 340
				gajim.connections[account].send_file_rejection(file_props)

dkirov's avatar
dkirov committed
341
			dialog2 = dialogs.FileChooserDialog(
342 343 344 345 346 347 348 349
				title_text = _('Save File as...'), 
				action = gtk.FILE_CHOOSER_ACTION_SAVE, 
				buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, 
				gtk.STOCK_SAVE, gtk.RESPONSE_OK),
				default_response = gtk.RESPONSE_OK,
				current_folder = gajim.config.get('last_save_dir'),
				on_response_ok = (on_ok, account, contact, file_props),
				on_response_cancel = (on_cancel, account, contact, file_props))
Yann Leboulanger's avatar
typo  
Yann Leboulanger committed
350

dkirov's avatar
dkirov committed
351 352 353
			dialog2.set_current_name(file_props['name'])
			dialog2.connect('delete-event', lambda widget, event:
				on_cancel(widget, account, contact, file_props))
354 355

		def on_response_cancel(widget, account, file_props):
dkirov's avatar
dkirov committed
356
			dialog.destroy()
357
			gajim.connections[account].send_file_rejection(file_props)
358

dkirov's avatar
dkirov committed
359
		dialog = dialogs.NonModalConfirmationDialog(prim_text, sec_text,
360 361
			on_response_ok = (on_response_ok, account, contact, file_props),
			on_response_cancel = (on_response_cancel, account, file_props))
dkirov's avatar
dkirov committed
362 363 364
		dialog.connect('delete-event', lambda widget, event: 
			on_response_cancel(widget, account, file_props))
		dialog.popup()
365

366
	def set_images(self):
dkirov's avatar
dkirov committed
367
		''' create pixbufs for status images in transfer rows'''
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
		self.images = {}
		self.images['upload'] = self.window.render_icon(gtk.STOCK_GO_UP, 
			gtk.ICON_SIZE_MENU)
		self.images['download'] = self.window.render_icon(gtk.STOCK_GO_DOWN, 
			gtk.ICON_SIZE_MENU)
		self.images['stop'] = self.window.render_icon(gtk.STOCK_STOP, 
			gtk.ICON_SIZE_MENU)
		self.images['waiting'] = self.window.render_icon(gtk.STOCK_REFRESH, 
			gtk.ICON_SIZE_MENU)
		self.images['pause'] = self.window.render_icon(gtk.STOCK_MEDIA_PAUSE, 
			gtk.ICON_SIZE_MENU)
		self.images['continue'] = self.window.render_icon(gtk.STOCK_MEDIA_PLAY, 
			gtk.ICON_SIZE_MENU)
		self.images['ok'] = self.window.render_icon(gtk.STOCK_APPLY, 
			gtk.ICON_SIZE_MENU)
383

384
	def set_status(self, typ, sid, status):
dkirov's avatar
dkirov committed
385
		''' change the status of a transfer to state 'status' '''
386 387 388
		iter = self.get_iter_by_sid(typ, sid)
		if iter is None:
			return
dkirov's avatar
dkirov committed
389
		sid = self.model[iter][C_SID].decode('utf-8')
390 391 392 393 394
		file_props = self.files_props[sid[0]][sid[1:]]
		if status == 'stop':
			file_props['stopped'] = True
		elif status == 'ok':
			file_props['completed'] = True
dkirov's avatar
dkirov committed
395
		self.model.set(iter, C_IMAGE, self.images[status])
396 397 398
		path = self.model.get_path(iter)
		self.select_func(path)

dkirov's avatar
dkirov committed
399
	def _format_percent(self, percent):
dkirov's avatar
dkirov committed
400 401 402 403 404 405 406 407 408
		''' add extra spaces from both sides of the percent, so that
		progress string has always a fixed size'''
		_str = '          '
		if percent != 100.:
			_str += ' '
		if percent < 10:
			_str += ' '
		_str += unicode(percent) + '%          \n'
		return _str
409

dkirov's avatar
dkirov committed
410 411 412 413 414 415 416 417 418
	def _format_time(self, _time):
		times = { 'hours': 0, 'minutes': 0, 'seconds': 0 }
		_time = int(_time)
		times['seconds'] = _time % 60
		if _time >= 60:
			_time /= 60
			times['minutes'] = _time % 60
			if _time >= 60:
				times['hours'] = _time / 60
419

dkirov's avatar
dkirov committed
420
		#Print remaining time in format 00:00:00
421
		#You can change the places of (hours), (minutes), (seconds) -
dkirov's avatar
dkirov committed
422 423
		#they are not translatable.
		return _('%(hours)02.d:%(minutes)02.d:%(seconds)02.d')  % times
424

dkirov's avatar
dkirov committed
425 426 427 428 429 430 431 432 433
	def _get_eta_and_speed(self, full_size, transfered_size, elapsed_time):
		if elapsed_time == 0:
			return 0., 0.
		speed = round(float(transfered_size) / elapsed_time)
		if speed == 0.:
			return 0., 0.
		remaining_size = full_size - transfered_size
		eta = remaining_size / speed
		return eta, speed
434

435 436 437 438 439 440 441 442
	def _remove_transfer(self, iter, sid, file_props):
		self.model.remove(iter)
		if  file_props.has_key('tt_account'):
			# file transfer is set
			account = file_props['tt_account']
			if gajim.connections.has_key(account):
				# there is a connection to the account
				gajim.connections[account].remove_transfer(file_props)
443 444 445 446 447 448 449 450
			if file_props['type'] == 'r': # we receive a file
				other = file_props['sender']
			else: # we send a file
				other = file_props['receiver']
			if isinstance(other, unicode):
				jid = gajim.get_jid_without_resource(other)
			else: # It's a Contact instance
				jid = other.jid
451 452 453
			for ev_type in ('file-error', 'file-completed', 'file-request-error',
			'file-send-error', 'file-stopped'):
				for event in gajim.events.get_events(account, jid, [ev_type]):
454
					if event.parameters['sid'] == file_props['sid']:
455
						gajim.events.remove_events(account, jid, event)
456 457
						gajim.interface.roster.draw_contact(jid, account)
						gajim.interface.roster.show_title()
458 459
		del(self.files_props[sid[0]][sid[1:]])
		del(file_props)
460

461
	def set_progress(self, typ, sid, transfered_size, iter = None):
dkirov's avatar
dkirov committed
462
		''' change the progress of a transfer with new transfered size'''
463 464 465 466 467 468 469 470 471 472 473
		if not self.files_props[typ].has_key(sid):
			return
		file_props = self.files_props[typ][sid]
		full_size = int(file_props['size'])
		if full_size == 0:
			percent = 0
		else:
			percent = round(float(transfered_size) / full_size * 100)
		if iter is None:
			iter = self.get_iter_by_sid(typ, sid)
		if iter is not None:
474 475 476
			just_began = False
			if self.model[iter][C_PERCENT] == 0 and int(percent > 0):
				just_began = True
dkirov's avatar
dkirov committed
477
			text = self._format_percent(percent)
478 479 480 481 482
			if transfered_size == 0:
				text += '0'
			else:
				text += helpers.convert_bytes(transfered_size)
			text += '/' + helpers.convert_bytes(full_size)
dkirov's avatar
dkirov committed
483
			# Kb/s
484

dkirov's avatar
dkirov committed
485
			# remaining time
486 487 488
			if file_props.has_key('offset') and file_props['offset']:
				transfered_size -= file_props['offset'] 
				full_size -= file_props['offset']
dkirov's avatar
dkirov committed
489 490
			eta, speed = self._get_eta_and_speed(full_size, transfered_size, 
				file_props['elapsed-time'])
491

492 493 494
			self.model.set(iter, C_PROGRESS, text)
			self.model.set(iter, C_PERCENT, int(percent))
			text = self._format_time(eta)
495
			text += '\n'
dkirov's avatar
dkirov committed
496 497
			#This should make the string Kb/s, 
			#where 'Kb' part is taken from %s.
498
			#Only the 's' after / (which means second) should be translated.
nkour's avatar
nkour committed
499
			text += _('(%(filesize_unit)s/s)') % {'filesize_unit':
500
				helpers.convert_bytes(speed)}
501
			self.model.set(iter, C_TIME, text)
502

503
			# try to guess what should be the status image
504 505 506 507 508 509 510 511 512 513 514
			if file_props['type'] == 'r':
				status = 'download'
			else:
				status = 'upload'
			if file_props.has_key('paused') and file_props['paused'] == True:
				status = 'pause'
			elif file_props.has_key('stalled') and file_props['stalled'] == True:
				status = 'waiting'
			if file_props.has_key('connected') and file_props['connected'] == False:
				status = 'stop'
			self.model.set(iter, 0, self.images[status])
dkirov's avatar
dkirov committed
515
			if transfered_size == full_size:
516
				self.set_status(typ, sid, 'ok')
517 518 519 520
			elif just_began:
				path = self.model.get_path(iter)
				self.select_func(path)

521 522 523 524 525
	def get_iter_by_sid(self, typ, sid):
		'''returns iter to the row, which holds file transfer, identified by the
		session id'''
		iter = self.model.get_iter_root()
		while iter:
dkirov's avatar
dkirov committed
526
			if typ + sid == self.model[iter][C_SID].decode('utf-8'):
527 528
				return iter
			iter = self.model.iter_next(iter)
529

530
	def get_send_file_props(self, account, contact, file_path, file_name):
dkirov's avatar
dkirov committed
531 532
		''' create new file_props dict and set initial file transfer 
		properties in it'''
533 534
		file_props = {'file-name' : file_path, 'name' : file_name, 
			'type' : 's'}
535
		if os.path.isfile(file_path):
536
			stat = os.stat(file_path)
537
		else:
538
			dialogs.ErrorDialog(_('Invalid File'), _('File: ')  + file_path)
539 540 541
			return None
		if stat[6] == 0:
			dialogs.ErrorDialog(_('Invalid File'), 
542
			_('It is not possible to send empty files'))
543
			return None
dkirov's avatar
dkirov committed
544
		file_props['elapsed-time'] = 0
545
		file_props['size'] = unicode(stat[6])
546
		file_props['sid'] = helpers.get_random_string_16()
547 548 549 550 551 552
		file_props['completed'] = False
		file_props['started'] = False
		file_props['sender'] = account
		file_props['receiver'] = contact
		file_props['tt_account'] = account
		return file_props
553

554
	def add_transfer(self, account, contact, file_props):
dkirov's avatar
dkirov committed
555
		''' add new transfer to FT window and show the FT window '''
556 557 558
		self.on_transfers_list_leave_notify_event(None)
		if file_props is None:
			return
dkirov's avatar
dkirov committed
559
		file_props['elapsed-time'] = 0
560 561 562 563 564 565 566
		self.files_props[file_props['type']][file_props['sid']] = file_props
		iter = self.model.append()
		text_labels = '<b>' + _('Name: ') + '</b>\n' 
		if file_props['type'] == 'r':
			text_labels += '<b>' + _('Sender: ') + '</b>' 
		else:
			text_labels += '<b>' + _('Recipient: ') + '</b>' 
567

568 569 570 571
		if file_props['type'] == 'r':
			(file_path, file_name) = os.path.split(file_props['file-name'])
		else:
			file_name = file_props['name']
572
		text_props = gtkgui_helpers.escape_for_pango_markup(file_name) + '\n'
573
		text_props += contact.get_shown_name()
nkour's avatar
nkour committed
574
		self.model.set(iter, 1, text_labels, 2, text_props, C_SID,
575 576 577 578 579 580 581 582 583 584
			file_props['type'] + file_props['sid'])
		self.set_progress(file_props['type'], file_props['sid'], 0, iter)
		if file_props.has_key('started') and file_props['started'] is False:
			status = 'waiting'
		elif file_props['type'] == 'r':
			status = 'download'
		else:
			status = 'upload'
		file_props['tt_account'] = account
		self.set_status(file_props['type'], file_props['sid'], status)
585
		self.set_cleanup_sensitivity()
586
		self.window.show_all()
587

588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603
	def on_transfers_list_motion_notify_event(self, widget, event):
		pointer = self.tree.get_pointer()
		orig = widget.window.get_origin()
		props = widget.get_path_at_pos(int(event.x), int(event.y))
		self.height_diff = pointer[1] - int(event.y)
		if self.tooltip.timeout > 0:
			if not props or self.tooltip.id != props[0]:
				self.tooltip.hide_tooltip()
		if props:
			[row, col, x, y] = props
			iter = None
			try:
				iter = self.model.get_iter(row)
			except:
				self.tooltip.hide_tooltip()
				return
dkirov's avatar
dkirov committed
604
			sid = self.model[iter][C_SID].decode('utf-8')
605 606 607 608 609 610
			file_props = self.files_props[sid[0]][sid[1:]]
			if file_props is not None:
				if self.tooltip.timeout == 0 or self.tooltip.id != props[0]:
					self.tooltip.id = row
					self.tooltip.timeout = gobject.timeout_add(500,
						self.show_tooltip, widget)
611

612 613 614 615 616 617 618 619 620 621 622
	def on_transfers_list_leave_notify_event(self, widget = None, event = None):
		if event is not None:
			self.height_diff = int(event.y)
		elif self.height_diff is 0:
			return
		pointer = self.tree.get_pointer()
		props = self.tree.get_path_at_pos(pointer[0], 
			pointer[1] - self.height_diff)
		if self.tooltip.timeout > 0:
			if not props or self.tooltip.id == props[0]:
				self.tooltip.hide_tooltip()
623

624 625 626
	def on_transfers_list_row_activated(self, widget, path, col):
		# try to open the containing folder
		self.on_open_folder_menuitem_activate(widget)
627

628 629 630 631 632 633 634 635
	def is_transfer_paused(self, file_props):
		if file_props.has_key('stopped') and file_props['stopped']:
			return False
		if file_props.has_key('completed') and file_props['completed']:
			return False
		if not file_props.has_key('disconnect_cb'):
			return False
		return file_props['paused']
636

637 638 639 640 641 642 643 644 645 646
	def is_transfer_active(self, file_props):
		if file_props.has_key('stopped') and file_props['stopped']:
			return False
		if file_props.has_key('completed') and file_props['completed']:
			return False
		if not file_props.has_key('started') or not file_props['started']:
			return False
		if not file_props.has_key('paused'):
			return True
		return not file_props['paused']
647

648 649 650 651 652
	def is_transfer_stoped(self, file_props):
		if file_props.has_key('error') and file_props['error'] != 0:
			return True
		if file_props.has_key('completed') and file_props['completed']:
			return True
653 654 655
		if file_props.has_key('connected') and file_props['connected'] == False:
			return True
		if not file_props.has_key('stopped') or not file_props['stopped']:
656 657
			return False
		return True
658 659 660

	def set_cleanup_sensitivity(self):
		''' check if there are transfer rows and set cleanup_button 
661
		sensitive, or insensitive if model is empty'''
662 663 664 665
		if len(self.model) == 0:
			self.cleanup_button.set_sensitive(False)
		else:
			self.cleanup_button.set_sensitive(True)
666

667
	def set_all_insensitive(self):
dkirov's avatar
dkirov committed
668
		''' make all buttons/menuitems insensitive '''
669 670 671 672 673 674 675
		self.pause_button.set_sensitive(False)
		self.pause_menuitem.set_sensitive(False)
		self.continue_menuitem.set_sensitive(False)
		self.remove_menuitem.set_sensitive(False)
		self.cancel_button.set_sensitive(False)
		self.cancel_menuitem.set_sensitive(False)
		self.open_folder_menuitem.set_sensitive(False)
676
		self.set_cleanup_sensitivity()
677

678
	def set_buttons_sensitive(self, path, is_row_selected):
dkirov's avatar
dkirov committed
679 680
		''' make buttons/menuitems sensitive as appropriate to 
		the state of file transfer located at path 'path' '''
681 682 683 684
		if path is None:
			self.set_all_insensitive()
			return
		current_iter = self.model.get_iter(path)
dkirov's avatar
dkirov committed
685
		sid = self.model[current_iter][C_SID].decode('utf-8')
686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714
		file_props = self.files_props[sid[0]][sid[1:]]
		self.remove_menuitem.set_sensitive(is_row_selected)
		self.open_folder_menuitem.set_sensitive(is_row_selected)
		is_stopped = False
		if self.is_transfer_stoped(file_props):
			is_stopped = True
		self.cancel_button.set_sensitive(not is_stopped)
		self.cancel_menuitem.set_sensitive(not is_stopped)
		if not is_row_selected:
			# no selection, disable the buttons
			self.set_all_insensitive()
		elif not is_stopped:
			if self.is_transfer_active(file_props):
				# file transfer is active
				self.toggle_pause_continue(True)
				self.pause_button.set_sensitive(True)
			elif self.is_transfer_paused(file_props):
				# file transfer is paused
				self.toggle_pause_continue(False)
				self.pause_button.set_sensitive(True)
			else:
				self.pause_button.set_sensitive(False)
				self.pause_menuitem.set_sensitive(False)
				self.continue_menuitem.set_sensitive(False)
		else:
			self.pause_button.set_sensitive(False)
			self.pause_menuitem.set_sensitive(False)
			self.continue_menuitem.set_sensitive(False)
		return True
715

716
	def selection_changed(self, args):
dkirov's avatar
dkirov committed
717 718
		''' selection has changed - change the sensitivity of the 
		buttons/menuitems'''
719 720 721 722 723 724 725
		selection = args
		selected = selection.get_selected_rows()
		if selected[1] != []:
			selected_path = selected[1][0]
			self.select_func(selected_path)
		else:
			self.set_all_insensitive()
726

727 728 729 730 731 732 733 734
	def select_func(self, path):
		is_selected = False
		selected = self.tree.get_selection().get_selected_rows()
		if selected[1] != []:
			selected_path = selected[1][0]
			if selected_path == path:
				is_selected = True
		self.set_buttons_sensitive(path, is_selected)
735
		self.set_cleanup_sensitivity()
736
		return True
737

nkour's avatar
nkour committed
738 739 740 741
	def on_cleanup_button_clicked(self, widget):
		i = len(self.model) - 1
		while i >= 0:
			iter = self.model.get_iter((i))
dkirov's avatar
dkirov committed
742
			sid = self.model[iter][C_SID].decode('utf-8')
nkour's avatar
nkour committed
743
			file_props = self.files_props[sid[0]][sid[1:]]
744 745
			if self.is_transfer_stoped(file_props):
				self._remove_transfer(iter, sid, file_props)
nkour's avatar
nkour committed
746 747
			i -= 1
		self.tree.get_selection().unselect_all()
748
		self.set_all_insensitive()
749

750 751 752 753
	def toggle_pause_continue(self, status):
		if status:
			label = _('Pause')
			self.pause_button.set_label(label)
nkour's avatar
nkour committed
754 755
			self.pause_button.set_image(gtk.image_new_from_stock(
				gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_MENU))
756

757 758 759 760
			self.pause_menuitem.set_sensitive(True)
			self.pause_menuitem.set_no_show_all(False)
			self.continue_menuitem.hide()
			self.continue_menuitem.set_no_show_all(True)
761

762 763 764
		else:
			label = _('_Continue')
			self.pause_button.set_label(label)
nkour's avatar
nkour committed
765 766
			self.pause_button.set_image(gtk.image_new_from_stock(
				gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU))
767 768 769 770
			self.pause_menuitem.hide()
			self.pause_menuitem.set_no_show_all(True)
			self.continue_menuitem.set_sensitive(True)
			self.continue_menuitem.set_no_show_all(False)
771

772 773 774 775 776
	def on_pause_restore_button_clicked(self, widget):
		selected = self.tree.get_selection().get_selected()
		if selected is None or selected[1] is None:
			return 
		s_iter = selected[1]
dkirov's avatar
dkirov committed
777
		sid = self.model[s_iter][C_SID].decode('utf-8')
778 779
		file_props = self.files_props[sid[0]][sid[1:]]
		if self.is_transfer_paused(file_props):
dkirov's avatar
dkirov committed
780
			file_props['last-time'] = time.time()
781 782 783 784
			file_props['paused'] = False
			types = {'r' : 'download', 's' : 'upload'}
			self.set_status(file_props['type'], file_props['sid'], types[sid[0]])
			self.toggle_pause_continue(True)
dkirov's avatar
dkirov committed
785
			file_props['continue_cb']()
786 787 788 789
		elif self.is_transfer_active(file_props):
			file_props['paused'] = True
			self.set_status(file_props['type'], file_props['sid'], 'pause')
			self.toggle_pause_continue(False)
790

791 792 793 794 795
	def on_cancel_button_clicked(self, widget):
		selected = self.tree.get_selection().get_selected()
		if selected is None or selected[1] is None:
			return 
		s_iter = selected[1]
dkirov's avatar
dkirov committed
796
		sid = self.model[s_iter][C_SID].decode('utf-8')
797 798 799 800 801 802 803 804
		file_props = self.files_props[sid[0]][sid[1:]]
		if not file_props.has_key('tt_account'):
			return 
		account = file_props['tt_account']
		if not gajim.connections.has_key(account):
			return
		gajim.connections[account].disconnect_transfer(file_props)
		self.set_status(file_props['type'], file_props['sid'], 'stop')
805

806 807 808 809 810 811 812
	def show_tooltip(self, widget):
		if self.height_diff == 0:
			self.tooltip.hide_tooltip()
			return
		pointer = self.tree.get_pointer()
		props = self.tree.get_path_at_pos(pointer[0], 
			pointer[1] - self.height_diff)
813 814
		# check if the current pointer is at the same path
		# as it was before setting the timeout
815 816
		if props and self.tooltip.id == props[0]:
			iter = self.model.get_iter(props[0])
dkirov's avatar
dkirov committed
817
			sid = self.model[iter][C_SID].decode('utf-8')
818
			file_props = self.files_props[sid[0]][sid[1:]]
819
			# bounding rectangle of coordinates for the cell within the treeview
820
			rect =  self.tree.get_cell_area(props[0],props[1])
821
			# position of the treeview on the screen
822
			position = widget.window.get_origin()
823 824
			self.tooltip.show_tooltip(file_props , rect.height, 
								position[1] + rect.y + self.height_diff)
825 826
		else:
			self.tooltip.hide_tooltip()
827

828 829 830
	def on_notify_ft_complete_checkbox_toggled(self, widget):
		gajim.config.set('notify_on_file_complete', 
			widget.get_active())
831

832 833 834 835
	def on_file_transfers_dialog_delete_event(self, widget, event):
		self.on_transfers_list_leave_notify_event(widget, None)
		self.window.hide()
		return True # do NOT destory window
836

837 838 839 840 841 842 843 844 845
	def on_close_button_clicked(self, widget):
		self.window.hide()

	def show_context_menu(self, event, iter):
		# change the sensitive propery of the buttons and menuitems
		path = None
		if iter is not None:
			path = self.model.get_path(iter)
		self.set_buttons_sensitive(path, True)
846

nkour's avatar
nkour committed
847
		event_button = gtkgui_helpers.get_possible_button_event(event)
848
		self.file_transfers_menu.show_all()
849 850
		self.file_transfers_menu.popup(None, self.tree, None, 
			event_button, event.time)
851

852 853 854 855 856 857 858 859
	def on_transfers_list_key_press_event(self, widget, event):
		'''when a key is pressed in the treeviews'''
		self.tooltip.hide_tooltip()
		iter = None
		try:
			store, iter = self.tree.get_selection().get_selected()
		except TypeError:
			self.tree.get_selection().unselect_all()
860

861 862 863
		if iter is not None:
			path = self.model.get_path(iter)
			self.tree.get_selection().select_path(path)
864

865 866 867
		if event.keyval == gtk.keysyms.Menu:
			self.show_context_menu(event, iter)
			return True
868 869


870 871 872 873 874 875 876 877 878 879 880 881 882
	def on_transfers_list_button_release_event(self, widget, event):
		# hide tooltip, no matter the button is pressed
		self.tooltip.hide_tooltip()
		path = None
		try:
			path, column, x, y = self.tree.get_path_at_pos(int(event.x), 
				int(event.y))
		except TypeError:
			self.tree.get_selection().unselect_all()
		if path is None:
			self.set_all_insensitive()
		else:
			self.select_func(path)
883

884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899
	def on_transfers_list_button_press_event(self, widget, event):
		# hide tooltip, no matter the button is pressed
		self.tooltip.hide_tooltip()
		path, iter = None, None
		try:
			path, column, x, y = self.tree.get_path_at_pos(int(event.x), 
				int(event.y))
		except TypeError:
			self.tree.get_selection().unselect_all()
		if event.button == 3: # Right click
			if path is not None:
				self.tree.get_selection().select_path(path)
				iter = self.model.get_iter(path)
			self.show_context_menu(event, iter)
			if path is not None:
				return True
900

901 902 903 904 905
	def on_open_folder_menuitem_activate(self, widget):
		selected = self.tree.get_selection().get_selected()
		if selected is None or selected[1] is None:
			return 
		s_iter = selected[1]
dkirov's avatar
dkirov committed
906
		sid = self.model[s_iter][C_SID].decode('utf-8')
907 908 909 910 911 912
		file_props = self.files_props[sid[0]][sid[1:]]
		if not file_props.has_key('file-name'):
			return
		(path, file) = os.path.split(file_props['file-name'])
		if os.path.exists(path) and os.path.isdir(path):
			helpers.launch_file_manager(path)
913

914 915
	def on_cancel_menuitem_activate(self, widget):
		self.on_cancel_button_clicked(widget)
916

917 918
	def on_continue_menuitem_activate(self, widget):
		self.on_pause_restore_button_clicked(widget)
919

920 921
	def on_pause_menuitem_activate(self, widget):
		self.on_pause_restore_button_clicked(widget)
922

923
	def on_remove_menuitem_activate(self, widget):
nkour's avatar
nkour committed
924 925 926 927
		selected = self.tree.get_selection().get_selected()
		if selected is None or selected[1] is None:
			return 
		s_iter = selected[1]
dkirov's avatar
dkirov committed
928
		sid = self.model[s_iter][C_SID].decode('utf-8')
nkour's avatar
nkour committed
929
		file_props = self.files_props[sid[0]][sid[1:]]
930
		self._remove_transfer(s_iter, sid, file_props)
nkour's avatar
nkour committed
931
		self.set_all_insensitive()
nkour's avatar
nkour committed
932 933 934 935

	def on_file_transfers_window_key_press_event(self, widget, event):
		if event.keyval == gtk.keysyms.Escape: # ESCAPE
			self.window.hide()
936