disco.py 78.9 KB
Newer Older
1
# -*- coding: utf-8 -*-
roidelapluie's avatar
roidelapluie committed
2
## src/disco.py
3
##
roidelapluie's avatar
roidelapluie committed
4
## Copyright (C) 2005-2006 Stéphan Kochen <stephan AT kochen.nl>
roidelapluie's avatar
roidelapluie committed
5
## Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
Dicson's avatar
Dicson committed
6
## Copyright (C) 2005-2014 Yann Leboulanger <asterix AT lagaule.org>
roidelapluie's avatar
roidelapluie committed
7 8 9
## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
## Copyright (C) 2007 Stephan Erb <steve-e AT h3c.de>
10
##
11 12 13
## This file is part of Gajim.
##
## 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.
16
##
17
## Gajim is distributed in the hope that it will be useful,
18
## but WITHOUT ANY WARRANTY; without even the implied warranty of
roidelapluie's avatar
roidelapluie committed
19
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 21
## GNU General Public License for more details.
##
22
## You should have received a copy of the GNU General Public License
roidelapluie's avatar
roidelapluie committed
23
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
24
##
25 26 27 28 29 30 31 32 33 34 35 36

# The appearance of the treeview, and parts of the dialog, are controlled by
# AgentBrowser (sub-)classes. Methods that probably should be overridden when
# subclassing are: (look at the docstrings and source for additional info)
# - def cleanup(self) *
# - def _create_treemodel(self) *
# - def _add_actions(self)
# - def _clean_actions(self)
# - def update_theme(self) *
# - def update_actions(self)
# - def default_action(self)
# - def _find_item(self, jid, node)
37
# - def _add_item(self, jid, node, parent_node, item, force)
38 39 40
# - def _update_item(self, iter_, jid, node, item)
# - def _update_info(self, iter_, jid, node, identities, features, data)
# - def _update_error(self, iter_, jid, node)
41 42 43 44
#
# * Should call the super class for this method.
# All others do not have to call back to the super class. (but can if they want
# the functionality)
nkour's avatar
nkour committed
45
# There are more methods, of course, but this is a basic set.
46

47
import types
48
import weakref
Yann Leboulanger's avatar
Yann Leboulanger committed
49
from gi.repository import GLib
50
from gi.repository import Gtk
Dicson's avatar
Dicson committed
51
from gi.repository import Gdk
52 53
from gi.repository import GdkPixbuf
from gi.repository import Pango
54

55 56
from gajim.gtk import ErrorDialog
from gajim.gtk import InformationDialog
André's avatar
André committed
57 58 59 60 61
from gajim import gtkgui_helpers
from gajim import groups
from gajim import adhoc_commands
from gajim import search_window
from gajim import gui_menu_builder
62
from gajim.gtk import ServiceRegistration
63

64
from gajim.common import app
65
import nbxmpp
André's avatar
André committed
66 67
from gajim.common import helpers
from gajim.common import ged
68

69 70 71 72 73 74
LABELS = {
    1: _('This service has not yet responded with detailed information'),
    2: _('This service could not respond with detailed information.\n'
         'It is most likely legacy or broken'),
}

75 76 77 78 79
# Dictionary mapping category, type pairs to browser class, image pairs.
# This is a function, so we can call it after the classes are declared.
# For the browser class, None means that the service will only be browsable
# when it advertises disco as it's feature, False means it's never browsable.
def _gen_agent_type_info():
80
    return {
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
        # Defaults
        (0, 0):                         (None, None),

        # Jabber server
        ('server', 'im'):               (ToplevelAgentBrowser, 'jabber'),
        ('services', 'jabber'):         (ToplevelAgentBrowser, 'jabber'),
        ('hierarchy', 'branch'):        (AgentBrowser, 'jabber'),

        # Services
        ('conference', 'text'):         (MucBrowser, 'conference'),
        ('headline', 'rss'):            (AgentBrowser, 'rss'),
        ('headline', 'weather'):        (False, 'weather'),
        ('gateway', 'weather'):         (False, 'weather'),
        ('_jid', 'weather'):            (False, 'weather'),
        ('gateway', 'sip'):             (False, 'sip'),
        ('directory', 'user'):          (None, 'jud'),
        ('pubsub', 'generic'):          (PubSubBrowser, 'pubsub'),
        ('pubsub', 'service'):          (PubSubBrowser, 'pubsub'),
        ('proxy', 'bytestreams'):       (None, 'bytestreams'), # Socks5 FT proxy
        ('headline', 'newmail'):        (ToplevelAgentBrowser, 'mail'),

        # Transports
        ('conference', 'irc'):          (ToplevelAgentBrowser, 'irc'),
        ('_jid', 'irc'):                (False, 'irc'),
105
        ('gateway', 'irc'):             (False, 'irc'),
106 107 108 109 110 111 112 113 114 115 116 117
        ('gateway', 'gadu-gadu'):       (False, 'gadu-gadu'),
        ('_jid', 'gadugadu'):           (False, 'gadu-gadu'),
        ('gateway', 'http-ws'):         (False, 'http-ws'),
        ('gateway', 'icq'):             (False, 'icq'),
        ('_jid', 'icq'):                (False, 'icq'),
        ('gateway', 'sms'):             (False, 'sms'),
        ('_jid', 'sms'):                (False, 'sms'),
        ('gateway', 'smtp'):            (False, 'mail'),
        ('gateway', 'mrim'):            (False, 'mrim'),
        ('_jid', 'mrim'):               (False, 'mrim'),
        ('gateway', 'facebook'):        (False, 'facebook'),
        ('_jid', 'facebook'):           (False, 'facebook'),
Dicson's avatar
Dicson committed
118
        ('gateway', 'tv'):              (False, 'tv'),
119
        ('gateway', 'twitter'):         (False, 'twitter'),
120
    }
121

122
# Category type to "human-readable" description string
123
_cat_to_descr = {
124 125 126
        'other':                _('Others'),
        'gateway':              _('Transports'),
        '_jid':                 _('Transports'),
127
        #conference is a category for listing mostly groupchats in service discovery
128
        'conference':           _('Conference'),
129 130 131
}


132
class CacheDictionary:
133 134 135
    """
    A dictionary that keeps items around for only a specific time.  Lifetime is
    in minutes. Getrefresh specifies whether to refresh when an item is merely
Alexander Krotov's avatar
Alexander Krotov committed
136
    accessed instead of set as well
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
    """

    def __init__(self, lifetime, getrefresh = True):
        self.lifetime = lifetime * 1000 * 60
        self.getrefresh = getrefresh
        self.cache = {}

    class CacheItem:
        """
        An object to store cache items and their timeouts
        """
        def __init__(self, value):
            self.value = value
            self.source = None

        def __call__(self):
            return self.value

    def cleanup(self):
156
        for key in list(self.cache.keys()):
157 158
            item = self.cache[key]
            if item.source:
Yann Leboulanger's avatar
Yann Leboulanger committed
159
                GLib.source_remove(item.source)
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
            del self.cache[key]

    def _expire_timeout(self, key):
        """
        The timeout has expired, remove the object
        """
        if key in self.cache:
            del self.cache[key]
        return False

    def _refresh_timeout(self, key):
        """
        The object was accessed, refresh the timeout
        """
        item = self.cache[key]
        if item.source:
Yann Leboulanger's avatar
Yann Leboulanger committed
176
            GLib.source_remove(item.source)
177
        if self.lifetime:
Yann Leboulanger's avatar
Yann Leboulanger committed
178
            source = GLib.timeout_add_seconds(int(self.lifetime/1000), self._expire_timeout, key)
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
            item.source = source

    def __getitem__(self, key):
        item = self.cache[key]
        if self.getrefresh:
            self._refresh_timeout(key)
        return item()

    def __setitem__(self, key, value):
        item = self.CacheItem(value)
        self.cache[key] = item
        self._refresh_timeout(key)

    def __delitem__(self, key):
        item = self.cache[key]
        if item.source:
Yann Leboulanger's avatar
Yann Leboulanger committed
195
            GLib.source_remove(item.source)
196 197 198 199
        del self.cache[key]

    def __contains__(self, key):
        return key in self.cache
200 201 202

_icon_cache = CacheDictionary(15)

203
def get_agent_address(jid, node = None):
204 205 206 207 208 209 210
    """
    Get an agent's address for displaying in the GUI
    """
    if node:
        return '%s@%s' % (node, str(jid))
    else:
        return str(jid)
211 212

class Closure(object):
213 214 215
    """
    A weak reference to a callback with arguments as an object

216
    Weak references to methods immediately die, even if the object is still
217 218 219 220 221 222 223 224 225 226 227
    alive. Besides a handy way to store a callback, this provides a workaround
    that keeps a reference to the object instead.

    Userargs and removeargs must be tuples.
    """

    def __init__(self, cb, userargs = (), remove = None, removeargs = ()):
        self.userargs = userargs
        self.remove = remove
        self.removeargs = removeargs
        if isinstance(cb, types.MethodType):
228
            self.meth_self = weakref.ref(cb.__self__, self._remove)
229
            self.meth_name = cb.__name__
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
        elif callable(cb):
            self.meth_self = None
            self.cb = weakref.ref(cb, self._remove)
        else:
            raise TypeError('Object is not callable')

    def _remove(self, ref):
        if self.remove:
            self.remove(self, *self.removeargs)

    def __call__(self, *args, **kwargs):
        if self.meth_self:
            obj = self.meth_self()
            cb = getattr(obj, self.meth_name)
        else:
            cb = self.cb()
        args = args + self.userargs
        return cb(*args, **kwargs)
248 249 250


class ServicesCache:
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
    """
    Class that caches our query results. Each connection will have it's own
    ServiceCache instance
    """

    def __init__(self, account):
        self.account = account
        self._items = CacheDictionary(0, getrefresh = False)
        self._info = CacheDictionary(0, getrefresh = False)
        self._subscriptions = CacheDictionary(5, getrefresh=False)
        self._cbs = {}

    def cleanup(self):
        self._items.cleanup()
        self._info.cleanup()

    def _clean_closure(self, cb, type_, addr):
        # A closure died, clean up
        cbkey = (type_, addr)
        try:
            self._cbs[cbkey].remove(cb)
        except KeyError:
            return
        except ValueError:
            return
        # Clean an empty list
        if not self._cbs[cbkey]:
            del self._cbs[cbkey]

280
    def get_icon(self, identities=None, addr=''):
281 282 283
        """
        Return the icon for an agent
        """
284 285
        if identities is None:
            identities = []
286
        # Grab the first identity with an icon
287
        quiet = False
288 289 290 291 292 293 294 295 296 297 298
        for identity in identities:
            try:
                cat, type_ = identity['category'], identity['type']
                info = _agent_type_info[(cat, type_)]
            except KeyError:
                continue
            filename = info[1]
            if filename:
                break
        else:
            # Loop fell through, default to unknown
299 300
            filename = addr.split('.')[0]
            quiet = True
301 302 303 304
        # Use the cache if possible
        if filename in _icon_cache:
            return _icon_cache[filename]
        # Or load it
305 306 307 308 309 310 311 312 313 314
        pix = gtkgui_helpers.get_icon_pixmap('gajim-agent-' + filename, size=32,
            quiet=quiet)
        if pix:
            # Store in cache
            _icon_cache[filename] = pix
            return pix
        if 'jabber' in _icon_cache:
            return _icon_cache['jabber']
        pix = gtkgui_helpers.get_icon_pixmap('gajim-agent-jabber', size=32)
        _icon_cache['jabber'] = pix
315 316
        return pix

317
    def get_browser(self, identities=None, features=None):
318 319 320
        """
        Return the browser class for an agent
        """
321 322 323 324
        if identities is None:
            identities = []
        if features is None:
            features = []
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
        # First pass, we try to find a ToplevelAgentBrowser
        for identity in identities:
            try:
                cat, type_ = identity['category'], identity['type']
                info = _agent_type_info[(cat, type_)]
            except KeyError:
                continue
            browser = info[0]
            if browser and browser == ToplevelAgentBrowser:
                return browser

        # second pass, we haven't found a ToplevelAgentBrowser
        for identity in identities:
            try:
                cat, type_ = identity['category'], identity['type']
                info = _agent_type_info[(cat, type_)]
            except KeyError:
                continue
            browser = info[0]
            if browser:
                return browser
        # NS_BROWSE is deprecated, but we check for it anyways.
        # Some services list it in features and respond to
        # NS_DISCO_ITEMS anyways.
Alexander Krotov's avatar
Alexander Krotov committed
349
        # Allow browsing for unknown types as well.
350
        if (not features and not identities) or \
351
        nbxmpp.NS_DISCO_ITEMS in features or nbxmpp.NS_BROWSE in features:
352 353 354
            return ToplevelAgentBrowser
        return None

355
    def get_info(self, jid, node, cb, force=False, nofetch=False, args=()):
356 357 358 359 360
        """
        Get info for an agent
        """
        addr = get_agent_address(jid, node)
        # Check the cache
361
        if addr in self._info and not force:
362 363 364 365 366 367 368 369
            args = self._info[addr] + args
            cb(jid, node, *args)
            return
        if nofetch:
            return

        # Create a closure object
        cbkey = ('info', addr)
370 371
        cb = Closure(cb, userargs=args, remove=self._clean_closure,
            removeargs=cbkey)
