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

Revision 4847, 40.4 kB (checked in by nk, 3 years ago)

doing pychecker from once in a while, hurts noone

  • Property svn:eol-style set to LF
Line 
1##      chat.py
2##
3## Contributors for this file:
4##      - Yann Le Boulanger <asterix@lagaule.org>
5##      - Nikos Kouremenos <kourem@gmail.com>
6##
7## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
8##                         Vincent Hanquez <tab@snarc.org>
9## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
10##                    Vincent Hanquez <tab@snarc.org>
11##                    Nikos Kouremenos <nkour@jabber.org>
12##                    Dimitur Kirov <dkirov@gmail.com>
13##                    Travis Shirk <travis@pobox.com>
14##                    Norman Rasmussen <norman@rasmussen.co.za>
15##
16## This program is free software; you can redistribute it and/or modify
17## it under the terms of the GNU General Public License as published
18## by the Free Software Foundation; version 2 only.
19##
20## This program is distributed in the hope that it will be useful,
21## but WITHOUT ANY WARRANTY; without even the implied warranty of
22## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23## GNU General Public License for more details.
24##
25
26import gtk
27import gtk.glade
28import pango
29import gobject
30import time
31import math
32import os
33
34import dialogs
35import history_window
36import gtkgui_helpers
37import tooltips
38import conversation_textview
39import message_textview
40
41try:
42        import gtkspell
43        HAS_GTK_SPELL = True
44except:
45        HAS_GTK_SPELL = False
46
47from common import gajim
48from common import helpers
49from common import i18n
50
51_ = i18n._
52APP = i18n.APP
53gtk.glade.bindtextdomain(APP, i18n.DIR)
54gtk.glade.textdomain(APP)
55
56GTKGUI_GLADE = 'gtkgui.glade'
57
58class Chat:
59        '''Class for chat/groupchat windows'''
60        def __init__(self, account, widget_name):
61                self.xml = gtk.glade.XML(GTKGUI_GLADE, widget_name, APP)
62                self.window = self.xml.get_widget(widget_name)
63
64                self.widget_name = widget_name
65
66                self.account = account
67                self.change_cursor = None
68                self.xmls = {}
69                self.conversation_textviews = {} # holds per jid conversation textview
70                self.message_textviews = {} # holds per jid message (where we write) textview
71                self.nb_unread = {}
72                self.print_time_timeout_id = {}
73                self.names = {} # what is printed in the tab (eg. contact.name)
74                self.childs = {} # holds the contents for every tab (VBox)
75
76                # the following vars are used to keep history of user's messages
77                self.sent_history = {}
78                self.sent_history_pos = {}
79                self.typing_new = {}
80                self.orig_msg = {}
81
82                # alignment before notebook (to control top padding for when showing tabs)
83                self.alignment = self.xml.get_widget('alignment')
84               
85                # notebook customizations
86                self.notebook = self.xml.get_widget('chat_notebook')
87                # Remove the page that is in glade
88                self.notebook.remove_page(0)
89                pref_pos = gajim.config.get('tabs_position')
90                if pref_pos != 'top':
91                        if pref_pos == 'bottom':
92                                nb_pos = gtk.POS_BOTTOM
93                        elif pref_pos == 'left':
94                                nb_pos = gtk.POS_LEFT
95                        elif pref_pos == 'right':
96                                nb_pos = gtk.POS_RIGHT
97                        else:
98                                nb_pos = gtk.POS_TOP
99                else:
100                        nb_pos = gtk.POS_TOP
101                self.notebook.set_tab_pos(nb_pos)
102                if gajim.config.get('tabs_always_visible'):
103                        self.notebook.set_show_tabs(True)
104                        self.alignment.set_property('top-padding', 2)
105                else:
106                        self.notebook.set_show_tabs(False)
107                self.notebook.set_show_border(gajim.config.get('tabs_border'))
108
109                if gajim.config.get('useemoticons'):
110                        self.emoticons_menu = self.prepare_emoticons_menu()
111
112                # muc attention states (when we are mentioned in a muc)
113                # if the room jid is in the list, the room has mentioned us
114                self.muc_attentions = []
115
116        def toggle_emoticons(self):
117                '''hide show emoticons_button and make sure emoticons_menu is always there
118                when needed'''
119                if gajim.config.get('useemoticons'):
120                        self.emoticons_menu = self.prepare_emoticons_menu()
121               
122                for jid in self.xmls:
123                        emoticons_button = self.xmls[jid].get_widget('emoticons_button')
124                        if gajim.config.get('useemoticons'):
125                                emoticons_button.show()
126                                emoticons_button.set_no_show_all(False)
127                        else:
128                                emoticons_button.hide()
129                                emoticons_button.set_no_show_all(True)
130
131        def update_font(self):
132                font = pango.FontDescription(gajim.config.get('conversation_font'))
133                for jid in self.xmls:
134                        self.conversation_textviews[jid].modify_font(font)
135                        msg_textview = self.message_textviews[jid]
136                        msg_textview.modify_font(font)
137
138        def update_tags(self):
139                for jid in self.conversation_textviews:
140                        self.conversation_textviews[jid].update_tags()
141
142        def update_print_time(self):
143                if gajim.config.get('print_time') != 'sometimes':
144                        list_jid = self.print_time_timeout_id.keys()
145                        for jid in list_jid:
146                                gobject.source_remove(self.print_time_timeout_id[jid])
147                                del self.print_time_timeout_id[jid]
148                else:
149                        for jid in self.xmls:
150                                if not self.print_time_timeout_id.has_key(jid):
151                                        self.print_time_timeout(jid)
152                                        self.print_time_timeout_id[jid] = gobject.timeout_add(300000,
153                                                self.print_time_timeout, jid)
154
155        def print_time_timeout(self, jid):
156                if not jid in self.xmls.keys():
157                        return False
158                if gajim.config.get('print_time') == 'sometimes':
159                        conv_textview = self.conversation_textviews[jid]
160                        buffer = conv_textview.get_buffer()
161                        end_iter = buffer.get_end_iter()
162                        tim = time.localtime()
163                        tim_format = time.strftime('%H:%M', tim)
164                        buffer.insert_with_tags_by_name(end_iter, '\n' + tim_format,
165                                'time_sometimes')
166                        # scroll to the end of the textview
167                        if conv_textview.at_the_end():
168                                # we are at the end
169                                conv_textview.scroll_to_end()
170                        return True # loop again
171                if self.print_time_timeout_id.has_key(jid):
172                        del self.print_time_timeout_id[jid]
173                return False
174
175        def show_title(self, urgent = True):
176                '''redraw the window's title'''
177                unread = 0
178                for jid in self.nb_unread:
179                        unread += self.nb_unread[jid]
180                start = ''
181                if unread > 1:
182                        start = '[' + unicode(unread) + '] '
183                elif unread == 1:
184                        start = '* '
185                if len(self.xmls) > 1: # if more than one tab in the same window
186                        if self.widget_name == 'tabbed_chat_window':
187                                add = _('Chat')
188                        elif self.widget_name == 'groupchat_window':
189                                add = _('Group Chat')
190                elif len(self.xmls) == 1: # just one tab
191                        if self.widget_name == 'tabbed_chat_window':
192                                c = gajim.get_first_contact_instance_from_jid(self.account, jid)
193                                if c is None:
194                                        add = ''
195                                else:
196                                        add = c.name
197                        elif self.widget_name == 'groupchat_window':
198                                name = gajim.get_nick_from_jid(jid)
199                                add = name
200
201                title = start + add
202                if len(gajim.connections) >= 2: # if we have 2 or more accounts
203                        title += ' (' + _('account: ') + self.account + ')'
204
205                self.window.set_title(title)
206                if urgent:
207                        gtkgui_helpers.set_unset_urgency_hint(self.window, unread)
208
209        def redraw_tab(self, jid, chatstate = None):
210                '''redraw the label of the tab
211                if chatstate is given that means we have HE SENT US a chatstate'''
212                # Update status images
213                self.set_state_image(jid)
214                       
215                unread = ''
216                num_unread = 0
217                child = self.childs[jid]
218                hb = self.notebook.get_tab_label(child).get_children()[0]
219                if self.widget_name == 'tabbed_chat_window':
220                        nickname = hb.get_children()[1]
221                        close_button = hb.get_children()[2]
222
223                        num_unread = self.nb_unread[jid]
224                        if num_unread == 1 and not gajim.config.get('show_unread_tab_icon'):
225                                unread = '*'
226                        elif num_unread > 1:
227                                unread = '[' + unicode(num_unread) + ']'
228
229                        # Draw tab label using chatstate
230                        theme = gajim.config.get('roster_theme')
231                        color = None
232                        if chatstate is not None:
233                                if chatstate == 'composing':
234                                        color = gajim.config.get_per('themes', theme,
235                                                'state_composing_color')
236                                elif chatstate == 'inactive':
237                                        color = gajim.config.get_per('themes', theme,
238                                                'state_inactive_color')
239                                elif chatstate == 'gone':
240                                        color = gajim.config.get_per('themes', theme, 'state_gone_color')
241                                elif chatstate == 'paused':
242                                        color = gajim.config.get_per('themes', theme, 'state_paused_color')
243                                else:
244                                        color = gajim.config.get_per('themes', theme, 'state_active_color')
245                        if color:
246                                color = gtk.gdk.colormap_get_system().alloc_color(color)
247                                # We set the color for when it's the current tab or not
248                                nickname.modify_fg(gtk.STATE_NORMAL, color)
249                                if chatstate in ('inactive', 'gone'):
250                                        # Adjust color to be lighter against the darker inactive
251                                        # background
252                                        p = 0.4
253                                        mask = 0
254                                        color.red = int((color.red * p) + (mask * (1 - p)))
255                                        color.green = int((color.green * p) + (mask * (1 - p)))
256                                        color.blue = int((color.blue * p) + (mask * (1 - p)))
257                                nickname.modify_fg(gtk.STATE_ACTIVE, color)
258                elif self.widget_name == 'groupchat_window':
259                        nickname = hb.get_children()[0]
260                        close_button = hb.get_children()[1]
261
262                        has_focus = self.window.get_property('has-toplevel-focus')
263                        current_tab = (self.notebook.page_num(child) == self.notebook.get_current_page())
264                        color = None
265                        theme = gajim.config.get('roster_theme')
266                        if chatstate == 'attention' and (not has_focus or not current_tab):
267                                if jid not in self.muc_attentions:
268                                        self.muc_attentions.append(jid)
269                                color = gajim.config.get_per('themes', theme, 'state_muc_directed_msg')
270                        elif chatstate:
271                                if chatstate == 'active' or (current_tab and has_focus):
272                                        if jid in self.muc_attentions:
273                                                self.muc_attentions.remove(jid)
274                                        color = gajim.config.get_per('themes', theme, 'state_active_color')
275                                elif chatstate == 'newmsg' and (not has_focus or not current_tab) and\
276                                     jid not in self.muc_attentions:
277                                        color = gajim.config.get_per('themes', theme, 'state_muc_msg')
278                        if color:
279                                color = gtk.gdk.colormap_get_system().alloc_color(color)
280                                # The widget state depend on whether this tab is the "current" tab
281                                if current_tab:
282                                        nickname.modify_fg(gtk.STATE_NORMAL, color)
283                                else:
284                                        nickname.modify_fg(gtk.STATE_ACTIVE, color)
285
286                if gajim.config.get('tabs_close_button'):
287                        close_button.show()
288                else:
289                        close_button.hide()
290
291                nickname.set_max_width_chars(10)
292                lbl = self.names[jid]
293                if num_unread: # if unread, text in the label becomes bold
294                        lbl = '<b>' + unread + lbl + '</b>'
295                nickname.set_markup(lbl)
296
297        def get_message_type(self, jid):
298                if self.widget_name == 'groupchat_window':
299                        return 'gc'
300                if gajim.contacts[self.account].has_key(jid):
301                        return 'chat'
302                return 'pm'
303
304        def get_nth_jid(self, page_number = None):
305                notebook = self.notebook
306                if page_number == None:
307                        page_number = notebook.get_current_page()
308                nth_child = notebook.get_nth_page(page_number)
309                nth_jid = ''
310                for jid in self.xmls:
311                        if self.childs[jid] == nth_child:
312                                nth_jid = jid
313                                break
314                return nth_jid
315
316        def move_to_next_unread_tab(self, forward):
317                ind = self.notebook.get_current_page()
318                current = ind
319                found = False
320                # loop until finding an unread tab or having done a complete cycle
321                while True: 
322                        if forward == True: # look for the first unread tab on the right
323                                ind = ind + 1
324                                if ind >= self.notebook.get_n_pages():
325                                        ind = 0
326                        else: # look for the first unread tab on the right
327                                ind = ind - 1
328                                if ind < 0:
329                                        ind = self.notebook.get_n_pages() - 1
330                        if ind == current:
331                                break # a complete cycle without finding an unread tab
332                        jid = self.get_nth_jid(ind)
333                        if self.nb_unread[jid] > 0:
334                                found = True
335                                break # found
336                if found:
337                        self.notebook.set_current_page(ind)
338                else: # not found
339                        if forward: # CTRL + TAB
340                                if current < (self.notebook.get_n_pages() - 1):
341                                        self.notebook.next_page()
342                                else: # traverse for ever (eg. don't stop at last tab)
343                                        self.notebook.set_current_page(0)
344                        else: # CTRL + SHIFT + TAB
345                                if current > 0:
346                                        self.notebook.prev_page()
347                                else: # traverse for ever (eg. don't stop at first tab)
348                                        self.notebook.set_current_page(
349                                                self.notebook.get_n_pages() - 1)
350               
351        def on_window_destroy(self, widget, kind): #kind is 'chats' or 'gc'
352                '''clean gajim.interface.instances[self.account][kind]'''
353                for jid in self.xmls:
354                        windows = gajim.interface.instances[self.account][kind]
355                        if kind == 'chats':
356                                # send 'gone' chatstate to every tabbed chat tab
357                                windows[jid].send_chatstate('gone', jid)
358                                gobject.source_remove(self.possible_paused_timeout_id[jid])
359                                gobject.source_remove(self.possible_inactive_timeout_id[jid])
360                        if gajim.interface.systray_enabled and self.nb_unread[jid] > 0:
361                                gajim.interface.systray.remove_jid(jid, self.account,
362                                        self.get_message_type(jid))
363                        del windows[jid]
364                        if self.print_time_timeout_id.has_key(jid):
365                                gobject.source_remove(self.print_time_timeout_id[jid])
366                if windows.has_key('tabbed'):
367                        del windows['tabbed']
368
369        def get_active_jid(self):
370                return self.get_nth_jid()
371
372        def on_close_button_clicked(self, button, jid):
373                '''When close button is pressed: close a tab'''
374                self.remove_tab(jid)
375
376        def on_history_menuitem_clicked(self, widget = None, jid = None):
377                '''When history menuitem is pressed: call history window'''
378                if jid is None: # None when viewing room and tc history
379                        jid = self.get_active_jid()
380                if gajim.interface.instances['logs'].has_key(jid):
381                        gajim.interface.instances['logs'][jid].window.present()
382                else:
383                        gajim.interface.instances['logs'][jid] = history_window.HistoryWindow(
384                                jid, self.account)
385
386        def on_chat_window_focus_in_event(self, widget, event):
387                '''When window gets focus'''
388                jid = self.get_active_jid()
389               
390                textview = self.conversation_textviews[jid]
391                if textview.at_the_end():
392                        #we are at the end
393                        if self.nb_unread[jid] > 0:
394                                self.nb_unread[jid] = 0 + self.get_specific_unread(jid)
395                                self.show_title()
396                                if gajim.interface.systray_enabled:
397                                        gajim.interface.systray.remove_jid(jid, self.account,
398                                                self.get_message_type(jid))
399               
400                '''TC/GC window received focus, so if we had urgency REMOVE IT
401                NOTE: we do not have to read the message (it maybe in a bg tab)
402                to remove urgency hint so this functions does that'''
403                if gtk.gtk_version >= (2, 8, 0) and gtk.pygtk_version >= (2, 8, 0):
404                        if widget.props.urgency_hint:
405                                widget.props.urgency_hint = False
406                # Undo "unread" state display, etc.
407                if self.widget_name == 'groupchat_window':
408                        self.redraw_tab(jid, 'active')
409                else:
410                        # NOTE: we do not send any chatstate to preserve inactive, gone, etc.
411                        self.redraw_tab(jid)
412       
413        def on_compact_view_menuitem_activate(self, widget):
414                isactive = widget.get_active()
415                self.set_compact_view(isactive)
416
417        def on_actions_button_clicked(self, widget):
418                '''popup action menu'''
419                #FIXME: BUG http://bugs.gnome.org/show_bug.cgi?id=316786
420                self.button_clicked = widget
421               
422                menu = self.prepare_context_menu()
423                menu.show_all()
424                menu.popup(None, None, self.position_menu_under_button, 1, 0)
425
426        def on_emoticons_button_clicked(self, widget):
427                '''popup emoticons menu'''
428                #FIXME: BUG http://bugs.gnome.org/show_bug.cgi?id=316786
429                self.button_clicked = widget
430                self.emoticons_menu.popup(None, None, self.position_menu_under_button, 1, 0)
431
432        def position_menu_under_button(self, menu):
433                #FIXME: BUG http://bugs.gnome.org/show_bug.cgi?id=316786
434                # pass btn instance when this bug is over
435                button = self.button_clicked
436                # here I get the coordinates of the button relative to
437                # window (self.window)
438                button_x, button_y = button.allocation.x, button.allocation.y
439               
440                # now convert them to X11-relative
441                window_x, window_y = self.window.window.get_origin()
442                x = window_x + button_x
443                y = window_y + button_y
444
445                menu_width, menu_height = menu.size_request()
446
447                ## should we pop down or up?
448                if (y + button.allocation.height + menu_height
449                    < gtk.gdk.screen_height()):
450                        # now move the menu below the button
451                        y += button.allocation.height
452                else:
453                        # now move the menu above the button
454                        y -= menu_height
455
456
457                # push_in is True so all the menuitems are always inside screen
458                push_in = True
459                return (x, y, push_in)
460
461        def remove_possible_switch_to_menuitems(self, menu):
462                ''' remove duplicate 'Switch to' if they exist and return clean menu'''
463                childs = menu.get_children()
464
465                if self.widget_name == 'tabbed_chat_window':
466                        jid = self.get_active_jid()
467                        c = gajim.get_first_contact_instance_from_jid(self.account, jid)
468                        if _('not in the roster') in c.groups: # for add_to_roster_menuitem
469                                childs[5].show()
470                                childs[5].set_no_show_all(False)
471                        else:
472                                childs[5].hide()
473                                childs[5].set_no_show_all(True)
474                       
475                        start_removing_from = 6 # this is from the seperator and after
476                       
477                else:
478                        start_removing_from = 7 # # this is from the seperator and after
479                               
480                for child in childs[start_removing_from:]:
481                        menu.remove(child)
482
483                return menu
484       
485        def prepare_context_menu(self):
486                '''sets compact view menuitem active state
487                sets active and sensitivity state for toggle_gpg_menuitem
488                and remove possible 'Switch to' menuitems'''
489                if self.widget_name == 'groupchat_window':
490                        menu = self.gc_popup_menu
491                        childs = menu.get_children()
492                        # compact_view_menuitem
493                        childs[5].set_active(self.compact_view_current_state)
494                elif self.widget_name == 'tabbed_chat_window':
495                        menu = self.tabbed_chat_popup_menu
496                        childs = menu.get_children()
497                        # check if gpg capabitlies or else make gpg toggle insensitive
498                        jid = self.get_active_jid()
499                        gpg_btn = self.xmls[jid].get_widget('gpg_togglebutton')
500                        isactive = gpg_btn.get_active()
501                        issensitive = gpg_btn.get_property('sensitive')
502                        childs[3].set_active(isactive)
503                        childs[3].set_property('sensitive', issensitive)
504                        # If we don't have resource, we can't do file transfert
505                        c = gajim.get_first_contact_instance_from_jid(self.account, jid)
506                        if not c.resource:
507                                childs[2].set_sensitive(False)
508                        else:
509                                childs[2].set_sensitive(True)
510                        # compact_view_menuitem
511                        childs[4].set_active(self.compact_view_current_state)
512                menu = self.remove_possible_switch_to_menuitems(menu)
513               
514                return menu
515
516        def prepare_emoticons_menu(self):
517                menu = gtk.Menu()
518       
519                def append_emoticon(w, d):
520                        jid = self.get_active_jid()
521                        message_textview = self.message_textviews[jid]
522                        buffer = message_textview.get_buffer()
523                        if buffer.get_char_count():
524                                buffer.insert_at_cursor(' %s ' % d)
525                        else: # we are the beginning of buffer
526                                buffer.insert_at_cursor('%s ' % d)
527                        message_textview.grab_focus()
528       
529                counter = 0
530                # Calculate the side lenght of the popup to make it a square
531                size = int(round(math.sqrt(len(gajim.interface.emoticons_images))))
532                for image in gajim.interface.emoticons_images:
533                        item = gtk.MenuItem()
534                        img = gtk.Image()
535                        if type(image[1]) == gtk.gdk.PixbufAnimation:
536                                img.set_from_animation(image[1])
537                        else:
538                                img.set_from_pixbuf(image[1])
539                        item.add(img)
540                        item.connect('activate', append_emoticon, image[0])
541                        #FIXME: add tooltip with ascii
542                        menu.attach(item,
543                                        counter % size, counter % size + 1,
544                                        counter / size, counter / size + 1)
545                        counter += 1
546                menu.show_all()
547                return menu
548
549        def popup_menu(self, event):
550                menu = self.prepare_context_menu()
551                # common menuitems (tab switches)
552                if len(self.xmls) > 1: # if there is more than one tab
553                        menu.append(gtk.SeparatorMenuItem()) # seperator
554                        for jid in self.xmls:
555                                if jid != self.get_active_jid():
556                                        item = gtk.ImageMenuItem(_('Switch to %s') % self.names[jid])
557                                        img = gtk.image_new_from_stock(gtk.STOCK_JUMP_TO,
558                                                gtk.ICON_SIZE_MENU)
559                                        item.set_image(img)
560                                        item.connect('activate', lambda obj, jid:self.set_active_tab(
561                                                jid), jid)
562                                        menu.append(item)
563
564                # show the menu
565                menu.popup(None, None, None, event.button, event.time)
566                menu.show_all()
567
568        def on_banner_eventbox_button_press_event(self, widget, event):
569                '''If right-clicked, show popup'''
570                if event.button == 3: # right click
571                        self.popup_menu(event)
572
573        def on_chat_notebook_switch_page(self, notebook, page, page_num):
574                # get the index of the page and then the page that we're leaving
575                old_no = notebook.get_current_page()
576                old_child = notebook.get_nth_page(old_no)
577               
578                new_child = notebook.get_nth_page(page_num)
579               
580                old_jid = ''
581                new_jid = ''
582                for jid in self.xmls:
583                        if self.childs[jid] == new_child:
584                                new_jid = jid
585                        elif self.childs[jid] == old_child:
586                                old_jid = jid
587                       
588                        if old_jid != '' and new_jid != '': # we found both jids
589                                break # so stop looping
590               
591                if self.widget_name == 'tabbed_chat_window':
592                        # send chatstate inactive to the one we're leaving
593                        # and active to the one we visit
594