#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (C) 2013-2016 Davide Andreoli <dave@gurumeditation.it>
#
# This file is part of Espionage.
#
# Espionage is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# Espionage is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Espionage.  If not, see <http://www.gnu.org/licenses/>.

import os
import sys
import dbus
import json
import ast
from xml.etree import ElementTree

from efl import elementary as elm
from efl.evas import EVAS_HINT_EXPAND, EVAS_HINT_FILL
from efl.elementary.window import StandardWindow, DialogWindow
from efl.elementary.box import Box
from efl.elementary.button import Button
from efl.elementary.check import Check
from efl.elementary.entry import Entry, markup_to_utf8, utf8_to_markup
from efl.elementary.flipselector import FlipSelector
from efl.elementary.icon import Icon
from efl.elementary.label import Label
from efl.elementary.panes import Panes
from efl.elementary.progressbar import Progressbar
from efl.elementary.popup import Popup
from efl.elementary.separator import Separator
from efl.elementary.table import Table
from efl.elementary.frame import Frame
from efl.elementary.genlist import Genlist, GenlistItem, GenlistItemClass, \
    ELM_GENLIST_ITEM_GROUP, ELM_GENLIST_ITEM_TREE, \
    ELM_OBJECT_SELECT_MODE_DISPLAY_ONLY, ELM_LIST_SCROLL
from efl.dbus_mainloop import DBusEcoreMainLoop


EXPAND_BOTH = EVAS_HINT_EXPAND, EVAS_HINT_EXPAND
EXPAND_HORIZ = EVAS_HINT_EXPAND, 0.0
FILL_BOTH = EVAS_HINT_FILL, EVAS_HINT_FILL
FILL_HORIZ = EVAS_HINT_FILL, 0.5


class Options(object):
    """class to contain application options"""
    def __init__(self):
        self.show_introspect_stuff = False
        self.show_private_stuff = False
        self.pretty_output = True
        self.scroll_on_signal = True
        self.theme_name = 'default'

        self.stl_name = 'font_weight=Bold color=#FFF  font_size=11'
        self.stl_value = 'color=#FF99FF'
        self.stl_iface = 'font_weight=Bold color=#999 font_size=11'
        self.stl_brackets = 'color=#FFCC00'
        self.stl_arrow = 'color=#AAAAAA'
        self.stl_ptype = 'color=#6699FF'
        self.stl_pname = 'color=#AAFFAA'


script_path = os.path.dirname(__file__)


def theme_resource_get(fname):
    return os.path.join(script_path, 'themes', options.theme_name, fname)


def prettify_if_needed(data):
    if options.pretty_output:
        return utf8_to_markup(json.dumps(data, indent=2))
    else:
        return utf8_to_markup(str(data))


def colored_params(plist, omit_braces=False):
    p = ', '.join(['<font %s>%s</>&nbsp;<font %s>%s</>' %
                   (options.stl_ptype, ty, options.stl_pname, name)
                   for name, ty in plist])
    if omit_braces:
        return p
    return '<font %s>(</>%s<font %s>)</>' % \
           (options.stl_brackets, p, options.stl_brackets)


# connect to session and system buses, and set session as the current one
session_bus = dbus.SessionBus(mainloop=DBusEcoreMainLoop())
system_bus = dbus.SystemBus(mainloop=DBusEcoreMainLoop())
bus = session_bus
options = Options()


# Classes to describe various DBus nodes
class DBusNode(object):
    """base object for the others DBus nodes"""
    def __init__(self, name, parent):
        self._name = name
        self._parent = parent

    @property
    def name(self):
        return self._name
    
    @property
    def parent(self):
        return self._parent


class DBusObject(DBusNode):
    """ object to represent a DBus Object """
    def __init__(self, name, parent_service):
        DBusNode.__init__(self, name, parent_service)
        self._interfaces = []

    @property
    def interfaces(self):
        return self._interfaces

    @property
    def icon(self):
        return 'object.png'