372 373 374 375 376
        # Are we already fetching this?
        if cbkey in self._cbs:
            self._cbs[cbkey].append(cb)
        else:
            self._cbs[cbkey] = [cb]
377 378 379
            con = app.connections[self.account]
            con.get_module('Discovery').disco_info(
                jid, node, self._disco_info_received, self._disco_info_error)
380

381
    def get_items(self, jid, node, cb, force=False, nofetch=False, args=()):
382 383 384 385 386
        """
        Get a list of items in an agent
        """
        addr = get_agent_address(jid, node)
        # Check the cache
387
        if addr in self._items and not force:
388 389 390 391 392 393 394 395
            args = (self._items[addr],) + args
            cb(jid, node, *args)
            return
        if nofetch:
            return

        # Create a closure object
        cbkey = ('items', addr)
396
        cb = Closure(cb, userargs=args, remove=self._clean_closure,
397
                     removeargs=cbkey)
398 399 400 401 402
        # Are we already fetching this?
        if cbkey in self._cbs:
            self._cbs[cbkey].append(cb)
        else:
            self._cbs[cbkey] = [cb]
403 404 405
            con = app.connections[self.account]
            con.get_module('Discovery').disco_items(
                jid, node, self._disco_items_received, self._disco_items_error)
406

407
    def _disco_info_received(self, from_, identities, features, data, node):
408 409 410 411
        """
        Callback for when we receive an agent's info
        array is (agent, node, identities, features, data)
        """
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
        self._on_agent_info(str(from_), node, identities, features, data)

    def _disco_info_error(self, from_, error):
        """
        Callback for when a query fails. Even after the browse and agents
        namespaces
        """
        addr = get_agent_address(from_)

        # Call callbacks
        cbkey = ('info', addr)
        if cbkey in self._cbs:
            for cb in self._cbs[cbkey]:
                cb(str(from_), '', 0, 0, 0)
            # clean_closure may have beaten us to it
            if cbkey in self._cbs:
                del self._cbs[cbkey]
429

430 431
    def _on_agent_info(self, fjid, node, identities, features, data):
        addr = get_agent_address(fjid, node)
432 433

        # Store in cache
434
        self._info[addr] = (identities, features, data)
435 436 437 438 439

        # Call callbacks
        cbkey = ('info', addr)
        if cbkey in self._cbs:
            for cb in self._cbs[cbkey]:
440
                cb(fjid, node, identities, features, data)
441 442 443 444
            # clean_closure may have beaten us to it
            if cbkey in self._cbs:
                del self._cbs[cbkey]

445
    def _disco_items_received(self, from_, node, items):
446 447 448 449
        """
        Callback for when we receive an agent's items
        array is (agent, node, items)
        """
450
        addr = get_agent_address(from_, node)
451 452

        # Store in cache
453
        self._items[addr] = items
454 455 456 457 458

        # Call callbacks
        cbkey = ('items', addr)
        if cbkey in self._cbs:
            for cb in self._cbs[cbkey]:
459
                cb(str(from_), node, items)
460 461 462 463
            # clean_closure may have beaten us to it
            if cbkey in self._cbs:
                del self._cbs[cbkey]

464
    def _disco_items_error(self, from_, error):
465 466 467 468
        """
        Callback for when a query fails. Even after the browse and agents
        namespaces
        """
469
        addr = get_agent_address(from_)
470 471 472 473 474

        # Call callbacks
        cbkey = ('items', addr)
        if cbkey in self._cbs:
            for cb in self._cbs[cbkey]:
475
                cb(str(from_), '', 0)
476 477 478
            # clean_closure may have beaten us to it
            if cbkey in self._cbs:
                del self._cbs[cbkey]
479

480

481
# object is needed so that @property works
482
class ServiceDiscoveryWindow(object):
483 484 485 486
    """
    Class that represents the Services Discovery window
    """

487 488
    def __init__(self, account, jid='', node='', address_entry=False,
    parent=None, initial_identities=None):
489
        self._account = account
490 491
        self.parent = parent
        if not jid:
492
            jid = app.config.get_per('accounts', account, 'hostname')
493 494 495 496 497 498 499
            node = ''

        self.jid = None
        self.browser = None
        self.children = []
        self.dying = False
        self.node = None
500
        self.reloading = False
501 502

        # Check connection
503
        if app.connections[account].connected < 2:
504
            ErrorDialog(_('You are not connected to the server'),
505
_('Without a connection, you can not browse available services'))
506
            raise RuntimeError('You must be connected to browse services')
507 508 509

        # Get a ServicesCache object.
        try:
510
            self.cache = app.connections[account].services_cache
511 512
        except AttributeError:
            self.cache = ServicesCache(account)
513
            app.connections[account].services_cache = self.cache
514

515
        if initial_identities:
516
            self.cache._on_agent_info(jid, node, initial_identities, [], None)
517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534
        self.xml = gtkgui_helpers.get_gtk_builder('service_discovery_window.ui')
        self.window = self.xml.get_object('service_discovery_window')
        self.services_treeview = self.xml.get_object('services_treeview')
        self.model = None
        # This is more reliable than the cursor-changed signal.
        selection = self.services_treeview.get_selection()
        selection.connect_after('changed',
                self.on_services_treeview_selection_changed)
        self.services_scrollwin = self.xml.get_object('services_scrollwin')
        self.progressbar = self.xml.get_object('services_progressbar')
        self.banner = self.xml.get_object('banner_agent_label')
        self.banner_icon = self.xml.get_object('banner_agent_icon')
        self.banner_eventbox = self.xml.get_object('banner_agent_eventbox')
        self.style_event_id = 0
        self.banner.realize()
        self.action_buttonbox = self.xml.get_object('action_buttonbox')

        # Address combobox
535
        self.address_comboboxtext = None
536 537
        address_table = self.xml.get_object('address_table')
        if address_entry:
538 539 540 541
            self.address_comboboxtext = self.xml.get_object(
                'address_comboboxtext')
            self.address_comboboxtext_entry = self.xml.get_object(
                'address_entry')
542

543
            self.latest_addresses = app.config.get(
544
                'latest_disco_addresses').split()
545 546 547 548 549 550
            if jid in self.latest_addresses:
                self.latest_addresses.remove(jid)
            self.latest_addresses.insert(0, jid)
            if len(self.latest_addresses) > 10:
                self.latest_addresses = self.latest_addresses[0:10]
            for j in self.latest_addresses:
551 552
                self.address_comboboxtext.append_text(j)
            self.address_comboboxtext.get_child().set_text(jid)
553 554 555 556 557
        else:
            # Don't show it at all if we didn't ask for it
            address_table.set_no_show_all(True)
            address_table.hide()

