root/trunk/src/groupchat_control.py

Revision 10666, 85.7 kB (checked in by asterix, 13 days ago)

hide tooltip when we press a button in groupchat. Fixes #4479

Line 
1# -*- coding:utf-8 -*-
2## src/groupchat_control.py
3##
4## Copyright (C) 2003-2008 Yann Leboulanger <asterix AT lagaule.org>
5## Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
6## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
7##                    Alex Mauer <hawke AT hawkesnest.net>
8## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
9##                         Travis Shirk <travis AT pobox.com>
10## Copyright (C) 2007-2008 Julien Pivotto <roidelapluie AT gmail.com>
11##                         Stephan Erb <steve-e AT h3c.de>
12## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
13##                    Jonathan Schleifer <js-gajim AT webkeks.org>
14##
15## This file is part of Gajim.
16##
17## Gajim is free software; you can redistribute it and/or modify
18## it under the terms of the GNU General Public License as published
19## by the Free Software Foundation; version 3 only.
20##
21## Gajim is distributed in the hope that it will be useful,
22## but WITHOUT ANY WARRANTY; without even the implied warranty of
23## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24## GNU General Public License for more details.
25##
26## You should have received a copy of the GNU General Public License
27## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
28##
29
30import os
31import time
32import gtk
33import pango
34import gobject
35import gtkgui_helpers
36import message_control
37import tooltips
38import dialogs
39import config
40import vcard
41import cell_renderer_image
42
43from common import gajim
44from common import helpers
45
46from chat_control import ChatControl
47from chat_control import ChatControlBase
48from conversation_textview import ConversationTextview
49from common.exceptions import GajimGeneralException
50
51#(status_image, type, nick, shown_nick)
52(
53C_IMG, # image to show state (online, new message etc)
54C_NICK, # contact nickame or ROLE name
55C_TYPE, # type of the row ('contact' or 'role')
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        # allocate space for avatar only if needed
74        parent_iter = model.iter_parent(iter_)
75        if isinstance(renderer, gtk.CellRendererPixbuf):
76                avatar_position = gajim.config.get('avatar_position_in_roster')
77                if avatar_position == 'right':
78                        renderer.set_property('xalign', 1) # align pixbuf to the right
79                else:
80                        renderer.set_property('xalign', 0.5)
81                if parent_iter and (model[iter_][C_AVATAR] or avatar_position == 'left'):
82                        renderer.set_property('visible', True)
83                        renderer.set_property('width', gajim.config.get('roster_avatar_width'))
84                else:
85                        renderer.set_property('visible', False)
86        if parent_iter:
87                bgcolor = gajim.config.get_per('themes', theme, 'contactbgcolor')
88                if bgcolor:
89                        renderer.set_property('cell-background', bgcolor)
90                else:
91                        renderer.set_property('cell-background', None)
92                if isinstance(renderer, gtk.CellRendererText):
93                        # foreground property is only with CellRendererText
94                        color = gajim.config.get_per('themes', theme, 'contacttextcolor')
95                        if color:
96                                renderer.set_property('foreground', color)
97                        else:
98                                renderer.set_property('foreground', None)
99                        renderer.set_property('font',
100                                gtkgui_helpers.get_theme_font_for_option(theme, 'contactfont'))
101        else: # it is root (eg. group)
102                bgcolor = gajim.config.get_per('themes', theme, 'groupbgcolor')
103                if bgcolor:
104                        renderer.set_property('cell-background', bgcolor)
105                else:
106                        set_renderer_color(tv, renderer)
107                if isinstance(renderer, gtk.CellRendererText):
108                        # foreground property is only with CellRendererText
109                        color = gajim.config.get_per('themes', theme, 'grouptextcolor')
110                        if color:
111                                renderer.set_property('foreground', color)
112                        else:
113                                set_renderer_color(tv, renderer, False)
114                        renderer.set_property('font',
115                                gtkgui_helpers.get_theme_font_for_option(theme, 'groupfont'))
116
117class PrivateChatControl(ChatControl):
118        TYPE_ID = message_control.TYPE_PM
119
120        def __init__(self, parent_win, gc_contact, contact, account, session):
121                room_jid = contact.jid.split('/')[0]
122                room_ctrl = gajim.interface.msg_win_mgr.get_gc_control(room_jid, account)
123                if room_jid in gajim.interface.minimized_controls[account]:
124                        room_ctrl = gajim.interface.minimized_controls[account][room_jid]
125                self.room_name = room_ctrl.name
126                self.gc_contact = gc_contact
127                ChatControl.__init__(self, parent_win, contact, account, session)
128                self.TYPE_ID = 'pm'
129
130        def send_message(self, message, xhtml=None):
131                '''call this function to send our message'''
132                if not message:
133                        return
134
135                message = helpers.remove_invalid_xml_chars(message)
136
137                if not message:
138                        return
139
140                # We need to make sure that we can still send through the room and that
141                # the recipient did not go away
142                contact = gajim.contacts.get_first_contact_from_jid(self.account,
143                        self.contact.jid)
144                if contact is None:
145                        # contact was from pm in MUC
146                        room, nick = gajim.get_room_and_nick_from_fjid(self.contact.jid)
147                        gc_contact = gajim.contacts.get_gc_contact(self.account, room, nick)
148                        if not gc_contact:
149                                dialogs.ErrorDialog(
150                                        _('Sending private message failed'),
151                                        #in second %s code replaces with nickname
152                                        _('You are no longer in group chat "%(room)s" or "%(nick)s" has '
153                                        'left.') % {'room': room, 'nick': nick})
154                                return
155
156                ChatControl.send_message(self, message, xhtml=xhtml)
157
158        def update_ui(self):
159                if self.contact.show == 'offline':
160                        self.got_disconnected()
161                else:
162                        self.got_connected()
163                ChatControl.update_ui(self)
164
165        def update_contact(self):
166                self.contact = gajim.contacts.contact_from_gc_contact(self.gc_contact)
167
168
169class GroupchatControl(ChatControlBase):
170        TYPE_ID = message_control.TYPE_GC
171        # alphanum sorted
172        MUC_CMDS = ['ban', 'chat', 'query', 'clear', 'close', 'compact',
173                'help', 'invite', 'join', 'kick', 'leave', 'me', 'msg', 'nick',
174                'part', 'names', 'say', 'topic']
175
176        def __init__(self, parent_win, contact, acct, is_continued=False):
177                ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
178                                        'muc_child_vbox', contact, acct);
179
180                self.is_continued=is_continued
181                self.is_anonymous = True
182                self.change_nick_dialog = None
183
184                self.actions_button = self.xml.get_widget('muc_window_actions_button')
185                id = self.actions_button.connect('clicked', self.on_actions_button_clicked)
186                self.handlers[id] = self.actions_button
187
188                widget = self.xml.get_widget('change_nick_button')
189                id = widget.connect('clicked', self._on_change_nick_menuitem_activate)
190                self.handlers[id] = widget
191
192                widget = self.xml.get_widget('change_subject_button')
193                id = widget.connect('clicked', self._on_change_subject_menuitem_activate)
194                self.handlers[id] = widget
195
196                widget = self.xml.get_widget('bookmark_button')
197                for bm in gajim.connections[self.account].bookmarks:
198                        if bm['jid'] == self.contact.jid:
199                                widget.hide()
200                                break
201                else:
202                        id = widget.connect('clicked',
203                                self._on_bookmark_room_menuitem_activate)
204                        self.handlers[id] = widget
205                        widget.show()
206
207                widget = self.xml.get_widget('list_treeview')
208                id = widget.connect('row_expanded', self.on_list_treeview_row_expanded)
209                self.handlers[id] = widget
210
211                id = widget.connect('row_collapsed', 
212                        self.on_list_treeview_row_collapsed)
213                self.handlers[id] = widget
214
215                id = widget.connect('row_activated', 
216                        self.on_list_treeview_row_activated)
217                self.handlers[id] = widget
218
219                id = widget.connect('button_press_event', 
220                        self.on_list_treeview_button_press_event)
221                self.handlers[id] = widget
222
223                id = widget.connect('key_press_event', 
224                        self.on_list_treeview_key_press_event)
225                self.handlers[id] = widget
226
227                id = widget.connect('motion_notify_event', 
228                        self.on_list_treeview_motion_notify_event)
229                self.handlers[id] = widget
230
231                id = widget.connect('leave_notify_event', 
232                        self.on_list_treeview_leave_notify_event)
233                self.handlers[id] = widget
234
235                self.room_jid = self.contact.jid
236                self.nick = contact.name.decode('utf-8')
237                self.new_nick = ''
238                self.name = ''
239                for bm in gajim.connections[self.account].bookmarks:
240                        if bm['jid'] == self.room_jid:
241                                self.name = bm['name']
242                                break
243                if not self.name:
244                        self.name = self.room_jid.split('@')[0]
245
246                compact_view = gajim.config.get('compact_view')
247                self.chat_buttons_set_visible(compact_view)
248                self.widget_set_visible(self.xml.get_widget('banner_eventbox'),
249                        gajim.config.get('hide_groupchat_banner'))
250                self.widget_set_visible(self.xml.get_widget('list_scrolledwindow'),
251                        gajim.config.get('hide_groupchat_occupants_list'))
252
253                self._last_selected_contact = None # None or holds jid, account tuple
254
255                # muc attention flag (when we are mentioned in a muc)
256                # if True, the room has mentioned us
257                self.attention_flag = False
258
259                # sorted list of nicks who mentioned us (last at the end)
260                self.attention_list = []
261                self.room_creation = int(time.time()) # Use int to reduce mem usage
262                self.nick_hits = []
263                self.cmd_hits = []
264                self.last_key_tabs = False
265
266                self.subject = ''
267                self.subject_tooltip = gtk.Tooltips()
268
269                self.tooltip = tooltips.GCTooltip()
270
271                # nickname coloring
272                self.gc_count_nicknames_colors = 0
273                self.gc_custom_colors = {} 
274                self.number_of_colors = len(gajim.config.get('gc_nicknames_colors').\
275                        split(':'))
276
277                # connect the menuitems to their respective functions
278                xm = gtkgui_helpers.get_glade('gc_control_popup_menu.glade')
279
280                self.bookmark_room_menuitem = xm.get_widget('bookmark_room_menuitem')
281                id = self.bookmark_room_menuitem.connect('activate',
282                        self._on_bookmark_room_menuitem_activate)
283                self.handlers[id] = self.bookmark_room_menuitem
284
285                self.change_nick_menuitem = xm.get_widget('change_nick_menuitem')
286                id = self.change_nick_menuitem.connect('activate',
287                        self._on_change_nick_menuitem_activate)
288                self.handlers[id] = self.change_nick_menuitem
289
290                self.configure_room_menuitem = xm.get_widget('configure_room_menuitem')
291                id = self.configure_room_menuitem.connect('activate',
292                        self._on_configure_room_menuitem_activate)
293                self.handlers[id] = self.configure_room_menuitem
294
295                self.destroy_room_menuitem = xm.get_widget('destroy_room_menuitem')
296                id = self.destroy_room_menuitem.connect('activate',
297                        self._on_destroy_room_menuitem_activate)
298                self.handlers[id] = self.destroy_room_menuitem
299
300                self.change_subject_menuitem = xm.get_widget('change_subject_menuitem')
301                id = self.change_subject_menuitem.connect('activate',
302                        self._on_change_subject_menuitem_activate)
303                self.handlers[id] = self.change_subject_menuitem
304
305                self.history_menuitem = xm.get_widget('history_menuitem')
306                id = self.history_menuitem.connect('activate',
307                        self._on_history_menuitem_activate)
308                self.handlers[id] = self.history_menuitem
309
310                self.minimize_menuitem = xm.get_widget('minimize_menuitem')
311                id = self.minimize_menuitem.connect('toggled',
312                        self.on_minimize_menuitem_toggled)
313                self.handlers[id] = self.minimize_menuitem
314
315                self.bookmark_separator = xm.get_widget('bookmark_separator')
316                self.separatormenuitem2 = xm.get_widget('separatormenuitem2')
317
318                self.gc_popup_menu = xm.get_widget('gc_control_popup_menu')
319
320                self.name_label = self.xml.get_widget('banner_name_label')
321                self.event_box = self.xml.get_widget('banner_eventbox')
322
323                # set the position of the current hpaned
324                self.hpaned_position = gajim.config.get('gc-hpaned-position')
325                self.hpaned = self.xml.get_widget('hpaned')
326                self.hpaned.set_position(self.hpaned_position)
327
328                self.list_treeview = self.xml.get_widget('list_treeview')
329                selection = self.list_treeview.get_selection()
330                id = selection.connect('changed', 
331                                self.on_list_treeview_selection_changed)
332                self.handlers[id] = selection
333                id = self.list_treeview.connect('style-set',
334                        self.on_list_treeview_style_set)
335                self.handlers[id] = self.list_treeview
336                # we want to know when the the widget resizes, because that is
337                # an indication that the hpaned has moved...
338                # FIXME: Find a better indicator that the hpaned has moved.
339                id = self.list_treeview.connect('size-allocate',
340                        self.on_treeview_size_allocate)
341                self.handlers[id] = self.list_treeview
342                #status_image, shown_nick, type, nickname, avatar
343                store = gtk.TreeStore(gtk.Image, str, str, str, gtk.gdk.Pixbuf)
344                store.set_sort_func(C_NICK, self.tree_compare_iters)
345                store.set_sort_column_id(C_NICK, gtk.SORT_ASCENDING)
346                self.list_treeview.set_model(store)
347
348                # columns
349
350                # this col has 3 cells:
351                # first one img, second one text, third is sec pixbuf
352                column = gtk.TreeViewColumn()
353
354                def add_avatar_renderer():
355                        renderer_pixbuf = gtk.CellRendererPixbuf() # avatar image
356                        column.pack_start(renderer_pixbuf, expand = False)
357                        column.add_attribute(renderer_pixbuf, 'pixbuf', C_AVATAR)
358                        column.set_cell_data_func(renderer_pixbuf, tree_cell_data_func,
359                                self.list_treeview)
360
361                if gajim.config.get('avatar_position_in_roster') == 'left':
362                        add_avatar_renderer()
363
364                renderer_image = cell_renderer_image.CellRendererImage(0, 0) # status img
365                renderer_image.set_property('width', 26)
366                column.pack_start(renderer_image, expand = False)
367                column.add_attribute(renderer_image, 'image', C_IMG)
368                column.set_cell_data_func(renderer_image, tree_cell_data_func, 
369                        self.list_treeview)
370
371                renderer_text = gtk.CellRendererText() # nickname
372                column.pack_start(renderer_text, expand = True)
373                column.add_attribute(renderer_text, 'markup', C_TEXT)
374                renderer_text.set_property("ellipsize", pango.ELLIPSIZE_END)
375                column.set_cell_data_func(renderer_text, tree_cell_data_func,
376                        self.list_treeview)
377
378                if gajim.config.get('avatar_position_in_roster') == 'right':
379                        add_avatar_renderer()
380
381                self.list_treeview.append_column(column)
382
383                # workaround to avoid gtk arrows to be shown
384                column = gtk.TreeViewColumn() # 2nd COLUMN
385                renderer = gtk.CellRendererPixbuf()
386                column.pack_start(renderer, expand = False)
387                self.list_treeview.append_column(column)
388                column.set_visible(False)
389                self.list_treeview.set_expander_column(column)
390
391                gajim.gc_connected[self.account][self.room_jid] = False
392                # disable win, we are not connected yet
393                ChatControlBase.got_disconnected(self)
394
395                self.update_ui()
396                self.conv_textview.tv.grab_focus()
397                self.widget.show_all()
398
399        def tree_compare_iters(self, model, iter1, iter2):
400                '''Compare two iters to sort them'''
401                type1 = model[iter1][C_TYPE]
402                type2 = model[iter2][C_TYPE]
403                if not type1 or not type2:
404                        return 0
405                nick1 = model[iter1][C_NICK]
406                nick2 = model[iter2][C_NICK]
407                if not nick1 or not nick2:
408                        return 0
409                nick1 = nick1.decode('utf-8')
410                nick2 = nick2.decode('utf-8')
411                if type1 == 'role':
412                        if nick1 < nick2:
413                                return -1
414                        return 1
415                if type1 == 'contact':
416                        gc_contact1 = gajim.contacts.get_gc_contact(self.account, self.room_jid,
417                                nick1)
418                        if not gc_contact1:
419                                return 0
420                if type2 == 'contact':
421                        gc_contact2 = gajim.contacts.get_gc_contact(self.account, self.room_jid,
422                                nick2)
423                        if not gc_contact2:
424                                return 0
425                if type1 == 'contact' and type2 == 'contact' and \
426                gajim.config.get('sort_by_show_in_muc'):
427                        cshow = {'chat':0, 'online': 1, 'away': 2, 'xa': 3, 'dnd': 4,
428                                'invisible': 5, 'offline': 6, 'error': 7}
429                        show1 = cshow[gc_contact1.show]
430                        show2 = cshow[gc_contact2.show]
431                        if show1 < show2:
432                                return -1
433                        elif show1 > show2:
434                                return 1
435                # We compare names
436                name1 = gc_contact1.get_shown_name()
437                name2 = gc_contact2.get_shown_name()
438                if name1.lower() < name2.lower():
439                        return -1
440                if name2.lower() < name1.lower():
441                        return 1
442                return 0
443
444        def on_msg_textview_populate_popup(self, textview, menu):
445                '''we override the default context menu and we prepend Clear
446                and the ability to insert a nick'''
447                ChatControlBase.on_msg_textview_populate_popup(self, textview, menu)
448                item = gtk.SeparatorMenuItem()
449                menu.prepend(item)
450
451                item = gtk.MenuItem(_('Insert Nickname'))
452                menu.prepend(item)
453                submenu = gtk.Menu()
454                item.set_submenu(submenu)
455
456                for nick in sorted(gajim.contacts.get_nick_list(self.account,
457                self.room_jid)):
458                        item = gtk.MenuItem(nick, use_underline = False)
459                        submenu.append(item)
460                        id = item.connect('activate', self.append_nick_in_msg_textview, nick)
461                        self.handlers[id] = item
462
463                menu.show_all()
464
465        def on_treeview_size_allocate(self, widget, allocation):
466                '''The MUC treeview has resized. Move the hpaned in all tabs to match'''
467                self.hpaned_position = self.hpaned.get_position()
468                self.hpaned.set_position(self.hpaned_position)
469
470        def iter_contact_rows(self):
471                '''iterate over all contact rows in the tree model'''
472                model = self.list_treeview.get_model()
473                role_iter = model.get_iter_root()
474                while role_iter:
475                        contact_iter = model.iter_children(role_iter)
476                        while contact_iter:
477                                yield model[contact_iter]
478                                contact_iter = model.iter_next(contact_iter)
479                        role_iter = model.iter_next(role_iter)
480
481        def on_list_treeview_style_set(self, treeview, style):
482                '''When style (theme) changes, redraw all contacts'''
483                # Get the room_jid from treeview
484                for contact in self.iter_contact_rows():
485                        nick = contact[C_NICK].decode('utf-8')
486                        self.draw_contact(nick)
487
488        def on_list_treeview_selection_changed(self, selection):
489                model, selected_iter = selection.get_selected()
490                self.draw_contact(self.nick)
491                if self._last_selected_contact is not None:
492                        self.draw_contact(self._last_selected_contact)
493                if selected_iter is None:
494                        self._last_selected_contact = None
495                        return
496                contact = model[selected_iter]
497                nick = contact[C_NICK].decode('utf-8')
498                self._last_selected_contact = nick
499                if contact[C_TYPE] != 'contact':
500                        return
501                self.draw_contact(nick, selected=True, focus=True)
502
503        def get_tab_label(self, chatstate):
504                '''Markup the label if necessary. Returns a tuple such as:
505                (new_label_str, color)
506                either of which can be None
507                if chatstate is given that means we have HE SENT US a chatstate'''
508
509                has_focus = self.parent_win.window.get_property('has-toplevel-focus')
510                current_tab = self.parent_win.get_active_control() == self
511                color_name = None
512                color = None
513                theme = gajim.config.get('roster_theme')
514                if chatstate == 'attention' and (not has_focus or not current_tab):
515                        self.attention_flag = True
516                        color_name = gajim.config.get_per('themes', theme,
517                                                        'state_muc_directed_msg_color')
518                elif chatstate:
519                        if chatstate == 'active' or (current_tab and has_focus):
520                                self.attention_flag = False
521                                # get active color from gtk
522                                color = self.parent_win.notebook.style.fg[gtk.STATE_ACTIVE]
523                        elif chatstate == 'newmsg' and (not has_focus or not current_tab) and\
524                                        not self.attention_flag:
525                                color_name = gajim.config.get_per('themes', theme,
526                                        'state_muc_msg_color')
527                if color_name:
528                        color = gtk.gdk.colormap_get_system().alloc_color(color_name)
529
530                if self.is_continued:
531                        # if this is a continued conversation
532                        label_str = self.get_continued_conversation_name()
533                else:
534                        label_str = self.name
535
536                # count waiting highlighted messages
537                unread = ''
538                num_unread = self.get_nb_unread()
539                if num_unread == 1:
540                        unread = '*'
541                elif num_unread > 1:
542                        unread = '[' + unicode(num_unread) + ']'
543                label_str = unread + label_str
544                return (label_str, color)
545
546        def get_tab_image(self):
547                # Set tab image (always 16x16)
548                tab_image = None
549                if gajim.gc_connected[self.account][self.room_jid]:
550                        tab_image = gtkgui_helpers.load_icon('muc_active')
551                else:
552                        tab_image = gtkgui_helpers.load_icon('muc_inactive')
553                return tab_image
554
555        def update_ui(self):
556                ChatControlBase.update_ui(self)
557                for nick in gajim.contacts.get_nick_list(self.account, self.room_jid):
558                        self.draw_contact(nick)
559
560        def _change_style(self, model, path, iter_):
561                model[iter_][C_NICK] = model[iter_][C_NICK]
562
563        def change_roster_style(self):
564                model = self.list_treeview.get_model()
565                model.foreach(self._change_style)
566
567        def repaint_themed_widgets(self):
568                ChatControlBase.repaint_themed_widgets(self)
569                self.change_roster_style()
570
571        def _update_banner_state_image(self):
572                banner_status_img = self.xml.get_widget('gc_banner_status_image')
573                images = gajim.interface.jabber_state_images
574                if self.room_jid in gajim.gc_connected[self.account] and \
575                gajim.gc_connected[self.account][self.room_jid]:
576                        image = 'muc_active'
577                else:
578                        image = 'muc_inactive'
579                if '32' in images and image in images['32']:
580                        muc_icon = images['32'][image]
581                        if muc_icon.get_storage_type() != gtk.IMAGE_EMPTY:
582                                pix = muc_icon.get_pixbuf()
583                                banner_status_img.set_from_pixbuf(pix)
584                                return
585                # we need to scale 16x16 to 32x32
586                muc_icon = images['16'][image]
587                pix = muc_icon.get_pixbuf()
588                scaled_pix = pix.scale_simple(32, 32, gtk.gdk.INTERP_BILINEAR)
589                banner_status_img.set_from_pixbuf(scaled_pix)
590
591        def get_continued_conversation_name(self):
592                '''Get the name of a continued conversation.
593                Will return Continued Conversation if there isn't any other
594                contact in the room
595                '''
596                nicks = []
597                for nick in gajim.contacts.get_nick_list(self.account,
598                self.room_jid):
599                        if nick != self.nick: