root/tags/gajim-0.11.4/src/disco.py

Revision 8955, 62.7 kB (checked in by asterix, 10 months ago)

merge some fixes from trunk: [8864] [8866] [8881] [8884] [8887] [8894] [8895] [8902] [8903] [8904] [8905] [8906] [8910] [8918] [8919] [8921] [8923] [8924] [8938] [8942] [8952] [8953] [8954]

Line 
1# -*- coding: utf-8 -*-
2##      config.py
3##
4## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org>
5## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
6## Copyright (C) 2005-2006 Stéphan Kochen <stephan@kochen.nl>
7##
8## This program is free software; you can redistribute it and/or modify
9## it under the terms of the GNU General Public License as published
10## by the Free Software Foundation; version 2 only.
11##
12## This program is distributed in the hope that it will be useful,
13## but WITHOUT ANY WARRANTY; without even the implied warranty of
14## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15## GNU General Public License for more details.
16##
17
18# The appearance of the treeview, and parts of the dialog, are controlled by
19# AgentBrowser (sub-)classes. Methods that probably should be overridden when
20# subclassing are: (look at the docstrings and source for additional info)
21# - def cleanup(self) *
22# - def _create_treemodel(self) *
23# - def _add_actions(self)
24# - def _clean_actions(self)
25# - def update_theme(self) *
26# - def update_actions(self)
27# - def default_action(self)
28# - def _find_item(self, jid, node)
29# - def _add_item(self, jid, node, item, force)
30# - def _update_item(self, iter, jid, node, item)
31# - def _update_info(self, iter, jid, node, identities, features, data)
32# - def _update_error(self, iter, jid, node)
33#
34# * Should call the super class for this method.
35# All others do not have to call back to the super class. (but can if they want
36# the functionality)
37# There are more methods, of course, but this is a basic set.
38
39import os
40import inspect
41import weakref
42import gobject
43import gtk
44import pango
45
46import dialogs
47import tooltips
48import gtkgui_helpers
49import groups
50import adhoc_commands
51
52from common import gajim
53from common import xmpp
54from common.exceptions import GajimGeneralException
55
56# Dictionary mapping category, type pairs to browser class, image pairs.
57# This is a function, so we can call it after the classes are declared.
58# For the browser class, None means that the service will only be browsable
59# when it advertises disco as it's feature, False means it's never browsable.
60def _gen_agent_type_info():
61        return {
62                # Defaults
63                (0, 0):                                                 (None, None),
64
65                # Jabber server
66                ('server', 'im'):                               (ToplevelAgentBrowser, 'jabber.png'),
67                ('services', 'jabber'):         (ToplevelAgentBrowser, 'jabber.png'),
68                ('hierarchy', 'branch'):        (AgentBrowser, 'jabber.png'),
69
70                # Services
71                ('conference', 'text'):         (MucBrowser, 'conference.png'),
72                ('headline', 'rss'):                    (AgentBrowser, 'rss.png'),
73                ('headline', 'weather'):        (False, 'weather.png'),
74                ('gateway', 'weather'):         (False, 'weather.png'),
75                ('_jid', 'weather'):                    (False, 'weather.png'),
76                ('gateway', 'sip'):                     (False, 'sip.png'),
77                ('directory', 'user'):          (None, 'jud.png'),
78                ('pubsub', 'generic'):          (None, 'pubsub.png'),
79                ('pubsub', 'service'):          (PubSubBrowser, 'pubsub.png'),
80                ('proxy', 'bytestreams'):       (None, 'bytestreams.png'), # Socks5 FT proxy
81                ('headline', 'newmail'):        (ToplevelAgentBrowser, 'mail.png'),
82
83                # Transports
84                ('conference', 'irc'):          (ToplevelAgentBrowser, 'irc.png'),
85                ('_jid', 'irc'):                                (False, 'irc.png'),
86                ('gateway', 'aim'):                     (False, 'aim.png'),
87                ('_jid', 'aim'):                                (False, 'aim.png'),
88                ('gateway', 'gadu-gadu'):       (False, 'gadu-gadu.png'),
89                ('_jid', 'gadugadu'):           (False, 'gadu-gadu.png'),
90                ('gateway', 'http-ws'):         (False, 'http-ws.png'),
91                ('gateway', 'icq'):                     (False, 'icq.png'),
92                ('_jid', 'icq'):                                (False, 'icq.png'),
93                ('gateway', 'msn'):                     (False, 'msn.png'),
94                ('_jid', 'msn'):                                (False, 'msn.png'),
95                ('gateway', 'sms'):                     (False, 'sms.png'),
96                ('_jid', 'sms'):                                (False, 'sms.png'),
97                ('gateway', 'smtp'):                    (False, 'mail.png'),
98                ('gateway', 'yahoo'):           (False, 'yahoo.png'),
99                ('_jid', 'yahoo'):                      (False, 'yahoo.png'),
100        }
101
102# Category type to "human-readable" description string, and sort priority
103_cat_to_descr = {
104        'other':                        (_('Others'),   2),
105        'gateway':              (_('Transports'),       0),
106        '_jid':                 (_('Transports'),       0),
107        #conference is a category for listing mostly groupchats in service discovery
108        'conference':   (_('Conference'),       1),
109}
110
111
112class CacheDictionary:
113        '''A dictionary that keeps items around for only a specific time.
114        Lifetime is in minutes. Getrefresh specifies whether to refresh when
115        an item is merely accessed instead of set aswell.'''
116        def __init__(self, lifetime, getrefresh = True):
117                self.lifetime = lifetime * 1000 * 60
118                self.getrefresh = getrefresh
119                self.cache = {}
120
121        class CacheItem:
122                '''An object to store cache items and their timeouts.'''
123                def __init__(self, value):
124                        self.value = value
125                        self.source = None
126
127                def __call__(self):
128                        return self.value
129
130        def cleanup(self):
131                for key in self.cache.keys():
132                        item = self.cache[key]
133                        if item.source:
134                                gobject.source_remove(item.source)
135                        del self.cache[key]
136
137        def _expire_timeout(self, key):
138                '''The timeout has expired, remove the object.'''
139                if key in self.cache:
140                        del self.cache[key]
141                return False
142
143        def _refresh_timeout(self, key):
144                '''The object was accessed, refresh the timeout.'''
145                item = self.cache[key]
146                if item.source:
147                        gobject.source_remove(item.source)
148                if self.lifetime:
149                        source = gobject.timeout_add(self.lifetime, self._expire_timeout, key)
150                        item.source = source
151
152        def __getitem__(self, key):
153                item = self.cache[key]
154                if self.getrefresh:
155                        self._refresh_timeout(key)
156                return item()
157
158        def __setitem__(self, key, value):
159                item = self.CacheItem(value)
160                self.cache[key] = item
161                self._refresh_timeout(key)
162
163        def __delitem__(self, key):
164                item = self.cache[key]
165                if item.source:
166                        gobject.source_remove(item.source)
167                del self.cache[key]
168
169        def __contains__(self, key):
170                return key in self.cache
171        has_key = __contains__
172
173_icon_cache = CacheDictionary(15)
174
175def get_agent_address(jid, node = None):
176        '''Returns an agent's address for displaying in the GUI.'''
177        if node:
178                return '%s@%s' % (node, str(jid))
179        else:
180                return str(jid)
181
182class Closure(object):
183        '''A weak reference to a callback with arguments as an object.
184
185        Weak references to methods immediatly die, even if the object is still
186        alive. Besides a handy way to store a callback, this provides a workaround
187        that keeps a reference to the object instead.
188
189        Userargs and removeargs must be tuples.'''
190        def __init__(self, cb, userargs = (), remove = None, removeargs = ()):
191                self.userargs = userargs
192                self.remove = remove
193                self.removeargs = removeargs
194                if inspect.ismethod(cb):
195                        self.meth_self = weakref.ref(cb.im_self, self._remove)
196                        self.meth_name = cb.func_name
197                elif callable(cb):
198                        self.meth_self = None
199                        self.cb = weakref.ref(cb, self._remove)
200                else:
201                        raise TypeError('Object is not callable')
202
203        def _remove(self, ref):
204                if self.remove:
205                        self.remove(self, *self.removeargs)
206
207        def __call__(self, *args, **kwargs):
208                if self.meth_self:
209                        obj = self.meth_self()
210                        cb = getattr(obj, self.meth_name)
211                else:
212                        cb = self.cb()
213                args = args + self.userargs
214                return cb(*args, **kwargs)
215
216
217class ServicesCache:
218        '''Class that caches our query results. Each connection will have it's own
219        ServiceCache instance.'''
220        def __init__(self, account):
221                self.account = account
222                self._items = CacheDictionary(0, getrefresh = False)
223                self._info = CacheDictionary(0, getrefresh = False)
224                self._subscriptions = CacheDictionary(5, getrefresh=False)
225                self._cbs = {}
226
227        def cleanup(self):
228                self._items.cleanup()
229                self._info.cleanup()
230
231        def _clean_closure(self, cb, type, addr):
232                # A closure died, clean up
233                cbkey = (type, addr)
234                try:
235                        self._cbs[cbkey].remove(cb)
236                except KeyError:
237                        return
238                except ValueError:
239                        return
240                # Clean an empty list
241                if not self._cbs[cbkey]:
242                        del self._cbs[cbkey]
243
244        def get_icon(self, identities = []):
245                '''Return the icon for an agent.'''
246                # Grab the first identity with an icon
247                for identity in identities:
248                        try:
249                                cat, type = identity['category'], identity['type']
250                                info = _agent_type_info[(cat, type)]
251                        except KeyError:
252                                continue
253                        filename = info[1]
254                        if filename:
255                                break
256                else:
257                        # Loop fell through, default to unknown
258                        cat = type = 0
259                        info = _agent_type_info[(0, 0)]
260                        filename = info[1]
261                if not filename: # we don't have an image to show for this type
262                        return
263                # Use the cache if possible
264                if filename in _icon_cache:
265                        return _icon_cache[filename]
266                # Or load it
267                filepath = os.path.join(gajim.DATA_DIR, 'pixmaps', 'agents', filename)
268                pix = gtk.gdk.pixbuf_new_from_file(filepath)
269                # Store in cache
270                _icon_cache[filename] = pix
271                return pix
272
273        def get_browser(self, identities = [], features = []):
274                '''Return the browser class for an agent.'''
275                # Grab the first identity with a browser
276                browser = None
277                for identity in identities:
278                        try:
279                                cat, type = identity['category'], identity['type']
280                                info = _agent_type_info[(cat, type)]
281                        except KeyError:
282                                continue
283                        browser = info[0]
284                        if browser:
285                                break
286                # Note: possible outcome here is browser=False
287                if browser is None:
288                        # NS_BROWSE is deprecated, but we check for it anyways.
289                        # Some services list it in features and respond to
290                        # NS_DISCO_ITEMS anyways.
291                        # Allow browsing for unknown types aswell.
292                        if (not features and not identities) or\
293                                        xmpp.NS_DISCO_ITEMS in features or\
294                                        xmpp.NS_BROWSE in features:
295                                browser = AgentBrowser
296                return browser
297
298        def get_info(self, jid, node, cb, force = False, nofetch = False, args = ()):
299                '''Get info for an agent.'''
300                addr = get_agent_address(jid, node)
301                # Check the cache
302                if self._info.has_key(addr):
303                        args = self._info[addr] + args
304                        cb(jid, node, *args)
305                        return
306                if nofetch:
307                        return
308
309                # Create a closure object
310                cbkey = ('info', addr)
311                cb = Closure(cb, userargs = args, remove = self._clean_closure,
312                                removeargs = cbkey)
313                # Are we already fetching this?
314                if self._cbs.has_key(cbkey):
315                        self._cbs[cbkey].append(cb)
316                else:
317                        self._cbs[cbkey] = [cb]
318                        gajim.connections[self.account].discoverInfo(jid, node)
319
320        def get_items(self, jid, node, cb, force = False, nofetch = False, args = ()):
321                '''Get a list of items in an agent.'''
322                addr = get_agent_address(jid, node)
323                # Check the cache
324                if self._items.has_key(addr):
325                        args = (self._items[addr],) + args
326                        cb(jid, node, *args)
327                        return
328                if nofetch:
329                        return
330
331                # Create a closure object
332                cbkey = ('items', addr)
333                cb = Closure(cb, userargs = args, remove = self._clean_closure,
334                                removeargs = cbkey)
335                # Are we already fetching this?
336                if self._cbs.has_key(cbkey):
337                        self._cbs[cbkey].append(cb)
338                else:
339                        self._cbs[cbkey] = [cb]
340                        gajim.connections[self.account].discoverItems(jid, node)
341
342        def agent_info(self, jid, node, identities, features, data):
343                '''Callback for when we receive an agent's info.'''
344                addr = get_agent_address(jid, node)
345
346                # Store in cache
347                self._info[addr] = (identities, features, data)
348
349                # Call callbacks
350                cbkey = ('info', addr)
351                if self._cbs.has_key(cbkey):
352                        for cb in self._cbs[cbkey]:
353                                cb(jid, node, identities, features, data)
354                        # clean_closure may have beaten us to it
355                        if self._cbs.has_key(cbkey):
356                                del self._cbs[cbkey]
357
358        def agent_items(self, jid, node, items):
359                '''Callback for when we receive an agent's items.'''
360                addr = get_agent_address(jid, node)
361
362                # Store in cache
363                self._items[addr] = items
364
365                # Call callbacks
366                cbkey = ('items', addr)
367                if self._cbs.has_key(cbkey):
368                        for cb in self._cbs[cbkey]:
369                                cb(jid, node, items)
370                        # clean_closure may have beaten us to it
371                        if self._cbs.has_key(cbkey):
372                                del self._cbs[cbkey]
373
374        def agent_info_error(self, jid):
375                '''Callback for when a query fails. (even after the browse and agents
376                namespaces)'''
377                addr = get_agent_address(jid)
378
379                # Call callbacks
380                cbkey = ('info', addr)
381                if self._cbs.has_key(cbkey):
382                        for cb in self._cbs[cbkey]:
383                                cb(jid, '', 0, 0, 0)
384                        # clean_closure may have beaten us to it
385                        if self._cbs.has_key(cbkey):
386                                del self._cbs[cbkey]
387
388        def agent_items_error(self, jid):
389                '''Callback for when a query fails. (even after the browse and agents
390                namespaces)'''
391                addr = get_agent_address(jid)
392
393                # Call callbacks
394                cbkey = ('items', addr)
395                if self._cbs.has_key(cbkey):
396                        for cb in self._cbs[cbkey]:
397                                cb(jid, '', 0)
398                        # clean_closure may have beaten us to it
399                        if self._cbs.has_key(cbkey):
400                                del self._cbs[cbkey]
401
402# object is needed so that @property works
403class ServiceDiscoveryWindow(object):
404        '''Class that represents the Services Discovery window.'''
405        def __init__(self, account, jid = '', node = '',
406                        address_entry = False, parent = None):
407                self.account = account
408                self.parent = parent
409                if not jid:
410                        jid = gajim.config.get_per('accounts', account, 'hostname')
411                        node = ''
412
413                self.jid = None
414                self.browser = None
415                self.children = []
416                self.dying = False
417
418                # Check connection
419                if gajim.connections[account].connected < 2:
420                        dialogs.ErrorDialog(_('You are not connected to the server'),
421_('Without a connection, you can not browse available services'))
422                        raise RuntimeError, 'You must be connected to browse services'
423
424                # Get a ServicesCache object.
425                try:
426                        self.cache = gajim.connections[account].services_cache
427                except AttributeError:
428                        self.cache = ServicesCache(account)
429                        gajim.connections[account].services_cache = self.cache
430
431                self.xml = gtkgui_helpers.get_glade('service_discovery_window.glade')
432                self.window = self.xml.get_widget('service_discovery_window')
433                self.services_treeview = self.xml.get_widget('services_treeview')
434                self.model = None
435                # This is more reliable than the cursor-changed signal.
436                selection = self.services_treeview.get_selection()
437                selection.connect_after('changed',
438                        self.on_services_treeview_selection_changed)
439                self.services_scrollwin = self.xml.get_widget('services_scrollwin')
440                self.progressbar = self.xml.get_widget('services_progressbar')
441                self.progressbar.set_no_show_all(True)
442                self.progressbar.hide()
443                self.banner = self.xml.get_widget('banner_agent_label')
444                self.banner_icon = self.xml.get_widget('banner_agent_icon')
445                self.banner_eventbox = self.xml.get_widget('banner_agent_eventbox')
446                self.style_event_id = 0
447                self.banner.realize()
448                self.paint_banner()
449                self.filter_hbox = self.xml.get_widget('filter_hbox')
450                self.filter_hbox.set_no_show_all(True)
451                self.filter_hbox.hide()
452                self.action_buttonbox = self.xml.get_widget('action_buttonbox')
453
454                # Address combobox
455                self.address_comboboxentry = None
456                address_table = self.xml.get_widget('address_table')
457                if address_entry:
458                        self.address_comboboxentry = self.xml.get_widget(
459                                'address_comboboxentry')
460                        self.address_comboboxentry_entry = self.address_comboboxentry.child
461                        self.address_comboboxentry_entry.set_activates_default(True)
462
463                        liststore = gtk.ListStore(str)
464                        self.address_comboboxentry.set_model(liststore)
465                        self.latest_addresses = gajim.config.get(
466                                'latest_disco_addresses').split()
467                        if jid in self.latest_addresses:
468                                self.latest_addresses.remove(jid)
469                        self.latest_addresses.insert(0, jid)
470                        if len(self.latest_addresses) > 10:
471                                self.latest_addresses = self.latest_addresses[0:10]
472                        for j in self.latest_addresses:
473                                self.address_comboboxentry.append_text(j)
474                        self.address_comboboxentry.child.set_text(jid)
475                else:
476                        # Don't show it at all if we didn't ask for it
477                        address_table.set_no_show_all(True)
478                        address_table.hide()
479
480                self._initial_state()
481                self.xml.signal_autoconnect(self)
482                self.travel(jid, node)
483                self.window.show_all()
484
485        @property
486        def _get_account(self):
487                return self.account
488
489        @property
490        def _set_account(self, value):
491                self.account = value
492                self.cache.account = value
493                if self.browser:
494                        self.browser.account = value
495
496        def _initial_state(self):
497                '''Set some initial state on the window. Separated in a method because
498                it's handy to use within browser's cleanup method.'''
499                self.progressbar.hide()
500                title_text = _('Service Discovery using account %s') % self.account
501                self.window.set_title(title_text)
502                self._set_window_banner_text(_('Service Discovery'))
503                if gtk.gtk_version >= (2, 8, 0) and gtk.pygtk_version >= (2, 8, 0):
504                        self.banner_icon.clear()
505                else:
506                        self.banner_icon.set_from_file(None)
507                self.banner_icon.hide() # Just clearing it doesn't work
508
509        def _set_window_banner_text(self, text, text_after = None):
510                theme = gajim.config.get('roster_theme')
511                bannerfont = gajim.config.get_per('themes', theme, 'bannerfont')
512                bannerfontattrs = gajim.config.get_per('themes', theme,
513                        'bannerfontattrs')
514               
515                if bannerfont:
516                        font = pango.FontDescription(bannerfont)
517                else:
518                        font = pango.FontDescription('Normal')
519                if bannerfontattrs:
520                        # B is attribute set by default
521                        if 'B' in bannerfontattrs:
522                                font.set_weight(pango.WEIGHT_HEAVY)
523                        if 'I' in bannerfontattrs:
524                                font.set_style(pango.STYLE_ITALIC)
525               
526                font_attrs = 'font_desc="%s"' % font.to_string()
527                font_size = font.get_size()
528               
529                # in case there is no font specified we use x-large font size
530                if font_size == 0:
531                        font_attrs = '%s size="large"' % font_attrs
532                markup = '<span %s>%s</span>' % (font_attrs, text)
533                if text_after:
534                        font.set_weight(pango.WEIGHT_NORMAL)
535                        markup = '%s\n<span font_desc="%s" size="small">%s</span>' % \
536                                                                        (markup, font.to_string(), text_after)
537                self.banner.set_markup(markup)
538       
539        def paint_banner(self):
540                '''Repaint the banner with theme color'''
541                theme = gajim.config.get('roster_theme')
542                bgcolor = gajim.config.get_per('themes', theme, 'bannerbgcolor')
543                textcolor = gajim.config.get_per('themes', theme, 'bannertextcolor')
544                self.disconnect_style_event()
545                if bgcolor:
546                        color = gtk.gdk.color_parse(bgcolor)
547                        self.banner_eventbox.modify_bg(gtk.STATE_NORMAL, color)
548                        default_bg = False
549                else:
550                        default_bg = True
551               
552                if textcolor:
553                        color = gtk.gdk.color_parse(textcolor)
554                        self.banner.modify_fg(gtk.STATE_NORMAL, color)
555                        default_fg = False
556                else:
557                        default_fg = True
558                if default_fg or default_bg:
559                        self._on_style_set_event(self.banner, None, default_fg, default_bg)
560                if self.browser:
561                        self.browser.update_theme()
562       
563        def disconnect_style_event(self):
564                if self.style_event_id:
565                        self.banner.disconnect(self.style_event_id)
566                        self.style_event_id = 0
567       
568        def connect_style_event(self, set_fg = False, set_bg = False):
569                self.disconnect_style_event()
570                self.style_event_id = self.banner.connect('style-set',
571                                        self._on_style_set_event, set_fg, set_bg)
572       
573        def _on_style_set_event(self, widget, style, *opts):
574                ''' set style of widget from style class *.Frame.Eventbox
575                        opts[0] == True -> set fg color
576                        opts[1] == True -> set bg color '''
577               
578                self.disconnect_style_event()
579                if opts[1]:
580                        bg_color = widget.style.bg[gtk.STATE_SELECTED]
581                        self.banner_eventbox.modify_bg(gtk.STATE_NORMAL, bg_color)
582                if opts[0]:
583                        fg_color = widget.style.fg[gtk.STATE_SELECTED]
584                        self.banner.modify_fg(gtk.STATE_NORMAL, fg_color)
585                self.banner.ensure_style()
586                self.connect_style_event(opts[0], opts[1])
587       
588        def destroy(self, chain = False):
589                '''Close the browser. This can optionally close its children and
590                propagate to the parent. This should happen on actions like register,
591                or join to kill off the entire browser chain.'''
592                if self.dying:
593                        return
594                self.dying = True
595
596                # self.browser._get_agent_address() would break when no browser.
597                addr = get_agent_address(self.jid, self.node)
598                del gajim.interface.instances[self.account]['disco'][addr]
599
600                if self.browser:
601                        self.window.hide()
602                        self.browser.cleanup()
603                        self.browser = None
604                self.window.destroy()
605
606                for child in self.children[:]:
607                        child.parent = None
608                        if chain:
609                                child.destroy(chain = chain)
610                                self.children.remove(child)
611                if self.parent:
612                        if self in self.parent.children:
613                                self.parent.children.remove(self)
614                        if chain and not self.parent.children:
615                                self.parent.destroy(chain = chain)
616                                self.parent = None
617                else:
618                        self.cache.cleanup()
619
620        def travel(self, jid, node):
621                '''Travel to an agent within the current services window.'''
622                if self.browser:
623                        self.browser.cleanup()
624                        self.browser = None
625                # Update the window list
626                if self.jid:
627                        old_addr = get_agent_address(self.jid, self.node)
628                        if gajim.interface.instances[self.account]['disco'].has_key(old_addr):
629                                del gajim.interface.instances[self.account]['disco'][old_addr]
630                addr = get_agent_address(jid, node)
631                gajim.interface.instances[self.account]['disco'][addr] = self
632                # We need to store these, self.browser is not always available.
633                self.jid = jid
634                self.node = node
635                self.cache.get_info(jid, node, self._travel)
636
637        def _travel(self, jid, node, identities, features, data):
638                '''Continuation of travel.'''
639                if self.dying or jid != self.jid or node != self.node:
640                        return
641                if not identities:
642                        if not self.address_comboboxentry:
643                                # We can't travel anywhere else.
644                                self.destroy()
645                        dialogs.ErrorDialog(_('The service could not be found'),
646_('There is no service at the address you entered, or it is not responding. Check the address and try again.'))
647                        return
648                klass = self.cache.get_browser(identities, features)
649                if not klass:
650                        dialogs.ErrorDialog(_('The service is not browsable'),
651_('This type of service does not contain any items to browse.'))
652                        return
653                elif klass is None:
654                        klass = AgentBrowser
655                self.browser = klass(self.account, jid, node)
656                self.browser.prepare_window(self)
657                self.browser.browse()
658
659        def open(self, jid, node):
660                '''Open an agent. By default, this happens in a new window.'''
661                try:
662                        win = gajim.interface.instances[self.account]['disco']\
663                                [get_agent_address(jid, node)]
664                        win.window.present()
665                        return
666                except KeyError:
667                        pass
668                try:
669                        win = ServiceDiscoveryWindow(self.account, jid, node, parent=self)
670                except RuntimeError:
671                        # Disconnected, perhaps
672                        return
673                self.children.append(win)
674
675        def on_service_discovery_window_destroy(self, widget):
676                self.destroy()
677
678        def on_close_button_clicked(self, widget):
679                self.destroy()
680
681        def on_address_comboboxentry_changed(self, widget):
682                if self.address_comboboxentry.get_active() != -1:
683                        # user selected one of the entries so do auto-visit
684                        jid = self.address_comboboxentry.child.get_text().decode('utf-8')
685                        self.travel(jid, '')
686
687        def on_go_button_clicked(self, widget):
688                jid = self.address_comboboxentry.child.get_text().decode('utf-8')
689                if jid == self.jid: # jid has not changed
690                        return
691                if jid in self.latest_addresses:
692                        self.latest_addresses.remove(jid)
693                self.latest_addresses.insert(0, jid)
694                if len(self.latest_addresses) > 10:
695                        self.latest_addresses = self.latest_addresses[0:10]
696                self.address_comboboxentry.get_model().clear()
697                for j in self.latest_addresses:
698                        self.address_comboboxentry.append_text(j)
699                gajim.config.set('latest_disco_addresses',
700                        ' '.join(self.latest_addresses))
701                gajim.interface.save_config()
702                self.travel(jid, '')
703
704        def on_services_treeview_row_activated(self, widget, path, col = 0):
705                self.browser.default_action()
706
707        def on_services_treeview_selection_changed(self, widget):
708                self.browser.update_actions()
709
710
711class AgentBrowser:
712        '''Class that deals with browsing agents and appearance of the browser
713        window. This class and subclasses should basically be treated as "part"
714        of the ServiceDiscoveryWindow class, but had to be separated because this part
715        is dynamic.'''
716        def __init__(self, account, jid, node):
717                self.account = account
718                self.jid = jid
719                self.node = node
720                self._total_items = 0
721                self.browse_button = None
722                # This is for some timeout callbacks
723                self.active = False
724
725        def _get_agent_address(self):
726                '''Returns the agent's address for displaying in the GUI.'''
727                return get_agent_address(self.jid, self.node)
728
729        def _set_initial_title(self):
730                '''Set the initial window title based on agent address.'''
731                self.window.window.set_title(_('Browsing %s using account %s') % \
732                        (self._get_agent_address(), self.account))
733                self.window._set_window_banner_text(self._get_agent_address())
734
735        def _create_treemodel(self):
736                '''Create the treemodel for the services treeview. When subclassing,
737                note that the first two columns should ALWAYS be of type string and
738                contain the JID and node of the item respectively.'''
739                # JID, node, name, address
740                self.model = gtk.ListStore(str, str, str, str)
741                self.model.set_sort_column_id(3, gtk.SORT_ASCENDING)
742                self.window.services_treeview.set_model(self.model)
743                # Name column
744                col = gtk.TreeViewColumn(_('Name'))
745                renderer = gtk.CellRendererText()
746                col.pack_start(renderer)
747                col.set_attributes(renderer, text = 2)
748                self.window.services_treeview.insert_column(col, -1)
749                col.set_resizable(True)
750                # Address column
751                col = gtk.TreeViewColumn(_('JID'))
752                renderer = gtk.CellRendererText()
753                col.pack_start(renderer)
754                col.set_attributes(renderer, text = 3)
755                self.window.services_treeview.insert_column(col, -1)
756                col.set_resizable(True)
757                self.window.services_treeview.set_headers_visible(True)
758
759        def _clean_treemodel(self):
760                self.model.clear()
761                for col in self.window.services_treeview.get_columns():
762                        self.window.services_treeview.remove_column(col)
763                self.window.services_treeview.set_headers_visible(False)
764
765        def _add_actions(self):
766                '''Add the action buttons to the buttonbox for actions the browser can
767                perform.'''
768                self.browse_button = gtk.Button()
769                image = gtk.image_new_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_BUTTON)
770                label = gtk.Label(_('_Browse'))
771                label.set_use_underline(True)
772                hbox = gtk.HBox()
773                hbox.pack_start(image, False, True, 6)
774                hbox.pack_end(label, True, True)
775                self.browse_button.add(hbox)
776                self.browse_button.connect('clicked', self.on_browse_button_clicked)
777                self.window.action_buttonbox.add(self.browse_button)
778                self.browse_button.show_all()
779
780        def _clean_actions(self):
781                '''Remove the action buttons specific to this browser.'''
782                if self.browse_button:
783                        self.browse_button.destroy()
784                        self.browse_button = None
785       
786        def _set_title(self, jid, node, identities, features, data):
787                '''Set the window title based on agent info.'''
788                # Set the banner and window title
789                if identities[0].has_key('name'):
790                        name = identities[0]['name']
791                        self.window._set_window_banner_text(self._get_agent_address(), name)
792
793                # Add an icon to the banner.
794                pix = self.cache.get_icon(identities)
795                self.window.banner_icon.set_from_pixbuf(pix)
796                self.window.banner_icon.show()
797
798        def _clean_title(self):