558 559 560
        accel_group = Gtk.AccelGroup()
        keyval, mod = Gtk.accelerator_parse('<Control>r')
        accel_group.connect(keyval, mod, Gtk.AccelFlags.VISIBLE,
561 562 563
            self.accel_group_func)
        self.window.add_accel_group(accel_group)

564 565 566 567 568 569
        self._initial_state()
        self.xml.connect_signals(self)
        self.travel(jid, node)
        self.window.show_all()

    @property
570 571
    def account(self):
        return self._account
572

573 574 575
    @account.setter
    def account(self, value):
        self._account = value
576 577 578 579
        self.cache.account = value
        if self.browser:
            self.browser.account = value

580 581 582 583
    def on_key_press_event(self, widget, event):
        if event.keyval == Gdk.KEY_Escape:
            self.window.destroy()

584
    def accel_group_func(self, accel_group, acceleratable, keyval, modifier):
585
        if (modifier & Gdk.ModifierType.CONTROL_MASK) and (keyval == Gdk.KEY_r):
586 587
            self.reload()

588 589 590 591 592 593 594 595 596 597 598 599 600
    def _initial_state(self):
        """
        Set some initial state on the window. Separated in a method because it's
        handy to use within browser's cleanup method
        """
        self.progressbar.hide()
        title_text = _('Service Discovery using account %s') % self.account
        self.window.set_title(title_text)
        self._set_window_banner_text(_('Service Discovery'))
        self.banner_icon.clear()
        self.banner_icon.hide() # Just clearing it doesn't work

    def _set_window_banner_text(self, text, text_after = None):
601 602 603
        theme = app.config.get('roster_theme')
        bannerfont = app.config.get_per('themes', theme, 'bannerfont')
        bannerfontattrs = app.config.get_per('themes', theme,
604 605 606
                'bannerfontattrs')

        if bannerfont:
607
            font = Pango.FontDescription(bannerfont)
608
        else:
609
            font = Pango.FontDescription('Normal')
610 611 612
        if bannerfontattrs:
            # B is attribute set by default
            if 'B' in bannerfontattrs:
613
                font.set_weight(Pango.Weight.HEAVY)
614
            if 'I' in bannerfontattrs:
615
                font.set_style(Pango.Style.ITALIC)
616 617 618 619 620 621 622 623 624

        font_attrs = 'font_desc="%s"' % font.to_string()
        font_size = font.get_size()

        # in case there is no font specified we use x-large font size
        if font_size == 0:
            font_attrs = '%s size="large"' % font_attrs
        markup = '<span %s>%s</span>' % (font_attrs, text)
        if text_after:
625
            font.set_weight(Pango.Weight.NORMAL)
626
            markup = '%s\n<span font_desc="%s" size="small">%s</span>' % \
Dicson's avatar
Dicson committed
627
                (markup, font.to_string(), text_after)
628 629 630 631 632 633 634 635 636 637 638 639 640 641
        self.banner.set_markup(markup)

    def destroy(self, chain = False):
        """
        Close the browser. This can optionally close its children and propagate
        to the parent. This should happen on actions like register, or join to
        kill off the entire browser chain
        """
        if self.dying:
            return
        self.dying = True

        # self.browser._get_agent_address() would break when no browser.
        addr = get_agent_address(self.jid, self.node)
642 643
        if addr in app.interface.instances[self.account]['disco']:
            del app.interface.instances[self.account]['disco'][addr]
644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664

        if self.browser:
            self.window.hide()
            self.browser.cleanup()
            self.browser = None
        self.window.destroy()

        for child in self.children[:]:
            child.parent = None
            if chain:
                child.destroy(chain = chain)
                self.children.remove(child)
        if self.parent:
            if self in self.parent.children:
                self.parent.children.remove(self)
            if chain and not self.parent.children:
                self.parent.destroy(chain = chain)
                self.parent = None
        else:
            self.cache.cleanup()

665 666 667 668 669 670
    def reload(self):
        if not self.jid:
            return
        self.reloading = True
        self.travel(self.jid, self.node)

671 672 673 674 675 676 677 678 679 680
    def travel(self, jid, node):
        """
        Travel to an agent within the current services window
        """
        if self.browser:
            self.browser.cleanup()
            self.browser = None
        # Update the window list
        if self.jid:
            old_addr = get_agent_address(self.jid, self.node)
681 682
            if old_addr in app.interface.instances[self.account]['disco']:
                del app.interface.instances[self.account]['disco'][old_addr]
683
        addr = get_agent_address(jid, node)
684
        app.interface.instances[self.account]['disco'][addr] = self
685 686 687
        # We need to store these, self.browser is not always available.
        self.jid = jid
        self.node = node
688
        self.cache.get_info(jid, node, self._travel, force=self.reloading)
689 690 691 692 693 694 695 696

    def _travel(self, jid, node, identities, features, data):
        """
        Continuation of travel
        """
        if self.dying or jid != self.jid or node != self.node:
            return
        if not identities:
697
            if not self.address_comboboxtext:
698 699
                # We can't travel anywhere else.
                self.destroy()
700
            ErrorDialog(_('The service could not be found'),
701 702 703
                _('There is no service at the address you entered, or it is '
                'not responding. Check the address and try again.'),
                transient_for=self.window)
704 705 706
            return
        klass = self.cache.get_browser(identities, features)
        if not klass:
707
            ErrorDialog(_('The service is not browsable'),
708 709
                _('This type of service does not contain any items to browse.'),
                transient_for=self.window)
710 711 712 713 714
            return
        elif klass is None:
            klass = AgentBrowser
        self.browser = klass(self.account, jid, node)
        self.browser.prepare_window(self)
715 716
        self.browser.browse(force=self.reloading)
        self.reloading = False
717 718 719 720 721 722

    def open(self, jid, node):
        """
        Open an agent. By default, this happens in a new window
        """
        try:
723
            win = app.interface.instances[self.account]['disco']\
724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741
                    [get_agent_address(jid, node)]
            win.window.present()
            return
        except KeyError:
            pass
        try:
            win = ServiceDiscoveryWindow(self.account, jid, node, parent=self)
        except RuntimeError:
            # Disconnected, perhaps
            return
        self.children.append(win)

    def on_service_discovery_window_destroy(self, widget):
        self.destroy()

    def on_close_button_clicked(self, widget):
        self.destroy()

742 743
    def on_address_comboboxtext_changed(self, widget):
        if self.address_comboboxtext.get_active() != -1:
744
            # user selected one of the entries so do auto-visit
745
            jid = self.address_comboboxtext_entry.get_text()
746 747
            try:
                jid = helpers.parse_jid(jid)
748
            except helpers.InvalidFormat as s:
749
                pritext = _('Invalid Server Name')
750
                ErrorDialog(pritext, str(s))
751 752 753 754
                return
            self.travel(jid, '')

    def on_go_button_clicked(self, widget):
755
        jid = self.address_comboboxtext_entry.get_text()
