root/branches/gajim_0.9.1/src/tabbed_chat_window.py

Revision 4811, 34.4 kB (checked in by nicfit, 3 years ago)

Show no JEP-85 status in the chat banner when the contact is offline; closes #1202

  • Property svn:eol-style set to LF
Line 
1##      tabbed_chat_window.py
2##
3## Contributors for this file:
4##      - Yann Le Boulanger <asterix@lagaule.org>
5##      - Nikos Kouremenos <kourem@gmail.com>
6##      - Travis Shirk <travis@pobox.com>
7##
8## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
9##                         Vincent Hanquez <tab@snarc.org>
10## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
11##                    Vincent Hanquez <tab@snarc.org>
12##                    Nikos Kouremenos <nkour@jabber.org>
13##                    Dimitur Kirov <dkirov@gmail.com>
14##                    Travis Shirk <travis@pobox.com>
15##                    Norman Rasmussen <norman@rasmussen.co.za>
16##
17## This program is free software; you can redistribute it and/or modify
18## it under the terms of the GNU General Public License as published
19## by the Free Software Foundation; version 2 only.
20##
21## This program is distributed in the hope that it will be useful,
22## but WITHOUT ANY WARRANTY; without even the implied warranty of
23## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24## GNU General Public License for more details.
25##
26
27import gtk
28import gtk.glade
29import pango
30import gobject
31import time
32import os
33
34import dialogs
35import chat
36import gtkgui_helpers
37
38from common import gajim
39from common import helpers
40from common.logger import Constants
41constants = Constants()
42
43from common import i18n
44
45_ = i18n._
46APP = i18n.APP
47gtk.glade.bindtextdomain(APP, i18n.DIR)
48gtk.glade.textdomain(APP)
49
50GTKGUI_GLADE = 'gtkgui.glade'
51
52class TabbedChatWindow(chat.Chat):
53        '''Class for tabbed chat window'''
54        def __init__(self, contact, account):
55                # we check that on opening new windows
56                self.always_compact_view = gajim.config.get('always_compact_view_chat')
57                chat.Chat.__init__(self, account, 'tabbed_chat_window')
58                self.contacts = {}
59                # keep check for possible paused timeouts per jid
60                self.possible_paused_timeout_id = {}
61                # keep check for possible inactive timeouts per jid
62                self.possible_inactive_timeout_id = {}
63               
64                # keep timeout id and window obj for possible big avatar
65                # it is on enter-notify and leave-notify so no need to be per jid
66                self.show_bigger_avatar_timeout_id = None
67                self.bigger_avatar_window = None
68               
69                # list that holds all the jid we have asked vcard once
70                # (so we do not have to ask again)
71                self.jids_for_which_we_asked_vcard_already = list()
72               
73                self.TARGET_TYPE_URI_LIST = 80
74                self.dnd_list = [ ( 'text/uri-list', 0, self.TARGET_TYPE_URI_LIST ) ]
75                self.new_tab(contact)
76                self.show_title()
77       
78                # NOTE: if it not a window event, do not connect here (new_tab() autoconnects)
79                signal_dict = {
80'on_tabbed_chat_window_destroy': self.on_tabbed_chat_window_destroy,
81'on_tabbed_chat_window_delete_event': self.on_tabbed_chat_window_delete_event,
82'on_tabbed_chat_window_focus_in_event': self.on_tabbed_chat_window_focus_in_event,
83'on_chat_notebook_key_press_event': self.on_chat_notebook_key_press_event,
84'on_chat_notebook_switch_page': self.on_chat_notebook_switch_page, # in chat.py
85'on_tabbed_chat_window_motion_notify_event': self.on_tabbed_chat_window_motion_notify_event,
86                }
87
88                self.xml.signal_autoconnect(signal_dict)
89
90
91                if gajim.config.get('saveposition') and \
92                        not gtkgui_helpers.one_window_opened('chats'):
93                        # get window position and size from config
94                        gtkgui_helpers.move_window(self.window, gajim.config.get('chat-x-position'),
95                                gajim.config.get('chat-y-position'))
96                        gtkgui_helpers.resize_window(self.window, gajim.config.get('chat-width'),
97                                        gajim.config.get('chat-height'))
98
99                # gtk+ doesn't make use of the motion notify on gtkwindow by default
100                # so this line adds that
101                self.window.set_events(gtk.gdk.POINTER_MOTION_MASK)
102               
103                self.window.show_all()
104       
105        def save_var(self, jid):
106                '''return the specific variable of a jid, like gpg_enabled
107                the return value have to be compatible with wthe one given to load_var'''
108                gpg_enabled = self.xmls[jid].get_widget('gpg_togglebutton').get_active()
109                return {'gpg_enabled': gpg_enabled}
110       
111        def load_var(self, jid, var):
112                if not self.xmls.has_key(jid):
113                        return
114                self.xmls[jid].get_widget('gpg_togglebutton').set_active(
115                        var['gpg_enabled'])
116               
117        def on_tabbed_chat_window_motion_notify_event(self, widget, event):
118                '''it gets called no matter if it is the active window or not'''
119                if widget.get_property('has-toplevel-focus'):
120                        # change chatstate only if window is the active one
121                        self.mouse_over_in_last_5_secs = True
122                        self.mouse_over_in_last_30_secs = True
123
124        def on_drag_data_received(self, widget, context, x, y, selection,
125                target_type, timestamp, contact):
126                # If not resource, we can't send file
127                if not contact.resource:
128                        return
129                if target_type == self.TARGET_TYPE_URI_LIST:
130                        uri = selection.data.strip()
131                        uri_splitted = uri.split() # we may have more than one file dropped
132                        for uri in uri_splitted:
133                                path = helpers.get_file_path_from_dnd_dropped_uri(uri)
134                                if os.path.isfile(path): # is it file?
135                                        gajim.interface.instances['file_transfers'].send_file(self.account,
136                                                contact, path)
137
138        def on_avatar_eventbox_enter_notify_event(self, widget, event):
139                '''we enter the eventbox area so we under conditions add a timeout
140                to show a bigger avatar after 0.5 sec'''
141                jid = self.get_active_jid()
142                real_jid = gajim.get_real_jid_from_fjid(self.account, jid)
143                if not real_jid: # this can happend if we're in a moderate room
144                        return
145                avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(real_jid)
146                if avatar_pixbuf in ('ask', None):
147                        return
148                avatar_w = avatar_pixbuf.get_width()
149                avatar_h = avatar_pixbuf.get_height()
150               
151                scaled_buf = self.xmls[jid].get_widget('avatar_image').get_pixbuf()
152                scaled_buf_w = scaled_buf.get_width()
153                scaled_buf_h = scaled_buf.get_height()
154               
155                # do we have something bigger to show?
156                if avatar_w > scaled_buf_w or avatar_h > scaled_buf_h:
157                        # wait for 0.5 sec in case we leave earlier
158                        self.show_bigger_avatar_timeout_id = gobject.timeout_add(500,
159                                self.show_bigger_avatar, widget)
160               
161        def on_avatar_eventbox_leave_notify_event(self, widget, event):
162                '''we left the eventbox area that holds the avatar img'''
163                # did we add a timeout? if yes remove it
164                if self.show_bigger_avatar_timeout_id is not None:
165                        gobject.source_remove(self.show_bigger_avatar_timeout_id)
166
167        def show_bigger_avatar(self, small_avatar):
168                '''resizes the avatar, if needed, so it has at max half the screen size
169                and shows it'''
170                jid = self.get_active_jid()
171                real_jid = gajim.get_real_jid_from_fjid(self.account, jid)
172                if not real_jid: # this can happend if we're in a moderate room
173                        return
174                avatar_pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(real_jid)
175                screen_w = gtk.gdk.screen_width()
176                screen_h = gtk.gdk.screen_height()
177                avatar_w = avatar_pixbuf.get_width()
178                avatar_h = avatar_pixbuf.get_height()
179                half_scr_w = screen_w / 2
180                half_scr_h = screen_h / 2
181                if avatar_w > half_scr_w:
182                        avatar_w = half_scr_w
183                if avatar_h > half_scr_h:
184                        avatar_h = half_scr_h
185                window = gtk.Window(gtk.WINDOW_POPUP)
186                self.bigger_avatar_window = window
187                pixmap, mask = avatar_pixbuf.render_pixmap_and_mask()
188                window.set_size_request(avatar_w, avatar_h)
189                # we should make the cursor visible
190                # gtk+ doesn't make use of the motion notify on gtkwindow by default
191                # so this line adds that
192                window.set_events(gtk.gdk.POINTER_MOTION_MASK)
193                window.set_app_paintable(True)
194               
195                window.realize()
196                window.window.set_back_pixmap(pixmap, False) # make it transparent
197                window.window.shape_combine_mask(mask, 0, 0)
198
199                # make the bigger avatar window show up centered
200                x0, y0 = small_avatar.window.get_origin()
201                x0 += small_avatar.allocation.x
202                y0 += small_avatar.allocation.y
203                center_x= x0 + (small_avatar.allocation.width / 2)
204                center_y = y0 + (small_avatar.allocation.height / 2)
205                pos_x, pos_y = center_x - (avatar_w / 2), center_y - (avatar_h / 2) 
206                window.move(pos_x, pos_y)
207                # make the cursor invisible so we can see the image
208                invisible_cursor = gtkgui_helpers.get_invisible_cursor()
209                window.window.set_cursor(invisible_cursor)
210
211                # we should hide the window
212                window.connect('leave_notify_event',
213                        self.on_window_avatar_leave_notify_event)
214                window.connect('motion-notify-event',
215                        self.on_window_motion_notify_event)
216
217                window.show_all()
218
219        def on_window_avatar_leave_notify_event(self, widget, event):
220                '''we just left the popup window that holds avatar'''
221                self.bigger_avatar_window.destroy()
222
223        def on_window_motion_notify_event(self, widget, event):
224                '''we just moved the mouse so show the cursor'''
225                cursor = gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)
226                self.bigger_avatar_window.window.set_cursor(cursor)
227
228        def draw_widgets(self, contact):
229                '''draw the widgets in a tab (f.e. gpg togglebutton)
230                according to the the information in the contact variable'''
231                jid = contact.jid
232                self.set_state_image(jid)
233                tb = self.xmls[jid].get_widget('gpg_togglebutton')
234                if contact.keyID: # we can do gpg
235                        tb.set_sensitive(True)
236                        tt = _('OpenPGP Encryption')
237                else:
238                        tb.set_sensitive(False)
239                        #we talk about a contact here
240                        tt = _('%s has not broadcasted an OpenPGP key nor you have assigned one') % contact.name
241                tip = gtk.Tooltips()
242                tip.set_tip(self.xmls[jid].get_widget('gpg_eventbox'), tt)
243
244                # add the fat line at the top
245                self.draw_name_banner(contact)
246
247        def draw_name_banner(self, contact, chatstate = None):
248                '''Draw the fat line at the top of the window that
249                houses the status icon, name, jid, and avatar'''
250                # this is the text for the big brown bar
251                # some chars need to be escaped..
252                jid = contact.jid
253                banner_name_label = self.xmls[jid].get_widget('banner_name_label')
254               
255                name = gtkgui_helpers.escape_for_pango_markup(contact.name)
256               
257                status = contact.status
258
259                if status is not None:
260                        banner_name_label.set_ellipsize(pango.ELLIPSIZE_END)
261                        status = gtkgui_helpers.reduce_chars_newlines(status, 0, 2)
262
263                status = gtkgui_helpers.escape_for_pango_markup(status)
264
265                #FIXME: uncomment me when we support sending messages to specific resource
266                # composing full jid
267                #fulljid = jid
268                #if self.contacts[jid].resource:
269                #       fulljid += '/' + self.contacts[jid].resource
270                #label_text = '<span weight="heavy" size="x-large">%s</span>\n%s' \
271                #       % (name, fulljid)
272               
273               
274                st = gajim.config.get('chat_state_notifications')
275                if contact.chatstate and st in ('composing_only', 'all'):
276                        if contact.show == 'offline':
277                                chatstate = ''
278                        elif st == 'all':
279                                chatstate = helpers.get_uf_chatstate(contact.chatstate)
280                        else: # 'composing_only'
281                                if chatstate in ('composing', 'paused'):
282                                        # only print composing, paused
283                                        chatstate = helpers.get_uf_chatstate(contact.chatstate)
284                                else:
285                                        chatstate = ''
286                        label_text = \
287                        '<span weight="heavy" size="x-large">%s</span> %s' % (name, chatstate)
288                else:
289                        label_text = '<span weight="heavy" size="x-large">%s</span>' % name
290               
291                if status is not None:
292                        label_text += '\n%s' % status
293
294                # setup the label that holds name and jid
295                banner_name_label.set_markup(label_text)
296                self.paint_banner(jid)
297
298        def get_specific_unread(self, jid):
299                # return the number of unread (private) msgs with contacts in the room
300                # when gc, and that is 0 in tc
301                # FIXME: maybe refactor so this func is not called at all if TC?
302                return 0
303
304        def show_avatar(self, jid, resource):
305                # Get the XML instance
306                jid_with_resource = jid
307                if resource:
308                        jid_with_resource += '/' + resource
309
310                xml = None
311                if self.xmls.has_key(jid):
312                        xml = self.xmls[jid]
313                else:
314                        # it can be xmls[jid/resource] if it's a vcard from pm
315                        if self.xmls.has_key(jid_with_resource):
316                                xml = self.xmls[jid_with_resource]
317                if not xml:
318                        return
319               
320                # we assume contact has no avatar
321                scaled_pixbuf = None
322
323                real_jid = gajim.get_real_jid_from_fjid(self.account, jid)
324                pixbuf = None
325                if real_jid:
326                        pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(real_jid)
327                if not real_jid or pixbuf == 'ask':
328                        # we don't have the vcard or it's pm and we don't have the real jid
329                        gajim.connections[self.account].request_vcard(jid_with_resource)
330                        return
331                if pixbuf is not None:
332                        scaled_pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'chat')
333                       
334
335                image = xml.get_widget('avatar_image')
336                image.set_from_pixbuf(scaled_pixbuf)
337                image.show_all()
338
339        def set_state_image(self, jid):
340                prio = 0
341                if gajim.contacts[self.account].has_key(jid):
342                        contacts_list = gajim.contacts[self.account][jid]
343                else:
344                        contacts_list = [self.contacts[jid]]
345
346                contact = contacts_list[0]
347                show = contact.show
348                jid = contact.jid
349                keyID = contact.keyID
350
351                for u in contacts_list:
352                        if u.priority > prio:
353                                prio = u.priority
354                                show = u.show
355                                keyID = u.keyID
356                child = self.childs[jid]
357                hb = self.notebook.get_tab_label(child).get_children()[0]
358                status_image = hb.get_children()[0]
359               
360                state_images_32 = gajim.interface.roster.get_appropriate_state_images(jid,
361                        size = '32')
362                state_images_16 = gajim.interface.roster.get_appropriate_state_images(jid)
363
364                # Set banner image
365                if state_images_32.has_key(show) and state_images_32[show].get_pixbuf():
366                        # we have 32x32! use it!
367                        banner_image = state_images_32[show]
368                        use_size_32 = True
369                else:
370                        banner_image = state_images_16[show]
371                        use_size_32 = False
372
373                banner_status_image = self.xmls[jid].get_widget('banner_status_image')
374                if banner_image.get_storage_type() == gtk.IMAGE_ANIMATION:
375                        banner_status_image.set_from_animation(banner_image.get_animation())
376                else:
377                        pix = banner_image.get_pixbuf()
378                        if use_size_32:
379                                banner_status_image.set_from_pixbuf(pix)
380                        else: # we need to scale 16x16 to 32x32
381                                scaled_pix = pix.scale_simple(32, 32, gtk.gdk.INTERP_BILINEAR)
382                                banner_status_image.set_from_pixbuf(scaled_pix)
383
384                # Set tab image (always 16x16); unread messages show the 'message' image
385                if self.nb_unread[jid] and gajim.config.get('show_unread_tab_icon'):
386                        tab_image = state_images_16['message']
387                else:
388                        tab_image = state_images_16[show]
389                if tab_image.get_storage_type() == gtk.IMAGE_ANIMATION:
390                        status_image.set_from_animation(tab_image.get_animation())
391                else:
392                        status_image.set_from_pixbuf(tab_image.get_pixbuf())
393
394                if keyID:
395                        self.xmls[jid].get_widget('gpg_togglebutton').set_sensitive(True)
396                else:
397                        self.xmls[jid].get_widget('gpg_togglebutton').set_sensitive(False)
398
399        def on_tabbed_chat_window_delete_event(self, widget, event):
400                '''close window'''
401                for jid in self.contacts:
402                        if time.time() - gajim.last_message_time[self.account][jid] < 2:
403                                # 2 seconds
404                                dialog = dialogs.ConfirmationDialog(
405                                        #%s is being replaced in the code with JID
406                                        _('You just received a new message from "%s"' % jid),
407                                        _('If you close this tab and you have history disabled, this message will be lost.'))
408                                if dialog.get_response() != gtk.RESPONSE_OK:
409                                        return True #stop the propagation of the event
410
411                if gajim.config.get('saveposition'):
412                        # save the window size and position
413                        x, y = self.window.get_position()
414                        gajim.config.set('chat-x-position', x)
415                        gajim.config.set('chat-y-position', y)
416                        width, height = self.window.get_size()
417                        gajim.config.set('chat-width', width)
418                        gajim.config.set('chat-height', height)
419
420        def on_tabbed_chat_window_destroy(self, widget):
421                # Reset contact chatstates to all open tabs
422                for jid in self.xmls:
423                        self.send_chatstate('gone', jid)
424                        self.contacts[jid].chatstate = None
425                        self.contacts[jid].our_chatstate = None
426                #clean gajim.interface.instances[self.account]['chats']
427                chat.Chat.on_window_destroy(self, widget, 'chats')
428
429        def on_tabbed_chat_window_focus_in_event(self, widget, event):
430                chat.Chat.on_chat_window_focus_in_event(self, widget, event)
431                # on focus in, send 'active' chatstate to current tab
432                self.send_chatstate('active')
433               
434        def on_chat_notebook_key_press_event(self, widget, event):
435                chat.Chat.on_chat_notebook_key_press_event(self, widget, event)
436
437        def on_send_file_menuitem_activate(self, widget):
438                jid = self.get_active_jid()
439                contact = gajim.get_first_contact_instance_from_jid(self.account, jid)
440                gajim.interface.instances['file_transfers'].show_file_send_request( 
441                        self.account, contact)
442
443        def on_add_to_roster_menuitem_activate(self, widget):
444                jid = self.get_active_jid()
445                dialogs.AddNewContactWindow(self.account, jid)
446
447        def on_send_button_clicked(self, widget):
448                '''When send button is pressed: send the current message'''
449                jid = self.get_active_jid()
450                message_textview = self.message_textviews[jid]
451                message_buffer = message_textview.get_buffer()
452                start_iter = message_buffer.get_start_iter()
453                end_iter = message_buffer.get_end_iter()
454                message = message_buffer.get_text(start_iter, end_iter, 0).decode('utf-8')
455
456                # send the message
457                self.send_message(message)
458
459        def remove_tab(self, jid):
460                if time.time() - gajim.last_message_time[self.account][jid] < 2:
461                        dialog = dialogs.ConfirmationDialog(
462                                _('You just received a new message from "%s"' % jid),
463                                _('If you close this tab and you have history disabled, the message will be lost.'))
464                        if dialog.get_response() != gtk.RESPONSE_OK:
465                                return
466
467                # chatstates - tab is destroyed, send gone and reset
468                self.send_chatstate('gone', jid)
469                self.contacts[jid].chatstate = None
470                self.contacts[jid].our_chatstate = None
471               
472                chat.Chat.remove_tab(self, jid, 'chats')
473                del self.contacts[jid]
474
475        def new_tab(self, contact):
476                '''when new tab is created'''
477                self.names[contact.jid] = contact.name
478                self.xmls[contact.jid] = gtk.glade.XML(GTKGUI_GLADE, 'chats_vbox', APP)
479                self.childs[contact.jid] = self.xmls[contact.jid].get_widget('chats_vbox')
480                self.contacts[contact.jid] = contact
481
482                self.show_avatar(contact.jid, contact.resource)                 
483
484                self.childs[contact.jid].connect('drag_data_received',
485                        self.on_drag_data_received, contact)
486                self.childs[contact.jid].drag_dest_set( gtk.DEST_DEFAULT_MOTION |
487                        gtk.DEST_DEFAULT_HIGHLIGHT | gtk.DEST_DEFAULT_DROP,
488                        self.dnd_list, gtk.gdk.ACTION_COPY)
489
490                chat.Chat.new_tab(self, contact.jid)
491
492                msg_textview = self.message_textviews[contact.jid]
493                message_tv_buffer = msg_textview.get_buffer()
494                message_tv_buffer.connect('changed',
495                        self.on_message_tv_buffer_changed, contact)
496
497                if contact.jid in gajim.encrypted_chats[self.account]:
498                        self.xmls[contact.jid].get_widget('gpg_togglebutton').set_active(True)
499
500                xm = gtk.glade.XML(GTKGUI_GLADE, 'tabbed_chat_popup_menu', APP)
501                xm.signal_autoconnect(self)
502                self.tabbed_chat_popup_menu = xm.get_widget('tabbed_chat_popup_menu')
503
504                self.redraw_tab(contact.jid)
505                self.draw_widgets(contact)
506
507                # restore previous conversation
508                self.restore_conversation(contact.jid)
509
510                if gajim.awaiting_events[self.account].has_key(contact.jid):
511                        self.read_queue(contact.jid)
512
513                self.childs[contact.jid].show_all()
514
515                # chatstates
516                self.reset_kbd_mouse_timeout_vars()
517
518                self.possible_paused_timeout_id[contact.jid] = gobject.timeout_add(
519                        5000, self.check_for_possible_paused_chatstate, contact.jid)
520                self.possible_inactive_timeout_id[contact.jid] = gobject.timeout_add(
521                        30000, self.check_for_possible_inactive_chatstate, contact.jid)
522
523        def handle_incoming_chatstate(self, account, contact):
524                ''' handle incoming chatstate that jid SENT TO us '''
525                self.draw_name_banner(contact, contact.chatstate)
526                # update chatstate in tab for this chat
527                self.redraw_tab(contact.jid, contact.chatstate)
528
529        def check_for_possible_paused_chatstate(self, jid):
530                ''' did we move mouse of that window or write something in message
531                textview
532                in the last 5 seconds?
533                if yes we go active for mouse, composing for kbd
534                if no we go paused if we were previously composing '''
535                contact = gajim.get_first_contact_instance_from_jid(self.account, jid)
536                if jid not in self.xmls or contact is None:
537                        # the tab with jid is no longer open or contact left
538                        # stop timer
539                        return False # stop looping
540
541                current_state = contact.our_chatstate
542                if current_state is False: # jid doesn't support chatstates
543                        return False # stop looping
544
545                message_textview = self.message_textviews[jid]
546                message_buffer = message_textview.get_buffer()
547                if self.kbd_activity_in_last_5_secs and message_buffer.get_char_count():
548                        # Only composing if the keyboard activity was in text entry
549                        self.send_chatstate('composing', jid)
550                elif self.mouse_over_in_last_5_secs and jid == self.get_active_jid():
551                        self.send_chatstate('active', jid)
552                else:
553                        if current_state == 'composing':
554                                self.send_chatstate('paused', jid) # pause composing
555
556                # assume no activity and let the motion-notify or 'insert-text' make them True
557                # refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds!
558                self.reset_kbd_mouse_timeout_vars()
559                return True # loop forever
560
561        def check_for_possible_inactive_chatstate(self, jid):
562                ''' did we move mouse over that window or wrote something in message
563                textview
564                in the last 30 seconds?
565                if yes we go active
566                if no we go inactive '''
567                contact = gajim.get_first_contact_instance_from_jid(self.account, jid)
568                if jid not in self.xmls or contact is None:
569                        # the tab with jid is no longer open or contact left
570                        return False # stop looping
571
572                current_state = contact.our_chatstate
573                if current_state is False: # jid doesn't support chatstates
574                        return False # stop looping
575
576                if self.mouse_over_in_last_5_secs or self.kbd_activity_in_last_5_secs:
577                        return True # loop forever
578
579                if not (self.mouse_over_in_last_30_secs or\
580                        self.kbd_activity_in_last_30_secs):
581                        self.send_chatstate('inactive', jid)
582
583                # assume no activity and let the motion-notify or 'insert-text' make them True
584                # refresh 30 seconds too or else it's 30 - 5 = 25 seconds!
585                self.reset_kbd_mouse_timeout_vars()
586
587                return True # loop forever
588
589        def on_message_tv_buffer_changed(self, textbuffer, contact):
590                self.kbd_activity_in_last_5_secs = True
591                self.kbd_activity_in_last_30_secs = True
592                if textbuffer.get_char_count():
593                        self.send_chatstate('composing', contact.jid)
594                else:
595                        self.send_chatstate('active', contact.jid)
596
597        def reset_kbd_mouse_timeout_vars(self):
598                self.kbd_activity_in_last_5_secs = False
599                self.mouse_over_in_last_5_secs = False
600                self.mouse_over_in_last_30_secs = False
601                self.kbd_activity_in_last_30_secs = False
602
603        def on_message_textview_mykeypress_event(self, widget, event_keyval,
604        event_keymod):
605                '''When a key is pressed:
606                if enter is pressed without the shift key, message (if not empty) is sent
607                and printed in the conversation'''
608                # NOTE: handles mykeypress which is custom signal connected to this
609                # CB in new_tab(). for this singal see message_textview.py
610                jid = self.get_active_jid()
611                conv_textview = self.conversation_textviews[jid]
612                message_textview = widget
613                message_buffer = message_textview.get_buffer()
614                start_iter, end_iter = message_buffer.get_bounds()
615                message = message_buffer.get_text(start_iter, end_iter, False).decode(
616                        'utf-8')
617
618                # construct event instance from binding
619                event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) # it's always a key-press here
620                event.keyval = event_keyval
621                event.state = event_keymod
622                event.time = 0 # assign current time
623
624                if event.keyval == gtk.keysyms.ISO_Left_Tab: # SHIFT + TAB
625                        if event.state & gtk.gdk.CONTROL_MASK: # CTRL + SHIFT + TAB
626                                self.notebook.emit('key_press_event', event)
627                if event.keyval == gtk<