# Copyright (C) 2006 Dimitur Kirov # Copyright (C) 2006-2014 Yann Leboulanger # Copyright (C) 2006-2008 Jean-Marie Traissard # Nikos Kouremenos # Travis Shirk # Copyright (C) 2007 Lukas Petrovicky # Julien Pivotto # Copyright (C) 2007-2008 Brendan Taylor # Stephan Erb # Copyright (C) 2008 Jonathan Schleifer # # This file is part of Gajim. # # Gajim is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published # by the Free Software Foundation; version 3 only. # # Gajim is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Gajim. If not, see . from typing import ClassVar # pylint: disable=unused-import from typing import Type # pylint: disable=unused-import from typing import Optional # pylint: disable=unused-import import os import time import logging from gi.repository import Gtk from gi.repository import Gio from gi.repository import Pango from gi.repository import GLib from gi.repository import Gdk from nbxmpp.namespaces import Namespace from gajim.common import app from gajim.common import helpers from gajim.common import ged from gajim.common import i18n from gajim.common.i18n import _ from gajim.common.helpers import AdditionalDataDict from gajim.common.helpers import open_uri from gajim.common.helpers import geo_provider_from_location from gajim.common.helpers import event_filter from gajim.common.helpers import open_file from gajim.common.contacts import GC_Contact from gajim.common.const import AvatarSize from gajim.common.const import KindConstant from gajim.common.const import Chatstate from gajim.common.const import PEPEventType from gajim.common.const import JingleState from gajim import gtkgui_helpers from gajim import gui_menu_builder from gajim import dialogs from gajim.gui.gstreamer import create_gtk_widget from gajim.gui.dialogs import DialogButton from gajim.gui.dialogs import ConfirmationDialog from gajim.gui.add_contact import AddNewContactWindow from gajim.gui.util import get_cursor from gajim.gui.util import format_mood from gajim.gui.util import format_activity from gajim.gui.util import format_tune from gajim.gui.util import format_location from gajim.gui.util import get_activity_icon_name from gajim.gui.util import make_href_markup from gajim.gui.const import ControlType from gajim.command_system.implementation.hosts import ChatCommands from gajim.command_system.framework import CommandHost # pylint: disable=unused-import from gajim.chat_control_base import ChatControlBase log = logging.getLogger('gajim.chat_control') class JingleObject: __slots__ = ('sid', 'state', 'available', 'update') def __init__(self, state, update): self.sid = None self.state = state self.available = False self.update = update ################################################################################ class ChatControl(ChatControlBase): """ A control for standard 1-1 chat """ _type = ControlType.CHAT old_msg_kind = None # last kind of the printed message # Set a command host to bound to. Every command given through a chat will be # processed with this command host. COMMAND_HOST = ChatCommands # type: ClassVar[Type[CommandHost]] def __init__(self, parent_win, contact, acct, session, resource=None): ChatControlBase.__init__(self, parent_win, 'chat_control', contact, acct, resource) self.last_recv_message_id = None self.last_recv_message_marks = None self.last_message_timestamp = None self.toggle_emoticons() if not app.settings.get('hide_chat_banner'): self.xml.banner_eventbox.set_no_show_all(False) self.xml.sendfile_button.set_action_name( 'win.send-file-%s' % self.control_id) # Menu for the HeaderBar self.control_menu = gui_menu_builder.get_singlechat_menu( self.control_id, self.account, self.contact.jid, self._type) # Settings menu self.xml.settings_menu.set_menu_model(self.control_menu) self.jingle = { 'audio': JingleObject( JingleState.NULL, self.update_audio), 'video': JingleObject( JingleState.NULL, self.update_video), } self._video_widget_other = None self._video_widget_self = None self.update_toolbar() self.update_all_pep_types() self._update_avatar() # Hook up signals widget = self.xml.location_eventbox id_ = widget.connect('button-release-event', self.on_location_eventbox_button_release_event) self.handlers[id_] = widget id_ = widget.connect('enter-notify-event', self.on_location_eventbox_enter_notify_event) self.handlers[id_] = widget id_ = widget.connect('leave-notify-event', self.on_location_eventbox_leave_notify_event) self.handlers[id_] = widget for key in ('1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'): widget = self.xml.get_object(key + '_button') id_ = widget.connect('pressed', self.on_num_button_pressed, key) self.handlers[id_] = widget id_ = widget.connect('released', self.on_num_button_released) self.handlers[id_] = widget widget = self.xml.mic_hscale id_ = widget.connect('value_changed', self.on_mic_hscale_value_changed) self.handlers[id_] = widget widget = self.xml.sound_hscale 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_halign(Gtk.Align.START) self.info_bar_label.set_valign(Gtk.Align.START) self.info_bar_label.set_ellipsize(Pango.EllipsizeMode.END) content_area.add(self.info_bar_label) self.info_bar.set_no_show_all(True) self.xml.vbox2.pack_start(self.info_bar, False, True, 5) self.xml.vbox2.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 if not resource: resource = contact.resource session = app.connections[self.account].find_controlless_session( self.contact.jid, resource) self.setup_seclabel() if session: session.control = self self.session = session self.add_actions() self.update_ui() self.set_lock_image() self.xml.encryption_menu.set_menu_model( gui_menu_builder.get_encryption_menu( self.control_id, self._type, self.account == 'Local')) self.set_encryption_menu_icon() # restore previous conversation self.restore_conversation() self.msg_textview.grab_focus() # pylint: disable=line-too-long self.register_events([ ('nickname-received', ged.GUI1, self._on_nickname_received), ('mood-received', ged.GUI1, self._on_mood_received), ('activity-received', ged.GUI1, self._on_activity_received), ('tune-received', ged.GUI1, self._on_tune_received), ('location-received', ged.GUI1, self._on_location_received), ('update-client-info', ged.GUI1, self._on_update_client_info), ('chatstate-received', ged.GUI1, self._on_chatstate_received), ('caps-update', ged.GUI1, self._on_caps_update), ('message-sent', ged.OUT_POSTCORE, self._on_message_sent), ('mam-decrypted-message-received', ged.GUI1, self._on_mam_decrypted_message_received), ('decrypted-message-received', ged.GUI1, self._on_decrypted_message_received), ('receipt-received', ged.GUI1, self._receipt_received), ('displayed-received', ged.GUI1, self._displayed_received), ('message-error', ged.GUI1, self._on_message_error), ('zeroconf-error', ged.GUI1, self._on_zeroconf_error), ]) if self._type.is_chat: # Don’t connect this when PrivateChatControl is used self.register_event('update-roster-avatar', ged.GUI1, self._on_update_roster_avatar) # pylint: enable=line-too-long # PluginSystem: adding GUI extension point for this ChatControl # instance object app.plugin_manager.gui_extension_point('chat_control', self) self.update_actions() @property def jid(self): return self.contact.jid def add_actions(self): super().add_actions() actions = [ ('invite-contacts-', self._on_invite_contacts), ('add-to-roster-', self._on_add_to_roster), ('block-contact-', self._on_block_contact), ('information-', self._on_information), ('start-call-', self._on_start_call), ] for action in actions: action_name, func = action act = Gio.SimpleAction.new(action_name + self.control_id, None) act.connect('activate', func) self.parent_win.window.add_action(act) chatstate = self.contact.settings.get('send_chatstate') act = Gio.SimpleAction.new_stateful( 'send-chatstate-' + self.control_id, GLib.VariantType.new("s"), GLib.Variant("s", chatstate)) act.connect('change-state', self._on_send_chatstate) self.parent_win.window.add_action(act) marker = self.contact.settings.get('send_marker') act = Gio.SimpleAction.new_stateful( f'send-marker-{self.control_id}', None, GLib.Variant.new_boolean(marker)) act.connect('change-state', self._on_send_marker) self.parent_win.window.add_action(act) def update_actions(self): win = self.parent_win.window online = app.account_is_connected(self.account) con = app.connections[self.account] # Add to roster if not isinstance(self.contact, GC_Contact) \ and _('Not in contact list') in self.contact.groups and \ app.connections[self.account].roster_supported and online: win.lookup_action( 'add-to-roster-' + self.control_id).set_enabled(True) else: win.lookup_action( 'add-to-roster-' + self.control_id).set_enabled(False) # Block contact win.lookup_action( 'block-contact-' + self.control_id).set_enabled( online and con.get_module('Blocking').supported) # Jingle AV detection if (self.contact.supports(Namespace.JINGLE_ICE_UDP) and app.is_installed('FARSTREAM') and self.contact.resource): self.jingle['audio'].available = self.contact.supports( Namespace.JINGLE_RTP_AUDIO) self.jingle['video'].available = self.contact.supports( Namespace.JINGLE_RTP_VIDEO) else: if (self.jingle['audio'].available or self.jingle['video'].available): self.stop_jingle() self.jingle['audio'].available = False self.jingle['video'].available = False win.lookup_action(f'start-call-{self.control_id}').set_enabled( online and (self.jingle['audio'].available or self.jingle['video'].available)) # Send message has_text = self.msg_textview.has_text() win.lookup_action( f'send-message-{self.control_id}').set_enabled(online and has_text) # Send file (HTTP File Upload) httpupload = win.lookup_action( 'send-file-httpupload-' + self.control_id) httpupload.set_enabled( online and con.get_module('HTTPUpload').available) # Send file (Jingle) jingle_support = self.contact.supports(Namespace.JINGLE_FILE_TRANSFER_5) jingle_conditions = jingle_support and self.contact.show != 'offline' jingle = win.lookup_action('send-file-jingle-' + self.control_id) jingle.set_enabled(online and jingle_conditions) # Send file win.lookup_action( 'send-file-' + self.control_id).set_enabled( jingle.get_enabled() or httpupload.get_enabled()) # Set File Transfer Button tooltip if online and (httpupload.get_enabled() or jingle.get_enabled()): tooltip_text = _('Send File…') else: tooltip_text = _('No File Transfer available') self.xml.sendfile_button.set_tooltip_text(tooltip_text) # Chat markers state = GLib.Variant.new_boolean( self.contact.settings.get('send_marker')) win.lookup_action( f'send-marker-{self.control_id}').change_state(state) # Convert to GC if app.settings.get_account_setting(self.account, 'is_zeroconf'): win.lookup_action( 'invite-contacts-' + self.control_id).set_enabled(False) else: if self.contact.supports(Namespace.MUC) and online: win.lookup_action( 'invite-contacts-' + self.control_id).set_enabled(True) else: win.lookup_action( 'invite-contacts-' + self.control_id).set_enabled(False) # Information win.lookup_action( 'information-' + self.control_id).set_enabled(online) def remove_actions(self): super().remove_actions() actions = [ 'invite-contacts-', 'add-to-roster-', 'block-contact-', 'information-', 'start-call-', 'send-chatstate-', 'send-marker-', ] for action in actions: self.parent_win.window.remove_action(f'{action}{self.control_id}') def focus(self): self.msg_textview.grab_focus() def delegate_action(self, action): res = super().delegate_action(action) if res == Gdk.EVENT_STOP: return res if action == 'show-contact-info': self.parent_win.window.lookup_action( 'information-%s' % self.control_id).activate() return Gdk.EVENT_STOP if action == 'send-file': if app.interface.msg_win_mgr.mode == \ app.interface.msg_win_mgr.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER: app.interface.roster.tree.grab_focus() return Gdk.EVENT_PROPAGATE self.parent_win.window.lookup_action( 'send-file-%s' % self.control_id).activate() return Gdk.EVENT_STOP return Gdk.EVENT_PROPAGATE def _on_add_to_roster(self, _action, _param): AddNewContactWindow(self.account, self.contact.jid) def _on_block_contact(self, _action, _param): def _block_contact(report=None): con = app.connections[self.account] con.get_module('Blocking').block([self.contact.jid], report=report) self.parent_win.remove_tab(self, None, force=True) if _('Not in contact list') in self.contact.get_shown_groups(): app.interface.roster.remove_contact( self.contact.jid, self.account, force=True, backend=True) return app.interface.roster.draw_contact(self.contact.jid, self.account) ConfirmationDialog( _('Block Contact'), _('Really block this contact?'), _('You will appear offline for this contact and you will ' 'not receive further messages.'), [DialogButton.make('Cancel'), DialogButton.make('OK', text=_('_Report Spam'), callback=_block_contact, kwargs={'report': 'spam'}), DialogButton.make('Remove', text=_('_Block'), callback=_block_contact)], modal=False).show() def _on_information(self, _action, _param): app.interface.roster.on_info(None, self.contact, self.account) def _on_invite_contacts(self, _action, _param): """ User wants to invite some friends to chat """ dialogs.TransformChatToMUC(self.account, [self.contact.jid]) def _on_send_chatstate(self, action, param): action.set_state(param) self.contact.settings.set('send_chatstate', param.get_string()) def _on_send_marker(self, action, param): action.set_state(param) self.contact.settings.set('send_marker', param.get_boolean()) def subscribe_events(self): """ Register listeners to the events class """ app.events.event_added_subscribe(self.on_event_added) app.events.event_removed_subscribe(self.on_event_removed) def unsubscribe_events(self): """ Unregister listeners to the events class """ app.events.event_added_unsubscribe(self.on_event_added) app.events.event_removed_unsubscribe(self.on_event_removed) def _update_toolbar(self): # Formatting # TODO: find out what encryption allows for xhtml and which not if self.contact.supports(Namespace.XHTML_IM): self.xml.formattings_button.set_sensitive(True) self.xml.formattings_button.set_tooltip_text(_( 'Show a list of formattings')) else: self.xml.formattings_button.set_sensitive(False) self.xml.formattings_button.set_tooltip_text( _('This contact does not support HTML')) def update_all_pep_types(self): self._update_pep(PEPEventType.LOCATION) self._update_pep(PEPEventType.MOOD) self._update_pep(PEPEventType.ACTIVITY) self._update_pep(PEPEventType.TUNE) def _update_pep(self, type_): image = self._get_pep_widget(type_) data = self.contact.pep.get(type_) if data is None: image.hide() return if type_ == PEPEventType.MOOD: icon = 'mood-%s' % data.mood formated_text = format_mood(*data) elif type_ == PEPEventType.ACTIVITY: icon = get_activity_icon_name(data.activity, data.subactivity) formated_text = format_activity(*data) elif type_ == PEPEventType.TUNE: icon = 'audio-x-generic' formated_text = format_tune(*data) elif type_ == PEPEventType.LOCATION: icon = 'applications-internet' formated_text = format_location(data) image.set_from_icon_name(icon, Gtk.IconSize.MENU) image.set_tooltip_markup(formated_text) image.show() def _get_pep_widget(self, type_): if type_ == PEPEventType.MOOD: return self.xml.mood_image if type_ == PEPEventType.ACTIVITY: return self.xml.activity_image if type_ == PEPEventType.TUNE: return self.xml.tune_image if type_ == PEPEventType.LOCATION: return self.xml.location_image return None @event_filter(['account', 'jid']) def _on_mood_received(self, _event): self._update_pep(PEPEventType.MOOD) @event_filter(['account', 'jid']) def _on_activity_received(self, _event): self._update_pep(PEPEventType.ACTIVITY) @event_filter(['account', 'jid']) def _on_tune_received(self, _event): self._update_pep(PEPEventType.TUNE) @event_filter(['account', 'jid']) def _on_location_received(self, _event): self._update_pep(PEPEventType.LOCATION) @event_filter(['account', 'jid']) def _on_nickname_received(self, _event): self.update_ui() self.parent_win.redraw_tab(self) self.parent_win.show_title() @event_filter(['account', 'jid']) def _on_update_client_info(self, event): contact = app.contacts.get_contact( self.account, event.jid, event.resource) if contact is None: return self.xml.phone_image.set_visible(contact.uses_phone) @event_filter(['account']) def _on_chatstate_received(self, event): if self._type.is_privatechat: if event.contact != self.gc_contact: return else: if event.contact.jid != self.contact.jid: return self.draw_banner_text() # update chatstate in tab for this chat if event.contact.is_gc_contact: chatstate = event.contact.chatstate else: chatstate = app.contacts.get_combined_chatstate( self.account, self.contact.jid) self.parent_win.redraw_tab(self, chatstate) @event_filter(['account']) def _on_caps_update(self, event): if self._type.is_chat and event.jid != self.contact.jid: return if self._type.is_privatechat and event.fjid != self.contact.jid: return self.update_ui() @event_filter(['account']) def _on_mam_decrypted_message_received(self, event): if event.properties.type.is_groupchat: return if event.properties.is_muc_pm: if not event.properties.jid == self.contact.get_full_jid(): return else: if not event.properties.jid.bare_match(self.contact.jid): return kind = '' # incoming if event.kind == KindConstant.CHAT_MSG_SENT: kind = 'outgoing' self.add_message(event.msgtxt, kind, tim=event.properties.mam.timestamp, correct_id=event.correct_id, message_id=event.properties.id, additional_data=event.additional_data) @event_filter(['account']) def _on_decrypted_message_received(self, event): if not event.msgtxt: return True if event.session.control != self: return typ = '' if event.properties.is_sent_carbon: typ = 'out' self.add_message(event.msgtxt, typ, tim=event.properties.timestamp, subject=event.properties.subject, displaymarking=event.displaymarking, msg_log_id=event.msg_log_id, message_id=event.properties.id, correct_id=event.correct_id, additional_data=event.additional_data) if event.msg_log_id: pw = self.parent_win end = self.conv_textview.autoscroll if not pw or (pw.get_active_control() and self \ == pw.get_active_control() and pw.is_active() and end): app.storage.archive.set_read_messages([event.msg_log_id]) @event_filter(['account', 'jid']) def _on_message_error(self, event): self.conv_textview.show_error(event.message_id, event.error) @event_filter(['account', 'jid']) def _on_message_sent(self, event): if not event.message: return self.last_sent_msg = event.message_id message_id = event.message_id if event.label: displaymarking = event.label.displaymarking else: displaymarking = None if self.correcting: self.correcting = False gtkgui_helpers.remove_css_class( self.msg_textview, 'gajim-msg-correcting') self.add_message(event.message, self.contact.jid, tim=event.timestamp, displaymarking=displaymarking, message_id=message_id, correct_id=event.correct_id, additional_data=event.additional_data) @event_filter(['account', 'jid']) def _receipt_received(self, event): self.conv_textview.show_receipt(event.receipt_id) @event_filter(['account', 'jid']) def _displayed_received(self, event): self.conv_textview.show_displayed(event.marker_id) @event_filter(['account', 'jid']) def _on_zeroconf_error(self, event): self.add_status_message(event.message) @event_filter(['account', 'jid']) def _on_update_roster_avatar(self, obj): self._update_avatar() @event_filter(['account']) def _nec_ping(self, event): if self.contact != event.contact: return if event.name == 'ping-sent': self.add_info_message(_('Ping?')) elif event.name == 'ping-reply': self.add_info_message( _('Pong! (%s seconds)') % event.seconds) elif event.name == 'ping-error': self.add_info_message(event.error) def change_resource(self, resource): old_full_jid = self.get_full_jid() self.resource = resource new_full_jid = self.get_full_jid() # update app.last_message_time if old_full_jid in app.last_message_time[self.account]: app.last_message_time[self.account][new_full_jid] = \ app.last_message_time[self.account][old_full_jid] # update events app.events.change_jid(self.account, old_full_jid, new_full_jid) # update MessageWindow._controls self.parent_win.change_jid(self.account, old_full_jid, new_full_jid) # Jingle AV def _on_start_call(self, *args): audio_state = self.jingle['audio'].state video_state = self.jingle['video'].state if audio_state == JingleState.NULL and video_state == JingleState.NULL: self.xml.av_box.set_no_show_all(False) self.xml.av_box.show_all() self.xml.jingle_audio_state.hide() self.xml.av_start_box.show() self.xml.av_start_mic_cam_button.set_sensitive( self.jingle['video'].available) self.xml.av_cam_button.set_sensitive(False) def _on_call_with_mic(self, _button): self._on_jingle_button_toggled(['audio']) self.xml.av_start_box.hide() def _on_call_with_mic_and_cam(self, _button): self._on_jingle_button_toggled(['audio', 'video']) self.xml.av_start_box.hide() def _on_video(self, *args): self._on_jingle_button_toggled(['video']) def update_audio(self): self.update_actions() audio_state = self.jingle['audio'].state video_state = self.jingle['video'].state if self.jingle['video'].available: self.xml.av_cam_button.set_sensitive( video_state not in ( JingleState.CONNECTING, JingleState.CONNECTED)) if audio_state == JingleState.NULL: self.xml.audio_buttons_box.set_sensitive(False) self.xml.jingle_audio_state.set_no_show_all(True) self.xml.jingle_audio_state.hide() self.xml.jingle_connection_state.set_text('') self.xml.jingle_connection_spinner.stop() self.xml.jingle_connection_spinner.hide() if video_state == JingleState.NULL: self.xml.av_box.set_no_show_all(True) self.xml.av_box.hide() else: self.xml.jingle_connection_spinner.show() self.xml.jingle_connection_spinner.start() if audio_state == JingleState.CONNECTING: self.xml.av_box.set_no_show_all(False) self.xml.av_box.show_all() self.xml.jingle_connection_state.set_text( _('Calling…')) self.xml.av_cam_button.set_sensitive(False) elif audio_state == JingleState.CONNECTION_RECEIVED: self.xml.jingle_connection_state.set_text( _('Incoming Call')) elif audio_state == JingleState.CONNECTED: self.xml.jingle_audio_state.set_no_show_all(False) self.xml.jingle_audio_state.show() self.xml.jingle_connection_state.set_text('') self.xml.jingle_connection_spinner.stop() self.xml.jingle_connection_spinner.hide() if self.jingle['video'].available: self.xml.av_cam_button.set_sensitive(True) input_vol = app.settings.get('audio_input_volume') output_vol = app.settings.get('audio_output_volume') self.xml.mic_hscale.set_value(max(min(input_vol, 100), 0)) self.xml.sound_hscale.set_value(max(min(output_vol, 100), 0)) self.xml.audio_buttons_box.set_sensitive(True) elif audio_state == JingleState.ERROR: self.xml.jingle_audio_state.hide() self.xml.jingle_connection_state.set_text( _('Connection Error')) self.xml.jingle_connection_spinner.stop() self.xml.jingle_connection_spinner.hide() if not self.jingle['audio'].sid: self.xml.audio_buttons_box.set_sensitive(False) def update_video(self): self.update_actions() audio_state = self.jingle['audio'].state video_state = self.jingle['video'].state if video_state == JingleState.NULL: self.xml.video_box.set_no_show_all(True) self.xml.video_box.hide() self.xml.outgoing_viewport.set_no_show_all(True) self.xml.outgoing_viewport.hide() if self._video_widget_other: self._video_widget_other.destroy() if self._video_widget_self: self._video_widget_self.destroy() if audio_state != JingleState.CONNECTED: self.xml.jingle_connection_state.set_text('') self.xml.jingle_connection_spinner.stop() self.xml.jingle_connection_spinner.hide() self.xml.av_cam_button.set_sensitive(True) self.xml.av_cam_button.set_tooltip_text(_('Turn Camera on')) self.xml.av_cam_image.set_from_icon_name( 'feather-camera-symbolic', Gtk.IconSize.BUTTON) if audio_state == JingleState.NULL: self.xml.av_box.set_no_show_all(True) self.xml.av_box.hide() else: self.xml.jingle_connection_spinner.show() self.xml.jingle_connection_spinner.start() if video_state == JingleState.CONNECTING: self.xml.jingle_connection_state.set_text(_('Calling (Video)…')) self.xml.av_box.set_no_show_all(False) self.xml.av_box.show_all() self.xml.av_cam_button.set_sensitive(False) self.xml.av_cam_button.set_tooltip_text(_('Turn Camera off')) self.xml.av_cam_image.set_from_icon_name( 'feather-camera-off-symbolic', Gtk.IconSize.BUTTON) elif video_state == JingleState.CONNECTION_RECEIVED: self.xml.jingle_connection_state.set_text( _('Incoming Call (Video)')) self.xml.av_cam_button.set_sensitive(False) self.xml.av_cam_button.set_tooltip_text(_('Turn Camera off')) self.xml.av_cam_image.set_from_icon_name( 'feather-camera-off-symbolic', Gtk.IconSize.BUTTON) elif video_state == JingleState.CONNECTED: self.xml.video_box.set_no_show_all(False) self.xml.video_box.show_all() if app.settings.get('video_see_self'): self.xml.outgoing_viewport.set_no_show_all(False) self.xml.outgoing_viewport.show() else: self.xml.outgoing_viewport.set_no_show_all(True) self.xml.outgoing_viewport.hide() sink_other, self._video_widget_other, _name = create_gtk_widget() sink_self, self._video_widget_self, _name = create_gtk_widget() self.xml.incoming_viewport.add(self._video_widget_other) self.xml.outgoing_viewport.add(self._video_widget_self) con = app.connections[self.account] session = con.get_module('Jingle').get_jingle_session( self.contact.get_full_jid(), self.jingle['video'].sid) content = session.get_content('video') content.do_setup(sink_self, sink_other) self.xml.jingle_connection_state.set_text('') self.xml.jingle_connection_spinner.stop() self.xml.jingle_connection_spinner.hide() self.xml.av_cam_button.set_sensitive(True) self.xml.av_cam_button.set_tooltip_text(_('Turn Camera off')) self.xml.av_cam_image.set_from_icon_name( 'feather-camera-off-symbolic', Gtk.IconSize.BUTTON) elif video_state == JingleState.ERROR: self.xml.jingle_connection_state.set_text( _('Connection Error')) self.xml.jingle_connection_spinner.stop() self.xml.jingle_connection_spinner.hide() def set_jingle_state(self, jingle_type: str, state: str, sid: str = None, reason: str = None) -> None: jingle = self.jingle[jingle_type] if state in ( JingleState.CONNECTING, JingleState.CONNECTED, JingleState.NULL, JingleState.ERROR) and reason: log.info('%s state: %s, reason: %s', jingle_type, state, reason) if state in (jingle.state, JingleState.ERROR): return if (state == JingleState.NULL and jingle.sid not in (None, sid)): return new_sid = None if state == JingleState.NULL: new_sid = None if state in ( JingleState.CONNECTION_RECEIVED, JingleState.CONNECTING, JingleState.CONNECTED): new_sid = sid jingle.state = state jingle.sid = new_sid jingle.update() def stop_jingle(self, sid=None, reason=None): audio_sid = self.jingle['audio'].sid video_sid = self.jingle['video'].sid if audio_sid and sid in (audio_sid, None): self.close_jingle_content('audio') if video_sid and sid in (video_sid, None): self.close_jingle_content('video') def close_jingle_content(self, jingle_type: str, shutdown: Optional[bool] = False) -> None: jingle = self.jingle[jingle_type] if not jingle.sid: return con = app.connections[self.account] session = con.get_module('Jingle').get_jingle_session( self.contact.get_full_jid(), jingle.sid) if session: content = session.get_content(jingle_type) if content: session.remove_content(content.creator, content.name) if not shutdown: jingle.sid = None jingle.state = JingleState.NULL jingle.update() def _on_end_call_clicked(self, _widget): self.close_jingle_content('audio') self.close_jingle_content('video') self.xml.jingle_audio_state.set_no_show_all(True) self.xml.jingle_audio_state.hide() self.xml.av_box.set_no_show_all(True) self.xml.av_box.hide() def _on_jingle_button_toggled(self, jingle_types): con = app.connections[self.account] if all(item in jingle_types for item in ['audio', 'video']): # Both 'audio' and 'video' in jingle_types sid = con.get_module('Jingle').start_audio_video( self.contact.get_full_jid()) self.set_jingle_state('audio', JingleState.CONNECTING, sid) self.set_jingle_state('video', JingleState.CONNECTING, sid) return if 'audio' in jingle_types: if self.jingle['audio'].state != JingleState.NULL: self.close_jingle_content('audio') else: sid = con.get_module('Jingle').start_audio( self.contact.get_full_jid()) self.set_jingle_state('audio', JingleState.CONNECTING, sid) if 'video' in jingle_types: if self.jingle['video'].state != JingleState.NULL: self.close_jingle_content('video') else: sid = con.get_module('Jingle').start_video( self.contact.get_full_jid()) self.set_jingle_state('video', JingleState.CONNECTING, sid) def _get_audio_content(self): con = app.connections[self.account] session = con.get_module('Jingle').get_jingle_session( self.contact.get_full_jid(), self.jingle['audio'].sid) return session.get_content('audio') def on_num_button_pressed(self, _widget, num): self._get_audio_content().start_dtmf(num) def on_num_button_released(self, _released): self._get_audio_content().stop_dtmf() def on_mic_hscale_value_changed(self, _widget, value): self._get_audio_content().set_mic_volume(value / 100) app.settings.set('audio_input_volume', int(value)) def on_sound_hscale_value_changed(self, _widget, value): self._get_audio_content().set_out_volume(value / 100) app.settings.set('audio_output_volume', int(value)) def on_location_eventbox_button_release_event(self, _widget, _event): if 'geoloc' in self.contact.pep: location = self.contact.pep['geoloc'].data if 'lat' in location and 'lon' in location: uri = geo_provider_from_location(location['lat'], location['lon']) open_uri(uri) def on_location_eventbox_leave_notify_event(self, _widget, _event): """ Just moved the mouse so show the cursor """ cursor = get_cursor('default') self.parent_win.window.get_window().set_cursor(cursor) def on_location_eventbox_enter_notify_event(self, _widget, _event): cursor = get_cursor('pointer') self.parent_win.window.get_window().set_cursor(cursor) def update_ui(self): # The name banner is drawn here ChatControlBase.update_ui(self) self.update_toolbar() self._update_avatar() self.update_actions() def draw_banner_text(self): """ Draw the text in the fat line at the top of the window that houses the name, jid """ contact = self.contact name = contact.get_shown_name() if self.resource: name += '/' + self.resource if self._type.is_privatechat: name = i18n.direction_mark + _( '%(nickname)s from group chat %(room_name)s') % \ {'nickname': name, 'room_name': self.room_name} name = i18n.direction_mark + GLib.markup_escape_text(name) status = contact.status if status is not None: status_reduced = helpers.reduce_chars_newlines(status, max_lines=1) else: status_reduced = '' status_escaped = GLib.markup_escape_text(status_reduced) if self._type.is_privatechat: cs = self.gc_contact.chatstate else: cs = app.contacts.get_combined_chatstate( self.account, self.contact.jid) if app.settings.get('show_chatstate_in_banner'): chatstate = helpers.get_uf_chatstate(cs) label_text = '%s %s' % \ (name, chatstate) label_tooltip = '%s %s' % (name, chatstate) else: label_text = '%s' % name label_tooltip = name if status_escaped: status_text = make_href_markup(status_escaped) status_text = '%s' % status_text self.xml.banner_label.set_tooltip_text(status) self.xml.banner_label.set_no_show_all(False) self.xml.banner_label.show() else: status_text = '' self.xml.banner_label.hide() self.xml.banner_label.set_no_show_all(True) self.xml.banner_label.set_markup(status_text) # setup the label that holds name and jid self.xml.banner_name_label.set_markup(label_text) self.xml.banner_name_label.set_tooltip_text(label_tooltip) def send_message(self, message, xhtml=None, process_commands=True, attention=False): """ Send a message to contact """ if self.encryption: self.sendmessage = True app.plugin_manager.extension_point('send_message' + self.encryption, self) if not self.sendmessage: return message = helpers.remove_invalid_xml_chars(message) if message in ('', None, '\n'): return ChatControlBase.send_message(self, message, type_='chat', xhtml=xhtml, process_commands=process_commands, attention=attention) def get_our_nick(self): return app.nicks[self.account] def add_message(self, text, frm='', tim=None, subject=None, displaymarking=None, msg_log_id=None, correct_id=None, message_id=None, additional_data=None, error=None): """ Print a line in the conversation If frm is set to status: it's a status message. if frm is set to error: it's an error message. The difference between status and error is mainly that with error, msg count as a new message (in systray and in control). If frm is set to info: it's a information message. If frm is set to print_queue: it is incoming from queue. If frm is set to another value: it's an outgoing message. If frm is not set: it's an incoming message. """ contact = self.contact if additional_data is None: additional_data = AdditionalDataDict() if frm == 'error': kind = 'error' name = '' else: if not frm: kind = 'incoming' name = contact.get_shown_name() elif frm == 'print_queue': kind = 'incoming_queue' name = contact.get_shown_name() else: kind = 'outgoing' name = self.get_our_nick() ChatControlBase.add_message(self, text, kind, name, tim, subject=subject, old_kind=self.old_msg_kind, displaymarking=displaymarking, msg_log_id=msg_log_id, message_id=message_id, correct_id=correct_id, additional_data=additional_data, error=error) if text.startswith('/me ') or text.startswith('/me\n'): self.old_msg_kind = None else: self.old_msg_kind = kind def get_tab_label(self): unread = '' if self.resource: jid = self.contact.get_full_jid() else: jid = self.contact.jid num_unread = len(app.events.get_events( self.account, jid, ['printed_%s' % self._type, str(self._type)])) if num_unread == 1: unread = '*' elif num_unread > 1: unread = '[' + str(num_unread) + ']' name = self.contact.get_shown_name() if self.resource: name += '/' + self.resource label_str = GLib.markup_escape_text(name) if num_unread: # if unread, text in the label becomes bold label_str = '' + unread + label_str + '' return label_str def get_tab_image(self): scale = self.parent_win.window.get_scale_factor() return app.contacts.get_avatar(self.account, self.contact.jid, AvatarSize.ROSTER, scale, self.contact.show) def prepare_context_menu(self, hide_buttonbar_items=False): """ Set compact view menuitem active state sets active and sensitivity state for history_menuitem (False for tranasports) and file_transfer_menuitem and hide()/show() for add_to_roster_menuitem """ if app.jid_is_transport(self.contact.jid): menu = gui_menu_builder.get_transport_menu(self.contact, self.account) else: menu = gui_menu_builder.get_contact_menu( self.contact, self.account, use_multiple_contacts=False, show_start_chat=False, show_encryption=True, control=self, show_buttonbar_items=not hide_buttonbar_items) return menu def shutdown(self): # PluginSystem: removing GUI extension points connected with ChatControl # instance object app.plugin_manager.remove_gui_extension_point('chat_control', self) self.unsubscribe_events() self.remove_actions() # Send 'gone' chatstate con = app.connections[self.account] con.get_module('Chatstate').set_chatstate(self.contact, Chatstate.GONE) for jingle_type in ('audio', 'video'): self.close_jingle_content(jingle_type, shutdown=True) self.jingle.clear() # disconnect self from session if self.session: self.session.control = None # Clean events app.events.remove_events( self.account, self.get_full_jid(), types=['printed_%s' % self._type, str(self._type)]) # Remove contact instance if contact has been removed key = (self.contact.jid, self.account) roster = app.interface.roster has_pending = roster.contact_has_pending_roster_events(self.contact, self.account) if key in roster.contacts_to_be_removed.keys() and not has_pending: backend = roster.contacts_to_be_removed[key]['backend'] del roster.contacts_to_be_removed[key] roster.remove_contact(self.contact.jid, self.account, force=True, backend=backend) super(ChatControl, self).shutdown() def minimizable(self): return False def safe_shutdown(self): return False def allow_shutdown(self, method, on_yes, on_no, _on_minimize): time_ = app.last_message_time[self.account][self.get_full_jid()] # 2 seconds if time.time() - time_ < 2: no_log_for = app.settings.get_account_setting( self.account, 'no_log_for').split() more = '' if self.contact.jid in no_log_for: more = _('Note: Chat history is disabled for this contact.') if self.account in no_log_for: more = _('Note: Chat history is disabled for this account.') text = _('You just received a new message from %s.\n' 'Do you want to close this tab?') % self.contact.get_shown_name() if more: text += '\n' + more ConfirmationDialog( _('Close'), _('New Message'), text, [DialogButton.make('Cancel', callback=lambda: on_no(self)), DialogButton.make('Remove', text=_('_Close'), callback=lambda: on_yes(self))], transient_for=self.parent_win.window).show() return on_yes(self) def _update_avatar(self): scale = self.parent_win.window.get_scale_factor() surface = app.contacts.get_avatar(self.account, self.contact.jid, AvatarSize.CHAT, scale, self.contact.show) self.xml.avatar_image.set_from_surface(surface) def _on_drag_data_received(self, widget, context, x, y, selection, target_type, timestamp): if not selection.get_data(): return if target_type == self.TARGET_TYPE_URI_LIST: # File drag and drop (handled in chat_control_base) self.drag_data_file_transfer(selection) else: # Convert single chat to MUC treeview = app.interface.roster.tree model = treeview.get_model() data = selection.get_data().decode() tree_selection = treeview.get_selection() if tree_selection.count_selected_rows() == 0: return path = tree_selection.get_selected_rows()[1][0] iter_ = model.get_iter(path) type_ = model[iter_][2] if type_ != 'contact': # Source is not a contact return dropped_jid = data dropped_transport = app.get_transport_name_from_jid(dropped_jid) c_transport = app.get_transport_name_from_jid(self.contact.jid) if dropped_transport or c_transport: return # transport contacts cannot be invited dialogs.TransformChatToMUC(self.account, [self.contact.jid], [dropped_jid]) def restore_conversation(self): jid = self.contact.jid # don't restore lines if it's a transport if app.jid_is_transport(jid): return # number of messages that are in queue and are already logged, we want # to avoid duplication pending = len(app.events.get_events(self.account, jid, ['chat', 'pm'])) if self.resource: pending += len(app.events.get_events(self.account, self.contact.get_full_jid(), ['chat', 'pm'])) rows = app.storage.archive.get_last_conversation_lines( self.account, jid, pending) local_old_kind = None self.conv_textview.just_cleared = True for row in rows: # time, kind, message, subject, additional_data msg = row.message additional_data = row.additional_data if not msg: # message is empty, we don't print it continue if row.kind in (KindConstant.CHAT_MSG_SENT, KindConstant.SINGLE_MSG_SENT): kind = 'outgoing' name = self.get_our_nick() elif row.kind in (KindConstant.SINGLE_MSG_RECV, KindConstant.CHAT_MSG_RECV): kind = 'incoming' name = self.contact.get_shown_name() elif row.kind == KindConstant.ERROR: kind = 'status' name = self.contact.get_shown_name() tim = float(row.time) if row.subject: msg = _('Subject: %(subject)s\n%(message)s') % \ {'subject': row.subject, 'message': msg} ChatControlBase.add_message(self, msg, kind, name, tim, restored=True, old_kind=local_old_kind, additional_data=additional_data, message_id=row.message_id, marker=row.marker, error=row.error) if (row.message.startswith('/me ') or row.message.startswith('/me\n')): local_old_kind = None else: local_old_kind = kind if rows: self.conv_textview.print_empty_line() def read_queue(self): """ Read queue and print messages contained in it """ jid = self.contact.jid jid_with_resource = jid if self.resource: jid_with_resource += '/' + self.resource events = app.events.get_events(self.account, jid_with_resource) # list of message ids which should be marked as read message_ids = [] for event in events: if event.type_ != str(self._type): continue kind = 'print_queue' if event.sent_forwarded: kind = 'out' self.add_message(event.message, kind, tim=event.time, subject=event.subject, displaymarking=event.displaymarking, correct_id=event.correct_id, message_id=event.message_id, additional_data=event.additional_data) if isinstance(event.msg_log_id, int): message_ids.append(event.msg_log_id) if event.session and not self.session: self.set_session(event.session) if message_ids: app.storage.archive.set_read_messages(message_ids) # XEP-0333 Send marker con = app.connections[self.account] con.get_module('ChatMarkers').send_displayed_marker( self.contact, self.last_msg_id, self._type) self.last_msg_id = None app.events.remove_events(self.account, jid_with_resource, types=[str(self._type)]) typ = 'chat' # Is it a normal chat or a pm ? # reset to status image in gc if it is a pm # Is it a pm ? room_jid, nick = app.get_room_and_nick_from_fjid(jid) control = app.interface.msg_win_mgr.get_gc_control(room_jid, self.account) if control and control.is_groupchat: control.update_ui() control.parent_win.show_title() typ = 'pm' self.redraw_after_event_removed(jid) if self.contact.show in ('offline', 'error'): show_offline = app.settings.get('showoffline') show_transports = app.settings.get('show_transports_group') if (not show_transports and app.jid_is_transport(jid)) or \ (not show_offline and typ == 'chat' and \ len(app.contacts.get_contacts(self.account, jid)) < 2): app.interface.roster.remove_to_be_removed(self.contact.jid, self.account) elif typ == 'pm': control.remove_contact(nick) def _on_convert_to_gc_menuitem_activate(self, _widget): """ User wants to invite some friends to chat """ dialogs.TransformChatToMUC(self.account, [self.contact.jid]) def got_connected(self): ChatControlBase.got_connected(self) # Refreshing contact contact = app.contacts.get_contact_with_highest_priority( self.account, self.contact.jid) if isinstance(contact, GC_Contact): contact = contact.as_contact() if contact: self.contact = contact self.draw_banner() self.update_actions() def got_disconnected(self): ChatControlBase.got_disconnected(self) self.update_actions() def update_status_display(self, name, uf_show, status): self.update_ui() self.parent_win.redraw_tab(self) if not app.settings.get('print_status_in_chats'): return if status: status = '- %s' % status status_line = _('%(name)s is now %(show)s %(status)s') % { 'name': name, 'show': uf_show, 'status': status or ''} self.add_status_message(status_line) 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) # Remove old buttons area = self.info_bar.get_action_area() for button in area.get_children(): area.remove(button) # Add new buttons 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.MessageType.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 = app.events.get_events(self.account, self.contact.jid, [type_]) for ev in evs: if ev.file_props == file_props: return ev return None def _on_accept_file_request(self, _widget, file_props): app.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: app.events.remove_events(self.account, self.contact.jid, event=ev) def _on_cancel_file_request(self, _widget, file_props): con = app.connections[self.account] con.get_module('Bytestream').send_file_rejection(file_props) ev = self._get_file_props_event(file_props, 'file-request') if ev: app.events.remove_events(self.account, self.contact.jid, event=ev) def _got_file_request(self, file_props): """ Show an InfoBar on top of control """ if app.settings.get('use_kib_mib'): units = GLib.FormatSizeFlags.IEC_UNITS else: units = GLib.FormatSizeFlags.DEFAULT markup = '%s\n%s' % (_('File Transfer'), file_props.name) if file_props.desc: markup += '\n(%s)' % file_props.desc markup += '\n%s: %s' % ( _('Size'), GLib.format_size_full(file_props.size, units)) button_decline = Gtk.Button.new_with_mnemonic(_('_Decline')) button_decline.connect( 'clicked', self._on_cancel_file_request, file_props) button_accept = Gtk.Button.new_with_mnemonic(_('_Accept')) button_accept.connect( 'clicked', self._on_accept_file_request, file_props) self._add_info_bar_message( markup, [button_decline, button_accept], file_props, Gtk.MessageType.QUESTION) def _on_open_ft_folder(self, _widget, file_props): path = os.path.split(file_props.file_name)[0] if os.path.exists(path) and os.path.isdir(path): open_file(path) ev = self._get_file_props_event(file_props, 'file-completed') if ev: app.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: app.events.remove_events(self.account, self.contact.jid, event=ev) def _got_file_completed(self, file_props): markup = '%s\n%s' % (_('File Transfer Completed'), file_props.name) if file_props.desc: markup += '\n(%s)' % file_props.desc b1 = Gtk.Button.new_with_mnemonic(_('Open _Folder')) b1.connect('clicked', self._on_open_ft_folder, file_props) b2 = Gtk.Button.new_with_mnemonic(_('_Close')) 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\n%s' % (pri_txt, sec_txt) button = Gtk.Button.new_with_mnemonic(_('_Close')) button.connect('clicked', self._on_ok, file_props, type_) self._add_info_bar_message( markup, [button], file_props, Gtk.MessageType.ERROR) def _on_accept_gc_invitation(self, _widget, event): app.interface.show_or_join_groupchat(self.account, str(event.muc), password=event.password) app.events.remove_events(self.account, self.contact.jid, event=event) def _on_cancel_gc_invitation(self, _widget, event): app.events.remove_events(self.account, self.contact.jid, event=event) def _get_gc_invitation(self, event): markup = '%s\n%s' % (_('Group Chat Invitation'), event.muc) if event.reason: markup += '\n(%s)' % event.reason button_decline = Gtk.Button.new_with_mnemonic(_('_Decline')) button_decline.connect('clicked', self._on_cancel_gc_invitation, event) button_accept = Gtk.Button.new_with_mnemonic(_('_Accept')) button_accept.connect('clicked', self._on_accept_gc_invitation, event) self._add_info_bar_message( markup, [button_decline, button_accept], (event.muc, event.reason), Gtk.MessageType.QUESTION) def _on_reject_call(self, _button, event): app.events.remove_events( self.account, self.contact.jid, types='jingle-incoming') con = app.connections[self.account] session = con.get_module('Jingle').get_jingle_session( event.peerjid, event.sid) if not session: return if not session.accepted: session.decline_session() else: for content in event.content_types: session.reject_content(content) def _on_accept_call(self, _button, event): app.events.remove_events( self.account, self.contact.jid, types='jingle-incoming') con = app.connections[self.account] session = con.get_module('Jingle').get_jingle_session( event.peerjid, event.sid) if not session: return audio = session.get_content('audio') video = session.get_content('video') if audio and not audio.negotiated: self.set_jingle_state('audio', JingleState.CONNECTING, event.sid) if video and not video.negotiated: self.set_jingle_state('video', JingleState.CONNECTING, event.sid) if not session.accepted: session.approve_session() for content in event.content_types: session.approve_content(content) def add_call_received_message(self, event): markup = '%s' % (_('Incoming Call')) if 'video' in event.content_types: markup += _('\nVideo Call') else: markup += _('\nVoice Call') button_reject = Gtk.Button.new_with_mnemonic(_('_Reject')) button_reject.connect('clicked', self._on_reject_call, event) button_accept = Gtk.Button.new_with_mnemonic(_('_Accept')) button_accept.connect('clicked', self._on_accept_call, event) self._add_info_bar_message( markup, [button_reject, button_accept], event, Gtk.MessageType.QUESTION) 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.file_props) elif event.type_ == 'file-completed': self._got_file_completed(event.file_props) elif event.type_ in ('file-error', 'file-stopped'): msg_err = '' if event.file_props.error == -1: msg_err = _('Remote contact stopped transfer') elif event.file_props.error == -6: msg_err = _('Error opening file') self._got_file_error(event.file_props, event.type_, _('File transfer stopped'), msg_err) elif event.type_ in ('file-request-error', 'file-send-error'): self._got_file_error( event.file_props, event.type_, _('File transfer cancelled'), _('Connection with peer cannot be established.')) elif event.type_ == 'gc-invitation': self._get_gc_invitation(event) 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', 'gc-invitation', 'jingle-incoming'): continue i = 0 removed = False for ib_msg in self.info_bar_queue: if ev.type_ == 'gc-invitation': if ev.muc == ib_msg[2][0]: self.info_bar_queue.remove(ib_msg) removed = True elif ev.type_ == 'jingle-incoming': # TODO: Need to be more specific here? self.info_bar_queue.remove(ib_msg) removed = True else: # file-* if ib_msg[2] == ev.file_props: self.info_bar_queue.remove(ib_msg) removed = True if removed: if i == 0: # We are removing the one currently displayed self.info_bar.set_no_show_all(True) self.info_bar.hide() # show next one? GLib.idle_add(self._info_bar_show_message) break i += 1