root/branches/gajim_0.10/src/groupchat_control.py

Revision 6407, 60.7 kB (checked in by dkirov, 2 years ago)

r6265, r6266, r6267, r6269, r6350, r6366

Line 
1##      groupchat_control.py
2##
3## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
4##                         Vincent Hanquez <tab@snarc.org>
5## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
6##                    Vincent Hanquez <tab@snarc.org>
7##                    Nikos Kouremenos <nkour@jabber.org>
8##                    Dimitur Kirov <dkirov@gmail.com>
9##                    Travis Shirk <travis@pobox.com>
10##                    Norman Rasmussen <norman@rasmussen.co.za>
11## Copyright (C) 2006 Travis Shirk <travis@pobox.com>
12##
13## This program is free software; you can redistribute it and/or modify
14## it under the terms of the GNU General Public License as published
15## by the Free Software Foundation; version 2 only.
16##
17## This program is distributed in the hope that it will be useful,
18## but WITHOUT ANY WARRANTY; without even the implied warranty of
19## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20## GNU General Public License for more details.
21##
22
23import os
24import time
25import gtk
26import gtk.glade
27import pango
28import gobject
29import gtkgui_helpers
30import message_control
31import tooltips
32import dialogs
33import config
34import vcard
35import cell_renderer_image
36
37from common import gajim
38from common import helpers
39
40from chat_control import ChatControl
41from chat_control import ChatControlBase
42from conversation_textview import ConversationTextview
43from common import i18n
44
45_ = i18n._
46Q_ = i18n.Q_
47APP = i18n.APP
48gtk.glade.bindtextdomain(APP, i18n.DIR)
49gtk.glade.textdomain(APP)
50
51#(status_image, type, nick, shown_nick)
52(
53C_IMG, # image to show state (online, new message etc)
54C_TYPE, # type of the row ('contact' or 'group')
55C_NICK, # contact nickame or group name
56C_TEXT, # text shown in the cellrenderer
57C_AVATAR, # avatar of the contact
58) = range(5)
59       
60def set_renderer_color(treeview, renderer, set_background = True):
61        '''set style for group row, using PRELIGHT system color'''
62        if set_background:
63                bgcolor = treeview.style.bg[gtk.STATE_PRELIGHT]
64                renderer.set_property('cell-background-gdk', bgcolor)
65        else:
66                fgcolor = treeview.style.fg[gtk.STATE_PRELIGHT]
67                renderer.set_property('foreground-gdk', fgcolor)
68
69def tree_cell_data_func(column, renderer, model, iter, tv=None):
70        # cell data func is global, because we don't want it to keep
71        # reference to GroupchatControl instance (self)
72        theme = gajim.config.get('roster_theme')
73        if model.iter_parent(iter):
74                bgcolor = gajim.config.get_per('themes', theme, 'contactbgcolor')
75                if bgcolor:
76                        renderer.set_property('cell-background', bgcolor)
77                else:
78                        renderer.set_property('cell-background', None)
79                if isinstance(renderer, gtk.CellRendererText):
80                        # foreground property is only with CellRendererText
81                        color = gajim.config.get_per('themes', theme, 
82                                'contacttextcolor')
83                        if color:
84                                renderer.set_property('foreground', color)
85                        else:
86                                renderer.set_property('foreground', None)
87        else: # it is root (eg. group)
88                bgcolor = gajim.config.get_per('themes', theme, 'groupbgcolor')
89                if bgcolor:
90                        renderer.set_property('cell-background', bgcolor)
91                else:
92                        set_renderer_color(tv, renderer)
93                if isinstance(renderer, gtk.CellRendererText):
94                        # foreground property is only with CellRendererText
95                        color = gajim.config.get_per('themes', theme, 'grouptextcolor')
96                        if color:
97                                renderer.set_property('foreground', color)
98                        else:
99                                set_renderer_color(tv, renderer, False)
100
101class PrivateChatControl(ChatControl):
102        TYPE_ID = message_control.TYPE_PM
103
104        def __init__(self, parent_win, contact, acct):
105                ChatControl.__init__(self, parent_win, contact, acct)
106                self.TYPE_ID = 'pm'
107                self.display_names = (_('Private Chat'), _('Private Chats'))
108
109        def send_message(self, message):
110                '''call this function to send our message'''
111                if not message:
112                        return
113
114                # We need to make sure that we can still send through the room and that the
115                # recipient did not go away
116                contact = gajim.contacts.get_first_contact_from_jid(self.account, self.contact.jid)
117                if contact is None:
118                        # contact was from pm in MUC
119                        room, nick = gajim.get_room_and_nick_from_fjid(self.contact.jid)
120                        gc_contact = gajim.contacts.get_gc_contact(self.account, room, nick)
121                        if not gc_contact:
122                                dialogs.ErrorDialog(
123                                        _('Sending private message failed'),
124                                        #in second %s code replaces with nickname
125                                        _('You are no longer in room "%s" or "%s" has left.') % \
126                                        (room, nick))
127                                return
128
129                ChatControl.send_message(self, message)
130
131
132class GroupchatControl(ChatControlBase):
133        TYPE_ID = message_control.TYPE_GC
134
135        def __init__(self, parent_win, contact, acct):
136                ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
137                                        'muc_child_vbox', (_('Group Chat'), _('Group Chats')),
138                                        contact, acct);
139
140                widget = self.xml.get_widget('muc_window_actions_button')
141                id = widget.connect('clicked', self.on_actions_button_clicked)
142                self.handlers[id] = widget
143               
144                widget = self.xml.get_widget('list_treeview')
145                id = widget.connect('row_expanded', self.on_list_treeview_row_expanded)
146                self.handlers[id] = widget
147               
148                id = widget.connect('row_collapsed', 
149                        self.on_list_treeview_row_collapsed)
150                self.handlers[id] = widget
151               
152                id = widget.connect('row_activated', 
153                        self.on_list_treeview_row_activated)
154                self.handlers[id] = widget
155               
156                id = widget.connect('button_press_event', 
157                        self.on_list_treeview_button_press_event)
158                self.handlers[id] = widget
159               
160                id = widget.connect('key_press_event', 
161                        self.on_list_treeview_key_press_event)
162                self.handlers[id] = widget
163               
164                id = widget.connect('motion_notify_event', 
165                        self.on_list_treeview_motion_notify_event)
166                self.handlers[id] = widget
167               
168                id = widget.connect('leave_notify_event', 
169                        self.on_list_treeview_leave_notify_event)
170                self.handlers[id] = widget
171       
172                self.room_jid = self.contact.jid
173                self.nick = contact.name
174                self.name = self.room_jid.split('@')[0]
175
176                self.hide_chat_buttons_always = gajim.config.get('always_hide_groupchat_buttons')
177                self.chat_buttons_set_visible(self.hide_chat_buttons_always)
178                self.widget_set_visible(self.xml.get_widget('banner_eventbox'), gajim.config.get('hide_groupchat_banner'))
179                self.widget_set_visible(self.xml.get_widget('list_scrolledwindow'), gajim.config.get('hide_groupchat_occupants_list'))
180                self.gc_refer_to_nick_char = gajim.config.get('gc_refer_to_nick_char')
181
182                self._last_selected_contact = None # None or holds jid, account tuple
183                # alphanum sorted
184                self.muc_cmds = ['ban', 'chat', 'query', 'clear', 'close', 'compact',
185                        'help', 'invite', 'join', 'kick', 'leave', 'me', 'msg', 'nick', 'part',
186                        'say', 'topic']
187                # muc attention flag (when we are mentioned in a muc)
188                # if True, the room has mentioned us
189                self.attention_flag = False
190                self.room_creation = time.time()
191                self.nick_hits = []
192                self.cmd_hits = []
193                self.last_key_tabs = False
194
195                self.subject = ''
196                self.subject_tooltip = gtk.Tooltips()
197
198                self.tooltip = tooltips.GCTooltip()
199
200                self.allow_focus_out_line = True
201                # holds the iter's offset which points to the end of --- line
202                self.focus_out_end_iter_offset = None
203
204                # connect the menuitems to their respective functions
205                xm = gtkgui_helpers.get_glade('gc_control_popup_menu.glade')
206
207                widget = xm.get_widget('bookmark_room_menuitem')
208                id = widget.connect('activate', self._on_bookmark_room_menuitem_activate)
209                self.handlers[id] = widget
210
211                widget = xm.get_widget('change_nick_menuitem')
212                id = widget.connect('activate', self._on_change_nick_menuitem_activate)
213                self.handlers[id] = widget
214
215                widget = xm.get_widget('configure_room_menuitem')
216                id = widget.connect('activate', self._on_configure_room_menuitem_activate)
217                self.handlers[id] = widget
218
219                widget = xm.get_widget('change_subject_menuitem')
220                id = widget.connect('activate', self._on_change_subject_menuitem_activate)
221                self.handlers[id] = widget
222
223                widget = xm.get_widget('compact_view_menuitem')
224                id = widget.connect('activate', self._on_compact_view_menuitem_activate)
225                self.handlers[id] = widget
226               
227                widget = xm.get_widget('history_menuitem')
228                id = widget.connect('activate', self._on_history_menuitem_activate)
229                self.handlers[id] = widget
230               
231                self.gc_popup_menu = xm.get_widget('gc_control_popup_menu')
232
233                self.name_label = self.xml.get_widget('banner_name_label')
234                id = self.parent_win.window.connect('focus-in-event',
235                                                self._on_window_focus_in_event)
236                self.handlers[id] = self.parent_win.window
237
238                # set the position of the current hpaned
239                self.hpaned_position = gajim.config.get('gc-hpaned-position')
240                self.hpaned = self.xml.get_widget('hpaned')
241                self.hpaned.set_position(self.hpaned_position)
242
243                list_treeview = self.list_treeview = self.xml.get_widget('list_treeview')
244                selection = list_treeview.get_selection()
245                id = selection.connect('changed', 
246                                self.on_list_treeview_selection_changed)
247                self.handlers[id] = selection
248                id = list_treeview.connect('style-set', self.on_list_treeview_style_set)
249                self.handlers[id] = list_treeview
250                # we want to know when the the widget resizes, because that is
251                # an indication that the hpaned has moved...
252                # FIXME: Find a better indicator that the hpaned has moved.
253                id = self.list_treeview.connect('size-allocate',
254                        self.on_treeview_size_allocate)
255                self.handlers[id] = self.list_treeview
256                #status_image, type, nickname, shown_nick
257                store = gtk.TreeStore(gtk.Image, str, str, str, gtk.gdk.Pixbuf)
258                store.set_sort_column_id(C_TEXT, gtk.SORT_ASCENDING)
259                self.list_treeview.set_model(store)
260
261                # columns
262
263                # this col has 3 cells:
264                # first one img, second one text, third is sec pixbuf
265                column = gtk.TreeViewColumn()
266
267                renderer_pixbuf = gtk.CellRendererPixbuf() # avatar image
268                column.pack_start(renderer_pixbuf, expand = False)
269                column.add_attribute(renderer_pixbuf, 'pixbuf', C_AVATAR)
270                column.set_cell_data_func(renderer_pixbuf, tree_cell_data_func,
271                        self.list_treeview)
272                renderer_pixbuf.set_property('xalign', 1) # align pixbuf to the right
273
274                renderer_image = cell_renderer_image.CellRendererImage(0, 0) # status img
275                column.pack_start(renderer_image, expand = False)
276                column.add_attribute(renderer_image, 'image', C_IMG)
277                column.set_cell_data_func(renderer_image, tree_cell_data_func, 
278                        self.list_treeview)
279
280                renderer_text = gtk.CellRendererText() # nickname
281                column.pack_start(renderer_text, expand = True)
282                column.add_attribute(renderer_text, 'markup', C_TEXT)
283                column.set_cell_data_func(renderer_text, tree_cell_data_func, self.list_treeview)
284
285                self.list_treeview.append_column(column)
286
287                # workaround to avoid gtk arrows to be shown
288                column = gtk.TreeViewColumn() # 2nd COLUMN
289                renderer = gtk.CellRendererPixbuf()
290                column.pack_start(renderer, expand = False)
291                self.list_treeview.append_column(column)
292                column.set_visible(False)
293                self.list_treeview.set_expander_column(column)
294
295                # set an empty subject to show the room_jid
296                self.set_subject('')
297                self.got_disconnected() #init some variables
298
299                self.update_ui()
300                self.conv_textview.tv.grab_focus()
301                self.widget.show_all()
302
303        def notify_on_new_messages(self):
304                return gajim.config.get('notify_on_all_muc_messages') or \
305                        self.attention_flag
306
307        def _on_window_focus_in_event(self, widget, event):
308                '''When window gets focus'''
309                if self.parent_win.get_active_jid() == self.room_jid:
310                        self.allow_focus_out_line = True
311       
312        def on_treeview_size_allocate(self, widget, allocation):
313                '''The MUC treeview has resized. Move the hpaned in all tabs to match'''
314                self.hpaned_position = self.hpaned.get_position()
315                self.hpaned.set_position(self.hpaned_position)
316
317        def iter_contact_rows(self):
318                '''iterate over all contact rows in the tree model'''
319                model = self.list_treeview.get_model()
320                role_iter = model.get_iter_root()
321                while role_iter:
322                        contact_iter = model.iter_children(role_iter)
323                        while contact_iter:
324                                yield model[contact_iter]
325                                contact_iter = model.iter_next(contact_iter)
326                        role_iter = model.iter_next(role_iter)
327
328        def on_list_treeview_style_set(self, treeview, style):
329                '''When style (theme) changes, redraw all contacts'''
330                # Get the room_jid from treeview
331                for contact in self.iter_contact_rows():
332                        nick = contact[C_NICK].decode('utf-8')
333                        self.draw_contact(nick)
334
335        def on_list_treeview_selection_changed(self, selection):
336                model, selected_iter = selection.get_selected()
337                self.draw_contact(self.nick)
338                if self._last_selected_contact is not None:
339                        self.draw_contact(self._last_selected_contact)
340                if selected_iter is None:
341                        self._last_selected_contact = None
342                        return
343                contact = model[selected_iter]
344                nick = contact[C_NICK].decode('utf-8')
345                self._last_selected_contact = nick
346                if contact[C_TYPE] != 'contact':
347                        return
348                self.draw_contact(nick, selected=True, focus=True)
349
350        def get_tab_label(self, chatstate):
351                '''Markup the label if necessary. Returns a tuple such as:
352                (new_label_str, color)
353                either of which can be None
354                if chatstate is given that means we have HE SENT US a chatstate'''
355
356                has_focus = self.parent_win.window.get_property('has-toplevel-focus')
357                current_tab = self.parent_win.get_active_control() == self
358                color = None
359                theme = gajim.config.get('roster_theme')
360                if chatstate == 'attention' and (not has_focus or not current_tab):
361                        self.attention_flag = True
362                        color = gajim.config.get_per('themes', theme,
363                                                        'state_muc_directed_msg_color')
364                elif chatstate:
365                        if chatstate == 'active' or (current_tab and has_focus):
366                                self.attention_flag = False
367                                color = gajim.config.get_per('themes', theme,
368                                                                'state_active_color')
369                        elif chatstate == 'newmsg' and (not has_focus or not current_tab) and\
370                                        not self.attention_flag:
371                                color = gajim.config.get_per('themes', theme, 'state_muc_msg_color')
372                if color:
373                        color = gtk.gdk.colormap_get_system().alloc_color(color)
374
375                label_str = self.name
376                return (label_str, color)
377
378        def get_tab_image(self):
379                # Set tab image (always 16x16); unread messages show the 'message' image
380                img_16 = gajim.interface.roster.get_appropriate_state_images(
381                        self.room_jid, icon_name = 'message')
382
383                tab_image = None
384                if self.attention_flag and gajim.config.get('show_unread_tab_icon'):
385                        tab_image = img_16['message']
386                else:
387                        if gajim.gc_connected[self.account][self.room_jid]:
388                                tab_image = img_16['muc_active']
389                        else:
390                                tab_image = img_16['muc_inactive']
391                return tab_image
392
393        def update_ui(self):
394                ChatControlBase.update_ui(self)
395                for nick in gajim.contacts.get_nick_list(self.account, self.room_jid):
396                        self.draw_contact(nick)
397
398        def prepare_context_menu(self):
399                '''sets compact view menuitem active state
400                sets sensitivity state for configure_room'''
401                menu = self.gc_popup_menu
402                childs = menu.get_children()
403                # hide chat buttons
404                childs[5].set_active(self.hide_chat_buttons_current)
405                if gajim.gc_connected[self.account][self.room_jid]:
406                        c = gajim.contacts.get_gc_contact(self.account, self.room_jid,
407                                self.nick)
408                        if c.affiliation not in ('owner', 'admin'):
409                                childs[1].set_sensitive(False)
410                else:
411                        # We are not connected to this groupchat, disable unusable menuitems
412                        childs[1].set_sensitive(False)
413                        childs[2].set_sensitive(False)
414                        childs[3].set_sensitive(False)
415                return menu
416
417        def on_message(self, nick, msg, tim):
418                if not nick:
419                        # message from server
420                        self.print_conversation(msg, tim = tim)
421                else:
422                        # message from someone
423                        self.print_conversation(msg, nick, tim)
424
425        def on_private_message(self, nick, msg, tim):
426                # Do we have a queue?
427                fjid = self.room_jid + '/' + nick
428                qs = gajim.awaiting_events[self.account]
429                no_queue = True
430                if qs.has_key(fjid):
431                        no_queue = False
432
433                # We print if window is opened
434                pm_control = gajim.interface.msg_win_mgr.get_control(fjid, self.account)
435                if pm_control:
436                        pm_control.print_conversation(msg, tim = tim)
437                        return
438
439                if no_queue:
440                        qs[fjid] = []
441                qs[fjid].append(('chat', (msg, '', 'incoming', tim, False, '')))
442
443                autopopup = gajim.config.get('autopopup')
444                autopopupaway = gajim.config.get('autopopupaway')
445                iter = self.get_contact_iter(nick)
446                path = self.list_treeview.get_model().get_path(iter)
447                if not autopopup or (not autopopupaway and \
448                                        gajim.connections[self.account].connected > 2):
449                        if no_queue: # We didn't have a queue: we change icons
450                                model = self.list_treeview.get_model()
451                                state_images =\
452                                        gajim.interface.roster.get_appropriate_state_images(
453                                                self.room_jid, icon_name = 'message')
454                                image = state_images['message']
455                                model[iter][C_IMG] = image
456                                if gajim.interface.systray_enabled:
457                                        gajim.interface.systray.add_jid(fjid, self.account, 'pm')
458                        self.parent_win.show_title()
459                else:
460                        self._start_private_message(nick)
461                # Scroll to line
462                self.list_treeview.expand_row(path[0:1], False)
463                self.list_treeview.scroll_to_cell(path)
464                self.list_treeview.set_cursor(path)
465
466        def get_contact_iter(self, nick):
467                model = self.list_treeview.get_model()
468                fin = False
469                role_iter = model.get_iter_root()
470                if not role_iter:
471                        return None
472                while not fin:
473                        fin2 = False
474                        user_iter = model.iter_children(role_iter)
475                        if not user_iter:
476                                fin2 = True
477                        while not fin2:
478                                if nick == model[user_iter][C_NICK].decode('utf-8'):
479                                        return user_iter
480                                user_iter = model.iter_next(user_iter)
481                                if not user_iter:
482                                        fin2 = True
483                        role_iter = model.iter_next(role_iter)
484                        if not role_iter:
485                                fin = True
486                return None
487
488        def print_conversation(self, text, contact = '', tim = None):
489                '''Print a line in the conversation:
490                if contact is set: it's a message from someone or an info message (contact
491                = 'info' in such a case)
492                if contact is not set: it's a message from the server or help'''
493                if isinstance(text, str):
494                        text = unicode(text, 'utf-8')
495                other_tags_for_name = []
496                other_tags_for_text = []
497                if contact:
498                        if contact == self.nick: # it's us
499                                kind = 'outgoing'
500                        elif contact == 'info':
501                                kind = 'info'
502                                contact = None
503                        else:
504                                kind = 'incoming'
505                                # muc-specific chatstate
506                                self.parent_win.redraw_tab(self, 'newmsg')
507                else:
508                        kind = 'status'
509
510                if kind == 'incoming': # it's a message NOT from us
511                        # highlighting and sounds
512                        (highlight, sound) = self.highlighting_for_message(text, tim)
513                        if highlight:
514                                # muc-specific chatstate
515                                self.parent_win.redraw_tab(self, 'attention')
516                                other_tags_for_name.append('bold')
517                                other_tags_for_text.append('marked')
518                        if sound == 'received':
519                                helpers.play_sound('muc_message_received')
520                        elif sound == 'highlight':
521                                helpers.play_sound('muc_message_highlight')
522
523                        self.check_and_possibly_add_focus_out_line()
524
525                ChatControlBase.print_conversation_line(self, text, kind, contact, tim,
526                        other_tags_for_name, [], other_tags_for_text)
527
528        def highlighting_for_message(self, text, tim):
529                '''Returns a 2-Tuple. The first says whether or not to highlight the
530                text, the second, what sound to play.'''
531                highlight, sound = (None, None)
532
533                # Do we play a sound on every muc message?
534                if gajim.config.get_per('soundevents', 'muc_message_received', 'enabled'):
535                        if gajim.config.get('notify_on_all_muc_messages'):
536                                sound = 'received'
537
538                # Are any of the defined highlighting words in the text?
539                if self.needs_visual_notification(text):
540                        highlight = True
541                        if gajim.config.get_per('soundevents', 'muc_message_highlight',
542                                                                        'enabled'):
543                                sound = 'highlight'
544
545                # Is it a history message? Don't want sound-floods when we join.
546                if tim != time.localtime():
547                        sound = None
548
549                return (highlight, sound)
550
551        def check_and_possibly_add_focus_out_line(self):
552                '''checks and possibly adds focus out line for room_jid if it needs it
553                and does not already have it as last event. If it goes to add this line
554                it removes previous line first'''
555
556                win = gajim.interface.msg_win_mgr.get_window(self.room_jid, self.account)
557                if self.room_jid == win.get_active_jid() and\
558                win.window.get_property('has-toplevel-focus'):
559                        # it's the current room and it's the focused window.
560                        # we have full focus (we are reading it!)
561                        return
562
563                if not self.allow_focus_out_line:
564                        # if room did not receive focus-in from the last time we added
565                        # --- line then do not readd
566                        return
567
568                print_focus_out_line = False
569                buffer = self.conv_textview.tv.get_buffer()
570
571                if self.focus_out_end_iter_offset is None:
572                        # this happens only first time we focus out on this room
573                        print_focus_out_line = True
574
575                else:
576                        if self.focus_out_end_iter_offset != buffer.get_end_iter().get_offset():
577                                # this means after last-focus something was printed
578                                # (else end_iter's offset is the same as before)
579                                # only then print ---- line (eg. we avoid printing many following
580                                # ---- lines)
581                                print_focus_out_line = True
582
583                if print_focus_out_line and buffer.get_char_count() > 0:
584                        buffer.begin_user_action()
585
586                        # remove previous focus out line if such focus out line exists
587                        if self.focus_out_end_iter_offset is not None:
588                                end_iter_for_previous_line = buffer.get_iter_at_offset(
589                                        self.focus_out_end_iter_offset)
590                                begin_iter_for_previous_line = end_iter_for_previous_line.copy()
591                                begin_iter_for_previous_line.backward_chars(2) # img_char+1 (the '\n')
592
593                                # remove focus out line
594                                buffer.delete(begin_iter_for_previous_line,
595                                        end_iter_for_previous_line)
596
597                        # add the new focus out line
598                        # FIXME: Why is this loaded from disk everytime
599                        path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps', 'muc_separator.png')
600                        focus_out_line_pixbuf = gtk.gdk.pixbuf_new_from_file(path_to_file)
601                        end_iter = buffer.get_end_iter()
602                        buffer.insert(end_iter, '\n')
603                        buffer.insert_pixbuf(end_iter, focus_out_line_pixbuf)
604
605                        end_iter = buffer