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

Revision 4776, 39.5 kB (checked in by asterix, 3 years ago)

set policy to never when needed to height is good

  • 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                chat = self.names[jid]
186                if len(self.xmls) > 1: # if more than one tab in the same window
187                        if self.widget_name == 'tabbed_chat_window':
188                                add = _('Chat')
189                        elif self.widget_name == 'groupchat_window':
190                                add = _('Group Chat')
191                elif len(self.xmls) == 1: # just one tab
192                        if self.widget_name == 'tabbed_chat_window':
193                                c = gajim.get_first_contact_instance_from_jid(self.account, jid)
194                                if c is None:
195                                        add = ''
196                                else:
197                                        add = c.name
198                        elif self.widget_name == 'groupchat_window':
199                                name = gajim.get_nick_from_jid(jid)
200                                add = name
201
202                title = start + add
203                if len(gajim.connections) >= 2: # if we have 2 or more accounts
204                        title += ' (' + _('account: ') + self.account + ')'
205
206                self.window.set_title(title)
207                if urgent:
208                        gtkgui_helpers.set_unset_urgency_hint(self.window, unread)
209
210        def redraw_tab(self, jid, chatstate = None):
211                '''redraw the label of the tab
212                if chatstate is given that means we have HE SENT US a chatstate'''
213                # Update status images
214                self.set_state_image(jid)
215                       
216                unread = ''
217                num_unread = 0
218                child = self.childs[jid]
219                hb = self.notebook.get_tab_label(child).get_children()[0]
220                if self.widget_name == 'tabbed_chat_window':
221                        nickname = hb.get_children()[1]
222                        close_button = hb.get_children()[2]
223
224                        num_unread = self.nb_unread[jid]
225                        if num_unread == 1 and not gajim.config.get('show_unread_tab_icon'):
226                                unread = '*'
227                        elif num_unread > 1:
228                                unread = '[' + unicode(num_unread) + ']'
229
230                        # Draw tab label using chatstate
231                        theme = gajim.config.get('roster_theme')
232                        color = None
233                        if chatstate is not None:
234                                if chatstate == 'composing':
235                                        color = gajim.config.get_per('themes', theme,
236                                                'state_composing_color')
237                                elif chatstate == 'inactive':
238                                        color = gajim.config.get_per('themes', theme,
239                                                'state_inactive_color')
240                                elif chatstate == 'gone':
241                                        color = gajim.config.get_per('themes', theme, 'state_gone_color')
242                                elif chatstate == 'paused':
243                                        color = gajim.config.get_per('themes', theme, 'state_paused_color')
244                                else:
245                                        color = gajim.config.get_per('themes', theme, 'state_active_color')
246                        if color:
247                                color = gtk.gdk.colormap_get_system().alloc_color(color)
248                                # We set the color for when it's the current tab or not
249                                nickname.modify_fg(gtk.STATE_NORMAL, color)
250                                if chatstate in ('inactive', 'gone'):
251                                        # Adjust color to be lighter against the darker inactive
252                                        # background
253                                        p = 0.4
254                                        mask = 0
255                                        color.red = int((color.red * p) + (mask * (1 - p)))
256                                        color.green = int((color.green * p) + (mask * (1 - p)))
257                                        color.blue = int((color.blue * p) + (mask * (1 - p)))
258                                nickname.modify_fg(gtk.STATE_ACTIVE, color)
259                elif self.widget_name == 'groupchat_window':
260                        nickname = hb.get_children()[0]
261                        close_button = hb.get_children()[1]
262
263                        has_focus = self.window.get_property('has-toplevel-focus')
264                        current_tab = (self.notebook.page_num(child) == self.notebook.get_current_page())
265                        color = None
266                        theme = gajim.config.get('roster_theme')
267                        if chatstate == 'attention' and (not has_focus or not current_tab):
268                                if jid not in self.muc_attentions:
269                                        self.muc_attentions.append(jid)
270                                color = gajim.config.get_per('themes', theme, 'state_muc_directed_msg')
271                        elif chatstate:
272                                if chatstate == 'active' or (current_tab and has_focus):
273                                        if jid in self.muc_attentions:
274                                                self.muc_attentions.remove(jid)
275                                        color = gajim.config.get_per('themes', theme, 'state_active_color')
276                                elif chatstate == 'newmsg' and (not has_focus or not current_tab) and\
277                                     jid not in self.muc_attentions:
278                                        color = gajim.config.get_per('themes', theme, 'state_muc_msg')
279                        if color:
280                                color = gtk.gdk.colormap_get_system().alloc_color(color)
281                                # The widget state depend on whether this tab is the "current" tab
282                                if current_tab:
283                                        nickname.modify_fg(gtk.STATE_NORMAL, color)
284                                else:
285                                        nickname.modify_fg(gtk.STATE_ACTIVE, color)
286
287                if gajim.config.get('tabs_close_button'):
288                        close_button.show()
289                else:
290                        close_button.hide()
291
292                nickname.set_max_width_chars(10)
293                lbl = self.names[jid]
294                if num_unread: # if unread, text in the label becomes bold
295                        lbl = '<b>' + unread + lbl + '</b>'
296                nickname.set_markup(lbl)
297
298        def get_message_type(self, jid):
299                if self.widget_name == 'groupchat_window':
300                        return 'gc'
301                if gajim.contacts[self.account].has_key(jid):
302                        return 'chat'
303                return 'pm'
304
305        def on_window_destroy(self, widget, kind): #kind is 'chats' or 'gc'
306                '''clean gajim.interface.instances[self.account][kind]'''
307                for jid in self.xmls:
308                        windows = gajim.interface.instances[self.account][kind]
309                        if kind == 'chats':
310                                # send 'gone' chatstate to every tabbed chat tab
311                                windows[jid].send_chatstate('gone', jid)
312                                gobject.source_remove(self.possible_paused_timeout_id[jid])
313                                gobject.source_remove(self.possible_inactive_timeout_id[jid])
314                        if gajim.interface.systray_enabled and self.nb_unread[jid] > 0:
315                                gajim.interface.systray.remove_jid(jid, self.account,
316                                        self.get_message_type(jid))
317                        del windows[jid]
318                        if self.print_time_timeout_id.has_key(jid):
319                                gobject.source_remove(self.print_time_timeout_id[jid])
320                if windows.has_key('tabbed'):
321                        del windows['tabbed']
322
323        def get_active_jid(self):
324                notebook = self.notebook
325                active_child = notebook.get_nth_page(notebook.get_current_page())
326                active_jid = ''
327                for jid in self.xmls:
328                        if self.childs[jid] == active_child:
329                                active_jid = jid
330                                break
331                return active_jid
332
333        def on_close_button_clicked(self, button, jid):
334                '''When close button is pressed: close a tab'''
335                self.remove_tab(jid)
336
337        def on_history_menuitem_clicked(self, widget = None, jid = None):
338                '''When history menuitem is pressed: call history window'''
339                if jid is None: # None when viewing room and tc history
340                        jid = self.get_active_jid()
341                if gajim.interface.instances['logs'].has_key(jid):
342                        gajim.interface.instances['logs'][jid].window.present()
343                else:
344                        gajim.interface.instances['logs'][jid] = history_window.HistoryWindow(
345                                jid, self.account)
346
347        def on_chat_window_focus_in_event(self, widget, event):
348                '''When window gets focus'''
349                jid = self.get_active_jid()
350               
351                textview = self.conversation_textviews[jid]
352                if textview.at_the_end():
353                        #we are at the end
354                        if self.nb_unread[jid] > 0:
355                                self.nb_unread[jid] = 0 + self.get_specific_unread(jid)
356                                self.show_title()
357                                if gajim.interface.systray_enabled:
358                                        gajim.interface.systray.remove_jid(jid, self.account,
359                                                self.get_message_type(jid))
360               
361                '''TC/GC window received focus, so if we had urgency REMOVE IT
362                NOTE: we do not have to read the message (it maybe in a bg tab)
363                to remove urgency hint so this functions does that'''
364                if gtk.gtk_version >= (2, 8, 0) and gtk.pygtk_version >= (2, 8, 0):
365                        if widget.props.urgency_hint:
366                                widget.props.urgency_hint = False
367                # Undo "unread" state display, etc.
368                if self.widget_name == 'groupchat_window':
369                        self.redraw_tab(jid, 'active')
370                else:
371                        # NOTE: we do not send any chatstate to preserve inactive, gone, etc.
372                        self.redraw_tab(jid)
373       
374        def on_compact_view_menuitem_activate(self, widget):
375                isactive = widget.get_active()
376                self.set_compact_view(isactive)
377
378        def on_actions_button_clicked(self, widget):
379                '''popup action menu'''
380                #FIXME: BUG http://bugs.gnome.org/show_bug.cgi?id=316786
381                self.button_clicked = widget
382               
383                menu = self.prepare_context_menu()
384                menu.show_all()
385                menu.popup(None, None, self.position_menu_under_button, 1, 0)
386
387        def on_emoticons_button_clicked(self, widget):
388                '''popup emoticons menu'''
389                #FIXME: BUG http://bugs.gnome.org/show_bug.cgi?id=316786
390                self.button_clicked = widget
391                self.emoticons_menu.popup(None, None, self.position_menu_under_button, 1, 0)
392
393        def position_menu_under_button(self, menu):
394                #FIXME: BUG http://bugs.gnome.org/show_bug.cgi?id=316786
395                # pass btn instance when this bug is over
396                button = self.button_clicked
397                # here I get the coordinates of the button relative to
398                # window (self.window)
399                button_x, button_y = button.allocation.x, button.allocation.y
400               
401                # now convert them to X11-relative
402                window_x, window_y = self.window.window.get_origin()
403                x = window_x + button_x
404                y = window_y + button_y
405
406                menu_width, menu_height = menu.size_request()
407
408                ## should we pop down or up?
409                if (y + button.allocation.height + menu_height
410                    < gtk.gdk.screen_height()):
411                        # now move the menu below the button
412                        y += button.allocation.height
413                else:
414                        # now move the menu above the button
415                        y -= menu_height
416
417
418                # push_in is True so all the menuitems are always inside screen
419                push_in = True
420                return (x, y, push_in)
421
422        def remove_possible_switch_to_menuitems(self, menu):
423                ''' remove duplicate 'Switch to' if they exist and return clean menu'''
424                childs = menu.get_children()
425
426                if self.widget_name == 'tabbed_chat_window':
427                        jid = self.get_active_jid()
428                        c = gajim.get_first_contact_instance_from_jid(self.account, jid)
429                        if _('not in the roster') in c.groups: # for add_to_roster_menuitem
430                                childs[5].show()
431                                childs[5].set_no_show_all(False)
432                        else:
433                                childs[5].hide()
434                                childs[5].set_no_show_all(True)
435                       
436                        start_removing_from = 6 # this is from the seperator and after
437                       
438                else:
439                        start_removing_from = 7 # # this is from the seperator and after
440                               
441                for child in childs[start_removing_from:]:
442                        menu.remove(child)
443
444                return menu
445       
446        def prepare_context_menu(self):
447                '''sets compact view menuitem active state
448                sets active and sensitivity state for toggle_gpg_menuitem
449                and remove possible 'Switch to' menuitems'''
450                if self.widget_name == 'groupchat_window':
451                        menu = self.gc_popup_menu
452                        childs = menu.get_children()
453                        # compact_view_menuitem
454                        childs[5].set_active(self.compact_view_current_state)
455                elif self.widget_name == 'tabbed_chat_window':
456                        menu = self.tabbed_chat_popup_menu
457                        childs = menu.get_children()
458                        # check if gpg capabitlies or else make gpg toggle insensitive
459                        jid = self.get_active_jid()
460                        gpg_btn = self.xmls[jid].get_widget('gpg_togglebutton')
461                        isactive = gpg_btn.get_active()
462                        issensitive = gpg_btn.get_property('sensitive')
463                        childs[3].set_active(isactive)
464                        childs[3].set_property('sensitive', issensitive)
465                        # If we don't have resource, we can't do file transfert
466                        c = gajim.get_first_contact_instance_from_jid(self.account, jid)
467                        if not c.resource:
468                                childs[2].set_sensitive(False)
469                        else:
470                                childs[2].set_sensitive(True)
471                        # compact_view_menuitem
472                        childs[4].set_active(self.compact_view_current_state)
473                menu = self.remove_possible_switch_to_menuitems(menu)
474               
475                return menu
476
477        def prepare_emoticons_menu(self):
478                menu = gtk.Menu()
479       
480                def append_emoticon(w, d):
481                        jid = self.get_active_jid()
482                        message_textview = self.message_textviews[jid]
483                        buffer = message_textview.get_buffer()
484                        if buffer.get_char_count():
485                                buffer.insert_at_cursor(' %s ' % d)
486                        else: # we are the beginning of buffer
487                                buffer.insert_at_cursor('%s ' % d)
488                        message_textview.grab_focus()
489       
490                counter = 0
491                # Calculate the side lenght of the popup to make it a square
492                size = int(round(math.sqrt(len(gajim.interface.emoticons_images))))
493                for image in gajim.interface.emoticons_images:
494                        item = gtk.MenuItem()
495                        img = gtk.Image()
496                        if type(image[1]) == gtk.gdk.PixbufAnimation:
497                                img.set_from_animation(image[1])
498                        else:
499                                img.set_from_pixbuf(image[1])
500                        item.add(img)
501                        item.connect('activate', append_emoticon, image[0])
502                        #FIXME: add tooltip with ascii
503                        menu.attach(item,
504                                        counter % size, counter % size + 1,
505                                        counter / size, counter / size + 1)
506                        counter += 1
507                menu.show_all()
508                return menu
509
510        def popup_menu(self, event):
511                menu = self.prepare_context_menu()
512                # common menuitems (tab switches)
513                if len(self.xmls) > 1: # if there is more than one tab
514                        menu.append(gtk.SeparatorMenuItem()) # seperator
515                        for jid in self.xmls:
516                                if jid != self.get_active_jid():
517                                        item = gtk.ImageMenuItem(_('Switch to %s') % self.names[jid])
518                                        img = gtk.image_new_from_stock(gtk.STOCK_JUMP_TO,
519                                                gtk.ICON_SIZE_MENU)
520                                        item.set_image(img)
521                                        item.connect('activate', lambda obj, jid:self.set_active_tab(
522                                                jid), jid)
523                                        menu.append(item)
524
525                # show the menu
526                menu.popup(None, None, None, event.button, event.time)
527                menu.show_all()
528
529        def on_banner_eventbox_button_press_event(self, widget, event):
530                '''If right-clicked, show popup'''
531                if event.button == 3: # right click
532                        self.popup_menu(event)
533
534        def on_chat_notebook_switch_page(self, notebook, page, page_num):
535                # get the index of the page and then the page that we're leaving
536                old_no = notebook.get_current_page()
537                old_child = notebook.get_nth_page(old_no)
538               
539                new_child = notebook.get_nth_page(page_num)
540               
541                old_jid = ''
542                new_jid = ''
543                for jid in self.xmls:
544                        if self.childs[jid] == new_child:
545                                new_jid = jid
546                        elif self.childs[jid] == old_child:
547                                old_jid = jid
548                       
549                        if old_jid != '' and new_jid != '': # we found both jids
550                                break # so stop looping
551               
552                if self.widget_name == 'tabbed_chat_window':
553                        # send chatstate inactive to the one we're leaving
554                        # and active to the one we visit
555                        if old_jid != '':
556                                self.send_chatstate('inactive', old_jid)
557                        self.send_chatstate('active', new_jid)
558
559                conv_textview = self.conversation_textviews[new_jid]
560                if conv_textview.at_the_end():
561                        #we are at the end
562                        if self.nb_unread[new_jid] > 0:
563                                self.nb_unread[new_jid] = 0 + self.get_specific_unread(new_jid)
564                                self.redraw_tab(new_jid)
565                                self.show_title()
566                                if gajim.interface.systray_enabled:
567                                        gajim.interface.systray.remove_jid(new_jid, self.account,
568                                                self.get_message_type(new_jid))
569
570                conv_textview.grab_focus()
571
572        def set_active_tab(self, jid):
573                self.notebook.set_current_page(self.notebook.page_num(self.childs[jid]))
574
575        def remove_tab(self, jid, kind): #kind is 'chats' or 'gc'
576                if len(self.xmls) == 1: # only one tab when we asked to remove
577                        # so destroy window
578
579                        # we check and possibly save positions here, because Ctrl+W, Escape
580                        # etc.. call remove_tab so similar code in delete_event callbacks
581                        # is not enough
582                        if gajim.config.get('saveposition'):
583                                if kind == 'chats':
584                                        x, y = self.window.get_position()
585                                        gajim.config.set('chat-x-position', x)
586                                        gajim.config.set('chat-y-position', y)
587                                        width, height = self.window.get_size()
588                                        gajim.config.set('chat-width', width)
589                                        gajim.config.set('chat-height', height)
590                                elif kind == 'gc':
591                                        gajim.config.set(