756 757
        try:
            jid = helpers.parse_jid(jid)
758
        except helpers.InvalidFormat as s:
759
            pritext = _('Invalid Server Name')
760
            ErrorDialog(pritext, str(s),
761
                transient_for=self.window)
762 763 764 765 766 767 768 769
            return
        if jid == self.jid: # jid has not changed
            return
        if jid in self.latest_addresses:
            self.latest_addresses.remove(jid)
        self.latest_addresses.insert(0, jid)
        if len(self.latest_addresses) > 10:
            self.latest_addresses = self.latest_addresses[0:10]
770
        self.address_comboboxtext.get_model().clear()
771
        for j in self.latest_addresses:
772
            self.address_comboboxtext.append_text(j)
773
        app.config.set('latest_disco_addresses',
774 775 776 777 778 779 780 781 782 783
                ' '.join(self.latest_addresses))
        self.travel(jid, '')

    def on_services_treeview_row_activated(self, widget, path, col = 0):
        if self.browser:
            self.browser.default_action()

    def on_services_treeview_selection_changed(self, widget):
        if self.browser:
            self.browser.update_actions()
784

785 786 787 788
    def _on_entry_key_press_event(self, widget, event):
        if event.keyval == Gdk.KEY_Return or event.keyval == Gdk.KEY_KP_Enter:
            self.on_go_button_clicked(widget)

789 790

class AgentBrowser:
791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816
    """
    Class that deals with browsing agents and appearance of the browser window.
    This class and subclasses should basically be treated as "part" of the
    ServiceDiscoveryWindow class, but had to be separated because this part is
    dynamic
    """

    def __init__(self, account, jid, node):
        self.account = account
        self.jid = jid
        self.node = node
        self._total_items = 0
        self.browse_button = None
        # This is for some timeout callbacks
        self.active = False

    def _get_agent_address(self):
        """
        Get the agent's address for displaying in the GUI
        """
        return get_agent_address(self.jid, self.node)

    def _set_initial_title(self):
        """
        Set the initial window title based on agent address
        """
Dicson's avatar
Dicson committed
817
        self.window.window.set_title(_('Browsing %(address)s using account '
818 819 820 821 822 823 824 825 826 827 828
                '%(account)s') % {'address': self._get_agent_address(),
                'account': self.account})
        self.window._set_window_banner_text(self._get_agent_address())

    def _create_treemodel(self):
        """
        Create the treemodel for the services treeview. When subclassing, note
        that the first two columns should ALWAYS be of type string and contain
        the JID and node of the item respectively
        """
        # JID, node, name, address
829 830
        self.model = Gtk.ListStore(str, str, str, str)
        self.model.set_sort_column_id(3, Gtk.SortType.ASCENDING)
831 832
        self.window.services_treeview.set_model(self.model)
        # Name column
833 834
        col = Gtk.TreeViewColumn(_('Name'))
        renderer = Gtk.CellRendererText()
835 836
        col.pack_start(renderer, True)
        col.add_attribute(renderer, 'text', 2)
837 838 839
        self.window.services_treeview.insert_column(col, -1)
        col.set_resizable(True)
        # Address column
840 841
        col = Gtk.TreeViewColumn(_('JID'))
        renderer = Gtk.CellRendererText()
842 843
        col.pack_start(renderer, True)
        col.add_attribute(renderer, 'text', 3)
844 845 846 847 848 849 850 851 852 853 854 855 856 857 858
        self.window.services_treeview.insert_column(col, -1)
        col.set_resizable(True)
        self.window.services_treeview.set_headers_visible(True)

    def _clean_treemodel(self):
        self.model.clear()
        for col in self.window.services_treeview.get_columns():
            self.window.services_treeview.remove_column(col)
        self.window.services_treeview.set_headers_visible(False)

    def _add_actions(self):
        """
        Add the action buttons to the buttonbox for actions the browser can
        perform
        """
859
        self.browse_button = Gtk.Button()
860 861
        self.browse_button.connect('clicked', self.on_browse_button_clicked)
        self.window.action_buttonbox.add(self.browse_button)
862
        image = Gtk.Image.new_from_icon_name("document-open", Gtk.IconSize.BUTTON)
863
        self.browse_button.set_image(image)
864
        label = _('Browse')
865
        self.browse_button.set_label(label)
866 867 868 869 870 871 872 873 874 875 876 877 878 879 880
        self.browse_button.show_all()

    def _clean_actions(self):
        """
        Remove the action buttons specific to this browser
        """
        if self.browse_button:
            self.browse_button.destroy()
            self.browse_button = None

    def _set_title(self, jid, node, identities, features, data):
        """
        Set the window title based on agent info
        """
        # Set the banner and window title
881
        name = ''
Dicson's avatar
Dicson committed
882
        if len(identities) > 1:
883 884 885 886 887 888 889
            # Check if an identity with server category is present
            for i, _identity in enumerate(identities):
                if _identity['category'] == 'server' and 'name' in _identity:
                    name = _identity['name']
                    break
                elif 'name' in identities[0]:
                    name = identities[0]['name']
Dicson's avatar
Dicson committed
890 891

        if name:
Yann Leboulanger's avatar
Yann Leboulanger committed
892
            self.window._set_window_banner_text(self._get_agent_address(), name)
893 894

        # Add an icon to the banner.
895
        pix = self.cache.get_icon(identities, addr=self._get_agent_address())
896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919
        self.window.banner_icon.set_from_pixbuf(pix)
        self.window.banner_icon.show()

    def _clean_title(self):
        # Everything done here is done in window._initial_state
        # This is for subclasses.
        pass

    def prepare_window(self, window):
        """
        Prepare the service discovery window. Called when a browser is hooked up
        with a ServiceDiscoveryWindow instance
        """
        self.window = window
        self.cache = window.cache

        self._set_initial_title()
        self._create_treemodel()
        self._add_actions()

        # This is a hack. The buttonbox apparently doesn't care about pack_start
        # or pack_end, so we repack the close button here to make sure it's last
        close_button = self.window.xml.get_object('close_button')
        self.window.action_buttonbox.remove(close_button)
920
        self.window.action_buttonbox.pack_end(close_button, True, True, 0)
921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953
        close_button.show_all()

        self.update_actions()

        self.active = True
        self.cache.get_info(self.jid, self.node, self._set_title)

    def cleanup(self):
        """
        Cleanup when the window intends to switch browsers
        """
        self.active = False

        self._clean_actions()
        self._clean_treemodel()
        self._clean_title()

        self.window._initial_state()

    def update_theme(self):
        """
        Called when the default theme is changed
        """
        pass

    def on_browse_button_clicked(self, widget = None):
        """
        When we want to browse an agent: open a new services window with a
        browser for the agent type
        """
        model, iter_ = self.window.services_treeview.get_selection().get_selected()
        if not iter_:
            return
