root/branches/gajim_0.9/src/filetransfers_window.py

Revision 4700, 31.8 kB (checked in by asterix, 3 years ago)

change copyright from "Gajim Team" to real people

  • Property svn:eol-style set to LF
Line 
1##      filetransfers_window.py
2##
3## Contributors for this file:
4##      - Yann Le Boulanger <asterix@lagaule.org>
5##      - Nikos Kouremenos <kourem@gmail.com>
6##      - Dimitur Kirov <dkirov@gmail.com>
7##
8## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
9##                         Vincent Hanquez <tab@snarc.org>
10## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
11##                    Vincent Hanquez <tab@snarc.org>
12##                    Nikos Kouremenos <nkour@jabber.org>
13##                    Dimitur Kirov <dkirov@gmail.com>
14##                    Travis Shirk <travis@pobox.com>
15##                    Norman Rasmussen <norman@rasmussen.co.za>
16##
17## This program is free software; you can redistribute it and/or modify
18## it under the terms of the GNU General Public License as published
19## by the Free Software Foundation; version 2 only.
20##
21## This program is distributed in the hope that it will be useful,
22## but WITHOUT ANY WARRANTY; without even the implied warranty of
23## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24## GNU General Public License for more details.
25##
26
27import gtk
28import gtk.glade
29import gobject
30import pango
31import os
32import sys
33import time
34
35import gtkgui_helpers
36import tooltips
37import dialogs
38
39from common import gajim
40from common import helpers
41from common import i18n
42
43_ = i18n._
44APP = i18n.APP
45gtk.glade.bindtextdomain (APP, i18n.DIR)
46gtk.glade.textdomain (APP)
47
48GTKGUI_GLADE = 'gtkgui.glade'
49
50C_IMAGE = 0
51C_LABELS = 1
52C_FILE = 2
53C_TIME = 3
54C_PROGRESS = 4
55C_PERCENT = 5
56C_SID = 6
57
58
59class FileTransfersWindow:
60        def __init__(self):
61                self.files_props = {'r' : {}, 's': {}}
62                self.height_diff = 0
63                self.xml = gtk.glade.XML(GTKGUI_GLADE, 'file_transfers_window', APP)
64                self.window = self.xml.get_widget('file_transfers_window')
65                self.tree = self.xml.get_widget('transfers_list')
66                self.cancel_button = self.xml.get_widget('cancel_button')
67                self.pause_button = self.xml.get_widget('pause_restore_button')
68                self.cleanup_button = self.xml.get_widget('cleanup_button')
69                self.notify_ft_checkbox = self.xml.get_widget(
70                        'notify_ft_complete_checkbox')
71                notify = gajim.config.get('notify_on_file_complete')
72                if notify:
73                        self.notify_ft_checkbox.set_active(True)
74                else:
75                        self.notify_ft_checkbox.set_active(False)
76                self.model = gtk.ListStore(gtk.gdk.Pixbuf, str, str, str, str, int, str)
77                self.tree.set_model(self.model)
78                col = gtk.TreeViewColumn()
79               
80                render_pixbuf = gtk.CellRendererPixbuf()
81               
82                col.pack_start(render_pixbuf, expand = True)
83                render_pixbuf.set_property('xpad', 3)
84                render_pixbuf.set_property('ypad', 3)
85                render_pixbuf.set_property('yalign', .0)
86                col.add_attribute(render_pixbuf, 'pixbuf', 0)
87                self.tree.append_column(col)
88               
89                col = gtk.TreeViewColumn(_('File'))
90                renderer = gtk.CellRendererText()
91                col.pack_start(renderer, expand=False)
92                col.add_attribute(renderer, 'markup' , C_LABELS)
93                renderer.set_property('yalign', 0.)
94                renderer = gtk.CellRendererText()
95                col.pack_start(renderer, expand=True)
96                col.add_attribute(renderer, 'markup' , C_FILE)
97                renderer.set_property('xalign', 0.)
98                renderer.set_property('yalign', 0.)
99                renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
100                col.set_resizable(True)
101                col.set_expand(True)
102                self.tree.append_column(col)
103               
104                col = gtk.TreeViewColumn(_('Time'))
105                renderer = gtk.CellRendererText()
106                col.pack_start(renderer, expand=False)
107                col.add_attribute(renderer, 'markup' , C_TIME)
108                renderer.set_property('yalign', 0.5)
109                renderer.set_property('xalign', 0.5)
110                renderer = gtk.CellRendererText()
111                renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
112                col.set_resizable(True)
113                col.set_expand(False)
114                self.tree.append_column(col)
115               
116                col = gtk.TreeViewColumn(_('Progress'))
117                renderer = gtk.CellRendererProgress()
118                renderer.set_property('yalign', 0.5)
119                renderer.set_property('xalign', 0.5)
120                col.pack_start(renderer, expand = False)
121                col.add_attribute(renderer, 'text' , C_PROGRESS)
122                col.add_attribute(renderer, 'value' , C_PERCENT)
123                col.set_resizable(True)
124                col.set_expand(False)
125                self.tree.append_column(col)
126               
127                self.set_images()
128                self.tree.get_selection().set_mode(gtk.SELECTION_SINGLE)
129                self.tree.get_selection().connect('changed', self.selection_changed)
130                self.tooltip = tooltips.FileTransfersTooltip()
131                self.xml.signal_autoconnect(self)
132                popup_xml = gtk.glade.XML(GTKGUI_GLADE, 'file_transfers_menu',
133                        APP)
134                self.file_transfers_menu = popup_xml.get_widget('file_transfers_menu')
135                self.open_folder_menuitem = popup_xml.get_widget('open_folder_menuitem')
136                self.cancel_menuitem = popup_xml.get_widget('cancel_menuitem')
137                self.pause_menuitem = popup_xml.get_widget('pause_menuitem')
138                self.continue_menuitem = popup_xml.get_widget('continue_menuitem')
139                self.continue_menuitem.hide()
140                self.continue_menuitem.set_no_show_all(True)
141                self.remove_menuitem = popup_xml.get_widget('remove_menuitem')
142                popup_xml.signal_autoconnect(self)
143               
144        def find_transfer_by_jid(self, account, jid):
145                ''' find all transfers with peer 'jid' that belong to 'account' '''
146                active_transfers = [[],[]] # ['senders', 'receivers']
147               
148                # 'account' is the sender
149                for file_props in self.files_props['s'].values():
150                        if file_props['tt_account'] == account:
151                                receiver_jid = unicode(file_props['receiver']).split('/')[0]
152                                if jid == receiver_jid:
153                                        if not self.is_transfer_stoped(file_props):
154                                                active_transfers[0].append(file_props)
155               
156                # 'account' is the recipient
157                for file_props in self.files_props['r'].values():
158                        if file_props['tt_account'] == account:
159                                sender_jid = unicode(file_props['sender']).split('/')[0]
160                                if jid == sender_jid:
161                                        if not self.is_transfer_stoped(file_props):
162                                                active_transfers[1].append(file_props)
163                return active_transfers
164       
165        def show_completed(self, jid, file_props):
166                ''' show a dialog saying that file (file_props) has been transferred'''
167                self.window.present()
168                self.window.window.focus()
169                if file_props['type'] == 'r':
170                        # file path is used below in 'Save in'
171                        (file_path, file_name) = os.path.split(file_props['file-name'])
172                else:
173                        file_name = file_props['name']
174                sectext = '\t' + _('Filename: %s') % \
175                        gtkgui_helpers.escape_for_pango_markup(file_name)
176                sectext += '\n\t' + _('Size: %s') % \
177                helpers.convert_bytes(file_props['size'])
178                if file_props['type'] == 'r':
179                        jid = unicode(file_props['sender']).split('/')[0]
180                        sender_name = gajim.get_first_contact_instance_from_jid( 
181                                file_props['tt_account'], jid).name
182                        sender = gtkgui_helpers.escape_for_pango_markup(sender_name)
183                else:
184                        #You is a reply of who sent a file
185                        sender = _('You')
186                sectext += '\n\t' +_('Sender: %s') % sender
187                sectext += '\n\t' +_('Recipient: ')
188                if file_props['type'] == 's':
189                        jid = unicode(file_props['receiver']).split('/')[0]
190                        receiver_name = gajim.get_first_contact_instance_from_jid( 
191                                file_props['tt_account'], jid).name
192                        recipient = gtkgui_helpers.escape_for_pango_markup(receiver_name)
193                else:
194                        #You is a reply of who received a file
195                        recipient = _('You')
196                sectext += recipient
197                if file_props['type'] == 'r':
198                        sectext += '\n\t' +_('Saved in: %s') % \
199                                gtkgui_helpers.escape_for_pango_markup(file_path)
200                dialog = dialogs.HigDialog(None, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE, 
201                                _('File transfer completed'), sectext)
202                if file_props['type'] == 'r':
203                        dialog.add_buttons(_('_Open Containing Folder'), gtk.RESPONSE_ACCEPT)
204                dialog.add_buttons(gtk.STOCK_OK, gtk.RESPONSE_OK)
205                dialog.show_all()
206                response = dialog.run()
207                dialog.destroy()
208                if response == gtk.RESPONSE_ACCEPT:
209                        if not file_props.has_key('file-name'):
210                                return
211                        (path, file) = os.path.split(file_props['file-name'])
212                        if os.path.exists(path) and os.path.isdir(path):
213                                helpers.launch_file_manager(path)
214                        self.tree.get_selection().unselect_all()
215               
216        def show_request_error(self, file_props):
217                ''' show error dialog to the recipient saying that transfer
218                has been canceled'''
219                self.window.present()
220                self.window.window.focus()
221                dialogs.InformationDialog(_('File transfer canceled'), _('Connection with peer cannot be established.'))
222                self.tree.get_selection().unselect_all()
223               
224        def show_send_error(self, file_props):
225                ''' show error dialog to the sender saying that transfer
226                has been canceled'''
227                self.window.present()
228                self.window.window.focus()
229                dialogs.InformationDialog(_('File transfer canceled'),
230_('Connection with peer cannot be established.'))
231                self.tree.get_selection().unselect_all()
232       
233        def show_stopped(self, jid, file_props):
234                self.window.present()
235                self.window.window.focus()
236                if file_props['type'] == 'r':
237                        file_name = os.path.basename(file_props['file-name'])
238                else:
239                        file_name = file_props['name']
240                sectext = '\t' + _('Filename: %s') % \
241                        gtkgui_helpers.escape_for_pango_markup(file_name)
242                sectext += '\n\t' + _('Sender: %s') % \
243                        gtkgui_helpers.escape_for_pango_markup(jid)
244                dialogs.ErrorDialog(_('File transfer stopped by the contact of the other side'), \
245                        sectext).get_response()
246                self.tree.get_selection().unselect_all()
247               
248        def show_file_send_request(self, account, contact):
249                last_send_dir = gajim.config.get('last_send_dir')
250                dialog = gtk.FileChooserDialog(title=_('Choose File to Send...'), 
251                        action=gtk.FILE_CHOOSER_ACTION_OPEN, 
252                        buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
253                butt = dialog.add_button(_('Send'), gtk.RESPONSE_OK)
254                butt.set_use_stock(True)
255                dialog.set_default_response(gtk.RESPONSE_OK)
256                dialog.set_select_multiple(True) # we can select many files to send
257                if last_send_dir and os.path.isdir(last_send_dir):
258                        dialog.set_current_folder(last_send_dir)
259                else:
260                        home_dir = os.path.expanduser('~')
261                        dialog.set_current_folder(home_dir)
262                file_props = {}
263                while True:
264                        response = dialog.run()
265                        if response == gtk.RESPONSE_OK:
266                                file_dir = None
267                                files_path_list = dialog.get_filenames()
268                                for file_path in files_path_list:
269                                        file_path = file_path.decode('utf8')
270                                        if self.send_file(account, contact, file_path) and file_dir is None:
271                                                file_dir = os.path.dirname(file_path)
272                                if file_dir:
273                                        gajim.config.set('last_send_dir', file_dir)
274                                        dialog.destroy()
275                                        break
276                        else:
277                                dialog.destroy()
278                                break
279               
280        def send_file(self, account, contact, file_path):
281                ''' start the real transfer(upload) of the file '''
282                if gtkgui_helpers.file_is_locked(file_path):
283                        pritext = _('Gajim cannot access this file')
284                        sextext = _('This file is being used by another process.')
285                        dialogs.ErrorDialog(pritext, sextext).get_response()
286                        return
287               
288                if isinstance(contact, str):
289                        if contact.find('/') == -1:
290                                return
291                        (jid, resource) = contact.split('/', 1)
292                        contact = gajim.Contact(jid = jid, resource = resource)
293                (file_dir, file_name) = os.path.split(file_path)
294                file_props = self.get_send_file_props(account, contact, 
295                                file_path, file_name)
296                if file_props is None:
297                        return False
298                self.add_transfer(account, contact, file_props)
299                gajim.connections[account].send_file_request(file_props)
300                return True
301       
302        def show_file_request(self, account, contact, file_props):
303                ''' show dialog asking for comfirmation and store location of new
304                file requested by a contact'''
305                if file_props is None or not file_props.has_key('name'):
306                        return
307                last_save_dir = gajim.config.get('last_save_dir')
308                sec_text = '\t' + _('File: %s') % file_props['name']
309                if file_props.has_key('size'):
310                        sec_text += '\n\t' + _('Size: %s') % \
311                                helpers.convert_bytes(file_props['size'])
312                if file_props.has_key('mime-type'):
313                        sec_text += '\n\t' + _('Type: %s') % file_props['mime-type']
314                if file_props.has_key('desc'):
315                        sec_text += '\n\t' + _('Description: %s') % file_props['desc']
316                prim_text = _('%s wants to send you a file:') % contact.jid
317                dialog = dialogs.ConfirmationDialog(prim_text, sec_text)
318                if dialog.get_response() == gtk.RESPONSE_OK:
319                        dialog = gtk.FileChooserDialog(title=_('Save File as...'), 
320                                action=gtk.FILE_CHOOSER_ACTION_SAVE, 
321                                buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, 
322                                gtk.STOCK_SAVE, gtk.RESPONSE_OK))
323                        dialog.set_current_name(file_props['name'])
324                        dialog.set_default_response(gtk.RESPONSE_OK)
325                        gtk28 = False
326                        if gtk.gtk_version >= (2, 8, 0) and gtk.pygtk_version >= (2, 8, 0):
327                                dialog.props.do_overwrite_confirmation = True
328                                gtk28 = True
329                        if last_save_dir and os.path.isdir(last_save_dir):
330                                dialog.set_current_folder(last_save_dir)
331                        else:
332                                home_dir = os.path.expanduser('~')
333                                dialog.set_current_folder(home_dir)
334                        while True:
335                                response = dialog.run()
336                                if response == gtk.RESPONSE_OK:
337                                        file_path = dialog.get_filename()
338                                        file_path = file_path.decode('utf-8')
339                                        if not gtk28 and os.path.exists(file_path):
340                                                primtext = _('This file already exists')
341                                                sectext = _('Would you like to overwrite it?')
342                                                dialog2 = dialogs.ConfirmationDialog(primtext, sectext)
343                                                if dialog2.get_response() != gtk.RESPONSE_OK:
344                                                        continue
345                                        file_dir = os.path.dirname(file_path)
346                                        if file_dir:
347                                                gajim.config.set('last_save_dir', file_dir)
348                                        file_props['file-name'] = file_path
349                                        self.add_transfer(account, contact, file_props)
350                                        gajim.connections[account].send_file_approval(file_props)
351                                else:
352                                        gajim.connections[account].send_file_rejection(file_props)
353                                dialog.destroy()
354                                break
355                else:
356                        gajim.connections[account].send_file_rejection(file_props)
357       
358        def set_images(self):
359                ''' create pixbufs for status images in transfer rows'''
360                self.images = {}
361                self.images['upload'] = self.window.render_icon(gtk.STOCK_GO_UP, 
362                        gtk.ICON_SIZE_MENU)
363                self.images['download'] = self.window.render_icon(gtk.STOCK_GO_DOWN, 
364                        gtk.ICON_SIZE_MENU)
365                self.images['stop'] = self.window.render_icon(gtk.STOCK_STOP, 
366                        gtk.ICON_SIZE_MENU)
367                self.images['waiting'] = self.window.render_icon(gtk.STOCK_REFRESH, 
368                        gtk.ICON_SIZE_MENU)
369                self.images['pause'] = self.window.render_icon(gtk.STOCK_MEDIA_PAUSE, 
370                        gtk.ICON_SIZE_MENU)
371                self.images['continue'] = self.window.render_icon(gtk.STOCK_MEDIA_PLAY, 
372                        gtk.ICON_SIZE_MENU)
373                self.images['ok'] = self.window.render_icon(gtk.STOCK_APPLY, 
374                        gtk.ICON_SIZE_MENU)
375                       
376        def set_status(self, typ, sid, status):
377                ''' change the status of a transfer to state 'status' '''
378                iter = self.get_iter_by_sid(typ, sid)
379                if iter is None:
380                        return
381                sid = self.model[iter][C_SID].decode('utf-8')
382                file_props = self.files_props[sid[0]][sid[1:]]
383                if status == 'stop':
384                        file_props['stopped'] = True
385                elif status == 'ok':
386                        file_props['completed'] = True
387                self.model.set(iter, C_IMAGE, self.images[status])
388               
389        def _format_percent(self, percent):
390                ''' add extra spaces from both sides of the percent, so that
391                progress string has always a fixed size'''
392                _str = '          '
393                if percent != 100.:
394                        _str += ' '
395                if percent < 10:
396                        _str += ' '
397                _str += unicode(percent) + '%          \n'
398                return _str
399               
400        def _format_time(self, _time):
401                times = { 'hours': 0, 'minutes': 0, 'seconds': 0 }
402                _time = int(_time)
403                times['seconds'] = _time % 60
404                if _time >= 60:
405                        _time /= 60
406                        times['minutes'] = _time % 60
407                        if _time >= 60:
408                                times['hours'] = _time / 60
409               
410                #Print remaining time in format 00:00:00
411                #You can change the places of (hours), (minutes), (seconds) -
412                #they are not translatable.
413                return _('%(hours)02.d:%(minutes)02.d:%(seconds)02.d')  % times
414               
415        def _get_eta_and_speed(self, full_size, transfered_size, elapsed_time):
416                if elapsed_time == 0:
417                        return 0., 0.
418                speed = round(float(transfered_size) / elapsed_time)
419                if speed == 0.:
420                        return 0., 0.
421                remaining_size = full_size - transfered_size
422                eta = remaining_size / speed
423                return eta, speed
424               
425        def _remove_transfer(self, iter, sid, file_props):
426                self.model.remove(iter)
427                if  file_props.has_key('tt_account'):
428                        # file transfer is set
429                        account = file_props['tt_account']
430                        if gajim.connections.has_key(account):
431                                # there is a connection to the account
432                                gajim.connections[account].remove_transfer(file_props)
433                del(self.files_props[sid[0]][sid[1:]])
434                del(file_props)
435               
436        def set_progress(self, typ, sid, transfered_size, iter = None):
437                ''' change the progress of a transfer with new transfered size'''
438                if not self.files_props[typ].has_key(sid):
439                        return
440                file_props = self.files_props[typ][sid]
441                full_size = int(file_props['size'])
442                if full_size == 0:
443                        percent = 0
444                else:
445                        percent = round(float(transfered_size) / full_size * 100)
446                if iter is None:
447                        iter = self.get_iter_by_sid(typ, sid)
448                if iter is not None:
449                        text = self._format_percent(percent)
450                        if transfered_size == 0:
451                                text += '0'
452                        else:
453                                text += helpers.convert_bytes(transfered_size)
454                        text += '/' + helpers.convert_bytes(full_size)
455                        # Kb/s
456                       
457                        # remaining time
458                        eta, speed = self._get_eta_and_speed(full_size, transfered_size, 
459                                file_props['elapsed-time'])
460                       
461                        self.model.set(iter, C_PROGRESS, text)
462                        self.model.set(iter, C_PERCENT, int(percent))
463                        text = self._format_time(eta)
464                        text += '\n'
465                        #This should make the string Kb/s,
466                        #where 'Kb' part is taken from %s.
467                        #Only the 's' after / (which means second) should be translated.
468                        text += _('(%(filesize_unit)s/s)') % {'filesize_unit':
469                                helpers.convert_bytes(speed)}
470                        self.model.set(iter, C_TIME, text)
471                       
472                        # try to guess what should be the status image
473                        if file_props['type'] == 'r':
474                                status = 'download'
475                        else:
476                                status = 'upload'
477                        if file_props.has_key('paused') and file_props['paused'] == True:
478                                status = 'pause'
479                        elif file_props.has_key('stalled') and file_props['stalled'] == True:
480                                status = 'waiting'
481                        if file_props.has_key('connected') and file_props['connected'] == False:
482                                status = 'stop'
483                        self.model.set(iter, 0, self.images[status])
484                        if percent == 100:
485                                self.set_status(typ, sid, 'ok')
486       
487        def get_iter_by_sid(self, typ, sid):
488                '''returns iter to the row, which holds file transfer, identified by the
489                session id'''
490                iter = self.model.get_iter_root()
491                while iter:
492                        if typ + sid == self.model[iter][C_SID].decode('utf-8'):
493                                return iter
494                        iter = self.model.iter_next(iter)
495       
496        def get_sid(self):
497                ''' create random string of length 16'''
498                rng = range(65, 90)
499                rng.extend(range(48, 57))
500                char_sequence = map(lambda e:chr(e), rng)
501                from random import sample
502                return reduce(lambda e1, e2: e1 + e2, 
503                                sample(char_sequence, 16))
504       
505        def get_send_file_props(self, account, contact, file_path, file_name):
506                ''' create new file_props dict and set initial file transfer
507                properties in it'''
508                file_props = {'file-name' : file_path, 'name' : file_name, 
509                        'type' : 's'}
510                if os.path.isfile(file_path):
511                       
512                        stat = os.stat(file_path)
513                else:
514                        dialogs.ErrorDialog(_('Invalid File'), _('File: ')  + file_path).get_response()
515                        return None
516                if stat[6] == 0:
517                        dialogs.ErrorDialog(_('Invalid File'), 
518                        _('It is not possible to send empty files')).get_response()
519                        return None
520                file_props['elapsed-time'] = 0
521                file_props['size'] = unicode(stat[6])
522                file_props['sid'] = self.get_sid()
523                file_props['completed'] = False
524                file_props['started'] = False
525                file_props['sender'] = account
526                file_props['receiver'] = contact
527                file_props['tt_account'] = account
528                return file_props
529               
530        def add_transfer(self, account, contact, file_props):
531                ''' add new transfer to FT window and show the FT window '''
532                self.on_transfers_list_leave_notify_event(None)
533                if file_props is None:
534                        return
535                file_props['elapsed-time'] = 0
536                self.files_props[file_props['type']][file_props['sid']] = file_props
537                iter = self.model.append()
538                text_labels = '<b>' + _('Name: ') + '</b>\n' 
539                if file_props['type'] == 'r':
540                        text_labels += '<b>' + _('Sender: ') + '</b>' 
541                else:
542                        text_labels += '<b>' + _('Recipient: ') + '</b>' 
543                       
544                if file_props['type']