root/branches/gajim_0.9.1/src/roster_window.py

Revision 4805, 98.3 kB (checked in by asterix, 3 years ago)

remove event from queue as soon as we click notification window

  • Property svn:eol-style set to LF
Line 
1##      roster_window.py
2##
3## Contributors for this file:
4##      - Yann Le Boulanger <asterix@lagaule.org>
5##      - Nikos Kouremenos <kourem@gmail.com>
6##      - Dimitur Kirov <dkirov@gmail.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 gobject
30import os
31import time
32
33import common.sleepy
34import tabbed_chat_window
35import groupchat_window
36import history_window
37import dialogs
38import vcard
39import config
40import disco
41import gtkgui_helpers
42import cell_renderer_image
43import tooltips
44
45from gajim import Contact
46from common import gajim
47from common import helpers
48from common import i18n
49
50_ = i18n._
51APP = i18n.APP
52gtk.glade.bindtextdomain(APP, i18n.DIR)
53gtk.glade.textdomain(APP)
54
55#(icon, name, type, jid, account, editable, second pixbuf)
56(
57C_IMG, # image to show state (online, new message etc)
58C_NAME, # cellrenderer text that holds contact nickame
59C_TYPE, # account, group or contact?
60C_JID, # the jid of the row
61C_ACCOUNT, # cellrenderer text that holds account name
62C_EDITABLE, # cellrenderer text that holds name editable or not?
63C_SECPIXBUF, # secondary_pixbuf (holds avatar or padlock)
64) = range(7)
65
66
67GTKGUI_GLADE = 'gtkgui.glade'
68
69class RosterWindow:
70        '''Class for main window of gtkgui interface'''
71
72        def get_account_iter(self, name):
73                model = self.tree.get_model()
74                if model is None:
75                        return
76                account_iter = model.get_iter_root()
77                if self.regroup:
78                        return account_iter
79                while account_iter:
80                        account_name = model[account_iter][C_NAME].decode('utf-8')
81                        if name == account_name:
82                                break
83                        account_iter = model.iter_next(account_iter)
84                return account_iter
85
86        def get_group_iter(self, name, account):
87                model = self.tree.get_model()
88                root = self.get_account_iter(account)
89                group_iter = model.iter_children(root)
90                while group_iter:
91                        group_name = model[group_iter][C_NAME].decode('utf-8')
92                        if name == group_name:
93                                break
94                        group_iter = model.iter_next(group_iter)
95                return group_iter
96
97        def get_contact_iter(self, jid, account):
98                model = self.tree.get_model()
99                acct = self.get_account_iter(account)
100                found = []
101                if model is None: # when closing Gajim model can be none (async pbs?)
102                        return found
103                group_iter = model.iter_children(acct)
104                while group_iter:
105                        user_iter = model.iter_children(group_iter)
106                        while user_iter:
107                                if jid == model[user_iter][C_JID].decode('utf-8') and \
108                                        account == model[user_iter][C_ACCOUNT].decode('utf-8'):
109                                        found.append(user_iter)
110                                user_iter = model.iter_next(user_iter)
111                        group_iter = model.iter_next(group_iter)
112                return found
113
114        def add_account_to_roster(self, account):
115                model = self.tree.get_model()
116                if self.get_account_iter(account):
117                        return
118
119                if self.regroup:
120                        show = helpers.get_global_show()
121                        model.append(None, [self.jabber_state_images['16'][show],
122                                _('Merged accounts'), 'account', '', 'all', False, None])
123                        return
124
125                show = gajim.SHOW_LIST[gajim.connections[account].connected]
126
127                tls_pixbuf = None
128                if gajim.con_types.has_key(account) and \
129                        gajim.con_types[account] in ('tls', 'ssl'):
130                        tls_pixbuf = self.window.render_icon(gtk.STOCK_DIALOG_AUTHENTICATION,
131                                gtk.ICON_SIZE_MENU) # the only way to create a pixbuf from stock
132
133                our_jid = gajim.get_jid_from_account(account)
134
135                model.append(None, [self.jabber_state_images['16'][show],
136                        gtkgui_helpers.escape_for_pango_markup(account),
137                        'account', our_jid, account, False, tls_pixbuf])
138
139        def remove_newly_added(self, jid, account):
140                if jid in gajim.newly_added[account]:
141                        gajim.newly_added[account].remove(jid)
142                        self.draw_contact(jid, account)
143
144        def add_contact_to_roster(self, jid, account):
145                '''Add a contact to the roster and add groups if they aren't in roster'''
146                showOffline = gajim.config.get('showoffline')
147                if not gajim.contacts[account].has_key(jid):
148                        return
149                users = gajim.contacts[account][jid]
150                user = users[0]
151                if user.jid.find('@') <= 0: # if not '@' or '@' starts the jid ==> agent
152                        user.groups = [_('Transports')]
153                elif user.groups == []:
154                        user.groups.append(_('General'))
155
156                hide = True
157                if user.sub in ('both', 'to'):
158                        hide = False
159                elif user.ask == 'subscribe':
160                        hide = False
161                # FIXME: uncomment when we support contacts in no group
162#               elif user.name or len(user.groups):
163                elif user.name:
164                        hide = False
165
166                # JEP-0162
167                if hide:
168                        return
169                if user.show in ('offline', 'error') and \
170                   not showOffline and (not _('Transports') in user.groups or \
171                        gajim.connections[account].connected < 2) and \
172                   not gajim.awaiting_events[account].has_key(user.jid):
173                        return
174
175                model = self.tree.get_model()
176                for g in user.groups:
177                        iterG = self.get_group_iter(g, account)
178                        if not iterG:
179                                IterAcct = self.get_account_iter(account)
180                                iterG = model.append(IterAcct, [
181                                        self.jabber_state_images['16']['closed'],
182                                        gtkgui_helpers.escape_for_pango_markup(g), 'group', g, account,
183                                        False, None])
184                        if not gajim.groups[account].has_key(g): #It can probably never append
185                                if account + g in self.collapsed_rows:
186                                        ishidden = False
187                                else:
188                                        ishidden = True
189                                gajim.groups[account][g] = { 'expand': ishidden }
190                        if not account in self.collapsed_rows:
191                                self.tree.expand_row((model.get_path(iterG)[0]), False)
192
193                        typestr = 'contact'
194                        if g == _('Transports'):
195                                typestr = 'agent'
196
197                        # we add some values here. see draw_contact for more
198                        model.append(iterG, (None, user.name,
199                                typestr, user.jid, account, False, None))
200
201                        if gajim.groups[account][g]['expand']:
202                                self.tree.expand_row(model.get_path(iterG), False)
203                self.draw_contact(jid, account)
204                self.draw_avatar(jid, account)
205
206        def really_remove_contact(self, user, account):
207                if user.jid in gajim.newly_added[account]:
208                        return
209                if user.jid.find('@') < 1 and gajim.connections[account].connected > 1: # It's an agent
210                        return
211                if user.jid in gajim.to_be_removed[account]:
212                        gajim.to_be_removed[account].remove(user.jid)
213                if gajim.config.get('showoffline'):
214                        self.draw_contact(user.jid, account)
215                        return
216                self.remove_contact(user, account)
217
218        def remove_contact(self, user, account):
219                '''Remove a user from the roster'''
220                if user.jid in gajim.to_be_removed[account]:
221                        return
222                model = self.tree.get_model()
223                for i in self.get_contact_iter(user.jid, account):
224                        parent_i = model.iter_parent(i)
225                        group = model.get_value(parent_i, 3).decode('utf-8')
226                        model.remove(i)
227                        if model.iter_n_children(parent_i) == 0:
228                                model.remove(parent_i)
229                                # We need to check all contacts, even offline contacts
230                                for jid in gajim.contacts[account]:
231                                        if group in gajim.get_contact_instance_with_highest_priority(account, jid).groups:
232                                                break
233                                else:
234                                        if gajim.groups[account].has_key(group):
235                                                del gajim.groups[account][group]
236
237        def get_appropriate_state_images(self, jid, size = '16'):
238                '''check jid and return the appropriate state images dict for
239                the demanded size'''
240                transport = gajim.get_transport_name_from_jid(jid)
241                if transport:
242                        return self.transports_state_images[size][transport]
243                return self.jabber_state_images[size]
244
245        def draw_contact(self, jid, account, selected = False, focus = False):
246                '''draw the correct state image, name BUT not avatar'''
247                # focus is about if the roster window has toplevel-focus or not
248                model = self.tree.get_model()
249                iters = self.get_contact_iter(jid, account)
250                if len(iters) == 0:
251                        return
252                contact_instances = gajim.get_contact_instances_from_jid(account, jid)
253                contact = gajim.get_highest_prio_contact_from_contacts(contact_instances)
254                name = gtkgui_helpers.escape_for_pango_markup(contact.name)
255
256                if len(contact_instances) > 1:
257                        name += ' (' + unicode(len(contact_instances)) + ')'
258
259                # FIXME: remove when we use metacontacts
260                # shoz (account_name) if there are 2 contact with same jid in merged mode
261                if self.regroup:
262                        add_acct = False
263                        # look through all contacts of all accounts
264                        for a in gajim.connections:
265                                for j in gajim.contacts[a]:
266                                        # [0] cause it'fster than highest_prio
267                                        c = gajim.contacts[a][j][0]
268                                        if c.name == contact.name and (j, a) != (jid, account):
269                                                add_acct = True
270                                                break
271                                if add_acct:
272                                        # No need to continue in other account if we already found one
273                                        break
274                        if add_acct:
275                                name += ' (' + account + ')'
276
277                # add status msg, if not empty, under contact name in the treeview
278                if contact.status and gajim.config.get('show_status_msgs_in_roster'):
279                        status = contact.status.strip()
280                        if status != '':
281                                status = gtkgui_helpers.reduce_chars_newlines(status, max_lines = 1)
282                                # escape markup entities and make them small italic and fg color
283                                color = gtkgui_helpers._get_fade_color(self.tree, selected, focus)
284                                colorstring = "#%04x%04x%04x" % (color.red, color.green, color.blue)
285                                name += '\n<span size="small" style="italic" foreground="%s">%s</span>'\
286                                        % (colorstring, gtkgui_helpers.escape_for_pango_markup(status))
287
288               
289                icon_name = helpers.get_icon_name_to_show(contact, account)
290                state_images = self.get_appropriate_state_images(jid, size = '16')
291               
292                img = state_images[icon_name]
293
294                for iter in iters:
295                        model[iter][C_IMG] = img
296                        model[iter][C_NAME] = name
297
298        def draw_avatar(self, jid, account):
299                '''draw the avatar'''
300                model = self.tree.get_model()
301                iters = self.get_contact_iter(jid, account)
302                if gajim.config.get('show_avatars_in_roster'):
303                        pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid)
304                        if pixbuf in ('ask', None):
305                                scaled_pixbuf = None
306                        else:
307                                scaled_pixbuf = gtkgui_helpers.get_scaled_pixbuf(pixbuf, 'roster')
308                else:
309                        scaled_pixbuf = None
310                for iter in iters:
311                        model[iter][C_SECPIXBUF] = scaled_pixbuf
312
313        def join_gc_room(self, account, room_jid, nick, password):
314                '''joins the room immediatelly'''
315                if room_jid in gajim.interface.instances[account]['gc'] and \
316                gajim.gc_connected[account][room_jid]:
317                        dialogs.ErrorDialog(_('You are already in room %s') % room_jid
318                                ).get_response()
319                        return
320                invisible_show = gajim.SHOW_LIST.index('invisible')
321                if gajim.connections[account].connected == invisible_show:
322                        dialogs.ErrorDialog(_('You cannot join a room while you are invisible')
323                                ).get_response()
324                        return
325                room, server = room_jid.split('@')
326                if not room_jid in gajim.interface.instances[account]['gc']:
327                        self.new_room(room_jid, nick, account)
328                gajim.interface.instances[account]['gc'][room_jid].set_active_tab(room_jid)
329                gajim.interface.instances[account]['gc'][room_jid].window.present()
330                gajim.connections[account].join_gc(nick, room, server, password)
331                if password:
332                        gajim.gc_passwords[room_jid] = password
333
334        def on_bookmark_menuitem_activate(self, widget, account, bookmark):
335                self.join_gc_room(account, bookmark['jid'], bookmark['nick'],
336                        bookmark['password'])
337
338        def on_bm_header_changed_state(self, widget, event):
339                widget.set_state(gtk.STATE_NORMAL) #do not allow selected_state
340
341        def on_send_server_message_menuitem_activate(self, widget, account):
342                server = gajim.config.get_per('accounts', account, 'hostname')
343                server += '/announce/online'
344                dialogs.SingleMessageWindow(account, server, 'send')
345
346        def on_xml_console_menuitem_activate(self, widget, account):
347                if gajim.interface.instances[account].has_key('xml_console'):
348                        gajim.interface.instances[account]['xml_console'].window.present()
349                else:
350                        gajim.interface.instances[account]['xml_console'].window.show_all()
351
352        def on_set_motd_menuitem_activate(self, widget, account):
353                server = gajim.config.get_per('accounts', account, 'hostname')
354                server += '/announce/motd'
355                dialogs.SingleMessageWindow(account, server, 'send')
356
357        def on_update_motd_menuitem_activate(self, widget, account):
358                server = gajim.config.get_per('accounts', account, 'hostname')
359                server += '/announce/motd/update'
360                dialogs.SingleMessageWindow(account, server, 'send')
361
362        def on_delete_motd_menuitem_activate(self, widget, account):
363                server = gajim.config.get_per('accounts', account, 'hostname')
364                server += '/announce/motd/delete'
365                gajim.connections[account].send_motd(server)
366
367        def on_online_users_menuitem_activate(self, widget, account):
368                pass #FIXME: impement disco in users for 0.9
369
370        def get_and_connect_advanced_menuitem_menu(self, account):
371                xml = gtk.glade.XML(GTKGUI_GLADE, 'advanced_menuitem_menu', APP)
372                advanced_menuitem_menu = xml.get_widget('advanced_menuitem_menu')
373
374                send_single_message_menuitem = xml.get_widget(
375                        'send_single_message_menuitem')
376                xml_console_menuitem = xml.get_widget('xml_console_menuitem')
377                administrator_menuitem = xml.get_widget('administrator_menuitem')
378                online_users_menuitem = xml.get_widget('online_users_menuitem')
379                send_server_message_menuitem = xml.get_widget(
380                        'send_server_message_menuitem')
381                set_motd_menuitem = xml.get_widget('set_motd_menuitem')
382                update_motd_menuitem = xml.get_widget('update_motd_menuitem')
383                delete_motd_menuitem = xml.get_widget('delete_motd_menuitem')
384
385                send_single_message_menuitem.connect('activate',
386                        self.on_send_single_message_menuitem_activate, account)
387
388                xml_console_menuitem.connect('activate',
389                        self.on_xml_console_menuitem_activate, account)
390
391                #FIXME: 0.9 should have this: it does disco in the place where users are
392                online_users_menuitem.set_no_show_all(True)
393                online_users_menuitem.hide()
394                online_users_menuitem.connect('activate',
395                        self.on_online_users_menuitem_activate, account)
396
397                send_server_message_menuitem.connect('activate',
398                        self.on_send_server_message_menuitem_activate, account)
399
400                set_motd_menuitem.connect('activate',
401                        self.on_set_motd_menuitem_activate, account)
402
403                update_motd_menuitem.connect('activate',
404                        self.on_update_motd_menuitem_activate, account)
405
406                delete_motd_menuitem.connect('activate',
407                        self.on_delete_motd_menuitem_activate, account)
408
409                advanced_menuitem_menu.show_all()
410
411                return advanced_menuitem_menu
412
413        def make_menu(self):
414                '''create the main window's menus'''
415                new_message_menuitem = self.xml.get_widget('new_message_menuitem')
416                join_gc_menuitem = self.xml.get_widget('join_gc_menuitem')
417                add_new_contact_menuitem  = self.xml.get_widget('add_new_contact_menuitem')
418                service_disco_menuitem  = self.xml.get_widget('service_disco_menuitem')
419                advanced_menuitem = self.xml.get_widget('advanced_menuitem')
420                show_offline_contacts_menuitem = self.xml.get_widget(
421                        'show_offline_contacts_menuitem')
422                profile_avatar_menuitem = self.xml.get_widget('profile_avatar_menuitem')
423
424                # make it sensitive. it is insensitive only if no accounts are *available*
425                advanced_menuitem.set_sensitive(True)
426
427
428                if self.add_new_contact_handler_id:
429                        add_new_contact_menuitem.handler_disconnect(
430                                self.add_new_contact_handler_id)
431                        self.add_new_contact_handler_id = None
432
433                if self.service_disco_handler_id:
434                        service_disco_menuitem.handler_disconnect(
435                                self.service_disco_handler_id)
436                        self.service_disco_handler_id = None
437
438                if self.new_message_menuitem_handler_id:
439                        new_message_menuitem.handler_disconnect(
440                                self.new_message_menuitem_handler_id)
441                        self.new_message_menuitem_handler_id = None
442
443                #remove the existing submenus
444                add_new_contact_menuitem.remove_submenu()
445                service_disco_menuitem.remove_submenu()
446                join_gc_menuitem.remove_submenu()
447                new_message_menuitem.remove_submenu()
448                advanced_menuitem.remove_submenu()
449
450                #remove the existing accelerator
451                if self.have_new_message_accel:
452                        ag = gtk.accel_groups_from_object(self.window)[0]
453                        new_message_menuitem.remove_accelerator(ag, gtk.keysyms.n,
454                                gtk.gdk.CONTROL_MASK)
455                        self.have_new_message_accel = False
456
457                #join gc
458                sub_menu = gtk.Menu()
459                join_gc_menuitem.set_submenu(sub_menu)
460                at_least_one_account_connected = False
461                multiple_accounts = len(gajim.connections) >= 2 #FIXME: stop using bool var here
462                for account in gajim.connections:
463                        if gajim.connections[account].connected <= 1: #if offline or connecting
464                                continue
465                        if not at_least_one_account_connected:
466                                at_least_one_account_connected = True
467                        if multiple_accounts:
468                                label = gtk.Label()
469                                label.set_markup('<u>' + account.upper() +'</u>')
470                                item = gtk.MenuItem()
471                                item.add(label)
472                                item.connect('state-changed', self.on_bm_header_changed_state)
473                                sub_menu.append(item)
474
475                        item = gtk.MenuItem(_('_Join New Room'))
476                        item.connect('activate', self.on_join_gc_activate, account)
477                        sub_menu.append(item)
478
479                        for bookmark in gajim.connections[account].bookmarks:
480                                item = gtk.MenuItem(bookmark['name'])
481                                item.connect('activate', self.on_bookmark_menuitem_activate,
482                                        account, bookmark)
483                                sub_menu.append(item)
484
485                if at_least_one_account_connected: #FIXME: move this below where we do this check
486                        #and make sure it works
487                        newitem = gtk.SeparatorMenuItem() # seperator
488                        sub_menu.append(newitem)
489
490                        newitem = gtk.ImageMenuItem(_('Manage Bookmarks...'))
491                        img = gtk.image_new_from_stock(gtk.STOCK_PREFERENCES,
492                                gtk.ICON_SIZE_MENU)
493                        newitem.set_image(img)
494                        newitem.connect('activate', self.on_manage_bookmarks_menuitem_activate)
495                        sub_menu.append(newitem)
496                        sub_menu.show_all()
497
498                if multiple_accounts: # 2 or more accounts? make submenus
499                        #add
500                        sub_menu = gtk.Menu()
501                        for account in gajim.connections:
502                                if gajim.connections[account].connected <= 1:
503                                        #if offline or connecting
504                                        continue
505                                item = gtk.MenuItem(_('to %s account') % account)
506                                sub_menu.append(item)
507                                item.connect('activate', self.on_add_new_contact, account)
508                        add_new_contact_menuitem.set_submenu(sub_menu)
509                        sub_menu.show_all()
510
511                        #disco
512                        sub_menu = gtk.Menu()
513                        for account in gajim.connections:
514                                if gajim.connections[account].connected <= 1:
515                                        #if offline or connecting
516                                        continue
517                                item = gtk.MenuItem(_('using %s account') % account)
518                                sub_menu.append(item)
519                                item.connect('activate', self.on_service_disco_menuitem_activate,
520                                        account)
521
522                        service_disco_menuitem.set_submenu(sub_menu)
523                        sub_menu.show_all()
524
525                        #new message
526                        sub_menu = gtk.Menu()
527                        for account in gajim.connections:
528                                if gajim.connections[account].connected <= 1:
529                                        #if offline or connecting
530                                        continue
531                                item = gtk.MenuItem(_('using account %s') % account)
532                                sub_menu.append(item)
533                                item.connect('activate', self.on_new_message_menuitem_activate,
534                                                                        account)
535
536                        new_message_menuitem.set_submenu(sub_menu)
537                        sub_menu.show_all()
538
539                        #Advanced Actions
540                        sub_menu = gtk.Menu()
541                        for account in gajim.connections:
542                                item = gtk.MenuItem(_('for account %s') % account)
543                                sub_menu.append(item)
544                                advanced_menuitem_menu = self.get_and_connect_advanced_menuitem_menu(
545                                        account)
546                                item.set_submenu(advanced_menuitem_menu)
547
548                        advanced_menuitem.set_submenu(sub_menu)
549                        sub_menu.show_all()
550
551                else:
552                        if len(gajim.connections) == 1: # user has only one account
553                                #add
554                                if not self.add_new_contact_handler_id:
555                                        self.add_new_contact_handler_id = add_new_contact_menuitem.connect(
556                                                'activate', self.on_add_new_contact, gajim.connections.keys()[0])
557                                #disco
558                                if not self.service_disco_handler_id:
559                                        self.service_disco_handler_id = service_disco_menuitem.connect(
560                                                'activate', self.on_service_disco_menuitem_activate,
561                                                gajim.connections.keys()[0])
562                                #new msg
563                                if not self.new_message_menuitem_handler_id:
564                                        self.new_message_menuitem_handler_id = new_message_menuitem.\
565                                                connect('activate', self.on_new_message_menuitem_activate,
566                                                gajim.connections.keys()[0])
567                                #new msg accel
568                                if not self.have_new_message_accel:
569                                        ag = gtk.accel_groups_from_object(self.window)[0]
570                                        new_message_menuitem.add_accelerator('activate', ag,
571                                                gtk.keysyms.n,  gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
572                                        self.have_new_message_accel = True
573
574                                account = gajim.connections.keys()[0]
575                                advanced_menuitem_menu = self.get_and_connect_advanced_menuitem_menu(
576                                        account)
577                                advanced_menuitem.set_submenu(advanced_menuitem_menu)
578                        elif len(gajim.connections) == 0: # user has no accounts
579                                advanced_menuitem.set_sensitive(False)
580
581                #FIXME: Gajim 0.9 should have this visible
582                profile_avatar_menuitem.set_no_show_all(True)
583                profile_avatar_menuitem.hide()
584
585                if at_least_one_account_connected:
586                        new_message_menuitem.set_sensitive(True)
587                        join_gc_menuitem.set_sensitive(True)
588                        add_new_contact_menuitem.set_sensitive(True)
589                        service_disco_menuitem.set_sensitive(True)
590                        show_offline_contacts_menuitem.set_sensitive(True)
591                else:
592                        # make the menuitems insensitive
593                        new_message_menuitem.set_sensitive(False)
594                        join_gc_menuitem.set_sensitive(False)
595                        add_new_contact_menuitem.set_sensitive(False)
596                        service_disco_menuitem.set_sensitive(False)
597                        show_offline_contacts_menuitem.set_sensitive(False)
598                        profile_avatar_menuitem.set_sensitive(False)
599
600        def _change_style(self, model, path, iter, option):
601                if option is None:
602                        model[