954
        jid = model[iter_][0]
955
        if jid:
956
            node = model[iter_][1]
957 958 959 960 961 962 963 964 965 966 967
            self.window.open(jid, node)

    def update_actions(self):
        """
        When we select a row: activate action buttons based on the agent's info
        """
        if self.browse_button:
            self.browse_button.set_sensitive(False)
        model, iter_ = self.window.services_treeview.get_selection().get_selected()
        if not iter_:
            return
968 969
        jid = model[iter_][0]
        node = model[iter_][1]
970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990
        if jid:
            self.cache.get_info(jid, node, self._update_actions, nofetch = True)

    def _update_actions(self, jid, node, identities, features, data):
        """
        Continuation of update_actions
        """
        if not identities or not self.browse_button:
            return
        klass = self.cache.get_browser(identities, features)
        if klass:
            self.browse_button.set_sensitive(True)

    def default_action(self):
        """
        When we double-click a row: perform the default action on the selected
        item
        """
        model, iter_ = self.window.services_treeview.get_selection().get_selected()
        if not iter_:
            return
991 992
        jid = model[iter_][0]
        node = model[iter_][1]
993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005
        if jid:
            self.cache.get_info(jid, node, self._default_action, nofetch = True)

    def _default_action(self, jid, node, identities, features, data):
        """
        Continuation of default_action
        """
        if self.cache.get_browser(identities, features):
            # Browse if we can
            self.on_browse_button_clicked()
            return True
        return False

1006
    def browse(self, force=False):
1007 1008 1009 1010 1011 1012
        """
        Fill the treeview with agents, fetching the info if necessary
        """
        self.model.clear()
        self._total_items = self._progress = 0
        self.window.progressbar.show()
Yann Leboulanger's avatar
Yann Leboulanger committed
1013
        self._pulse_timeout = GLib.timeout_add(250, self._pulse_timeout_cb)
1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030
        self.cache.get_items(self.jid, self.node, self._agent_items,
                force=force, args=(force,))

    def _pulse_timeout_cb(self, *args):
        """
        Simple callback to keep the progressbar pulsing
        """
        if not self.active:
            return False
        self.window.progressbar.pulse()
        return True

    def _find_item(self, jid, node):
        """
        Check if an item is already in the treeview. Return an iter to it if so,
        None otherwise
        """
1031
        iter_ = self.model.get_iter_first()
1032
        while iter_:
1033 1034
            cjid = self.model.get_value(iter_, 0)
            cnode = self.model.get_value(iter_, 1)
1035 1036 1037 1038 1039 1040 1041
            if jid == cjid and node == cnode:
                break
            iter_ = self.model.iter_next(iter_)
        if iter_:
            return iter_
        return None

1042 1043 1044
    def add_self_line(self):
        pass

1045 1046 1047 1048 1049
    def _agent_items(self, jid, node, items, force):
        """
        Callback for when we receive a list of agent items
        """
        self.model.clear()
1050
        self.add_self_line()
1051
        self._total_items = 0
Yann Leboulanger's avatar
Yann Leboulanger committed
1052
        GLib.source_remove(self._pulse_timeout)
1053 1054 1055
        self.window.progressbar.hide()
        # The server returned an error
        if items == 0:
1056
            if not self.window.address_comboboxtext:
1057 1058
                # We can't travel anywhere else.
                self.window.destroy()
1059
            ErrorDialog(_('The service is not browsable'),
1060 1061
                _('This service does not contain any items to browse.'),
                transient_for=self.window.window)
1062 1063
            return
        # We got a list of items
1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084
        def fill_partial_rows(items):
            '''Generator to fill the listmodel of a treeview progressively.'''
            self.window.services_treeview.freeze_child_notify()
            for item in items:
                if self.window.dying:
                    yield False
                jid_ = item['jid']
                node_ = item.get('node', '')
                # If such an item is already here: don't add it
                if self._find_item(jid_, node_):
                    continue
                self._total_items += 1
                self._add_item(jid_, node_, node, item, force)
                if (self._total_items % 10) == 0:
                    self.window.services_treeview.thaw_child_notify()
                    yield True
                    self.window.services_treeview.freeze_child_notify()
            self.window.services_treeview.thaw_child_notify()
            #stop idle_add()
            yield False
        loader = fill_partial_rows(items)
1085
        GLib.idle_add(next, loader)
1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131

    def _agent_info(self, jid, node, identities, features, data):
        """
        Callback for when we receive info about an agent's item
        """
        iter_ = self._find_item(jid, node)
        if not iter_:
            # Not in the treeview, stop
            return
        if identities == 0:
            # The server returned an error
            self._update_error(iter_, jid, node)
        else:
            # We got our info
            self._update_info(iter_, jid, node, identities, features, data)
        self.update_actions()

    def _add_item(self, jid, node, parent_node, item, force):
        """
        Called when an item should be added to the model. The result of a
        disco#items query
        """
        self.model.append((jid, node, item.get('name', ''),
                get_agent_address(jid, node)))
        self.cache.get_info(jid, node, self._agent_info, force = force)

    def _update_item(self, iter_, jid, node, item):
        """
        Called when an item should be updated in the model. The result of a
        disco#items query
        """
        if 'name' in item:
            self.model[iter_][2] = item['name']

    def _update_info(self, iter_, jid, node, identities, features, data):
        """
        Called when an item should be updated in the model with further info.
        The result of a disco#info query
        """
        name = identities[0].get('name', '')
        if name:
            self.model[iter_][2] = name

    def _update_error(self, iter_, jid, node):
        '''Called when a disco#info query failed for an item.'''
        pass
1132 1133 1134


class ToplevelAgentBrowser(AgentBrowser):
1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152
    """
    This browser is used at the top level of a jabber server to browse services
    such as transports, conference servers, etc
    """

    def __init__(self, *args):
        AgentBrowser.__init__(self, *args)
        self._progressbar_sourceid = None
        self._renderer = None
        self._progress = 0
        self.register_button = None
        self.join_button = None
        self.execute_button = None
        self.search_button = None
        # Keep track of our treeview signals
        self._view_signals = []
        self._scroll_signal = None

1153 1154 1155 1156 1157
    def add_self_line(self):
        addr = get_agent_address(self.jid, self.node)
        descr = "<b>%s</b>" % addr
        # Guess which kind of service this is
        identities = []
1158
        type_ = app.get_transport_name_from_jid(self.jid,
1159 1160 1161 1162 1163
            use_config_setting=False)
        if type_:
            identity = {'category': '_jid', 'type': type_}
            identities.append(identity)
        # Set the pixmap for the row
1164
        pix = self.cache.get_icon(identities, addr=addr)
1165
        self.model.append(None, (self.jid, self.node, pix, descr, LABELS[1]))
