1# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
2# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
3# Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
4# Copyright (C) 2006 Travis Shirk <travis AT pobox.com>
5#
6# This file is part of Gajim.
7#
8# Gajim is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published
10# by the Free Software Foundation; version 3 only.
11#
12# Gajim is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
19
20import os
21import time
22import logging
23from functools import partial
24from pathlib import Path
25from enum import IntEnum, unique
26from datetime import datetime
27
28from gi.repository import Gtk
29from gi.repository import Gdk
30from gi.repository import GLib
31from gi.repository import Pango
32
33from gajim import gtkgui_helpers
34
35from gajim.common import app
36from gajim.common import helpers
37from gajim.common.i18n import _
38from gajim.common.file_props import FilesProp
39from gajim.common.helpers import open_file
40from gajim.common.modules.bytestream import (is_transfer_active,
41                                             is_transfer_paused,
42                                             is_transfer_stopped)
43
44from .dialogs import DialogButton
45from .dialogs import ConfirmationDialog
46from .dialogs import HigDialog
47from .dialogs import InformationDialog
48from .dialogs import ErrorDialog
49from .filechoosers import FileSaveDialog
50from .filechoosers import FileChooserDialog
51from .tooltips import FileTransfersTooltip
52from .util import get_builder
53
54log = logging.getLogger('gajim.gui.filetransfer')
55
56
57@unique
58class Column(IntEnum):
59    IMAGE = 0
60    LABELS = 1
61    FILE = 2
62    TIME = 3
63    PROGRESS = 4
64    PERCENT = 5
65    PULSE = 6
66    SID = 7
67
68
69class FileTransfersWindow:
70    def __init__(self):
71        self.files_props = {'r': {}, 's': {}}
72        self.height_diff = 0
73
74        self._ui = get_builder('filetransfers.ui')
75        self.window = self._ui.file_transfers_window
76        show_notification = app.settings.get('notify_on_file_complete')
77        self._ui.notify_ft_complete.set_active(show_notification)
78        self.model = Gtk.ListStore(str, str, str, str, str, int, int, str)
79        self._ui.transfers_list.set_model(self.model)
80        col = Gtk.TreeViewColumn()
81
82        render_pixbuf = Gtk.CellRendererPixbuf()
83
84        col.pack_start(render_pixbuf, True)
85        render_pixbuf.set_property('xpad', 6)
86        render_pixbuf.set_property('ypad', 6)
87        render_pixbuf.set_property('yalign', 0.5)
88        col.add_attribute(render_pixbuf, 'icon_name', 0)
89        self._ui.transfers_list.append_column(col)
90
91        col = Gtk.TreeViewColumn(_('File'))
92        renderer = Gtk.CellRendererText()
93        col.pack_start(renderer, False)
94        col.add_attribute(renderer, 'markup', Column.LABELS)
95        renderer.set_property('xalign', 0.0)
96        renderer.set_property('yalign', 0.0)
97        renderer = Gtk.CellRendererText()
98        col.pack_start(renderer, True)
99        col.add_attribute(renderer, 'markup', Column.FILE)
100        renderer.set_property('xalign', 0.0)
101        renderer.set_property('yalign', 0.0)
102        renderer.set_property('ellipsize', Pango.EllipsizeMode.END)
103        col.set_resizable(True)
104        col.set_min_width(160)
105        col.set_expand(True)
106        self._ui.transfers_list.append_column(col)
107
108        col = Gtk.TreeViewColumn(_('Time'))
109        renderer = Gtk.CellRendererText()
110        col.pack_start(renderer, False)
111        col.add_attribute(renderer, 'markup', Column.TIME)
112        renderer.set_property('yalign', 0.5)
113        renderer.set_property('xalign', 0.5)
114        renderer = Gtk.CellRendererText()
115        renderer.set_property('ellipsize', Pango.EllipsizeMode.END)
116        renderer.set_property('xalign', 0.5)
117        col.set_resizable(True)
118        col.set_min_width(70)
119        self._ui.transfers_list.append_column(col)
120
121        col = Gtk.TreeViewColumn(_('Progress'))
122        renderer = Gtk.CellRendererProgress()
123        renderer.set_property('yalign', 0.5)
124        renderer.set_property('xalign', 0.5)
125        col.pack_start(renderer, False)
126        col.add_attribute(renderer, 'text', Column.PROGRESS)
127        col.add_attribute(renderer, 'value', Column.PERCENT)
128        col.add_attribute(renderer, 'pulse', Column.PULSE)
129        col.set_resizable(False)
130        col.set_fixed_width(150)
131        self._ui.transfers_list.append_column(col)
132
133        self.icons = {
134            'upload': 'go-up-symbolic',
135            'download': 'go-down-symbolic',
136            'stop': 'process-stop-symbolic',
137            'waiting': 'emblem-synchronizing-symbolic',
138            'pause': 'media-playback-pause-symbolic',
139            'continue': 'media-playback-start-symbolic',
140            'ok': 'emblem-ok-symbolic',
141            'computing': 'system-run-symbolic',
142            'hash_error': 'network-error-symbolic',
143        }
144
145        if app.settings.get('use_kib_mib'):
146            self.units = GLib.FormatSizeFlags.IEC_UNITS
147        else:
148            self.units = GLib.FormatSizeFlags.DEFAULT
149
150        self._ui.transfers_list.get_selection().set_mode(
151            Gtk.SelectionMode.SINGLE)
152        self._ui.transfers_list.get_selection().connect(
153            'changed', self._selection_changed)
154
155        # Tooltip
156        self._ui.transfers_list.connect('query-tooltip', self._query_tooltip)
157        self._ui.transfers_list.set_has_tooltip(True)
158        self.tooltip = FileTransfersTooltip()
159
160        self._ui.connect_signals(self)
161
162    def _query_tooltip(self, widget, x_pos, y_pos, keyboard_mode, tooltip):
163        try:
164            x_pos, y_pos = widget.convert_widget_to_bin_window_coords(
165                x_pos, y_pos)
166            row = widget.get_path_at_pos(x_pos, y_pos)[0]
167        except TypeError:
168            self.tooltip.clear_tooltip()
169            return False
170        if not row:
171            self.tooltip.clear_tooltip()
172            return False
173
174        iter_ = None
175        try:
176            model = widget.get_model()
177            iter_ = model.get_iter(row)
178        except Exception:
179            self.tooltip.clear_tooltip()
180            return False
181
182        sid = self.model[iter_][Column.SID]
183        file_props = FilesProp.getFilePropByType(sid[0], sid[1:])
184
185        value, widget = self.tooltip.get_tooltip(file_props, sid)
186        tooltip.set_custom(widget)
187        return value
188
189    def find_transfer_by_jid(self, account, jid):
190        """
191        Find all transfers with peer 'jid' that belong to 'account'
192        """
193        active_transfers = [[], []]  # ['senders', 'receivers']
194        allfp = FilesProp.getAllFileProp()
195        for file_props in allfp:
196            if file_props.type_ == 's' and file_props.tt_account == account:
197                # 'account' is the sender
198                receiver_jid = file_props.receiver.split('/')[0]
199                if jid == receiver_jid and not is_transfer_stopped(file_props):
200                    active_transfers[0].append(file_props)
201            elif file_props.type_ == 'r' and file_props.tt_account == account:
202                # 'account' is the recipient
203                sender_jid = file_props.sender.split('/')[0]
204                if jid == sender_jid and not is_transfer_stopped(file_props):
205                    active_transfers[1].append(file_props)
206            else:
207                raise Exception('file_props has no type')
208        return active_transfers
209
210    def show_completed(self, jid, file_props):
211        """
212        Show a dialog saying that file (file_props) has been transferred
213        """
214        def on_open(widget, file_props):
215            dialog.destroy()
216            if not file_props.file_name:
217                return
218            path = os.path.split(file_props.file_name)[0]
219            if os.path.exists(path) and os.path.isdir(path):
220                open_file(path)
221            self._ui.transfers_list.get_selection().unselect_all()
222
223        if file_props.type_ == 'r':
224            # file path is used below in 'Save in'
225            (file_path, file_name) = os.path.split(file_props.file_name)
226        else:
227            file_name = file_props.name
228        sectext = _('File name: %s') % GLib.markup_escape_text(file_name)
229        sectext += '\n' + _('Size: %s') % GLib.format_size_full(
230            file_props.size, self.units)
231        if file_props.type_ == 'r':
232            jid = file_props.sender.split('/')[0]
233            sender_name = app.contacts.get_first_contact_from_jid(
234                file_props.tt_account, jid).get_shown_name()
235            sender = sender_name
236        else:
237            # You is a reply of who sent a file
238            sender = _('You')
239        sectext += '\n' + _('Sender: %s') % sender
240        sectext += '\n' + _('Recipient: ')
241        if file_props.type_ == 's':
242            jid = file_props.receiver.split('/')[0]
243            receiver_name = app.contacts.get_first_contact_from_jid(
244                file_props.tt_account, jid).get_shown_name()
245            recipient = receiver_name
246        else:
247            # You is a reply of who received a file
248            recipient = _('You')
249        sectext += recipient
250        if file_props.type_ == 'r':
251            sectext += '\n' + _('Saved in: %s') % file_path
252
253        dialog = HigDialog(app.interface.roster.window,
254                           Gtk.MessageType.INFO,
255                           Gtk.ButtonsType.NONE,
256                           _('File transfer completed'),
257                           sectext)
258        if file_props.type_ == 'r':
259            button = Gtk.Button.new_with_mnemonic(_('Open _Folder'))
260            button.connect('clicked', on_open, file_props)
261            dialog.action_area.pack_start(button, True, True, 0)
262        ok_button = dialog.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
263
264        def on_ok(widget):
265            dialog.destroy()
266        ok_button.connect('clicked', on_ok)
267        dialog.show_all()
268
269    def show_request_error(self, file_props):
270        """
271        Show error dialog to the recipient saying that
272        transfer has been canceled
273        """
274        InformationDialog(
275            _('File transfer cancelled'),
276            _('Connection with peer could not be established.'))
277        self._ui.transfers_list.get_selection().unselect_all()
278
279    def show_send_error(self, file_props):
280        """
281        Show error dialog to the sender saying that transfer has been canceled
282        """
283        InformationDialog(
284            _('File transfer cancelled'),
285            _('Connection with peer could not be established.'))
286        self._ui.transfers_list.get_selection().unselect_all()
287
288    def show_stopped(self, jid, file_props, error_msg=''):
289        if file_props.type_ == 'r':
290            file_name = os.path.basename(file_props.file_name)
291        else:
292            file_name = file_props.name
293        sectext = '\t' + _('Filename: %s') % GLib.markup_escape_text(file_name)
294        sectext += '\n\t' + _('Recipient: %s') % jid
295        if error_msg:
296            sectext += '\n\t' + _('Error message: %s') % error_msg
297        ErrorDialog(_('File transfer stopped'), sectext)
298        self._ui.transfers_list.get_selection().unselect_all()
299
300    def show_hash_error(self, jid, file_props, account):
301
302        def on_yes(dummy, fjid, file_props, account):
303            # Delete old file
304            os.remove(file_props.file_name)
305            jid, resource = app.get_room_and_nick_from_fjid(fjid)
306            if resource:
307                contact = app.contacts.get_contact(account, jid, resource)
308            else:
309                contact = app.contacts.get_contact_with_highest_priority(
310                    account, jid)
311                fjid = contact.get_full_jid()
312            # Request the file to the sender
313            sid = helpers.get_random_string()
314            new_file_props = FilesProp.getNewFileProp(account, sid)
315            new_file_props.file_name = file_props.file_name
316            new_file_props.name = file_props.name
317            new_file_props.desc = file_props.desc
318            new_file_props.size = file_props.size
319            new_file_props.date = file_props.date
320            new_file_props.hash_ = file_props.hash_
321            new_file_props.type_ = 'r'
322            tsid = app.connections[account].get_module(
323                'Jingle').start_file_transfer(fjid, new_file_props, True)
324
325            new_file_props.transport_sid = tsid
326            self.add_transfer(account, contact, new_file_props)
327
328        if file_props.type_ == 'r':
329            file_name = os.path.basename(file_props.file_name)
330        else:
331            file_name = file_props.name
332        ConfirmationDialog(
333            _('File Transfer Error'),
334            _('File Transfer Error'),
335            _('The file %s has been received, but it seems to have '
336              'been damaged along the way.\n'
337              'Do you want to download it again?') % file_name,
338            [DialogButton.make('Cancel',
339                               text=_('_No')),
340             DialogButton.make('Accept',
341                               text=_('_Download Again'),
342                               callback=on_yes,
343                               args=[jid,
344                                     file_props,
345                                     account])]).show()
346
347    def show_file_send_request(self, account, contact):
348        send_callback = partial(self.send_file, account, contact)
349        SendFileDialog(send_callback, self.window)
350
351    def send_file(self, account, contact, file_path, file_desc=''):
352        """
353        Start the real transfer(upload) of the file
354        """
355        if gtkgui_helpers.file_is_locked(file_path):
356            pritext = _('Gajim can not read this file')
357            sextext = _('Another process is using this file.')
358            ErrorDialog(pritext, sextext)
359            return
360
361        if isinstance(contact, str):
362            if contact.find('/') == -1:
363                return
364            (jid, resource) = contact.split('/', 1)
365            contact = app.contacts.create_contact(jid=jid, account=account,
366                                                  resource=resource)
367        file_name = os.path.split(file_path)[1]
368        file_props = self.get_send_file_props(account, contact, file_path,
369                                              file_name, file_desc)
370        if file_props is None:
371            return False
372
373        app.connections[account].get_module('Jingle').start_file_transfer(
374            contact.get_full_jid(), file_props)
375        self.add_transfer(account, contact, file_props)
376
377        return True
378
379    def _start_receive(self, file_path, account, contact, file_props):
380        file_dir = os.path.dirname(file_path)
381        if file_dir:
382            app.settings.set('last_save_dir', file_dir)
383        file_props.file_name = file_path
384        file_props.type_ = 'r'
385        self.add_transfer(account, contact, file_props)
386        app.connections[account].get_module('Bytestream').send_file_approval(
387            file_props)
388
389    def on_file_request_accepted(self, account, contact, file_props):
390        def _on_accepted(account, contact, file_props, file_path):
391            if os.path.exists(file_path):
392                app.settings.set('last_save_dir', os.path.dirname(file_path))
393
394                # Check if we have write permissions
395                if not os.access(file_path, os.W_OK):
396                    file_name = GLib.markup_escape_text(
397                        os.path.basename(file_path))
398                    ErrorDialog(
399                        _('Cannot overwrite existing file \'%s\'') % file_name,
400                        _('A file with this name already exists and you do '
401                          'not have permission to overwrite it.'))
402                    return
403
404                stat = os.stat(file_path)
405                dl_size = stat.st_size
406                file_size = file_props.size
407                dl_finished = dl_size >= file_size
408
409                def _on_resume():
410                    if not dl_finished:
411                        file_props.offset = dl_size
412                    self._start_receive(
413                        file_path, account, contact, file_props)
414
415                def _on_replace():
416                    self._start_receive(
417                        file_path, account, contact, file_props)
418
419                def _on_cancel():
420                    con.get_module('Bytestream').send_file_rejection(
421                        file_props)
422
423                ConfirmationDialog(
424                    _('File Transfer Conflict'),
425                    _('File already exists'),
426                    _('Resume download or replace file?'),
427                    [DialogButton.make('Cancel',
428                                       callback=_on_cancel),
429                     DialogButton.make('OK',
430                                       text=_('Resume _Download'),
431                                       callback=_on_resume),
432                     DialogButton.make('Accept',
433                                       text=_('Replace _File'),
434                                       callback=_on_replace)]).show()
435
436            # File does not exist yet
437            dirname = os.path.dirname(file_path)
438            if not os.access(dirname, os.W_OK) and os.name != 'nt':
439                # read-only bit is used to mark special folder under
440                # windows, not to mark that a folder is read-only.
441                # See ticket #3587
442                ErrorDialog(
443                    _('Directory \'%s\' is not writable') % dirname,
444                    _('You do not have permissions to create files '
445                      'in this directory.'))
446                return
447            self._start_receive(file_path, account, contact, file_props)
448
449        # Show file save as dialog
450        con = app.connections[account]
451        accept_cb = partial(_on_accepted, account, contact, file_props)
452        cancel_cb = partial(con.get_module('Bytestream').send_file_rejection,
453                            file_props)
454        FileSaveDialog(accept_cb,
455                       cancel_cb,
456                       path=app.settings.get('last_save_dir'),
457                       file_name=file_props.name)
458
459    def show_file_request(self, account, contact, file_props):
460        """
461        Show dialog asking for comfirmation and store location of new file
462        requested by a contact
463        """
464        if not file_props or not file_props.name:
465            return
466
467        sectext = _('File: %s') % GLib.markup_escape_text(
468            file_props.name)
469        if file_props.size:
470            sectext += '\n' + _('Size: %s') % GLib.format_size_full(
471                file_props.size, self.units)
472        if file_props.mime_type:
473            sectext += '\n' + _('Type: %s') % file_props.mime_type
474        if file_props.desc:
475            sectext += '\n' + _('Description: %s') % file_props.desc
476
477        def _on_ok():
478            self.on_file_request_accepted(account, contact, file_props)
479
480        def _on_cancel():
481            app.connections[account].get_module(
482                'Bytestream').send_file_rejection(file_props)
483
484        ConfirmationDialog(
485            _('File Transfer Request'),
486            _('%s wants to send you a file') % contact.get_shown_name(),
487            sectext,
488            [DialogButton.make('Cancel',
489                               callback=_on_cancel),
490             DialogButton.make('Accept',
491                               callback=_on_ok)]).show()
492
493    def set_status(self, file_props, status):
494        """
495        Change the status of a transfer to state 'status'
496        """
497        iter_ = self.get_iter_by_sid(file_props.type_, file_props.sid)
498        if iter_ is None:
499            return
500
501        if status == 'stop':
502            file_props.stopped = True
503        elif status == 'ok':
504            file_props.completed = True
505            text = self._format_percent(100)
506            received_size = GLib.format_size_full(
507                int(file_props.received_len), self.units)
508            full_size = GLib.format_size_full(file_props.size, self.units)
509            text += received_size + '/' + full_size
510            self.model.set(iter_, Column.PROGRESS, text)
511            self.model.set(iter_, Column.PULSE, GLib.MAXINT32)
512        elif status == 'computing':
513            self.model.set(iter_, Column.PULSE, 1)
514            text = _('Checking file…') + '\n'
515            received_size = GLib.format_size_full(
516                int(file_props.received_len), self.units)
517            full_size = GLib.format_size_full(file_props.size, self.units)
518            text += received_size + '/' + full_size
519            self.model.set(iter_, Column.PROGRESS, text)
520
521            def pulse():
522                p = self.model.get(iter_, Column.PULSE)[0]
523                if p == GLib.MAXINT32:
524                    return False
525                self.model.set(iter_, Column.PULSE, p + 1)
526                return True
527            GLib.timeout_add(100, pulse)
528        elif status == 'hash_error':
529            text = _('File error') + '\n'
530            received_size = GLib.format_size_full(
531                int(file_props.received_len), self.units)
532            full_size = GLib.format_size_full(file_props.size, self.units)
533            text += received_size + '/' + full_size
534            self.model.set(iter_, Column.PROGRESS, text)
535            self.model.set(iter_, Column.PULSE, GLib.MAXINT32)
536        self.model.set(iter_, Column.IMAGE, self.icons[status])
537        path = self.model.get_path(iter_)
538        self._select_func(path)
539
540    def _format_percent(self, percent):
541        """
542        Add extra spaces from both sides of the percent, so that progress
543        string has always a fixed size
544        """
545        _str = '          '
546        if percent != 100.:
547            _str += ' '
548        if percent < 10:
549            _str += ' '
550        _str += str(percent) + '%          \n'
551        return _str
552
553    def _format_time(self, _time):
554        times = {'hours': 0, 'minutes': 0, 'seconds': 0}
555        _time = int(_time)
556        times['seconds'] = _time % 60
557        if _time >= 60:
558            _time /= 60
559            times['minutes'] = _time % 60
560            if _time >= 60:
561                times['hours'] = _time / 60
562
563        # Print remaining time in format 00:00:00
564        # You can change the places of (hours), (minutes), (seconds) -
565        # they are not translatable.
566        return _('%(hours)02.d:%(minutes)02.d:%(seconds)02.d') % times
567
568    def _get_eta_and_speed(self, full_size, transfered_size, file_props):
569        if not file_props.transfered_size:
570            return 0., 0.
571
572        if len(file_props.transfered_size) == 1:
573            speed = round(float(transfered_size) / file_props.elapsed_time)
574        else:
575            # first and last are (time, transfered_size)
576            first = file_props.transfered_size[0]
577            last = file_props.transfered_size[-1]
578            transfered = last[1] - first[1]
579            tim = last[0] - first[0]
580            if tim == 0:
581                return 0., 0.
582            speed = round(float(transfered) / tim)
583        if speed == 0.:
584            return 0., 0.
585        remaining_size = full_size - transfered_size
586        eta = remaining_size / speed
587        return eta, speed
588
589    def _remove_transfer(self, iter_, sid, file_props):
590        self.model.remove(iter_)
591        if not file_props:
592            return
593        if file_props.tt_account:
594            # file transfer is set
595            account = file_props.tt_account
596            if account in app.connections:
597                # there is a connection to the account
598                app.connections[account].get_module(
599                    'Bytestream').remove_transfer(file_props)
600            if file_props.type_ == 'r':  # we receive a file
601                other = file_props.sender
602            else:  # we send a file
603                other = file_props.receiver
604            if isinstance(other, str):
605                jid = app.get_jid_without_resource(other)
606            else:  # It's a Contact instance
607                jid = other.jid
608            for ev_type in ('file-error', 'file-completed',
609                            'file-request-error', 'file-send-error',
610                            'file-stopped'):
611                for event in app.events.get_events(account, jid, [ev_type]):
612                    if event.file_props.sid == file_props.sid:
613                        app.events.remove_events(account, jid, event)
614                        app.interface.roster.draw_contact(jid, account)
615                        app.interface.roster.show_title()
616        FilesProp.deleteFileProp(file_props)
617        del file_props
618
619    def set_progress(self, typ, sid, transfered_size, iter_=None):
620        """
621        Change the progress of a transfer with new transfered size
622        """
623        file_props = FilesProp.getFilePropByType(typ, sid)
624        full_size = file_props.size
625        if full_size == 0:
626            percent = 0
627        else:
628            percent = round(float(transfered_size) / full_size * 100, 1)
629        if iter_ is None:
630            iter_ = self.get_iter_by_sid(typ, sid)
631        if iter_ is not None:
632            just_began = False
633            if self.model[iter_][Column.PERCENT] == 0 and int(percent > 0):
634                just_began = True
635            text = self._format_percent(percent)
636            if transfered_size == 0:
637                text += '0'
638            else:
639                text += GLib.format_size_full(transfered_size, self.units)
640            text += '/' + GLib.format_size_full(full_size, self.units)
641            # Kb/s
642
643            # remaining time
644            if file_props.offset:
645                transfered_size -= file_props.offset
646                full_size -= file_props.offset
647
648            if file_props.elapsed_time > 0:
649                file_props.transfered_size.append((file_props.last_time,
650                                                   transfered_size))
651            if len(file_props.transfered_size) > 6:
652                file_props.transfered_size.pop(0)
653            eta, speed = self._get_eta_and_speed(full_size, transfered_size,
654                                                 file_props)
655
656            self.model.set(iter_, Column.PROGRESS, text)
657            self.model.set(iter_, Column.PERCENT, int(percent))
658            text = self._format_time(eta)
659            text += '\n'
660            # This should make the string KB/s,
661            # where 'KB' part is taken from %s.
662            # Only the 's' after / (which means second) should be translated.
663            text += _('(%(filesize_unit)s/s)') % {
664                'filesize_unit': GLib.format_size_full(speed, self.units)}
665            self.model.set(iter_, Column.TIME, text)
666
667            # try to guess what should be the status image
668            if file_props.type_ == 'r':
669                status = 'download'
670            else:
671                status = 'upload'
672            if file_props.paused is True:
673                status = 'pause'
674            elif file_props.stalled is True:
675                status = 'waiting'
676            if file_props.connected is False:
677                status = 'stop'
678            self.model.set(iter_, 0, self.icons[status])
679            if transfered_size == full_size:
680                # If we are receiver and this is a jingle session
681                if file_props.type_ == 'r' and  \
682                file_props.session_type == 'jingle' and file_props.hash_:
683                    # Show that we are computing the hash
684                    self.set_status(file_props, 'computing')
685                else:
686                    self.set_status(file_props, 'ok')
687            elif just_began:
688                path = self.model.get_path(iter_)
689                self._select_func(path)
690
691    def get_iter_by_sid(self, typ, sid):
692        """
693        Return iter to the row, which holds file transfer, identified by the
694        session id
695        """
696        iter_ = self.model.get_iter_first()
697        while iter_:
698            if typ + sid == self.model[iter_][Column.SID]:
699                return iter_
700            iter_ = self.model.iter_next(iter_)
701
702    def __convert_date(self, epoch):
703        # Converts date-time from seconds from epoch to iso 8601
704        dt = datetime.utcfromtimestamp(epoch)
705        return dt.isoformat() + 'Z'
706
707    def get_send_file_props(self, account, contact, file_path, file_name,
708                            file_desc=''):
709        """
710        Create new file_props object and set initial file transfer
711        properties in it
712        """
713        if os.path.isfile(file_path):
714            stat = os.stat(file_path)
715        else:
716            ErrorDialog(_('Invalid File'), _('File: ') + file_path)
717            return None
718        if stat[6] == 0:
719            ErrorDialog(
720                _('Invalid File'),
721                _('It is not possible to send empty files'))
722            return None
723        file_props = FilesProp.getNewFileProp(
724            account, sid=helpers.get_random_string())
725        mod_date = os.path.getmtime(file_path)
726        file_props.file_name = file_path
727        file_props.name = file_name
728        file_props.date = self.__convert_date(mod_date)
729        file_props.type_ = 's'
730        file_props.desc = file_desc
731        file_props.elapsed_time = 0
732        file_props.size = stat[6]
733        file_props.sender = account
734        file_props.receiver = contact
735        file_props.tt_account = account
736        return file_props
737
738    def add_transfer(self, account, contact, file_props):
739        """
740        Add new transfer to FT window and show the FT window
741        """
742        if file_props is None:
743            return
744        file_props.elapsed_time = 0
745        iter_ = self.model.prepend()
746        if file_props.type_ == 'r':
747            text_labels = '\n<b>' + _('Sender: ') + '</b>'
748        else:
749            text_labels = '\n<b>' + _('Recipient: ') + '</b>'
750
751        if file_props.type_ == 'r':
752            file_name = os.path.split(file_props.file_name)[1]
753        else:
754            file_name = file_props.name
755        text_props = GLib.markup_escape_text(file_name) + '\n'
756        text_props += contact.get_shown_name()
757        self.model.set(iter_,
758                       1,
759                       text_labels,
760                       2,
761                       text_props,
762                       Column.PULSE,
763                       -1,
764                       Column.SID,
765                       file_props.type_ + file_props.sid)
766        self.set_progress(file_props.type_, file_props.sid, 0, iter_)
767        if file_props.started is False:
768            status = 'waiting'
769        elif file_props.type_ == 'r':
770            status = 'download'
771        else:
772            status = 'upload'
773        file_props.tt_account = account
774        self.set_status(file_props, status)
775        self._set_cleanup_sensitivity()
776        self.window.show_all()
777
778    def _on_transfers_list_row_activated(self, widget, path, col):
779        # try to open the containing folder
780        self._on_open_folder_menuitem_activate(widget)
781
782    def _set_cleanup_sensitivity(self):
783        """
784        Check if there are transfer rows and set cleanup_button sensitive, or
785        insensitive if model is empty
786        """
787        if not self.model:
788            self._ui.cleanup_button.set_sensitive(False)
789        else:
790            self._ui.cleanup_button.set_sensitive(True)
791
792    def _set_all_insensitive(self):
793        """
794        Make all buttons/menuitems insensitive
795        """
796        self._ui.pause_resume_button.set_sensitive(False)
797        self._ui.pause_resume_menuitem.set_sensitive(False)
798        self._ui.remove_menuitem.set_sensitive(False)
799        self._ui.cancel_button.set_sensitive(False)
800        self._ui.cancel_menuitem.set_sensitive(False)
801        self._ui.open_folder_menuitem.set_sensitive(False)
802        self._set_cleanup_sensitivity()
803
804    def _set_buttons_sensitive(self, path, is_row_selected):
805        """
806        Make buttons/menuitems sensitive as appropriate to the state of file
807        transfer located at path 'path'
808        """
809        if path is None:
810            self._set_all_insensitive()
811            return
812        current_iter = self.model.get_iter(path)
813        sid = self.model[current_iter][Column.SID]
814        file_props = FilesProp.getFilePropByType(sid[0], sid[1:])
815        self._ui.remove_menuitem.set_sensitive(is_row_selected)
816        self._ui.open_folder_menuitem.set_sensitive(is_row_selected)
817        is_stopped = False
818        if is_transfer_stopped(file_props):
819            is_stopped = True
820        self._ui.cancel_button.set_sensitive(not is_stopped)
821        self._ui.cancel_menuitem.set_sensitive(not is_stopped)
822        if not is_row_selected:
823            # No selection, disable the buttons
824            self._set_all_insensitive()
825        elif not is_stopped and file_props.continue_cb:
826            if is_transfer_active(file_props):
827                # File transfer is active
828                self._toggle_pause_continue(True)
829                self._ui.pause_resume_button.set_sensitive(True)
830                self._ui.pause_resume_menuitem.set_sensitive(True)
831            elif is_transfer_paused(file_props):
832                # File transfer is paused
833                self._toggle_pause_continue(False)
834                self._ui.pause_resume_button.set_sensitive(True)
835                self._ui.pause_resume_menuitem.set_sensitive(True)
836            else:
837                self._ui.pause_resume_button.set_sensitive(False)
838                self._ui.pause_resume_menuitem.set_sensitive(False)
839        else:
840            self._ui.pause_resume_button.set_sensitive(False)
841            self._ui.pause_resume_menuitem.set_sensitive(False)
842        return True
843
844    def _selection_changed(self, args):
845        """
846        Selection has changed - change the sensitivity of the buttons/menuitems
847        """
848        selection = args
849        selected = selection.get_selected_rows()
850        if selected[1] != []:
851            selected_path = selected[1][0]
852            self._select_func(selected_path)
853        else:
854            self._set_all_insensitive()
855
856    def _select_func(self, path):
857        is_selected = False
858        selected = self._ui.transfers_list.get_selection().get_selected_rows()
859        if selected[1] != []:
860            selected_path = selected[1][0]
861            if selected_path == path:
862                is_selected = True
863        self._set_buttons_sensitive(path, is_selected)
864        self._set_cleanup_sensitivity()
865        return True
866
867    def _on_cleanup_button_clicked(self, widget):
868        i = len(self.model) - 1
869        while i >= 0:
870            iter_ = self.model.get_iter((i))
871            sid = self.model[iter_][Column.SID]
872            file_props = FilesProp.getFilePropByType(sid[0], sid[1:])
873            if is_transfer_stopped(file_props):
874                self._remove_transfer(iter_, sid, file_props)
875            i -= 1
876        self._ui.transfers_list.get_selection().unselect_all()
877        self._set_all_insensitive()
878
879    def _toggle_pause_continue(self, status):
880        if status:
881            self._ui.pause_resume_button.set_icon_name(
882                'media-playback-pause-symbolic')
883        else:
884            self._ui.pause_resume_button.set_icon_name(
885                'media-playback-start-symbolic')
886
887    def _on_pause_resume_button_clicked(self, widget):
888        selected = self._ui.transfers_list.get_selection().get_selected()
889        if selected is None or selected[1] is None:
890            return
891        s_iter = selected[1]
892        sid = self.model[s_iter][Column.SID]
893        file_props = FilesProp.getFilePropByType(sid[0], sid[1:])
894        if is_transfer_paused(file_props):
895            file_props.last_time = time.time()
896            file_props.paused = False
897            types = {'r': 'download', 's': 'upload'}
898            self.set_status(file_props, types[sid[0]])
899            self._toggle_pause_continue(True)
900            if file_props.continue_cb:
901                file_props.continue_cb()
902        elif is_transfer_active(file_props):
903            file_props.paused = True
904            self.set_status(file_props, 'pause')
905            # Reset that to compute speed only when we resume
906            file_props.transfered_size = []
907            self._toggle_pause_continue(False)
908
909    def _on_cancel_button_clicked(self, widget):
910        selected = self._ui.transfers_list.get_selection().get_selected()
911        if selected is None or selected[1] is None:
912            return
913        s_iter = selected[1]
914        sid = self.model[s_iter][Column.SID]
915        file_props = FilesProp.getFilePropByType(sid[0], sid[1:])
916        account = file_props.tt_account
917        if account not in app.connections:
918            return
919        con = app.connections[account]
920        # Check if we are in a IBB transfer
921        if file_props.direction:
922            con.get_module('IBB').send_close(file_props)
923        con.get_module('Bytestream').disconnect_transfer(file_props)
924        self.set_status(file_props, 'stop')
925
926    def _on_notify_ft_complete_toggled(self, widget, *args):
927        app.settings.set('notify_on_file_complete', widget.get_active())
928
929    def _on_file_transfers_dialog_delete_event(self, widget, event):
930        self.window.hide()
931        return True  # Do NOT destroy window
932
933    def _show_context_menu(self, event, iter_):
934        # change the sensitive property of the buttons and menuitems
935        if iter_:
936            path = self.model.get_path(iter_)
937            self._set_buttons_sensitive(path, True)
938
939        event_button = gtkgui_helpers.get_possible_button_event(event)
940        self._ui.file_transfers_menu.show_all()
941        self._ui.file_transfers_menu.popup(
942            None,
943            self._ui.transfers_list,
944            None,
945            None,
946            event_button,
947            event.time)
948
949    def _on_transfers_list_key_press_event(self, widget, event):
950        """
951        When a key is pressed in the treeviews
952        """
953        iter_ = None
954        try:
955            iter_ = self._ui.transfers_list.get_selection().get_selected()[1]
956        except TypeError:
957            self._ui.transfers_list.get_selection().unselect_all()
958
959        if iter_ is not None:
960            path = self.model.get_path(iter_)
961            self._ui.transfers_list.get_selection().select_path(path)
962
963        if event.keyval == Gdk.KEY_Menu:
964            self._show_context_menu(event, iter_)
965            return True
966
967    def _on_transfers_list_button_release_event(self, widget, event):
968        # hide tooltip, no matter the button is pressed
969        path = None
970        try:
971            path = self._ui.transfers_list.get_path_at_pos(int(event.x),
972                                                           int(event.y))[0]
973        except TypeError:
974            self._ui.transfers_list.get_selection().unselect_all()
975        if path is None:
976            self._set_all_insensitive()
977        else:
978            self._select_func(path)
979
980    def _on_transfers_list_button_press_event(self, widget, event):
981        # hide tooltip, no matter the button is pressed
982        path, iter_ = None, None
983        try:
984            path = self._ui.transfers_list.get_path_at_pos(int(event.x),
985                                                           int(event.y))[0]
986        except TypeError:
987            self._ui.transfers_list.get_selection().unselect_all()
988        if event.button == 3:  # Right click
989            if path:
990                self._ui.transfers_list.get_selection().select_path(path)
991                iter_ = self.model.get_iter(path)
992            self._show_context_menu(event, iter_)
993            if path:
994                return True
995
996    def _on_open_folder_menuitem_activate(self, widget):
997        selected = self._ui.transfers_list.get_selection().get_selected()
998        if not selected or not selected[1]:
999            return
1000        s_iter = selected[1]
1001        sid = self.model[s_iter][Column.SID]
1002        file_props = FilesProp.getFilePropByType(sid[0], sid[1:])
1003        if not file_props.file_name:
1004            return
1005        path = os.path.split(file_props.file_name)[0]
1006        if os.path.exists(path) and os.path.isdir(path):
1007            open_file(path)
1008
1009    def _on_cancel_menuitem_activate(self, widget):
1010        self._on_cancel_button_clicked(widget)
1011
1012    def _on_continue_menuitem_activate(self, widget):
1013        self._on_pause_resume_button_clicked(widget)
1014
1015    def _on_pause_resume_menuitem_activate(self, widget):
1016        self._on_pause_resume_button_clicked(widget)
1017
1018    def _on_remove_menuitem_activate(self, widget):
1019        selected = self._ui.transfers_list.get_selection().get_selected()
1020        if not selected or not selected[1]:
1021            return
1022        s_iter = selected[1]
1023        sid = self.model[s_iter][Column.SID]
1024        file_props = FilesProp.getFilePropByType(sid[0], sid[1:])
1025        self._remove_transfer(s_iter, sid, file_props)
1026        self._set_all_insensitive()
1027
1028    def _on_file_transfers_window_key_press_event(self, widget, event):
1029        if event.keyval == Gdk.KEY_Escape:  # ESCAPE
1030            self.window.hide()
1031
1032
1033class SendFileDialog(Gtk.ApplicationWindow):
1034    def __init__(self, send_callback, transient_for):
1035        Gtk.ApplicationWindow.__init__(self)
1036        self.set_name('SendFileDialog')
1037        self.set_application(app.app)
1038        self.set_show_menubar(False)
1039        self.set_resizable(True)
1040        self.set_default_size(500, 350)
1041        self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
1042        self.set_transient_for(transient_for)
1043        self.set_title(_('Choose a File to Send…'))
1044        self.set_destroy_with_parent(True)
1045
1046        self._send_callback = send_callback
1047
1048        self._ui = get_builder('filetransfers_send_file_dialog.ui')
1049        self.add(self._ui.send_file_grid)
1050        self.connect('key-press-event', self._key_press_event)
1051        self._ui.connect_signals(self)
1052        self.show_all()
1053
1054    def _send(self, button):
1055        for file in self._ui.listbox.get_children():
1056            self._send_callback(str(file.path), self._get_description())
1057        self.destroy()
1058
1059    def _select_files(self, button):
1060        FileChooserDialog(self._set_files,
1061                          select_multiple=True,
1062                          transient_for=self,
1063                          path=app.settings.get('last_send_dir'))
1064
1065    def _remove_files(self, button):
1066        selected = self._ui.listbox.get_selected_rows()
1067        for item in selected:
1068            self._ui.listbox.remove(item)
1069
1070    def _set_files(self, filenames):
1071        for file in filenames:
1072            row = FileRow(file)
1073            if row.path.is_dir():
1074                continue
1075            last_dir = row.path.parent
1076            self._ui.listbox.add(row)
1077        self._ui.listbox.show_all()
1078        app.settings.set('last_send_dir', str(last_dir))
1079
1080    def _get_description(self):
1081        buffer_ = self._ui.description.get_buffer()
1082        start, end = buffer_.get_bounds()
1083        return buffer_.get_text(start, end, False)
1084
1085    def _key_press_event(self, widget, event):
1086        if event.keyval == Gdk.KEY_Escape:
1087            self.destroy()
1088
1089
1090class FileRow(Gtk.ListBoxRow):
1091    def __init__(self, path):
1092        Gtk.ListBoxRow.__init__(self)
1093        self.path = Path(path)
1094        label = Gtk.Label(label=self.path.name)
1095        label.set_ellipsize(Pango.EllipsizeMode.END)
1096        label.set_xalign(0)
1097        self.add(label)
1098