fix http message parsing, it may contain \n\n!
[gajim.git] / src / disco.py
blob0d3621f1396ac22ee4d3aa277f908969125dc898
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-2008 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
66 # Dictionary mapping category, type pairs to browser class, image pairs.
67 # This is a function, so we can call it after the classes are declared.
68 # For the browser class, None means that the service will only be browsable
69 # when it advertises disco as it's feature, False means it's never browsable.
70 def _gen_agent_type_info():
71 return {
72 # Defaults
73 (0, 0): (None, None),
75 # Jabber server
76 ('server', 'im'): (ToplevelAgentBrowser, 'jabber.png'),
77 ('services', 'jabber'): (ToplevelAgentBrowser, 'jabber.png'),
78 ('hierarchy', 'branch'): (AgentBrowser, 'jabber.png'),
80 # Services
81 ('conference', 'text'): (MucBrowser, 'conference.png'),
82 ('headline', 'rss'): (AgentBrowser, 'rss.png'),
83 ('headline', 'weather'): (False, 'weather.png'),
84 ('gateway', 'weather'): (False, 'weather.png'),
85 ('_jid', 'weather'): (False, 'weather.png'),
86 ('gateway', 'sip'): (False, 'sip.png'),
87 ('directory', 'user'): (None, 'jud.png'),
88 ('pubsub', 'generic'): (PubSubBrowser, 'pubsub.png'),
89 ('pubsub', 'service'): (PubSubBrowser, 'pubsub.png'),
90 ('proxy', 'bytestreams'): (None, 'bytestreams.png'), # Socks5 FT proxy
91 ('headline', 'newmail'): (ToplevelAgentBrowser, 'mail.png'),
93 # Transports
94 ('conference', 'irc'): (ToplevelAgentBrowser, 'irc.png'),
95 ('_jid', 'irc'): (False, 'irc.png'),
96 ('gateway', 'aim'): (False, 'aim.png'),
97 ('_jid', 'aim'): (False, 'aim.png'),
98 ('gateway', 'gadu-gadu'): (False, 'gadu-gadu.png'),
99 ('_jid', 'gadugadu'): (False, 'gadu-gadu.png'),
100 ('gateway', 'http-ws'): (False, 'http-ws.png'),
101 ('gateway', 'icq'): (False, 'icq.png'),
102 ('_jid', 'icq'): (False, 'icq.png'),
103 ('gateway', 'msn'): (False, 'msn.png'),
104 ('_jid', 'msn'): (False, 'msn.png'),
105 ('gateway', 'sms'): (False, 'sms.png'),
106 ('_jid', 'sms'): (False, 'sms.png'),
107 ('gateway', 'smtp'): (False, 'mail.png'),
108 ('gateway', 'yahoo'): (False, 'yahoo.png'),
109 ('_jid', 'yahoo'): (False, 'yahoo.png'),
110 ('gateway', 'mrim'): (False, 'mrim.png'),
111 ('_jid', 'mrim'): (False, 'mrim.png'),
112 ('gateway', 'facebook'): (False, 'facebook.png'),
113 ('_jid', 'facebook'): (False, 'facebook.png'),
116 # Category type to "human-readable" description string, and sort priority
117 _cat_to_descr = {
118 'other': (_('Others'), 2),
119 'gateway': (_('Transports'), 0),
120 '_jid': (_('Transports'), 0),
121 #conference is a category for listing mostly groupchats in service discovery
122 'conference': (_('Conference'), 1),
126 class CacheDictionary:
127 '''A dictionary that keeps items around for only a specific time.
128 Lifetime is in minutes. Getrefresh specifies whether to refresh when
129 an item is merely accessed instead of set aswell.'''
130 def __init__(self, lifetime, getrefresh = True):
131 self.lifetime = lifetime * 1000 * 60
132 self.getrefresh = getrefresh
133 self.cache = {}
135 class CacheItem:
136 '''An object to store cache items and their timeouts.'''
137 def __init__(self, value):
138 self.value = value
139 self.source = None
141 def __call__(self):
142 return self.value
144 def cleanup(self):
145 for key in self.cache.keys():
146 item = self.cache[key]
147 if item.source:
148 gobject.source_remove(item.source)
149 del self.cache[key]
151 def _expire_timeout(self, key):
152 '''The timeout has expired, remove the object.'''
153 if key in self.cache:
154 del self.cache[key]
155 return False
157 def _refresh_timeout(self, key):
158 '''The object was accessed, refresh the timeout.'''
159 item = self.cache[key]
160 if item.source:
161 gobject.source_remove(item.source)
162 if self.lifetime:
163 source = gobject.timeout_add_seconds(self.lifetime/1000, self._expire_timeout, key)
164 item.source = source
166 def __getitem__(self, key):
167 item = self.cache[key]
168 if self.getrefresh:
169 self._refresh_timeout(key)
170 return item()
172 def __setitem__(self, key, value):
173 item = self.CacheItem(value)
174 self.cache[key] = item
175 self._refresh_timeout(key)
177 def __delitem__(self, key):
178 item = self.cache[key]
179 if item.source:
180 gobject.source_remove(item.source)
181 del self.cache[key]
183 def __contains__(self, key):
184 return key in self.cache
185 has_key = __contains__
187 _icon_cache = CacheDictionary(15)
189 def get_agent_address(jid, node = None):
190 '''Returns an agent's address for displaying in the GUI.'''
191 if node:
192 return '%s@%s' % (node, str(jid))
193 else:
194 return str(jid)
196 class Closure(object):
197 '''A weak reference to a callback with arguments as an object.
199 Weak references to methods immediatly die, even if the object is still
200 alive. Besides a handy way to store a callback, this provides a workaround
201 that keeps a reference to the object instead.
203 Userargs and removeargs must be tuples.'''
204 def __init__(self, cb, userargs = (), remove = None, removeargs = ()):
205 self.userargs = userargs
206 self.remove = remove
207 self.removeargs = removeargs
208 if isinstance(cb, types.MethodType):
209 self.meth_self = weakref.ref(cb.im_self, self._remove)
210 self.meth_name = cb.func_name
211 elif callable(cb):
212 self.meth_self = None
213 self.cb = weakref.ref(cb, self._remove)
214 else:
215 raise TypeError('Object is not callable')
217 def _remove(self, ref):
218 if self.remove:
219 self.remove(self, *self.removeargs)
221 def __call__(self, *args, **kwargs):
222 if self.meth_self:
223 obj = self.meth_self()
224 cb = getattr(obj, self.meth_name)
225 else:
226 cb = self.cb()
227 args = args + self.userargs
228 return cb(*args, **kwargs)
231 class ServicesCache:
232 '''Class that caches our query results. Each connection will have it's own
233 ServiceCache instance.'''
234 def __init__(self, account):
235 self.account = account
236 self._items = CacheDictionary(0, getrefresh = False)
237 self._info = CacheDictionary(0, getrefresh = False)
238 self._subscriptions = CacheDictionary(5, getrefresh=False)
239 self._cbs = {}
241 def cleanup(self):
242 self._items.cleanup()
243 self._info.cleanup()
245 def _clean_closure(self, cb, type_, addr):
246 # A closure died, clean up
247 cbkey = (type_, addr)
248 try:
249 self._cbs[cbkey].remove(cb)
250 except KeyError:
251 return
252 except ValueError:
253 return
254 # Clean an empty list
255 if not self._cbs[cbkey]:
256 del self._cbs[cbkey]
258 def get_icon(self, identities = []):
259 '''Return the icon for an agent.'''
260 # Grab the first identity with an icon
261 for identity in identities:
262 try:
263 cat, type_ = identity['category'], identity['type']
264 info = _agent_type_info[(cat, type_)]
265 except KeyError:
266 continue
267 filename = info[1]
268 if filename:
269 break
270 else:
271 # Loop fell through, default to unknown
272 info = _agent_type_info[(0, 0)]
273 filename = info[1]
274 if not filename: # we don't have an image to show for this type
275 filename = 'jabber.png'
276 # Use the cache if possible
277 if filename in _icon_cache:
278 return _icon_cache[filename]
279 # Or load it
280 filepath = os.path.join(gajim.DATA_DIR, 'pixmaps', 'agents', filename)
281 pix = gtk.gdk.pixbuf_new_from_file(filepath)
282 # Store in cache
283 _icon_cache[filename] = pix
284 return pix
286 def get_browser(self, identities=[], features=[]):
287 '''Return the browser class for an agent.'''
288 # First pass, we try to find a ToplevelAgentBrowser
289 for identity in identities:
290 try:
291 cat, type_ = identity['category'], identity['type']
292 info = _agent_type_info[(cat, type_)]
293 except KeyError:
294 continue
295 browser = info[0]
296 if browser and browser == ToplevelAgentBrowser:
297 return browser
299 # second pass, we haven't found a ToplevelAgentBrowser
300 for identity in identities:
301 try:
302 cat, type_ = identity['category'], identity['type']
303 info = _agent_type_info[(cat, type_)]
304 except KeyError:
305 continue
306 browser = info[0]
307 if browser:
308 return browser
309 # NS_BROWSE is deprecated, but we check for it anyways.
310 # Some services list it in features and respond to
311 # NS_DISCO_ITEMS anyways.
312 # Allow browsing for unknown types aswell.
313 if (not features and not identities) or \
314 xmpp.NS_DISCO_ITEMS in features or xmpp.NS_BROWSE in features:
315 return ToplevelAgentBrowser
316 return None
318 def get_info(self, jid, node, cb, force = False, nofetch = False, args = ()):
319 '''Get info for an agent.'''
320 addr = get_agent_address(jid, node)
321 # Check the cache
322 if addr in self._info:
323 args = self._info[addr] + args
324 cb(jid, node, *args)
325 return
326 if nofetch:
327 return
329 # Create a closure object
330 cbkey = ('info', addr)
331 cb = Closure(cb, userargs = args, remove = self._clean_closure,
332 removeargs = cbkey)
333 # Are we already fetching this?
334 if cbkey in self._cbs:
335 self._cbs[cbkey].append(cb)
336 else:
337 self._cbs[cbkey] = [cb]
338 gajim.connections[self.account].discoverInfo(jid, node)
340 def get_items(self, jid, node, cb, force = False, nofetch = False, args = ()):
341 '''Get a list of items in an agent.'''
342 addr = get_agent_address(jid, node)
343 # Check the cache
344 if addr in self._items:
345 args = (self._items[addr],) + args
346 cb(jid, node, *args)
347 return
348 if nofetch:
349 return
351 # Create a closure object
352 cbkey = ('items', addr)
353 cb = Closure(cb, userargs = args, remove = self._clean_closure,
354 removeargs = cbkey)
355 # Are we already fetching this?
356 if cbkey in self._cbs:
357 self._cbs[cbkey].append(cb)
358 else:
359 self._cbs[cbkey] = [cb]
360 gajim.connections[self.account].discoverItems(jid, node)
362 def agent_info(self, jid, node, identities, features, data):
363 '''Callback for when we receive an agent's info.'''
364 addr = get_agent_address(jid, node)
366 # Store in cache
367 self._info[addr] = (identities, features, data)
369 # Call callbacks
370 cbkey = ('info', addr)
371 if cbkey in self._cbs:
372 for cb in self._cbs[cbkey]:
373 cb(jid, node, identities, features, data)
374 # clean_closure may have beaten us to it
375 if cbkey in self._cbs:
376 del self._cbs[cbkey]
378 def agent_items(self, jid, node, items):
379 '''Callback for when we receive an agent's items.'''
380 addr = get_agent_address(jid, node)
382 # Store in cache
383 self._items[addr] = items
385 # Call callbacks
386 cbkey = ('items', addr)
387 if cbkey in self._cbs:
388 for cb in self._cbs[cbkey]:
389 cb(jid, node, items)
390 # clean_closure may have beaten us to it
391 if cbkey in self._cbs:
392 del self._cbs[cbkey]
394 def agent_info_error(self, jid):
395 '''Callback for when a query fails. (even after the browse and agents
396 namespaces)'''
397 addr = get_agent_address(jid)
399 # Call callbacks
400 cbkey = ('info', addr)
401 if cbkey in self._cbs:
402 for cb in self._cbs[cbkey]:
403 cb(jid, '', 0, 0, 0)
404 # clean_closure may have beaten us to it
405 if cbkey in self._cbs:
406 del self._cbs[cbkey]
408 def agent_items_error(self, jid):
409 '''Callback for when a query fails. (even after the browse and agents
410 namespaces)'''
411 addr = get_agent_address(jid)
413 # Call callbacks
414 cbkey = ('items', addr)
415 if cbkey in self._cbs:
416 for cb in self._cbs[cbkey]:
417 cb(jid, '', 0)
418 # clean_closure may have beaten us to it
419 if cbkey in self._cbs:
420 del self._cbs[cbkey]
422 # object is needed so that @property works
423 class ServiceDiscoveryWindow(object):
424 '''Class that represents the Services Discovery window.'''
425 def __init__(self, account, jid = '', node = '',
426 address_entry = False, parent = None):
427 self.account = account
428 self.parent = parent
429 if not jid:
430 jid = gajim.config.get_per('accounts', account, 'hostname')
431 node = ''
433 self.jid = None
434 self.browser = None
435 self.children = []
436 self.dying = False
437 self.node = None
439 # Check connection
440 if gajim.connections[account].connected < 2:
441 dialogs.ErrorDialog(_('You are not connected to the server'),
442 _('Without a connection, you can not browse available services'))
443 raise RuntimeError, 'You must be connected to browse services'
445 # Get a ServicesCache object.
446 try:
447 self.cache = gajim.connections[account].services_cache
448 except AttributeError:
449 self.cache = ServicesCache(account)
450 gajim.connections[account].services_cache = self.cache
452 self.xml = gtkgui_helpers.get_glade('service_discovery_window.glade')
453 self.window = self.xml.get_widget('service_discovery_window')
454 self.services_treeview = self.xml.get_widget('services_treeview')
455 self.model = None
456 # This is more reliable than the cursor-changed signal.
457 selection = self.services_treeview.get_selection()
458 selection.connect_after('changed',
459 self.on_services_treeview_selection_changed)
460 self.services_scrollwin = self.xml.get_widget('services_scrollwin')
461 self.progressbar = self.xml.get_widget('services_progressbar')
462 self.banner = self.xml.get_widget('banner_agent_label')
463 self.banner_icon = self.xml.get_widget('banner_agent_icon')
464 self.banner_eventbox = self.xml.get_widget('banner_agent_eventbox')
465 self.style_event_id = 0
466 self.banner.realize()
467 self.paint_banner()
468 self.action_buttonbox = self.xml.get_widget('action_buttonbox')
470 # Address combobox
471 self.address_comboboxentry = None
472 address_table = self.xml.get_widget('address_table')
473 if address_entry:
474 self.address_comboboxentry = self.xml.get_widget(
475 'address_comboboxentry')
476 self.address_comboboxentry_entry = self.address_comboboxentry.child
477 self.address_comboboxentry_entry.set_activates_default(True)
479 liststore = gtk.ListStore(str)
480 self.address_comboboxentry.set_model(liststore)
481 self.latest_addresses = gajim.config.get(
482 'latest_disco_addresses').split()
483 if jid in self.latest_addresses:
484 self.latest_addresses.remove(jid)
485 self.latest_addresses.insert(0, jid)
486 if len(self.latest_addresses) > 10:
487 self.latest_addresses = self.latest_addresses[0:10]
488 for j in self.latest_addresses:
489 self.address_comboboxentry.append_text(j)
490 self.address_comboboxentry.child.set_text(jid)
491 else:
492 # Don't show it at all if we didn't ask for it
493 address_table.set_no_show_all(True)
494 address_table.hide()
496 self._initial_state()
497 self.xml.signal_autoconnect(self)
498 self.travel(jid, node)
499 self.window.show_all()
501 @property
502 def _get_account(self):
503 return self.account
505 @property
506 def _set_account(self, value):
507 self.account = value
508 self.cache.account = value
509 if self.browser:
510 self.browser.account = value
512 def _initial_state(self):
513 '''Set some initial state on the window. Separated in a method because
514 it's handy to use within browser's cleanup method.'''
515 self.progressbar.hide()
516 title_text = _('Service Discovery using account %s') % self.account
517 self.window.set_title(title_text)
518 self._set_window_banner_text(_('Service Discovery'))
519 self.banner_icon.clear()
520 self.banner_icon.hide() # Just clearing it doesn't work
522 def _set_window_banner_text(self, text, text_after = None):
523 theme = gajim.config.get('roster_theme')
524 bannerfont = gajim.config.get_per('themes', theme, 'bannerfont')
525 bannerfontattrs = gajim.config.get_per('themes', theme,
526 'bannerfontattrs')
528 if bannerfont:
529 font = pango.FontDescription(bannerfont)
530 else:
531 font = pango.FontDescription('Normal')
532 if bannerfontattrs:
533 # B is attribute set by default
534 if 'B' in bannerfontattrs:
535 font.set_weight(pango.WEIGHT_HEAVY)
536 if 'I' in bannerfontattrs:
537 font.set_style(pango.STYLE_ITALIC)
539 font_attrs = 'font_desc="%s"' % font.to_string()
540 font_size = font.get_size()
542 # in case there is no font specified we use x-large font size
543 if font_size == 0:
544 font_attrs = '%s size="large"' % font_attrs
545 markup = '<span %s>%s</span>' % (font_attrs, text)
546 if text_after:
547 font.set_weight(pango.WEIGHT_NORMAL)
548 markup = '%s\n<span font_desc="%s" size="small">%s</span>' % \
549 (markup, font.to_string(), text_after)
550 self.banner.set_markup(markup)
552 def paint_banner(self):
553 '''Repaint the banner with theme color'''
554 theme = gajim.config.get('roster_theme')
555 bgcolor = gajim.config.get_per('themes', theme, 'bannerbgcolor')
556 textcolor = gajim.config.get_per('themes', theme, 'bannertextcolor')
557 self.disconnect_style_event()
558 if bgcolor:
559 color = gtk.gdk.color_parse(bgcolor)
560 self.banner_eventbox.modify_bg(gtk.STATE_NORMAL, color)
561 default_bg = False
562 else:
563 default_bg = True
565 if textcolor:
566 color = gtk.gdk.color_parse(textcolor)
567 self.banner.modify_fg(gtk.STATE_NORMAL, color)
568 default_fg = False
569 else:
570 default_fg = True
571 if default_fg or default_bg:
572 self._on_style_set_event(self.banner, None, default_fg, default_bg)
573 if self.browser:
574 self.browser.update_theme()
576 def disconnect_style_event(self):
577 if self.style_event_id:
578 self.banner.disconnect(self.style_event_id)
579 self.style_event_id = 0
581 def connect_style_event(self, set_fg = False, set_bg = False):
582 self.disconnect_style_event()
583 self.style_event_id = self.banner.connect('style-set',
584 self._on_style_set_event, set_fg, set_bg)
586 def _on_style_set_event(self, widget, style, *opts):
587 ''' set style of widget from style class *.Frame.Eventbox
588 opts[0] == True -> set fg color
589 opts[1] == True -> set bg color '''
591 self.disconnect_style_event()
592 if opts[1]:
593 bg_color = widget.style.bg[gtk.STATE_SELECTED]
594 self.banner_eventbox.modify_bg(gtk.STATE_NORMAL, bg_color)
595 if opts[0]:
596 fg_color = widget.style.fg[gtk.STATE_SELECTED]
597 self.banner.modify_fg(gtk.STATE_NORMAL, fg_color)
598 self.banner.ensure_style()
599 self.connect_style_event(opts[0], opts[1])
601 def destroy(self, chain = False):
602 '''Close the browser. This can optionally close its children and
603 propagate to the parent. This should happen on actions like register,
604 or join to kill off the entire browser chain.'''
605 if self.dying:
606 return
607 self.dying = True
609 # self.browser._get_agent_address() would break when no browser.
610 addr = get_agent_address(self.jid, self.node)
611 if addr in gajim.interface.instances[self.account]['disco']:
612 del gajim.interface.instances[self.account]['disco'][addr]
614 if self.browser:
615 self.window.hide()
616 self.browser.cleanup()
617 self.browser = None
618 self.window.destroy()
620 for child in self.children[:]:
621 child.parent = None
622 if chain:
623 child.destroy(chain = chain)
624 self.children.remove(child)
625 if self.parent:
626 if self in self.parent.children:
627 self.parent.children.remove(self)
628 if chain and not self.parent.children:
629 self.parent.destroy(chain = chain)
630 self.parent = None
631 else:
632 self.cache.cleanup()
634 def travel(self, jid, node):
635 '''Travel to an agent within the current services window.'''
636 if self.browser:
637 self.browser.cleanup()
638 self.browser = None
639 # Update the window list
640 if self.jid:
641 old_addr = get_agent_address(self.jid, self.node)
642 if old_addr in gajim.interface.instances[self.account]['disco']:
643 del gajim.interface.instances[self.account]['disco'][old_addr]
644 addr = get_agent_address(jid, node)
645 gajim.interface.instances[self.account]['disco'][addr] = self
646 # We need to store these, self.browser is not always available.
647 self.jid = jid
648 self.node = node
649 self.cache.get_info(jid, node, self._travel)
651 def _travel(self, jid, node, identities, features, data):
652 '''Continuation of travel.'''
653 if self.dying or jid != self.jid or node != self.node:
654 return
655 if not identities:
656 if not self.address_comboboxentry:
657 # We can't travel anywhere else.
658 self.destroy()
659 dialogs.ErrorDialog(_('The service could not be found'),
660 _('There is no service at the address you entered, or it is not responding. Check the address and try again.'))
661 return
662 klass = self.cache.get_browser(identities, features)
663 if not klass:
664 dialogs.ErrorDialog(_('The service is not browsable'),
665 _('This type of service does not contain any items to browse.'))
666 return
667 elif klass is None:
668 klass = AgentBrowser
669 self.browser = klass(self.account, jid, node)
670 self.browser.prepare_window(self)
671 self.browser.browse()
673 def open(self, jid, node):
674 '''Open an agent. By default, this happens in a new window.'''
675 try:
676 win = gajim.interface.instances[self.account]['disco']\
677 [get_agent_address(jid, node)]
678 win.window.present()
679 return
680 except KeyError:
681 pass
682 try:
683 win = ServiceDiscoveryWindow(self.account, jid, node, parent=self)
684 except RuntimeError:
685 # Disconnected, perhaps
686 return
687 self.children.append(win)
689 def on_service_discovery_window_destroy(self, widget):
690 self.destroy()
692 def on_close_button_clicked(self, widget):
693 self.destroy()
695 def on_address_comboboxentry_changed(self, widget):
696 if self.address_comboboxentry.get_active() != -1:
697 # user selected one of the entries so do auto-visit
698 jid = self.address_comboboxentry.child.get_text().decode('utf-8')
699 try:
700 jid = helpers.parse_jid(jid)
701 except helpers.InvalidFormat, s:
702 pritext = _('Invalid Server Name')
703 dialogs.ErrorDialog(pritext, str(s))
704 return
705 self.travel(jid, '')
707 def on_go_button_clicked(self, widget):
708 jid = self.address_comboboxentry.child.get_text().decode('utf-8')
709 try:
710 jid = helpers.parse_jid(jid)
711 except helpers.InvalidFormat, s:
712 pritext = _('Invalid Server Name')
713 dialogs.ErrorDialog(pritext, str(s))
714 return
715 if jid == self.jid: # jid has not changed
716 return
717 if jid in self.latest_addresses:
718 self.latest_addresses.remove(jid)
719 self.latest_addresses.insert(0, jid)
720 if len(self.latest_addresses) > 10:
721 self.latest_addresses = self.latest_addresses[0:10]
722 self.address_comboboxentry.get_model().clear()
723 for j in self.latest_addresses:
724 self.address_comboboxentry.append_text(j)
725 gajim.config.set('latest_disco_addresses',
726 ' '.join(self.latest_addresses))
727 gajim.interface.save_config()
728 self.travel(jid, '')
730 def on_services_treeview_row_activated(self, widget, path, col = 0):
731 if self.browser:
732 self.browser.default_action()
734 def on_services_treeview_selection_changed(self, widget):
735 if self.browser:
736 self.browser.update_actions()
739 class AgentBrowser:
740 '''Class that deals with browsing agents and appearance of the browser
741 window. This class and subclasses should basically be treated as "part"
742 of the ServiceDiscoveryWindow class, but had to be separated because this part
743 is dynamic.'''
744 def __init__(self, account, jid, node):
745 self.account = account
746 self.jid = jid
747 self.node = node
748 self._total_items = 0
749 self.browse_button = None
750 # This is for some timeout callbacks
751 self.active = False
753 def _get_agent_address(self):
754 '''Returns the agent's address for displaying in the GUI.'''
755 return get_agent_address(self.jid, self.node)
757 def _set_initial_title(self):
758 '''Set the initial window title based on agent address.'''
759 self.window.window.set_title(_('Browsing %(address)s using account '
760 '%(account)s') % {'address': self._get_agent_address(),
761 'account': self.account})
762 self.window._set_window_banner_text(self._get_agent_address())
764 def _create_treemodel(self):
765 '''Create the treemodel for the services treeview. When subclassing,
766 note that the first two columns should ALWAYS be of type string and
767 contain the JID and node of the item respectively.'''
768 # JID, node, name, address
769 self.model = gtk.ListStore(str, str, str, str)
770 self.model.set_sort_column_id(3, gtk.SORT_ASCENDING)
771 self.window.services_treeview.set_model(self.model)
772 # Name column
773 col = gtk.TreeViewColumn(_('Name'))
774 renderer = gtk.CellRendererText()
775 col.pack_start(renderer)
776 col.set_attributes(renderer, text = 2)
777 self.window.services_treeview.insert_column(col, -1)
778 col.set_resizable(True)
779 # Address column
780 col = gtk.TreeViewColumn(_('JID'))
781 renderer = gtk.CellRendererText()
782 col.pack_start(renderer)
783 col.set_attributes(renderer, text = 3)
784 self.window.services_treeview.insert_column(col, -1)
785 col.set_resizable(True)
786 self.window.services_treeview.set_headers_visible(True)
788 def _clean_treemodel(self):
789 self.model.clear()
790 for col in self.window.services_treeview.get_columns():
791 self.window.services_treeview.remove_column(col)
792 self.window.services_treeview.set_headers_visible(False)
794 def _add_actions(self):
795 '''Add the action buttons to the buttonbox for actions the browser can
796 perform.'''
797 self.browse_button = gtk.Button()
798 image = gtk.image_new_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_BUTTON)
799 label = gtk.Label(_('_Browse'))
800 label.set_use_underline(True)
801 hbox = gtk.HBox()
802 hbox.pack_start(image, False, True, 6)
803 hbox.pack_end(label, True, True)
804 self.browse_button.add(hbox)
805 self.browse_button.connect('clicked', self.on_browse_button_clicked)
806 self.window.action_buttonbox.add(self.browse_button)
807 self.browse_button.show_all()
809 def _clean_actions(self):
810 '''Remove the action buttons specific to this browser.'''
811 if self.browse_button:
812 self.browse_button.destroy()
813 self.browse_button = None
815 def _set_title(self, jid, node, identities, features, data):
816 '''Set the window title based on agent info.'''
817 # Set the banner and window title
818 if 'name' in identities[0]:
819 name = identities[0]['name']
820 self.window._set_window_banner_text(self._get_agent_address(), name)
822 # Add an icon to the banner.
823 pix = self.cache.get_icon(identities)
824 self.window.banner_icon.set_from_pixbuf(pix)
825 self.window.banner_icon.show()
827 def _clean_title(self):
828 # Everything done here is done in window._initial_state
829 # This is for subclasses.
830 pass
832 def prepare_window(self, window):
833 '''Prepare the service discovery window. Called when a browser is hooked
834 up with a ServiceDiscoveryWindow instance.'''
835 self.window = window
836 self.cache = window.cache
838 self._set_initial_title()
839 self._create_treemodel()
840 self._add_actions()
842 # This is a hack. The buttonbox apparently doesn't care about pack_start
843 # or pack_end, so we repack the close button here to make sure it's last
844 close_button = self.window.xml.get_widget('close_button')
845 self.window.action_buttonbox.remove(close_button)
846 self.window.action_buttonbox.pack_end(close_button)
847 close_button.show_all()
849 self.update_actions()
851 self.active = True
852 self.cache.get_info(self.jid, self.node, self._set_title)
854 def cleanup(self):
855 '''Cleanup when the window intends to switch browsers.'''
856 self.active = False
858 self._clean_actions()
859 self._clean_treemodel()
860 self._clean_title()
862 self.window._initial_state()
864 def update_theme(self):
865 '''Called when the default theme is changed.'''
866 pass
868 def on_browse_button_clicked(self, widget = None):
869 '''When we want to browse an agent:
870 Open a new services window with a browser for the agent type.'''
871 model, iter_ = self.window.services_treeview.get_selection().get_selected()
872 if not iter_:
873 return
874 jid = model[iter_][0].decode('utf-8')
875 if jid:
876 node = model[iter_][1].decode('utf-8')
877 self.window.open(jid, node)
879 def update_actions(self):
880 '''When we select a row:
881 activate action buttons based on the agent's info.'''
882 if self.browse_button:
883 self.browse_button.set_sensitive(False)
884 model, iter_ = self.window.services_treeview.get_selection().get_selected()
885 if not iter_:
886 return
887 jid = model[iter_][0].decode('utf-8')
888 node = model[iter_][1].decode('utf-8')
889 if jid:
890 self.cache.get_info(jid, node, self._update_actions, nofetch = True)
892 def _update_actions(self, jid, node, identities, features, data):
893 '''Continuation of update_actions.'''
894 if not identities or not self.browse_button:
895 return
896 klass = self.cache.get_browser(identities, features)
897 if klass:
898 self.browse_button.set_sensitive(True)
900 def default_action(self):
901 '''When we double-click a row:
902 perform the default action on the selected item.'''
903 model, iter_ = self.window.services_treeview.get_selection().get_selected()
904 if not iter_:
905 return
906 jid = model[iter_][0].decode('utf-8')
907 node = model[iter_][1].decode('utf-8')
908 if jid:
909 self.cache.get_info(jid, node, self._default_action, nofetch = True)
911 def _default_action(self, jid, node, identities, features, data):
912 '''Continuation of default_action.'''
913 if self.cache.get_browser(identities, features):
914 # Browse if we can
915 self.on_browse_button_clicked()
916 return True
917 return False
919 def browse(self, force = False):
920 '''Fill the treeview with agents, fetching the info if necessary.'''
921 self.model.clear()
922 self._total_items = self._progress = 0
923 self.window.progressbar.show()
924 self._pulse_timeout = gobject.timeout_add(250, self._pulse_timeout_cb)
925 self.cache.get_items(self.jid, self.node, self._agent_items,
926 force = force, args = (force,))
928 def _pulse_timeout_cb(self, *args):
929 '''Simple callback to keep the progressbar pulsing.'''
930 if not self.active:
931 return False
932 self.window.progressbar.pulse()
933 return True
935 def _find_item(self, jid, node):
936 '''Check if an item is already in the treeview. Return an iter to it
937 if so, None otherwise.'''
938 iter_ = self.model.get_iter_root()
939 while iter_:
940 cjid = self.model.get_value(iter_, 0).decode('utf-8')
941 cnode = self.model.get_value(iter_, 1).decode('utf-8')
942 if jid == cjid and node == cnode:
943 break
944 iter_ = self.model.iter_next(iter_)
945 if iter_:
946 return iter_
947 return None
949 def _agent_items(self, jid, node, items, force):
950 '''Callback for when we receive a list of agent items.'''
951 self.model.clear()
952 self._total_items = 0
953 gobject.source_remove(self._pulse_timeout)
954 self.window.progressbar.hide()
955 # The server returned an error
956 if items == 0:
957 if not self.window.address_comboboxentry:
958 # We can't travel anywhere else.
959 self.window.destroy()
960 dialogs.ErrorDialog(_('The service is not browsable'),
961 _('This service does not contain any items to browse.'))
962 return
963 # We got a list of items
964 self.window.services_treeview.set_model(None)
965 for item in items:
966 jid_ = item['jid']
967 node_ = item.get('node', '')
968 # If such an item is already here: don't add it
969 if self._find_item(jid_, node_):
970 continue
971 self._total_items += 1
972 self._add_item(jid_, node_, node, item, force)
973 self.window.services_treeview.set_model(self.model)
975 def _agent_info(self, jid, node, identities, features, data):
976 '''Callback for when we receive info about an agent's item.'''
977 iter_ = self._find_item(jid, node)
978 if not iter_:
979 # Not in the treeview, stop
980 return
981 if identities == 0:
982 # The server returned an error
983 self._update_error(iter_, jid, node)
984 else:
985 # We got our info
986 self._update_info(iter_, jid, node, identities, features, data)
987 self.update_actions()
989 def _add_item(self, jid, node, parent_node, item, force):
990 '''Called when an item should be added to the model. The result of a
991 disco#items query.'''
992 self.model.append((jid, node, item.get('name', ''),
993 get_agent_address(jid, node)))
994 self.cache.get_info(jid, node, self._agent_info, force = force)
996 def _update_item(self, iter_, jid, node, item):
997 '''Called when an item should be updated in the model. The result of a
998 disco#items query. (seldom)'''
999 if 'name' in item:
1000 self.model[iter_][2] = item['name']
1002 def _update_info(self, iter_, jid, node, identities, features, data):
1003 '''Called when an item should be updated in the model with further info.
1004 The result of a disco#info query.'''
1005 name = identities[0].get('name', '')
1006 if name:
1007 self.model[iter_][2] = name
1009 def _update_error(self, iter_, jid, node):
1010 '''Called when a disco#info query failed for an item.'''
1011 pass
1014 class ToplevelAgentBrowser(AgentBrowser):
1015 '''This browser is used at the top level of a jabber server to browse
1016 services such as transports, conference servers, etc.'''
1017 def __init__(self, *args):
1018 AgentBrowser.__init__(self, *args)
1019 self._progressbar_sourceid = None
1020 self._renderer = None
1021 self._progress = 0
1022 self.tooltip = tooltips.ServiceDiscoveryTooltip()
1023 self.register_button = None
1024 self.join_button = None
1025 self.execute_button = None
1026 self.search_button = None
1027 # Keep track of our treeview signals
1028 self._view_signals = []
1029 self._scroll_signal = None
1031 def _pixbuf_renderer_data_func(self, col, cell, model, iter_):
1032 '''Callback for setting the pixbuf renderer's properties.'''
1033 jid = model.get_value(iter_, 0)
1034 if jid:
1035 pix = model.get_value(iter_, 2)
1036 cell.set_property('visible', True)
1037 cell.set_property('pixbuf', pix)
1038 else:
1039 cell.set_property('visible', False)
1041 def _text_renderer_data_func(self, col, cell, model, iter_):
1042 '''Callback for setting the text renderer's properties.'''
1043 jid = model.get_value(iter_, 0)
1044 markup = model.get_value(iter_, 3)
1045 state = model.get_value(iter_, 4)
1046 cell.set_property('markup', markup)
1047 if jid:
1048 cell.set_property('cell_background_set', False)
1049 if state > 0:
1050 # 1 = fetching, 2 = error
1051 cell.set_property('foreground_set', True)
1052 else:
1053 # Normal/succes
1054 cell.set_property('foreground_set', False)
1055 else:
1056 theme = gajim.config.get('roster_theme')
1057 bgcolor = gajim.config.get_per('themes', theme, 'groupbgcolor')
1058 if bgcolor:
1059 cell.set_property('cell_background_set', True)
1060 cell.set_property('foreground_set', False)
1062 def _treemodel_sort_func(self, model, iter1, iter2):
1063 '''Sort function for our treemodel.'''
1064 # Compare state
1065 statecmp = cmp(model.get_value(iter1, 4), model.get_value(iter2, 4))
1066 if statecmp == 0:
1067 # These can be None, apparently
1068 descr1 = model.get_value(iter1, 3)
1069 if descr1:
1070 descr1 = descr1.decode('utf-8')
1071 descr2 = model.get_value(iter2, 3)
1072 if descr2:
1073 descr2 = descr2.decode('utf-8')
1074 # Compare strings
1075 return cmp(descr1, descr2)
1076 return statecmp
1078 def _show_tooltip(self, state):
1079 view = self.window.services_treeview
1080 pointer = view.get_pointer()
1081 props = view.get_path_at_pos(pointer[0], pointer[1])
1082 # check if the current pointer is at the same path
1083 # as it was before setting the timeout
1084 if props and self.tooltip.id == props[0]:
1085 # bounding rectangle of coordinates for the cell within the treeview
1086 rect = view.get_cell_area(props[0], props[1])
1087 # position of the treeview on the screen
1088 position = view.window.get_origin()
1089 self.tooltip.show_tooltip(state, rect.height, position[1] + rect.y)
1090 else:
1091 self.tooltip.hide_tooltip()
1093 # These are all callbacks to make tooltips work
1094 def on_treeview_leave_notify_event(self, widget, event):
1095 props = widget.get_path_at_pos(int(event.x), int(event.y))
1096 if self.tooltip.timeout > 0:
1097 if not props or self.tooltip.id == props[0]:
1098 self.tooltip.hide_tooltip()
1100 def on_treeview_motion_notify_event(self, widget, event):
1101 props = widget.get_path_at_pos(int(event.x), int(event.y))
1102 if self.tooltip.timeout > 0:
1103 if not props or self.tooltip.id != props[0]:
1104 self.tooltip.hide_tooltip()
1105 if props:
1106 row = props[0]
1107 iter_ = None
1108 try:
1109 iter_ = self.model.get_iter(row)
1110 except Exception:
1111 self.tooltip.hide_tooltip()
1112 return
1113 jid = self.model[iter_][0]
1114 state = self.model[iter_][4]
1115 # Not a category, and we have something to say about state
1116 if jid and state > 0 and \
1117 (self.tooltip.timeout == 0 or self.tooltip.id != props[0]):
1118 self.tooltip.id = row
1119 self.tooltip.timeout = gobject.timeout_add(500,
1120 self._show_tooltip, state)
1122 def on_treeview_event_hide_tooltip(self, widget, event):
1123 ''' This happens on scroll_event, key_press_event
1124 and button_press_event '''
1125 self.tooltip.hide_tooltip()
1127 def _create_treemodel(self):
1128 # JID, node, icon, description, state
1129 # State means 2 when error, 1 when fetching, 0 when succes.
1130 view = self.window.services_treeview
1131 self.model = gtk.TreeStore(str, str, gtk.gdk.Pixbuf, str, int)
1132 self.model.set_sort_func(4, self._treemodel_sort_func)
1133 self.model.set_sort_column_id(4, gtk.SORT_ASCENDING)
1134 view.set_model(self.model)
1136 col = gtk.TreeViewColumn()
1137 # Icon Renderer
1138 renderer = gtk.CellRendererPixbuf()
1139 renderer.set_property('xpad', 6)
1140 col.pack_start(renderer, expand=False)
1141 col.set_cell_data_func(renderer, self._pixbuf_renderer_data_func)
1142 # Text Renderer
1143 renderer = gtk.CellRendererText()
1144 col.pack_start(renderer, expand=True)
1145 col.set_cell_data_func(renderer, self._text_renderer_data_func)
1146 renderer.set_property('foreground', 'dark gray')
1147 # Save this so we can go along with theme changes
1148 self._renderer = renderer
1149 self.update_theme()
1151 view.insert_column(col, -1)
1152 col.set_resizable(True)
1154 # Connect signals
1155 scrollwin = self.window.services_scrollwin
1156 self._view_signals.append(view.connect('leave-notify-event',
1157 self.on_treeview_leave_notify_event))
1158 self._view_signals.append(view.connect('motion-notify-event',
1159 self.on_treeview_motion_notify_event))
1160 self._view_signals.append(view.connect('key-press-event',
1161 self.on_treeview_event_hide_tooltip))
1162 self._view_signals.append(view.connect('button-press-event',
1163 self.on_treeview_event_hide_tooltip))
1164 self._scroll_signal = scrollwin.connect('scroll-event',
1165 self.on_treeview_event_hide_tooltip)
1167 def _clean_treemodel(self):
1168 # Disconnect signals
1169 view = self.window.services_treeview
1170 for sig in self._view_signals:
1171 view.disconnect(sig)
1172 self._view_signals = []
1173 if self._scroll_signal:
1174 scrollwin = self.window.services_scrollwin
1175 scrollwin.disconnect(self._scroll_signal)
1176 self._scroll_signal = None
1177 AgentBrowser._clean_treemodel(self)
1179 def _add_actions(self):
1180 AgentBrowser._add_actions(self)
1181 self.execute_button = gtk.Button()
1182 image = gtk.image_new_from_stock(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_BUTTON)
1183 label = gtk.Label(_('_Execute Command'))
1184 label.set_use_underline(True)
1185 hbox = gtk.HBox()
1186 hbox.pack_start(image, False, True, 6)
1187 hbox.pack_end(label, True, True)
1188 self.execute_button.add(hbox)
1189 self.execute_button.connect('clicked', self.on_execute_button_clicked)
1190 self.window.action_buttonbox.add(self.execute_button)
1191 self.execute_button.show_all()
1193 self.register_button = gtk.Button(label=_("Re_gister"),
1194 use_underline=True)
1195 self.register_button.connect('clicked', self.on_register_button_clicked)
1196 self.window.action_buttonbox.add(self.register_button)
1197 self.register_button.show_all()
1199 self.join_button = gtk.Button()
1200 image = gtk.image_new_from_stock(gtk.STOCK_CONNECT, gtk.ICON_SIZE_BUTTON)
1201 label = gtk.Label(_('_Join'))
1202 label.set_use_underline(True)
1203 hbox = gtk.HBox()
1204 hbox.pack_start(image, False, True, 6)
1205 hbox.pack_end(label, True, True)
1206 self.join_button.add(hbox)
1207 self.join_button.connect('clicked', self.on_join_button_clicked)
1208 self.window.action_buttonbox.add(self.join_button)
1209 self.join_button.show_all()
1211 self.search_button = gtk.Button()
1212 image = gtk.image_new_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_BUTTON)
1213 label = gtk.Label(_('_Search'))
1214 label.set_use_underline(True)
1215 hbox = gtk.HBox()
1216 hbox.pack_start(image, False, True, 6)
1217 hbox.pack_end(label, True, True)
1218 self.search_button.add(hbox)
1219 self.search_button.connect('clicked', self.on_search_button_clicked)
1220 self.window.action_buttonbox.add(self.search_button)
1221 self.search_button.show_all()
1223 def _clean_actions(self):
1224 if self.execute_button:
1225 self.execute_button.destroy()
1226 self.execute_button = None
1227 if self.register_button:
1228 self.register_button.destroy()
1229 self.register_button = None
1230 if self.join_button:
1231 self.join_button.destroy()
1232 self.join_button = None
1233 if self.search_button:
1234 self.search_button.destroy()
1235 self.search_button = None
1236 AgentBrowser._clean_actions(self)
1238 def on_search_button_clicked(self, widget = None):
1239 '''When we want to search something:
1240 open search window'''
1241 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1242 if not iter_:
1243 return
1244 service = model[iter_][0].decode('utf-8')
1245 if service in gajim.interface.instances[self.account]['search']:
1246 gajim.interface.instances[self.account]['search'][service].window.\
1247 present()
1248 else:
1249 gajim.interface.instances[self.account]['search'][service] = \
1250 search_window.SearchWindow(self.account, service)
1252 def cleanup(self):
1253 self.tooltip.hide_tooltip()
1254 AgentBrowser.cleanup(self)
1256 def update_theme(self):
1257 theme = gajim.config.get('roster_theme')
1258 bgcolor = gajim.config.get_per('themes', theme, 'groupbgcolor')
1259 if bgcolor:
1260 self._renderer.set_property('cell-background', bgcolor)
1261 self.window.services_treeview.queue_draw()
1263 def on_execute_button_clicked(self, widget=None):
1264 '''When we want to execute a command:
1265 open adhoc command window'''
1266 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1267 if not iter_:
1268 return
1269 service = model[iter_][0].decode('utf-8')
1270 node = model[iter_][1].decode('utf-8')
1271 adhoc_commands.CommandWindow(self.account, service, commandnode=node)
1273 def on_register_button_clicked(self, widget = None):
1274 '''When we want to register an agent:
1275 request information about registering with the agent and close the
1276 window.'''
1277 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1278 if not iter_:
1279 return
1280 jid = model[iter_][0].decode('utf-8')
1281 if jid:
1282 gajim.connections[self.account].request_register_agent_info(jid)
1283 self.window.destroy(chain = True)
1285 def on_join_button_clicked(self, widget):
1286 '''When we want to join an IRC room or create a new MUC room:
1287 Opens the join_groupchat_window.'''
1288 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1289 if not iter_:
1290 return
1291 service = model[iter_][0].decode('utf-8')
1292 if 'join_gc' not in gajim.interface.instances[self.account]:
1293 try:
1294 dialogs.JoinGroupchatWindow(self.account, service)
1295 except GajimGeneralException:
1296 pass
1297 else:
1298 gajim.interface.instances[self.account]['join_gc'].window.present()
1299 self.window.destroy(chain = True)
1301 def update_actions(self):
1302 if self.execute_button:
1303 self.execute_button.set_sensitive(False)
1304 if self.register_button:
1305 self.register_button.set_sensitive(False)
1306 if self.browse_button:
1307 self.browse_button.set_sensitive(False)
1308 if self.join_button:
1309 self.join_button.set_sensitive(False)
1310 if self.search_button:
1311 self.search_button.set_sensitive(False)
1312 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1313 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1314 if not iter_:
1315 return
1316 if not model[iter_][0]:
1317 # We're on a category row
1318 return
1319 if model[iter_][4] != 0:
1320 # We don't have the info (yet)
1321 # It's either unknown or a transport, register button should be active
1322 if self.register_button:
1323 self.register_button.set_sensitive(True)
1324 # Guess what kind of service we're dealing with
1325 if self.browse_button:
1326 jid = model[iter_][0].decode('utf-8')
1327 type_ = gajim.get_transport_name_from_jid(jid,
1328 use_config_setting = False)
1329 if type_:
1330 identity = {'category': '_jid', 'type': type_}
1331 klass = self.cache.get_browser([identity])
1332 if klass:
1333 self.browse_button.set_sensitive(True)
1334 else:
1335 # We couldn't guess
1336 self.browse_button.set_sensitive(True)
1337 else:
1338 # Normal case, we have info
1339 AgentBrowser.update_actions(self)
1341 def _update_actions(self, jid, node, identities, features, data):
1342 AgentBrowser._update_actions(self, jid, node, identities, features, data)
1343 if self.execute_button and xmpp.NS_COMMANDS in features:
1344 self.execute_button.set_sensitive(True)
1345 if self.search_button and xmpp.NS_SEARCH in features:
1346 self.search_button.set_sensitive(True)
1347 if self.register_button and xmpp.NS_REGISTER in features:
1348 # We can register this agent
1349 registered_transports = []
1350 jid_list = gajim.contacts.get_jid_list(self.account)
1351 for jid in jid_list:
1352 contact = gajim.contacts.get_first_contact_from_jid(
1353 self.account, jid)
1354 if _('Transports') in contact.groups:
1355 registered_transports.append(jid)
1356 if jid in registered_transports:
1357 self.register_button.set_label(_('_Edit'))
1358 else:
1359 self.register_button.set_label(_('Re_gister'))
1360 self.register_button.set_sensitive(True)
1361 if self.join_button and xmpp.NS_MUC in features:
1362 self.join_button.set_sensitive(True)
1364 def _default_action(self, jid, node, identities, features, data):
1365 if AgentBrowser._default_action(self, jid, node, identities, features, data):
1366 return True
1367 if xmpp.NS_REGISTER in features:
1368 # Register if we can't browse
1369 self.on_register_button_clicked()
1370 return True
1371 return False
1373 def browse(self, force = False):
1374 self._progress = 0
1375 AgentBrowser.browse(self, force = force)
1377 def _expand_all(self):
1378 '''Expand all items in the treeview'''
1379 # GTK apparently screws up here occasionally. :/
1380 #def expand_all(*args):
1381 # self.window.services_treeview.expand_all()
1382 # self.expanding = False
1383 # return False
1384 #self.expanding = True
1385 #gobject.idle_add(expand_all)
1386 self.window.services_treeview.expand_all()
1388 def _update_progressbar(self):
1389 '''Update the progressbar.'''
1390 # Refresh this every update
1391 if self._progressbar_sourceid:
1392 gobject.source_remove(self._progressbar_sourceid)
1394 fraction = 0
1395 if self._total_items:
1396 self.window.progressbar.set_text(_("Scanning %(current)d / %(total)d.."
1397 ) % {'current': self._progress, 'total': self._total_items})
1398 fraction = float(self._progress) / float(self._total_items)
1399 if self._progress >= self._total_items:
1400 # We show the progressbar for just a bit before hiding it.
1401 id_ = gobject.timeout_add_seconds(2, self._hide_progressbar_cb)
1402 self._progressbar_sourceid = id_
1403 else:
1404 self.window.progressbar.show()
1405 # Hide the progressbar if we're timing out anyways. (20 secs)
1406 id_ = gobject.timeout_add_seconds(20, self._hide_progressbar_cb)
1407 self._progressbar_sourceid = id_
1408 self.window.progressbar.set_fraction(fraction)
1410 def _hide_progressbar_cb(self, *args):
1411 '''Simple callback to hide the progressbar a second after we finish.'''
1412 if self.active:
1413 self.window.progressbar.hide()
1414 return False
1416 def _friendly_category(self, category, type_=None):
1417 '''Get the friendly category name and priority.'''
1418 cat = None
1419 if type_:
1420 # Try type-specific override
1421 try:
1422 cat, prio = _cat_to_descr[(category, type_)]
1423 except KeyError:
1424 pass
1425 if not cat:
1426 try:
1427 cat, prio = _cat_to_descr[category]
1428 except KeyError:
1429 cat, prio = _cat_to_descr['other']
1430 return cat, prio
1432 def _create_category(self, cat, type_=None):
1433 '''Creates a category row.'''
1434 cat, prio = self._friendly_category(cat, type_)
1435 return self.model.append(None, ('', '', None, cat, prio))
1437 def _find_category(self, cat, type_=None):
1438 '''Looks up a category row and returns the iterator to it, or None.'''
1439 cat = self._friendly_category(cat, type_)[0]
1440 iter_ = self.model.get_iter_root()
1441 while iter_:
1442 if self.model.get_value(iter_, 3).decode('utf-8') == cat:
1443 break
1444 iter_ = self.model.iter_next(iter_)
1445 if iter_:
1446 return iter_
1447 return None
1449 def _find_item(self, jid, node):
1450 iter_ = None
1451 cat_iter = self.model.get_iter_root()
1452 while cat_iter and not iter_:
1453 iter_ = self.model.iter_children(cat_iter)
1454 while iter_:
1455 cjid = self.model.get_value(iter_, 0).decode('utf-8')
1456 cnode = self.model.get_value(iter_, 1).decode('utf-8')
1457 if jid == cjid and node == cnode:
1458 break
1459 iter_ = self.model.iter_next(iter_)
1460 cat_iter = self.model.iter_next(cat_iter)
1461 if iter_:
1462 return iter_
1463 return None
1465 def _add_item(self, jid, node, parent_node, item, force):
1466 # Row text
1467 addr = get_agent_address(jid, node)
1468 if 'name' in item:
1469 descr = "<b>%s</b>\n%s" % (item['name'], addr)
1470 else:
1471 descr = "<b>%s</b>" % addr
1472 # Guess which kind of service this is
1473 identities = []
1474 type_ = gajim.get_transport_name_from_jid(jid,
1475 use_config_setting = False)
1476 if type_:
1477 identity = {'category': '_jid', 'type': type_}
1478 identities.append(identity)
1479 cat_args = ('_jid', type_)
1480 else:
1481 # Put it in the 'other' category for now
1482 cat_args = ('other',)
1483 # Set the pixmap for the row
1484 pix = self.cache.get_icon(identities)
1485 # Put it in the right category
1486 cat = self._find_category(*cat_args)
1487 if not cat:
1488 cat = self._create_category(*cat_args)
1489 self.model.append(cat, (jid, node, pix, descr, 1))
1490 gobject.idle_add(self._expand_all)
1491 # Grab info on the service
1492 self.cache.get_info(jid, node, self._agent_info, force=force)
1493 self._update_progressbar()
1495 def _update_item(self, iter_, jid, node, item):
1496 addr = get_agent_address(jid, node)
1497 if 'name' in item:
1498 descr = "<b>%s</b>\n%s" % (item['name'], addr)
1499 else:
1500 descr = "<b>%s</b>" % addr
1501 self.model[iter_][3] = descr
1503 def _update_info(self, iter_, jid, node, identities, features, data):
1504 addr = get_agent_address(jid, node)
1505 name = identities[0].get('name', '')
1506 if name:
1507 descr = "<b>%s</b>\n%s" % (name, addr)
1508 else:
1509 descr = "<b>%s</b>" % addr
1511 # Update progress
1512 self._progress += 1
1513 self._update_progressbar()
1515 # Search for an icon and category we can display
1516 pix = self.cache.get_icon(identities)
1517 for identity in identities:
1518 try:
1519 cat, type_ = identity['category'], identity['type']
1520 except KeyError:
1521 continue
1522 break
1524 # Check if we have to move categories
1525 old_cat_iter = self.model.iter_parent(iter_)
1526 old_cat = self.model.get_value(old_cat_iter, 3).decode('utf-8')
1527 if self.model.get_value(old_cat_iter, 3) == cat:
1528 # Already in the right category, just update
1529 self.model[iter_][2] = pix
1530 self.model[iter_][3] = descr
1531 self.model[iter_][4] = 0
1532 return
1533 # Not in the right category, move it.
1534 self.model.remove(iter_)
1536 # Check if the old category is empty
1537 if not self.model.iter_is_valid(old_cat_iter):
1538 old_cat_iter = self._find_category(old_cat)
1539 if not self.model.iter_children(old_cat_iter):
1540 self.model.remove(old_cat_iter)
1542 cat_iter = self._find_category(cat, type_)
1543 if not cat_iter:
1544 cat_iter = self._create_category(cat, type_)
1545 self.model.append(cat_iter, (jid, node, pix, descr, 0))
1546 self._expand_all()
1548 def _update_error(self, iter_, jid, node):
1549 self.model[iter_][4] = 2
1550 self._progress += 1
1551 self._update_progressbar()
1554 class MucBrowser(AgentBrowser):
1555 def __init__(self, *args, **kwargs):
1556 AgentBrowser.__init__(self, *args, **kwargs)
1557 self.join_button = None
1558 self.bookmark_button = None
1560 def _create_treemodel(self):
1561 # JID, node, name, users_int, users_str, description, fetched
1562 # This is rather long, I'd rather not use a data_func here though.
1563 # Users is a string, because want to be able to leave it empty.
1564 self.model = gtk.ListStore(str, str, str, int, str, str, bool)
1565 self.model.set_sort_column_id(2, gtk.SORT_ASCENDING)
1566 self.window.services_treeview.set_model(self.model)
1567 # Name column
1568 col = gtk.TreeViewColumn(_('Name'))
1569 col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1570 col.set_fixed_width(100)
1571 renderer = gtk.CellRendererText()
1572 col.pack_start(renderer)
1573 col.set_attributes(renderer, text = 2)
1574 col.set_sort_column_id(2)
1575 self.window.services_treeview.insert_column(col, -1)
1576 col.set_resizable(True)
1577 # Users column
1578 col = gtk.TreeViewColumn(_('Users'))
1579 renderer = gtk.CellRendererText()
1580 col.pack_start(renderer)
1581 col.set_attributes(renderer, text = 4)
1582 col.set_sort_column_id(3)
1583 self.window.services_treeview.insert_column(col, -1)
1584 col.set_resizable(True)
1585 # Description column
1586 col = gtk.TreeViewColumn(_('Description'))
1587 renderer = gtk.CellRendererText()
1588 col.pack_start(renderer)
1589 col.set_attributes(renderer, text = 5)
1590 col.set_sort_column_id(4)
1591 self.window.services_treeview.insert_column(col, -1)
1592 col.set_resizable(True)
1593 # Id column
1594 col = gtk.TreeViewColumn(_('Id'))
1595 renderer = gtk.CellRendererText()
1596 col.pack_start(renderer)
1597 col.set_attributes(renderer, text = 0)
1598 col.set_sort_column_id(0)
1599 self.window.services_treeview.insert_column(col, -1)
1600 col.set_resizable(True)
1601 self.window.services_treeview.set_headers_visible(True)
1602 self.window.services_treeview.set_headers_clickable(True)
1603 # Source id for idle callback used to start disco#info queries.
1604 self._fetch_source = None
1605 # Query failure counter
1606 self._broken = 0
1607 # Connect to scrollwindow scrolling
1608 self.vadj = self.window.services_scrollwin.get_property('vadjustment')
1609 self.vadj_cbid = self.vadj.connect('value-changed', self.on_scroll)
1610 # And to size changes
1611 self.size_cbid = self.window.services_scrollwin.connect(
1612 'size-allocate', self.on_scroll)
1614 def _clean_treemodel(self):
1615 if self.size_cbid:
1616 self.window.services_scrollwin.disconnect(self.size_cbid)
1617 self.size_cbid = None
1618 if self.vadj_cbid:
1619 self.vadj.disconnect(self.vadj_cbid)
1620 self.vadj_cbid = None
1621 AgentBrowser._clean_treemodel(self)
1623 def _add_actions(self):
1624 self.bookmark_button = gtk.Button(label=_('_Bookmark'), use_underline=True)
1625 self.bookmark_button.connect('clicked', self.on_bookmark_button_clicked)
1626 self.window.action_buttonbox.add(self.bookmark_button)
1627 self.bookmark_button.show_all()
1628 self.join_button = gtk.Button(label=_('_Join'), use_underline=True)
1629 self.join_button.connect('clicked', self.on_join_button_clicked)
1630 self.window.action_buttonbox.add(self.join_button)
1631 self.join_button.show_all()
1633 def _clean_actions(self):
1634 if self.bookmark_button:
1635 self.bookmark_button.destroy()
1636 self.bookmark_button = None
1637 if self.join_button:
1638 self.join_button.destroy()
1639 self.join_button = None
1641 def on_bookmark_button_clicked(self, *args):
1642 model, iter = self.window.services_treeview.get_selection().get_selected()
1643 if not iter:
1644 return
1645 name = gajim.config.get_per('accounts', self.account, 'name')
1646 room_jid = model[iter][0].decode('utf-8')
1647 bm = {
1648 'name': room_jid.split('@')[0],
1649 'jid': room_jid,
1650 'autojoin': '0',
1651 'minimize': '0',
1652 'password': '',
1653 'nick': name
1656 for bookmark in gajim.connections[self.account].bookmarks:
1657 if bookmark['jid'] == bm['jid']:
1658 dialogs.ErrorDialog(
1659 _('Bookmark already set'),
1660 _('Group Chat "%s" is already in your bookmarks.') % bm['jid'])
1661 return
1663 gajim.connections[self.account].bookmarks.append(bm)
1664 gajim.connections[self.account].store_bookmarks()
1666 gajim.interface.roster.set_actions_menu_needs_rebuild()
1668 dialogs.InformationDialog(
1669 _('Bookmark has been added successfully'),
1670 _('You can manage your bookmarks via Actions menu in your roster.'))
1672 def on_join_button_clicked(self, *args):
1673 '''When we want to join a conference:
1674 Ask specific informations about the selected agent and close the window'''
1675 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1676 if not iter_:
1677 return
1678 service = model[iter_][0].decode('utf-8')
1679 room = model[iter_][1].decode('utf-8')
1680 if 'join_gc' not in gajim.interface.instances[self.account]:
1681 try:
1682 dialogs.JoinGroupchatWindow(self.account, service)
1683 except GajimGeneralException:
1684 pass
1685 else:
1686 gajim.interface.instances[self.account]['join_gc'].window.present()
1687 self.window.destroy(chain = True)
1689 def update_actions(self):
1690 sens = self.window.services_treeview.get_selection().count_selected_rows()
1691 if self.bookmark_button:
1692 self.bookmark_button.set_sensitive(sens > 0)
1693 if self.join_button:
1694 self.join_button.set_sensitive(sens > 0)
1696 def default_action(self):
1697 self.on_join_button_clicked()
1699 def _start_info_query(self):
1700 '''Idle callback to start checking for visible rows.'''
1701 self._fetch_source = None
1702 self._query_visible()
1703 return False
1705 def on_scroll(self, *args):
1706 '''Scrollwindow callback to trigger new queries on scolling.'''
1707 # This apparently happens when inactive sometimes
1708 self._query_visible()
1710 def _query_visible(self):
1711 '''Query the next visible row for info.'''
1712 if self._fetch_source:
1713 # We're already fetching
1714 return
1715 view = self.window.services_treeview
1716 if not view.flags() & gtk.REALIZED:
1717 # Prevent a silly warning, try again in a bit.
1718 self._fetch_source = gobject.timeout_add(100, self._start_info_query)
1719 return
1720 # We have to do this in a pygtk <2.8 compatible way :/
1721 #start, end = self.window.services_treeview.get_visible_range()
1722 rect = view.get_visible_rect()
1723 iter_ = end = None
1724 # Top row
1725 try:
1726 sx, sy = view.tree_to_widget_coords(rect.x, rect.y)
1727 spath = view.get_path_at_pos(sx, sy)[0]
1728 iter_ = self.model.get_iter(spath)
1729 except TypeError:
1730 self._fetch_source = None
1731 return
1732 # Bottom row
1733 # Iter compare is broke, use the path instead
1734 try:
1735 ex, ey = view.tree_to_widget_coords(rect.x + rect.height,
1736 rect.y + rect.height)
1737 end = view.get_path_at_pos(ex, ey)[0]
1738 # end is the last visible, we want to query that aswell
1739 end = (end[0] + 1,)
1740 except TypeError:
1741 # We're at the end of the model, we can leave end=None though.
1742 pass
1743 while iter_ and self.model.get_path(iter_) != end:
1744 if not self.model.get_value(iter_, 6):
1745 jid = self.model.get_value(iter_, 0).decode('utf-8')
1746 node = self.model.get_value(iter_, 1).decode('utf-8')
1747 self.cache.get_info(jid, node, self._agent_info)
1748 self._fetch_source = True
1749 return
1750 iter_ = self.model.iter_next(iter_)
1751 self._fetch_source = None
1753 def _channel_altinfo(self, jid, node, items, name = None):
1754 '''Callback for the alternate disco#items query. We try to atleast get
1755 the amount of users in the room if the service does not support MUC
1756 dataforms.'''
1757 if items == 0:
1758 # The server returned an error
1759 self._broken += 1
1760 if self._broken >= 3:
1761 # Disable queries completely after 3 failures
1762 if self.size_cbid:
1763 self.window.services_scrollwin.disconnect(self.size_cbid)
1764 self.size_cbid = None
1765 if self.vadj_cbid:
1766 self.vadj.disconnect(self.vadj_cbid)
1767 self.vadj_cbid = None
1768 self._fetch_source = None
1769 return
1770 else:
1771 iter_ = self._find_item(jid, node)
1772 if iter_:
1773 if name:
1774 self.model[iter_][2] = name
1775 self.model[iter_][3] = len(items) # The number of users
1776 self.model[iter_][4] = str(len(items)) # The number of users
1777 self.model[iter_][6] = True
1778 self._fetch_source = None
1779 self._query_visible()
1781 def _add_item(self, jid, node, parent_node, item, force):
1782 self.model.append((jid, node, item.get('name', ''), -1, '', '', False))
1783 if not self._fetch_source:
1784 self._fetch_source = gobject.idle_add(self._start_info_query)
1786 def _update_info(self, iter_, jid, node, identities, features, data):
1787 name = identities[0].get('name', '')
1788 for form in data:
1789 typefield = form.getField('FORM_TYPE')
1790 if typefield and typefield.getValue() == \
1791 'http://jabber.org/protocol/muc#roominfo':
1792 # Fill model row from the form's fields
1793 users = form.getField('muc#roominfo_occupants')
1794 descr = form.getField('muc#roominfo_description')
1795 if users:
1796 self.model[iter_][3] = int(users.getValue())
1797 self.model[iter_][4] = users.getValue()
1798 if descr:
1799 self.model[iter_][5] = descr.getValue()
1800 # Only set these when we find a form with additional info
1801 # Some servers don't support forms and put extra info in
1802 # the name attribute, so we preserve it in that case.
1803 self.model[iter_][2] = name
1804 self.model[iter_][6] = True
1805 break
1806 else:
1807 # We didn't find a form, switch to alternate query mode
1808 self.cache.get_items(jid, node, self._channel_altinfo, args = (name,))
1809 return
1810 # Continue with the next
1811 self._fetch_source = None
1812 self._query_visible()
1814 def _update_error(self, iter_, jid, node):
1815 # switch to alternate query mode
1816 self.cache.get_items(jid, node, self._channel_altinfo)
1818 def PubSubBrowser(account, jid, node):
1819 ''' Returns an AgentBrowser subclass that will display service discovery
1820 for particular pubsub service. Different pubsub services may need to
1821 present different data during browsing. '''
1822 # for now, only discussion groups are supported...
1823 # TODO: check if it has appropriate features to be such kind of service
1824 return DiscussionGroupsBrowser(account, jid, node)
1826 class DiscussionGroupsBrowser(AgentBrowser):
1827 ''' For browsing pubsub-based discussion groups service. '''
1828 def __init__(self, account, jid, node):
1829 AgentBrowser.__init__(self, account, jid, node)
1831 # this will become set object when we get subscriptions; None means
1832 # we don't know yet which groups are subscribed
1833 self.subscriptions = None
1835 # this will become our action widgets when we create them; None means
1836 # we don't have them yet (needed for check in callback)
1837 self.subscribe_button = None
1838 self.unsubscribe_button = None
1840 gajim.connections[account].send_pb_subscription_query(jid, self._subscriptionsCB)
1842 def _create_treemodel(self):
1843 ''' Create treemodel for the window. '''
1844 # JID, node, name (with description) - pango markup, dont have info?, subscribed?
1845 self.model = gtk.TreeStore(str, str, str, bool, bool)
1846 # sort by name
1847 self.model.set_sort_column_id(2, gtk.SORT_ASCENDING)
1848 self.window.services_treeview.set_model(self.model)
1850 # Name column
1851 # Pango markup for name and description, description printed with
1852 # <small/> font
1853 renderer = gtk.CellRendererText()
1854 col = gtk.TreeViewColumn(_('Name'))
1855 col.pack_start(renderer)
1856 col.set_attributes(renderer, markup=2)
1857 col.set_resizable(True)
1858 self.window.services_treeview.insert_column(col, -1)
1859 self.window.services_treeview.set_headers_visible(True)
1861 # Subscription state
1862 renderer = gtk.CellRendererToggle()
1863 col = gtk.TreeViewColumn(_('Subscribed'))
1864 col.pack_start(renderer)
1865 col.set_attributes(renderer, inconsistent=3, active=4)
1866 col.set_resizable(False)
1867 self.window.services_treeview.insert_column(col, -1)
1869 # Node Column
1870 renderer = gtk.CellRendererText()
1871 col = gtk.TreeViewColumn(_('Node'))
1872 col.pack_start(renderer)
1873 col.set_attributes(renderer, markup=1)
1874 col.set_resizable(True)
1875 self.window.services_treeview.insert_column(col, -1)
1877 def _add_items(self, jid, node, items, force):
1878 for item in items:
1879 jid_ = item['jid']
1880 node_ = item.get('node', '')
1881 self._total_items += 1
1882 self._add_item(jid_, node_, node, item, force)
1884 def _in_list_foreach(self, model, path, iter_, node):
1885 if model[path][1] == node:
1886 self.in_list = True
1888 def _in_list(self, node):
1889 self.in_list = False
1890 self.model.foreach(self._in_list_foreach, node)
1891 return self.in_list
1893 def _add_item(self, jid, node, parent_node, item, force):
1894 ''' Called when we got basic information about new node from query.
1895 Show the item. '''
1896 name = item.get('name', '')
1898 if self.subscriptions is not None:
1899 dunno = False
1900 subscribed = node in self.subscriptions
1901 else:
1902 dunno = True
1903 subscribed = False
1905 name = gobject.markup_escape_text(name)
1906 name = '<b>%s</b>' % name
1908 parent_iter = self._get_iter(parent_node)
1909 if not self._in_list(node):
1910 self.model.append(parent_iter, (jid, node, name, dunno, subscribed))
1911 self.cache.get_items(jid, node, self._add_items, force = force,
1912 args = (force,))
1914 def _get_child_iter(self, parent_iter, node):
1915 child_iter = self.model.iter_children(parent_iter)
1916 while child_iter:
1917 if self.model[child_iter][1] == node:
1918 return child_iter
1919 child_iter = self.model.iter_next(child_iter)
1920 return None
1922 def _get_iter(self, node):
1923 ''' Look for an iter with the given node '''
1924 self.found_iter = None
1925 def is_node(model, path, iter, node):
1926 if model[iter][1] == node:
1927 self.found_iter = iter
1928 return True
1929 self.model.foreach(is_node, node)
1930 return self.found_iter
1932 def _add_actions(self):
1933 self.post_button = gtk.Button(label=_('New post'), use_underline=True)
1934 self.post_button.set_sensitive(False)
1935 self.post_button.connect('clicked', self.on_post_button_clicked)
1936 self.window.action_buttonbox.add(self.post_button)
1937 self.post_button.show_all()
1939 self.subscribe_button = gtk.Button(label=_('_Subscribe'), use_underline=True)
1940 self.subscribe_button.set_sensitive(False)
1941 self.subscribe_button.connect('clicked', self.on_subscribe_button_clicked)
1942 self.window.action_buttonbox.add(self.subscribe_button)
1943 self.subscribe_button.show_all()
1945 self.unsubscribe_button = gtk.Button(label=_('_Unsubscribe'), use_underline=True)
1946 self.unsubscribe_button.set_sensitive(False)
1947 self.unsubscribe_button.connect('clicked', self.on_unsubscribe_button_clicked)
1948 self.window.action_buttonbox.add(self.unsubscribe_button)
1949 self.unsubscribe_button.show_all()
1951 def _clean_actions(self):
1952 if self.post_button is not None:
1953 self.post_button.destroy()
1954 self.post_button = None
1956 if self.subscribe_button is not None:
1957 self.subscribe_button.destroy()
1958 self.subscribe_button = None
1960 if self.unsubscribe_button is not None:
1961 self.unsubscribe_button.destroy()
1962 self.unsubscribe_button = None
1964 def update_actions(self):
1965 '''Called when user selected a row. Make subscribe/unsubscribe buttons
1966 sensitive appropriatelly.'''
1967 # we have nothing to do if we don't have buttons...
1968 if self.subscribe_button is None: return
1970 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1971 if not iter_ or self.subscriptions is None:
1972 # no item selected or no subscriptions info, all buttons are insensitive
1973 self.post_button.set_sensitive(False)
1974 self.subscribe_button.set_sensitive(False)
1975 self.unsubscribe_button.set_sensitive(False)
1976 else:
1977 subscribed = model.get_value(iter_, 4) # 4 = subscribed?
1978 self.post_button.set_sensitive(subscribed)
1979 self.subscribe_button.set_sensitive(not subscribed)
1980 self.unsubscribe_button.set_sensitive(subscribed)
1982 def on_post_button_clicked(self, widget):
1983 '''Called when 'post' button is pressed. Open window to create post'''
1984 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1985 if iter_ is None: return
1987 groupnode = model.get_value(iter_, 1) # 1 = groupnode
1989 groups.GroupsPostWindow(self.account, self.jid, groupnode)
1991 def on_subscribe_button_clicked(self, widget):
1992 '''Called when 'subscribe' button is pressed. Send subscribtion request.'''
1993 model, iter_ = self.window.services_treeview.get_selection().get_selected()
1994 if iter_ is None: return
1996 groupnode = model.get_value(iter_, 1) # 1 = groupnode
1998 gajim.connections[self.account].send_pb_subscribe(self.jid, groupnode, self._subscribeCB, groupnode)
2000 def on_unsubscribe_button_clicked(self, widget):
2001 '''Called when 'unsubscribe' button is pressed. Send unsubscription request.'''
2002 model, iter_ = self.window.services_treeview.get_selection().get_selected()
2003 if iter_ is None: return
2005 groupnode = model.get_value(iter_, 1) # 1 = groupnode
2007 gajim.connections[self.account].send_pb_unsubscribe(self.jid, groupnode, self._unsubscribeCB, groupnode)
2009 def _subscriptionsCB(self, conn, request):
2010 ''' We got the subscribed groups list stanza. Now, if we already
2011 have items on the list, we should actualize them. '''
2012 try:
2013 subscriptions = request.getTag('pubsub').getTag('subscriptions')
2014 except Exception:
2015 return
2017 groups = set()
2018 for child in subscriptions.getTags('subscription'):
2019 groups.add(child['node'])
2021 self.subscriptions = groups
2023 # try to setup existing items in model
2024 model = self.window.services_treeview.get_model()
2025 for row in model:
2026 # 1 = group node
2027 # 3 = insensitive checkbox for subscribed
2028 # 4 = subscribed?
2029 groupnode = row[1]
2030 row[3]=False
2031 row[4]=groupnode in groups
2033 # we now know subscriptions, update button states
2034 self.update_actions()
2036 raise xmpp.NodeProcessed
2038 def _subscribeCB(self, conn, request, groupnode):
2039 '''We have just subscribed to a node. Update UI'''
2040 self.subscriptions.add(groupnode)
2042 model = self.window.services_treeview.get_model()
2043 for row in model:
2044 if row[1] == groupnode: # 1 = groupnode
2045 row[4]=True
2046 break
2048 self.update_actions()
2050 raise xmpp.NodeProcessed
2052 def _unsubscribeCB(self, conn, request, groupnode):
2053 '''We have just unsubscribed from a node. Update UI'''
2054 self.subscriptions.remove(groupnode)
2056 model = self.window.services_treeview.get_model()
2057 for row in model:
2058 if row[1] == groupnode: # 1 = groupnode
2059 row[4]=False
2060 break
2062 self.update_actions()
2064 raise xmpp.NodeProcessed
2066 # Fill the global agent type info dictionary
2067 _agent_type_info = _gen_agent_type_info()
2069 # vim: se ts=3: