root/branches/gajim_0.11/src/disco.py

Revision 7984, 62.1 kB (checked in by asterix, 19 months ago)

mrege diff from trunk

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