class DBusInterface(DBusNode):
    """ object to represent a DBus Interface """
    def __init__(self, name, parent_obj):
        DBusNode.__init__(self, name, parent_obj)
        self._properties = []
        self._methods = []
        self._signals = []
        
        parent_obj.interfaces.append(self)
    
    @property
    def properties(self):
        return self._properties
    
    @property
    def methods(self):
        return self._methods
    
    @property
    def signals(self):
        return self._signals

    @property
    def icon(self):
        return 'interface.png'


class DBusProperty(DBusNode):
    """ object to represent a DBus Property """
    def __init__(self, name, parent_iface, typ='unknown', access='unknown'):
        DBusNode.__init__(self, name, parent_iface)
        parent_iface.properties.append(self)
        self._type = typ
        self._access = access
        self._value = None

    def fetch_value(self):
        named_service = self.parent.parent.parent
        object_path = self.parent.parent.name
        iface_name = self.parent.name

        obj = bus.get_object(named_service, object_path)
        iface = dbus.Interface(obj, 'org.freedesktop.DBus.Properties')
        self._value = iface.Get(iface_name, self.name)

    @property
    def value(self):
        return self._value

    @property
    def type(self):
        return self._type

    @property
    def access(self):
        return self._access

    @property
    def icon(self):
        return 'property.png'


class DBusMethod(DBusNode):
    """ object to represent a DBus Method """
    def __init__(self, name, parent_iface):
        DBusNode.__init__(self, name, parent_iface)
        parent_iface.methods.append(self)
        self._params = []
        self._returns = []

    @property
    def params(self):
        return self._params

    @property
    def params_str(self):
        return ', '.join([(ty+' '+name).strip() for name, ty in self._params])

    @property
    def returns(self):
        return self._returns

    @property
    def returns_str(self):
        return ', '.join([(ty+' '+name).strip() for name, ty in self._returns])

    @property
    def icon(self):
        return 'method.png'


class DBusSignal(DBusNode):
    """ object to represent a DBus Signal """
    def __init__(self, name, parent_iface):
        DBusNode.__init__(self, name, parent_iface)
        parent_iface.signals.append(self)
        self._params = []

    @property
    def params(self):
        return self._params

    @property
    def params_str(self):
        return ', '.join([(ty+' '+name).strip() for name, ty in self._params])

    @property
    def icon(self):
        return 'signal.png'


def recursive_introspect(named_service, object_path, ret_data=None):
    """ Introspect a named service and return a list of DBusObjects """
    # first recursion, create an empty list
    if ret_data is None:
        ret_data = []

    # parse the xml string from the Introspectable interface
    obj = bus.get_object(named_service, object_path)
    iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
    xml_data = iface.Introspect()
    xml_root = ElementTree.fromstring(xml_data)

    # debug
    # print('=' * 80)
    # print("Introspecting path:'%s' on service:'%s'" % (object_path, named_service))
    # print('=' * 80)
    # print(xml_data)
    # print('=' * 80)

    # traverse the xml tree
    if xml_root.find('interface') is not None:
        # found a new object
        obj = DBusObject(object_path, named_service)
        ret_data.append(obj)
    
    for xml_node in xml_root:
        # found an interface
        if xml_node.tag == 'interface':
            iface = DBusInterface(xml_node.attrib['name'], obj)

            for child in xml_node:
                if child.tag == 'property':
                    typ = child.attrib['type']
                    access = child.attrib['access']
                    DBusProperty(child.attrib['name'], iface, typ, access)

                if child.tag == 'method':
                    meth = DBusMethod(child.attrib['name'], iface)
                    for arg in child:
                        if arg.tag == 'arg':
                            if arg.attrib['direction'] == 'out':
                                li = meth.returns
                            else:
                                li = meth.params
                            li.append((
                                arg.attrib['name'] if 'name' in arg.attrib else '',
                                arg.attrib['type'] if 'type' in arg.attrib else ''))

                if child.tag == 'signal':
                    sig = DBusSignal(child.attrib['name'], iface)
                    for arg in child:
                        if arg.tag == 'arg':
                            sig.params.append((
                                arg.attrib['name'] if 'name' in arg.attrib else '',
                                arg.attrib['type'] if 'type' in arg.attrib else ''))

        # found another node, introspect it...
        if xml_node.tag == 'node':
            if object_path == '/':
                object_path = ''
            new_path = '/'.join((object_path, xml_node.attrib['name']))
            recursive_introspect(named_service, new_path, ret_data)

    return ret_data


