Skip to content
Snippets Groups Projects
filetransfers_window.py 39.6 KiB
Newer Older
roidelapluie's avatar
roidelapluie committed
# -*- coding:utf-8 -*-
roidelapluie's avatar
roidelapluie committed
## src/filetransfers_window.py
## Copyright (C) 2003-2010 Yann Leboulanger <asterix AT lagaule.org>
roidelapluie's avatar
roidelapluie committed
## Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
## Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
## Copyright (C) 2006 Travis Shirk <travis AT pobox.com>
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 3 only.
## Gajim is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
roidelapluie's avatar
roidelapluie committed
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
roidelapluie's avatar
roidelapluie committed
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
dkirov's avatar
dkirov committed
import pango
import time

import gtkgui_helpers
import tooltips
import dialogs

from common import gajim
from common import helpers
from common.protocol.bytestream import (is_transfer_active, is_transfer_paused,
C_IMAGE = 0
C_LABELS = 1
C_FILE = 2
C_TIME = 3
C_PROGRESS = 4
C_PERCENT = 5
class FileTransfersWindow:
    def __init__(self):
        self.files_props = {'r' : {}, 's': {}}
        self.height_diff = 0
        self.xml = gtkgui_helpers.get_gtk_builder('filetransfers.ui')
        self.window = self.xml.get_object('file_transfers_window')
        self.tree = self.xml.get_object('transfers_list')
        self.cancel_button = self.xml.get_object('cancel_button')
        self.pause_button = self.xml.get_object('pause_restore_button')
        self.cleanup_button = self.xml.get_object('cleanup_button')
        self.notify_ft_checkbox = self.xml.get_object(
                'notify_ft_complete_checkbox')

        shall_notify = gajim.config.get('notify_on_file_complete')
        self.notify_ft_checkbox.set_active(shall_notify
                                                                                                )
        self.model = gtk.ListStore(gtk.gdk.Pixbuf, str, str, str, str, int, str)
        self.tree.set_model(self.model)
        col = gtk.TreeViewColumn()

        render_pixbuf = gtk.CellRendererPixbuf()

        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)

        col = gtk.TreeViewColumn(_('File'))
        renderer = gtk.CellRendererText()
        col.pack_start(renderer, expand=False)
        col.add_attribute(renderer, 'markup', C_LABELS)
        renderer.set_property('yalign', 0.)
        renderer = gtk.CellRendererText()
        col.pack_start(renderer, expand=True)
        col.add_attribute(renderer, 'markup', C_FILE)
        renderer.set_property('xalign', 0.)
        renderer.set_property('yalign', 0.)
        renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
        col.set_resizable(True)
        col.set_expand(True)
        self.tree.append_column(col)

        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)
        self.tree.append_column(col)

        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)
        col.set_expand(False)
        self.tree.append_column(col)

        self.images = {}
        self.icons = {
                'upload': gtk.STOCK_GO_UP,
                'download': gtk.STOCK_GO_DOWN,
                'stop': gtk.STOCK_STOP,
                'waiting': gtk.STOCK_REFRESH,
                'pause': gtk.STOCK_MEDIA_PAUSE,
                'continue': gtk.STOCK_MEDIA_PLAY,
                'ok': gtk.STOCK_APPLY,
        }

        self.tree.get_selection().set_mode(gtk.SELECTION_SINGLE)
        self.tree.get_selection().connect('changed', self.selection_changed)
        self.tooltip = tooltips.FileTransfersTooltip()
        self.file_transfers_menu = self.xml.get_object('file_transfers_menu')
        self.open_folder_menuitem = self.xml.get_object('open_folder_menuitem')
        self.cancel_menuitem = self.xml.get_object('cancel_menuitem')
        self.pause_menuitem = self.xml.get_object('pause_menuitem')
        self.continue_menuitem = self.xml.get_object('continue_menuitem')
        self.remove_menuitem = self.xml.get_object('remove_menuitem')
        self.xml.connect_signals(self)

    def find_transfer_by_jid(self, account, jid):
        """
        Find all transfers with peer 'jid' that belong to 'account'
        """
        active_transfers = [[], []] # ['senders', 'receivers']

        # 'account' is the sender
        for file_props in self.files_props['s'].values():
            if file_props['tt_account'] == account:
                receiver_jid = unicode(file_props['receiver']).split('/')[0]
                if jid == receiver_jid:
                    if not is_transfer_stopped(file_props):
                        active_transfers[0].append(file_props)

        # 'account' is the recipient
        for file_props in self.files_props['r'].values():
            if file_props['tt_account'] == account:
                sender_jid = unicode(file_props['sender']).split('/')[0]
                if jid == sender_jid:
                    if not is_transfer_stopped(file_props):
                        active_transfers[1].append(file_props)
        return active_transfers

    def show_completed(self, jid, file_props):
        """
        Show a dialog saying that file (file_props) has been transferred
        """
        def on_open(widget, file_props):
            dialog.destroy()
            if 'file-name' not in file_props:
                return
            path = os.path.split(file_props['file-name'])[0]
            if os.path.exists(path) and os.path.isdir(path):
                helpers.launch_file_manager(path)
            self.tree.get_selection().unselect_all()

        if file_props['type'] == 'r':
            # file path is used below in 'Save in'
            (file_path, file_name) = os.path.split(file_props['file-name'])
        else:
            file_name = file_props['name']
        sectext = '\t' + _('Filename: %s') % file_name
        sectext += '\n\t' + _('Size: %s') % \
        helpers.convert_bytes(file_props['size'])
        if file_props['type'] == 'r':
            jid = unicode(file_props['sender']).split('/')[0]
            sender_name = gajim.contacts.get_first_contact_from_jid(
                    file_props['tt_account'], jid).get_shown_name()
            sender = sender_name
        else:
            #You is a reply of who sent a file
            sender = _('You')
        sectext += '\n\t' + _('Sender: %s') % sender
        sectext += '\n\t' + _('Recipient: ')
        if file_props['type'] == 's':
            jid = unicode(file_props['receiver']).split('/')[0]
            receiver_name = gajim.contacts.get_first_contact_from_jid(
                    file_props['tt_account'], jid).get_shown_name()
            recipient = receiver_name
        else:
            #You is a reply of who received a file
            recipient = _('You')
        sectext += recipient
        if file_props['type'] == 'r':
            sectext += '\n\t' + _('Saved in: %s') % file_path
        dialog = dialogs.HigDialog(None, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE,
                        _('File transfer completed'), sectext)
        if file_props['type'] == 'r':
            button = gtk.Button(_('_Open Containing Folder'))
            button.connect('clicked', on_open, file_props)
            dialog.action_area.pack_start(button)
        ok_button = dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
        def on_ok(widget):
            dialog.destroy()
        ok_button.connect('clicked', on_ok)
        dialog.show_all()

    def show_request_error(self, file_props):
        """
        Show error dialog to the recipient saying that transfer has been canceled
        """
        dialogs.InformationDialog(_('File transfer cancelled'), _('Connection with peer cannot be established.'))
        self.tree.get_selection().unselect_all()

    def show_send_error(self, file_props):
        """
        Show error dialog to the sender saying that transfer has been canceled
        """
        dialogs.InformationDialog(_('File transfer cancelled'),
                _('Connection with peer cannot be established.'))
        self.tree.get_selection().unselect_all()

    def show_stopped(self, jid, file_props, error_msg=''):
        if file_props['type'] == 'r':
            file_name = os.path.basename(file_props['file-name'])
        else:
            file_name = file_props['name']
        sectext = '\t' + _('Filename: %s') % file_name
        sectext += '\n\t' + _('Recipient: %s') % jid
        if error_msg:
            sectext += '\n\t' + _('Error message: %s') % error_msg
        dialogs.ErrorDialog(_('File transfer stopped'), sectext)
        self.tree.get_selection().unselect_all()

    def show_file_send_request(self, account, contact):
        win = gtk.ScrolledWindow()
        win.set_shadow_type(gtk.SHADOW_IN)
        win.set_policy(gtk.POLICY_NEVER, gtk.POLICY_NEVER)

        from message_textview import MessageTextView
        desc_entry = MessageTextView()
        win.add(desc_entry)

        def on_ok(widget):
            file_dir = None
            files_path_list = dialog.get_filenames()
            files_path_list = gtkgui_helpers.decode_filechooser_file_paths(
                    files_path_list)
            text_buffer = desc_entry.get_buffer()
            desc = text_buffer.get_text(text_buffer.get_start_iter(),
                text_buffer.get_end_iter())
            for file_path in files_path_list:
                if self.send_file(account, contact, file_path, desc) \
                and file_dir is None:
                    file_dir = os.path.dirname(file_path)
            if file_dir:
                gajim.config.set('last_send_dir', file_dir)
                dialog.destroy()

        dialog = dialogs.FileChooserDialog(_('Choose File to Send...'),
                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'),
                on_response_ok=on_ok,
                on_response_cancel=lambda e:dialog.destroy()
                )

        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)

        desc_hbox = gtk.HBox(False, 5)
        desc_hbox.pack_start(gtk.Label(_('Description: ')), False, False, 0)
        desc_hbox.pack_start(win, True, True, 0)

        dialog.vbox.pack_start(desc_hbox, False, False, 0)

        btn.show()
        desc_hbox.show_all()

    def send_file(self, account, contact, file_path, file_desc=''):
        """
        Start the real transfer(upload) of the file
        """
        if gtkgui_helpers.file_is_locked(file_path):
            pritext = _('Gajim cannot access this file')
            sextext = _('This file is being used by another process.')
            dialogs.ErrorDialog(pritext, sextext)
            return

        if isinstance(contact, str):
            if contact.find('/') == -1:
                return
            (jid, resource) = contact.split('/', 1)
            contact = gajim.contacts.create_contact(jid=jid, account=account,
                resource=resource)
        file_name = os.path.split(file_path)[1]
        file_props = self.get_send_file_props(account, contact,
                        file_path, file_name, file_desc)
        if file_props is None:
            return False
        self.add_transfer(account, contact, file_props)
        gajim.connections[account].send_file_request(file_props)
        return True

    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)

    def show_file_request(self, account, contact, file_props):
        """
        Show dialog asking for comfirmation and store location of new file
        requested by a contact
        """
        if file_props is None or 'name' not in file_props:
            return
        sec_text = '\t' + _('File: %s') % gobject.markup_escape_text(
                file_props['name'])
        if 'size' in file_props:
            sec_text += '\n\t' + _('Size: %s') % \
                    helpers.convert_bytes(file_props['size'])
        if 'mime-type' in file_props:
            sec_text += '\n\t' + _('Type: %s') % file_props['mime-type']
        if 'desc' in file_props:
            sec_text += '\n\t' + _('Description: %s') % file_props['desc']
        prim_text = _('%s wants to send you a file:') % contact.jid
        dialog = None

        def on_response_ok(account, contact, file_props):

            def on_ok(widget, account, contact, file_props):
                file_path = dialog2.get_filename()
                file_path = gtkgui_helpers.decode_filechooser_file_paths(
                        (file_path,))[0]
                if os.path.exists(file_path):
                    # 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
                    stat = os.stat(file_path)
                    dl_size = stat.st_size
                    file_size = file_props['size']
                    dl_finished = dl_size >= file_size

                    def on_response(response):
                        if response < 0:
                            return
                        elif response == 100:
                            file_props['offset'] = dl_size
                        dialog2.destroy()
                        self._start_receive(file_path, account, contact, file_props)

                    dialog = dialogs.FTOverwriteConfirmationDialog(
                            _('This file already exists'), _('What do you want to do?'),
                            propose_resume=not dl_finished, on_response=on_response)
                    dialog.set_transient_for(dialog2)
                    dialog.set_destroy_with_parent(True)
                    return
                else:
                    dirname = os.path.dirname(file_path)
                    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
                        dialogs.ErrorDialog(_('Directory "%s" is not writable') % dirname, _('You do not have permission to create files in this directory.'))
                        return
                dialog2.destroy()
                self._start_receive(file_path, account, contact, file_props)

            def on_cancel(widget, account, contact, file_props):
                dialog2.destroy()
                gajim.connections[account].send_file_rejection(file_props)

            dialog2 = dialogs.FileChooserDialog(
                    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))

            dialog2.set_current_name(file_props['name'])
            dialog2.connect('delete-event', lambda widget, event:
                    on_cancel(widget, account, contact, file_props))

        def on_response_cancel(account, file_props):
            gajim.connections[account].send_file_rejection(file_props)

        dialog = dialogs.NonModalConfirmationDialog(prim_text, sec_text,
                on_response_ok=(on_response_ok, account, contact, file_props),
                on_response_cancel=(on_response_cancel, account, file_props))
        dialog.connect('delete-event', lambda widget, event:
            on_response_cancel(account, file_props))
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 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 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813
        dialog.popup()

    def get_icon(self, ident):
        return self.images.setdefault(ident,
                self.window.render_icon(self.icons[ident], gtk.ICON_SIZE_MENU))

    def set_status(self, typ, sid, status):
        """
        Change the status of a transfer to state 'status'
        """
        iter_ = self.get_iter_by_sid(typ, sid)
        if iter_ is None:
            return
        sid = self.model[iter_][C_SID].decode('utf-8')
        file_props = self.files_props[sid[0]][sid[1:]]
        if status == 'stop':
            file_props['stopped'] = True
        elif status == 'ok':
            file_props['completed'] = True
        self.model.set(iter_, C_IMAGE, self.get_icon(status))
        path = self.model.get_path(iter_)
        self.select_func(path)

    def _format_percent(self, percent):
        """
        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

    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

        #Print remaining time in format 00:00:00
        #You can change the places of (hours), (minutes), (seconds) -
        #they are not translatable.
        return _('%(hours)02.d:%(minutes)02.d:%(seconds)02.d') % times

    def _get_eta_and_speed(self, full_size, transfered_size, file_props):
        if len(file_props['transfered_size']) == 0:
            return 0., 0.
        elif len(file_props['transfered_size']) == 1:
            speed = round(float(transfered_size) / file_props['elapsed-time'])
        else:
            # first and last are (time, transfered_size)
            first = file_props['transfered_size'][0]
            last = file_props['transfered_size'][-1]
            transfered = last[1] - first[1]
            tim = last[0] - first[0]
            if tim == 0:
                return 0., 0.
            speed = round(float(transfered) / tim)
        if speed == 0.:
            return 0., 0.
        remaining_size = full_size - transfered_size
        eta = remaining_size / speed
        return eta, speed

    def _remove_transfer(self, iter_, sid, file_props):
        self.model.remove(iter_)
        if  'tt_account' in file_props:
            # file transfer is set
            account = file_props['tt_account']
            if account in gajim.connections:
                # there is a connection to the account
                gajim.connections[account].remove_transfer(file_props)
            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
            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]):
                    if event.parameters['sid'] == file_props['sid']:
                        gajim.events.remove_events(account, jid, event)
                        gajim.interface.roster.draw_contact(jid, account)
                        gajim.interface.roster.show_title()
        del(self.files_props[sid[0]][sid[1:]])
        del(file_props)

    def set_progress(self, typ, sid, transfered_size, iter_=None):
        """
        Change the progress of a transfer with new transfered size
        """
        if sid not in self.files_props[typ]:
            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, 1)
        if iter_ is None:
            iter_ = self.get_iter_by_sid(typ, sid)
        if iter_ is not None:
            just_began = False
            if self.model[iter_][C_PERCENT] == 0 and int(percent > 0):
                just_began = True
            text = self._format_percent(percent)
            if transfered_size == 0:
                text += '0'
            else:
                text += helpers.convert_bytes(transfered_size)
            text += '/' + helpers.convert_bytes(full_size)
            # Kb/s

            # remaining time
            if 'offset' in file_props and file_props['offset']:
                transfered_size -= file_props['offset']
                full_size -= file_props['offset']

            if file_props['elapsed-time'] > 0:
                file_props['transfered_size'].append((file_props['last-time'], transfered_size))
            if len(file_props['transfered_size']) > 6:
                file_props['transfered_size'].pop(0)
            eta, speed = self._get_eta_and_speed(full_size, transfered_size,
                    file_props)

            self.model.set(iter_, C_PROGRESS, text)
            self.model.set(iter_, C_PERCENT, int(percent))
            text = self._format_time(eta)
            text += '\n'
            #This should make the string Kb/s,
            #where 'Kb' part is taken from %s.
            #Only the 's' after / (which means second) should be translated.
            text += _('(%(filesize_unit)s/s)') % {'filesize_unit':
                    helpers.convert_bytes(speed)}
            self.model.set(iter_, C_TIME, text)

            # try to guess what should be the status image
            if file_props['type'] == 'r':
                status = 'download'
            else:
                status = 'upload'
            if 'paused' in file_props and file_props['paused'] == True:
                status = 'pause'
            elif 'stalled' in file_props and file_props['stalled'] == True:
                status = 'waiting'
            if 'connected' in file_props and file_props['connected'] == False:
                status = 'stop'
            self.model.set(iter_, 0, self.get_icon(status))
            if transfered_size == full_size:
                self.set_status(typ, sid, 'ok')
            elif just_began:
                path = self.model.get_path(iter_)
                self.select_func(path)

    def get_iter_by_sid(self, typ, sid):
        """
        Return iter to the row, which holds file transfer, identified by the
        session id
        """
        iter_ = self.model.get_iter_root()
        while iter_:
            if typ + sid == self.model[iter_][C_SID].decode('utf-8'):
                return iter_
            iter_ = self.model.iter_next(iter_)

    def get_send_file_props(self, account, contact, file_path, file_name,
                    file_desc=''):
        """
        Create new file_props dict and set initial file transfer properties in it
        """
        file_props = {'file-name' : file_path, 'name' : file_name,
                'type' : 's', 'desc' : file_desc}
        if os.path.isfile(file_path):
            stat = os.stat(file_path)
        else:
            dialogs.ErrorDialog(_('Invalid File'), _('File: ') + file_path)
            return None
        if stat[6] == 0:
            dialogs.ErrorDialog(_('Invalid File'),
            _('It is not possible to send empty files'))
            return None
        file_props['elapsed-time'] = 0
        file_props['size'] = unicode(stat[6])
        file_props['sid'] = helpers.get_random_string_16()
        file_props['completed'] = False
        file_props['started'] = False
        file_props['sender'] = account
        file_props['receiver'] = contact
        file_props['tt_account'] = account
        # keep the last time: transfered_size to compute transfer speed
        file_props['transfered_size'] = []
        return file_props

    def add_transfer(self, account, contact, file_props):
        """
        Add new transfer to FT window and show the FT window
        """
        self.on_transfers_list_leave_notify_event(None)
        if file_props is None:
            return
        file_props['elapsed-time'] = 0
        self.files_props[file_props['type']][file_props['sid']] = file_props
        iter_ = self.model.prepend()
        text_labels = '<b>' + _('Name: ') + '</b>\n'
        if file_props['type'] == 'r':
            text_labels += '<b>' + _('Sender: ') + '</b>'
        else:
            text_labels += '<b>' + _('Recipient: ') + '</b>'

        if file_props['type'] == 'r':
            file_name = os.path.split(file_props['file-name'])[1]
        else:
            file_name = file_props['name']
        text_props = gobject.markup_escape_text(file_name) + '\n'
        text_props += contact.get_shown_name()
        self.model.set(iter_, 1, text_labels, 2, text_props, C_SID,
                file_props['type'] + file_props['sid'])
        self.set_progress(file_props['type'], file_props['sid'], 0, iter_)
        if 'started' in file_props 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)
        self.set_cleanup_sensitivity()
        self.window.show_all()

    def on_transfers_list_motion_notify_event(self, widget, event):
        pointer = self.tree.get_pointer()
        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 = props[0]
            iter_ = None
            try:
                iter_ = self.model.get_iter(row)
            except Exception:
                self.tooltip.hide_tooltip()
                return
            sid = self.model[iter_][C_SID].decode('utf-8')
            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)

    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()

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

    def set_cleanup_sensitivity(self):
        """
        Check if there are transfer rows and set cleanup_button sensitive, or
        insensitive if model is empty
        """
        if len(self.model) == 0:
            self.cleanup_button.set_sensitive(False)
        else:
            self.cleanup_button.set_sensitive(True)

    def set_all_insensitive(self):
        """
        Make all buttons/menuitems insensitive
        """
        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)
        self.set_cleanup_sensitivity()

    def set_buttons_sensitive(self, path, is_row_selected):
        """
        Make buttons/menuitems sensitive as appropriate to the state of file
        transfer located at path 'path'
        """
        if path is None:
            self.set_all_insensitive()
            return
        current_iter = self.model.get_iter(path)
        sid = self.model[current_iter][C_SID].decode('utf-8')
        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 is_transfer_stopped(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 is_transfer_active(file_props):
                # file transfer is active
                self.toggle_pause_continue(True)
                self.pause_button.set_sensitive(True)
            elif 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

    def selection_changed(self, args):
        """
        Selection has changed - change the sensitivity of the buttons/menuitems
        """
        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()

    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)
        self.set_cleanup_sensitivity()
        return True

    def on_cleanup_button_clicked(self, widget):
        i = len(self.model) - 1
        while i >= 0:
            iter_ = self.model.get_iter((i))
            sid = self.model[iter_][C_SID].decode('utf-8')
            file_props = self.files_props[sid[0]][sid[1:]]
            if is_transfer_stopped(file_props):
                self._remove_transfer(iter_, sid, file_props)
            i -= 1
        self.tree.get_selection().unselect_all()
        self.set_all_insensitive()

    def toggle_pause_continue(self, status):
        if status:
            label = _('Pause')
            self.pause_button.set_label(label)
            self.pause_button.set_image(gtk.image_new_from_stock(
                    gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_MENU))

            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)

        else:
            label = _('_Continue')
            self.pause_button.set_label(label)
            self.pause_button.set_image(gtk.image_new_from_stock(
                    gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU))
            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)

    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]
        sid = self.model[s_iter][C_SID].decode('utf-8')
        file_props = self.files_props[sid[0]][sid[1:]]
            file_props['last-time'] = time.time()
            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)
            if file_props['continue_cb']:
                file_props['continue_cb']()
        elif is_transfer_active(file_props):
            file_props['paused'] = True
            self.set_status(file_props['type'], file_props['sid'], 'pause')
            # reset that to compute speed only when we resume
            file_props['transfered_size'] = []
            self.toggle_pause_continue(False)

    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]
        sid = self.model[s_iter][C_SID].decode('utf-8')
        file_props = self.files_props[sid[0]][sid[1:]]
        if 'tt_account' not in file_props:
            return
        account = file_props['tt_account']
        if account not in gajim.connections:
            return
        gajim.connections[account].disconnect_transfer(file_props)
        self.set_status(file_props['type'], file_props['sid'], 'stop')

    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)
        # check if the current pointer is at the same path
        # as it was before setting the timeout
        if props and self.tooltip.id == props[0]:
            iter_ = self.model.get_iter(props[0])
            sid = self.model[iter_][C_SID].decode('utf-8')
            file_props = self.files_props[sid[0]][sid[1:]]
            # bounding rectangle of coordinates for the cell within the treeview
            rect = self.tree.get_cell_area(props[0], props[1])
            # position of the treeview on the screen
            position = widget.window.get_origin()
            self.tooltip.show_tooltip(file_props, rect.height,
                                                    position[1] + rect.y + self.height_diff)
        else:
            self.tooltip.hide_tooltip()

    def on_notify_ft_complete_checkbox_toggled(self, widget):
        gajim.config.set('notify_on_file_complete',
                widget.get_active())

    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

    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
        if iter_:
            path = self.model.get_path(iter_)
            self.set_buttons_sensitive(path, True)

        event_button = gtkgui_helpers.get_possible_button_event(event)
        self.file_transfers_menu.show_all()
        self.file_transfers_menu.popup(None, self.tree, None,
                event_button, event.time)

    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:
            iter_ = self.tree.get_selection().get_selected()[1]
        except TypeError:
            self.tree.get_selection().unselect_all()

        if iter_ is not None:
            path = self.model.get_path(iter_)
            self.tree.get_selection().select_path(path)

        if event.keyval == gtk.keysyms.Menu:
            self.show_context_menu(event, iter_)
            return True


    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 = self.tree.get_path_at_pos(int(event.x), int(event.y))[0]
        except TypeError:
            self.tree.get_selection().unselect_all()
        if path is None:
            self.set_all_insensitive()
        else:
            self.select_func(path)

    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 = self.tree.get_path_at_pos(int(event.x), int(event.y))[0]
        except TypeError:
            self.tree.get_selection().unselect_all()
        if event.button == 3: # Right click
            if path:
                self.tree.get_selection().select_path(path)
                iter_ = self.model.get_iter(path)
            self.show_context_menu(event, iter_)
            if path:
                return True

    def on_open_folder_menuitem_activate(self, widget):
        selected = self.tree.get_selection().get_selected()
        if not selected or not selected[1]:
            return
        s_iter = selected[1]
        sid = self.model[s_iter][C_SID].decode('utf-8')
        file_props = self.files_props[sid[0]][sid[1:]]
        if 'file-name' not in file_props:
            return
        path = os.path.split(file_props['file-name'])[0]
        if os.path.exists(path) and os.path.isdir(path):
            helpers.launch_file_manager(path)

    def on_cancel_menuitem_activate(self, widget):
        self.on_cancel_button_clicked(widget)

    def on_continue_menuitem_activate(self, widget):
        self.on_pause_restore_button_clicked(widget)

    def on_pause_menuitem_activate(self, widget):
        self.on_pause_restore_button_clicked(widget)

    def on_remove_menuitem_activate(self, widget):
        selected = self.tree.get_selection().get_selected()
        if not selected or not selected[1]:
            return
        s_iter = selected[1]
        sid = self.model[s_iter][C_SID].decode('utf-8')
        file_props = self.files_props[sid[0]][sid[1:]]
        self._remove_transfer(s_iter, sid, file_props)
        self.set_all_insensitive()

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