1166 1167 1168
        # Grab info on the service
        self.cache.get_info(self.jid, self.node, self._agent_info, force=False)

1169
    def _pixbuf_renderer_data_func(self, col, cell, model, iter_, data=None):
1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180
        """
        Callback for setting the pixbuf renderer's properties
        """
        jid = model.get_value(iter_, 0)
        if jid:
            pix = model.get_value(iter_, 2)
            cell.set_property('visible', True)
            cell.set_property('pixbuf', pix)
        else:
            cell.set_property('visible', False)

1181
    def _text_renderer_data_func(self, col, cell, model, iter_, data=None):
1182 1183 1184 1185 1186 1187 1188 1189 1190
        """
        Callback for setting the text renderer's properties
        """
        jid = model.get_value(iter_, 0)
        markup = model.get_value(iter_, 3)
        state = model.get_value(iter_, 4)
        cell.set_property('markup', markup)
        if jid:
            cell.set_property('cell_background_set', False)
1191 1192
            if state is not None:
                # fetching or error
1193 1194
                cell.set_property('foreground_set', True)
            else:
Alexander Krotov's avatar
Alexander Krotov committed
1195
                # Normal/success
1196 1197
                cell.set_property('foreground_set', False)
        else:
1198 1199
            theme = app.config.get('roster_theme')
            bgcolor = app.config.get_per('themes', theme, 'groupbgcolor')
1200 1201 1202 1203
            if bgcolor:
                cell.set_property('cell_background_set', True)
            cell.set_property('foreground_set', False)

1204
    def _treemodel_sort_func(self, model, iter1, iter2, data=None):
1205 1206 1207 1208
        """
        Sort function for our treemode
        """
        # Compare state
1209 1210
        state1 = model.get_value(iter1, 4)
        state2 = model.get_value(iter2, 4)
1211
        if state1 is not None:
1212
            return 1
1213
        if state2 is not None:
1214 1215 1216 1217 1218 1219 1220 1221 1222
            return -1
        descr1 = model.get_value(iter1, 3)
        descr2 = model.get_value(iter2, 3)
        # Compare strings
        if descr1 > descr2:
            return 1
        if descr1 < descr2:
            return -1
        return 0
1223 1224 1225

    def _create_treemodel(self):
        # JID, node, icon, description, state
1226
        # state is None on success or has a string
1227
        # from LABELS on error or while fetching
1228
        view = self.window.services_treeview
1229
        self.model = Gtk.TreeStore(str, str, GdkPixbuf.Pixbuf, str, str)
1230
        self.model.set_sort_func(4, self._treemodel_sort_func)
1231
        self.model.set_sort_column_id(4, Gtk.SortType.ASCENDING)
1232 1233
        view.set_model(self.model)

1234
        col = Gtk.TreeViewColumn()
1235
        # Icon Renderer
1236
        renderer = Gtk.CellRendererPixbuf()
1237
        renderer.set_property('xpad', 6)
1238
        col.pack_start(renderer, False)
1239 1240
        col.set_cell_data_func(renderer, self._pixbuf_renderer_data_func)
        # Text Renderer
1241
        renderer = Gtk.CellRendererText()
1242
        col.pack_start(renderer, True)
1243 1244 1245 1246 1247 1248
        col.set_cell_data_func(renderer, self._text_renderer_data_func)
        renderer.set_property('foreground', 'dark gray')
        # Save this so we can go along with theme changes
        self._renderer = renderer
        self.update_theme()

1249
        view.set_tooltip_column(4)
1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266
        view.insert_column(col, -1)
        col.set_resizable(True)

    def _clean_treemodel(self):
        # Disconnect signals
        view = self.window.services_treeview
        for sig in self._view_signals:
            view.disconnect(sig)
        self._view_signals = []
        if self._scroll_signal:
            scrollwin = self.window.services_scrollwin
            scrollwin.disconnect(self._scroll_signal)
            self._scroll_signal = None
        AgentBrowser._clean_treemodel(self)

    def _add_actions(self):
        AgentBrowser._add_actions(self)
1267
        self.execute_button = Gtk.Button()
1268 1269
        self.execute_button.connect('clicked', self.on_execute_button_clicked)
        self.window.action_buttonbox.add(self.execute_button)
1270
        image = Gtk.Image.new_from_icon_name("system-run", Gtk.IconSize.BUTTON)
1271
        self.execute_button.set_image(image)
1272
        label = _('Execute Command')
1273
        self.execute_button.set_label(label)
1274 1275
        self.execute_button.show_all()

1276
        self.register_button = Gtk.Button(label=_("Re_gister"),
1277 1278 1279 1280 1281
                use_underline=True)
        self.register_button.connect('clicked', self.on_register_button_clicked)
        self.window.action_buttonbox.add(self.register_button)
        self.register_button.show_all()

1282
        self.join_button = Gtk.Button()
1283 1284
        self.join_button.connect('clicked', self.on_join_button_clicked)
        self.window.action_buttonbox.add(self.join_button)
1285
        label = _('Join')
1286
        self.join_button.set_label(label)
1287 1288
        self.join_button.show_all()

1289
        self.search_button = Gtk.Button()
1290 1291
        self.search_button.connect('clicked', self.on_search_button_clicked)
        self.window.action_buttonbox.add(self.search_button)
1292
        image = Gtk.Image.new_from_icon_name("edit-find", Gtk.IconSize.BUTTON)
1293
        self.search_button.set_image(image)
1294
        label = _('Search')
1295
        self.search_button.set_label(label)
1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319
        self.search_button.show_all()

    def _clean_actions(self):
        if self.execute_button:
            self.execute_button.destroy()
            self.execute_button = None
        if self.register_button:
            self.register_button.destroy()
            self.register_button = None
        if self.join_button:
            self.join_button.destroy()
            self.join_button = None
        if self.search_button:
            self.search_button.destroy()
            self.search_button = None
        AgentBrowser._clean_actions(self)

    def on_search_button_clicked(self, widget = None):
        """
        When we want to search something: open search window
        """
        model, iter_ = self.window.services_treeview.get_selection().get_selected()
        if not iter_:
            return
1320
        service = model[iter_][0]
1321 1322
        if service in app.interface.instances[self.account]['search']:
            app.interface.instances[self.account]['search'][service].window.\
1323 1324
                    present()
        else:
1325
            app.interface.instances[self.account]['search'][service] = \
1326 1327 1328 1329 1330 1331
                    search_window.SearchWindow(self.account, service)

    def cleanup(self):
        AgentBrowser.cleanup(self)

    def update_theme(self):
1332 1333
        theme = app.config.get('roster_theme')
        bgcolor = app.config.get_per('themes', theme, 'groupbgcolor')