# Names genlist (the one on the left)
class NamesListGroupItemClass(GenlistItemClass):
    def __init__(self):
        GenlistItemClass.__init__(self, item_style='group_index')

    @staticmethod
    def text_get(_gl, _part, name):
        return name


class NamesListItemClass(GenlistItemClass):
    def __init__(self):
        GenlistItemClass.__init__(self, item_style='default')

    @staticmethod
    def text_get(_gl, _part, name):
        return name


class NamesList(Genlist):
    def __init__(self, parent):

        self.win = parent
        self.sig1 = None
        self.waiting_activation = None
        self.waiting_popup = None

        # create the genlist
        Genlist.__init__(self, parent)
        self.itc = NamesListItemClass()
        self.itc_g = NamesListGroupItemClass()
        self.callback_selected_add(self.item_selected_cb)

        # add public group item
        self.public_group = self.item_append(self.itc_g, 'Public Services',
                                             flags=ELM_GENLIST_ITEM_GROUP)
        self.public_group.select_mode_set(ELM_OBJECT_SELECT_MODE_DISPLAY_ONLY)

        # add activatables group item
        self.activatable_group = self.item_append(self.itc_g, 'Activatable Services',
                                                  flags=ELM_GENLIST_ITEM_GROUP)
        self.activatable_group.select_mode_set(ELM_OBJECT_SELECT_MODE_DISPLAY_ONLY)
        
        # add private group item
        if options.show_private_stuff:
            self.private_group = self.item_append(self.itc_g, 'Private Services',
                                                  flags=ELM_GENLIST_ITEM_GROUP)
            self.private_group.select_mode_set(ELM_OBJECT_SELECT_MODE_DISPLAY_ONLY)

        # populate the genlist
        self.populate()

    def populate(self):
        active = set(bus.list_names())
        activatables = set(bus.list_activatable_names()) - active

        for name in active:
            self.service_add(name)

        for name in activatables:
            self.service_activatable_add(name)

        # keep the list updated when a name changes
        if self.sig1:
            self.sig1.remove()
        self.sig1 = bus.add_signal_receiver(self.name_owner_changed_cb, 
                                            'NameOwnerChanged')
        # bus.add_signal_receiver(self.name_acquired_cb, 'NameAcquired')
        # bus.add_signal_receiver(self.name_lost_cb, 'NameLost')
    
    def clear(self):
        self.public_group.subitems_clear()
        if options.show_private_stuff:
            self.private_group.subitems_clear()

    def item_selected_cb(self, _gl, item):
        name = item.data

        if item.parent is self.activatable_group:
            # activate the service, async with a cool popup
            bus.call_async('org.freedesktop.DBus', '/', 'org.freedesktop.DBus',
                           'StartServiceByName', 'su', (name, 0), None, None)
            spinner = Progressbar(self.win, style='wheel', pulse_mode=True)
            spinner.pulse(True)

            def stop_waiting_cb(_btn):
                self.waiting_popup.delete()
                self.waiting_activation = None
            button = Button(self.win, text='Stop waiting')
            button.callback_clicked_add(stop_waiting_cb)
            popup = Popup(self.win, content=spinner)
            popup.part_text_set('title,text', 'Activating service...')
            popup.part_content_set('button1', button)
            popup.show()
            self.waiting_activation = name
            self.waiting_popup = popup
        else:
            self.win.detail_list.populate(name)
    
    @staticmethod
    def sort_cb(it1, it2):
        return 1 if it1.data.lower() < it2.data.lower() else -1

    def service_activatable_add(self, name):
        self.item_sorted_insert(self.itc, name, self.sort_cb,
                                self.activatable_group, 0, None)

    def service_add(self, name):
        is_private = name.startswith(':')
        if is_private and not options.show_private_stuff:
            return

        group_item = self.private_group if is_private else self.public_group
        item = self.item_sorted_insert(self.itc, name, self.sort_cb,
                                       group_item, 0, None)

        if self.waiting_activation is not None and name == self.waiting_activation:
            self.waiting_popup.delete()
            self.waiting_activation = None
            item.selected = True
            item.show()

    def service_del(self, name):
        item = self.first_item
        while item:
            if item.data == name:
                item.delete()
                return
            item = item.next
        
    def name_owner_changed_cb(self, name, old_owner, new_owner):
        if old_owner == '':
            self.service_add(name)
        elif new_owner == '':
            self.service_del(name)


