lighter import
[gajim.git] / src / disco.py
blobb6679d4e3b306e3d53a44c8235e412448316265b
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 if not iter_:
1478 return
1479 if not model[iter_][0]:
1480 # We're on a category row
1481 return
1482 if model[iter_][4] != 0:
1483 # We don't have the info (yet)
1484 # It's either unknown or a transport, register button should be active
1485 if self.register_button:
1486 self.register_button.set_sensitive(True)
1487 # Guess what kind of service we're dealing with
1488 if self.browse_button:
1489 jid = model[iter_][0].decode('utf-8')
1490 type_ = gajim.get_transport_name_from_jid(jid,
1491 use_config_setting = False)
1492 if type_:
1493 identity = {'category': '_jid', 'type': type_}
1494 klass = self.cache.get_browser([identity])
1495 if klass:
1496 self.browse_button.set_sensitive(True)
1497 else:
1498 # We couldn't guess
1499 self.browse_button.set_sensitive(True)
1500 else:
1501 # Normal case, we have info
1502 AgentBrowser.update_actions(self)
1504 def _update_actions(self, jid, node, identities, features, data):
1505 AgentBrowser._update_actions(self, jid, node, identities, features, data)
1506 if self.execute_button and xmpp.NS_COMMANDS in features:
1507 self.execute_button.set_sensitive(True)
1508 if self.search_button and xmpp.NS_SEARCH in features:
1509 self.search_button.set_sensitive(True)
1510 if self.register_button and xmpp.NS_REGISTER in features:
1511 # We can register this agent
1512 registered_transports = []
1513 jid_list = gajim.contacts.get_jid_list(self.account)
1514 for jid in jid_list:
1515 contact = gajim.contacts.get_first_contact_from_jid(
1516 self.account, jid)
1517 if _('Transports') in contact.groups:
1518 registered_transports.append(jid)
1519 if jid in registered_transports:
1520 self.register_button.set_label(_('_Edit'))
1521 else:
1522 self.register_button.set_label(_('Re_gister'))
1523 self.register_button.set_sensitive(True)
1524 if self.join_button and xmpp.NS_MUC in features:
1525 self.join_button.set_sensitive(True)
1527 def _default_action(self, jid, node, identities, features, data):
1528 if AgentBrowser._default_action(self, jid, node, identities, features, data):
1529 return True
1530 if xmpp.NS_REGISTER in features:
1531 # Register if we can't browse
1532 self.on_register_button_clicked()
1533 return True
1534 return False
1536 def browse(self, force=False):
1537 self._progress = 0
1538 AgentBrowser.browse(self, force = force)
1540 def _expand_all(self):
1542 Expand all items in the treeview
1544 # GTK apparently screws up here occasionally. :/
1545 #def expand_all(*args):
1546 # self.window.services_treeview.expand_all()
1547 # self.expanding = False
1548 # return False
1549 #self.expanding = True
1550 #gobject.idle_add(expand_all)
1551 self.window.services_treeview.expand_all()
1553 def _update_progressbar(self):
1555 Update the progressbar
1557 # Refresh this every update
1558 if self._progressbar_sourceid:
1559 gobject.source_remove(self._progressbar_sourceid)
1561 fraction = 0
1562 if self._total_items:
1563 self.window.progressbar.set_text(_("Scanning %(current)d / %(total)d.."
1564 ) % {'current': self._progress, 'total': self._total_items})
1565 fraction = float(self._progress) / float(self._total_items)
1566 if self._progress >= self._total_items:
1567 # We show the progressbar for just a bit before hiding it.
1568 id_ = gobject.timeout_add_seconds(2, self._hide_progressbar_cb)
1569 self._progressbar_sourceid = id_
1570 else:
1571 self.window.progressbar.show()
1572 # Hide the progressbar if we're timing out anyways. (20 secs)
1573 id_ = gobject.timeout_add_seconds(20, self._hide_progressbar_cb)
1574 self._progressbar_sourceid = id_
1575 self.window.progressbar.set_fraction(fraction)
1577 def _hide_progressbar_cb(self, *args):
1579 Simple callback to hide the progressbar a second after we finish
1581 if self.active:
1582 self.window.progressbar.hide()
1583 return False
1585 def _friendly_category(self, category, type_=None):
1587 Get the friendly category name and priority
1589 cat = None
1590 if type_:
1591 # Try type-specific override
1592 try:
1593 cat, prio = _cat_to_descr[(category, type_)]
1594 except KeyError:
1595 pass
1596 if not cat:
1597 try:
1598 cat, prio = _cat_to_descr[category]
1599 except KeyError:
1600 cat, prio = _cat_to_descr['other']
1601 return cat, prio
1603 def _create_category(self, cat, type_=None):
1605 Creates a category row
1607 cat, prio = self._friendly_category(cat, type_)
1608 return self.model.append(None, ('', '', None, cat, prio))
1610 def _find_category(self, cat, type_=None):
1612 Looks up a category row and returns the iterator to it, or None
1614 cat = self._friendly_category(cat, type_)[0]
1615 iter_ = self.model.get_iter_root()
1616 while iter_:
1617 if self.model.get_value(iter_, 3).decode('utf-8') == cat:
1618 break
1619 iter_ = self.model.iter_next(iter_)
1620 if iter_:
1621 return iter_
1622 return None
1624 def _find_item(self, jid, node):
1625 iter_ = None
1626 cat_iter = self.model.get_iter_root()
1627 while cat_iter and not iter_:
1628 iter_ = self.model.iter_children(cat_iter)
1629 while iter_:
1630 cjid = self.model.get_value(iter_, 0).decode('utf-8')
1631 cnode = self.model.get_value(iter_, 1).decode('utf-8')
1632 if jid == cjid and node == cnode:
1633 break
1634 iter_ = self.model.iter_next(iter_)
1635 cat_iter = self.model.iter_next(cat_iter)
1636 if iter_:
1637 return iter_
1638 return None
1640 def _add_item(self, jid, node, parent_node, item, force):
1641 # Row text
1642 addr = get_agent_address(jid, node)
1643 if 'name' in item:
1644 descr = "<b>%s</b>\n%s" % (item['name'], addr)
1645 else:
1646 descr = "<b>%s</b>" % addr
1647 # Guess which kind of service this is
1648 identities = []
1649 type_ = gajim.get_transport_name_from_jid(jid,
1650 use_config_setting = False)
1651 if type_:
1652 identity = {'category': '_jid', 'type': type_}
1653 identities.append(identity)
1654 cat_args = ('_jid', type_)
1655 else:
1656 # Put it in the 'other' category for now
1657 cat_args = ('other',)
1658 # Set the pixmap for the row
1659 pix = self.cache.get_icon(identities)
1660 # Put it in the right category
1661 cat = self._find_category(*cat_args)
1662 if not cat:
1663 cat = self._create_category(*cat_args)
1664 self.model.append(cat, (jid, node, pix, descr, 1))
1665 gobject.idle_add(self._expand_all)
1666 # Grab info on the service
1667 self.cache.get_info(jid, node, self._agent_info, force=force)
1668 self._update_progressbar()
1670 def _update_item(self, iter_, jid, node, item):
1671 addr = get_agent_address(jid, node)
1672 if 'name' in item:
1673 descr = "<b>%s</b>\n%s" % (item['name'], addr)
1674 else:
1675 descr = "<b>%s</b>" % addr
1676 self.model[iter_][3] = descr
1678 def _update_info(self, iter_, jid, node, identities, features, data):
1679 addr = get_agent_address(jid, node)
1680 name = identities[0].get('name', '')
1681 if name:
1682 descr = "<b>%s</b>\n%s" % (name, addr)
1683 else:
1684 descr = "<b>%s</b>" % addr
1686 # Update progress
1687 self._progress += 1
1688 self._update_progressbar()
1690 # Search for an icon and category we can display
1691 pix = self.cache.get_icon(identities)
1692 cat, type_ = None, None
1693 for identity in identities:
1694 try:
1695 cat, type_ = identity['category'], identity['type']
1696 except KeyError:
1697 continue
1698 break
1700 # Check if we have to move categories
1701 old_cat_iter = self.model.iter_parent(iter_)
1702 old_cat = self.model.get_value(old_cat_iter, 3).decode('utf-8')
1703 if self.model.get_value(old_cat_iter, 3) == cat:
1704 # Already in the right category, just update
1705 self.model[iter_][2] = pix
1706 self.model[iter_][3] = descr
1707 self.model[iter_][4] = 0
1708 return
1709 # Not in the right category, move it.
1710 self.model.remove(iter_)
1712 # Check if the old category is empty
1713 if not self.model.iter_is_valid(old_cat_iter):
1714 old_cat_iter = self._find_category(old_cat)
1715 if not self.model.iter_children(old_cat_iter):
1716 self.model.remove(old_cat_iter)
1718 cat_iter = self._find_category(cat, type_)
1719 if not cat_iter:
1720 cat_iter = self._create_category(cat, type_)
1721 self.model.append(cat_iter, (jid, node, pix, descr, 0))
1722 self._expand_all()
1724 def _update_error(self, iter_, jid, node):
1725 self.model[iter_][4] = 2
1726 self._progress += 1
1727 self._update_progressbar()
1730 class MucBrowser(AgentBrowser):
1731 def __init__(self, *args, **kwargs):
1732 AgentBrowser.__init__(self, *args, **kwargs)
1733 self.join_button = None
1734 self.bookmark_button = None
1736 def _create_treemodel(self):
1737 # JID, node, name, users_int, users_str, description, fetched
1738 # This is rather long, I'd rather not use a data_func here though.
1739 # Users is a string, because want to be able to leave it empty.
1740 self.model = gtk.ListStore(str, str, str, int, str, str, bool)
1741 self.model.set_sort_column_id(2, gtk.SORT_ASCENDING)
1742 self.window.services_treeview.set_model(self.model)
1743 # Name column
1744 col = gtk.TreeViewColumn(_('Name'))
1745 col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1746 col.set_fixed_width(100)
1747 renderer = gtk.CellRendererText()
1748 col.pack_start(renderer)
1749 col.set_attributes(renderer, text = 2)
1750 col.set_sort_column_id(2)
1751 self.window.services_treeview.insert_column(col, -1)
1752 col.set_resizable(True)
1753 # Users column
1754 col = gtk.TreeViewColumn(_('Users'))
1755 renderer = gtk.CellRendererText()
1756 col.pack_start(renderer)
1757 col.set_attributes(renderer, text = 4)
1758 col.set_sort_column_id(3)
1759 self.window.services_treeview.insert_column(col, -1)
1760 col.set_resizable(True)
1761 # Description column
1762 col = gtk.TreeViewColumn(_('Description'))
1763 renderer = gtk.CellRendererText()
1764 col.pack_start(renderer)
1765 col.set_attributes(renderer, text = 5)
1766 col.set_sort_column_id(4)
1767 self.window.services_treeview.insert_column(col, -1)
1768 col.set_resizable(True)
1769 # Id column
1770 col = gtk.TreeViewColumn(_('Id'))
1771 renderer = gtk.CellRendererText()
1772 col.pack_start(renderer)
1773 col.set_attributes(renderer, text = 0)
1774 col.set_sort_column_id(0)
1775 self.window.services_treeview.insert_column(col, -1)
1776 col.set_resizable(True)
1777 self.window.services_treeview.set_headers_visible(True)
1778 self.window.services_treeview.set_headers_clickable(True)
1779 # Source id for idle callback used to start disco#info queries.
1780 self._fetch_source = None
1781 # Query failure counter
1782 self._broken = 0
1783 # Connect to scrollwindow scrolling
1784 self.vadj = self.window.services_scrollwin.get_property('vadjustment')
1785 self.vadj_cbid = self.vadj.connect('value-changed', self.on_scroll)
1786 # And to size changes
1787 self.size_cbid = self.window.services_scrollwin.connect(
1788 'size-allocate', self.on_scroll)
1790 def _clean_treemodel(self):
1791 if self.size_cbid:
1792 self.window.services_scrollwin.disconnect(self.size_cbid)
1793 self.size_cbid = None
1794 if self.vadj_cbid:
1795 self.vadj.disconnect(self.vadj_cbid)
1796 self.vadj_cbid = None
1797 AgentBrowser._clean_treemodel(self)
1799 def _add_actions(self):
1800 self.bookmark_button = gtk.Button(label=_('_Bookmark'), use_underline=True)
1801 self.bookmark_button.connect('clicked', self.on_bookmark_button_clicked)
1802 self.window.action_buttonbox.add(self.bookmark_button)
1803 self.bookmark_button.show_all()
1804 self.join_button = gtk.Button(label=_('_Join'), use_underline=True)
1805 self.join_button.connect('clicked', self.on_join_button_clicked)
1806 self.window.action_buttonbox.add(self.join_button)
1807 self.join_button.show_all()
1809 def _clean_actions(self):
1810 if self.bookmark_button:
1811 self.bookmark_button.destroy()
1812 self.bookmark_button = None
1813 if self.join_button:
1814 self.join_button.destroy()
1815 self.join_button = None
1817 def on_bookmark_button_clicked(self, *args):
1818 model, iter = self.window.services_treeview.get_selection().get_selected()
1819 if not iter:
1820 return
1821 name = gajim.config.get_per('accounts', self.account, 'name')
1822 room_jid = model[iter][0].decode('utf-8')
1823 bm = {
1824 'name': room_jid.split('@')[0],
1825 'jid': room_jid,
1826 'autojoin': '0',
1827 'minimize': '0',
1828 'password': '',
1829 'nick': name
1832 for bookmark in gajim.connections[self.account].bookmarks:
1833 if bookmark['jid'] == bm['jid']:
1834 dialogs.ErrorDialog(
1835 _('Bookmark already set'),
1836 _('Group Chat "%s" is already in your bookmarks.') % bm['jid'])
1837 return
1839 gajim.connections[self.account].bookmarks.append(bm)
1840 gajim.connections[self.account].store_bookmarks()
1842 gajim.interface.roster.set_actions_menu_needs_rebuild()
1844 dialogs.InformationDialog(
1845 _('Bookmark has been added successfully'),
1846 _('You can manage your bookmarks via Actions menu in your roster.'))
1848 def on_join_button_clicked(self, *args):
1850 When we want to join a conference: ask specific informations about the
1851 selected agent and close the window
1853 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1854 if not iter_:
1855 return
1856 service = model[iter_][0].decode('utf-8')
1857 if 'join_gc' not in gajim.interface.instances[self.account]:
1858 try:
1859 dialogs.JoinGroupchatWindow(self.account, service)
1860 except GajimGeneralException:
1861 pass
1862 else:
1863 gajim.interface.instances[self.account]['join_gc']._set_room_jid(
1864 service)
1865 gajim.interface.instances[self.account]['join_gc'].window.present()
1867 def update_actions(self):
1868 sens = self.window.services_treeview.get_selection().count_selected_rows()
1869 if self.bookmark_button:
1870 self.bookmark_button.set_sensitive(sens > 0)
1871 if self.join_button:
1872 self.join_button.set_sensitive(sens > 0)
1874 def default_action(self):
1875 self.on_join_button_clicked()
1877 def _start_info_query(self):
1879 Idle callback to start checking for visible rows
1881 self._fetch_source = None
1882 self._query_visible()
1883 return False
1885 def on_scroll(self, *args):
1887 Scrollwindow callback to trigger new queries on scolling
1889 # This apparently happens when inactive sometimes
1890 self._query_visible()
1892 def _query_visible(self):
1894 Query the next visible row for info
1896 if self._fetch_source:
1897 # We're already fetching
1898 return
1899 view = self.window.services_treeview
1900 if not view.flags() & gtk.REALIZED:
1901 # Prevent a silly warning, try again in a bit.
1902 self._fetch_source = gobject.timeout_add(100, self._start_info_query)
1903 return
1904 # We have to do this in a pygtk <2.8 compatible way :/
1905 #start, end = self.window.services_treeview.get_visible_range()
1906 rect = view.get_visible_rect()
1907 iter_ = end = None
1908 # Top row
1909 try:
1910 sx, sy = view.tree_to_widget_coords(rect.x, rect.y)
1911 spath = view.get_path_at_pos(sx, sy)[0]
1912 iter_ = self.model.get_iter(spath)
1913 except TypeError:
1914 self._fetch_source = None
1915 return
1916 # Bottom row
1917 # Iter compare is broke, use the path instead
1918 try:
1919 ex, ey = view.tree_to_widget_coords(rect.x + rect.height,
1920 rect.y + rect.height)
1921 end = view.get_path_at_pos(ex, ey)[0]
1922 # end is the last visible, we want to query that aswell
1923 end = (end[0] + 1,)
1924 except TypeError:
1925 # We're at the end of the model, we can leave end=None though.
1926 pass
1927 while iter_ and self.model.get_path(iter_) != end:
1928 if not self.model.get_value(iter_, 6):
1929 jid = self.model.get_value(iter_, 0).decode('utf-8')
1930 node = self.model.get_value(iter_, 1).decode('utf-8')
1931 self.cache.get_info(jid, node, self._agent_info)
1932 self._fetch_source = True
1933 return
1934 iter_ = self.model.iter_next(iter_)
1935 self._fetch_source = None
1937 def _channel_altinfo(self, jid, node, items, name = None):
1939 Callback for the alternate disco#items query. We try to atleast get the
1940 amount of users in the room if the service does not support MUC dataforms
1942 if items == 0:
1943 # The server returned an error
1944 self._broken += 1
1945 if self._broken >= 3:
1946 # Disable queries completely after 3 failures
1947 if self.size_cbid:
1948 self.window.services_scrollwin.disconnect(self.size_cbid)
1949 self.size_cbid = None
1950 if self.vadj_cbid:
1951 self.vadj.disconnect(self.vadj_cbid)
1952 self.vadj_cbid = None
1953 self._fetch_source = None
1954 return
1955 else:
1956 iter_ = self._find_item(jid, node)
1957 if iter_:
1958 if name:
1959 self.model[iter_][2] = name
1960 self.model[iter_][3] = len(items) # The number of users
1961 self.model[iter_][4] = str(len(items)) # The number of users
1962 self.model[iter_][6] = True
1963 self._fetch_source = None
1964 self._query_visible()
1966 def _add_item(self, jid, node, parent_node, item, force):
1967 self.model.append((jid, node, item.get('name', ''), -1, '', '', False))
1968 if not self._fetch_source:
1969 self._fetch_source = gobject.idle_add(self._start_info_query)
1971 def _update_info(self, iter_, jid, node, identities, features, data):
1972 name = identities[0].get('name', '')
1973 for form in data:
1974 typefield = form.getField('FORM_TYPE')
1975 if typefield and typefield.getValue() == \
1976 'http://jabber.org/protocol/muc#roominfo':
1977 # Fill model row from the form's fields
1978 users = form.getField('muc#roominfo_occupants')
1979 descr = form.getField('muc#roominfo_description')
1980 if users:
1981 self.model[iter_][3] = int(users.getValue())
1982 self.model[iter_][4] = users.getValue()
1983 if descr:
1984 self.model[iter_][5] = descr.getValue()
1985 # Only set these when we find a form with additional info
1986 # Some servers don't support forms and put extra info in
1987 # the name attribute, so we preserve it in that case.
1988 self.model[iter_][2] = name
1989 self.model[iter_][6] = True
1990 break
1991 else:
1992 # We didn't find a form, switch to alternate query mode
1993 self.cache.get_items(jid, node, self._channel_altinfo, args = (name,))
1994 return
1995 # Continue with the next
1996 self._fetch_source = None
1997 self._query_visible()
1999 def _update_error(self, iter_, jid, node):
2000 # switch to alternate query mode
2001 self.cache.get_items(jid, node, self._channel_altinfo)
2003 def PubSubBrowser(account, jid, node):
2005 Return an AgentBrowser subclass that will display service discovery for
2006 particular pubsub service. Different pubsub services may need to present
2007 different data during browsing
2009 # for now, only discussion groups are supported...
2010 # TODO: check if it has appropriate features to be such kind of service
2011 return DiscussionGroupsBrowser(account, jid, node)
2013 class DiscussionGroupsBrowser(AgentBrowser):
2015 For browsing pubsub-based discussion groups service
2018 def __init__(self, account, jid, node):
2019 AgentBrowser.__init__(self, account, jid, node)
2021 # this will become set object when we get subscriptions; None means
2022 # we don't know yet which groups are subscribed
2023 self.subscriptions = None
2025 # this will become our action widgets when we create them; None means
2026 # we don't have them yet (needed for check in callback)
2027 self.subscribe_button = None
2028 self.unsubscribe_button = None
2030 gajim.connections[account].send_pb_subscription_query(jid, self._on_pep_subscriptions)
2032 def _create_treemodel(self):
2034 Create treemodel for the window
2036 # JID, node, name (with description) - pango markup, dont have info?, subscribed?
2037 self.model = gtk.TreeStore(str, str, str, bool, bool)
2038 # sort by name
2039 self.model.set_sort_column_id(2, gtk.SORT_ASCENDING)
2040 self.window.services_treeview.set_model(self.model)
2042 # Name column
2043 # Pango markup for name and description, description printed with
2044 # <small/> font
2045 renderer = gtk.CellRendererText()
2046 col = gtk.TreeViewColumn(_('Name'))
2047 col.pack_start(renderer)
2048 col.set_attributes(renderer, markup=2)
2049 col.set_resizable(True)
2050 self.window.services_treeview.insert_column(col, -1)
2051 self.window.services_treeview.set_headers_visible(True)
2053 # Subscription state
2054 renderer = gtk.CellRendererToggle()
2055 col = gtk.TreeViewColumn(_('Subscribed'))
2056 col.pack_start(renderer)
2057 col.set_attributes(renderer, inconsistent=3, active=4)
2058 col.set_resizable(False)
2059 self.window.services_treeview.insert_column(col, -1)
2061 # Node Column
2062 renderer = gtk.CellRendererText()
2063 col = gtk.TreeViewColumn(_('Node'))
2064 col.pack_start(renderer)
2065 col.set_attributes(renderer, markup=1)
2066 col.set_resizable(True)
2067 self.window.services_treeview.insert_column(col, -1)
2069 def _add_items(self, jid, node, items, force):
2070 for item in items:
2071 jid_ = item['jid']
2072 node_ = item.get('node', '')
2073 self._total_items += 1
2074 self._add_item(jid_, node_, node, item, force)
2076 def _in_list_foreach(self, model, path, iter_, node):
2077 if model[path][1] == node:
2078 self.in_list = True
2080 def _in_list(self, node):
2081 self.in_list = False
2082 self.model.foreach(self._in_list_foreach, node)
2083 return self.in_list
2085 def _add_item(self, jid, node, parent_node, item, force):
2087 Called when we got basic information about new node from query. Show the
2088 item
2090 name = item.get('name', '')
2092 if self.subscriptions is not None:
2093 dunno = False
2094 subscribed = node in self.subscriptions
2095 else:
2096 dunno = True
2097 subscribed = False
2099 name = gobject.markup_escape_text(name)
2100 name = '<b>%s</b>' % name
2102 parent_iter = self._get_iter(parent_node)
2103 if not self._in_list(node):
2104 self.model.append(parent_iter, (jid, node, name, dunno, subscribed))
2105 self.cache.get_items(jid, node, self._add_items, force = force,
2106 args = (force,))
2108 def _get_child_iter(self, parent_iter, node):
2109 child_iter = self.model.iter_children(parent_iter)
2110 while child_iter:
2111 if self.model[child_iter][1] == node:
2112 return child_iter
2113 child_iter = self.model.iter_next(child_iter)
2114 return None
2116 def _get_iter(self, node):
2117 ''' Look for an iter with the given node '''
2118 self.found_iter = None
2119 def is_node(model, path, iter, node):
2120 if model[iter][1] == node:
2121 self.found_iter = iter
2122 return True
2123 self.model.foreach(is_node, node)
2124 return self.found_iter
2126 def _add_actions(self):
2127 self.post_button = gtk.Button(label=_('New post'), use_underline=True)
2128 self.post_button.set_sensitive(False)
2129 self.post_button.connect('clicked', self.on_post_button_clicked)
2130 self.window.action_buttonbox.add(self.post_button)
2131 self.post_button.show_all()
2133 self.subscribe_button = gtk.Button(label=_('_Subscribe'), use_underline=True)
2134 self.subscribe_button.set_sensitive(False)
2135 self.subscribe_button.connect('clicked', self.on_subscribe_button_clicked)
2136 self.window.action_buttonbox.add(self.subscribe_button)
2137 self.subscribe_button.show_all()
2139 self.unsubscribe_button = gtk.Button(label=_('_Unsubscribe'), use_underline=True)
2140 self.unsubscribe_button.set_sensitive(False)
2141 self.unsubscribe_button.connect('clicked', self.on_unsubscribe_button_clicked)
2142 self.window.action_buttonbox.add(self.unsubscribe_button)
2143 self.unsubscribe_button.show_all()
2145 def _clean_actions(self):
2146 if self.post_button is not None:
2147 self.post_button.destroy()
2148 self.post_button = None
2150 if self.subscribe_button is not None:
2151 self.subscribe_button.destroy()
2152 self.subscribe_button = None
2154 if self.unsubscribe_button is not None:
2155 self.unsubscribe_button.destroy()
2156 self.unsubscribe_button = None
2158 def update_actions(self):
2160 Called when user selected a row. Make subscribe/unsubscribe buttons
2161 sensitive appropriatelly
2163 # we have nothing to do if we don't have buttons...
2164 if self.subscribe_button is None: return
2166 model, iter_ = self.window.services_treeview.get_selection().get_selected()
2167 if not iter_ or self.subscriptions is None:
2168 # no item selected or no subscriptions info, all buttons are insensitive
2169 self.post_button.set_sensitive(False)
2170 self.subscribe_button.set_sensitive(False)
2171 self.unsubscribe_button.set_sensitive(False)
2172 else:
2173 subscribed = model.get_value(iter_, 4) # 4 = subscribed?
2174 self.post_button.set_sensitive(subscribed)
2175 self.subscribe_button.set_sensitive(not subscribed)
2176 self.unsubscribe_button.set_sensitive(subscribed)
2178 def on_post_button_clicked(self, widget):
2180 Called when 'post' button is pressed. Open window to create post
2182 model, iter_ = self.window.services_treeview.get_selection().get_selected()
2183 if iter_ is None: return
2185 groupnode = model.get_value(iter_, 1) # 1 = groupnode
2187 groups.GroupsPostWindow(self.account, self.jid, groupnode)
2189 def on_subscribe_button_clicked(self, widget):
2191 Called when 'subscribe' button is pressed. Send subscribtion request
2193 model, iter_ = self.window.services_treeview.get_selection().get_selected()
2194 if iter_ is None: return
2196 groupnode = model.get_value(iter_, 1) # 1 = groupnode
2198 gajim.connections[self.account].send_pb_subscribe(self.jid, groupnode, self._on_pep_subscribe, groupnode)
2200 def on_unsubscribe_button_clicked(self, widget):
2202 Called when 'unsubscribe' button is pressed. Send unsubscription request
2204 model, iter_ = self.window.services_treeview.get_selection().get_selected()
2205 if iter_ is None: return
2207 groupnode = model.get_value(iter_, 1) # 1 = groupnode
2209 gajim.connections[self.account].send_pb_unsubscribe(self.jid, groupnode, self._on_pep_unsubscribe, groupnode)
2211 def _on_pep_subscriptions(self, conn, request):
2213 We got the subscribed groups list stanza. Now, if we already have items
2214 on the list, we should actualize them
2216 try:
2217 subscriptions = request.getTag('pubsub').getTag('subscriptions')
2218 except Exception:
2219 return
2221 groups = set()
2222 for child in subscriptions.getTags('subscription'):
2223 groups.add(child['node'])
2225 self.subscriptions = groups
2227 # try to setup existing items in model
2228 model = self.window.services_treeview.get_model()
2229 for row in model:
2230 # 1 = group node
2231 # 3 = insensitive checkbox for subscribed
2232 # 4 = subscribed?
2233 groupnode = row[1]
2234 row[3] = False
2235 row[4] = groupnode in groups
2237 # we now know subscriptions, update button states
2238 self.update_actions()
2240 raise xmpp.NodeProcessed
2242 def _on_pep_subscribe(self, conn, request, groupnode):
2244 We have just subscribed to a node. Update UI
2246 self.subscriptions.add(groupnode)
2248 model = self.window.services_treeview.get_model()
2249 for row in model:
2250 if row[1] == groupnode: # 1 = groupnode
2251 row[4] = True
2252 break
2254 self.update_actions()
2256 raise xmpp.NodeProcessed
2258 def _on_pep_unsubscribe(self, conn, request, groupnode):
2260 We have just unsubscribed from a node. Update UI
2262 self.subscriptions.remove(groupnode)
2264 model = self.window.services_treeview.get_model()
2265 for row in model:
2266 if row[1] == groupnode: # 1 = groupnode
2267 row[4]=False
2268 break
2270 self.update_actions()
2272 raise xmpp.NodeProcessed
2274 # Fill the global agent type info dictionary
2275 _agent_type_info = _gen_agent_type_info()