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

Revision 4763, 62.7 kB (checked in by asterix, 3 years ago)

don't remove contact in gc roster when he leaves if there are awaiting events from him, but only when we read his messages

  • Property svn:eol-style set to LF
Line 
1## groupchat_window.py
2##
3## Contributors for this file:
4## - Yann Le Boulanger <asterix@lagaule.org>
5## - Nikos Kouremenos <kourem@gmail.com>
6## - Travis Shirk <travis@pobox.com>
7##
8## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
9##                         Vincent Hanquez <tab@snarc.org>
10## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
11##                    Vincent Hanquez <tab@snarc.org>
12##                    Nikos Kouremenos <nkour@jabber.org>
13##                    Dimitur Kirov <dkirov@gmail.com>
14##                    Travis Shirk <travis@pobox.com>
15##                    Norman Rasmussen <norman@rasmussen.co.za>
16##
17## This program 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 2 only.
20##
21## This program 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
27import gtk
28import gtk.glade
29import pango
30import gobject
31import time
32import os
33
34import dialogs
35import vcard
36import chat
37import cell_renderer_image
38import gtkgui_helpers
39import history_window
40import tooltips
41
42from gajim import Contact
43from common import gajim
44from common import helpers
45from gettext import ngettext
46from common import i18n
47
48_ = i18n._
49Q_ = i18n.Q_
50APP = i18n.APP
51gtk.glade.bindtextdomain(APP, i18n.DIR)
52gtk.glade.textdomain(APP)
53
54#(status_image, type, nick, shown_nick)
55(
56C_IMG, # image to show state (online, new message etc)
57C_TYPE, # type of the row ('contact' or 'group')
58C_NICK, # contact nickame or group name
59C_TEXT, # text shown in the cellrenderer
60) = range(4)
61
62GTKGUI_GLADE = 'gtkgui.glade'
63
64class GroupchatWindow(chat.Chat):
65        '''Class for Groupchat window'''
66        def __init__(self, room_jid, nick, account):
67                # we check that on opening new windows
68                self.always_compact_view = gajim.config.get('always_compact_view_gc')
69                chat.Chat.__init__(self, account, 'groupchat_window')
70
71                # alphanum sorted
72                self.muc_cmds = ['ban', 'chat', 'query', 'clear', 'close', 'compact', 'help', 'invite',
73                        'join', 'kick', 'leave', 'me', 'msg', 'nick', 'part', 'say', 'topic']
74
75                self.nicks = {} # our nick for each groupchat we are in
76                self.list_treeview = {}
77                self.subjects = {}
78                self.name_labels = {}
79                self.subject_tooltip = {}
80                self.room_creation = {}
81                self.nick_hits = {} # possible candidates for nick completion
82                self.cmd_hits = {} # possible candidates for command completion
83                self.last_key_tabs = {}
84                self.hpaneds = {} # used for auto positioning
85                # holds the iter's offset which points to the end of --- line per jid
86                self.focus_out_end_iter_offset = {}
87                self.allow_focus_out_line = {}
88                self.hpaned_position = gajim.config.get('gc-hpaned-position')
89                self.gc_refer_to_nick_char = gajim.config.get('gc_refer_to_nick_char')
90                self.new_room(room_jid, nick)
91                self.show_title()
92                self.tooltip = tooltips.GCTooltip()
93
94
95                # NOTE: if it not a window event, connect in new_room function
96                signal_dict = {
97'on_groupchat_window_destroy': self.on_groupchat_window_destroy,
98'on_groupchat_window_delete_event': self.on_groupchat_window_delete_event,
99'on_groupchat_window_focus_in_event': self.on_groupchat_window_focus_in_event,
100'on_chat_notebook_key_press_event': self.on_chat_notebook_key_press_event,
101'on_chat_notebook_switch_page': self.on_chat_notebook_switch_page,
102                }
103
104                self.xml.signal_autoconnect(signal_dict)
105
106                # get size and position from config
107                if gajim.config.get('saveposition') and \
108                        not gtkgui_helpers.one_window_opened('gc'):
109                        gtkgui_helpers.move_window(self.window,
110                                gajim.config.get('gc-x-position'),
111                                gajim.config.get('gc-y-position'))
112                        gtkgui_helpers.resize_window(self.window,
113                                gajim.config.get('gc-width'),
114                                gajim.config.get('gc-height'))
115                self.window.show_all()
116
117        def save_var(self, room_jid):
118                if not room_jid in self.nicks:
119                        return {}
120                return {
121                        'nick': self.nicks[room_jid],
122                        'model': self.list_treeview[room_jid].get_model(),
123                        'subject': self.subjects[room_jid],
124                        'contacts': gajim.gc_contacts[self.account][room_jid],
125                        'connected': gajim.gc_connected[self.account][room_jid],
126                }
127
128        def load_var(self, room_jid, var):
129                if not self.xmls.has_key(room_jid):
130                        return
131                self.list_treeview[room_jid].set_model(var['model'])
132                self.list_treeview[room_jid].expand_all()
133                self.set_subject(room_jid, var['subject'])
134                self.subjects[room_jid] = var['subject']
135                gajim.gc_contacts[self.account][room_jid] = var['contacts']
136                gajim.gc_connected[self.account][room_jid] = var['connected']
137                if gajim.gc_connected[self.account][room_jid]:
138                        self.got_connected(room_jid)
139
140        def confirm_close(self, room_jid = None):
141                '''Returns True if we confirm we want to close
142                Returns False if we don't want to close anymore
143                if room_jid is given, ask only for it else ask for all opened rooms'''
144                # whether to ask for comfirmation before closing muc
145                if gajim.config.get('confirm_close_muc'):
146                        names = []
147                        if not room_jid:
148                                for r_jid in self.xmls:
149                                        if gajim.gc_connected[self.account][r_jid]:
150                                                names.append(gajim.get_nick_from_jid(r_jid))
151                        else:
152                                names = [room_jid]
153
154                        rooms_no = len(names)
155                        if rooms_no >= 2: # if we are in many rooms
156                                pritext = _('Are you sure you want to leave rooms "%s"?') % ', '.join(names)
157                                sectext = _('If you close this window, you will be disconnected from these rooms.')
158
159                        elif rooms_no == 1: # just in one room
160                                pritext = _('Are you sure you want to leave room "%s"?') % names[0]
161                                sectext = _('If you close this window, you will be disconnected from this room.')
162
163                        if rooms_no > 0:
164                                dialog = dialogs.ConfirmationDialogCheck(pritext, sectext,
165                                        _('Do _not ask me again'))
166
167                                if dialog.is_checked():
168                                        gajim.config.set('confirm_close_muc', False)
169                                        dialog.destroy()
170
171                                if dialog.get_response() != gtk.RESPONSE_OK:
172                                        return False
173                return True
174
175        def on_groupchat_window_delete_event(self, widget, event):
176                '''close window'''
177                if not self.confirm_close():
178                        return True # stop propagation of the delete event
179                for room_jid in self.xmls:
180                        if gajim.gc_connected[self.account][room_jid]:
181                                gajim.connections[self.account].send_gc_status(self.nicks[room_jid],
182                                        room_jid, 'offline', 'offline')
183
184                if gajim.config.get('saveposition'):
185                        # save window position and size
186                        gajim.config.set('gc-hpaned-position', self.hpaned_position)
187                        x, y = self.window.get_position()
188                        gajim.config.set('gc-x-position', x)
189                        gajim.config.set('gc-y-position', y)
190                        width, height = self.window.get_size()
191                        gajim.config.set('gc-width', width)
192                        gajim.config.set('gc-height', height)
193
194        def on_groupchat_window_destroy(self, widget):
195                chat.Chat.on_window_destroy(self, widget, 'gc')
196                for room_jid in self.xmls:
197                        del gajim.gc_contacts[self.account][room_jid]
198                        del gajim.gc_connected[self.account][room_jid]
199
200        def on_groupchat_window_focus_in_event(self, widget, event):
201                '''When window gets focus'''
202                room_jid = self.get_active_jid()
203                self.allow_focus_out_line[room_jid] = True
204                chat.Chat.on_chat_window_focus_in_event(self, widget, event)
205
206        def check_and_possibly_add_focus_out_line(self, room_jid):
207                '''checks and possibly adds focus out line for room_jid if it needs it
208                and does not already have it as last event. If it goes to add this line
209                it removes previous line first'''
210
211                if room_jid == self.get_active_jid() and self.window.get_property('has-toplevel-focus'):
212                        # it's the current room and it's the focused window.
213                        # we have full focus (we are reading it!)
214                        return
215
216                if not self.allow_focus_out_line[room_jid]:
217                        # if room did not receive focus-in from the last time we added
218                        # --- line then do not readd
219                        return
220
221                print_focus_out_line = False
222                textview = self.conversation_textviews[room_jid]
223                buffer = textview.get_buffer()
224
225                if self.focus_out_end_iter_offset[room_jid] is None:
226                        # this happens only first time we focus out on this room
227                        print_focus_out_line = True
228
229                else:
230                        if self.focus_out_end_iter_offset[room_jid] != buffer.get_end_iter().get_offset():
231                                # this means after last-focus something was printed
232                                # (else end_iter's offset is the same as before)
233                                # only then print ---- line (eg. we avoid printing many following
234                                # ---- lines)
235                                print_focus_out_line = True
236
237                if print_focus_out_line and buffer.get_char_count() > 0:
238                        buffer.begin_user_action()
239
240                        # remove previous focus out line if such focus out line exists
241                        if self.focus_out_end_iter_offset[room_jid] is not None:
242                                end_iter_for_previous_line = buffer.get_iter_at_offset(
243                                        self.focus_out_end_iter_offset[room_jid])
244                                begin_iter_for_previous_line = end_iter_for_previous_line.copy()
245                                begin_iter_for_previous_line.backward_chars(2) # img_char+1 (the '\n')
246
247                                # remove focus out line
248                                buffer.delete(begin_iter_for_previous_line,
249                                        end_iter_for_previous_line)
250
251                        # add the new focus out line
252                        path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps', 'muc_separator.png')
253                        focus_out_line_pixbuf = gtk.gdk.pixbuf_new_from_file(path_to_file)
254                        end_iter = buffer.get_end_iter()
255                        buffer.insert(end_iter, '\n')
256                        buffer.insert_pixbuf(end_iter, focus_out_line_pixbuf)
257
258                        end_iter = buffer.get_end_iter()
259                        before_img_iter = end_iter.copy()
260                        before_img_iter.backward_char() # one char back (an image also takes one char)
261                        buffer.apply_tag_by_name('focus-out-line', before_img_iter, end_iter)
262                        #FIXME: remove this workaround when bug is fixed
263                        # c http://bugzilla.gnome.org/show_bug.cgi?id=318569
264
265                        self.allow_focus_out_line[room_jid] = False
266
267                        # update the iter we hold to make comparison the next time
268                        self.focus_out_end_iter_offset[room_jid] = buffer.get_end_iter(
269                                ).get_offset()
270
271                        buffer.end_user_action()
272
273                        # scroll to the end (via idle in case the scrollbar has appeared)
274                        gobject.idle_add(textview.scroll_to_end)
275
276        def on_chat_notebook_key_press_event(self, widget, event):
277                chat.Chat.on_chat_notebook_key_press_event(self, widget, event)
278
279        def on_chat_notebook_switch_page(self, notebook, page, page_num):
280                old_child = notebook.get_nth_page(notebook.get_current_page())
281                new_child = notebook.get_nth_page(page_num)
282                old_jid = ''
283                new_jid = ''
284                for room_jid in self.xmls:
285                        if self.childs[room_jid] == new_child:
286                                new_jid = room_jid
287                                self.redraw_tab(new_jid, 'active')
288                        elif self.childs[room_jid] == old_child:
289                                old_jid = room_jid
290                                self.redraw_tab(old_jid, 'active')
291                        if old_jid != '' and new_jid != '': # we found both jids
292                                break # so stop looping
293
294                subject = self.subjects[new_jid]
295                subject_escaped = gtkgui_helpers.escape_for_pango_markup(subject)
296                new_jid_escaped = gtkgui_helpers.escape_for_pango_markup(new_jid)
297
298                name_label = self.name_labels[new_jid]
299                name_label.set_markup('<span weight="heavy" size="x-large">%s</span>\n%s'\
300                        % (new_jid_escaped, subject_escaped))
301                event_box = name_label.get_parent()
302                if subject == '':
303                        subject = _('This room has no subject')
304                self.subject_tooltip[new_jid].set_tip(event_box, subject)
305
306                if len(self.xmls) > 1 and old_jid != '': # if we have more than one tab
307                        # and we have the old_jid (eg we did NOT close the tab
308                        # but we just switched)
309                        # then add the focus-out line to the tab we are leaving
310                        self.check_and_possibly_add_focus_out_line(old_jid)
311                chat.Chat.on_chat_notebook_switch_page(self, notebook, page, page_num)
312
313        def get_role_iter(self, room_jid, role):
314                model = self.list_treeview[room_jid].get_model()
315                fin = False
316                iter = model.get_iter_root()
317                if not iter:
318                        return None
319                while not fin:
320                        role_name = model[iter][C_NICK].decode('utf-8')
321                        if role == role_name:
322                                return iter
323                        iter = model.iter_next(iter)
324                        if not iter:
325                                fin = True
326                return None
327
328        def get_contact_iter(self, room_jid, nick):
329                model = self.list_treeview[room_jid].get_model()
330                fin = False
331                role_iter = model.get_iter_root()
332                if not role_iter:
333                        return None
334                while not fin:
335                        fin2 = False
336                        user_iter = model.iter_children(role_iter)
337                        if not user_iter:
338                                fin2 = True
339                        while not fin2:
340                                if nick == model[user_iter][C_NICK].decode('utf-8'):
341                                        return user_iter
342                                user_iter = model.iter_next(user_iter)
343                                if not user_iter:
344                                        fin2 = True
345                        role_iter = model.iter_next(role_iter)
346                        if not role_iter:
347                                fin = True
348                return None
349
350        def get_nick_list(self, room_jid):
351                '''get nicks of contacts in a room'''
352                return gajim.gc_contacts[self.account][room_jid].keys()
353
354        def remove_contact(self, room_jid, nick):
355                '''Remove a user from the contacts_list'''
356                model = self.list_treeview[room_jid].get_model()
357                iter = self.get_contact_iter(room_jid, nick)
358                if not iter:
359                        return
360                if gajim.gc_contacts[self.account][room_jid].has_key(nick):
361                        del gajim.gc_contacts[self.account][room_jid][nick]
362                parent_iter = model.iter_parent(iter)
363                model.remove(iter)
364                if model.iter_n_children(parent_iter) == 0:
365                        model.remove(parent_iter)
366
367        def add_contact_to_roster(self, room_jid, nick, show, role, jid, affiliation, status):
368                model = self.list_treeview[room_jid].get_model()
369                resource = ''
370                role_name = helpers.get_uf_role(role, plural = True)
371
372                if jid:
373                        jids = jid.split('/', 1)
374                        j = jids[0]
375                        if len(jids) > 1:
376                                resource = jids[1]
377                else:
378                        j = ''
379
380                name = nick
381
382                role_iter = self.get_role_iter(room_jid, role)
383                if not role_iter:
384                        role_iter = model.append(None,
385                                (gajim.interface.roster.jabber_state_images['16']['closed'], 'role', role,
386                                '<b>%s</b>' % role_name))
387                iter = model.append(role_iter, (None, 'contact', nick, name))
388                if not gajim.gc_contacts[self.account][room_jid].has_key(nick):
389                        gajim.gc_contacts[self.account][room_jid][nick] = \
390                                Contact(jid = j, name = nick, show = show, resource = resource,
391                                role = role, affiliation = affiliation, status = status)
392                self.draw_contact(room_jid, nick)
393                if nick == self.nicks[room_jid]: # we became online
394                        self.got_connected(room_jid)
395                self.list_treeview[room_jid].expand_row((model.get_path(role_iter)),
396                        False)
397                return iter
398
399        def draw_contact(self, room_jid, nick, selected=False, focus=False):
400                iter = self.get_contact_iter(room_jid, nick)
401                if not iter:
402                        return
403                model = self.list_treeview[room_jid].get_model()
404                contact = gajim.gc_contacts[self.account][room_jid][nick]
405                state_images = gajim.interface.roster.jabber_state_images['16']
406                if gajim.awaiting_events[self.account].has_key(room_jid + '/' + nick):
407                        image = state_images['message']
408                else:
409                        image = state_images[contact.show]
410
411                name = gtkgui_helpers.escape_for_pango_markup(contact.name)
412                status = contact.status
413                # add status msg, if not empty, under contact name in the treeview
414                if status and gajim.config.get('show_status_msgs_in_roster'):
415                        status = status.strip()
416                        if status != '':
417                                status = gtkgui_helpers.reduce_chars_newlines(status, max_lines = 1)
418                                # escape markup entities and make them small italic and fg color
419                                color = gtkgui_helpers._get_fade_color(self.list_treeview[room_jid],
420                                        selected, focus)
421                                colorstring = "#%04x%04x%04x" % (color.red, color.green, color.blue)
422                                name += '\n' '<span size="small" style="italic" foreground="%s">%s</span>'\
423                                        % (colorstring, gtkgui_helpers.escape_for_pango_markup(status))
424
425                model[iter][C_IMG] = image
426                model[iter][C_TEXT] = name
427
428        def draw_roster(self, room_jid):
429                model = self.list_treeview[room_jid].get_model()
430                model.clear()
431                for nick in gajim.gc_contacts[self.account][room_jid]:
432                        contact = gajim.gc_contacts[self.account][room_jid][nick]
433                        fjid = contact.jid
434                        if contact.resource:
435                                fjid += '/' + contact.resource
436                        self.add_contact_to_roster(room_jid, nick, contact.show, contact.role,
437                                fjid, contact.affiliation, contact.status)
438
439        def get_role(self, room_jid, nick):
440                if gajim.gc_contacts[self.account][room_jid].has_key(nick):
441                        return gajim.gc_contacts[self.account][room_jid][nick].role
442                else:
443                        return 'visitor'
444
445        def draw_all_roster(self):
446                for room_jid in self.list_treeview:
447                        self.draw_roster(room_jid)
448
449        def chg_contact_status(self, room_jid, nick, show, status, role, affiliation,
450                jid, reason, actor, statusCode, new_nick, account):
451                '''When an occupant changes his or her status'''
452                if show == 'invisible':
453                        return
454                if not role:
455                        role = 'visitor'
456                if not affiliation:
457                        affiliation = 'none'
458                if show in ('offline', 'error'):
459                        if statusCode == '307':
460                                if actor is None: # do not print 'kicked by None'
461                                        s = _('%(nick)s has been kicked: %(reason)s') % {
462                                                'nick': nick,
463                                                'reason': reason }
464                                else:
465                                        s = _('%(nick)s has been kicked by %(who)s: %(reason)s') % {
466                                                'nick': nick,
467                                                'who': actor,
468                                                'reason': reason }
469                                self.print_conversation(s, room_jid)
470                        elif statusCode == '301':
471                                if actor is None: # do not print 'banned by None'
472                                        s = _('%(nick)s has been banned: %(reason)s') % {
473                                                'nick': nick,
474                                                'reason': reason }
475                                else:
476                                        s = _('%(nick)s has been banned by %(who)s: %(reason)s') % {
477                                                'nick': nick,
478                                                'who': actor,
479                                                'reason': reason }
480                                self.print_conversation(s, room_jid)
481                        elif statusCode == '303': # Someone changed his or her nick
482                                if nick == self.nicks[room_jid]: # We changed our nick
483                                        self.nicks[room_jid] = new_nick
484                                        s = _('You are now known as %s') % new_nick
485                                else:
486                                        s = _('%s is now known as %s') % (nick, new_nick)
487                                self.print_conversation(s, room_jid)
488
489                        if not gajim.awaiting_events[self.account].has_key(
490                                room_jid + '/' + nick):
491                                self.remove_contact(room_jid, nick)
492                        else:
493                                c = gajim.gc_contacts[self.account][room_jid][nick]
494                                c.show = show
495                                c.status = status
496                        if nick == self.nicks[room_jid] and statusCode != '303': # We became offline
497                                self.got_disconnected(room_jid)
498                else:
499                        iter = self.get_contact_iter(room_jid, nick)
500                        if not iter:
501                                iter = self.add_contact_to_roster(room_jid, nick, show, role, jid,
502                                        affiliation, status)
503                        else:
504                                actual_role = self.get_role(room_jid, nick)
505                                if role != actual_role:
506                                        self.remove_contact(room_jid, nick)
507                                        self.add_contact_to_roster(room_jid, nick, show, role, jid,
508                                                affiliation, status)
509                                else:
510                                        c = gajim.gc_contacts[self.account][room_jid][nick]
511                                        if c.show == show and c.status == status and \
512                                                c.affiliation == affiliation: #no change
513                                                return
514                                        c.show = show
515                                        c.affiliation = affiliation
516                                        c.status = status
517                                        self.draw_contact(room_jid, nick)
518                if (time.time() - self.room_creation[room_jid]) > 30 and \
519                                nick != self.nicks[room_jid] and statusCode != '303':
520                        if show == 'offline':
521                                st = _('%s has left') % nick
522                        else:
523                                st = _('%s is now %s') % (nick, helpers.get_uf_show(show))
524                        if status:
525                                st += ' (' + status + ')'
526                        self.print_conversation(st, room_jid)
527
528        def set_subject(self, room_jid, subject):
529                self.subjects[room_jid] = subject
530                name_label = self.name_labels[room_jid]
531                full_subject = None
532
533                subject = gtkgui_helpers.reduce_chars_newlines(subject, 0, 2)
534                subject = gtkgui_helpers.escape_for_pango_markup(subject)
535                name_label.set_markup(
536                '<span weight="heavy" size="x-large">%s</span>\n%s' % (room_jid, subject))
537                event_box = name_label.get_parent()
538                if subject == '':
539                        subject = _('This room has no subject')
540
541                if full_subject is not None:
542                        subject = full_subject # tooltip must always hold ALL the subject
543                self.subject_tooltip[room_jid].set_tip(event_box, subject)
544
545        def get_specific_unread(self, room_jid):
546                # returns the number of the number of unread msgs
547                # for room_jid & number of unread private msgs with each contact
548                # that we have
549                nb = 0
550                for nick in self.get_nick_list(room_jid):
551                        fjid = room_jid + '/' + nick
552                        if gajim.awaiting_events[self.account].has_key(fjid):
553                                # gc can only have messages as event
554                                nb += len(gajim.awaiting_events[self.account][fjid])
555                return nb
556
557        def on_change_subject_menuitem_activate(self, widget):
558                room_jid = self.get_active_jid()
559                subject = self.subjects[room_jid]
560                instance = dialogs.InputDialog(_('Changing Subject'),
561                        _('Please specify the new subject:'), subject)
562                response = instance.get_response()
563                if response == gtk.RESPONSE_OK:
564                        subject = instance.input_entry.get_text().decode('utf-8')
565                        gajim.connections[self.account].send_gc_subject(room_jid, subject)
566
567        def on_change_nick_menuitem_activate(self, widget):
568                room_jid = self.get_active_jid()
569                nick = self.nicks[room_jid]
570                title = _('Changing Nickname')
571                prompt = _('Please specify the new nickname you want to use:')
572                self.show_change_nick_input_dialog(title, prompt, nick, room_jid)
573
574        def show_change_nick_input_dialog(self, title, prompt, proposed_nick = None,
575                room_jid = None):
576