# Detail genlist (the one on the right)
class ObjectItemClass(GenlistItemClass):
    def __init__(self):
        GenlistItemClass.__init__(self, item_style='group_index')

    @staticmethod
    def text_get(_gl, _part, obj):
        return obj.name

    @staticmethod
    def content_get(gl, part, obj):
        if part == 'elm.swallow.icon':
            return Icon(gl, size_hint_min=(22, 22),
                        file=theme_resource_get(obj.icon))


class NodeItemClass(GenlistItemClass):
    def __init__(self):
        GenlistItemClass.__init__(self, item_style='default_style')

    @staticmethod
    def text_get(_gl, _part, obj):
        if isinstance(obj, DBusInterface):
            return '<font %s>%s</>' % (options.stl_iface, obj.name)
        if isinstance(obj, DBusProperty):
            if obj.value is not None:
                return '<font %s>%s</> <font %s>%s</> %s <font %s>→</> <font %s>%s</>' % \
                    (options.stl_name, obj.name, options.stl_ptype, obj.type,
                     obj.access, options.stl_arrow, options.stl_value, str(obj.value))
            else:
                return '<font %s>%s</> <font %s>%s</> %s <font %s>→</>' % \
                        (options.stl_name, obj.name, options.stl_ptype, obj.type,
                         obj.access, options.stl_arrow)
        if isinstance(obj, DBusMethod):
            params = colored_params(obj.params)
            if obj.returns:
                rets = colored_params(obj.returns)
                return '<font %s>%s</> %s <font %s>→</> %s' % \
                       (options.stl_name, obj.name, params, options.stl_arrow, rets)
            else:
                return '<font %s>%s</> %s' % (options.stl_name, obj.name, params)
        if isinstance(obj, DBusSignal):
            params = colored_params(obj.params)
            return '<font %s>%s</> %s' % (options.stl_name, obj.name, params)

    @staticmethod
    def content_get(gl, part, obj):
        if part == 'elm.swallow.icon':
            return Icon(gl, file=theme_resource_get(obj.icon))


class DetailList(Genlist):
    def __init__(self, parent):
        Genlist.__init__(self, parent)
        self._parent = parent
        self.service_name = None
        self.itc = NodeItemClass()
        self.itc_g = ObjectItemClass()
        self.callback_expand_request_add(self.expand_request_cb)
        self.callback_expanded_add(self.expanded_cb)
        self.callback_contract_request_add(self.contract_request_cb)
        self.callback_contracted_add(self.contracted_cb)
        self.callback_clicked_double_add(self.double_click_cb)

    def populate(self, name):
        self.service_name = name
        self.clear()

        # objects
        for obj in recursive_introspect(name, '/'):
            obj_item = self.item_append(self.itc_g, obj,
                                        flags=ELM_GENLIST_ITEM_GROUP)
            obj_item.select_mode_set(ELM_OBJECT_SELECT_MODE_DISPLAY_ONLY)
            
            # interfaces
            for iface in obj.interfaces:
                if not options.show_introspect_stuff and \
                   iface.name.startswith('org.freedesktop.DBus'):
                    continue
                self.item_append(self.itc, iface, parent_item=obj_item,
                                 flags=ELM_GENLIST_ITEM_TREE)

    @staticmethod
    def sort_cb(it1, it2):
        pri1 = pri2 = 0
        if isinstance(it1.data, DBusProperty):
            pri1 = 3
        elif isinstance(it1.data, DBusMethod):
            pri1 = 2
        elif isinstance(it1.data, DBusSignal):
            pri1 = 1
        if isinstance(it2.data, DBusProperty):
            pri2 = 3
        elif isinstance(it2.data, DBusMethod):
            pri2 = 2
        elif isinstance(it2.data, DBusSignal):
            pri2 = 1
        if pri1 > pri2:
            return 1
        elif pri1 < pri2:
            return -1
        return 1 if it1.data.name.lower() < it2.data.name.lower() else -1

    @staticmethod
    def expand_request_cb(_genlist, item):
        item.expanded = True

    def expanded_cb(self, _genlist, item):
        iface = item.data
        for obj in iface.properties + iface.methods + iface.signals:
            self.item_sorted_insert(self.itc, obj, self.sort_cb, parent_item=item)
    
    @staticmethod
    def contract_request_cb(_genlist, item):
        item.expanded = False

    @staticmethod
    def contracted_cb(_genlist, item):
        item.subitems_clear()

    def double_click_cb(self, _genlist, item):
        if isinstance(item.data, DBusMethod):
            MethodRunner(self._parent, item.data)
        elif isinstance(item.data, DBusProperty):
            item.data.fetch_value()
            item.update()


# Methods runner
class MethodRunner(DialogWindow):
    def __init__(self, parent, method):
        DialogWindow.__init__(self, parent, 'espionage', 'Method', autodel=True)
        self._method = method
        self._param_entry = None
        self._return_entry = None

        # content is vbox (with surrounding pad frame)
        pad = Frame(self, style='pad_medium')
        pad.size_hint_weight = EVAS_HINT_EXPAND, EVAS_HINT_EXPAND
        pad.size_hint_align = EVAS_HINT_FILL, EVAS_HINT_FILL
        self.resize_object_add(pad)
        pad.show()
        
        vbox = Box(self)
        vbox.size_hint_weight = EVAS_HINT_EXPAND, EVAS_HINT_EXPAND
        vbox.size_hint_align = EVAS_HINT_FILL, EVAS_HINT_FILL
        pad.content = vbox
        vbox.show()

        # title
        f = 'font_size=16 align=0.5 font_weight=Bold'
        en = Entry(self, text='<font %s>%s()</>' % (f, method.name))
        en.size_hint_weight = EVAS_HINT_EXPAND, 0.0
        en.size_hint_align = EVAS_HINT_FILL, 0.0
        en.editable = False
        vbox.pack_end(en)
        en.show()

        # params label + entry
        if len(method.params) > 0:
            label = Entry(self, editable=False)
            label.size_hint_weight = EVAS_HINT_EXPAND, 0.0
            label.size_hint_align = EVAS_HINT_FILL, 0.0
            pars = colored_params(method.params, omit_braces=True)
            label.text = 'Params: %s' % (pars if method.params else 'None')
            vbox.pack_end(label)
            label.show()

            en = Entry(self, editable=True, scrollable=True, single_line=True)
            en.size_hint_weight = EVAS_HINT_EXPAND, 0.0
            en.size_hint_align = EVAS_HINT_FILL, 0.0
            self._param_entry = en
            vbox.pack_end(en)
            en.show()

        # returns label + entry
        label = Entry(self, editable=False)
        label.size_hint_weight = EVAS_HINT_EXPAND, 0.0
        label.size_hint_align = EVAS_HINT_FILL, 0.0
        rets = colored_params(method.returns, omit_braces=True)
        label.text = 'Returns: %s' % (rets if method.returns else 'None')
        vbox.pack_end(label)
        label.show()

        en = Entry(self, editable=False, scrollable=True)
        en.size_hint_weight = EVAS_HINT_EXPAND, EVAS_HINT_EXPAND
        en.size_hint_align = EVAS_HINT_FILL, EVAS_HINT_FILL
        self._return_entry = en
        vbox.pack_end(en)
        en.show()

        # pretty print check button
        def pretty_output_clicked_cb(chk):
            options.pretty_output = chk.state
        ch = Check(self)
        ch.size_hint_align = 0.0, 0.5
        ch.text = 'Prettify output (loosing type infos)'
        ch.state = options.pretty_output
        ch.callback_changed_add(pretty_output_clicked_cb)
        ch.show()
        vbox.pack_end(ch)

        sep = Separator(self, horizontal=True)
        vbox.pack_end(sep)
        sep.show()

        # buttons
        hbox = Box(self, horizontal=True)
        hbox.size_hint_weight = EVAS_HINT_EXPAND, 0.0
        hbox.size_hint_align = EVAS_HINT_FILL, 0.5
        vbox.pack_end(hbox)
        hbox.show()

        btn = Button(self)
        btn.text = 'Close'
        btn.callback_clicked_add(lambda b: self.delete())
        hbox.pack_end(btn)
        btn.show()

        btn = Button(self)
        btn.text = 'Clear output'
        btn.callback_clicked_add(lambda b: self._return_entry.entry_set(''))
        hbox.pack_end(btn)
        btn.show()

        btn = Button(self)
        btn.text = 'Run method'
        btn.callback_clicked_add(self.run_clicked_cb)
        hbox.pack_end(btn)
        btn.show()

        # show the window
        self.resize(300, 300)
        self.show()

    def run_clicked_cb(self, _btn):
        # collect method infos
        named_service = self._method.parent.parent.parent
        object_path = self._method.parent.parent.name
        iface_name = self._method.parent.name
        method_name = self._method.name
        if self._param_entry:
            user_params = markup_to_utf8(self._param_entry.entry)
        else:
            user_params = None

        # create the dbus proxy
        obj = bus.get_object(named_service, object_path)
        iface = dbus.Interface(obj, iface_name)
        meth = iface.get_dbus_method(method_name)

        # async method call
        try:
            if user_params:
                params = ast.literal_eval(user_params)
                if type(params) is tuple:
                    meth(*params, reply_handler=self.reply_handler,
                         error_handler=self.error_handler)
                else:
                    meth(params, reply_handler=self.reply_handler,
                         error_handler=self.error_handler)
            else:
                meth(reply_handler=self.reply_handler,
                     error_handler=self.error_handler)
        except Exception as e:
            self._return_entry.entry = \
                    '<b><failure>Error running method</failure><br><br>' \
                    'Exception:</b><br>%s' % utf8_to_markup(str(e))

        # TODO find a way to catch errors after this point
        #      wrong params for example are raised later :/

    def reply_handler(self, *rets):
        self._return_entry.entry = prettify_if_needed(rets) if rets \
                else 'Method executed successfully.<br>Nothing returned.'

    def error_handler(self, e):
        self._return_entry.entry = \
                '<b><failure>Error executing method</failure><br><br>' \
                'Exception:</b><br>%s' % utf8_to_markup(str(e))


