2 ## src/filetransfers_window.py
4 ## Copyright (C) 2003-2010 Yann Leboulanger <asterix AT lagaule.org>
5 ## Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
6 ## Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
7 ## Copyright (C) 2006 Travis Shirk <travis AT pobox.com>
9 ## This file is part of Gajim.
11 ## Gajim is free software; you can redistribute it and/or modify
12 ## it under the terms of the GNU General Public License as published
13 ## by the Free Software Foundation; version 3 only.
15 ## Gajim is distributed in the hope that it will be useful,
16 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ## GNU General Public License for more details.
20 ## You should have received a copy of the GNU General Public License
21 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
34 from common
import gajim
35 from common
import helpers
36 from common
.protocol
.bytestream
import (is_transfer_active
, is_transfer_paused
,
48 class FileTransfersWindow
:
50 self
.files_props
= {'r' : {}, 's': {}}
52 self
.xml
= gtkgui_helpers
.get_gtk_builder('filetransfers.ui')
53 self
.window
= self
.xml
.get_object('file_transfers_window')
54 self
.tree
= self
.xml
.get_object('transfers_list')
55 self
.cancel_button
= self
.xml
.get_object('cancel_button')
56 self
.pause_button
= self
.xml
.get_object('pause_restore_button')
57 self
.cleanup_button
= self
.xml
.get_object('cleanup_button')
58 self
.notify_ft_checkbox
= self
.xml
.get_object(
59 'notify_ft_complete_checkbox')
61 shall_notify
= gajim
.config
.get('notify_on_file_complete')
62 self
.notify_ft_checkbox
.set_active(shall_notify
64 self
.model
= gtk
.ListStore(gtk
.gdk
.Pixbuf
, str, str, str, str, int, str)
65 self
.tree
.set_model(self
.model
)
66 col
= gtk
.TreeViewColumn()
68 render_pixbuf
= gtk
.CellRendererPixbuf()
70 col
.pack_start(render_pixbuf
, expand
=True)
71 render_pixbuf
.set_property('xpad', 3)
72 render_pixbuf
.set_property('ypad', 3)
73 render_pixbuf
.set_property('yalign', .0)
74 col
.add_attribute(render_pixbuf
, 'pixbuf', 0)
75 self
.tree
.append_column(col
)
77 col
= gtk
.TreeViewColumn(_('File'))
78 renderer
= gtk
.CellRendererText()
79 col
.pack_start(renderer
, expand
=False)
80 col
.add_attribute(renderer
, 'markup', C_LABELS
)
81 renderer
.set_property('yalign', 0.)
82 renderer
= gtk
.CellRendererText()
83 col
.pack_start(renderer
, expand
=True)
84 col
.add_attribute(renderer
, 'markup', C_FILE
)
85 renderer
.set_property('xalign', 0.)
86 renderer
.set_property('yalign', 0.)
87 renderer
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
88 col
.set_resizable(True)
90 self
.tree
.append_column(col
)
92 col
= gtk
.TreeViewColumn(_('Time'))
93 renderer
= gtk
.CellRendererText()
94 col
.pack_start(renderer
, expand
=False)
95 col
.add_attribute(renderer
, 'markup', C_TIME
)
96 renderer
.set_property('yalign', 0.5)
97 renderer
.set_property('xalign', 0.5)
98 renderer
= gtk
.CellRendererText()
99 renderer
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
100 col
.set_resizable(True)
101 col
.set_expand(False)
102 self
.tree
.append_column(col
)
104 col
= gtk
.TreeViewColumn(_('Progress'))
105 renderer
= gtk
.CellRendererProgress()
106 renderer
.set_property('yalign', 0.5)
107 renderer
.set_property('xalign', 0.5)
108 col
.pack_start(renderer
, expand
=False)
109 col
.add_attribute(renderer
, 'text', C_PROGRESS
)
110 col
.add_attribute(renderer
, 'value', C_PERCENT
)
111 col
.set_resizable(True)
112 col
.set_expand(False)
113 self
.tree
.append_column(col
)
117 'upload': gtk
.STOCK_GO_UP
,
118 'download': gtk
.STOCK_GO_DOWN
,
119 'stop': gtk
.STOCK_STOP
,
120 'waiting': gtk
.STOCK_REFRESH
,
121 'pause': gtk
.STOCK_MEDIA_PAUSE
,
122 'continue': gtk
.STOCK_MEDIA_PLAY
,
123 'ok': gtk
.STOCK_APPLY
,
126 self
.tree
.get_selection().set_mode(gtk
.SELECTION_SINGLE
)
127 self
.tree
.get_selection().connect('changed', self
.selection_changed
)
128 self
.tooltip
= tooltips
.FileTransfersTooltip()
129 self
.file_transfers_menu
= self
.xml
.get_object('file_transfers_menu')
130 self
.open_folder_menuitem
= self
.xml
.get_object('open_folder_menuitem')
131 self
.cancel_menuitem
= self
.xml
.get_object('cancel_menuitem')
132 self
.pause_menuitem
= self
.xml
.get_object('pause_menuitem')
133 self
.continue_menuitem
= self
.xml
.get_object('continue_menuitem')
134 self
.remove_menuitem
= self
.xml
.get_object('remove_menuitem')
135 self
.xml
.connect_signals(self
)
137 def find_transfer_by_jid(self
, account
, jid
):
139 Find all transfers with peer 'jid' that belong to 'account'
141 active_transfers
= [[], []] # ['senders', 'receivers']
143 # 'account' is the sender
144 for file_props
in self
.files_props
['s'].values():
145 if file_props
['tt_account'] == account
:
146 receiver_jid
= unicode(file_props
['receiver']).split('/')[0]
147 if jid
== receiver_jid
:
148 if not is_transfer_stopped(file_props
):
149 active_transfers
[0].append(file_props
)
151 # 'account' is the recipient
152 for file_props
in self
.files_props
['r'].values():
153 if file_props
['tt_account'] == account
:
154 sender_jid
= unicode(file_props
['sender']).split('/')[0]
155 if jid
== sender_jid
:
156 if not is_transfer_stopped(file_props
):
157 active_transfers
[1].append(file_props
)
158 return active_transfers
160 def show_completed(self
, jid
, file_props
):
162 Show a dialog saying that file (file_props) has been transferred
164 def on_open(widget
, file_props
):
166 if 'file-name' not in file_props
:
168 path
= os
.path
.split(file_props
['file-name'])[0]
169 if os
.path
.exists(path
) and os
.path
.isdir(path
):
170 helpers
.launch_file_manager(path
)
171 self
.tree
.get_selection().unselect_all()
173 if file_props
['type'] == 'r':
174 # file path is used below in 'Save in'
175 (file_path
, file_name
) = os
.path
.split(file_props
['file-name'])
177 file_name
= file_props
['name']
178 sectext
= '\t' + _('Filename: %s') % file_name
179 sectext
+= '\n\t' + _('Size: %s') % \
180 helpers
.convert_bytes(file_props
['size'])
181 if file_props
['type'] == 'r':
182 jid
= unicode(file_props
['sender']).split('/')[0]
183 sender_name
= gajim
.contacts
.get_first_contact_from_jid(
184 file_props
['tt_account'], jid
).get_shown_name()
187 #You is a reply of who sent a file
189 sectext
+= '\n\t' + _('Sender: %s') % sender
190 sectext
+= '\n\t' + _('Recipient: ')
191 if file_props
['type'] == 's':
192 jid
= unicode(file_props
['receiver']).split('/')[0]
193 receiver_name
= gajim
.contacts
.get_first_contact_from_jid(
194 file_props
['tt_account'], jid
).get_shown_name()
195 recipient
= receiver_name
197 #You is a reply of who received a file
200 if file_props
['type'] == 'r':
201 sectext
+= '\n\t' + _('Saved in: %s') % file_path
202 dialog
= dialogs
.HigDialog(None, gtk
.MESSAGE_INFO
, gtk
.BUTTONS_NONE
,
203 _('File transfer completed'), sectext
)
204 if file_props
['type'] == 'r':
205 button
= gtk
.Button(_('_Open Containing Folder'))
206 button
.connect('clicked', on_open
, file_props
)
207 dialog
.action_area
.pack_start(button
)
208 ok_button
= dialog
.add_button(gtk
.STOCK_OK
, gtk
.RESPONSE_OK
)
211 ok_button
.connect('clicked', on_ok
)
214 def show_request_error(self
, file_props
):
216 Show error dialog to the recipient saying that transfer has been canceled
218 dialogs
.InformationDialog(_('File transfer cancelled'), _('Connection with peer cannot be established.'))
219 self
.tree
.get_selection().unselect_all()
221 def show_send_error(self
, file_props
):
223 Show error dialog to the sender saying that transfer has been canceled
225 dialogs
.InformationDialog(_('File transfer cancelled'),
226 _('Connection with peer cannot be established.'))
227 self
.tree
.get_selection().unselect_all()
229 def show_stopped(self
, jid
, file_props
, error_msg
=''):
230 if file_props
['type'] == 'r':
231 file_name
= os
.path
.basename(file_props
['file-name'])
233 file_name
= file_props
['name']
234 sectext
= '\t' + _('Filename: %s') % file_name
235 sectext
+= '\n\t' + _('Recipient: %s') % jid
237 sectext
+= '\n\t' + _('Error message: %s') % error_msg
238 dialogs
.ErrorDialog(_('File transfer stopped'), sectext
)
239 self
.tree
.get_selection().unselect_all()
241 def show_file_send_request(self
, account
, contact
):
242 win
= gtk
.ScrolledWindow()
243 win
.set_shadow_type(gtk
.SHADOW_IN
)
244 win
.set_policy(gtk
.POLICY_NEVER
, gtk
.POLICY_NEVER
)
246 from message_textview
import MessageTextView
247 desc_entry
= MessageTextView()
252 files_path_list
= dialog
.get_filenames()
253 files_path_list
= gtkgui_helpers
.decode_filechooser_file_paths(
255 text_buffer
= desc_entry
.get_buffer()
256 desc
= text_buffer
.get_text(text_buffer
.get_start_iter(),
257 text_buffer
.get_end_iter())
258 for file_path
in files_path_list
:
259 if self
.send_file(account
, contact
, file_path
, desc
) \
260 and file_dir
is None:
261 file_dir
= os
.path
.dirname(file_path
)
263 gajim
.config
.set('last_send_dir', file_dir
)
266 dialog
= dialogs
.FileChooserDialog(_('Choose File to Send...'),
267 gtk
.FILE_CHOOSER_ACTION_OPEN
, (gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
),
269 True, # select multiple true as we can select many files to send
270 gajim
.config
.get('last_send_dir'),
271 on_response_ok
=on_ok
,
272 on_response_cancel
=lambda e
:dialog
.destroy()
275 btn
= gtk
.Button(_('_Send'))
276 btn
.set_property('can-default', True)
277 # FIXME: add send icon to this button (JUMP_TO)
278 dialog
.add_action_widget(btn
, gtk
.RESPONSE_OK
)
279 dialog
.set_default_response(gtk
.RESPONSE_OK
)
281 desc_hbox
= gtk
.HBox(False, 5)
282 desc_hbox
.pack_start(gtk
.Label(_('Description: ')), False, False, 0)
283 desc_hbox
.pack_start(win
, True, True, 0)
285 dialog
.vbox
.pack_start(desc_hbox
, False, False, 0)
290 def send_file(self
, account
, contact
, file_path
, file_desc
=''):
292 Start the real transfer(upload) of the file
294 if gtkgui_helpers
.file_is_locked(file_path
):
295 pritext
= _('Gajim cannot access this file')
296 sextext
= _('This file is being used by another process.')
297 dialogs
.ErrorDialog(pritext
, sextext
)
300 if isinstance(contact
, str):
301 if contact
.find('/') == -1:
303 (jid
, resource
) = contact
.split('/', 1)
304 contact
= gajim
.contacts
.create_contact(jid
=jid
, account
=account
,
306 file_name
= os
.path
.split(file_path
)[1]
307 file_props
= self
.get_send_file_props(account
, contact
,
308 file_path
, file_name
, file_desc
)
309 if file_props
is None:
311 self
.add_transfer(account
, contact
, file_props
)
312 gajim
.connections
[account
].send_file_request(file_props
)
315 def _start_receive(self
, file_path
, account
, contact
, file_props
):
316 file_dir
= os
.path
.dirname(file_path
)
318 gajim
.config
.set('last_save_dir', file_dir
)
319 file_props
['file-name'] = file_path
320 self
.add_transfer(account
, contact
, file_props
)
321 gajim
.connections
[account
].send_file_approval(file_props
)
323 def show_file_request(self
, account
, contact
, file_props
):
325 Show dialog asking for comfirmation and store location of new file
326 requested by a contact
328 if file_props
is None or 'name' not in file_props
:
330 sec_text
= '\t' + _('File: %s') % gobject
.markup_escape_text(
332 if 'size' in file_props
:
333 sec_text
+= '\n\t' + _('Size: %s') % \
334 helpers
.convert_bytes(file_props
['size'])
335 if 'mime-type' in file_props
:
336 sec_text
+= '\n\t' + _('Type: %s') % file_props
['mime-type']
337 if 'desc' in file_props
:
338 sec_text
+= '\n\t' + _('Description: %s') % file_props
['desc']
339 prim_text
= _('%s wants to send you a file:') % contact
.jid
342 def on_response_ok(account
, contact
, file_props
):
344 def on_ok(widget
, account
, contact
, file_props
):
345 file_path
= dialog2
.get_filename()
346 file_path
= gtkgui_helpers
.decode_filechooser_file_paths(
348 if os
.path
.exists(file_path
):
349 # check if we have write permissions
350 if not os
.access(file_path
, os
.W_OK
):
351 file_name
= os
.path
.basename(file_path
)
352 dialogs
.ErrorDialog(_('Cannot overwrite existing file "%s"' % file_name
),
353 _('A file with this name already exists and you do not have permission to overwrite it.'))
355 stat
= os
.stat(file_path
)
356 dl_size
= stat
.st_size
357 file_size
= file_props
['size']
358 dl_finished
= dl_size
>= file_size
360 def on_response(response
):
363 elif response
== 100:
364 file_props
['offset'] = dl_size
366 self
._start
_receive
(file_path
, account
, contact
, file_props
)
368 dialog
= dialogs
.FTOverwriteConfirmationDialog(
369 _('This file already exists'), _('What do you want to do?'),
370 propose_resume
=not dl_finished
, on_response
=on_response
)
371 dialog
.set_transient_for(dialog2
)
372 dialog
.set_destroy_with_parent(True)
375 dirname
= os
.path
.dirname(file_path
)
376 if not os
.access(dirname
, os
.W_OK
) and os
.name
!= 'nt':
377 # read-only bit is used to mark special folder under windows,
378 # not to mark that a folder is read-only. See ticket #3587
379 dialogs
.ErrorDialog(_('Directory "%s" is not writable') % dirname
, _('You do not have permission to create files in this directory.'))
382 self
._start
_receive
(file_path
, account
, contact
, file_props
)
384 def on_cancel(widget
, account
, contact
, file_props
):
386 gajim
.connections
[account
].send_file_rejection(file_props
)
388 dialog2
= dialogs
.FileChooserDialog(
389 title_text
=_('Save File as...'),
390 action
=gtk
.FILE_CHOOSER_ACTION_SAVE
,
391 buttons
=(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
,
392 gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
),
393 default_response
=gtk
.RESPONSE_OK
,
394 current_folder
=gajim
.config
.get('last_save_dir'),
395 on_response_ok
=(on_ok
, account
, contact
, file_props
),
396 on_response_cancel
=(on_cancel
, account
, contact
, file_props
))
398 dialog2
.set_current_name(file_props
['name'])
399 dialog2
.connect('delete-event', lambda widget
, event
:
400 on_cancel(widget
, account
, contact
, file_props
))
402 def on_response_cancel(account
, file_props
):
403 gajim
.connections
[account
].send_file_rejection(file_props
)
405 dialog
= dialogs
.NonModalConfirmationDialog(prim_text
, sec_text
,
406 on_response_ok
=(on_response_ok
, account
, contact
, file_props
),
407 on_response_cancel
=(on_response_cancel
, account
, file_props
))
408 dialog
.connect('delete-event', lambda widget
, event
:
409 on_response_cancel(account
, file_props
))
412 def get_icon(self
, ident
):
413 return self
.images
.setdefault(ident
,
414 self
.window
.render_icon(self
.icons
[ident
], gtk
.ICON_SIZE_MENU
))
416 def set_status(self
, typ
, sid
, status
):
418 Change the status of a transfer to state 'status'
420 iter_
= self
.get_iter_by_sid(typ
, sid
)
423 sid
= self
.model
[iter_
][C_SID
].decode('utf-8')
424 file_props
= self
.files_props
[sid
[0]][sid
[1:]]
426 file_props
['stopped'] = True
428 file_props
['completed'] = True
429 self
.model
.set(iter_
, C_IMAGE
, self
.get_icon(status
))
430 path
= self
.model
.get_path(iter_
)
431 self
.select_func(path
)
433 def _format_percent(self
, percent
):
435 Add extra spaces from both sides of the percent, so that progress string
436 has always a fixed size
443 _str
+= unicode(percent
) + '% \n'
446 def _format_time(self
, _time
):
447 times
= { 'hours': 0, 'minutes': 0, 'seconds': 0 }
449 times
['seconds'] = _time
% 60
452 times
['minutes'] = _time
% 60
454 times
['hours'] = _time
/ 60
456 #Print remaining time in format 00:00:00
457 #You can change the places of (hours), (minutes), (seconds) -
458 #they are not translatable.
459 return _('%(hours)02.d:%(minutes)02.d:%(seconds)02.d') % times
461 def _get_eta_and_speed(self
, full_size
, transfered_size
, file_props
):
462 if len(file_props
['transfered_size']) == 0:
464 elif len(file_props
['transfered_size']) == 1:
465 speed
= round(float(transfered_size
) / file_props
['elapsed-time'])
467 # first and last are (time, transfered_size)
468 first
= file_props
['transfered_size'][0]
469 last
= file_props
['transfered_size'][-1]
470 transfered
= last
[1] - first
[1]
471 tim
= last
[0] - first
[0]
474 speed
= round(float(transfered
) / tim
)
477 remaining_size
= full_size
- transfered_size
478 eta
= remaining_size
/ speed
481 def _remove_transfer(self
, iter_
, sid
, file_props
):
482 self
.model
.remove(iter_
)
483 if 'tt_account' in file_props
:
484 # file transfer is set
485 account
= file_props
['tt_account']
486 if account
in gajim
.connections
:
487 # there is a connection to the account
488 gajim
.connections
[account
].remove_transfer(file_props
)
489 if file_props
['type'] == 'r': # we receive a file
490 other
= file_props
['sender']
491 else: # we send a file
492 other
= file_props
['receiver']
493 if isinstance(other
, unicode):
494 jid
= gajim
.get_jid_without_resource(other
)
495 else: # It's a Contact instance
497 for ev_type
in ('file-error', 'file-completed', 'file-request-error',
498 'file-send-error', 'file-stopped'):
499 for event
in gajim
.events
.get_events(account
, jid
, [ev_type
]):
500 if event
.parameters
['sid'] == file_props
['sid']:
501 gajim
.events
.remove_events(account
, jid
, event
)
502 gajim
.interface
.roster
.draw_contact(jid
, account
)
503 gajim
.interface
.roster
.show_title()
504 del(self
.files_props
[sid
[0]][sid
[1:]])
507 def set_progress(self
, typ
, sid
, transfered_size
, iter_
=None):
509 Change the progress of a transfer with new transfered size
511 if sid
not in self
.files_props
[typ
]:
513 file_props
= self
.files_props
[typ
][sid
]
514 full_size
= int(file_props
['size'])
518 percent
= round(float(transfered_size
) / full_size
* 100, 1)
520 iter_
= self
.get_iter_by_sid(typ
, sid
)
521 if iter_
is not None:
523 if self
.model
[iter_
][C_PERCENT
] == 0 and int(percent
> 0):
525 text
= self
._format
_percent
(percent
)
526 if transfered_size
== 0:
529 text
+= helpers
.convert_bytes(transfered_size
)
530 text
+= '/' + helpers
.convert_bytes(full_size
)
534 if 'offset' in file_props
and file_props
['offset']:
535 transfered_size
-= file_props
['offset']
536 full_size
-= file_props
['offset']
538 if file_props
['elapsed-time'] > 0:
539 file_props
['transfered_size'].append((file_props
['last-time'], transfered_size
))
540 if len(file_props
['transfered_size']) > 6:
541 file_props
['transfered_size'].pop(0)
542 eta
, speed
= self
._get
_eta
_and
_speed
(full_size
, transfered_size
,
545 self
.model
.set(iter_
, C_PROGRESS
, text
)
546 self
.model
.set(iter_
, C_PERCENT
, int(percent
))
547 text
= self
._format
_time
(eta
)
549 #This should make the string Kb/s,
550 #where 'Kb' part is taken from %s.
551 #Only the 's' after / (which means second) should be translated.
552 text
+= _('(%(filesize_unit)s/s)') % {'filesize_unit':
553 helpers
.convert_bytes(speed
)}
554 self
.model
.set(iter_
, C_TIME
, text
)
556 # try to guess what should be the status image
557 if file_props
['type'] == 'r':
561 if 'paused' in file_props
and file_props
['paused'] == True:
563 elif 'stalled' in file_props
and file_props
['stalled'] == True:
565 if 'connected' in file_props
and file_props
['connected'] == False:
567 self
.model
.set(iter_
, 0, self
.get_icon(status
))
568 if transfered_size
== full_size
:
569 self
.set_status(typ
, sid
, 'ok')
571 path
= self
.model
.get_path(iter_
)
572 self
.select_func(path
)
574 def get_iter_by_sid(self
, typ
, sid
):
576 Return iter to the row, which holds file transfer, identified by the
579 iter_
= self
.model
.get_iter_root()
581 if typ
+ sid
== self
.model
[iter_
][C_SID
].decode('utf-8'):
583 iter_
= self
.model
.iter_next(iter_
)
585 def get_send_file_props(self
, account
, contact
, file_path
, file_name
,
588 Create new file_props dict and set initial file transfer properties in it
590 file_props
= {'file-name' : file_path
, 'name' : file_name
,
591 'type' : 's', 'desc' : file_desc
}
592 if os
.path
.isfile(file_path
):
593 stat
= os
.stat(file_path
)
595 dialogs
.ErrorDialog(_('Invalid File'), _('File: ') + file_path
)
598 dialogs
.ErrorDialog(_('Invalid File'),
599 _('It is not possible to send empty files'))
601 file_props
['elapsed-time'] = 0
602 file_props
['size'] = unicode(stat
[6])
603 file_props
['sid'] = helpers
.get_random_string_16()
604 file_props
['completed'] = False
605 file_props
['started'] = False
606 file_props
['sender'] = account
607 file_props
['receiver'] = contact
608 file_props
['tt_account'] = account
609 # keep the last time: transfered_size to compute transfer speed
610 file_props
['transfered_size'] = []
613 def add_transfer(self
, account
, contact
, file_props
):
615 Add new transfer to FT window and show the FT window
617 self
.on_transfers_list_leave_notify_event(None)
618 if file_props
is None:
620 file_props
['elapsed-time'] = 0
621 self
.files_props
[file_props
['type']][file_props
['sid']] = file_props
622 iter_
= self
.model
.prepend()
623 text_labels
= '<b>' + _('Name: ') + '</b>\n'
624 if file_props
['type'] == 'r':
625 text_labels
+= '<b>' + _('Sender: ') + '</b>'
627 text_labels
+= '<b>' + _('Recipient: ') + '</b>'
629 if file_props
['type'] == 'r':
630 file_name
= os
.path
.split(file_props
['file-name'])[1]
632 file_name
= file_props
['name']
633 text_props
= gobject
.markup_escape_text(file_name
) + '\n'
634 text_props
+= contact
.get_shown_name()
635 self
.model
.set(iter_
, 1, text_labels
, 2, text_props
, C_SID
,
636 file_props
['type'] + file_props
['sid'])
637 self
.set_progress(file_props
['type'], file_props
['sid'], 0, iter_
)
638 if 'started' in file_props
and file_props
['started'] is False:
640 elif file_props
['type'] == 'r':
644 file_props
['tt_account'] = account
645 self
.set_status(file_props
['type'], file_props
['sid'], status
)
646 self
.set_cleanup_sensitivity()
647 self
.window
.show_all()
649 def on_transfers_list_motion_notify_event(self
, widget
, event
):
650 pointer
= self
.tree
.get_pointer()
651 props
= widget
.get_path_at_pos(int(event
.x
), int(event
.y
))
652 self
.height_diff
= pointer
[1] - int(event
.y
)
653 if self
.tooltip
.timeout
> 0:
654 if not props
or self
.tooltip
.id != props
[0]:
655 self
.tooltip
.hide_tooltip()
660 iter_
= self
.model
.get_iter(row
)
662 self
.tooltip
.hide_tooltip()
664 sid
= self
.model
[iter_
][C_SID
].decode('utf-8')
665 file_props
= self
.files_props
[sid
[0]][sid
[1:]]
666 if file_props
is not None:
667 if self
.tooltip
.timeout
== 0 or self
.tooltip
.id != props
[0]:
668 self
.tooltip
.id = row
669 self
.tooltip
.timeout
= gobject
.timeout_add(500,
670 self
.show_tooltip
, widget
)
672 def on_transfers_list_leave_notify_event(self
, widget
=None, event
=None):
673 if event
is not None:
674 self
.height_diff
= int(event
.y
)
675 elif self
.height_diff
is 0:
677 pointer
= self
.tree
.get_pointer()
678 props
= self
.tree
.get_path_at_pos(pointer
[0], pointer
[1] - self
.height_diff
)
679 if self
.tooltip
.timeout
> 0:
680 if not props
or self
.tooltip
.id == props
[0]:
681 self
.tooltip
.hide_tooltip()
683 def on_transfers_list_row_activated(self
, widget
, path
, col
):
684 # try to open the containing folder
685 self
.on_open_folder_menuitem_activate(widget
)
687 def set_cleanup_sensitivity(self
):
689 Check if there are transfer rows and set cleanup_button sensitive, or
690 insensitive if model is empty
692 if len(self
.model
) == 0:
693 self
.cleanup_button
.set_sensitive(False)
695 self
.cleanup_button
.set_sensitive(True)
697 def set_all_insensitive(self
):
699 Make all buttons/menuitems insensitive
701 self
.pause_button
.set_sensitive(False)
702 self
.pause_menuitem
.set_sensitive(False)
703 self
.continue_menuitem
.set_sensitive(False)
704 self
.remove_menuitem
.set_sensitive(False)
705 self
.cancel_button
.set_sensitive(False)
706 self
.cancel_menuitem
.set_sensitive(False)
707 self
.open_folder_menuitem
.set_sensitive(False)
708 self
.set_cleanup_sensitivity()
710 def set_buttons_sensitive(self
, path
, is_row_selected
):
712 Make buttons/menuitems sensitive as appropriate to the state of file
713 transfer located at path 'path'
716 self
.set_all_insensitive()
718 current_iter
= self
.model
.get_iter(path
)
719 sid
= self
.model
[current_iter
][C_SID
].decode('utf-8')
720 file_props
= self
.files_props
[sid
[0]][sid
[1:]]
721 self
.remove_menuitem
.set_sensitive(is_row_selected
)
722 self
.open_folder_menuitem
.set_sensitive(is_row_selected
)
724 if is_transfer_stopped(file_props
):
726 self
.cancel_button
.set_sensitive(not is_stopped
)
727 self
.cancel_menuitem
.set_sensitive(not is_stopped
)
728 if not is_row_selected
:
729 # no selection, disable the buttons
730 self
.set_all_insensitive()
732 if is_transfer_active(file_props
):
733 # file transfer is active
734 self
.toggle_pause_continue(True)
735 self
.pause_button
.set_sensitive(True)
736 elif is_transfer_paused(file_props
):
737 # file transfer is paused
738 self
.toggle_pause_continue(False)
739 self
.pause_button
.set_sensitive(True)
741 self
.pause_button
.set_sensitive(False)
742 self
.pause_menuitem
.set_sensitive(False)
743 self
.continue_menuitem
.set_sensitive(False)
745 self
.pause_button
.set_sensitive(False)
746 self
.pause_menuitem
.set_sensitive(False)
747 self
.continue_menuitem
.set_sensitive(False)
750 def selection_changed(self
, args
):
752 Selection has changed - change the sensitivity of the buttons/menuitems
755 selected
= selection
.get_selected_rows()
756 if selected
[1] != []:
757 selected_path
= selected
[1][0]
758 self
.select_func(selected_path
)
760 self
.set_all_insensitive()
762 def select_func(self
, path
):
764 selected
= self
.tree
.get_selection().get_selected_rows()
765 if selected
[1] != []:
766 selected_path
= selected
[1][0]
767 if selected_path
== path
:
769 self
.set_buttons_sensitive(path
, is_selected
)
770 self
.set_cleanup_sensitivity()
773 def on_cleanup_button_clicked(self
, widget
):
774 i
= len(self
.model
) - 1
776 iter_
= self
.model
.get_iter((i
))
777 sid
= self
.model
[iter_
][C_SID
].decode('utf-8')
778 file_props
= self
.files_props
[sid
[0]][sid
[1:]]
779 if is_transfer_stopped(file_props
):
780 self
._remove
_transfer
(iter_
, sid
, file_props
)
782 self
.tree
.get_selection().unselect_all()
783 self
.set_all_insensitive()
785 def toggle_pause_continue(self
, status
):
788 self
.pause_button
.set_label(label
)
789 self
.pause_button
.set_image(gtk
.image_new_from_stock(
790 gtk
.STOCK_MEDIA_PAUSE
, gtk
.ICON_SIZE_MENU
))
792 self
.pause_menuitem
.set_sensitive(True)
793 self
.pause_menuitem
.set_no_show_all(False)
794 self
.continue_menuitem
.hide()
795 self
.continue_menuitem
.set_no_show_all(True)
798 label
= _('_Continue')
799 self
.pause_button
.set_label(label
)
800 self
.pause_button
.set_image(gtk
.image_new_from_stock(
801 gtk
.STOCK_MEDIA_PLAY
, gtk
.ICON_SIZE_MENU
))
802 self
.pause_menuitem
.hide()
803 self
.pause_menuitem
.set_no_show_all(True)
804 self
.continue_menuitem
.set_sensitive(True)
805 self
.continue_menuitem
.set_no_show_all(False)
807 def on_pause_restore_button_clicked(self
, widget
):
808 selected
= self
.tree
.get_selection().get_selected()
809 if selected
is None or selected
[1] is None:
812 sid
= self
.model
[s_iter
][C_SID
].decode('utf-8')
813 file_props
= self
.files_props
[sid
[0]][sid
[1:]]
814 if is_transfer_paused(file_props
):
815 file_props
['last-time'] = time
.time()
816 file_props
['paused'] = False
817 types
= {'r' : 'download', 's' : 'upload'}
818 self
.set_status(file_props
['type'], file_props
['sid'], types
[sid
[0]])
819 self
.toggle_pause_continue(True)
820 if file_props
['continue_cb']:
821 file_props
['continue_cb']()
822 elif is_transfer_active(file_props
):
823 file_props
['paused'] = True
824 self
.set_status(file_props
['type'], file_props
['sid'], 'pause')
825 # reset that to compute speed only when we resume
826 file_props
['transfered_size'] = []
827 self
.toggle_pause_continue(False)
829 def on_cancel_button_clicked(self
, widget
):
830 selected
= self
.tree
.get_selection().get_selected()
831 if selected
is None or selected
[1] is None:
834 sid
= self
.model
[s_iter
][C_SID
].decode('utf-8')
835 file_props
= self
.files_props
[sid
[0]][sid
[1:]]
836 if 'tt_account' not in file_props
:
838 account
= file_props
['tt_account']
839 if account
not in gajim
.connections
:
841 gajim
.connections
[account
].disconnect_transfer(file_props
)
842 self
.set_status(file_props
['type'], file_props
['sid'], 'stop')
844 def show_tooltip(self
, widget
):
845 if self
.height_diff
== 0:
846 self
.tooltip
.hide_tooltip()
848 pointer
= self
.tree
.get_pointer()
849 props
= self
.tree
.get_path_at_pos(pointer
[0],
850 pointer
[1] - self
.height_diff
)
851 # check if the current pointer is at the same path
852 # as it was before setting the timeout
853 if props
and self
.tooltip
.id == props
[0]:
854 iter_
= self
.model
.get_iter(props
[0])
855 sid
= self
.model
[iter_
][C_SID
].decode('utf-8')
856 file_props
= self
.files_props
[sid
[0]][sid
[1:]]
857 # bounding rectangle of coordinates for the cell within the treeview
858 rect
= self
.tree
.get_cell_area(props
[0], props
[1])
859 # position of the treeview on the screen
860 position
= widget
.window
.get_origin()
861 self
.tooltip
.show_tooltip(file_props
, rect
.height
,
862 position
[1] + rect
.y
+ self
.height_diff
)
864 self
.tooltip
.hide_tooltip()
866 def on_notify_ft_complete_checkbox_toggled(self
, widget
):
867 gajim
.config
.set('notify_on_file_complete',
870 def on_file_transfers_dialog_delete_event(self
, widget
, event
):
871 self
.on_transfers_list_leave_notify_event(widget
, None)
873 return True # do NOT destory window
875 def on_close_button_clicked(self
, widget
):
878 def show_context_menu(self
, event
, iter_
):
879 # change the sensitive propery of the buttons and menuitems
881 path
= self
.model
.get_path(iter_
)
882 self
.set_buttons_sensitive(path
, True)
884 event_button
= gtkgui_helpers
.get_possible_button_event(event
)
885 self
.file_transfers_menu
.show_all()
886 self
.file_transfers_menu
.popup(None, self
.tree
, None,
887 event_button
, event
.time
)
889 def on_transfers_list_key_press_event(self
, widget
, event
):
891 When a key is pressed in the treeviews
893 self
.tooltip
.hide_tooltip()
896 iter_
= self
.tree
.get_selection().get_selected()[1]
898 self
.tree
.get_selection().unselect_all()
900 if iter_
is not None:
901 path
= self
.model
.get_path(iter_
)
902 self
.tree
.get_selection().select_path(path
)
904 if event
.keyval
== gtk
.keysyms
.Menu
:
905 self
.show_context_menu(event
, iter_
)
909 def on_transfers_list_button_release_event(self
, widget
, event
):
910 # hide tooltip, no matter the button is pressed
911 self
.tooltip
.hide_tooltip()
914 path
= self
.tree
.get_path_at_pos(int(event
.x
), int(event
.y
))[0]
916 self
.tree
.get_selection().unselect_all()
918 self
.set_all_insensitive()
920 self
.select_func(path
)
922 def on_transfers_list_button_press_event(self
, widget
, event
):
923 # hide tooltip, no matter the button is pressed
924 self
.tooltip
.hide_tooltip()
925 path
, iter_
= None, None
927 path
= self
.tree
.get_path_at_pos(int(event
.x
), int(event
.y
))[0]
929 self
.tree
.get_selection().unselect_all()
930 if event
.button
== 3: # Right click
932 self
.tree
.get_selection().select_path(path
)
933 iter_
= self
.model
.get_iter(path
)
934 self
.show_context_menu(event
, iter_
)
938 def on_open_folder_menuitem_activate(self
, widget
):
939 selected
= self
.tree
.get_selection().get_selected()
940 if not selected
or not selected
[1]:
943 sid
= self
.model
[s_iter
][C_SID
].decode('utf-8')
944 file_props
= self
.files_props
[sid
[0]][sid
[1:]]
945 if 'file-name' not in file_props
:
947 path
= os
.path
.split(file_props
['file-name'])[0]
948 if os
.path
.exists(path
) and os
.path
.isdir(path
):
949 helpers
.launch_file_manager(path
)
951 def on_cancel_menuitem_activate(self
, widget
):
952 self
.on_cancel_button_clicked(widget
)
954 def on_continue_menuitem_activate(self
, widget
):
955 self
.on_pause_restore_button_clicked(widget
)
957 def on_pause_menuitem_activate(self
, widget
):
958 self
.on_pause_restore_button_clicked(widget
)
960 def on_remove_menuitem_activate(self
, widget
):
961 selected
= self
.tree
.get_selection().get_selected()
962 if not selected
or not selected
[1]:
965 sid
= self
.model
[s_iter
][C_SID
].decode('utf-8')
966 file_props
= self
.files_props
[sid
[0]][sid
[1:]]
967 self
._remove
_transfer
(s_iter
, sid
, file_props
)
968 self
.set_all_insensitive()
970 def on_file_transfers_window_key_press_event(self
, widget
, event
):
971 if event
.keyval
== gtk
.keysyms
.Escape
: # ESCAPE