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