1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344
        if bgcolor:
            self._renderer.set_property('cell-background', bgcolor)
        self.window.services_treeview.queue_draw()

    def on_execute_button_clicked(self, widget=None):
        """
        When we want to execute a command: open adhoc command window
        """
        model, iter_ = self.window.services_treeview.get_selection().get_selected()
        if not iter_:
            return
1345 1346
        service = model[iter_][0]
        node = model[iter_][1]
1347 1348 1349 1350 1351 1352 1353 1354 1355 1356
        adhoc_commands.CommandWindow(self.account, service, commandnode=node)

    def on_register_button_clicked(self, widget = None):
        """
        When we want to register an agent: request information about registering
        with the agent and close the window
        """
        model, iter_ = self.window.services_treeview.get_selection().get_selected()
        if not iter_:
            return
1357
        jid = model[iter_][0]
1358
        if jid:
1359 1360
            ServiceRegistration(self.account, jid)
            self.window.destroy(chain=True)
1361 1362 1363 1364 1365 1366 1367 1368 1369

    def on_join_button_clicked(self, widget):
        """
        When we want to join an IRC room or create a new MUC room: Opens the
        join_groupchat_window
        """
        model, iter_ = self.window.services_treeview.get_selection().get_selected()
        if not iter_:
            return
1370
        service = model[iter_][0]
1371 1372
        app.interface.join_gc_minimal(self.account, service,
            transient_for=self.window.window)
1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390

    def update_actions(self):
        if self.execute_button:
            self.execute_button.set_sensitive(False)
        if self.register_button:
            self.register_button.set_sensitive(False)
        if self.browse_button:
            self.browse_button.set_sensitive(False)
        if self.join_button:
            self.join_button.set_sensitive(False)
        if self.search_button:
            self.search_button.set_sensitive(False)
        model, iter_ = self.window.services_treeview.get_selection().get_selected()
        if not iter_:
            return
        if not model[iter_][0]:
            # We're on a category row
            return
1391
        if model[iter_][4] is not None:
1392 1393 1394 1395 1396 1397
            # We don't have the info (yet)
            # It's either unknown or a transport, register button should be active
            if self.register_button:
                self.register_button.set_sensitive(True)
            # Guess what kind of service we're dealing with
            if self.browse_button:
1398
                jid = model[iter_][0]
1399
                type_ = app.get_transport_name_from_jid(jid,
1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414
                                        use_config_setting = False)
                if type_:
                    identity = {'category': '_jid', 'type': type_}
                    klass = self.cache.get_browser([identity])
                    if klass:
                        self.browse_button.set_sensitive(True)
                else:
                    # We couldn't guess
                    self.browse_button.set_sensitive(True)
        else:
            # Normal case, we have info
            AgentBrowser.update_actions(self)

    def _update_actions(self, jid, node, identities, features, data):
        AgentBrowser._update_actions(self, jid, node, identities, features, data)
1415
        if self.execute_button and nbxmpp.NS_COMMANDS in features:
1416
            self.execute_button.set_sensitive(True)
1417
        if self.search_button and nbxmpp.NS_SEARCH in features:
1418
            self.search_button.set_sensitive(True)
1419
        # Don't authorize to register with a server via disco
1420 1421
        if self.register_button and nbxmpp.NS_REGISTER in features and \
        jid != self.jid:
1422 1423
            # We can register this agent
            registered_transports = []
1424
            jid_list = app.contacts.get_jid_list(self.account)
1425
            for jid_ in jid_list:
1426
                contact = app.contacts.get_first_contact_from_jid(
1427
                        self.account, jid_)
1428
                if _('Transports') in contact.groups:
1429 1430
                    registered_transports.append(jid_)
            registered_transports.append(self.jid)
1431 1432 1433 1434 1435
            if jid in registered_transports:
                self.register_button.set_label(_('_Edit'))
            else:
                self.register_button.set_label(_('Re_gister'))
            self.register_button.set_sensitive(True)
1436
        if self.join_button and nbxmpp.NS_MUC in features:
1437 1438 1439 1440 1441
            self.join_button.set_sensitive(True)

    def _default_action(self, jid, node, identities, features, data):
        if AgentBrowser._default_action(self, jid, node, identities, features, data):
            return True
1442
        if nbxmpp.NS_REGISTER in features:
1443 1444 1445 1446 1447
            # Register if we can't browse
            self.on_register_button_clicked()
            return True
        return False

1448
    def browse(self, force=False):
1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461
        self._progress = 0
        AgentBrowser.browse(self, force = force)

    def _expand_all(self):
        """
        Expand all items in the treeview
        """
        # GTK apparently screws up here occasionally. :/
        #def expand_all(*args):
        #       self.window.services_treeview.expand_all()
        #       self.expanding = False
        #       return False
        #self.expanding = True
Yann Leboulanger's avatar
Yann Leboulanger committed
1462
        #GLib.idle_add(expand_all)
1463 1464 1465 1466 1467 1468 1469 1470
        self.window.services_treeview.expand_all()

    def _update_progressbar(self):
        """
        Update the progressbar
        """
        # Refresh this every update
        if self._progressbar_sourceid:
Yann Leboulanger's avatar
Yann Leboulanger committed
1471
            GLib.source_remove(self._progressbar_sourceid)
1472 1473 1474 1475 1476 1477 1478 1479

        fraction = 0
        if self._total_items:
            self.window.progressbar.set_text(_("Scanning %(current)d / %(total)d.."
                    ) % {'current': self._progress, 'total': self._total_items})
            fraction = float(self._progress) / float(self._total_items)
            if self._progress >= self._total_items:
                # We show the progressbar for just a bit before hiding it.
Yann Leboulanger's avatar
Yann Leboulanger committed
1480
                id_ = GLib.timeout_add_seconds(2, self._hide_progressbar_cb)
1481 1482 1483 1484
                self._progressbar_sourceid = id_
            else:
                self.window.progressbar.show()
                # Hide the progressbar if we're timing out anyways. (20 secs)
Yann Leboulanger's avatar
Yann Leboulanger committed
1485
                id_ = GLib.timeout_add_seconds(20, self._hide_progressbar_cb)
1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498
                self._progressbar_sourceid = id_
        self.window.progressbar.set_fraction(fraction)

    def _hide_progressbar_cb(self, *args):
        """
        Simple callback to hide the progressbar a second after we finish
        """
        if self.active:
            self.window.progressbar.hide()
        return False

    def _friendly_category(self, category, type_=None):
        """
1499
        Get the friendly category name
1500 1501 1502 1503 1504
        """
        cat = None
        if type_:
            # Try type-specific override
            try:
1505
                cat = _cat_to_descr[(category, type_)]
1506 1507 1508 1509
            except KeyError:
                pass
        if not cat:
            try:
1510
                cat = _cat_to_descr[category]
1511
            except KeyError: