# -*- coding:utf-8 -*- ## src/chat_control_base.py ## ## 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 . ## import os import time from gi.repository import Gtk from gi.repository import Gdk from gi.repository import Pango from gi.repository import GObject from gi.repository import GLib from gi.repository import Gio import gtkgui_helpers from gtkgui_helpers import Color import message_control import dialogs import history_window import notify import re from common import events from common import gajim from common import helpers from common import ged from message_control import MessageControl from conversation_textview import ConversationTextview from message_textview import MessageTextView from common.contacts import GC_Contact from common.connection_handlers_events import MessageOutgoingEvent from command_system.implementation.middleware import ChatCommandProcessor from command_system.implementation.middleware import CommandTools # The members of these modules are not referenced directly anywhere in this # module, but still they need to be kept around. Importing them automatically # registers the contained CommandContainers with the command system, thereby # populating the list of available commands. import command_system.implementation.standard import command_system.implementation.execute try: import gtkspell HAS_GTK_SPELL = True except (ImportError, ValueError): HAS_GTK_SPELL = False ################################################################################ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): """ A base class containing a banner, ConversationTextview, MessageTextView """ keymap = Gdk.Keymap.get_default() try: keycode_c = keymap.get_entries_for_keyval(Gdk.KEY_c)[1][0].keycode except TypeError: keycode_c = 54 try: keycode_ins = keymap.get_entries_for_keyval(Gdk.KEY_Insert)[1][0].keycode except TypeError: keycode_ins = 118 except IndexError: # There is no KEY_Insert (MacOS) keycode_ins = None def make_href(self, match): url_color = gajim.config.get('urlmsgcolor') url = match.group() if not '://' in url: url = 'http://' + url return '%s' % (url, url_color, match.group()) def get_font_attrs(self): """ Get pango font attributes for banner from theme settings """ theme = gajim.config.get('roster_theme') bannerfont = gajim.config.get_per('themes', theme, 'bannerfont') bannerfontattrs = gajim.config.get_per('themes', theme, 'bannerfontattrs') if bannerfont: font = Pango.FontDescription(bannerfont) else: font = Pango.FontDescription('Normal') if bannerfontattrs: # B attribute is set by default if 'B' in bannerfontattrs: font.set_weight(Pango.Weight.HEAVY) if 'I' in bannerfontattrs: font.set_style(Pango.Style.ITALIC) font_attrs = 'font_desc="%s"' % font.to_string() # in case there is no font specified we use x-large font size if font.get_size() == 0: font_attrs = '%s size="x-large"' % font_attrs font.set_weight(Pango.Weight.NORMAL) font_attrs_small = 'font_desc="%s" size="small"' % font.to_string() return (font_attrs, font_attrs_small) def get_nb_unread(self): jid = self.contact.jid if self.resource: jid += '/' + self.resource type_ = self.type_id return len(gajim.events.get_events(self.account, jid, ['printed_' + type_, type_])) def draw_banner(self): """ Draw the fat line at the top of the window that houses the icon, jid, etc Derived types MAY implement this. """ self.draw_banner_text() self._update_banner_state_image() gajim.plugin_manager.gui_extension_point('chat_control_base_draw_banner', self) def update_toolbar(self): """ update state of buttons in toolbar """ self._update_toolbar() gajim.plugin_manager.gui_extension_point( 'chat_control_base_update_toolbar', self) def draw_banner_text(self): """ Derived types SHOULD implement this """ pass def update_ui(self): """ Derived types SHOULD implement this """ self.draw_banner() def repaint_themed_widgets(self): """ Derived types MAY implement this """ self.draw_banner() def _update_banner_state_image(self): """ Derived types MAY implement this """ pass def _update_toolbar(self): """ Derived types MAY implement this """ pass def _nec_our_status(self, obj): if self.account != obj.conn.name: return if obj.show == 'offline' or (obj.show == 'invisible' and \ obj.conn.is_zeroconf): self.got_disconnected() else: # Other code rejoins all GCs, so we don't do it here if not self.type_id == message_control.TYPE_GC: self.got_connected() if self.parent_win: self.parent_win.redraw_tab(self) def _nec_ping_sent(self, obj): if self.contact != obj.contact: return self.print_conversation(_('Ping?'), 'status') def _nec_ping_error(self, obj): if self.contact != obj.contact: return self.print_conversation(_('Error.'), 'status') def status_url_clicked(self, widget, url): helpers.launch_browser_mailer('url', url) def setup_seclabel(self, combo): self.seclabel_combo = combo self.seclabel_combo.hide() self.seclabel_combo.set_no_show_all(True) lb = Gtk.ListStore(str) self.seclabel_combo.set_model(lb) cell = Gtk.CellRendererText() cell.set_property('xpad', 5) # padding for status text self.seclabel_combo.pack_start(cell, True) # text to show is in in first column of liststore self.seclabel_combo.add_attribute(cell, 'text', 0) if gajim.connections[self.account].seclabel_supported: gajim.connections[self.account].seclabel_catalogue(self.contact.jid, self.on_seclabels_ready) def on_seclabels_ready(self): lb = self.seclabel_combo.get_model() lb.clear() i = 0 sel = 0 catalogue = gajim.connections[self.account].seclabel_catalogues[ self.contact.jid] for label in catalogue[2]: lb.append([label]) if label == catalogue[3]: sel = i i += 1 self.seclabel_combo.set_active(sel) self.seclabel_combo.set_no_show_all(False) self.seclabel_combo.show_all() def __init__(self, type_id, parent_win, widget_name, contact, acct, resource=None): # Undo needs this variable to know if space has been pressed. # Initialize it to True so empty textview is saved in undo list self.space_pressed = True if resource is None: # We very likely got a contact with a random resource. # This is bad, we need the highest for caps etc. c = gajim.contacts.get_contact_with_highest_priority(acct, contact.jid) if c and not isinstance(c, GC_Contact): contact = c MessageControl.__init__(self, type_id, parent_win, widget_name, contact, acct, resource=resource) widget = self.xml.get_object('history_button') # set document-open-recent icon for history button if gtkgui_helpers.gtk_icon_theme.has_icon('document-open-recent'): img = self.xml.get_object('history_image') img.set_from_icon_name('document-open-recent', Gtk.IconSize.MENU) id_ = widget.connect('clicked', self._on_history_menuitem_activate) self.handlers[id_] = widget # when/if we do XHTML we will put formatting buttons back widget = self.xml.get_object('emoticons_button') widget.set_sensitive(False) id_ = widget.connect('clicked', self.on_emoticons_button_clicked) self.handlers[id_] = widget # Create banner and connect signals widget = self.xml.get_object('banner_eventbox') id_ = widget.connect('button-press-event', self._on_banner_eventbox_button_press_event) self.handlers[id_] = widget self.urlfinder = re.compile( r"(www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s<>'\"]+[^!,\.\s<>\)'\"\]]") self.banner_status_label = self.xml.get_object('banner_label') id_ = self.banner_status_label.connect('populate_popup', self.on_banner_label_populate_popup) self.handlers[id_] = self.banner_status_label # Init DND self.TARGET_TYPE_URI_LIST = 80 self.dnd_list = [Gtk.TargetEntry.new('text/uri-list', 0, self.TARGET_TYPE_URI_LIST), Gtk.TargetEntry.new('MY_TREE_MODEL_ROW', Gtk.TargetFlags.SAME_APP, 0)] id_ = self.widget.connect('drag_data_received', self._on_drag_data_received) self.handlers[id_] = self.widget self.widget.drag_dest_set(Gtk.DestDefaults.MOTION | Gtk.DestDefaults.HIGHLIGHT | Gtk.DestDefaults.DROP, self.dnd_list, Gdk.DragAction.COPY) # Create textviews and connect signals self.conv_textview = ConversationTextview(self.account) id_ = self.conv_textview.connect('quote', self.on_quote) self.handlers[id_] = self.conv_textview.tv id_ = self.conv_textview.tv.connect('key_press_event', self._conv_textview_key_press_event) self.handlers[id_] = self.conv_textview.tv # FIXME: DND on non editable TextView, find a better way self.drag_entered = False id_ = self.conv_textview.tv.connect('drag_data_received', self._on_drag_data_received) self.handlers[id_] = self.conv_textview.tv id_ = self.conv_textview.tv.connect('drag_motion', self._on_drag_motion) self.handlers[id_] = self.conv_textview.tv id_ = self.conv_textview.tv.connect('drag_leave', self._on_drag_leave) self.handlers[id_] = self.conv_textview.tv self.conv_textview.tv.drag_dest_set(Gtk.DestDefaults.MOTION | Gtk.DestDefaults.HIGHLIGHT | Gtk.DestDefaults.DROP, self.dnd_list, Gdk.DragAction.COPY) self.conv_scrolledwindow = self.xml.get_object( 'conversation_scrolledwindow') self.conv_scrolledwindow.add(self.conv_textview.tv) widget = self.conv_scrolledwindow.get_vadjustment() id_ = widget.connect('value-changed', self.on_conversation_vadjustment_value_changed) self.handlers[id_] = widget id_ = widget.connect('changed', self.on_conversation_vadjustment_changed) self.handlers[id_] = widget self.was_at_the_end = True self.correcting = False self.last_sent_msg = None self.last_received_txt = {} # one per name self.last_received_id = {} # one per name # add MessageTextView to UI and connect signals self.msg_scrolledwindow = self.xml.get_object('message_scrolledwindow') self.msg_textview = MessageTextView() self.msg_scrolledwindow.add(self.msg_textview) id_ = self.msg_textview.connect('key_press_event', self._on_message_textview_key_press_event) self.handlers[id_] = self.msg_textview id_ = self.msg_textview.connect('configure-event', self.on_configure_event) self.handlers[id_] = self.msg_textview id_ = self.msg_textview.connect('populate_popup', self.on_msg_textview_populate_popup) self.handlers[id_] = self.msg_textview # Setup DND id_ = self.msg_textview.connect('drag_data_received', self._on_drag_data_received) self.handlers[id_] = self.msg_textview self.msg_textview.drag_dest_set(Gtk.DestDefaults.MOTION | Gtk.DestDefaults.HIGHLIGHT, self.dnd_list, Gdk.DragAction.COPY) # Hook up send button widget = self.xml.get_object('send_button') id_ = widget.connect('clicked', self._on_send_button_clicked) widget.set_sensitive(False) self.handlers[id_] = widget # the following vars are used to keep history of user's messages self.sent_history = [] self.sent_history_pos = 0 self.received_history = [] self.received_history_pos = 0 self.orig_msg = None # Emoticons menu # set image no matter if user wants at this time emoticons or not # (so toggle works ok) img = self.xml.get_object('emoticons_button_image') img.set_from_file(os.path.join(gajim.DATA_DIR, 'emoticons', 'static', 'smile.png')) self.toggle_emoticons() # Attach speller if gajim.config.get('use_speller') and HAS_GTK_SPELL: self.set_speller() self.conv_textview.tv.show() # For XEP-0172 self.user_nick = None self.smooth = True self.command_hits = [] self.last_key_tabs = False # chatstate timers and state self.reset_kbd_mouse_timeout_vars() self._schedule_activity_timers() message_tv_buffer = self.msg_textview.get_buffer() id_ = message_tv_buffer.connect('changed', self._on_message_tv_buffer_changed) self.handlers[id_] = message_tv_buffer if parent_win is not None: id_ = parent_win.window.connect('motion-notify-event', self._on_window_motion_notify) self.handlers[id_] = parent_win.window self.encryption = self.get_encryption_state() if self.parent_win: self.add_window_actions() # PluginSystem: adding GUI extension point for ChatControlBase # instance object (also subclasses, eg. ChatControl or GroupchatControl) gajim.plugin_manager.gui_extension_point('chat_control_base', self) gajim.ged.register_event_handler('our-show', ged.GUI1, self._nec_our_status) gajim.ged.register_event_handler('ping-sent', ged.GUI1, self._nec_ping_sent) gajim.ged.register_event_handler('ping-reply', ged.GUI1, self._nec_ping_reply) gajim.ged.register_event_handler('ping-error', ged.GUI1, self._nec_ping_error) # This is bascially a very nasty hack to surpass the inability # to properly use the super, because of the old code. CommandTools.__init__(self) def add_window_actions(self): action = Gio.SimpleAction.new_stateful( "%s-encryptiongroup" % self.contact.jid, GLib.VariantType.new("s"), GLib.Variant("s", self.encryption or 'disabled')) action.connect("change-state", self.change_encryption) self.parent_win.window.add_action(action) def change_encryption(self, action, param): encryption = param.get_string() if encryption == 'disabled': encryption = None if self.encryption == encryption: return if encryption: plugin = gajim.plugin_manager.encryption_plugins[encryption] if not plugin.activate_encryption(self): return else: if not self.widget_name == 'groupchat_control': self.terminate_esessions() action.set_state(param) self.set_encryption_state(encryption) self.set_encryption_menu_icon() self.set_lock_image() def set_encryption_state(self, encryption): config_key = '%s-%s' % (self.account, self.contact.jid) self.encryption = encryption gajim.config.set_per('encryption', config_key, 'encryption', self.encryption or '') def get_encryption_state(self): config_key = '%s-%s' % (self.account, self.contact.jid) state = gajim.config.get_per('encryption', config_key, 'encryption') return state or None def set_encryption_menu_icon(self): for child in self.encryption_menu.get_children(): if isinstance(child, Gtk.Image): image = child break if not self.encryption: icon = gtkgui_helpers.get_icon_pixmap( 'channel-insecure-symbolic', color=[Color.BLACK]) else: icon = gtkgui_helpers.get_icon_pixmap('channel-secure-symbolic') image.set_from_pixbuf(icon) def set_speller(self): # now set the one the user selected per_type = 'contacts' if self.type_id == message_control.TYPE_GC: per_type = 'rooms' lang = gajim.config.get_per(per_type, self.contact.jid, 'speller_language') if not lang: # use the default one lang = gajim.config.get('speller_language') if not lang: lang = gajim.LANG if lang: try: self.spell = gtkspell.Spell(self.msg_textview, lang) self.msg_textview.lang = lang self.spell.connect('language_changed', self.on_language_changed) except (GObject.GError, RuntimeError, TypeError, OSError): dialogs.AspellDictError(lang) def on_language_changed(self, spell, lang): per_type = 'contacts' if self.type_id == message_control.TYPE_GC: per_type = 'rooms' if not gajim.config.get_per(per_type, self.contact.jid): gajim.config.add_per(per_type, self.contact.jid) gajim.config.set_per(per_type, self.contact.jid, 'speller_language', lang) self.msg_textview.lang = lang def on_banner_label_populate_popup(self, label, menu): """ Override the default context menu and add our own menutiems """ item = Gtk.SeparatorMenuItem.new() menu.prepend(item) menu2 = self.prepare_context_menu() i = 0 for item in menu2: menu2.remove(item) menu.prepend(item) menu.reorder_child(item, i) i += 1 menu.show_all() def shutdown(self): super(ChatControlBase, self).shutdown() # Disconnect timer callbacks GLib.source_remove(self.possible_paused_timeout_id) GLib.source_remove(self.possible_inactive_timeout_id) # PluginSystem: removing GUI extension points connected with ChatControlBase # instance object gajim.plugin_manager.remove_gui_extension_point('chat_control_base', self) gajim.plugin_manager.remove_gui_extension_point( 'chat_control_base_draw_banner', self) gajim.ged.remove_event_handler('our-show', ged.GUI1, self._nec_our_status) def on_msg_textview_populate_popup(self, textview, menu): """ Override the default context menu and we prepend an option to switch languages """ item = Gtk.MenuItem.new_with_mnemonic(_('_Undo')) menu.prepend(item) id_ = item.connect('activate', self.msg_textview.undo) self.handlers[id_] = item item = Gtk.SeparatorMenuItem.new() menu.prepend(item) item = Gtk.MenuItem.new_with_mnemonic(_('_Clear')) menu.prepend(item) id_ = item.connect('activate', self.msg_textview.clear) self.handlers[id_] = item menu.show_all() def on_quote(self, widget, text): text = '>' + text.replace('\n', '\n>') + '\n' message_buffer = self.msg_textview.get_buffer() message_buffer.insert_at_cursor(text) # moved from ChatControl def _on_banner_eventbox_button_press_event(self, widget, event): """ If right-clicked, show popup """ if event.button == 3: # right click self.parent_win.popup_menu(event) def _on_send_button_clicked(self, widget): """ When send button is pressed: send the current message """ message_buffer = self.msg_textview.get_buffer() start_iter = message_buffer.get_start_iter() end_iter = message_buffer.get_end_iter() message = message_buffer.get_text(start_iter, end_iter, False) xhtml = self.msg_textview.get_xhtml() # send the message self.send_message(message, xhtml=xhtml) def _conv_textview_key_press_event(self, widget, event): # translate any layout to latin_layout valid, entries = self.keymap.get_entries_for_keyval(event.keyval) keycode = entries[0].keycode if (event.get_state() & Gdk.ModifierType.CONTROL_MASK and keycode in ( self.keycode_c, self.keycode_ins)) or ( event.get_state() & Gdk.ModifierType.SHIFT_MASK and \ event.keyval in (Gdk.KEY_Page_Down, Gdk.KEY_Page_Up)): return False self.parent_win.notebook.event(event) return True def show_emoticons_menu(self): if not gajim.config.get('emoticons_theme'): return gajim.interface.emoticon_menuitem_clicked = self.append_emoticon gajim.interface.emoticons_menu.popup(None, None, None, None, 1, 0) def _on_message_textview_key_press_event(self, widget, event): if event.keyval == Gdk.KEY_space: self.space_pressed = True elif (self.space_pressed or self.msg_textview.undo_pressed) and \ event.keyval not in (Gdk.KEY_Control_L, Gdk.KEY_Control_R) and \ not (event.keyval == Gdk.KEY_z and event.get_state() & Gdk.ModifierType.CONTROL_MASK): # If the space key has been pressed and now it hasnt, # we save the buffer into the undo list. But be carefull we're not # pressiong Control again (as in ctrl+z) _buffer = widget.get_buffer() start_iter, end_iter = _buffer.get_bounds() self.msg_textview.save_undo(_buffer.get_text(start_iter, end_iter, True)) self.space_pressed = False # Ctrl [+ Shift] + Tab are not forwarded to notebook. We handle it here if self.widget_name == 'groupchat_control': if event.keyval not in (Gdk.KEY_ISO_Left_Tab, Gdk.KEY_Tab): self.last_key_tabs = False if event.get_state() & Gdk.ModifierType.SHIFT_MASK: # CTRL + SHIFT + TAB if event.get_state() & Gdk.ModifierType.CONTROL_MASK and \ event.keyval == Gdk.KEY_ISO_Left_Tab: self.parent_win.move_to_next_unread_tab(False) return True # SHIFT + PAGE_[UP|DOWN]: send to conv_textview elif event.keyval == Gdk.KEY_Page_Down or \ event.keyval == Gdk.KEY_Page_Up: self.conv_textview.tv.event(event) return True elif event.get_state() & Gdk.ModifierType.CONTROL_MASK: if event.keyval == Gdk.KEY_Tab: # CTRL + TAB self.parent_win.move_to_next_unread_tab(True) return True message_buffer = self.msg_textview.get_buffer() event_state = event.get_state() if event.keyval == Gdk.KEY_Tab: start, end = message_buffer.get_bounds() position = message_buffer.get_insert() end = message_buffer.get_iter_at_mark(position) text = message_buffer.get_text(start, end, False) splitted = text.split() if (text.startswith(self.COMMAND_PREFIX) and not text.startswith(self.COMMAND_PREFIX * 2) and len(splitted) == 1): text = splitted[0] bare = text.lstrip(self.COMMAND_PREFIX) if len(text) == 1: self.command_hits = [] for command in self.list_commands(): for name in command.names: self.command_hits.append(name) else: if (self.last_key_tabs and self.command_hits and self.command_hits[0].startswith(bare)): self.command_hits.append(self.command_hits.pop(0)) else: self.command_hits = [] for command in self.list_commands(): for name in command.names: if name.startswith(bare): self.command_hits.append(name) if self.command_hits: message_buffer.delete(start, end) message_buffer.insert_at_cursor(self.COMMAND_PREFIX + \ self.command_hits[0] + ' ') self.last_key_tabs = True return True if self.widget_name != 'groupchat_control': self.last_key_tabs = False if event.keyval == Gdk.KEY_Up: if event_state & Gdk.ModifierType.CONTROL_MASK: if event_state & Gdk.ModifierType.SHIFT_MASK: # Ctrl+Shift+UP self.scroll_messages('up', message_buffer, 'received') else: # Ctrl+UP self.scroll_messages('up', message_buffer, 'sent') return True elif event.keyval == Gdk.KEY_Down: if event_state & Gdk.ModifierType.CONTROL_MASK: if event_state & Gdk.ModifierType.SHIFT_MASK: # Ctrl+Shift+Down self.scroll_messages('down', message_buffer, 'received') else: # Ctrl+Down self.scroll_messages('down', message_buffer, 'sent') return True elif event.keyval == Gdk.KEY_Return or \ event.keyval == Gdk.KEY_KP_Enter: # ENTER message_textview = widget message_buffer = message_textview.get_buffer() start_iter, end_iter = message_buffer.get_bounds() message = message_buffer.get_text(start_iter, end_iter, False) xhtml = self.msg_textview.get_xhtml() if event_state & Gdk.ModifierType.SHIFT_MASK: send_message = False else: is_ctrl_enter = bool(event_state & Gdk.ModifierType.CONTROL_MASK) send_message = is_ctrl_enter == gajim.config.get('send_on_ctrl_enter') if send_message and gajim.connections[self.account].connected < 2: # we are not connected dialogs.ErrorDialog(_('A connection is not available'), _('Your message can not be sent until you are connected.')) elif send_message: self.send_message(message, xhtml=xhtml) else: message_buffer.insert_at_cursor('\n') return True elif event.keyval == Gdk.KEY_z: # CTRL+z if event_state & Gdk.ModifierType.CONTROL_MASK: self.msg_textview.undo() return True return False def _on_drag_data_received(self, widget, context, x, y, selection, target_type, timestamp): """ Derived types SHOULD implement this """ pass def _on_drag_leave(self, widget, context, time): # FIXME: DND on non editable TextView, find a better way self.drag_entered = False self.conv_textview.tv.set_editable(False) def _on_drag_motion(self, widget, context, x, y, time): # FIXME: DND on non editable TextView, find a better way if not self.drag_entered: # We drag new data over the TextView, make it editable to catch dnd self.drag_entered_conv = True self.conv_textview.tv.set_editable(True) def get_seclabel(self): label = None if self.seclabel_combo is not None: idx = self.seclabel_combo.get_active() if idx != -1: cat = gajim.connections[self.account].seclabel_catalogues[self.contact.jid] lname = cat[2][idx] label = cat[1][lname] return label def send_message(self, message, keyID='', type_='chat', chatstate=None, msg_id=None, resource=None, xhtml=None, callback=None, callback_args=None, process_commands=True, attention=False): """ Send the given message to the active tab. Doesn't return None if error """ if not message or message == '\n': return None if callback_args is None: callback_args = [] if process_commands and self.process_as_command(message): return # refresh timers self.reset_kbd_mouse_timeout_vars() notifications = gajim.config.get('outgoing_chat_state_notifications') if (self.contact.jid == gajim.get_jid_from_account(self.account) or notifications == 'disabled'): chatstate = None label = self.get_seclabel() def _cb(obj, msg, cb, *cb_args): self.last_sent_msg = obj.msg_id if cb: cb(obj, msg, *cb_args) if self.correcting and self.last_sent_msg: correct_id = self.last_sent_msg else: correct_id = None gajim.nec.push_outgoing_event(MessageOutgoingEvent(None, account=self.account, jid=self.contact.jid, message=message, keyID=keyID, type_=type_, chatstate=chatstate, msg_id=msg_id, resource=resource, user_nick=self.user_nick, xhtml=xhtml, label=label, callback=_cb, callback_args=[callback] + callback_args, control=self, attention=attention, correct_id=correct_id, automatic_message=False, encryption=self.encryption)) # Record the history of sent messages self.save_message(message, 'sent') # Be sure to send user nickname only once according to JEP-0172 self.user_nick = None # Clear msg input message_buffer = self.msg_textview.get_buffer() message_buffer.set_text('') # clear message buffer (and tv of course) def check_for_possible_paused_chatstate(self, arg): """ Did we move mouse of that window or write something in message textview in the last 5 seconds? If yes - we go active for mouse, composing for kbd. If not - we go paused if we were previously composing """ contact = self.contact jid = contact.jid current_state = contact.our_chatstate if current_state is False: # jid doesn't support chatstates return False # stop looping message_buffer = self.msg_textview.get_buffer() if (self.kbd_activity_in_last_5_secs and message_buffer.get_char_count()): # Only composing if the keyboard activity was in text entry self.send_chatstate('composing', self.contact) elif (self.mouse_over_in_last_5_secs and current_state == 'inactive' and jid == self.parent_win.get_active_jid()): self.send_chatstate('active', self.contact) else: if current_state == 'composing': self.send_chatstate('paused', self.contact) # pause composing # assume no activity and let the motion-notify or 'insert-text' make them # True refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds! self.reset_kbd_mouse_timeout_vars() return True # loop forever def check_for_possible_inactive_chatstate(self, arg): """ Did we move mouse over that window or wrote something in message textview in the last 30 seconds? if yes - we go active. If no - we go inactive """ contact = self.contact current_state = contact.our_chatstate if current_state is False: # jid doesn't support chatstates return False # stop looping if self.mouse_over_in_last_5_secs or self.kbd_activity_in_last_5_secs: return True # loop forever if not self.mouse_over_in_last_30_secs or \ self.kbd_activity_in_last_30_secs: self.send_chatstate('inactive', contact) # assume no activity and let the motion-notify or 'insert-text' make them # True refresh 30 seconds too or else it's 30 - 5 = 25 seconds! self.reset_kbd_mouse_timeout_vars() return True # loop forever def _schedule_activity_timers(self): self.possible_paused_timeout_id = GLib.timeout_add_seconds(5, self.check_for_possible_paused_chatstate, None) self.possible_inactive_timeout_id = GLib.timeout_add_seconds(30, self.check_for_possible_inactive_chatstate, None) def reset_kbd_mouse_timeout_vars(self): self.kbd_activity_in_last_5_secs = False self.mouse_over_in_last_5_secs = False self.mouse_over_in_last_30_secs = False self.kbd_activity_in_last_30_secs = False def _on_window_motion_notify(self, widget, event): """ It gets called no matter if it is the active window or not """ if not self.parent_win: # when a groupchat is minimized there is no parent window return if self.parent_win.get_active_jid() == self.contact.jid: # if window is the active one, change vars assisting chatstate self.mouse_over_in_last_5_secs = True self.mouse_over_in_last_30_secs = True def _on_message_tv_buffer_changed(self, textbuffer): self.kbd_activity_in_last_5_secs = True self.kbd_activity_in_last_30_secs = True if textbuffer.get_char_count(): self.send_chatstate('composing', self.contact) else: self.send_chatstate('active', self.contact) def save_message(self, message, msg_type): # save the message, so user can scroll though the list with key up/down if msg_type == 'sent': history = self.sent_history pos = self.sent_history_pos else: history = self.received_history pos = self.received_history_pos size = len(history) scroll = False if pos == size else True # are we scrolling? # we don't want size of the buffer to grow indefinately max_size = gajim.config.get('key_up_lines') for i in range(size - max_size + 1): if pos == 0: break history.pop(0) pos -= 1 history.append(message) if not scroll or msg_type == 'sent': pos = len(history) if msg_type == 'sent': self.sent_history_pos = pos self.orig_msg = None else: self.received_history_pos = pos def print_conversation_line(self, text, kind, name, tim, other_tags_for_name=[], other_tags_for_time=[], other_tags_for_text=[], count_as_new=True, subject=None, old_kind=None, xhtml=None, simple=False, xep0184_id=None, graphics=True, displaymarking=None, msg_log_id=None, msg_stanza_id=None, correct_id=None, additional_data=None, encrypted=None): """ Print 'chat' type messages correct_id = (message_id, correct_id) """ jid = self.contact.jid full_jid = self.get_full_jid() textview = self.conv_textview end = False if self.was_at_the_end or kind == 'outgoing': end = True if other_tags_for_name is None: other_tags_for_name = [] if other_tags_for_time is None: other_tags_for_time = [] if other_tags_for_text is None: other_tags_for_text = [] if additional_data is None: additional_data = {} textview.print_conversation_line(text, jid, kind, name, tim, other_tags_for_name, other_tags_for_time, other_tags_for_text, subject, old_kind, xhtml, simple=simple, graphics=graphics, displaymarking=displaymarking, msg_stanza_id=msg_stanza_id, correct_id=correct_id, additional_data=additional_data, encrypted=encrypted) if xep0184_id is not None: textview.add_xep0184_mark(xep0184_id) if not count_as_new: return if kind in ('incoming', 'incoming_queue', 'outgoing'): self.last_received_txt[name] = text if correct_id: self.last_received_id[name] = correct_id[0] if kind == 'incoming': if not self.type_id == message_control.TYPE_GC or \ gajim.config.get('notify_on_all_muc_messages') or \ 'marked' in other_tags_for_text: # it's a normal message, or a muc message with want to be # notified about if quitting just after # other_tags_for_text == ['marked'] --> highlighted gc message gajim.last_message_time[self.account][full_jid] = time.time() if kind in ('incoming', 'incoming_queue'): # Record the history of received messages self.save_message(text, 'received') if kind in ('incoming', 'incoming_queue', 'error'): gc_message = False if self.type_id == message_control.TYPE_GC: gc_message = True if ((self.parent_win and (not self.parent_win.get_active_control() or \ self != self.parent_win.get_active_control() or \ not self.parent_win.is_active() or not end)) or \ (gc_message and \ jid in gajim.interface.minimized_controls[self.account])) and \ kind in ('incoming', 'incoming_queue', 'error'): # we want to have save this message in events list # other_tags_for_text == ['marked'] --> highlighted gc message if gc_message: if 'marked' in other_tags_for_text: event_type = events.PrintedMarkedGcMsgEvent else: event_type = events.PrintedGcMsgEvent event = 'gc_message_received' else: if self.type_id == message_control.TYPE_CHAT: event_type = events.PrintedChatEvent else: event_type = events.PrintedPmEvent event = 'message_received' show_in_roster = notify.get_show_in_roster(event, self.account, self.contact, self.session) show_in_systray = notify.get_show_in_systray(event, self.account, self.contact, event_type.type_) event = event_type(text, subject, self, msg_log_id, show_in_roster=show_in_roster, show_in_systray=show_in_systray) gajim.events.add_event(self.account, full_jid, event) # We need to redraw contact if we show in roster if show_in_roster: gajim.interface.roster.draw_contact(self.contact.jid, self.account) if not self.parent_win: return if (not self.parent_win.get_active_control() or \ self != self.parent_win.get_active_control() or \ not self.parent_win.is_active() or not end) and \ kind in ('incoming', 'incoming_queue', 'error'): self.parent_win.redraw_tab(self) if not self.parent_win.is_active(): self.parent_win.show_title(True, self) # Enabled Urgent hint else: self.parent_win.show_title(False, self) # Disabled Urgent hint def toggle_emoticons(self): """ Hide show emoticons_button and make sure emoticons_menu is always there when needed """ emoticons_button = self.xml.get_object('emoticons_button') if gajim.config.get('emoticons_theme'): emoticons_button.show() emoticons_button.set_no_show_all(False) else: emoticons_button.hide() emoticons_button.set_no_show_all(True) def append_emoticon(self, str_): buffer_ = self.msg_textview.get_buffer() if buffer_.get_char_count(): buffer_.insert_at_cursor(' %s ' % str_) else: # we are the beginning of buffer buffer_.insert_at_cursor('%s ' % str_) self.msg_textview.grab_focus() def on_emoticons_button_clicked(self, widget): """ Popup emoticons menu """ gajim.interface.emoticon_menuitem_clicked = self.append_emoticon gajim.interface.popup_emoticons_under_button(widget, self.parent_win) def on_color_menuitem_activate(self, widget): color_dialog = Gtk.ColorChooserDialog(None, self.parent_win.window) color_dialog.set_use_alpha(False) color_dialog.connect('response', self.msg_textview.color_set) color_dialog.show_all() def on_font_menuitem_activate(self, widget): font_dialog = Gtk.FontChooserDialog(None, self.parent_win.window) start, finish = self.msg_textview.get_active_iters() font_dialog.connect('response', self.msg_textview.font_set, start, finish) font_dialog.show_all() def on_formatting_menuitem_activate(self, widget): tag = widget.get_name() self.msg_textview.set_tag(tag) def on_clear_formatting_menuitem_activate(self, widget): self.msg_textview.clear_tags() def on_actions_button_clicked(self, widget): """ Popup action menu """ menu = self.prepare_context_menu(hide_buttonbar_items=True) menu.show_all() menu.attach_to_widget(widget, None) gtkgui_helpers.popup_emoticons_under_button(menu, widget, self.parent_win) def update_tags(self): self.conv_textview.update_tags() def clear(self, tv): buffer_ = tv.get_buffer() start, end = buffer_.get_bounds() buffer_.delete(start, end) def _on_history_menuitem_activate(self, widget=None, jid=None): """ When history menuitem is pressed: call history window """ if not jid: jid = self.contact.jid if 'logs' in gajim.interface.instances: gajim.interface.instances['logs'].window.present() gajim.interface.instances['logs'].open_history(jid, self.account) else: gajim.interface.instances['logs'] = \ history_window.HistoryWindow(jid, self.account) def _on_send_file(self, gc_contact=None): """ gc_contact can be set when we are in a groupchat control """ def _on_ok(c): gajim.interface.instances['file_transfers'].show_file_send_request( self.account, c) if self.TYPE_ID == message_control.TYPE_PM: gc_contact = self.gc_contact if gc_contact: # gc or pm gc_control = gajim.interface.msg_win_mgr.get_gc_control( gc_contact.room_jid, self.account) self_contact = gajim.contacts.get_gc_contact(self.account, gc_control.room_jid, gc_control.nick) if gc_control.is_anonymous and gc_contact.affiliation not in ['admin', 'owner'] and self_contact.affiliation in ['admin', 'owner']: contact = gajim.contacts.get_contact(self.account, gc_contact.jid) if not contact or contact.sub not in ('both', 'to'): prim_text = _('Really send file?') sec_text = _('If you send a file to %s, he/she will know your ' 'real JID.') % gc_contact.name dialog = dialogs.NonModalConfirmationDialog(prim_text, sec_text, on_response_ok=(_on_ok, gc_contact)) dialog.popup() return _on_ok(gc_contact) return _on_ok(self.contact) def on_minimize_menuitem_toggled(self, widget): """ When a grouchat is minimized, unparent the tab, put it in roster etc """ old_value = True non_minimized_gc = gajim.config.get_per('accounts', self.account, 'non_minimized_gc').split() if self.contact.jid in non_minimized_gc: old_value = False minimize = widget.get_active() if not minimize and not self.contact.jid in non_minimized_gc: non_minimized_gc.append(self.contact.jid) if minimize and self.contact.jid in non_minimized_gc: non_minimized_gc.remove(self.contact.jid) if old_value != minimize: gajim.config.set_per('accounts', self.account, 'non_minimized_gc', ' '.join(non_minimized_gc)) def set_control_active(self, state): if state: jid = self.contact.jid if self.was_at_the_end: # we are at the end type_ = ['printed_' + self.type_id] if self.type_id == message_control.TYPE_GC: type_ = ['printed_gc_msg', 'printed_marked_gc_msg'] if not gajim.events.remove_events(self.account, self.get_full_jid(), types=type_): # There were events to remove self.redraw_after_event_removed(jid) # send chatstate inactive to the one we're leaving # and active to the one we visit message_buffer = self.msg_textview.get_buffer() if message_buffer.get_char_count(): self.send_chatstate('paused', self.contact) else: self.send_chatstate('active', self.contact) self.reset_kbd_mouse_timeout_vars() GLib.source_remove(self.possible_paused_timeout_id) GLib.source_remove(self.possible_inactive_timeout_id) self._schedule_activity_timers() else: self.send_chatstate('inactive', self.contact) def scroll_to_end_iter(self): self.conv_textview.scroll_to_end_iter() return False def on_configure_event(self, msg_textview, event): """ When message_textview changes its size: if the new height will enlarge the window, enable the scrollbar automatic policy. Also enable scrollbar automatic policy for horizontal scrollbar if message we have in message_textview is too big """ if msg_textview.get_window() is None: return min_height = self.conv_scrolledwindow.get_property('height-request') conversation_height = self.conv_textview.tv.get_window().get_size()[1] message_height = msg_textview.get_window().get_size()[1] message_width = msg_textview.get_window().get_size()[0] # new tab is not exposed yet if conversation_height < 2: return if conversation_height < min_height: min_height = conversation_height # we don't want to always resize in height the message_textview # so we have minimum on conversation_textview's scrolled window # but we also want to avoid window resizing so if we reach that # minimum for conversation_textview and maximum for message_textview # we set to automatic the scrollbar policy diff_y = message_height - event.height if diff_y != 0: if conversation_height + diff_y < min_height: if message_height + conversation_height - min_height > min_height: policy = self.msg_scrolledwindow.get_property( 'vscrollbar-policy') if policy != Gtk.PolicyType.AUTOMATIC: self.msg_scrolledwindow.set_property('vscrollbar-policy', Gtk.PolicyType.AUTOMATIC) self.msg_scrolledwindow.set_property('height-request', message_height + conversation_height - min_height) else: self.msg_scrolledwindow.set_property('vscrollbar-policy', Gtk.PolicyType.NEVER) self.msg_scrolledwindow.set_property('height-request', -1) self.smooth = True # reinit the flag # enable scrollbar automatic policy for horizontal scrollbar # if message we have in message_textview is too big if event.width > message_width: self.msg_scrolledwindow.set_property('hscrollbar-policy', Gtk.PolicyType.AUTOMATIC) else: self.msg_scrolledwindow.set_property('hscrollbar-policy', Gtk.PolicyType.NEVER) return True def on_conversation_vadjustment_changed(self, adjustment): # used to stay at the end of the textview when we shrink conversation # textview. if self.was_at_the_end: self.scroll_to_end_iter() self.was_at_the_end = (adjustment.get_upper() - adjustment.get_value()\ - adjustment.get_page_size()) < 18 def on_conversation_vadjustment_value_changed(self, adjustment): self.was_at_the_end = (adjustment.get_upper() - adjustment.get_value() \ - adjustment.get_page_size()) < 18 if self.resource: jid = self.contact.get_full_jid() else: jid = self.contact.jid types_list = [] type_ = self.type_id if type_ == message_control.TYPE_GC: type_ = 'gc_msg' types_list = ['printed_' + type_, type_, 'printed_marked_gc_msg'] else: # Not a GC types_list = ['printed_' + type_, type_] if not len(gajim.events.get_events(self.account, jid, types_list)): return if not self.parent_win: return if self.conv_textview.at_the_end() and \ self.parent_win.get_active_control() == self and \ self.parent_win.window.is_active(): # we are at the end if self.type_id == message_control.TYPE_GC: if not gajim.events.remove_events(self.account, jid, types=types_list): self.redraw_after_event_removed(jid) elif self.session and self.session.remove_events(types_list): # There were events to remove self.redraw_after_event_removed(jid) def redraw_after_event_removed(self, jid): """ We just removed a 'printed_*' event, redraw contact in roster or gc_roster and titles in roster and msg_win """ self.parent_win.redraw_tab(self) self.parent_win.show_title() # TODO : get the contact and check notify.get_show_in_roster() if self.type_id == message_control.TYPE_PM: room_jid, nick = gajim.get_room_and_nick_from_fjid(jid) groupchat_control = gajim.interface.msg_win_mgr.get_gc_control( room_jid, self.account) if room_jid in gajim.interface.minimized_controls[self.account]: groupchat_control = \ gajim.interface.minimized_controls[self.account][room_jid] contact = gajim.contacts.get_contact_with_highest_priority( self.account, room_jid) if contact: gajim.interface.roster.draw_contact(room_jid, self.account) if groupchat_control: groupchat_control.draw_contact(nick) if groupchat_control.parent_win: groupchat_control.parent_win.redraw_tab(groupchat_control) else: gajim.interface.roster.draw_contact(jid, self.account) gajim.interface.roster.show_title() def scroll_messages(self, direction, msg_buf, msg_type): if msg_type == 'sent': history = self.sent_history pos = self.sent_history_pos self.received_history_pos = len(self.received_history) else: history = self.received_history pos = self.received_history_pos self.sent_history_pos = len(self.sent_history) size = len(history) if self.orig_msg is None: # user was typing something and then went into history, so save # whatever is already typed start_iter = msg_buf.get_start_iter() end_iter = msg_buf.get_end_iter() self.orig_msg = msg_buf.get_text(start_iter, end_iter, False) if pos == size and size > 0 and direction == 'up' and \ msg_type == 'sent' and not self.correcting and (not \ history[pos - 1].startswith('/') or history[pos - 1].startswith('/me')): self.correcting = True gtkgui_helpers.add_css_class( self.msg_textview, 'msgcorrectingcolor') message = history[pos - 1] msg_buf.set_text(message) return if self.correcting: # We were previously correcting gtkgui_helpers.remove_css_class( self.msg_textview, 'msgcorrectingcolor') self.correcting = False pos += -1 if direction == 'up' else +1 if pos == -1: return if pos >= size: pos = size message = self.orig_msg self.orig_msg = None else: message = history[pos] if msg_type == 'sent': self.sent_history_pos = pos else: self.received_history_pos = pos if self.orig_msg is not None: message = '> %s\n' % message.replace('\n', '\n> ') msg_buf.set_text(message) def widget_set_visible(self, widget, state): """ Show or hide a widget """ # make the last message visible, when changing to "full view" if not state: GLib.idle_add(self.conv_textview.scroll_to_end_iter) widget.set_no_show_all(state) if state: widget.hide() else: widget.show_all() def chat_buttons_set_visible(self, state): """ Toggle chat buttons """ MessageControl.chat_buttons_set_visible(self, state) self.widget_set_visible(self.xml.get_object('actions_hbox'), state) def got_connected(self): self.msg_textview.set_sensitive(True) self.msg_textview.set_editable(True) self.update_toolbar() def got_disconnected(self): self.msg_textview.set_sensitive(False) self.msg_textview.set_editable(False) self.conv_textview.tv.grab_focus() self.no_autonegotiation = False self.update_toolbar()