[kepi] ability to use subkeys. Fixes #6051
[gajim.git] / src / disco.py
blob9a47a8765edc46b5b21b26591c83d49cde2cd3aa
1 # -*- coding: utf-8 -*-
2 ## src/disco.py
3 ##
4 ## Copyright (C) 2005-2006 Stéphan Kochen <stephan AT kochen.nl>
5 ## Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
6 ## Copyright (C) 2005-2010 Yann Leboulanger <asterix AT lagaule.org>
7 ## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
8 ## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
9 ## Copyright (C) 2007 Stephan Erb <steve-e AT h3c.de>
11 ## This file is part of Gajim.
13 ## Gajim is free software; you can redistribute it and/or modify
14 ## it under the terms of the GNU General Public License as published
15 ## by the Free Software Foundation; version 3 only.
17 ## Gajim is distributed in the hope that it will be useful,
18 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ## GNU General Public License for more details.
22 ## You should have received a copy of the GNU General Public License
23 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
26 # The appearance of the treeview, and parts of the dialog, are controlled by
27 # AgentBrowser (sub-)classes. Methods that probably should be overridden when
28 # subclassing are: (look at the docstrings and source for additional info)
29 # - def cleanup(self) *
30 # - def _create_treemodel(self) *
31 # - def _add_actions(self)
32 # - def _clean_actions(self)
33 # - def update_theme(self) *
34 # - def update_actions(self)
35 # - def default_action(self)
36 # - def _find_item(self, jid, node)
37 # - def _add_item(self, jid, node, parent_node, item, force)
38 # - def _update_item(self, iter_, jid, node, item)
39 # - def _update_info(self, iter_, jid, node, identities, features, data)
40 # - def _update_error(self, iter_, jid, node)
42 # * Should call the super class for this method.
43 # All others do not have to call back to the super class. (but can if they want
44 # the functionality)
45 # There are more methods, of course, but this is a basic set.
47 import os
48 import types
49 import weakref
50 import gobject
51 import gtk
52 import pango
54 import dialogs
55 import tooltips
56 import gtkgui_helpers
57 import groups
58 import adhoc_commands
59 import search_window
61 from common import gajim
62 from common import xmpp
63 from common.exceptions import GajimGeneralException
64 from common import helpers
65 from common import ged
67 # Dictionary mapping category, type pairs to browser class, image pairs.
68 # This is a function, so we can call it after the classes are declared.
69 # For the browser class, None means that the service will only be browsable
70 # when it advertises disco as it's feature, False means it's never browsable.
71 def _gen_agent_type_info():
72 return {
73 # Defaults
74 (0, 0): (None, None),
76 # Jabber server
77 ('server', 'im'): (ToplevelAgentBrowser, 'jabber'),
78 ('services', 'jabber'): (ToplevelAgentBrowser, 'jabber'),
79 ('hierarchy', 'branch'): (AgentBrowser, 'jabber'),
81 # Services
82 ('conference', 'text'): (MucBrowser, 'conference'),
83 ('headline', 'rss'): (AgentBrowser, 'rss'),
84 ('headline', 'weather'): (False, 'weather'),
85 ('gateway', 'weather'): (False, 'weather'),
86 ('_jid', 'weather'): (False, 'weather'),
87 ('gateway', 'sip'): (False, 'sip'),
88 ('directory', 'user'): (None, 'jud'),
89 ('pubsub', 'generic'): (PubSubBrowser, 'pubsub'),
90 ('pubsub', 'service'): (PubSubBrowser, 'pubsub'),
91 ('proxy', 'bytestreams'): (None, 'bytestreams'), # Socks5 FT proxy
92 ('headline', 'newmail'): (ToplevelAgentBrowser, 'mail'),
94 # Transports
95 ('conference', 'irc'): (ToplevelAgentBrowser, 'irc'),
96 ('_jid', 'irc'): (False, 'irc'),
97 ('gateway', 'aim'): (False, 'aim'),
98 ('_jid', 'aim'): (False, 'aim'),
99 ('gateway', 'gadu-gadu'): (False, 'gadu-gadu'),
100 ('_jid', 'gadugadu'): (False, 'gadu-gadu'),
101 ('gateway', 'http-ws'): (False, 'http-ws'),
102 ('gateway', 'icq'): (False, 'icq'),
103 ('_jid', 'icq'): (False, 'icq'),
104 ('gateway', 'msn'): (False, 'msn'),
105 ('_jid', 'msn'): (False, 'msn'),
106 ('gateway', 'sms'): (False, 'sms'),
107 ('_jid', 'sms'): (False, 'sms'),
108 ('gateway', 'smtp'): (False, 'mail'),
109 ('gateway', 'yahoo'): (False, 'yahoo'),
110 ('_jid', 'yahoo'): (False, 'yahoo'),
111 ('gateway', 'mrim'): (False, 'mrim'),
112 ('_jid', 'mrim'): (False, 'mrim'),
113 ('gateway', 'facebook'): (False, 'facebook'),
114 ('_jid', 'facebook'): (False, 'facebook'),
117 # Category type to "human-readable" description string, and sort priority
118 _cat_to_descr = {
119 'other': (_('Others'), 2),
120 'gateway': (_('Transports'), 0),
121 '_jid': (_('Transports'), 0),
122 #conference is a category for listing mostly groupchats in service discovery
123 'conference': (_('Conference'), 1),
127 class CacheDictionary:
129 A dictionary that keeps items around for only a specific time. Lifetime is
130 in minutes. Getrefresh specifies whether to refresh when an item is merely
131 accessed instead of set aswell
134 def __init__(self, lifetime, getrefresh = True):
135 self.lifetime = lifetime * 1000 * 60
136 self.getrefresh = getrefresh
137 self.cache = {}
139 class CacheItem:
141 An object to store cache items and their timeouts
143 def __init__(self, value):
144 self.value = value
145 self.source = None
147 def __call__(self):
148 return self.value
150 def cleanup(self):
151 for key in self.cache.keys():
152 item = self.cache[key]
153 if item.source:
154 gobject.source_remove(item.source)
155 del self.cache[key]
157 def _expire_timeout(self, key):
159 The timeout has expired, remove the object
161 if key in self.cache:
162 del self.cache[key]
163 return False
165 def _refresh_timeout(self, key):
167 The object was accessed, refresh the timeout
169 item = self.cache[key]
170 if item.source:
171 gobject.source_remove(item.source)
172 if self.lifetime:
173 source = gobject.timeout_add_seconds(self.lifetime/1000, self._expire_timeout, key)
174 item.source = source
176 def __getitem__(self, key):
177 item = self.cache[key]
178 if self.getrefresh:
179 self._refresh_timeout(key)
180 return item()
182 def __setitem__(self, key, value):
183 item = self.CacheItem(value)
184 self.cache[key] = item
185 self._refresh_timeout(key)
187 def __delitem__(self, key):
188 item = self.cache[key]
189 if item.source:
190 gobject.source_remove(item.source)
191 del self.cache[key]
193 def __contains__(self, key):
194 return key in self.cache
195 has_key = __contains__
197 _icon_cache = CacheDictionary(15)
199 def get_agent_address(jid, node = None):
201 Get an agent's address for displaying in the GUI
203 if node:
204 return '%s@%s' % (node, str(jid))
205 else:
206 return str(jid)
208 class Closure(object):
210 A weak reference to a callback with arguments as an object
212 Weak references to methods immediatly die, even if the object is still
213 alive. Besides a handy way to store a callback, this provides a workaround
214 that keeps a reference to the object instead.
216 Userargs and removeargs must be tuples.
219 def __init__(self, cb, userargs = (), remove = None, removeargs = ()):
220 self.userargs = userargs
221 self.remove = remove
222 self.removeargs = removeargs
223 if isinstance(cb, types.MethodType):
224 self.meth_self = weakref.ref(cb.im_self, self._remove)
225 self.meth_name = cb.func_name
226 elif callable(cb):
227 self.meth_self = None
228 self.cb = weakref.ref(cb, self._remove)
229 else:
230 raise TypeError('Object is not callable')
232 def _remove(self, ref):
233 if self.remove:
234 self.remove(self, *self.removeargs)
236 def __call__(self, *args, **kwargs):
237 if self.meth_self:
238 obj = self.meth_self()
239 cb = getattr(obj, self.meth_name)
240 else:
241 cb = self.cb()
242 args = args + self.userargs
243 return cb(*args, **kwargs)
246 class ServicesCache:
248 Class that caches our query results. Each connection will have it's own
249 ServiceCache instance
252 def __init__(self, account):
253 self.account = account
254 self._items = CacheDictionary(0, getrefresh = False)
255 self._info = CacheDictionary(0, getrefresh = False)
256 self._subscriptions = CacheDictionary(5, getrefresh=False)
257 self._cbs = {}
258 gajim.ged.register_event_handler('agent-items-received', ged.GUI1,
259 self._nec_agent_items_received)
260 gajim.ged.register_event_handler('agent-items-error-received', ged.GUI1,
261 self._nec_agent_items_error_received)
262 gajim.ged.register_event_handler('agent-info-received', ged.GUI1,
263 self._nec_agent_info_received)
264 gajim.ged.register_event_handler('agent-info-error-received', ged.GUI1,
265 self._nec_agent_info_error_received)
267 def __del__(self):
268 gajim.ged.remove_event_handler('agent-items-received', ged.GUI1,
269 self._nec_agent_items_received)
270 gajim.ged.remove_event_handler('agent-items-error-received', ged.GUI1,
271 self._nec_agent_items_error_received)
272 gajim.ged.remove_event_handler('agent-info-received', ged.GUI1,
273 self._nec_agent_info_received)
274 gajim.ged.remove_event_handler('agent-info-error-received', ged.GUI1,
275 self._nec_agent_info_error_received)
277 def cleanup(self):
278 self._items.cleanup()
279 self._info.cleanup()
281 def _clean_closure(self, cb, type_, addr):
282 # A closure died, clean up
283 cbkey = (type_, addr)
284 try:
285 self._cbs[cbkey].remove(cb)
286 except KeyError:
287 return
288 except ValueError:
289 return
290 # Clean an empty list
291 if not self._cbs[cbkey]:
292 del self._cbs[cbkey]
294 def get_icon(self, identities = []):
296 Return the icon for an agent
298 # Grab the first identity with an icon
299 for identity in identities:
300 try:
301 cat, type_ = identity['category'], identity['type']
302 info = _agent_type_info[(cat, type_)]
303 except KeyError:
304 continue
305 filename = info[1]
306 if filename:
307 break
308 else:
309 # Loop fell through, default to unknown
310 info = _agent_type_info[(0, 0)]
311 filename = info[1]
312 if not filename: # we don't have an image to show for this type
313 filename = 'jabber'
314 # Use the cache if possible
315 if filename in _icon_cache:
316 return _icon_cache[filename]
317 # Or load it
318 pix = gtkgui_helpers.get_icon_pixmap('gajim-agent-' + filename, size=32)
319 # Store in cache
320 _icon_cache[filename] = pix
321 return pix
323 def get_browser(self, identities=[], features=[]):
325 Return the browser class for an agent
327 # First pass, we try to find a ToplevelAgentBrowser
328 for identity in identities:
329 try:
330 cat, type_ = identity['category'], identity['type']
331 info = _agent_type_info[(cat, type_)]
332 except KeyError:
333 continue
334 browser = info[0]
335 if browser and browser == ToplevelAgentBrowser:
336 return browser
338 # second pass, we haven't found a ToplevelAgentBrowser
339 for identity in identities:
340 try:
341 cat, type_ = identity['category'], identity['type']
342 info = _agent_type_info[(cat, type_)]
343 except KeyError:
344 continue
345 browser = info[0]
346 if browser:
347 return browser
348 # NS_BROWSE is deprecated, but we check for it anyways.
349 # Some services list it in features and respond to
350 # NS_DISCO_ITEMS anyways.
351 # Allow browsing for unknown types aswell.
352 if (not features and not identities) or \
353 xmpp.NS_DISCO_ITEMS in features or xmpp.NS_BROWSE in features:
354 return ToplevelAgentBrowser
355 return None
357 def get_info(self, jid, node, cb, force=False, nofetch=False, args=()):
359 Get info for an agent
361 addr = get_agent_address(jid, node)
362 # Check the cache
363 if addr in self._info and not force:
364 args = self._info[addr] + args
365 cb(jid, node, *args)
366 return
367 if nofetch:
368 return
370 # Create a closure object
371 cbkey = ('info', addr)
372 cb = Closure(cb, userargs=args, remove=self._clean_closure,
373 removeargs=cbkey)
374 # Are we already fetching this?
375 if cbkey in self._cbs:
376 self._cbs[cbkey].append(cb)
377 else:
378 self._cbs[cbkey] = [cb]
379 gajim.connections[self.account].discoverInfo(jid, node)
381 def get_items(self, jid, node, cb, force=False, nofetch=False, args=()):
383 Get a list of items in an agent
385 addr = get_agent_address(jid, node)
386 # Check the cache
387 if addr in self._items and not force:
388 args = (self._items[addr],) + args
389 cb(jid, node, *args)
390 return
391 if nofetch:
392 return
394 # Create a closure object
395 cbkey = ('items', addr)
396 cb = Closure(cb, userargs=args, remove=self._clean_closure,
397 removeargs=cbkey)
398 # Are we already fetching this?
399 if cbkey in self._cbs:
400 self._cbs[cbkey].append(cb)
401 else:
402 self._cbs[cbkey] = [cb]
403 gajim.connections[self.account].discoverItems(jid, node)
405 def _nec_agent_info_received(self, obj):
407 Callback for when we receive an agent's info
408 array is (agent, node, identities, features, data)
410 # We receive events from all accounts from GED
411 if obj.conn.name != self.account:
412 return
413 self._on_agent_info(obj.fjid, obj.node, obj.identities, obj.features,
414 obj.data)
416 def _on_agent_info(self, fjid, node, identities, features, data):
417 addr = get_agent_address(fjid, node)
419 # Store in cache
420 self._info[addr] = (identities, features, data)
422 # Call callbacks
423 cbkey = ('info', addr)
424 if cbkey in self._cbs:
425 for cb in self._cbs[cbkey]:
426 cb(fjid, node, identities, features, data)
427 # clean_closure may have beaten us to it
428 if cbkey in self._cbs:
429 del self._cbs[cbkey]
431 def _nec_agent_items_received(self, obj):
433 Callback for when we receive an agent's items
434 array is (agent, node, items)
436 # We receive events from all accounts from GED
437 if obj.conn.name != self.account:
438 return
440 addr = get_agent_address(obj.fjid, obj.node)
442 # Store in cache
443 self._items[addr] = obj.items
445 # Call callbacks
446 cbkey = ('items', addr)
447 if cbkey in self._cbs:
448 for cb in self._cbs[cbkey]:
449 cb(obj.fjid, obj.node, obj.items)
450 # clean_closure may have beaten us to it
451 if cbkey in self._cbs:
452 del self._cbs[cbkey]
454 def _nec_agent_info_error_received(self, obj):
456 Callback for when a query fails. Even after the browse and agents
457 namespaces
459 # We receive events from all accounts from GED
460 if obj.conn.name != self.account:
461 return
462 addr = get_agent_address(obj.fjid)
464 # Call callbacks
465 cbkey = ('info', addr)
466 if cbkey in self._cbs:
467 for cb in self._cbs[cbkey]:
468 cb(obj.fjid, '', 0, 0, 0)
469 # clean_closure may have beaten us to it
470 if cbkey in self._cbs:
471 del self._cbs[cbkey]
473 def _nec_agent_items_error_received(self, obj):
475 Callback for when a query fails. Even after the browse and agents
476 namespaces
478 # We receive events from all accounts from GED
479 if obj.conn.name != self.account:
480 return
481 addr = get_agent_address(obj.fjid)
483 # Call callbacks
484 cbkey = ('items', addr)
485 if cbkey in self._cbs:
486 for cb in self._cbs[cbkey]:
487 cb(obj.fjid, '', 0)
488 # clean_closure may have beaten us to it
489 if cbkey in self._cbs:
490 del self._cbs[cbkey]
492 # object is needed so that @property works
493 class ServiceDiscoveryWindow(object):
495 Class that represents the Services Discovery window
498 def __init__(self, account, jid='', node='', address_entry=False,
499 parent=None, initial_identities=None):
500 self.account = account
501 self.parent = parent
502 if not jid:
503 jid = gajim.config.get_per('accounts', account, 'hostname')
504 node = ''
506 self.jid = None
507 self.browser = None
508 self.children = []
509 self.dying = False
510 self.node = None
511 self.reloading = False
513 # Check connection
514 if gajim.connections[account].connected < 2:
515 dialogs.ErrorDialog(_('You are not connected to the server'),
516 _('Without a connection, you can not browse available services'))
517 raise RuntimeError, 'You must be connected to browse services'
519 # Get a ServicesCache object.
520 try:
521 self.cache = gajim.connections[account].services_cache
522 except AttributeError:
523 self.cache = ServicesCache(account)
524 gajim.connections[account].services_cache = self.cache
526 if initial_identities:
527 self.cache._on_agent_info(jid, node, initial_identities, [], None)
528 self.xml = gtkgui_helpers.get_gtk_builder('service_discovery_window.ui')
529 self.window = self.xml.get_object('service_discovery_window')
530 self.services_treeview = self.xml.get_object('services_treeview')
531 self.model = None
532 # This is more reliable than the cursor-changed signal.
533 selection = self.services_treeview.get_selection()
534 selection.connect_after('changed',
535 self.on_services_treeview_selection_changed)
536 self.services_scrollwin = self.xml.get_object('services_scrollwin')
537 self.progressbar = self.xml.get_object('services_progressbar')
538 self.banner = self.xml.get_object('banner_agent_label')
539 self.banner_icon = self.xml.get_object('banner_agent_icon')
540 self.banner_eventbox = self.xml.get_object('banner_agent_eventbox')
541 self.style_event_id = 0
542 self.banner.realize()
543 self.paint_banner()
544 self.action_buttonbox = self.xml.get_object('action_buttonbox')
546 # Address combobox
547 self.address_comboboxentry = None
548 address_table = self.xml.get_object('address_table')
549 if address_entry:
550 self.address_comboboxentry = self.xml.get_object(
551 'address_comboboxentry')
552 self.address_comboboxentry_entry = self.address_comboboxentry.child
553 self.address_comboboxentry_entry.set_activates_default(True)
555 self.latest_addresses = gajim.config.get(
556 'latest_disco_addresses').split()
557 if jid in self.latest_addresses:
558 self.latest_addresses.remove(jid)
559 self.latest_addresses.insert(0, jid)
560 if len(self.latest_addresses) > 10:
561 self.latest_addresses = self.latest_addresses[0:10]
562 for j in self.latest_addresses:
563 self.address_comboboxentry.append_text(j)
564 self.address_comboboxentry.child.set_text(jid)
565 else:
566 # Don't show it at all if we didn't ask for it
567 address_table.set_no_show_all(True)
568 address_table.hide()
570 accel_group = gtk.AccelGroup()
571 keyval, mod = gtk.accelerator_parse('<Control>r')
572 accel_group.connect_group(keyval, mod, gtk.ACCEL_VISIBLE,
573 self.accel_group_func)
574 self.window.add_accel_group(accel_group)
576 self._initial_state()
577 self.xml.connect_signals(self)
578 self.travel(jid, node)
579 self.window.show_all()
581 @property
582 def _get_account(self):
583 return self.account
585 @property
586 def _set_account(self, value):
587 self.account = value
588 self.cache.account = value
589 if self.browser:
590 self.browser.account = value
592 def accel_group_func(self, accel_group, acceleratable, keyval, modifier):
593 if (modifier & gtk.gdk.CONTROL_MASK) and (keyval == gtk.keysyms.r):
594 self.reload()
596 def _initial_state(self):
598 Set some initial state on the window. Separated in a method because it's
599 handy to use within browser's cleanup method
601 self.progressbar.hide()
602 title_text = _('Service Discovery using account %s') % self.account
603 self.window.set_title(title_text)
604 self._set_window_banner_text(_('Service Discovery'))
605 self.banner_icon.clear()
606 self.banner_icon.hide() # Just clearing it doesn't work
608 def _set_window_banner_text(self, text, text_after = None):
609 theme = gajim.config.get('roster_theme')
610 bannerfont = gajim.config.get_per('themes', theme, 'bannerfont')
611 bannerfontattrs = gajim.config.get_per('themes', theme,
612 'bannerfontattrs')
614 if bannerfont:
615 font = pango.FontDescription(bannerfont)
616 else:
617 font = pango.FontDescription('Normal')
618 if bannerfontattrs:
619 # B is attribute set by default
620 if 'B' in bannerfontattrs:
621 font.set_weight(pango.WEIGHT_HEAVY)
622 if 'I' in bannerfontattrs:
623 font.set_style(pango.STYLE_ITALIC)
625 font_attrs = 'font_desc="%s"' % font.to_string()
626 font_size = font.get_size()
628 # in case there is no font specified we use x-large font size
629 if font_size == 0:
630 font_attrs = '%s size="large"' % font_attrs
631 markup = '<span %s>%s</span>' % (font_attrs, text)
632 if text_after:
633 font.set_weight(pango.WEIGHT_NORMAL)
634 markup = '%s\n<span font_desc="%s" size="small">%s</span>' % \
635 (markup, font.to_string(), text_after)
636 self.banner.set_markup(markup)
638 def paint_banner(self):
640 Repaint the banner with theme color
642 theme = gajim.config.get('roster_theme')
643 bgcolor = gajim.config.get_per('themes', theme, 'bannerbgcolor')
644 textcolor = gajim.config.get_per('themes', theme, 'bannertextcolor')
645 self.disconnect_style_event()
646 if bgcolor:
647 color = gtk.gdk.color_parse(bgcolor)
648 self.banner_eventbox.modify_bg(gtk.STATE_NORMAL, color)
649 default_bg = False
650 else:
651 default_bg = True
653 if textcolor:
654 color = gtk.gdk.color_parse(textcolor)
655 self.banner.modify_fg(gtk.STATE_NORMAL, color)
656 default_fg = False
657 else:
658 default_fg = True
659 if default_fg or default_bg:
660 self._on_style_set_event(self.banner, None, default_fg, default_bg)
661 if self.browser:
662 self.browser.update_theme()
664 def disconnect_style_event(self):
665 if self.style_event_id:
666 self.banner.disconnect(self.style_event_id)
667 self.style_event_id = 0
669 def connect_style_event(self, set_fg = False, set_bg = False):
670 self.disconnect_style_event()
671 self.style_event_id = self.banner.connect('style-set',
672 self._on_style_set_event, set_fg, set_bg)
674 def _on_style_set_event(self, widget, style, *opts):
676 Set style of widget from style class *.Frame.Eventbox
677 opts[0] == True -> set fg color
678 opts[1] == True -> set bg color
680 self.disconnect_style_event()
681 if opts[1]:
682 bg_color = widget.style.bg[gtk.STATE_SELECTED]
683 self.banner_eventbox.modify_bg(gtk.STATE_NORMAL, bg_color)
684 if opts[0]:
685 fg_color = widget.style.fg[gtk.STATE_SELECTED]
686 self.banner.modify_fg(gtk.STATE_NORMAL, fg_color)
687 self.banner.ensure_style()
688 self.connect_style_event(opts[0], opts[1])
690 def destroy(self, chain = False):
692 Close the browser. This can optionally close its children and propagate
693 to the parent. This should happen on actions like register, or join to
694 kill off the entire browser chain
696 if self.dying:
697 return
698 self.dying = True
700 # self.browser._get_agent_address() would break when no browser.
701 addr = get_agent_address(self.jid, self.node)
702 if addr in gajim.interface.instances[self.account]['disco']:
703 del gajim.interface.instances[self.account]['disco'][addr]
705 if self.browser:
706 self.window.hide()
707 self.browser.cleanup()
708 self.browser = None
709 self.window.destroy()
711 for child in self.children[:]:
712 child.parent = None
713 if chain:
714 child.destroy(chain = chain)
715 self.children.remove(child)
716 if self.parent:
717 if self in self.parent.children:
718 self.parent.children.remove(self)
719 if chain and not self.parent.children:
720 self.parent.destroy(chain = chain)
721 self.parent = None
722 else:
723 self.cache.cleanup()
725 def reload(self):
726 if not self.jid:
727 return
728 self.reloading = True
729 self.travel(self.jid, self.node)
731 def travel(self, jid, node):
733 Travel to an agent within the current services window
735 if self.browser:
736 self.browser.cleanup()
737 self.browser = None
738 # Update the window list
739 if self.jid:
740 old_addr = get_agent_address(self.jid, self.node)
741 if old_addr in gajim.interface.instances[self.account]['disco']:
742 del gajim.interface.instances[self.account]['disco'][old_addr]
743 addr = get_agent_address(jid, node)
744 gajim.interface.instances[self.account]['disco'][addr] = self
745 # We need to store these, self.browser is not always available.
746 self.jid = jid
747 self.node = node
748 self.cache.get_info(jid, node, self._travel, force=self.reloading)
750 def _travel(self, jid, node, identities, features, data):
752 Continuation of travel
754 if self.dying or jid != self.jid or node != self.node:
755 return
756 if not identities:
757 if not self.address_comboboxentry:
758 # We can't travel anywhere else.
759 self.destroy()
760 dialogs.ErrorDialog(_('The service could not be found'),
761 _('There is no service at the address you entered, or it is not responding. Check the address and try again.'))
762 return
763 klass = self.cache.get_browser(identities, features)
764 if not klass:
765 dialogs.ErrorDialog(_('The service is not browsable'),
766 _('This type of service does not contain any items to browse.'))
767 return
768 elif klass is None:
769 klass = AgentBrowser
770 self.browser = klass(self.account, jid, node)
771 self.browser.prepare_window(self)
772 self.browser.browse(force=self.reloading)
773 self.reloading = False
775 def open(self, jid, node):
777 Open an agent. By default, this happens in a new window
779 try:
780 win = gajim.interface.instances[self.account]['disco']\
781 [get_agent_address(jid, node)]
782 win.window.present()
783 return
784 except KeyError:
785 pass
786 try:
787 win = ServiceDiscoveryWindow(self.account, jid, node, parent=self)
788 except RuntimeError:
789 # Disconnected, perhaps
790 return
791 self.children.append(win)
793 def on_service_discovery_window_destroy(self, widget):
794 self.destroy()
796 def on_close_button_clicked(self, widget):
797 self.destroy()
799 def on_address_comboboxentry_changed(self, widget):
800 if self.address_comboboxentry.get_active() != -1:
801 # user selected one of the entries so do auto-visit
802 jid = self.address_comboboxentry.child.get_text().decode('utf-8')
803 try:
804 jid = helpers.parse_jid(jid)
805 except helpers.InvalidFormat, s:
806 pritext = _('Invalid Server Name')
807 dialogs.ErrorDialog(pritext, str(s))
808 return
809 self.travel(jid, '')
811 def on_go_button_clicked(self, widget):
812 jid = self.address_comboboxentry.child.get_text().decode('utf-8')
813 try:
814 jid = helpers.parse_jid(jid)
815 except helpers.InvalidFormat, s:
816 pritext = _('Invalid Server Name')
817 dialogs.ErrorDialog(pritext, str(s))
818 return
819 if jid == self.jid: # jid has not changed
820 return
821 if jid in self.latest_addresses:
822 self.latest_addresses.remove(jid)
823 self.latest_addresses.insert(0, jid)
824 if len(self.latest_addresses) > 10:
825 self.latest_addresses = self.latest_addresses[0:10]
826 self.address_comboboxentry.get_model().clear()
827 for j in self.latest_addresses:
828 self.address_comboboxentry.append_text(j)
829 gajim.config.set('latest_disco_addresses',
830 ' '.join(self.latest_addresses))
831 gajim.interface.save_config()
832 self.travel(jid, '')
834 def on_services_treeview_row_activated(self, widget, path, col = 0):
835 if self.browser:
836 self.browser.default_action()
838 def on_services_treeview_selection_changed(self, widget):
839 if self.browser:
840 self.browser.update_actions()
843 class AgentBrowser:
845 Class that deals with browsing agents and appearance of the browser window.
846 This class and subclasses should basically be treated as "part" of the
847 ServiceDiscoveryWindow class, but had to be separated because this part is
848 dynamic
851 def __init__(self, account, jid, node):
852 self.account = account
853 self.jid = jid
854 self.node = node
855 self._total_items = 0
856 self.browse_button = None
857 # This is for some timeout callbacks
858 self.active = False
860 def _get_agent_address(self):
862 Get the agent's address for displaying in the GUI
864 return get_agent_address(self.jid, self.node)
866 def _set_initial_title(self):
868 Set the initial window title based on agent address
870 self.window.window.set_title(_('Browsing %(address)s using account '
871 '%(account)s') % {'address': self._get_agent_address(),
872 'account': self.account})
873 self.window._set_window_banner_text(self._get_agent_address())
875 def _create_treemodel(self):
877 Create the treemodel for the services treeview. When subclassing, note
878 that the first two columns should ALWAYS be of type string and contain
879 the JID and node of the item respectively
881 # JID, node, name, address
882 self.model = gtk.ListStore(str, str, str, str)
883 self.model.set_sort_column_id(3, gtk.SORT_ASCENDING)
884 self.window.services_treeview.set_model(self.model)
885 # Name column
886 col = gtk.TreeViewColumn(_('Name'))
887 renderer = gtk.CellRendererText()
888 col.pack_start(renderer)
889 col.set_attributes(renderer, text = 2)
890 self.window.services_treeview.insert_column(col, -1)
891 col.set_resizable(True)
892 # Address column
893 col = gtk.TreeViewColumn(_('JID'))
894 renderer = gtk.CellRendererText()
895 col.pack_start(renderer)
896 col.set_attributes(renderer, text = 3)
897 self.window.services_treeview.insert_column(col, -1)
898 col.set_resizable(True)
899 self.window.services_treeview.set_headers_visible(True)
901 def _clean_treemodel(self):
902 self.model.clear()
903 for col in self.window.services_treeview.get_columns():
904 self.window.services_treeview.remove_column(col)
905 self.window.services_treeview.set_headers_visible(False)
907 def _add_actions(self):
909 Add the action buttons to the buttonbox for actions the browser can
910 perform
912 self.browse_button = gtk.Button()
913 image = gtk.image_new_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_BUTTON)
914 label = gtk.Label(_('_Browse'))
915 label.set_use_underline(True)
916 hbox = gtk.HBox()
917 hbox.pack_start(image, False, True, 6)
918 hbox.pack_end(label, True, True)
919 self.browse_button.add(hbox)
920 self.browse_button.connect('clicked', self.on_browse_button_clicked)
921 self.window.action_buttonbox.add(self.browse_button)
922 self.browse_button.show_all()
924 def _clean_actions(self):
926 Remove the action buttons specific to this browser
928 if self.browse_button:
929 self.browse_button.destroy()
930 self.browse_button = None
932 def _set_title(self, jid, node, identities, features, data):
934 Set the window title based on agent info
936 # Set the banner and window title
937 if 'name' in identities[0]:
938 name = identities[0]['name']
939 self.window._set_window_banner_text(self._get_agent_address(), name)
941 # Add an icon to the banner.
942 pix = self.cache.get_icon(identities)
943 self.window.banner_icon.set_from_pixbuf(pix)
944 self.window.banner_icon.show()
946 def _clean_title(self):
947 # Everything done here is done in window._initial_state
948 # This is for subclasses.
949 pass
951 def prepare_window(self, window):
953 Prepare the service discovery window. Called when a browser is hooked up
954 with a ServiceDiscoveryWindow instance
956 self.window = window
957 self.cache = window.cache
959 self._set_initial_title()
960 self._create_treemodel()
961 self._add_actions()
963 # This is a hack. The buttonbox apparently doesn't care about pack_start
964 # or pack_end, so we repack the close button here to make sure it's last
965 close_button = self.window.xml.get_object('close_button')
966 self.window.action_buttonbox.remove(close_button)
967 self.window.action_buttonbox.pack_end(close_button)
968 close_button.show_all()
970 self.update_actions()
972 self.active = True
973 self.cache.get_info(self.jid, self.node, self._set_title)
975 def cleanup(self):
977 Cleanup when the window intends to switch browsers
979 self.active = False
981 self._clean_actions()
982 self._clean_treemodel()
983 self._clean_title()
985 self.window._initial_state()
987 def update_theme(self):
989 Called when the default theme is changed
991 pass
993 def on_browse_button_clicked(self, widget = None):
995 When we want to browse an agent: open a new services window with a
996 browser for the agent type
998 model, iter_ = self.window.services_treeview.get_selection().get_selected()
999 if not iter_:
1000 return
1001 jid = model[iter_][0].decode('utf-8')
1002 if jid:
1003 node = model[iter_][1].decode('utf-8')
1004 self.window.open(jid, node)
1006 def update_actions(self):
1008 When we select a row: activate action buttons based on the agent's info
1010 if self.browse_button:
1011 self.browse_button.set_sensitive(False)
1012 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1013 if not iter_:
1014 return
1015 jid = model[iter_][0].decode('utf-8')
1016 node = model[iter_][1].decode('utf-8')
1017 if jid:
1018 self.cache.get_info(jid, node, self._update_actions, nofetch = True)
1020 def _update_actions(self, jid, node, identities, features, data):
1022 Continuation of update_actions
1024 if not identities or not self.browse_button:
1025 return
1026 klass = self.cache.get_browser(identities, features)
1027 if klass:
1028 self.browse_button.set_sensitive(True)
1030 def default_action(self):
1032 When we double-click a row: perform the default action on the selected
1033 item
1035 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1036 if not iter_:
1037 return
1038 jid = model[iter_][0].decode('utf-8')
1039 node = model[iter_][1].decode('utf-8')
1040 if jid:
1041 self.cache.get_info(jid, node, self._default_action, nofetch = True)
1043 def _default_action(self, jid, node, identities, features, data):
1045 Continuation of default_action
1047 if self.cache.get_browser(identities, features):
1048 # Browse if we can
1049 self.on_browse_button_clicked()
1050 return True
1051 return False
1053 def browse(self, force=False):
1055 Fill the treeview with agents, fetching the info if necessary
1057 self.model.clear()
1058 self._total_items = self._progress = 0
1059 self.window.progressbar.show()
1060 self._pulse_timeout = gobject.timeout_add(250, self._pulse_timeout_cb)
1061 self.cache.get_items(self.jid, self.node, self._agent_items,
1062 force=force, args=(force,))
1064 def _pulse_timeout_cb(self, *args):
1066 Simple callback to keep the progressbar pulsing
1068 if not self.active:
1069 return False
1070 self.window.progressbar.pulse()
1071 return True
1073 def _find_item(self, jid, node):
1075 Check if an item is already in the treeview. Return an iter to it if so,
1076 None otherwise
1078 iter_ = self.model.get_iter_root()
1079 while iter_:
1080 cjid = self.model.get_value(iter_, 0).decode('utf-8')
1081 cnode = self.model.get_value(iter_, 1).decode('utf-8')
1082 if jid == cjid and node == cnode:
1083 break
1084 iter_ = self.model.iter_next(iter_)
1085 if iter_:
1086 return iter_
1087 return None
1089 def _agent_items(self, jid, node, items, force):
1091 Callback for when we receive a list of agent items
1093 self.model.clear()
1094 self._total_items = 0
1095 gobject.source_remove(self._pulse_timeout)
1096 self.window.progressbar.hide()
1097 # The server returned an error
1098 if items == 0:
1099 if not self.window.address_comboboxentry:
1100 # We can't travel anywhere else.
1101 self.window.destroy()
1102 dialogs.ErrorDialog(_('The service is not browsable'),
1103 _('This service does not contain any items to browse.'))
1104 return
1105 # We got a list of items
1106 self.window.services_treeview.set_model(None)
1107 for item in items:
1108 jid_ = item['jid']
1109 node_ = item.get('node', '')
1110 # If such an item is already here: don't add it
1111 if self._find_item(jid_, node_):
1112 continue
1113 self._total_items += 1
1114 self._add_item(jid_, node_, node, item, force)
1115 self.window.services_treeview.set_model(self.model)
1117 def _agent_info(self, jid, node, identities, features, data):
1119 Callback for when we receive info about an agent's item
1121 iter_ = self._find_item(jid, node)
1122 if not iter_:
1123 # Not in the treeview, stop
1124 return
1125 if identities == 0:
1126 # The server returned an error
1127 self._update_error(iter_, jid, node)
1128 else:
1129 # We got our info
1130 self._update_info(iter_, jid, node, identities, features, data)
1131 self.update_actions()
1133 def _add_item(self, jid, node, parent_node, item, force):
1135 Called when an item should be added to the model. The result of a
1136 disco#items query
1138 self.model.append((jid, node, item.get('name', ''),
1139 get_agent_address(jid, node)))
1140 self.cache.get_info(jid, node, self._agent_info, force = force)
1142 def _update_item(self, iter_, jid, node, item):
1144 Called when an item should be updated in the model. The result of a
1145 disco#items query
1147 if 'name' in item:
1148 self.model[iter_][2] = item['name']
1150 def _update_info(self, iter_, jid, node, identities, features, data):
1152 Called when an item should be updated in the model with further info.
1153 The result of a disco#info query
1155 name = identities[0].get('name', '')
1156 if name:
1157 self.model[iter_][2] = name
1159 def _update_error(self, iter_, jid, node):
1160 '''Called when a disco#info query failed for an item.'''
1161 pass
1164 class ToplevelAgentBrowser(AgentBrowser):
1166 This browser is used at the top level of a jabber server to browse services
1167 such as transports, conference servers, etc
1170 def __init__(self, *args):
1171 AgentBrowser.__init__(self, *args)
1172 self._progressbar_sourceid = None
1173 self._renderer = None
1174 self._progress = 0
1175 self.tooltip = tooltips.ServiceDiscoveryTooltip()
1176 self.register_button = None
1177 self.join_button = None
1178 self.execute_button = None
1179 self.search_button = None
1180 # Keep track of our treeview signals
1181 self._view_signals = []
1182 self._scroll_signal = None
1184 def _pixbuf_renderer_data_func(self, col, cell, model, iter_):
1186 Callback for setting the pixbuf renderer's properties
1188 jid = model.get_value(iter_, 0)
1189 if jid:
1190 pix = model.get_value(iter_, 2)
1191 cell.set_property('visible', True)
1192 cell.set_property('pixbuf', pix)
1193 else:
1194 cell.set_property('visible', False)
1196 def _text_renderer_data_func(self, col, cell, model, iter_):
1198 Callback for setting the text renderer's properties
1200 jid = model.get_value(iter_, 0)
1201 markup = model.get_value(iter_, 3)
1202 state = model.get_value(iter_, 4)
1203 cell.set_property('markup', markup)
1204 if jid:
1205 cell.set_property('cell_background_set', False)
1206 if state > 0:
1207 # 1 = fetching, 2 = error
1208 cell.set_property('foreground_set', True)
1209 else:
1210 # Normal/succes
1211 cell.set_property('foreground_set', False)
1212 else:
1213 theme = gajim.config.get('roster_theme')
1214 bgcolor = gajim.config.get_per('themes', theme, 'groupbgcolor')
1215 if bgcolor:
1216 cell.set_property('cell_background_set', True)
1217 cell.set_property('foreground_set', False)
1219 def _treemodel_sort_func(self, model, iter1, iter2):
1221 Sort function for our treemode
1223 # Compare state
1224 statecmp = cmp(model.get_value(iter1, 4), model.get_value(iter2, 4))
1225 if statecmp == 0:
1226 # These can be None, apparently
1227 descr1 = model.get_value(iter1, 3)
1228 if descr1:
1229 descr1 = descr1.decode('utf-8')
1230 descr2 = model.get_value(iter2, 3)
1231 if descr2:
1232 descr2 = descr2.decode('utf-8')
1233 # Compare strings
1234 return cmp(descr1, descr2)
1235 return statecmp
1237 def _show_tooltip(self, state):
1238 view = self.window.services_treeview
1239 pointer = view.get_pointer()
1240 props = view.get_path_at_pos(pointer[0], pointer[1])
1241 # check if the current pointer is at the same path
1242 # as it was before setting the timeout
1243 if props and self.tooltip.id == props[0]:
1244 # bounding rectangle of coordinates for the cell within the treeview
1245 rect = view.get_cell_area(props[0], props[1])
1246 # position of the treeview on the screen
1247 position = view.window.get_origin()
1248 self.tooltip.show_tooltip(state, rect.height, position[1] + rect.y)
1249 else:
1250 self.tooltip.hide_tooltip()
1252 # These are all callbacks to make tooltips work
1253 def on_treeview_leave_notify_event(self, widget, event):
1254 props = widget.get_path_at_pos(int(event.x), int(event.y))
1255 if self.tooltip.timeout > 0:
1256 if not props or self.tooltip.id == props[0]:
1257 self.tooltip.hide_tooltip()
1259 def on_treeview_motion_notify_event(self, widget, event):
1260 props = widget.get_path_at_pos(int(event.x), int(event.y))
1261 if self.tooltip.timeout > 0:
1262 if not props or self.tooltip.id != props[0]:
1263 self.tooltip.hide_tooltip()
1264 if props:
1265 row = props[0]
1266 iter_ = None
1267 try:
1268 iter_ = self.model.get_iter(row)
1269 except Exception:
1270 self.tooltip.hide_tooltip()
1271 return
1272 jid = self.model[iter_][0]
1273 state = self.model[iter_][4]
1274 # Not a category, and we have something to say about state
1275 if jid and state > 0 and \
1276 (self.tooltip.timeout == 0 or self.tooltip.id != props[0]):
1277 self.tooltip.id = row
1278 self.tooltip.timeout = gobject.timeout_add(500,
1279 self._show_tooltip, state)
1281 def on_treeview_event_hide_tooltip(self, widget, event):
1283 This happens on scroll_event, key_press_event and button_press_event
1285 self.tooltip.hide_tooltip()
1287 def _create_treemodel(self):
1288 # JID, node, icon, description, state
1289 # State means 2 when error, 1 when fetching, 0 when succes.
1290 view = self.window.services_treeview
1291 self.model = gtk.TreeStore(str, str, gtk.gdk.Pixbuf, str, int)
1292 self.model.set_sort_func(4, self._treemodel_sort_func)
1293 self.model.set_sort_column_id(4, gtk.SORT_ASCENDING)
1294 view.set_model(self.model)
1296 col = gtk.TreeViewColumn()
1297 # Icon Renderer
1298 renderer = gtk.CellRendererPixbuf()
1299 renderer.set_property('xpad', 6)
1300 col.pack_start(renderer, expand=False)
1301 col.set_cell_data_func(renderer, self._pixbuf_renderer_data_func)
1302 # Text Renderer
1303 renderer = gtk.CellRendererText()
1304 col.pack_start(renderer, expand=True)
1305 col.set_cell_data_func(renderer, self._text_renderer_data_func)
1306 renderer.set_property('foreground', 'dark gray')
1307 # Save this so we can go along with theme changes
1308 self._renderer = renderer
1309 self.update_theme()
1311 view.insert_column(col, -1)
1312 col.set_resizable(True)
1314 # Connect signals
1315 scrollwin = self.window.services_scrollwin
1316 self._view_signals.append(view.connect('leave-notify-event',
1317 self.on_treeview_leave_notify_event))
1318 self._view_signals.append(view.connect('motion-notify-event',
1319 self.on_treeview_motion_notify_event))
1320 self._view_signals.append(view.connect('key-press-event',
1321 self.on_treeview_event_hide_tooltip))
1322 self._view_signals.append(view.connect('button-press-event',
1323 self.on_treeview_event_hide_tooltip))
1324 self._scroll_signal = scrollwin.connect('scroll-event',
1325 self.on_treeview_event_hide_tooltip)
1327 def _clean_treemodel(self):
1328 # Disconnect signals
1329 view = self.window.services_treeview
1330 for sig in self._view_signals:
1331 view.disconnect(sig)
1332 self._view_signals = []
1333 if self._scroll_signal:
1334 scrollwin = self.window.services_scrollwin
1335 scrollwin.disconnect(self._scroll_signal)
1336 self._scroll_signal = None
1337 AgentBrowser._clean_treemodel(self)
1339 def _add_actions(self):
1340 AgentBrowser._add_actions(self)
1341 self.execute_button = gtk.Button()
1342 image = gtk.image_new_from_stock(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_BUTTON)
1343 label = gtk.Label(_('_Execute Command'))
1344 label.set_use_underline(True)
1345 hbox = gtk.HBox()
1346 hbox.pack_start(image, False, True, 6)
1347 hbox.pack_end(label, True, True)
1348 self.execute_button.add(hbox)
1349 self.execute_button.connect('clicked', self.on_execute_button_clicked)
1350 self.window.action_buttonbox.add(self.execute_button)
1351 self.execute_button.show_all()
1353 self.register_button = gtk.Button(label=_("Re_gister"),
1354 use_underline=True)
1355 self.register_button.connect('clicked', self.on_register_button_clicked)
1356 self.window.action_buttonbox.add(self.register_button)
1357 self.register_button.show_all()
1359 self.join_button = gtk.Button()
1360 image = gtk.image_new_from_stock(gtk.STOCK_CONNECT, gtk.ICON_SIZE_BUTTON)
1361 label = gtk.Label(_('_Join'))
1362 label.set_use_underline(True)
1363 hbox = gtk.HBox()
1364 hbox.pack_start(image, False, True, 6)
1365 hbox.pack_end(label, True, True)
1366 self.join_button.add(hbox)
1367 self.join_button.connect('clicked', self.on_join_button_clicked)
1368 self.window.action_buttonbox.add(self.join_button)
1369 self.join_button.show_all()
1371 self.search_button = gtk.Button()
1372 image = gtk.image_new_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_BUTTON)
1373 label = gtk.Label(_('_Search'))
1374 label.set_use_underline(True)
1375 hbox = gtk.HBox()
1376 hbox.pack_start(image, False, True, 6)
1377 hbox.pack_end(label, True, True)
1378 self.search_button.add(hbox)
1379 self.search_button.connect('clicked', self.on_search_button_clicked)
1380 self.window.action_buttonbox.add(self.search_button)
1381 self.search_button.show_all()
1383 def _clean_actions(self):
1384 if self.execute_button:
1385 self.execute_button.destroy()
1386 self.execute_button = None
1387 if self.register_button:
1388 self.register_button.destroy()
1389 self.register_button = None
1390 if self.join_button:
1391 self.join_button.destroy()
1392 self.join_button = None
1393 if self.search_button:
1394 self.search_button.destroy()
1395 self.search_button = None
1396 AgentBrowser._clean_actions(self)
1398 def on_search_button_clicked(self, widget = None):
1400 When we want to search something: open search window
1402 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1403 if not iter_:
1404 return
1405 service = model[iter_][0].decode('utf-8')
1406 if service in gajim.interface.instances[self.account]['search']:
1407 gajim.interface.instances[self.account]['search'][service].window.\
1408 present()
1409 else:
1410 gajim.interface.instances[self.account]['search'][service] = \
1411 search_window.SearchWindow(self.account, service)
1413 def cleanup(self):
1414 self.tooltip.hide_tooltip()
1415 AgentBrowser.cleanup(self)
1417 def update_theme(self):
1418 theme = gajim.config.get('roster_theme')
1419 bgcolor = gajim.config.get_per('themes', theme, 'groupbgcolor')
1420 if bgcolor:
1421 self._renderer.set_property('cell-background', bgcolor)
1422 self.window.services_treeview.queue_draw()
1424 def on_execute_button_clicked(self, widget=None):
1426 When we want to execute a command: open adhoc command window
1428 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1429 if not iter_:
1430 return
1431 service = model[iter_][0].decode('utf-8')
1432 node = model[iter_][1].decode('utf-8')
1433 adhoc_commands.CommandWindow(self.account, service, commandnode=node)
1435 def on_register_button_clicked(self, widget = None):
1437 When we want to register an agent: request information about registering
1438 with the agent and close the window
1440 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1441 if not iter_:
1442 return
1443 jid = model[iter_][0].decode('utf-8')
1444 if jid:
1445 gajim.connections[self.account].request_register_agent_info(jid)
1446 self.window.destroy(chain = True)
1448 def on_join_button_clicked(self, widget):
1450 When we want to join an IRC room or create a new MUC room: Opens the
1451 join_groupchat_window
1453 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1454 if not iter_:
1455 return
1456 service = model[iter_][0].decode('utf-8')
1457 if 'join_gc' not in gajim.interface.instances[self.account]:
1458 try:
1459 dialogs.JoinGroupchatWindow(self.account, service)
1460 except GajimGeneralException:
1461 pass
1462 else:
1463 gajim.interface.instances[self.account]['join_gc'].window.present()
1465 def update_actions(self):
1466 if self.execute_button:
1467 self.execute_button.set_sensitive(False)
1468 if self.register_button:
1469 self.register_button.set_sensitive(False)
1470 if self.browse_button:
1471 self.browse_button.set_sensitive(False)
1472 if self.join_button:
1473 self.join_button.set_sensitive(False)
1474 if self.search_button:
1475 self.search_button.set_sensitive(False)
1476 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1477 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1478 if not iter_:
1479 return
1480 if not model[iter_][0]:
1481 # We're on a category row
1482 return
1483 if model[iter_][4] != 0:
1484 # We don't have the info (yet)
1485 # It's either unknown or a transport, register button should be active
1486 if self.register_button:
1487 self.register_button.set_sensitive(True)
1488 # Guess what kind of service we're dealing with
1489 if self.browse_button:
1490 jid = model[iter_][0].decode('utf-8')
1491 type_ = gajim.get_transport_name_from_jid(jid,
1492 use_config_setting = False)
1493 if type_:
1494 identity = {'category': '_jid', 'type': type_}
1495 klass = self.cache.get_browser([identity])
1496 if klass:
1497 self.browse_button.set_sensitive(True)
1498 else:
1499 # We couldn't guess
1500 self.browse_button.set_sensitive(True)
1501 else:
1502 # Normal case, we have info
1503 AgentBrowser.update_actions(self)
1505 def _update_actions(self, jid, node, identities, features, data):
1506 AgentBrowser._update_actions(self, jid, node, identities, features, data)
1507 if self.execute_button and xmpp.NS_COMMANDS in features:
1508 self.execute_button.set_sensitive(True)
1509 if self.search_button and xmpp.NS_SEARCH in features:
1510 self.search_button.set_sensitive(True)
1511 if self.register_button and xmpp.NS_REGISTER in features:
1512 # We can register this agent
1513 registered_transports = []
1514 jid_list = gajim.contacts.get_jid_list(self.account)
1515 for jid in jid_list:
1516 contact = gajim.contacts.get_first_contact_from_jid(
1517 self.account, jid)
1518 if _('Transports') in contact.groups:
1519 registered_transports.append(jid)
1520 if jid in registered_transports:
1521 self.register_button.set_label(_('_Edit'))
1522 else:
1523 self.register_button.set_label(_('Re_gister'))
1524 self.register_button.set_sensitive(True)
1525 if self.join_button and xmpp.NS_MUC in features:
1526 self.join_button.set_sensitive(True)
1528 def _default_action(self, jid, node, identities, features, data):
1529 if AgentBrowser._default_action(self, jid, node, identities, features, data):
1530 return True
1531 if xmpp.NS_REGISTER in features:
1532 # Register if we can't browse
1533 self.on_register_button_clicked()
1534 return True
1535 return False
1537 def browse(self, force=False):
1538 self._progress = 0
1539 AgentBrowser.browse(self, force = force)
1541 def _expand_all(self):
1543 Expand all items in the treeview
1545 # GTK apparently screws up here occasionally. :/
1546 #def expand_all(*args):
1547 # self.window.services_treeview.expand_all()
1548 # self.expanding = False
1549 # return False
1550 #self.expanding = True
1551 #gobject.idle_add(expand_all)
1552 self.window.services_treeview.expand_all()
1554 def _update_progressbar(self):
1556 Update the progressbar
1558 # Refresh this every update
1559 if self._progressbar_sourceid:
1560 gobject.source_remove(self._progressbar_sourceid)
1562 fraction = 0
1563 if self._total_items:
1564 self.window.progressbar.set_text(_("Scanning %(current)d / %(total)d.."
1565 ) % {'current': self._progress, 'total': self._total_items})
1566 fraction = float(self._progress) / float(self._total_items)
1567 if self._progress >= self._total_items:
1568 # We show the progressbar for just a bit before hiding it.
1569 id_ = gobject.timeout_add_seconds(2, self._hide_progressbar_cb)
1570 self._progressbar_sourceid = id_
1571 else:
1572 self.window.progressbar.show()
1573 # Hide the progressbar if we're timing out anyways. (20 secs)
1574 id_ = gobject.timeout_add_seconds(20, self._hide_progressbar_cb)
1575 self._progressbar_sourceid = id_
1576 self.window.progressbar.set_fraction(fraction)
1578 def _hide_progressbar_cb(self, *args):
1580 Simple callback to hide the progressbar a second after we finish
1582 if self.active:
1583 self.window.progressbar.hide()
1584 return False
1586 def _friendly_category(self, category, type_=None):
1588 Get the friendly category name and priority
1590 cat = None
1591 if type_:
1592 # Try type-specific override
1593 try:
1594 cat, prio = _cat_to_descr[(category, type_)]
1595 except KeyError:
1596 pass
1597 if not cat:
1598 try:
1599 cat, prio = _cat_to_descr[category]
1600 except KeyError:
1601 cat, prio = _cat_to_descr['other']
1602 return cat, prio
1604 def _create_category(self, cat, type_=None):
1606 Creates a category row
1608 cat, prio = self._friendly_category(cat, type_)
1609 return self.model.append(None, ('', '', None, cat, prio))
1611 def _find_category(self, cat, type_=None):
1613 Looks up a category row and returns the iterator to it, or None
1615 cat = self._friendly_category(cat, type_)[0]
1616 iter_ = self.model.get_iter_root()
1617 while iter_:
1618 if self.model.get_value(iter_, 3).decode('utf-8') == cat:
1619 break
1620 iter_ = self.model.iter_next(iter_)
1621 if iter_:
1622 return iter_
1623 return None
1625 def _find_item(self, jid, node):
1626 iter_ = None
1627 cat_iter = self.model.get_iter_root()
1628 while cat_iter and not iter_:
1629 iter_ = self.model.iter_children(cat_iter)
1630 while iter_:
1631 cjid = self.model.get_value(iter_, 0).decode('utf-8')
1632 cnode = self.model.get_value(iter_, 1).decode('utf-8')
1633 if jid == cjid and node == cnode:
1634 break
1635 iter_ = self.model.iter_next(iter_)
1636 cat_iter = self.model.iter_next(cat_iter)
1637 if iter_:
1638 return iter_
1639 return None
1641 def _add_item(self, jid, node, parent_node, item, force):
1642 # Row text
1643 addr = get_agent_address(jid, node)
1644 if 'name' in item:
1645 descr = "<b>%s</b>\n%s" % (item['name'], addr)
1646 else:
1647 descr = "<b>%s</b>" % addr
1648 # Guess which kind of service this is
1649 identities = []
1650 type_ = gajim.get_transport_name_from_jid(jid,
1651 use_config_setting = False)
1652 if type_:
1653 identity = {'category': '_jid', 'type': type_}
1654 identities.append(identity)
1655 cat_args = ('_jid', type_)
1656 else:
1657 # Put it in the 'other' category for now
1658 cat_args = ('other',)
1659 # Set the pixmap for the row
1660 pix = self.cache.get_icon(identities)
1661 # Put it in the right category
1662 cat = self._find_category(*cat_args)
1663 if not cat:
1664 cat = self._create_category(*cat_args)
1665 self.model.append(cat, (jid, node, pix, descr, 1))
1666 gobject.idle_add(self._expand_all)
1667 # Grab info on the service
1668 self.cache.get_info(jid, node, self._agent_info, force=force)
1669 self._update_progressbar()
1671 def _update_item(self, iter_, jid, node, item):
1672 addr = get_agent_address(jid, node)
1673 if 'name' in item:
1674 descr = "<b>%s</b>\n%s" % (item['name'], addr)
1675 else:
1676 descr = "<b>%s</b>" % addr
1677 self.model[iter_][3] = descr
1679 def _update_info(self, iter_, jid, node, identities, features, data):
1680 addr = get_agent_address(jid, node)
1681 name = identities[0].get('name', '')
1682 if name:
1683 descr = "<b>%s</b>\n%s" % (name, addr)
1684 else:
1685 descr = "<b>%s</b>" % addr
1687 # Update progress
1688 self._progress += 1
1689 self._update_progressbar()
1691 # Search for an icon and category we can display
1692 pix = self.cache.get_icon(identities)
1693 cat, type_ = None, None
1694 for identity in identities:
1695 try:
1696 cat, type_ = identity['category'], identity['type']
1697 except KeyError:
1698 continue
1699 break
1701 # Check if we have to move categories
1702 old_cat_iter = self.model.iter_parent(iter_)
1703 old_cat = self.model.get_value(old_cat_iter, 3).decode('utf-8')
1704 if self.model.get_value(old_cat_iter, 3) == cat:
1705 # Already in the right category, just update
1706 self.model[iter_][2] = pix
1707 self.model[iter_][3] = descr
1708 self.model[iter_][4] = 0
1709 return
1710 # Not in the right category, move it.
1711 self.model.remove(iter_)
1713 # Check if the old category is empty
1714 if not self.model.iter_is_valid(old_cat_iter):
1715 old_cat_iter = self._find_category(old_cat)
1716 if not self.model.iter_children(old_cat_iter):
1717 self.model.remove(old_cat_iter)
1719 cat_iter = self._find_category(cat, type_)
1720 if not cat_iter:
1721 cat_iter = self._create_category(cat, type_)
1722 self.model.append(cat_iter, (jid, node, pix, descr, 0))
1723 self._expand_all()
1725 def _update_error(self, iter_, jid, node):
1726 self.model[iter_][4] = 2
1727 self._progress += 1
1728 self._update_progressbar()
1731 class MucBrowser(AgentBrowser):
1732 def __init__(self, *args, **kwargs):
1733 AgentBrowser.__init__(self, *args, **kwargs)
1734 self.join_button = None
1735 self.bookmark_button = None
1737 def _create_treemodel(self):
1738 # JID, node, name, users_int, users_str, description, fetched
1739 # This is rather long, I'd rather not use a data_func here though.
1740 # Users is a string, because want to be able to leave it empty.
1741 self.model = gtk.ListStore(str, str, str, int, str, str, bool)
1742 self.model.set_sort_column_id(2, gtk.SORT_ASCENDING)
1743 self.window.services_treeview.set_model(self.model)
1744 # Name column
1745 col = gtk.TreeViewColumn(_('Name'))
1746 col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1747 col.set_fixed_width(100)
1748 renderer = gtk.CellRendererText()
1749 col.pack_start(renderer)
1750 col.set_attributes(renderer, text = 2)
1751 col.set_sort_column_id(2)
1752 self.window.services_treeview.insert_column(col, -1)
1753 col.set_resizable(True)
1754 # Users column
1755 col = gtk.TreeViewColumn(_('Users'))
1756 renderer = gtk.CellRendererText()
1757 col.pack_start(renderer)
1758 col.set_attributes(renderer, text = 4)
1759 col.set_sort_column_id(3)
1760 self.window.services_treeview.insert_column(col, -1)
1761 col.set_resizable(True)
1762 # Description column
1763 col = gtk.TreeViewColumn(_('Description'))
1764 renderer = gtk.CellRendererText()
1765 col.pack_start(renderer)
1766 col.set_attributes(renderer, text = 5)
1767 col.set_sort_column_id(4)
1768 self.window.services_treeview.insert_column(col, -1)
1769 col.set_resizable(True)
1770 # Id column
1771 col = gtk.TreeViewColumn(_('Id'))
1772 renderer = gtk.CellRendererText()
1773 col.pack_start(renderer)
1774 col.set_attributes(renderer, text = 0)
1775 col.set_sort_column_id(0)
1776 self.window.services_treeview.insert_column(col, -1)
1777 col.set_resizable(True)
1778 self.window.services_treeview.set_headers_visible(True)
1779 self.window.services_treeview.set_headers_clickable(True)
1780 # Source id for idle callback used to start disco#info queries.
1781 self._fetch_source = None
1782 # Query failure counter
1783 self._broken = 0
1784 # Connect to scrollwindow scrolling
1785 self.vadj = self.window.services_scrollwin.get_property('vadjustment')
1786 self.vadj_cbid = self.vadj.connect('value-changed', self.on_scroll)
1787 # And to size changes
1788 self.size_cbid = self.window.services_scrollwin.connect(
1789 'size-allocate', self.on_scroll)
1791 def _clean_treemodel(self):
1792 if self.size_cbid:
1793 self.window.services_scrollwin.disconnect(self.size_cbid)
1794 self.size_cbid = None
1795 if self.vadj_cbid:
1796 self.vadj.disconnect(self.vadj_cbid)
1797 self.vadj_cbid = None
1798 AgentBrowser._clean_treemodel(self)
1800 def _add_actions(self):
1801 self.bookmark_button = gtk.Button(label=_('_Bookmark'), use_underline=True)
1802 self.bookmark_button.connect('clicked', self.on_bookmark_button_clicked)
1803 self.window.action_buttonbox.add(self.bookmark_button)
1804 self.bookmark_button.show_all()
1805 self.join_button = gtk.Button(label=_('_Join'), use_underline=True)
1806 self.join_button.connect('clicked', self.on_join_button_clicked)
1807 self.window.action_buttonbox.add(self.join_button)
1808 self.join_button.show_all()
1810 def _clean_actions(self):
1811 if self.bookmark_button:
1812 self.bookmark_button.destroy()
1813 self.bookmark_button = None
1814 if self.join_button:
1815 self.join_button.destroy()
1816 self.join_button = None
1818 def on_bookmark_button_clicked(self, *args):
1819 model, iter = self.window.services_treeview.get_selection().get_selected()
1820 if not iter:
1821 return
1822 name = gajim.config.get_per('accounts', self.account, 'name')
1823 room_jid = model[iter][0].decode('utf-8')
1824 bm = {
1825 'name': room_jid.split('@')[0],
1826 'jid': room_jid,
1827 'autojoin': '0',
1828 'minimize': '0',
1829 'password': '',
1830 'nick': name
1833 for bookmark in gajim.connections[self.account].bookmarks:
1834 if bookmark['jid'] == bm['jid']:
1835 dialogs.ErrorDialog(
1836 _('Bookmark already set'),
1837 _('Group Chat "%s" is already in your bookmarks.') % bm['jid'])
1838 return
1840 gajim.connections[self.account].bookmarks.append(bm)
1841 gajim.connections[self.account].store_bookmarks()
1843 gajim.interface.roster.set_actions_menu_needs_rebuild()
1845 dialogs.InformationDialog(
1846 _('Bookmark has been added successfully'),
1847 _('You can manage your bookmarks via Actions menu in your roster.'))
1849 def on_join_button_clicked(self, *args):
1851 When we want to join a conference: ask specific informations about the
1852 selected agent and close the window
1854 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1855 if not iter_:
1856 return
1857 service = model[iter_][0].decode('utf-8')
1858 if 'join_gc' not in gajim.interface.instances[self.account]:
1859 try:
1860 dialogs.JoinGroupchatWindow(self.account, service)
1861 except GajimGeneralException:
1862 pass
1863 else:
1864 gajim.interface.instances[self.account]['join_gc']._set_room_jid(
1865 service)
1866 gajim.interface.instances[self.account]['join_gc'].window.present()
1868 def update_actions(self):
1869 sens = self.window.services_treeview.get_selection().count_selected_rows()
1870 if self.bookmark_button:
1871 self.bookmark_button.set_sensitive(sens > 0)
1872 if self.join_button:
1873 self.join_button.set_sensitive(sens > 0)
1875 def default_action(self):
1876 self.on_join_button_clicked()
1878 def _start_info_query(self):
1880 Idle callback to start checking for visible rows
1882 self._fetch_source = None
1883 self._query_visible()
1884 return False
1886 def on_scroll(self, *args):
1888 Scrollwindow callback to trigger new queries on scolling
1890 # This apparently happens when inactive sometimes
1891 self._query_visible()
1893 def _query_visible(self):
1895 Query the next visible row for info
1897 if self._fetch_source:
1898 # We're already fetching
1899 return
1900 view = self.window.services_treeview
1901 if not view.flags() & gtk.REALIZED:
1902 # Prevent a silly warning, try again in a bit.
1903 self._fetch_source = gobject.timeout_add(100, self._start_info_query)
1904 return
1905 # We have to do this in a pygtk <2.8 compatible way :/
1906 #start, end = self.window.services_treeview.get_visible_range()
1907 rect = view.get_visible_rect()
1908 iter_ = end = None
1909 # Top row
1910 try:
1911 sx, sy = view.tree_to_widget_coords(rect.x, rect.y)
1912 spath = view.get_path_at_pos(sx, sy)[0]
1913 iter_ = self.model.get_iter(spath)
1914 except TypeError:
1915 self._fetch_source = None
1916 return
1917 # Bottom row
1918 # Iter compare is broke, use the path instead
1919 try:
1920 ex, ey = view.tree_to_widget_coords(rect.x + rect.height,
1921 rect.y + rect.height)
1922 end = view.get_path_at_pos(ex, ey)[0]
1923 # end is the last visible, we want to query that aswell
1924 end = (end[0] + 1,)
1925 except TypeError:
1926 # We're at the end of the model, we can leave end=None though.
1927 pass
1928 while iter_ and self.model.get_path(iter_) != end:
1929 if not self.model.get_value(iter_, 6):
1930 jid = self.model.get_value(iter_, 0).decode('utf-8')
1931 node = self.model.get_value(iter_, 1).decode('utf-8')
1932 self.cache.get_info(jid, node, self._agent_info)
1933 self._fetch_source = True
1934 return
1935 iter_ = self.model.iter_next(iter_)
1936 self._fetch_source = None
1938 def _channel_altinfo(self, jid, node, items, name = None):
1940 Callback for the alternate disco#items query. We try to atleast get the
1941 amount of users in the room if the service does not support MUC dataforms
1943 if items == 0:
1944 # The server returned an error
1945 self._broken += 1
1946 if self._broken >= 3:
1947 # Disable queries completely after 3 failures
1948 if self.size_cbid:
1949 self.window.services_scrollwin.disconnect(self.size_cbid)
1950 self.size_cbid = None
1951 if self.vadj_cbid:
1952 self.vadj.disconnect(self.vadj_cbid)
1953 self.vadj_cbid = None
1954 self._fetch_source = None
1955 return
1956 else:
1957 iter_ = self._find_item(jid, node)
1958 if iter_:
1959 if name:
1960 self.model[iter_][2] = name
1961 self.model[iter_][3] = len(items) # The number of users
1962 self.model[iter_][4] = str(len(items)) # The number of users
1963 self.model[iter_][6] = True
1964 self._fetch_source = None
1965 self._query_visible()
1967 def _add_item(self, jid, node, parent_node, item, force):
1968 self.model.append((jid, node, item.get('name', ''), -1, '', '', False))
1969 if not self._fetch_source:
1970 self._fetch_source = gobject.idle_add(self._start_info_query)
1972 def _update_info(self, iter_, jid, node, identities, features, data):
1973 name = identities[0].get('name', '')
1974 for form in data:
1975 typefield = form.getField('FORM_TYPE')
1976 if typefield and typefield.getValue() == \
1977 'http://jabber.org/protocol/muc#roominfo':
1978 # Fill model row from the form's fields
1979 users = form.getField('muc#roominfo_occupants')
1980 descr = form.getField('muc#roominfo_description')
1981 if users:
1982 self.model[iter_][3] = int(users.getValue())
1983 self.model[iter_][4] = users.getValue()
1984 if descr:
1985 self.model[iter_][5] = descr.getValue()
1986 # Only set these when we find a form with additional info
1987 # Some servers don't support forms and put extra info in
1988 # the name attribute, so we preserve it in that case.
1989 self.model[iter_][2] = name
1990 self.model[iter_][6] = True
1991 break
1992 else:
1993 # We didn't find a form, switch to alternate query mode
1994 self.cache.get_items(jid, node, self._channel_altinfo, args = (name,))
1995 return
1996 # Continue with the next
1997 self._fetch_source = None
1998 self._query_visible()
2000 def _update_error(self, iter_, jid, node):
2001 # switch to alternate query mode
2002 self.cache.get_items(jid, node, self._channel_altinfo)
2004 def PubSubBrowser(account, jid, node):
2006 Return an AgentBrowser subclass that will display service discovery for
2007 particular pubsub service. Different pubsub services may need to present
2008 different data during browsing
2010 # for now, only discussion groups are supported...
2011 # TODO: check if it has appropriate features to be such kind of service
2012 return DiscussionGroupsBrowser(account, jid, node)
2014 class DiscussionGroupsBrowser(AgentBrowser):
2016 For browsing pubsub-based discussion groups service
2019 def __init__(self, account, jid, node):
2020 AgentBrowser.__init__(self, account, jid, node)
2022 # this will become set object when we get subscriptions; None means
2023 # we don't know yet which groups are subscribed
2024 self.subscriptions = None
2026 # this will become our action widgets when we create them; None means
2027 # we don't have them yet (needed for check in callback)
2028 self.subscribe_button = None
2029 self.unsubscribe_button = None
2031 gajim.connections[account].send_pb_subscription_query(jid, self._on_pep_subscriptions)
2033 def _create_treemodel(self):
2035 Create treemodel for the window
2037 # JID, node, name (with description) - pango markup, dont have info?, subscribed?
2038 self.model = gtk.TreeStore(str, str, str, bool, bool)
2039 # sort by name
2040 self.model.set_sort_column_id(2, gtk.SORT_ASCENDING)
2041 self.window.services_treeview.set_model(self.model)
2043 # Name column
2044 # Pango markup for name and description, description printed with
2045 # <small/> font
2046 renderer = gtk.CellRendererText()
2047 col = gtk.TreeViewColumn(_('Name'))
2048 col.pack_start(renderer)
2049 col.set_attributes(renderer, markup=2)
2050 col.set_resizable(True)
2051 self.window.services_treeview.insert_column(col, -1)
2052 self.window.services_treeview.set_headers_visible(True)
2054 # Subscription state
2055 renderer = gtk.CellRendererToggle()
2056 col = gtk.TreeViewColumn(_('Subscribed'))
2057 col.pack_start(renderer)
2058 col.set_attributes(renderer, inconsistent=3, active=4)
2059 col.set_resizable(False)
2060 self.window.services_treeview.insert_column(col, -1)
2062 # Node Column
2063 renderer = gtk.CellRendererText()
2064 col = gtk.TreeViewColumn(_('Node'))
2065 col.pack_start(renderer)
2066 col.set_attributes(renderer, markup=1)
2067 col.set_resizable(True)
2068 self.window.services_treeview.insert_column(col, -1)
2070 def _add_items(self, jid, node, items, force):
2071 for item in items:
2072 jid_ = item['jid']
2073 node_ = item.get('node', '')
2074 self._total_items += 1
2075 self._add_item(jid_, node_, node, item, force)
2077 def _in_list_foreach(self, model, path, iter_, node):
2078 if model[path][1] == node:
2079 self.in_list = True
2081 def _in_list(self, node):
2082 self.in_list = False
2083 self.model.foreach(self._in_list_foreach, node)
2084 return self.in_list
2086 def _add_item(self, jid, node, parent_node, item, force):
2088 Called when we got basic information about new node from query. Show the
2089 item
2091 name = item.get('name', '')
2093 if self.subscriptions is not None:
2094 dunno = False
2095 subscribed = node in self.subscriptions
2096 else:
2097 dunno = True
2098 subscribed = False
2100 name = gobject.markup_escape_text(name)
2101 name = '<b>%s</b>' % name
2103 parent_iter = self._get_iter(parent_node)
2104 if not self._in_list(node):
2105 self.model.append(parent_iter, (jid, node, name, dunno, subscribed))
2106 self.cache.get_items(jid, node, self._add_items, force = force,
2107 args = (force,))
2109 def _get_child_iter(self, parent_iter, node):
2110 child_iter = self.model.iter_children(parent_iter)
2111 while child_iter:
2112 if self.model[child_iter][1] == node:
2113 return child_iter
2114 child_iter = self.model.iter_next(child_iter)
2115 return None
2117 def _get_iter(self, node):
2118 ''' Look for an iter with the given node '''
2119 self.found_iter = None
2120 def is_node(model, path, iter, node):
2121 if model[iter][1] == node:
2122 self.found_iter = iter
2123 return True
2124 self.model.foreach(is_node, node)
2125 return self.found_iter
2127 def _add_actions(self):
2128 self.post_button = gtk.Button(label=_('New post'), use_underline=True)
2129 self.post_button.set_sensitive(False)
2130 self.post_button.connect('clicked', self.on_post_button_clicked)
2131 self.window.action_buttonbox.add(self.post_button)
2132 self.post_button.show_all()
2134 self.subscribe_button = gtk.Button(label=_('_Subscribe'), use_underline=True)
2135 self.subscribe_button.set_sensitive(False)
2136 self.subscribe_button.connect('clicked', self.on_subscribe_button_clicked)
2137 self.window.action_buttonbox.add(self.subscribe_button)
2138 self.subscribe_button.show_all()
2140 self.unsubscribe_button = gtk.Button(label=_('_Unsubscribe'), use_underline=True)
2141 self.unsubscribe_button.set_sensitive(False)
2142 self.unsubscribe_button.connect('clicked', self.on_unsubscribe_button_clicked)
2143 self.window.action_buttonbox.add(self.unsubscribe_button)
2144 self.unsubscribe_button.show_all()
2146 def _clean_actions(self):
2147 if self.post_button is not None:
2148 self.post_button.destroy()
2149 self.post_button = None
2151 if self.subscribe_button is not None:
2152 self.subscribe_button.destroy()
2153 self.subscribe_button = None
2155 if self.unsubscribe_button is not None:
2156 self.unsubscribe_button.destroy()
2157 self.unsubscribe_button = None
2159 def update_actions(self):
2161 Called when user selected a row. Make subscribe/unsubscribe buttons
2162 sensitive appropriatelly
2164 # we have nothing to do if we don't have buttons...
2165 if self.subscribe_button is None: return
2167 model, iter_ = self.window.services_treeview.get_selection().get_selected()
2168 if not iter_ or self.subscriptions is None:
2169 # no item selected or no subscriptions info, all buttons are insensitive
2170 self.post_button.set_sensitive(False)
2171 self.subscribe_button.set_sensitive(False)
2172 self.unsubscribe_button.set_sensitive(False)
2173 else:
2174 subscribed = model.get_value(iter_, 4) # 4 = subscribed?
2175 self.post_button.set_sensitive(subscribed)
2176 self.subscribe_button.set_sensitive(not subscribed)
2177 self.unsubscribe_button.set_sensitive(subscribed)
2179 def on_post_button_clicked(self, widget):
2181 Called when 'post' button is pressed. Open window to create post
2183 model, iter_ = self.window.services_treeview.get_selection().get_selected()
2184 if iter_ is None: return
2186 groupnode = model.get_value(iter_, 1) # 1 = groupnode
2188 groups.GroupsPostWindow(self.account, self.jid, groupnode)
2190 def on_subscribe_button_clicked(self, widget):
2192 Called when 'subscribe' button is pressed. Send subscribtion request
2194 model, iter_ = self.window.services_treeview.get_selection().get_selected()
2195 if iter_ is None: return
2197 groupnode = model.get_value(iter_, 1) # 1 = groupnode
2199 gajim.connections[self.account].send_pb_subscribe(self.jid, groupnode, self._on_pep_subscribe, groupnode)
2201 def on_unsubscribe_button_clicked(self, widget):
2203 Called when 'unsubscribe' button is pressed. Send unsubscription request
2205 model, iter_ = self.window.services_treeview.get_selection().get_selected()
2206 if iter_ is None: return
2208 groupnode = model.get_value(iter_, 1) # 1 = groupnode
2210 gajim.connections[self.account].send_pb_unsubscribe(self.jid, groupnode, self._on_pep_unsubscribe, groupnode)
2212 def _on_pep_subscriptions(self, conn, request):
2214 We got the subscribed groups list stanza. Now, if we already have items
2215 on the list, we should actualize them
2217 try:
2218 subscriptions = request.getTag('pubsub').getTag('subscriptions')
2219 except Exception:
2220 return
2222 groups = set()
2223 for child in subscriptions.getTags('subscription'):
2224 groups.add(child['node'])
2226 self.subscriptions = groups
2228 # try to setup existing items in model
2229 model = self.window.services_treeview.get_model()
2230 for row in model:
2231 # 1 = group node
2232 # 3 = insensitive checkbox for subscribed
2233 # 4 = subscribed?
2234 groupnode = row[1]
2235 row[3] = False
2236 row[4] = groupnode in groups
2238 # we now know subscriptions, update button states
2239 self.update_actions()
2241 raise xmpp.NodeProcessed
2243 def _on_pep_subscribe(self, conn, request, groupnode):
2245 We have just subscribed to a node. Update UI
2247 self.subscriptions.add(groupnode)
2249 model = self.window.services_treeview.get_model()
2250 for row in model:
2251 if row[1] == groupnode: # 1 = groupnode
2252 row[4] = True
2253 break
2255 self.update_actions()
2257 raise xmpp.NodeProcessed
2259 def _on_pep_unsubscribe(self, conn, request, groupnode):
2261 We have just unsubscribed from a node. Update UI
2263 self.subscriptions.remove(groupnode)
2265 model = self.window.services_treeview.get_model()
2266 for row in model:
2267 if row[1] == groupnode: # 1 = groupnode
2268 row[4]=False
2269 break
2271 self.update_actions()
2273 raise xmpp.NodeProcessed
2275 # Fill the global agent type info dictionary
2276 _agent_type_info = _gen_agent_type_info()