# Signals receiver
class SignalItemClass(GenlistItemClass):
    def __init__(self):
        GenlistItemClass.__init__(self, item_style='default_style')

    @staticmethod
    def text_get(_gl, _part, data):
        return '<font %s>%s</>  <font %s>iface: %s</> <font %s>path: %s</> <font %s>sender: %s</>' % \
               (options.stl_name, data['signal'], options.stl_ptype, data['iface'],
                options.stl_pname, data['path'], options.stl_value, data['sender'])

    @staticmethod
    def content_get(gl, part, _data):
        if part == 'elm.swallow.icon':
            return Icon(gl, file=theme_resource_get('signal.png'))


class SignalReceiver(Frame):
    def __init__(self, parent):
        Frame.__init__(self, parent, text='Signals')
        self._parent = parent

        vbox = Box(self)
        vbox.show()
        self.content = vbox

        self.siglist = Genlist(self, homogeneous=True, mode=ELM_LIST_SCROLL)
        self.siglist.size_hint_weight = EVAS_HINT_EXPAND, EVAS_HINT_EXPAND
        self.siglist.size_hint_align = EVAS_HINT_FILL, EVAS_HINT_FILL
        self.siglist.callback_clicked_double_add(self.signal_clicked_cb)
        self.siglist.show()
        vbox.pack_end(self.siglist)
        self.itc = SignalItemClass()

        hbox = Box(self, horizontal=True)
        hbox.size_hint_align = 0.0, 0.5
        hbox.show()
        vbox.pack_end(hbox)

        bt = Button(self, text='Clear')
        bt.callback_clicked_add(lambda b_: self.siglist.clear())
        hbox.pack_end(bt)
        bt.show()

        def scroll_on_signal_clicked_cb(chk):
            options.scroll_on_signal = chk.state
        ck = Check(self, text='Scroll on signal')
        ck.state = options.scroll_on_signal
        ck.callback_changed_add(scroll_on_signal_clicked_cb)
        hbox.pack_end(ck)
        ck.show()

        for b in session_bus, system_bus:
            b.add_signal_receiver(
                self.signal_cb, sender_keyword='sender',
                destination_keyword='dest', interface_keyword='iface',
                member_keyword='signal', path_keyword='path')

    def signal_cb(self, *args, **kargs):
        # print('*** SIGNAL RECEIVED ***')
        # print(json.dumps(args, indent=2))
        # print(json.dumps(kargs, indent=2))

        kargs['args'] = args
        item = self.siglist.item_append(self.itc, kargs)
        if options.scroll_on_signal is True:
            item.bring_in()

        if self.siglist.items_count > 200:
            self.siglist.first_item.delete()

    def signal_clicked_cb(self, _gl, item):
        pp = Popup(self._parent)
        title = '<font %s>Signal: %s</font><br>' \
                '<font %s>Iface: %s</font><br>' \
                '<font %s>Path: %s</font><br>' \
                '<font %s>Sender: %s</font><br><br>' \
                '<font align=left>Content:</font>' % (
                    options.stl_name,  item.data.get('signal', 'Unknown'),
                    options.stl_ptype, item.data.get('iface', 'Unknown'),
                    options.stl_pname, item.data.get('path', 'Unknown'),
                    options.stl_value, item.data.get('sender', 'Unknown'))
        pp.part_text_set('title,text', title)

        # good'old table+rect trick to let the entry actually show up
        from efl.evas import Rectangle
        tb = Table(pp)
        tb.show()
        pp.content = tb

        rect = Rectangle(pp.evas, size_hint_min=(200, 200),
                         size_hint_weight=EXPAND_BOTH,
                         size_hint_align=FILL_BOTH)
        tb.pack(rect, 0, 0, 1, 1)

        en = Entry(self,
                   text='<code>%s</>' % prettify_if_needed(item.data['args']),
                   editable=False, scrollable=True,
                   size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH)
        # en.size_hint_min=(200, 200) # THIS SHOULD WORK... but dont :/
        tb.pack(en, 0, 0, 1, 1)
        en.show()

        bt = Button(pp, text='Close')
        bt.callback_clicked_add(lambda b: pp.delete())
        pp.part_content_set('button2', bt)

        def prettify_clicked_cb(chk):
            options.pretty_output = chk.state
            en.text = '<code>%s</>' % prettify_if_needed(item.data['args'])
        ck = Check(pp, text='Prettify')
        ck.state = options.pretty_output
        ck.callback_changed_add(prettify_clicked_cb)
        pp.part_content_set('button1', ck)

        pp.show()


# The main window
class EspionageWin(StandardWindow):
    def __init__(self):
        StandardWindow.__init__(self, 'espionage', 'Espionage D-Bus inspector')

        self.autodel_set(True)
        self.callback_delete_request_add(lambda o: elm.exit())

        box = Box(self)
        self.resize_object_add(box)
        box.size_hint_weight = EVAS_HINT_EXPAND, EVAS_HINT_EXPAND
        box.show()

        tb = Table(self)
        box.pack_end(tb)
        tb.show()

        lb = Label(self, text='Connect to:', scale=1.3)
        tb.pack(lb, 0, 0, 1, 2)
        lb.show()

        flip = FlipSelector(self, scale=1.3)
        flip.item_append('Session Bus', self.flip_selected_cb, session_bus)
        flip.item_append('System Bus', self.flip_selected_cb, system_bus)
        tb.pack(flip, 1, 0, 1, 2)
        flip.show()

        chk = Check(self, text='Show private services')
        chk.size_hint_align = 0.0, 1.0
        chk.state = options.show_private_stuff
        chk.callback_changed_add(self.show_private_cb)
        tb.pack(chk, 2, 0, 1, 1)
        chk.show()

        chk = Check(self, text='Show DBus introspectables')
        chk.size_hint_align = 0.0, 0.0
        chk.state = options.show_introspect_stuff
        chk.callback_changed_add(self.show_introspectables_cb)
        tb.pack(chk, 2, 1, 1, 1)
        chk.show()

        vpanes = Panes(self, horizontal=True)
        vpanes.size_hint_weight = EVAS_HINT_EXPAND, EVAS_HINT_EXPAND
        vpanes.size_hint_align = EVAS_HINT_FILL, EVAS_HINT_FILL
        vpanes.content_left_size = 2.0 / 3
        box.pack_end(vpanes)
        vpanes.show()
        
        hpanes = Panes(self)
        hpanes.size_hint_weight = EVAS_HINT_EXPAND, EVAS_HINT_EXPAND
        hpanes.size_hint_align = EVAS_HINT_FILL, EVAS_HINT_FILL
        hpanes.content_left_size = 1.0 / 3
        vpanes.part_content_set('left', hpanes)
        self.panes = hpanes
        hpanes.show()

        self.names_list = NamesList(self)
        hpanes.part_content_set('left', self.names_list)

        self.detail_list = DetailList(self)
        hpanes.part_content_set('right', self.detail_list)

        self.sigs_receiver = SignalReceiver(self)
        vpanes.part_content_set('right', self.sigs_receiver)

        self.resize(700, 500)
        self.show()

    def flip_selected_cb(self, _flipselector, _item, selected_bus):
        global bus

        bus = selected_bus
        self.detail_list.clear()
        self.names_list.clear()
        self.names_list.populate()

    def show_private_cb(self, chk):
        options.show_private_stuff = chk.state
        self.names_list.delete()
        self.names_list = NamesList(self)
        self.panes.part_content_set('left', self.names_list)

    def show_introspectables_cb(self, chk):
        options.show_introspect_stuff = chk.state
        self.detail_list.populate(self.detail_list.service_name)


def main():
    elm.init()
    EspionageWin()
    elm.run()
    elm.shutdown()
    return 0


if __name__ == '__main__':
    sys.exit(main())
