From df040bb8e51cea95975bec42da0917f1dba232a8 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Mon, 17 Oct 2011 17:12:19 +0200 Subject: [PATCH] add infobar in chat window for file transfer events. Fixes #1205 --- src/chat_control.py | 168 ++++++++++++++++++++++++++++++++++++ src/filetransfers_window.py | 123 +++++++++++++------------- 2 files changed, 233 insertions(+), 58 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 2969ba474..9e79bf7eb 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1592,6 +1592,22 @@ class ChatControl(ChatControlBase): id_ = widget.connect('value_changed', self.on_sound_hscale_value_changed) self.handlers[id_] = widget + self.info_bar = gtk.InfoBar() + content_area = self.info_bar.get_content_area() + self.info_bar_label = gtk.Label() + self.info_bar_label.set_use_markup(True) + self.info_bar_label.set_alignment(0, 0) + content_area.add(self.info_bar_label) + self.info_bar.set_no_show_all(True) + widget = self.xml.get_object('vbox2') + widget.pack_start(self.info_bar, expand=False, padding=5) + widget.reorder_child(self.info_bar, 1) + + # List of waiting infobar messages + self.info_bar_queue = [] + + self.subscribe_events() + if not session: # Don't use previous session if we want to a specific resource # and it's not the same @@ -1661,6 +1677,20 @@ class ChatControl(ChatControlBase): # instance object gajim.plugin_manager.gui_extension_point('chat_control', self) + def subscribe_events(self): + """ + Register listeners to the events class + """ + gajim.events.event_added_subscribe(self.on_event_added) + gajim.events.event_removed_subscribe(self.on_event_removed) + + def unsubscribe_events(self): + """ + Unregister listeners to the events class + """ + gajim.events.event_added_unsubscribe(self.on_event_added) + gajim.events.event_removed_unsubscribe(self.on_event_removed) + def _update_toolbar(self): # Formatting if self.contact.supports(NS_XHTML_IM) and not self.gpg_is_active: @@ -2598,6 +2628,8 @@ class ChatControl(ChatControlBase): gajim.ged.remove_event_handler('caps-received', ged.GUI1, self._nec_caps_received) + self.unsubscribe_events() + # Send 'gone' chatstate self.send_chatstate('gone', self.contact) self.contact.chatstate = None @@ -3109,3 +3141,139 @@ class ChatControl(ChatControlBase): self.print_conversation(' (', 'status', simple=True) self.print_conversation('%s' % (status), 'status', simple=True) self.print_conversation(')', 'status', simple=True) + + def _info_bar_show_message(self): + if self.info_bar.get_visible(): + # A message is already shown + return + if not self.info_bar_queue: + return + markup, buttons, args, type_ = self.info_bar_queue[0] + self.info_bar_label.set_markup(markup) + for button in buttons: + self.info_bar.add_action_widget(button, 0) + self.info_bar.set_message_type(type_) + self.info_bar.set_no_show_all(False) + self.info_bar.show_all() + + def _add_info_bar_message(self, markup, buttons, args, + type_=gtk.MESSAGE_INFO): + self.info_bar_queue.append((markup, buttons, args, type_)) + self._info_bar_show_message() + + def _get_file_props_event(self, file_props, type_): + evs = gajim.events.get_events(self.account, self.contact.jid, [type_]) + for ev in evs: + if ev.parameters == file_props: + return ev + return None + + def _on_accept_file_request(self, widget, file_props): + gajim.interface.instances['file_transfers'].on_file_request_accepted( + self.account, self.contact, file_props) + ev = self._get_file_props_event(file_props, 'file-request') + if ev: + gajim.events.remove_events(self.account, self.contact.jid, event=ev) + + def _on_cancel_file_request(self, widget, file_props): + gajim.connections[self.account].send_file_rejection(file_props) + ev = self._get_file_props_event(file_props, 'file-request') + if ev: + gajim.events.remove_events(self.account, self.contact.jid, event=ev) + + def _got_file_request(self, file_props): + """ + Show an InfoBar on top of control + """ + markup = '' + _('File transfer:') + ' ' + file_props['name'] + if file_props['desc']: + markup += ' (' + file_props['desc'] + ')' + markup += '\n' + _('Size:') + ' ' + helpers.convert_bytes( + file_props['size']) + b1 = gtk.Button(_('_Accept')) + b1.connect('clicked', self._on_accept_file_request, file_props) + b2 = gtk.Button(stock=gtk.STOCK_CANCEL) + b2.connect('clicked', self._on_cancel_file_request, file_props) + self._add_info_bar_message(markup, [b1, b2], file_props, + gtk.MESSAGE_QUESTION) + + def _on_open_ft_folder(self, widget, file_props): + if 'file-name' not in file_props: + return + path = os.path.split(file_props['file-name'])[0] + if os.path.exists(path) and os.path.isdir(path): + helpers.launch_file_manager(path) + ev = self._get_file_props_event(file_props, 'file-completed') + if ev: + gajim.events.remove_events(self.account, self.contact.jid, event=ev) + + def _on_ok(self, widget, file_props, type_): + ev = self._get_file_props_event(file_props, type_) + if ev: + gajim.events.remove_events(self.account, self.contact.jid, event=ev) + + def _got_file_completed(self, file_props): + markup = '' + _('File transfer completed:') + ' ' + \ + file_props['name'] + if file_props['desc']: + markup += ' (' + file_props['desc'] + ')' + b1 = gtk.Button(_('_Open Containing Folder')) + b1.connect('clicked', self._on_open_ft_folder, file_props) + b2 = gtk.Button(stock=gtk.STOCK_OK) + b2.connect('clicked', self._on_ok, file_props, 'file-completed') + self._add_info_bar_message(markup, [b1, b2], file_props) + + def _got_file_error(self, file_props, type_, pri_txt, sec_txt): + markup = '%s: %s' % (pri_txt, sec_txt) + b = gtk.Button(stock=gtk.STOCK_OK) + b.connect('clicked', self._on_ok, file_props, type_) + self._add_info_bar_message(markup, [b], file_props, gtk.MESSAGE_ERROR) + + def on_event_added(self, event): + if event.account != self.account: + return + if event.jid != self.contact.jid: + return + if event.type_ == 'file-request': + self._got_file_request(event.parameters) + elif event.type_ == 'file-completed': + self._got_file_completed(event.parameters) + elif event.type_ in ('file-error', 'file-stopped'): + msg_err = '' + if event.parameters['error'] == -1: + msg_err = _('Remote contact stopped transfer') + elif event.parameters['error'] == -6: + msg_err = _('Error opening file') + self._got_file_error(event.parameters, event.type_, + _('File transfer stopped'), msg_err) + elif event.type_ in ('file-request-error', 'file-send-error'): + self._got_file_error(event.parameters, event.type_, + _('File transfer cancelled'), + _('Connection with peer cannot be established.')) + + def on_event_removed(self, event_list): + """ + Called when one or more events are removed from the event list + """ + for ev in event_list: + if ev.account != self.account: + continue + if ev.jid != self.contact.jid: + continue + if ev.type_ not in ('file-request', 'file-completed', 'file-error', + 'file-stopped', 'file-request-error', 'file-send-error'): + continue + i = 0 + for ib_msg in self.info_bar_queue: + if ib_msg[2] == ev.parameters: + self.info_bar_queue.remove(ib_msg) + if i == 0: + # We are removing the one currently displayed + area = self.info_bar.get_action_area() + for b in area.get_children(): + area.remove(b) + self.info_bar.hide() + # show next one? + gobject.idle_add(self._info_bar_show_message) + break + i += 1 diff --git a/src/filetransfers_window.py b/src/filetransfers_window.py index 899a96cab..c48964925 100644 --- a/src/filetransfers_window.py +++ b/src/filetransfers_window.py @@ -320,6 +320,70 @@ class FileTransfersWindow: self.add_transfer(account, contact, file_props) gajim.connections[account].send_file_approval(file_props) + def on_file_request_accepted(self, account, contact, file_props): + def on_ok(widget, account, contact, file_props): + file_path = dialog2.get_filename() + file_path = gtkgui_helpers.decode_filechooser_file_paths( + (file_path,))[0] + if os.path.exists(file_path): + # check if we have write permissions + if not os.access(file_path, os.W_OK): + file_name = os.path.basename(file_path) + dialogs.ErrorDialog( + _('Cannot overwrite existing file "%s"' % file_name), + _('A file with this name already exists and you do not ' + 'have permission to overwrite it.')) + return + stat = os.stat(file_path) + dl_size = stat.st_size + file_size = file_props['size'] + dl_finished = dl_size >= file_size + + def on_response(response): + if response < 0: + return + elif response == 100: + file_props['offset'] = dl_size + dialog2.destroy() + self._start_receive(file_path, account, contact, file_props) + + dialog = dialogs.FTOverwriteConfirmationDialog( + _('This file already exists'), _('What do you want to do?'), + propose_resume=not dl_finished, on_response=on_response) + dialog.set_transient_for(dialog2) + dialog.set_destroy_with_parent(True) + return + else: + dirname = os.path.dirname(file_path) + if not os.access(dirname, os.W_OK) and os.name != 'nt': + # read-only bit is used to mark special folder under + # windows, not to mark that a folder is read-only. + # See ticket #3587 + dialogs.ErrorDialog(_('Directory "%s" is not writable') % \ + dirname, _('You do not have permission to create files ' + 'in this directory.')) + return + dialog2.destroy() + self._start_receive(file_path, account, contact, file_props) + + def on_cancel(widget, account, contact, file_props): + dialog2.destroy() + gajim.connections[account].send_file_rejection(file_props) + + dialog2 = dialogs.FileChooserDialog( + title_text=_('Save File as...'), + action=gtk.FILE_CHOOSER_ACTION_SAVE, + buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_SAVE, gtk.RESPONSE_OK), + default_response=gtk.RESPONSE_OK, + current_folder=gajim.config.get('last_save_dir'), + on_response_ok=(on_ok, account, contact, file_props), + on_response_cancel=(on_cancel, account, contact, file_props)) + + dialog2.set_current_name(file_props['name']) + dialog2.connect('delete-event', lambda widget, event: + on_cancel(widget, account, contact, file_props)) + def show_file_request(self, account, contact, file_props): """ Show dialog asking for comfirmation and store location of new file @@ -340,64 +404,7 @@ class FileTransfersWindow: dialog = None def on_response_ok(account, contact, file_props): - - def on_ok(widget, account, contact, file_props): - file_path = dialog2.get_filename() - file_path = gtkgui_helpers.decode_filechooser_file_paths( - (file_path,))[0] - if os.path.exists(file_path): - # check if we have write permissions - if not os.access(file_path, os.W_OK): - file_name = os.path.basename(file_path) - dialogs.ErrorDialog(_('Cannot overwrite existing file "%s"' % file_name), - _('A file with this name already exists and you do not have permission to overwrite it.')) - return - stat = os.stat(file_path) - dl_size = stat.st_size - file_size = file_props['size'] - dl_finished = dl_size >= file_size - - def on_response(response): - if response < 0: - return - elif response == 100: - file_props['offset'] = dl_size - dialog2.destroy() - self._start_receive(file_path, account, contact, file_props) - - dialog = dialogs.FTOverwriteConfirmationDialog( - _('This file already exists'), _('What do you want to do?'), - propose_resume=not dl_finished, on_response=on_response) - dialog.set_transient_for(dialog2) - dialog.set_destroy_with_parent(True) - return - else: - dirname = os.path.dirname(file_path) - if not os.access(dirname, os.W_OK) and os.name != 'nt': - # read-only bit is used to mark special folder under windows, - # not to mark that a folder is read-only. See ticket #3587 - dialogs.ErrorDialog(_('Directory "%s" is not writable') % dirname, _('You do not have permission to create files in this directory.')) - return - dialog2.destroy() - self._start_receive(file_path, account, contact, file_props) - - def on_cancel(widget, account, contact, file_props): - dialog2.destroy() - gajim.connections[account].send_file_rejection(file_props) - - dialog2 = dialogs.FileChooserDialog( - title_text=_('Save File as...'), - action=gtk.FILE_CHOOSER_ACTION_SAVE, - buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, - gtk.STOCK_SAVE, gtk.RESPONSE_OK), - default_response=gtk.RESPONSE_OK, - current_folder=gajim.config.get('last_save_dir'), - on_response_ok=(on_ok, account, contact, file_props), - on_response_cancel=(on_cancel, account, contact, file_props)) - - dialog2.set_current_name(file_props['name']) - dialog2.connect('delete-event', lambda widget, event: - on_cancel(widget, account, contact, file_props)) + self.on_file_request_accepted(account, contact, file_props) def on_response_cancel(account, file_props): gajim.connections[account].send_file_rejection(file_props)