| 1 | ## tabbed_chat_window.py |
|---|
| 2 | ## |
|---|
| 3 | ## Gajim Team: |
|---|
| 4 | ## - Yann Le Boulanger <asterix@lagaule.org> |
|---|
| 5 | ## - Vincent Hanquez <tab@snarc.org> |
|---|
| 6 | ## - Nikos Kouremenos <kourem@gmail.com> |
|---|
| 7 | ## |
|---|
| 8 | ## Copyright (C) 2003-2005 Gajim Team |
|---|
| 9 | ## |
|---|
| 10 | ## This program is free software; you can redistribute it and/or modify |
|---|
| 11 | ## it under the terms of the GNU General Public License as published |
|---|
| 12 | ## by the Free Software Foundation; version 2 only. |
|---|
| 13 | ## |
|---|
| 14 | ## This program is distributed in the hope that it will be useful, |
|---|
| 15 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 16 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 17 | ## GNU General Public License for more details. |
|---|
| 18 | ## |
|---|
| 19 | |
|---|
| 20 | import gtk |
|---|
| 21 | import gtk.glade |
|---|
| 22 | import pango |
|---|
| 23 | import gobject |
|---|
| 24 | import time |
|---|
| 25 | import urllib |
|---|
| 26 | import base64 |
|---|
| 27 | import os |
|---|
| 28 | |
|---|
| 29 | import dialogs |
|---|
| 30 | import chat |
|---|
| 31 | import gtkgui_helpers |
|---|
| 32 | |
|---|
| 33 | from common import gajim |
|---|
| 34 | from common import helpers |
|---|
| 35 | from common import i18n |
|---|
| 36 | |
|---|
| 37 | _ = i18n._ |
|---|
| 38 | APP = i18n.APP |
|---|
| 39 | gtk.glade.bindtextdomain(APP, i18n.DIR) |
|---|
| 40 | gtk.glade.textdomain(APP) |
|---|
| 41 | |
|---|
| 42 | GTKGUI_GLADE = 'gtkgui.glade' |
|---|
| 43 | |
|---|
| 44 | class TabbedChatWindow(chat.Chat): |
|---|
| 45 | """Class for tabbed chat window""" |
|---|
| 46 | def __init__(self, user, plugin, account): |
|---|
| 47 | chat.Chat.__init__(self, plugin, account, 'tabbed_chat_window') |
|---|
| 48 | self.contacts = {} |
|---|
| 49 | self.chatstates = {} |
|---|
| 50 | # keep check for possible paused timeouts per jid |
|---|
| 51 | self.possible_paused_timeout_id = {} |
|---|
| 52 | # keep check for possible inactive timeouts per jid |
|---|
| 53 | self.possible_inactive_timeout_id = {} |
|---|
| 54 | self.TARGET_TYPE_URI_LIST = 80 |
|---|
| 55 | self.dnd_list = [ ( 'text/uri-list', 0, self.TARGET_TYPE_URI_LIST ) ] |
|---|
| 56 | self.new_user(user) |
|---|
| 57 | self.show_title() |
|---|
| 58 | |
|---|
| 59 | # NOTE: if it not a window event, connect in new_user function |
|---|
| 60 | signal_dict = { |
|---|
| 61 | 'on_tabbed_chat_window_destroy': self.on_tabbed_chat_window_destroy, |
|---|
| 62 | 'on_tabbed_chat_window_delete_event': self.on_tabbed_chat_window_delete_event, |
|---|
| 63 | 'on_tabbed_chat_window_focus_in_event': self.on_tabbed_chat_window_focus_in_event, |
|---|
| 64 | 'on_tabbed_chat_window_focus_out_event': self.on_tabbed_chat_window_focus_out_event, |
|---|
| 65 | 'on_chat_notebook_key_press_event': self.on_chat_notebook_key_press_event, |
|---|
| 66 | 'on_chat_notebook_switch_page': self.on_chat_notebook_switch_page, # in chat.py |
|---|
| 67 | 'on_tabbed_chat_window_motion_notify_event': self.on_tabbed_chat_window_motion_notify_event, |
|---|
| 68 | } |
|---|
| 69 | |
|---|
| 70 | self.xml.signal_autoconnect(signal_dict) |
|---|
| 71 | |
|---|
| 72 | |
|---|
| 73 | if gajim.config.get('saveposition'): |
|---|
| 74 | # get window position and size from config |
|---|
| 75 | self.window.move(gajim.config.get('chat-x-position'), |
|---|
| 76 | gajim.config.get('chat-y-position')) |
|---|
| 77 | self.window.resize(gajim.config.get('chat-width'), |
|---|
| 78 | gajim.config.get('chat-height')) |
|---|
| 79 | |
|---|
| 80 | # gtk+ doesn't make use of the motion notify on gtkwindow by default |
|---|
| 81 | # so this line adds that |
|---|
| 82 | self.window.set_events(gtk.gdk.POINTER_MOTION_MASK) |
|---|
| 83 | |
|---|
| 84 | self.window.show_all() |
|---|
| 85 | |
|---|
| 86 | def save_var(self, jid): |
|---|
| 87 | '''return the specific variable of a jid, like gpg_enabled |
|---|
| 88 | the return value have to be compatible with wthe one given to load_var''' |
|---|
| 89 | gpg_enabled = self.xmls[jid].get_widget('gpg_togglebutton').get_active() |
|---|
| 90 | return {'gpg_enabled': gpg_enabled} |
|---|
| 91 | |
|---|
| 92 | def load_var(self, jid, var): |
|---|
| 93 | if not self.xmls.has_key(jid): |
|---|
| 94 | return |
|---|
| 95 | self.xmls[jid].get_widget('gpg_togglebutton').set_active( |
|---|
| 96 | var['gpg_enabled']) |
|---|
| 97 | |
|---|
| 98 | def on_tabbed_chat_window_motion_notify_event(self, widget, event): |
|---|
| 99 | '''it gets called no matter if it is the active window or not''' |
|---|
| 100 | if widget.get_property('has-toplevel-focus'): |
|---|
| 101 | # change chatstate only if window is the active one |
|---|
| 102 | self.mouse_over_in_last_5_secs = True |
|---|
| 103 | self.mouse_over_in_last_30_secs = True |
|---|
| 104 | |
|---|
| 105 | def on_drag_data_received(self, widget, context, x, y, selection, target_type, |
|---|
| 106 | timestamp, contact): |
|---|
| 107 | if target_type == self.TARGET_TYPE_URI_LIST: |
|---|
| 108 | uri = selection.data.strip() |
|---|
| 109 | uri_splitted = uri.split() # we may have more than one file dropped |
|---|
| 110 | for uri in uri_splitted: |
|---|
| 111 | path = helpers.get_file_path_from_dnd_dropped_uri(uri) |
|---|
| 112 | if os.path.isfile(path): # is it file? |
|---|
| 113 | self.plugin.windows['file_transfers'].send_file(self.account, |
|---|
| 114 | contact, path) |
|---|
| 115 | |
|---|
| 116 | def draw_widgets(self, contact): |
|---|
| 117 | """draw the widgets in a tab (status_image, contact_button ...) |
|---|
| 118 | according to the the information in the contact variable""" |
|---|
| 119 | jid = contact.jid |
|---|
| 120 | self.set_state_image(jid) |
|---|
| 121 | contact_button = self.xmls[jid].get_widget('contact_button') |
|---|
| 122 | contact_button.set_use_underline(False) |
|---|
| 123 | tb = self.xmls[jid].get_widget('gpg_togglebutton') |
|---|
| 124 | if contact.keyID: # we can do gpg |
|---|
| 125 | tb.set_sensitive(True) |
|---|
| 126 | tt = _('OpenPGP Encryption') |
|---|
| 127 | else: |
|---|
| 128 | tb.set_sensitive(False) |
|---|
| 129 | #we talk about a contact here |
|---|
| 130 | tt = _('%s has not broadcasted an OpenPGP key nor you have assigned one') % contact.name |
|---|
| 131 | tip = gtk.Tooltips() |
|---|
| 132 | tip.set_tip(self.xmls[jid].get_widget('gpg_eventbox'), tt) |
|---|
| 133 | |
|---|
| 134 | # add the fat line at the top |
|---|
| 135 | self.draw_name_banner(contact) |
|---|
| 136 | |
|---|
| 137 | def draw_name_banner(self, contact, chatstate = None): |
|---|
| 138 | '''Draw the fat line at the top of the window that |
|---|
| 139 | houses the status icon, name, jid, and avatar''' |
|---|
| 140 | # this is the text for the big brown bar |
|---|
| 141 | # some chars need to be escaped.. |
|---|
| 142 | jid = contact.jid |
|---|
| 143 | banner_name_label = self.xmls[jid].get_widget('banner_name_label') |
|---|
| 144 | |
|---|
| 145 | name = gtkgui_helpers.escape_for_pango_markup(contact.name) |
|---|
| 146 | |
|---|
| 147 | status = contact.status |
|---|
| 148 | #FIXME: when gtk2.4 is OOOOLD do it via glade2.10+ |
|---|
| 149 | if gtk.pygtk_version >= (2, 6, 0) and gtk.gtk_version >= (2, 6, 0) and \ |
|---|
| 150 | status is not None: |
|---|
| 151 | banner_name_label.set_ellipsize(pango.ELLIPSIZE_END) |
|---|
| 152 | status = gtkgui_helpers.reduce_chars_newlines(status, 0, 2) |
|---|
| 153 | #FIXME: remove me when gtk24 is OLD |
|---|
| 154 | elif status is not None: |
|---|
| 155 | status = gtkgui_helpers.reduce_chars_newlines(status, 50, 2) |
|---|
| 156 | |
|---|
| 157 | status = gtkgui_helpers.escape_for_pango_markup(status) |
|---|
| 158 | |
|---|
| 159 | #FIXME: uncomment me when we support sending messages to specific resource |
|---|
| 160 | # composing full jid |
|---|
| 161 | #fulljid = jid |
|---|
| 162 | #if self.contacts[jid].resource: |
|---|
| 163 | # fulljid += '/' + self.contacts[jid].resource |
|---|
| 164 | #label_text = '<span weight="heavy" size="x-large">%s</span>\n%s' \ |
|---|
| 165 | # % (name, fulljid) |
|---|
| 166 | |
|---|
| 167 | |
|---|
| 168 | st = gajim.config.get('chat_state_notifications') |
|---|
| 169 | if chatstate and st in ('composing_only', 'all'): |
|---|
| 170 | if st == 'all': |
|---|
| 171 | chatstate = helpers.get_uf_chatstate(chatstate) |
|---|
| 172 | else: # 'composing_only' |
|---|
| 173 | if chatstate in ('composing', 'paused'): |
|---|
| 174 | # only print composing, paused |
|---|
| 175 | chatstate = helpers.get_uf_chatstate(chatstate) |
|---|
| 176 | else: |
|---|
| 177 | chatstate = '' |
|---|
| 178 | label_text = \ |
|---|
| 179 | '<span weight="heavy" size="x-large">%s</span> %s' % (name, chatstate) |
|---|
| 180 | else: |
|---|
| 181 | label_text = '<span weight="heavy" size="x-large">%s</span>' % name |
|---|
| 182 | |
|---|
| 183 | if status is not None: |
|---|
| 184 | label_text += '\n%s' % status |
|---|
| 185 | |
|---|
| 186 | # setup the label that holds name and jid |
|---|
| 187 | banner_name_label.set_markup(label_text) |
|---|
| 188 | self.paint_banner(jid) |
|---|
| 189 | |
|---|
| 190 | def set_avatar(self, vcard): |
|---|
| 191 | if not vcard.has_key('PHOTO'): |
|---|
| 192 | return |
|---|
| 193 | if type(vcard['PHOTO']) != type({}): |
|---|
| 194 | return |
|---|
| 195 | img_decoded = None |
|---|
| 196 | if vcard['PHOTO'].has_key('BINVAL'): |
|---|
| 197 | try: |
|---|
| 198 | img_decoded = base64.decodestring(vcard['PHOTO']['BINVAL']) |
|---|
| 199 | except: |
|---|
| 200 | pass |
|---|
| 201 | elif vcard['PHOTO'].has_key('EXTVAL'): |
|---|
| 202 | url = vcard['PHOTO']['EXTVAL'] |
|---|
| 203 | try: |
|---|
| 204 | fd = urllib.urlopen(url) |
|---|
| 205 | img_decoded = fd.read() |
|---|
| 206 | except: |
|---|
| 207 | pass |
|---|
| 208 | if img_decoded: |
|---|
| 209 | pixbufloader = gtk.gdk.PixbufLoader() |
|---|
| 210 | try: |
|---|
| 211 | pixbufloader.write(img_decoded) |
|---|
| 212 | pixbuf = pixbufloader.get_pixbuf() |
|---|
| 213 | pixbufloader.close() |
|---|
| 214 | |
|---|
| 215 | scaled_buf = pixbuf.scale_simple(52, 52, gtk.gdk.INTERP_HYPER) |
|---|
| 216 | x = None |
|---|
| 217 | if self.xmls.has_key(vcard['jid']): |
|---|
| 218 | x = self.xmls[vcard['jid']] |
|---|
| 219 | # it can be xmls[jid/resource] if it's a vcard from pm |
|---|
| 220 | elif self.xmls.has_key(vcard['jid'] + '/' + vcard['resource']): |
|---|
| 221 | x = self.xmls[vcard['jid'] + '/' + vcard['resource']] |
|---|
| 222 | image = x.get_widget('avatar_image') |
|---|
| 223 | image.set_from_pixbuf(scaled_buf) |
|---|
| 224 | image.show_all() |
|---|
| 225 | except gobject.GError: # we may get "unknown image format" |
|---|
| 226 | pass |
|---|
| 227 | |
|---|
| 228 | def set_state_image(self, jid): |
|---|
| 229 | prio = 0 |
|---|
| 230 | if gajim.contacts[self.account].has_key(jid): |
|---|
| 231 | contacts_list = gajim.contacts[self.account][jid] |
|---|
| 232 | else: |
|---|
| 233 | contacts_list = [self.contacts[jid]] |
|---|
| 234 | |
|---|
| 235 | user = contacts_list[0] |
|---|
| 236 | show = user.show |
|---|
| 237 | jid = user.jid |
|---|
| 238 | keyID = user.keyID |
|---|
| 239 | |
|---|
| 240 | for u in contacts_list: |
|---|
| 241 | if u.priority > prio: |
|---|
| 242 | prio = u.priority |
|---|
| 243 | show = u.show |
|---|
| 244 | keyID = u.keyID |
|---|
| 245 | child = self.childs[jid] |
|---|
| 246 | hb = self.notebook.get_tab_label(child).get_children()[0] |
|---|
| 247 | status_image = hb.get_children()[0] |
|---|
| 248 | state_images = self.plugin.roster.get_appropriate_state_images(jid) |
|---|
| 249 | image = state_images[show] |
|---|
| 250 | banner_status_image = self.xmls[jid].get_widget('banner_status_image') |
|---|
| 251 | |
|---|
| 252 | if keyID: |
|---|
| 253 | self.xmls[jid].get_widget('gpg_togglebutton').set_sensitive(True) |
|---|
| 254 | else: |
|---|
| 255 | self.xmls[jid].get_widget('gpg_togglebutton').set_sensitive(False) |
|---|
| 256 | |
|---|
| 257 | if image.get_storage_type() == gtk.IMAGE_ANIMATION: |
|---|
| 258 | banner_status_image.set_from_animation(image.get_animation()) |
|---|
| 259 | status_image.set_from_animation(image.get_animation()) |
|---|
| 260 | elif image.get_storage_type() == gtk.IMAGE_PIXBUF: |
|---|
| 261 | # make a copy because one will be scaled, one not (tab icon) |
|---|
| 262 | pix = image.get_pixbuf() |
|---|
| 263 | scaled_pix = pix.scale_simple(32, 32, gtk.gdk.INTERP_BILINEAR) |
|---|
| 264 | banner_status_image.set_from_pixbuf(scaled_pix) |
|---|
| 265 | status_image.set_from_pixbuf(pix) |
|---|
| 266 | |
|---|
| 267 | def on_tabbed_chat_window_delete_event(self, widget, event): |
|---|
| 268 | '''close window''' |
|---|
| 269 | for jid in self.contacts: |
|---|
| 270 | if time.time() - gajim.last_message_time[self.account][jid] < 2: |
|---|
| 271 | # 2 seconds |
|---|
| 272 | dialog = dialogs.ConfirmationDialog( |
|---|
| 273 | #%s is being replaced in the code with JID |
|---|
| 274 | _('You just received a new message from "%s"' % jid), |
|---|
| 275 | _('If you close the window, this message will be lost.')) |
|---|
| 276 | if dialog.get_response() != gtk.RESPONSE_OK: |
|---|
| 277 | return True #stop the propagation of the event |
|---|
| 278 | |
|---|
| 279 | if gajim.config.get('saveposition'): |
|---|
| 280 | # save the window size and position |
|---|
| 281 | x, y = self.window.get_position() |
|---|
| 282 | gajim.config.set('chat-x-position', x) |
|---|
| 283 | gajim.config.set('chat-y-position', y) |
|---|
| 284 | width, height = self.window.get_size() |
|---|
| 285 | gajim.config.set('chat-width', width) |
|---|
| 286 | gajim.config.set('chat-height', height) |
|---|
| 287 | |
|---|
| 288 | def on_tabbed_chat_window_destroy(self, widget): |
|---|
| 289 | #clean self.plugin.windows[self.account]['chats'] |
|---|
| 290 | chat.Chat.on_window_destroy(self, widget, 'chats') |
|---|
| 291 | |
|---|
| 292 | def on_tabbed_chat_window_focus_in_event(self, widget, event): |
|---|
| 293 | chat.Chat.on_chat_window_focus_in_event(self, widget, event) |
|---|
| 294 | # on focus in, send 'active' chatstate to current tab |
|---|
| 295 | self.send_chatstate('active') |
|---|
| 296 | |
|---|
| 297 | def on_tabbed_chat_window_focus_out_event(self, widget, event): |
|---|
| 298 | '''catch focus out and minimized and send inactive chatstate; |
|---|
| 299 | minimize action also focuses out first so it's catched here''' |
|---|
| 300 | window_state = widget.window.get_state() |
|---|
| 301 | if window_state is None: |
|---|
| 302 | return |
|---|
| 303 | |
|---|
| 304 | # focus-out is also emitted by showing context menu |
|---|
| 305 | # so check to see if we're really not paying attention to window/tab |
|---|
| 306 | # NOTE: if the user changes tab, switch-tab sends inactive to the tab |
|---|
| 307 | # we are leaving so we just send to active tab here |
|---|
| 308 | if self.popup_is_shown is False: # we are outside of the window |
|---|
| 309 | # so no context menu, so send 'inactive' to active tab |
|---|
| 310 | self.send_chatstate('inactive') |
|---|
| 311 | |
|---|
| 312 | def on_chat_notebook_key_press_event(self, widget, event): |
|---|
| 313 | chat.Chat.on_chat_notebook_key_press_event(self, widget, event) |
|---|
| 314 | |
|---|
| 315 | def on_send_file_menuitem_activate(self, widget): |
|---|
| 316 | jid = self.get_active_jid() |
|---|
| 317 | contact = gajim.get_first_contact_instance_from_jid(self.account, jid) |
|---|
| 318 | self.plugin.windows['file_transfers'].show_file_send_request( |
|---|
| 319 | self.account, contact) |
|---|
| 320 | |
|---|
| 321 | def on_add_to_roster_menuitem_activate(self, widget): |
|---|
| 322 | jid = self.get_active_jid() |
|---|
| 323 | dialogs.AddNewContactWindow(self.plugin, self.account, jid) |
|---|
| 324 | |
|---|
| 325 | def on_send_button_clicked(self, widget): |
|---|
| 326 | """When send button is pressed: send the current message""" |
|---|
| 327 | jid = self.get_active_jid() |
|---|
| 328 | message_textview = self.xmls[jid].get_widget('message_textview') |
|---|
| 329 | message_buffer = message_textview.get_buffer() |
|---|
| 330 | start_iter = message_buffer.get_start_iter() |
|---|
| 331 | end_iter = message_buffer.get_end_iter() |
|---|
| 332 | message = message_buffer.get_text(start_iter, end_iter, 0) |
|---|
| 333 | |
|---|
| 334 | # send the message |
|---|
| 335 | self.send_message(message) |
|---|
| 336 | |
|---|
| 337 | message_buffer.set_text('') |
|---|
| 338 | |
|---|
| 339 | def remove_tab(self, jid): |
|---|
| 340 | if time.time() - gajim.last_message_time[self.account][jid] < 2: |
|---|
| 341 | dialog = dialogs.ConfirmationDialog( |
|---|
| 342 | _('You just received a new message from "%s"' % jid), |
|---|
| 343 | _('If you close this tab, the message will be lost.')) |
|---|
| 344 | if dialog.get_response() != gtk.RESPONSE_OK: |
|---|
| 345 | return |
|---|
| 346 | |
|---|
| 347 | # chatstates - tab is destroyed, send gone |
|---|
| 348 | self.send_chatstate('gone', jid) |
|---|
| 349 | |
|---|
| 350 | chat.Chat.remove_tab(self, jid, 'chats') |
|---|
| 351 | del self.contacts[jid] |
|---|
| 352 | |
|---|
| 353 | def new_user(self, contact): |
|---|
| 354 | '''when new tab is created''' |
|---|
| 355 | self.names[contact.jid] = contact.name |
|---|
| 356 | self.xmls[contact.jid] = gtk.glade.XML(GTKGUI_GLADE, 'chats_vbox', APP) |
|---|
| 357 | self.childs[contact.jid] = self.xmls[contact.jid].get_widget('chats_vbox') |
|---|
| 358 | self.contacts[contact.jid] = contact |
|---|
| 359 | |
|---|
| 360 | |
|---|
| 361 | self.childs[contact.jid].connect('drag_data_received', |
|---|
| 362 | self.on_drag_data_received, contact) |
|---|
| 363 | self.childs[contact.jid].drag_dest_set( gtk.DEST_DEFAULT_MOTION | |
|---|
| 364 | gtk.DEST_DEFAULT_HIGHLIGHT | gtk.DEST_DEFAULT_DROP, |
|---|
| 365 | self.dnd_list, gtk.gdk.ACTION_COPY) |
|---|
| 366 | |
|---|
| 367 | message_textview = self.xmls[contact.jid].get_widget('message_textview') |
|---|
| 368 | message_tv_buffer = message_textview.get_buffer() |
|---|
| 369 | message_tv_buffer.connect('insert-text', |
|---|
| 370 | self.on_message_tv_buffer_insert_text, contact.jid) |
|---|
| 371 | |
|---|
| 372 | if contact.jid in gajim.encrypted_chats[self.account]: |
|---|
| 373 | self.xmls[contact.jid].get_widget('gpg_togglebutton').set_active(True) |
|---|
| 374 | |
|---|
| 375 | xm = gtk.glade.XML(GTKGUI_GLADE, 'tabbed_chat_popup_menu', APP) |
|---|
| 376 | xm.signal_autoconnect(self) |
|---|
| 377 | self.tabbed_chat_popup_menu = xm.get_widget('tabbed_chat_popup_menu') |
|---|
| 378 | |
|---|
| 379 | chat.Chat.new_tab(self, contact.jid) |
|---|
| 380 | self.redraw_tab(contact.jid) |
|---|
| 381 | self.draw_widgets(contact) |
|---|
| 382 | |
|---|
| 383 | # restore previous conversation |
|---|
| 384 | self.restore_conversation(contact.jid) |
|---|
| 385 | |
|---|
| 386 | # print queued messages |
|---|
| 387 | if gajim.awaiting_messages[self.account].has_key(contact.jid): |
|---|
| 388 | self.read_queue(contact.jid) |
|---|
| 389 | |
|---|
| 390 | gajim.connections[self.account].request_vcard(contact.jid) |
|---|
| 391 | self.childs[contact.jid].show_all() |
|---|
| 392 | |
|---|
| 393 | # chatstates |
|---|
| 394 | self.reset_kbd_mouse_timeout_vars() |
|---|
| 395 | |
|---|
| 396 | self.chatstates[contact.jid] = None # OUR current chatstate with contact |
|---|
| 397 | self.possible_paused_timeout_id[contact.jid] =\ |
|---|
| 398 | gobject.timeout_add(5000, self.check_for_possible_paused_chatstate, |
|---|
| 399 | contact.jid) |
|---|
| 400 | self.possible_inactive_timeout_id[contact.jid] =\ |
|---|
| 401 | gobject.timeout_add(30000, self.check_for_possible_inactive_chatstate, |
|---|
| 402 | contact.jid) |
|---|
| 403 | |
|---|
| 404 | def handle_incoming_chatstate(self, account, jid, chatstate): |
|---|
| 405 | ''' handle incoming chatstate that jid SENT TO us ''' |
|---|
| 406 | contact = gajim.get_first_contact_instance_from_jid(account, jid) |
|---|
| 407 | self.draw_name_banner(contact, chatstate) |
|---|
| 408 | |
|---|
| 409 | def check_for_possible_paused_chatstate(self, jid): |
|---|
| 410 | ''' did we move mouse of that window or wrote something in message textview |
|---|
| 411 | in the last 5 seconds? |
|---|
| 412 | if yes we go active for mouse, composing for kbd |
|---|
| 413 | if no we go paused if we were previously composing ''' |
|---|
| 414 | if jid not in self.xmls: |
|---|
| 415 | # the tab with jid is no longer open. stop timer |
|---|
| 416 | return False # stop looping |
|---|
| 417 | current_state = self.chatstates[jid] |
|---|
| 418 | if current_state is False: # jid doesn't support chatstates |
|---|
| 419 | return False # stop looping |
|---|
| 420 | |
|---|
| 421 | if self.mouse_over_in_last_5_secs: |
|---|
| 422 | self.send_chatstate('active', jid) |
|---|
| 423 | elif self.kbd_activity_in_last_5_secs: |
|---|
| 424 | self.send_chatstate('composing', jid) |
|---|
| 425 | else: |
|---|
| 426 | if self.chatstates[jid] == 'composing': |
|---|
| 427 | self.send_chatstate('paused', jid) # pause composing |
|---|
| 428 | |
|---|
| 429 | # assume no activity and let the motion-notify or 'insert-text' make them True |
|---|
| 430 | # refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds! |
|---|
| 431 | self.reset_kbd_mouse_timeout_vars() |
|---|
| 432 | return True # loop forever |
|---|
| 433 | |
|---|
| 434 | def check_for_possible_inactive_chatstate(self, jid): |
|---|
| 435 | ''' did we move mouse over that window or wrote something in message textview |
|---|
| 436 | in the last 30 seconds? |
|---|
| 437 | if yes we go active |
|---|
| 438 | if no we go inactive ''' |
|---|
| 439 | if jid not in self.xmls: |
|---|
| 440 | # the tab with jid is no longer open. stop timer |
|---|
| 441 | return False # stop looping |
|---|
| 442 | |
|---|
| 443 | current_state = self.chatstates[jid] |
|---|
| 444 | if current_state is False: # jid doesn't support chatstates |
|---|
| 445 | return False # stop looping |
|---|
| 446 | |
|---|
| 447 | if self.mouse_over_in_last_5_secs or self.kbd_activity_in_last_5_secs: |
|---|
| 448 | return True # loop forever |
|---|
| 449 | |
|---|
| 450 | if not (self.mouse_over_in_last_30_secs or\ |
|---|
| 451 | self.kbd_activity_in_last_30_secs): |
|---|
| 452 | self.send_chatstate('inactive', jid) |
|---|
| 453 | |
|---|
| 454 | # assume no activity and let the motion-notify or 'insert-text' make them True |
|---|
| 455 | # refresh 30 seconds too or else it's 30 - 5 = 25 seconds! |
|---|
| 456 | self.reset_kbd_mouse_timeout_vars() |
|---|
| 457 | |
|---|
| 458 | return True # loop forever |
|---|
| 459 | |
|---|
| 460 | def on_message_tv_buffer_insert_text(self, textbuffer, textiter, text, |
|---|
| 461 | length, jid): |
|---|
| 462 | self.kbd_activity_in_last_5_secs = True |
|---|
| 463 | self.kbd_activity_in_last_30_secs = True |
|---|
| 464 | self.send_chatstate('composing', jid) |
|---|
| 465 | |
|---|
| 466 | def reset_kbd_mouse_timeout_vars(self): |
|---|
| 467 | self.kbd_activity_in_last_5_secs = False |
|---|
| 468 | self.mouse_over_in_last_5_secs = False |
|---|
| 469 | self.mouse_over_in_last_30_secs = False |
|---|
| 470 | self.kbd_activity_in_last_30_secs = False |
|---|
| 471 | |
|---|
| 472 | def on_message_textview_key_press_event(self, widget, event): |
|---|
| 473 | """When a key is pressed: |
|---|
| 474 | if enter is pressed without the shift key, message (if not empty) is sent |
|---|
| 475 | and printed in the conversation""" |
|---|
| 476 | |
|---|
| 477 | jid = self.get_active_jid() |
|---|
| 478 | conversation_textview = widget |
|---|
| 479 | message_buffer = conversation_textview.get_buffer() |
|---|
| 480 | start_iter, end_iter = message_buffer.get_bounds() |
|---|
| 481 | message = message_buffer.get_text(start_iter, end_iter, False) |
|---|
| 482 | |
|---|
| 483 | if event.keyval == gtk.keysyms.ISO_Left_Tab: # SHIFT + TAB |
|---|
| 484 | if event.state & gtk.gdk.CONTROL_MASK: # CTRL + SHIFT + TAB |
|---|
| 485 | self.notebook.emit('key_press_event', event) |
|---|
| 486 | if event.keyval == gtk.keysyms.Tab: |
|---|
| 487 | if event.state & gtk.gdk.CONTROL_MASK: # CTRL + TAB |
|---|
| 488 | self.notebook.emit('key_press_event', event) |
|---|
| 489 | elif event.keyval == gtk.keysyms.Page_Down: # PAGE DOWN |
|---|
| 490 | if event.state & gtk.gdk.CONTROL_MASK: # CTRL + PAGE DOWN |
|---|
| 491 | self.notebook.emit('key_press_event', event) |
|---|
| 492 | elif event.state & gtk.gdk.SHIFT_MASK: # SHIFT + PAGE DOWN |
|---|
| 493 | conversation_textview.emit('key_press_event', event) |
|---|
| 494 | elif event.keyval == gtk.keysyms.Page_Up: # PAGE UP |
|---|
| 495 | if event.state & gtk.gdk.CONTROL_MASK: # CTRL + PAGE UP |
|---|
| 496 | self.notebook.emit('key_press_event', event) |
|---|
| 497 | elif event.state & gtk.gdk.SHIFT_MASK: # SHIFT + PAGE UP |
|---|
| 498 | conversation_textview.emit('key_press_event', event) |
|---|
| 499 | elif event.keyval == gtk.keysyms.Up: |
|---|
| 500 | if event.state & gtk.gdk.CONTROL_MASK: #Ctrl+UP |
|---|
| 501 | self.sent_messages_scroll(jid, 'up', widget.get_buffer()) |
|---|
| 502 | return True # override the default gtk+ thing for ctrl+up |
|---|
| 503 | elif event.keyval == gtk.keysyms.Down: |
|---|
| 504 | if event.state & gtk.gdk.CONTROL_MASK: #Ctrl+Down |
|---|
| 505 | self.sent_messages_scroll(jid, 'down', widget.get_buffer()) |
|---|
| 506 | return True # override the default gtk+ thing for ctrl+down |
|---|
| 507 | elif event.keyval == gtk.keysyms.Return or \ |
|---|
| 508 | event.keyval == gtk.keysyms.KP_Enter: # ENTER |
|---|
| 509 | if gajim.config.get('send_on_ctrl_enter'): |
|---|
| 510 | if not (event.state & gtk.gdk.CONTROL_MASK): |
|---|
| 511 | return False |
|---|
| 512 | elif (event.state & gtk.gdk.SHIFT_MASK): |
|---|
| 513 | return False |
|---|
| 514 | if gajim.connections[self.account].connected < 2: #we are not connected |
|---|
| 515 | dialogs.ErrorDialog(_('A connection is not available'), |
|---|
| 516 | _('Your message can not be sent until you are connected.')).get_response() |
|---|
| 517 | return True |
|---|
| 518 | |
|---|
| 519 | # send the message |
|---|
| 520 | self.send_message(message) |
|---|
| 521 | |
|---|
| 522 | message_buffer.set_text('') |
|---|
| 523 | return True |
|---|
| 524 | |
|---|
| 525 | def send_chatstate(self, state, jid = None): |
|---|
| 526 | ''' sends OUR chatstate as STANDLONE chat state message (eg. no body) |
|---|
| 527 | to jid only if new chatstate is different |
|---|
| 528 | from the previous one |
|---|
| 529 | if jid is not specified, send to active tab''' |
|---|
| 530 | # JEP 85 does not allow resending the same chatstate |
|---|
| 531 | # this function checks for that and just returns so it's safe to call it |
|---|
| 532 | # with same state. |
|---|
| 533 | |
|---|
| 534 | # This functions also checks for violation in state transitions |
|---|
| 535 | # and raises RuntimeException with appropriate message |
|---|
| 536 | # more on that http://www.jabber.org/jeps/jep-0085.html#statechart |
|---|
| 537 | |
|---|
| 538 | # do not send nothing if we have chat state notifications disabled |
|---|
| 539 | # that means we won't reply to the <active/> from other peer |
|---|
| 540 | # so we do not broadcast jep85 capabalities |
|---|
| 541 | if gajim.config.get('chat_state_notifications') == 'disabled': |
|---|
| 542 | return |
|---|
| 543 | |
|---|
| 544 | if jid is None: |
|---|
| 545 | jid = self.get_active_jid() |
|---|
| 546 | |
|---|
| 547 | contact = gajim.get_first_contact_instance_from_jid(self.account, jid) |
|---|
| 548 | |
|---|
| 549 | if contact is None: |
|---|
| 550 | # contact was from pm in MUC, and left the room so contact is None |
|---|
| 551 | # so we cannot send chatstate anymore |
|---|
| 552 | return |
|---|
| 553 | |
|---|
| 554 | if contact.chatstate is False: # jid cannot do jep85 |
|---|
| 555 | return |
|---|
| 556 | |
|---|
| 557 | # if the new state we wanna send (state) equals |
|---|
| 558 | # the current state (contact.chastate) then return |
|---|
| 559 | if contact.chatstate == state: |
|---|
| 560 | return |
|---|
| 561 | |
|---|
| 562 | if contact.chatstate is None: |
|---|
| 563 | # we don't know anything about jid, so return |
|---|
| 564 | # NOTE: |
|---|
| 565 | # send 'active', set current state to 'ask' and return is done |
|---|
| 566 | # in self.send_message() because we need REAL message (with <body>) |
|---|
| 567 | # for that procedure so return to make sure we send only once 'active' |
|---|
| 568 | # until we know peer supports jep85 |
|---|
| 569 | return |
|---|
| 570 | |
|---|
| 571 | if contact.chatstate == 'ask': |
|---|
| 572 | return |
|---|
| 573 | |
|---|
| 574 | # prevent going paused if we we were not composing (JEP violation) |
|---|
| 575 | if state == 'paused' and not contact.chatstate == 'composing': |
|---|
| 576 | gajim.connections[self.account].send_message(jid, None, None, |
|---|
| 577 | chatstate = 'active') # go active before |
|---|
| 578 | contact.chatstate = 'active' |
|---|
| 579 | self.reset_kbd_mouse_timeout_vars() |
|---|
| 580 | |
|---|
| 581 | # if we're inactive prevent composing (JEP violation) |
|---|
| 582 | if contact.chatstate == 'inactive' and state == 'composing': |
|---|
| 583 | gajim.connections[self.account].send_message(jid, None, None, |
|---|
| 584 | chatstate = 'active') # go active before |
|---|
| 585 | contact.chatstate = 'active' |
|---|
| 586 | self.reset_kbd_mouse_timeout_vars() |
|---|
| 587 | |
|---|
| 588 | gajim.connections[self.account].send_message(jid, None, None, |
|---|
| 589 | chatstate = state) |
|---|
| 590 | contact.chatstate = state |
|---|
| 591 | if contact.chatstate == 'active': |
|---|
| 592 | self.reset_kbd_mouse_timeout_vars() |
|---|
| 593 | |
|---|
| 594 | def send_message(self, message): |
|---|
| 595 | """Send the given message to the active tab""" |
|---|
| 596 | if not message: |
|---|
| 597 | return |
|---|
| 598 | |
|---|
| 599 | jid = self.get_active_jid() |
|---|
| 600 | contact = gajim.get_first_contact_instance_from_jid(self.account, jid) |
|---|
| 601 | if contact is None: |
|---|
| 602 | # contact was from pm in MUC, and left the room, or we left the room |
|---|
| 603 | room, nick = gajim.get_room_and_nick_from_fjid(jid) |
|---|
| 604 | dialogs.ErrorDialog(_('Sending private message failed'), |
|---|
| 605 | #in second %s code replaces with nickname |
|---|
| 606 | _('You are no longer in room "%s" or "%s" has left.') % \ |
|---|
| 607 | (room, nick)) |
|---|