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

Revision 8736, 69.9 kB (checked in by asterix, 15 months ago)

don't check if there is a @ when we invite a JID.

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 <kourem@gmail.com>
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 pango
27import gobject
28import gtkgui_helpers
29import message_control
30import tooltips
31import dialogs
32import config
33import vcard
34import cell_renderer_image
35
36from common import gajim
37from common import helpers
38
39from chat_control import ChatControl
40from chat_control import ChatControlBase
41from conversation_textview import ConversationTextview
42from common.exceptions import GajimGeneralException
43
44#(status_image, type, nick, shown_nick)
45(
46C_IMG, # image to show state (online, new message etc)
47C_NICK, # contact nickame or ROLE name
48C_TYPE, # type of the row ('contact' or 'role')
49C_TEXT, # text shown in the cellrenderer
50C_AVATAR, # avatar of the contact
51) = range(5)
52       
53def set_renderer_color(treeview, renderer, set_background = True):
54        '''set style for group row, using PRELIGHT system color'''
55        if set_background:
56                bgcolor = treeview.style.bg[gtk.STATE_PRELIGHT]
57                renderer.set_property('cell-background-gdk', bgcolor)
58        else:
59                fgcolor = treeview.style.fg[gtk.STATE_PRELIGHT]
60                renderer.set_property('foreground-gdk', fgcolor)
61
62def tree_cell_data_func(column, renderer, model, iter, tv=None):
63        # cell data func is global, because we don't want it to keep
64        # reference to GroupchatControl instance (self)
65        theme = gajim.config.get('roster_theme')
66        # allocate space for avatar only if needed
67        if isinstance(renderer, gtk.CellRendererPixbuf):
68                if model[iter][C_AVATAR]:
69                        renderer.set_property('visible', True)
70                else:
71                        renderer.set_property('visible', False)
72        if model.iter_parent(iter):
73                bgcolor = gajim.config.get_per('themes', theme, 'contactbgcolor')
74                if bgcolor:
75                        renderer.set_property('cell-background', bgcolor)
76                else:
77                        renderer.set_property('cell-background', None)
78                if isinstance(renderer, gtk.CellRendererText):
79                        # foreground property is only with CellRendererText
80                        color = gajim.config.get_per('themes', theme, 'contacttextcolor')
81                        if color:
82                                renderer.set_property('foreground', color)
83                        else:
84                                renderer.set_property('foreground', None)
85                        renderer.set_property('font',
86                                gtkgui_helpers.get_theme_font_for_option(theme, 'contactfont'))
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                        renderer.set_property('font',
101                                gtkgui_helpers.get_theme_font_for_option(theme, 'groupfont'))
102
103class PrivateChatControl(ChatControl):
104        TYPE_ID = message_control.TYPE_PM
105
106        def __init__(self, parent_win, gc_contact, contact, acct):
107                room_jid = contact.jid.split('/')[0]
108                room_ctrl = gajim.interface.msg_win_mgr.get_control(room_jid, acct)
109                self.room_name = room_ctrl.name
110                self.gc_contact = gc_contact
111                ChatControl.__init__(self, parent_win, contact, acct)
112                self.TYPE_ID = 'pm'
113
114        def send_message(self, message):
115                '''call this function to send our message'''
116                if not message:
117                        return
118
119                # We need to make sure that we can still send through the room and that
120                # the recipient did not go away
121                contact = gajim.contacts.get_first_contact_from_jid(self.account,
122                        self.contact.jid)
123                if contact is None:
124                        # contact was from pm in MUC
125                        room, nick = gajim.get_room_and_nick_from_fjid(self.contact.jid)
126                        gc_contact = gajim.contacts.get_gc_contact(self.account, room, nick)
127                        if not gc_contact:
128                                dialogs.ErrorDialog(
129                                        _('Sending private message failed'),
130                                        #in second %s code replaces with nickname
131                                        _('You are no longer in group chat "%s" or "%s" has left.') % \
132                                        (room, nick))
133                                return
134
135                ChatControl.send_message(self, message)
136       
137        def update_ui(self):
138                if self.contact.show == 'offline':
139                        self.got_disconnected()
140                else:
141                        self.got_connected()
142                ChatControl.update_ui(self)
143
144
145class GroupchatControl(ChatControlBase):
146        TYPE_ID = message_control.TYPE_GC
147        # alphanum sorted
148        MUC_CMDS = ['ban', 'chat', 'query', 'clear', 'close', 'compact',
149                'help', 'invite', 'join', 'kick', 'leave', 'me', 'msg', 'nick',
150                'part', 'names', 'say', 'topic']
151
152        def __init__(self, parent_win, contact, acct):
153                ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
154                                        'muc_child_vbox', contact, acct);
155
156                widget = self.xml.get_widget('muc_window_actions_button')
157                id = widget.connect('clicked', self.on_actions_button_clicked)
158                self.handlers[id] = widget
159
160                widget = self.xml.get_widget('list_treeview')
161                id = widget.connect('row_expanded', self.on_list_treeview_row_expanded)
162                self.handlers[id] = widget
163
164                id = widget.connect('row_collapsed', 
165                        self.on_list_treeview_row_collapsed)
166                self.handlers[id] = widget
167
168                id = widget.connect('row_activated', 
169                        self.on_list_treeview_row_activated)
170                self.handlers[id] = widget
171
172                id = widget.connect('button_press_event', 
173                        self.on_list_treeview_button_press_event)
174                self.handlers[id] = widget
175
176                id = widget.connect('key_press_event', 
177                        self.on_list_treeview_key_press_event)
178                self.handlers[id] = widget
179
180                id = widget.connect('motion_notify_event', 
181                        self.on_list_treeview_motion_notify_event)
182                self.handlers[id] = widget
183
184                id = widget.connect('leave_notify_event', 
185                        self.on_list_treeview_leave_notify_event)
186                self.handlers[id] = widget
187
188                self.room_jid = self.contact.jid
189                self.nick = contact.name
190                self.name = self.room_jid.split('@')[0]
191
192                hide_chat_buttons_always = gajim.config.get(
193                        'always_hide_groupchat_buttons')
194                self.chat_buttons_set_visible(hide_chat_buttons_always)
195                self.widget_set_visible(self.xml.get_widget('banner_eventbox'),
196                        gajim.config.get('hide_groupchat_banner'))
197                self.widget_set_visible(self.xml.get_widget('list_scrolledwindow'),
198                        gajim.config.get('hide_groupchat_occupants_list'))
199
200                self._last_selected_contact = None # None or holds jid, account tuple
201
202                # muc attention flag (when we are mentioned in a muc)
203                # if True, the room has mentioned us
204                self.attention_flag = False
205                self.room_creation = int(time.time()) # Use int to reduce mem usage
206                self.nick_hits = []
207                self.cmd_hits = []
208                self.last_key_tabs = False
209
210                self.subject = ''
211                self.subject_tooltip = gtk.Tooltips()
212
213                self.tooltip = tooltips.GCTooltip()
214
215                # connect the menuitems to their respective functions
216                xm = gtkgui_helpers.get_glade('gc_control_popup_menu.glade')
217
218                self.bookmark_room_menuitem = xm.get_widget('bookmark_room_menuitem')
219                id = self.bookmark_room_menuitem.connect('activate',
220                        self._on_bookmark_room_menuitem_activate)
221                self.handlers[id] = self.bookmark_room_menuitem
222
223                widget = xm.get_widget('change_nick_menuitem')
224                id = widget.connect('activate', self._on_change_nick_menuitem_activate)
225                self.handlers[id] = widget
226
227                widget = xm.get_widget('configure_room_menuitem')
228                id = widget.connect('activate',
229                        self._on_configure_room_menuitem_activate)
230                self.handlers[id] = widget
231
232                widget = xm.get_widget('change_subject_menuitem')
233                id = widget.connect('activate',
234                        self._on_change_subject_menuitem_activate)
235                self.handlers[id] = widget
236
237                widget = xm.get_widget('compact_view_menuitem')
238                id = widget.connect('activate', self._on_compact_view_menuitem_activate)
239                self.handlers[id] = widget
240
241                widget = xm.get_widget('history_menuitem')
242                id = widget.connect('activate', self._on_history_menuitem_activate)
243                self.handlers[id] = widget
244
245                self.gc_popup_menu = xm.get_widget('gc_control_popup_menu')
246
247                self.name_label = self.xml.get_widget('banner_name_label')
248                self.event_box = self.xml.get_widget('banner_eventbox')
249
250                # set the position of the current hpaned
251                self.hpaned_position = gajim.config.get('gc-hpaned-position')
252                self.hpaned = self.xml.get_widget('hpaned')
253                self.hpaned.set_position(self.hpaned_position)
254
255                self.list_treeview = self.xml.get_widget('list_treeview')
256                selection = self.list_treeview.get_selection()
257                id = selection.connect('changed', 
258                                self.on_list_treeview_selection_changed)
259                self.handlers[id] = selection
260                id = self.list_treeview.connect('style-set',
261                        self.on_list_treeview_style_set)
262                self.handlers[id] = self.list_treeview
263                # we want to know when the the widget resizes, because that is
264                # an indication that the hpaned has moved...
265                # FIXME: Find a better indicator that the hpaned has moved.
266                id = self.list_treeview.connect('size-allocate',
267                        self.on_treeview_size_allocate)
268                self.handlers[id] = self.list_treeview
269                #status_image, shown_nick, type, nickname, avatar
270                store = gtk.TreeStore(gtk.Image, str, str, str, gtk.gdk.Pixbuf)
271                store.set_sort_column_id(C_TEXT, gtk.SORT_ASCENDING)
272                self.list_treeview.set_model(store)
273
274                # columns
275
276                # this col has 3 cells:
277                # first one img, second one text, third is sec pixbuf
278                column = gtk.TreeViewColumn()
279
280                renderer_image = cell_renderer_image.CellRendererImage(0, 0) # status img
281                renderer_image.set_property('width', 26)
282                column.pack_start(renderer_image, expand = False)
283                column.add_attribute(renderer_image, 'image', C_IMG)
284                column.set_cell_data_func(renderer_image, tree_cell_data_func, 
285                        self.list_treeview)
286
287                renderer_text = gtk.CellRendererText() # nickname
288                column.pack_start(renderer_text, expand = True)
289                column.add_attribute(renderer_text, 'markup', C_TEXT)
290                renderer_text.set_property("ellipsize", pango.ELLIPSIZE_END)
291                column.set_cell_data_func(renderer_text, tree_cell_data_func,
292                        self.list_treeview)
293
294                renderer_pixbuf = gtk.CellRendererPixbuf() # avatar image
295                column.pack_start(renderer_pixbuf, expand = False)
296                column.add_attribute(renderer_pixbuf, 'pixbuf', C_AVATAR)
297                column.set_cell_data_func(renderer_pixbuf, tree_cell_data_func,
298                        self.list_treeview)
299                renderer_pixbuf.set_property('xalign', 1) # align pixbuf to the right
300
301                self.list_treeview.append_column(column)
302
303                # workaround to avoid gtk arrows to be shown
304                column = gtk.TreeViewColumn() # 2nd COLUMN
305                renderer = gtk.CellRendererPixbuf()
306                column.pack_start(renderer, expand = False)
307                self.list_treeview.append_column(column)
308                column.set_visible(False)
309                self.list_treeview.set_expander_column(column)
310
311                gajim.gc_connected[self.account][self.room_jid] = False
312                # disable win, we are not connected yet
313                ChatControlBase.got_disconnected(self) 
314
315                self.update_ui()
316                self.conv_textview.tv.grab_focus()
317                self.widget.show_all()
318
319        def on_msg_textview_populate_popup(self, textview, menu):
320                '''we override the default context menu and we prepend Clear
321                and the ability to insert a nick'''
322                ChatControlBase.on_msg_textview_populate_popup(self, textview, menu)
323                item = gtk.SeparatorMenuItem()
324                menu.prepend(item)
325
326                item = gtk.MenuItem(_('Insert Nickname'))
327                menu.prepend(item)
328                submenu = gtk.Menu()
329                item.set_submenu(submenu)
330
331                for nick in sorted(gajim.contacts.get_nick_list(self.account,
332                self.room_jid)):
333                        item = gtk.MenuItem(nick, use_underline = False)
334                        submenu.append(item)
335                        id = item.connect('activate', self.append_nick_in_msg_textview, nick)
336                        self.handlers[id] = item
337
338                menu.show_all()
339
340        def on_treeview_size_allocate(self, widget, allocation):
341                '''The MUC treeview has resized. Move the hpaned in all tabs to match'''
342                self.hpaned_position = self.hpaned.get_position()
343                self.hpaned.set_position(self.hpaned_position)
344
345        def iter_contact_rows(self):
346                '''iterate over all contact rows in the tree model'''
347                model = self.list_treeview.get_model()
348                role_iter = model.get_iter_root()
349                while role_iter:
350                        contact_iter = model.iter_children(role_iter)
351                        while contact_iter:
352                                yield model[contact_iter]
353                                contact_iter = model.iter_next(contact_iter)
354                        role_iter = model.iter_next(role_iter)
355
356        def on_list_treeview_style_set(self, treeview, style):
357                '''When style (theme) changes, redraw all contacts'''
358                # Get the room_jid from treeview
359                for contact in self.iter_contact_rows():
360                        nick = contact[C_NICK].decode('utf-8')
361                        self.draw_contact(nick)
362
363        def on_list_treeview_selection_changed(self, selection):
364                model, selected_iter = selection.get_selected()
365                self.draw_contact(self.nick)
366                if self._last_selected_contact is not None:
367                        self.draw_contact(self._last_selected_contact)
368                if selected_iter is None:
369                        self._last_selected_contact = None
370                        return
371                contact = model[selected_iter]
372                nick = contact[C_NICK].decode('utf-8')
373                self._last_selected_contact = nick
374                if contact[C_TYPE] != 'contact':
375                        return
376                self.draw_contact(nick, selected=True, focus=True)
377
378        def get_tab_label(self, chatstate):
379                '''Markup the label if necessary. Returns a tuple such as:
380                (new_label_str, color)
381                either of which can be None
382                if chatstate is given that means we have HE SENT US a chatstate'''
383
384                has_focus = self.parent_win.window.get_property('has-toplevel-focus')
385                current_tab = self.parent_win.get_active_control() == self
386                color_name = None
387                color = None
388                theme = gajim.config.get('roster_theme')
389                if chatstate == 'attention' and (not has_focus or not current_tab):
390                        self.attention_flag = True
391                        color_name = gajim.config.get_per('themes', theme,
392                                                        'state_muc_directed_msg_color')
393                elif chatstate:
394                        if chatstate == 'active' or (current_tab and has_focus):
395                                self.attention_flag = False
396                                # get active color from gtk
397                                color = self.parent_win.notebook.style.fg[gtk.STATE_ACTIVE]
398                        elif chatstate == 'newmsg' and (not has_focus or not current_tab) and\
399                                        not self.attention_flag:
400                                color_name = gajim.config.get_per('themes', theme,
401                                        'state_muc_msg_color')
402                if color_name:
403                        color = gtk.gdk.colormap_get_system().alloc_color(color_name)
404                       
405                label_str = self.name
406               
407                # count waiting highlighted messages
408                unread = ''
409                num_unread = self.get_nb_unread()
410                if num_unread == 1:
411                        unread = '*'
412                elif num_unread > 1:
413                        unread = '[' + unicode(num_unread) + ']'
414                label_str = unread + label_str
415                return (label_str, color)
416
417        def get_tab_image(self):
418                # Set tab image (always 16x16)
419                tab_image = None
420                if gajim.gc_connected[self.account][self.room_jid]:
421                        tab_image = gajim.interface.roster.load_icon('muc_active')
422                else:
423                        tab_image = gajim.interface.roster.load_icon('muc_inactive')
424                return tab_image
425
426        def update_ui(self):
427                ChatControlBase.update_ui(self)
428                for nick in gajim.contacts.get_nick_list(self.account, self.room_jid):
429                        self.draw_contact(nick)
430
431        def _change_style(self, model, path, iter):
432                model[iter][C_NICK] = model[iter][C_NICK]
433
434        def change_roster_style(self):
435                model = self.list_treeview.get_model()
436                model.foreach(self._change_style)
437
438        def repaint_themed_widgets(self):
439                ChatControlBase.repaint_themed_widgets(self)
440                self.change_roster_style()
441
442        def _update_banner_state_image(self):
443                banner_status_img = self.xml.get_widget('gc_banner_status_image')
444                images = gajim.interface.roster.jabber_state_images
445                if gajim.gc_connected[self.account].has_key(self.room_jid) and \
446                gajim.gc_connected[self.account][self.room_jid]:
447                        image = 'muc_active'
448                else:
449                        image = 'muc_inactive'
450                if images.has_key('32') and images['32'].has_key(image):
451                        muc_icon = images['32'][image]
452                        if muc_icon.get_storage_type() != gtk.IMAGE_EMPTY:
453                                pix = muc_icon.get_pixbuf()
454                                banner_status_img.set_from_pixbuf(pix)
455                                return
456                # we need to scale 16x16 to 32x32
457                muc_icon = images['16'][image]
458                pix = muc_icon.get_pixbuf()
459                scaled_pix = pix.scale_simple(32, 32, gtk.gdk.INTERP_BILINEAR)
460                banner_status_img.set_from_pixbuf(scaled_pix)
461
462        def draw_banner_text(self):
463                '''Draw the text in the fat line at the top of the window that
464                houses the room jid, subject.
465                '''
466                self.name_label.set_ellipsize(pango.ELLIPSIZE_END)
467                font_attrs, font_attrs_small = self.get_font_attrs()
468                text = '<span %s>%s</span>' % (font_attrs, self.room_jid)
469                if self.subject:
470                        subject = helpers.reduce_chars_newlines(self.subject, max_lines = 2)
471                        subject = gtkgui_helpers.escape_for_pango_markup(subject)
472                        text += '\n<span %s>%s</span>' % (font_attrs_small, subject)
473
474                        # tooltip must always hold ALL the subject
475                        self.subject_tooltip.set_tip(self.event_box, self.subject)
476
477                self.name_label.set_markup(text)
478       
479        def prepare_context_menu(self):
480                '''sets compact view menuitem active state
481                sets sensitivity state for configure_room'''
482                menu = self.gc_popup_menu
483                childs = menu.get_children()
484                # hide chat buttons
485                childs[5].set_active(self.hide_chat_buttons_current)
486                if not gajim.connections[self.account].private_storage_supported:
487                        self.bookmark_room_menuitem.set_sensitive(False)
488                if gajim.gc_connected[self.account][self.room_jid]:
489                        c = gajim.contacts.get_gc_contact(self.account, self.room_jid,
490                                self.nick)
491                        if c.affiliation not in ('owner', 'admin'):
492                                childs[1].set_sensitive(False)
493                else:
494                        # We are not connected to this groupchat, disable unusable menuitems
495                        childs[1].set_sensitive(False)
496                        childs[2].set_sensitive(False)
497                        childs[3].set_sensitive(False)
498                return menu
499
500        def on_message(self, nick, msg, tim, has_timestamp = False, xhtml = None):
501                if not nick:
502                        # message from server
503                        self.print_conversation(msg, tim = tim, xhtml = xhtml)
504                else:
505                        # message from someone
506                        if has_timestamp:
507                                self.print_old_conversation(msg, nick, tim, xhtml)
508                        else:
509                                self.print_conversation(msg, nick, tim, xhtml)
510
511        def on_private_message(self, nick, msg, tim, xhtml, msg_id = None):
512                # Do we have a queue?
513                fjid = self.room_jid + '/' + nick
514                no_queue = len(gajim.events.get_events(self.account, fjid)) == 0
515
516                # We print if window is opened
517                pm_control = gajim.interface.msg_win_mgr.get_control(fjid, self.account)
518                if pm_control:
519                        pm_control.print_conversation(msg, tim = tim, xhtml = xhtml)
520                        return
521
522                event = gajim.events.create_event('pm', (msg, '', 'incoming', tim,
523                        False, '', msg_id, xhtml))
524                gajim.events.add_event(self.account, fjid, event)
525
526                autopopup = gajim.config.get('autopopup')
527                autopopupaway = gajim.config.get('autopopupaway')
528                iter = self.get_contact_iter(nick)
529                path = self.list_treeview.get_model().get_path(iter)
530                if not autopopup or (not autopopupaway and \
531                gajim.connections[self.account].connected > 2):
532                        if no_queue: # We didn't have a queue: we change icons
533                                model = self.list_treeview.get_model()
534                                state_images =\
535                                        gajim.interface.roster.get_appropriate_state_images(
536                                                self.room_jid, icon_name = 'message')
537                                image = state_images['message']
538                                model[iter][C_IMG] = image
539                        self.parent_win.show_title()
540                        self.parent_win.redraw_tab(self)
541                else:
542                        self._start_private_message(nick)
543                # Scroll to line
544                self.list_treeview.expand_row(path[0:1], False)
545                self.list_treeview.scroll_to_cell(path)
546                self.list_treeview.set_cursor(path)
547
548        def get_contact_iter(self, nick):
549                model = self.list_treeview.get_model()
550                fin = False
551                role_iter = model.get_iter_root()
552                if not role_iter:
553                        return None
554                while not fin:
555                        fin2 = False
556                        user_iter = model.iter_children(role_iter)
557                        if not user_iter:
558                                fin2 = True
559                        while not fin2:
560                                if nick == model[user_iter][C_NICK].decode('utf-8'):
561                                        return user_iter
562                                user_iter = model.iter_next(user_iter)
563                                if not user_iter:
564                                        fin2 = True
565                        role_iter = model.iter_next(role_iter)
566                        if not role_iter:
567                                fin = True
568                return None
569
570        gc_count_nicknames_colors = 0
571        gc_custom_colors = {} 
572
573        def print_old_conversation(self, text, contact = '', tim = None,
574        xhtml = None):
575                if isinstance(text, str):
576                        text = unicode(text, 'utf-8')
577                if contact:
578                        if contact == self.nick: # it's us
579                                kind = 'outgoing'
580                        else:
581                                kind = 'incoming'
582                else:
583                        kind = 'status'
584                if gajim.config.get('restored_messages_small'):
585                        small_attr = ['small']
586                else:
587                        small_attr = []
588                ChatControlBase.print_conversation_line(self, text, kind, contact, tim,
589                        small_attr, small_attr + ['restored_message'],
590                        small_attr + ['restored_message'], xhtml = xhtml)
591
592        def print_conversation(self, text, contact = '', tim = None, xhtml = None):
593                '''Print a line in the conversation:
594                if contact is set: it's a message from someone or an info message (contact
595                = 'info' in such a case)