#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2010-2012 Red Hat, Inc.
#
# Authors:
# Thomas Woerner <twoerner@redhat.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

import sys

try:
    from gi.repository import Gtk
    Gtk.init (sys.argv)
except RuntimeError as e:
    print ("firewall-applet: %s" % e)
    print ("This is a graphical application and requires DISPLAY to be set.")
    sys.exit (1)

from gi.repository import GLib, GObject, Gio, Notify, NetworkManager

# force use of pygobject3 in python-slip
sys.modules['gobject'] = GObject

import os
import dbus.mainloop.glib
import slip.dbus

from firewall.config import *
from firewall.config.dbus import *
from firewall.client import FirewallClient
from firewall.dbus_utils import dbus_to_python, command_of_pid
from firewall.functions import ppid_of_pid
from firewall.errors import *
import dbus


PATH = [ ]
for p in os.getenv("PATH").split(":"):
    if p not in PATH:
        PATH.append(p)

def search_app(app):
    for p in PATH:
        _app = "%s/%s" % (p, app)
        if os.path.exists(_app):
            return _app
    return None

def check_gnome3():
    pid = os.getpid()
    while pid != None:
        ppid = ppid_of_pid(pid)
        if ppid:
            cmd = command_of_pid(ppid)
            if cmd != None and "gnome-shell" in cmd:
                return True
        pid = ppid
    return False

APPLET_SCHEMA = "org.fedoraproject.FirewallApplet"
NM_CONNECTION_EDITOR = ""
for binary in [ "/usr/bin/nm-connection-editor",
                    "/bin/nm-connection-editor",
                "/usr/bin/kde-nm-connection-editor",
                    "/bin/kde-nm-connection-editor" ]:
    if os.path.exists(binary):
        NM_CONNECTION_EDITOR = binary
        break

def combobox_select_text(combobox, value):
    model = combobox.get_model()
    iter = model.get_iter_first()
    while iter:
        if model.get_value(iter, 0) == value:
            combobox.set_active_iter(iter)
            return True
        iter = model.iter_next(iter)
    combobox.set_active(0)
    return False

def escape(text):
    text = text.replace('&', '&amp;')
    text = text.replace('>', '&gt;')
    text = text.replace('<', '&lt;')
    return text

class ZoneConnectionEditor(Gtk.Dialog):
    def __init__(self, fw, connection, zone):
        self.fw = fw
        self.connection = connection
        self.zone = zone
        self.title = _("Select zone for connection '%s'") % self.connection

        super(ZoneConnectionEditor, self).__init__(self.title)

        self.set_property("width-request", 100)
        self.resize_to_geometry(100, 50)
        self.set_resizable(True)

        self.add_button("gtk-close", 1)
        self.ok_button = self.add_button("gtk-ok", 2)
        self.ok_button.set_sensitive(False)

        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        vbox.set_border_width(12)
        vbox.set_homogeneous(False)

        label = Gtk.Label()
        label.set_text(self.title)
        label.set_line_wrap(True)
        label.set_justify(Gtk.Justification.LEFT)
        label.set_alignment(0, 0.5)
        vbox.pack_start(label, True, True, 0)

        self.combo = Gtk.ComboBoxText()
        zones = self.fw.getZones()
        for zone in zones:
            self.combo.append_text(zone)
        vbox.pack_start(self.combo, True, True, 0)

        box = self.get_content_area()
        box.set_border_width(6)
        box.set_homogeneous(False)
        box.pack_start(vbox, False, True, 0)

        combobox_select_text(self.combo, self.zone)
        self.combo.connect("changed", self.combo_changed)

    def combo_changed(self, combo):
        self.ok_button.set_sensitive(self.combo.get_active_text() != self.zone)

    def set_zone(self, zone):
        old_zone = self.zone
        self.zone = zone
        if self.combo.get_active_text() == old_zone:
            combobox_select_text(self.combo, self.zone)
        else:
            self.combo_changed(None)

    def get_zone(self):
        return self.combo.get_active_text()

    def run(self):
        result = super(ZoneConnectionEditor, self).run()
        if result != 2:
            return
        dbus_obj = self.fw.bus.get_object(NetworkManager.DBUS_INTERFACE,
                                          NetworkManager.DBUS_PATH_SETTINGS)
        settings_iface = dbus.Interface(
            dbus_obj, dbus_interface=NetworkManager.DBUS_IFACE_SETTINGS)
        conection_obj = None
        settings = None
        for connection in dbus_to_python(settings_iface.ListConnections()):
            connection_obj = self.fw.bus.get_object(
                NetworkManager.DBUS_INTERFACE, connection)
            settings = connection_obj.GetSettings()
            if settings["connection"]["id"] == self.connection:
                break
            else:
                connection_obj = None
                settings = None
        if settings and connection_obj:
            settings["connection"]["zone"] = self.get_zone()
            connection_obj.Update(settings)

class ZoneInterfaceEditor(Gtk.Dialog):
    def __init__(self, fw, interface, zone):
        self.fw = fw
        self.interface = interface
        self.zone = zone
        self.title = _("Select zone for interface '%s'") % self.interface

        super(ZoneInterfaceEditor, self).__init__(self.title)

        self.set_property("width-request", 100)
        self.resize_to_geometry(100, 50)
        self.set_resizable(True)

        self.add_button("gtk-close", 1)
        self.ok_button = self.add_button("gtk-ok", 2)
        self.ok_button.set_sensitive(False)

        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        vbox.set_border_width(12)
        vbox.set_homogeneous(False)

        label = Gtk.Label()
        label.set_text(self.title)
        label.set_line_wrap(True)
        label.set_justify(Gtk.Justification.LEFT)
        label.set_alignment(0, 0.5)
        vbox.pack_start(label, True, True, 0)

        self.combo = Gtk.ComboBoxText()
        zones = self.fw.getZones()
        for zone in zones:
            self.combo.append_text(zone)
        vbox.pack_start(self.combo, True, True, 0)

        box = self.get_content_area()
        box.set_border_width(6)
        box.set_homogeneous(False)
        box.pack_start(vbox, False, True, 0)

        combobox_select_text(self.combo, self.zone)
        self.combo.connect("changed", self.combo_changed)

    def combo_changed(self, combo):
        self.ok_button.set_sensitive(self.combo.get_active_text() != self.zone)

    def set_zone(self, zone):
        old_zone = self.zone
        self.zone = zone
        if self.combo.get_active_text() == old_zone:
            combobox_select_text(self.combo, self.zone)
        else:
            self.combo_changed(None)

    def get_zone(self):
        return self.combo.get_active_text()

class ZoneSourceEditor(Gtk.Dialog):
    def __init__(self, fw, source, zone):
        self.fw = fw
        self.source = source
        self.zone = zone
        self.title = _("Select zone for source %s") % self.source

        super(ZoneSourceEditor, self).__init__(self.title)

        self.set_property("width-request", 100)
        self.resize_to_geometry(100, 50)
        self.set_resizable(True)

        self.add_button("gtk-close", 1)
        self.ok_button = self.add_button("gtk-ok", 2)
        self.ok_button.set_sensitive(False)

        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        vbox.set_border_width(12)
        vbox.set_homogeneous(False)

        label = Gtk.Label()
        label.set_text(self.title)
        label.set_line_wrap(True)
        label.set_justify(Gtk.Justification.LEFT)
        label.set_alignment(0, 0.5)
        vbox.pack_start(label, True, True, 0)

        self.combo = Gtk.ComboBoxText()
        zones = self.fw.getZones()
        for zone in zones:
            self.combo.append_text(zone)
        vbox.pack_start(self.combo, True, True, 0)

        box = self.get_content_area()
        box.set_border_width(6)
        box.set_homogeneous(False)
        box.pack_start(vbox, False, True, 0)

        combobox_select_text(self.combo, self.zone)
        self.combo.connect("changed", self.combo_changed)

    def combo_changed(self, combo):
        self.ok_button.set_sensitive(self.combo.get_active_text() != self.zone)

    def set_zone(self, zone):
        old_zone = self.zone
        self.zone = zone
        if self.combo.get_active_text() == old_zone:
            combobox_select_text(self.combo, self.zone)
        else:
            self.combo_changed(None)

    def get_zone(self):
        return self.combo.get_active_text()

class TrayApplet(object):
    @staticmethod
    def position_function(menu, icon):
        return (Gtk.StatusIcon.position_menu(menu, icon))

    def __init__(self):
        self.name = _("Firewall Applet")
        self.icon_name = "firewall-applet"
        self.settings = Gio.Settings.new(APPLET_SCHEMA)

        self.icons = { "normal": None, "error": None, "panic": None, }
        self.timer = None
        self.mode = None
        self._blink = False
        self._blink_count = 0
        self.connected = False

        self.active_zones = { }
        self.connections = { }
        self.connections_uuid = { }
        self.default_zone = None
        self.zone_connection_editors = { }
        self.zone_interface_editors = { }
        self.zone_source_editors = { }

        # fix gnome 3 applet icon handling?
        self.fix_gnome3 = check_gnome3()

        # get blink bool and blink count from user settings
        self.blink_enabled = self.settings.get_boolean("blink")
        self.settings.connect("changed::blink",
                              self.bool_changed, self.blink_enabled)
        self.blink_count = self.settings.get_int("blink-count")
        self.settings.connect("changed::blink-count",
                              self.int_changed, self.blink_count)

        self.show_inactive = self.settings.get_boolean("show-inactive")
        self.settings.connect("changed::show-inactive",
                              self.show_inactive_changed)

        self.icontheme = Gtk.IconTheme.get_default()

        self.statusicon = Gtk.StatusIcon.new()
        self.statusicon.set_from_icon_name(self.icon_name)

        self.left_menu = Gtk.Menu.new()
        self.left_menu.set_reserve_toggle_size(False)

        self.right_menu = Gtk.Menu.new()

        self.shieldsup_check = Gtk.CheckMenuItem.new_with_mnemonic(
            _("Shields Up"))        
        self.shieldsup_check.set_active(False)
        self.shieldsup_check_id = \
            self.shieldsup_check.connect('toggled',
                                         self.shieldsup_check_toggled)
        self.right_menu.append(self.shieldsup_check)

        self.settings.connect("changed::shields-up",
                              self.settings_shields_up_changed)
        self.settings.connect("changed::shields-down",
                              self.settings_shields_down_changed)

        self.notification_check = Gtk.CheckMenuItem.new_with_mnemonic(
            _("Enable Notifications"))
        self.notification_check.set_active(self.settings.get_boolean(
                "notifications"))
        self.settings.connect("changed::notifications",
                              self.settings_check_changed,
                              self.notification_check)
        self.notification_check.connect('toggled',
                                        self.notification_check_toggled,
                                        self.settings, "notifications")
        self.right_menu.append(self.notification_check)

        self.right_menu.append(Gtk.SeparatorMenuItem.new())

        item = Gtk.ImageMenuItem.new_with_mnemonic(_("Edit Firewall Settings..."))
        item.set_image(Gtk.Image.new_from_stock("gtk-preferences",
                                                Gtk.IconSize.MENU))
        item.connect("activate", self.configure_cb)
        if not search_app("firewall-config"):
            item.set_sensitive(False)
        self.right_menu.append(item)
        
        item = Gtk.ImageMenuItem.new_with_mnemonic(_("Change Zones of Connections..."))
        item.set_image(Gtk.Image.new_from_stock("gtk-preferences",
                                                Gtk.IconSize.MENU))
        item.connect("activate", self.nm_connection_editor)
        item.set_sensitive(NM_CONNECTION_EDITOR != "")
        self.right_menu.append(item)

        item = Gtk.ImageMenuItem.new_with_mnemonic(_("Configure Shields UP/Down Zones..."))
        item.set_image(Gtk.Image.new_from_stock("gtk-preferences",
                                                Gtk.IconSize.MENU))
        item.connect("activate", self.configure_shields_cb)
        self.right_menu.append(item)

        self.right_menu.append(Gtk.SeparatorMenuItem.new())

        self.panic_check = Gtk.CheckMenuItem.new_with_mnemonic(\
            _("Block all network traffic"))
        self.panic_check.set_active(False)
        self.panic_check_id = self.panic_check.connect("toggled",
                                                       self.panic_mode_cb)
        self.right_menu.append(self.panic_check)

        self.right_menu.append(Gtk.SeparatorMenuItem.new())

        item = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_ABOUT, None)
        item.connect('activate', self.about_cb)
        self.right_menu.append(item)

        self.statusicon.connect("activate", self.left_menu_cb,
                                self.left_menu)
        self.statusicon.connect("popup-menu", self.right_menu_cb,
                                self.right_menu)
        self.statusicon.connect("size-changed", self.size_changed)

        self.about_dialog = Gtk.AboutDialog.new()
        self.about_dialog.set_name(self.name)
        self.about_dialog.set_version(VERSION)
        self.about_dialog.set_license(LICENSE)
        self.about_dialog.set_wrap_license(True)
        self.about_dialog.set_copyright(COPYRIGHT)
        self.about_dialog.set_authors(AUTHORS)
        self.about_dialog.set_logo_icon_name(self.icon_name)

        # shields up/down configure dialog

        self.shields_dialog = Gtk.Dialog(_("Configure Shields Up/Down Zones"))
        self.shields_dialog.set_property("width-request", 400)
        self.shields_dialog.resize_to_geometry(400, 100)
        self.shields_dialog.add_buttons("gtk-close", 1)
        self.shields_dialog.set_modal(True)
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        vbox.set_border_width(12)
        vbox.set_homogeneous(False)

        label = Gtk.Label()
        label.set_text(_("Here you can select the zones used for Shields "
                         "Up and Shields Down."))
        label.set_line_wrap(True)
        label.set_justify(Gtk.Justification.LEFT)
        label.set_alignment(0, 0.5)
        vbox.pack_start(label, True, True, 0)

        label = Gtk.Label()
        label.set_text(_("This feature is useful for people using the "
                         "default zones mostly. For users, that are "
                         "changing zones of connections, it might be of "
                         "limited use."))
        label.set_line_wrap(True)
        label.set_justify(Gtk.Justification.LEFT)
        label.set_alignment(0, 0.5)
        vbox.pack_start(label, True, True, 0)

        grid = Gtk.Grid()
        grid.set_row_spacing(6)
        grid.set_column_spacing(6)
        grid.set_border_width(6)

        label = Gtk.Label()
        label.set_text(_("Shields Up Zone:"))
        label.set_line_wrap(True)
        label.set_justify(Gtk.Justification.LEFT)
        label.set_alignment(1, 0.5)
        grid.attach(label, 0, 0, 1, 1)

        self.shields_up_combo = Gtk.ComboBoxText()
        self.shields_up_combo.connect("changed", self.shields_up_changed)
        grid.attach(self.shields_up_combo, 1, 0, 1, 1)

        label = Gtk.Label()
        label.set_text(_("Shields Down Zone:"))
        label.set_line_wrap(True)
        label.set_justify(Gtk.Justification.LEFT)
        label.set_alignment(1, 0.5)
        grid.attach(label, 0, 1, 1, 1)

        self.shields_down_combo = Gtk.ComboBoxText()
        self.shields_down_combo.connect("changed", self.shields_down_changed)
        grid.attach(self.shields_down_combo, 1, 1, 1, 1)

        button = Gtk.Button("Set To Default")
        button.connect("clicked", self.reset_shields_up_zone)
        grid.attach(button, 2, 0, 1, 1)

        button = Gtk.Button("Set To Default")
        button.connect("clicked", self.reset_shields_down_zone)
        grid.attach(button, 2, 1, 1, 1)

        vbox.pack_start(grid, True, True, 0)

        box = self.shields_dialog.get_content_area()
        box.set_border_width(6)
        box.set_homogeneous(False)
        box.pack_start(vbox, False, True, 0)

        # init notification

        Notify.init(self.name)

        # status icon

        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
        try:
            self.bus = slip.dbus.SystemBus()
            self.bus.default_timeout = None
        except Exception as msg:
            print("Not using slip", msg)
            self.bus = dbus.SystemBus()

        self.fw = FirewallClient(self.bus, wait=1)
        self.set_mode("error")
        if not self.show_inactive:
            self.hide_icon()

        self.fw.connect("connection-established", self.connection_established)
        self.fw.connect("connection-lost", self.connection_lost)
        self.fw.connect("reloaded", self.reloaded),
        self.fw.connect("default-zone-changed", self.default_zone_changed)
        self.fw.connect("panic-mode-enabled", self.panic_mode_enabled)
        self.fw.connect("panic-mode-disabled", self.panic_mode_disabled)
        self.fw.connect("interface-added", self.interface_added)
        self.fw.connect("interface-removed", self.interface_removed)
        self.fw.connect("zone-of-interface-changed",
                        self.zone_of_interface_changed)
        self.fw.connect("source-added", self.source_added)
        self.fw.connect("source-removed", self.source_removed)
        self.fw.connect("zone-of-source-changed",
                        self.zone_of_source_changed)

        self.bus.add_signal_receiver(
            self.nm_signal_receiver,
            dbus_interface=NetworkManager.DBUS_INTERFACE,
            signal_name='PropertiesChanged',
            member_keyword='member')
        self.nm_signal_receiver()

    def set_icon(self, mode):
        self.statusicon.set_from_pixbuf(self.icons[mode])
        if self.fix_gnome3 and (self.fw.connected or self.show_inactive):
            self.hide_icon()
            self.show_icon()

    def size_changed(self, statusicon, size):
        _dict = { "normal": "",
                  "error": "-error",
                  "panic": "-panic" }
        for _type in _dict:
            name = "firewall-applet%s" % _dict[_type]
            info = self.icontheme.lookup_icon(name, size, 0)
            if info:
                self.icons[_type] = info.load_icon()
            else:
                print("Icon '%s' could not be loaded" % name)
        self.set_icon(self.mode)

    def settings_check_changed(self, settings, key, button):
        button.set_active(settings.get_boolean(key))

    def bool_changed(self, settings, key, item):
        item = settings.get_boolean(key)

    def show_inactive_changed(self, settings, key):
        self.show_inactive = settings.get_boolean(key)
        if not self.fw.connected:
            if self.show_inactive:
                self.show_icon()
            else:
                self.hide_icon()

    def int_changed(self, settings, key, item):
        item = settings.get_int(key)

    def shieldsup_check_toggled(self, button):
        if button.get_active():
            zone = self.settings.get_string("shields-up")
        else:
            zone = self.settings.get_string("shields-down")

        if self.fw.connected and self.fw.getDefaultZone() != zone:
            try:
                self.fw.setDefaultZone(zone)
            except dbus.exceptions.DBusException as e:
                print("Error: %s" % e.get_dbus_message())

    def notification_check_toggled(self, button, settings, key):
        settings.set_boolean(key, button.get_active())

    def notify(self, msg, sender=None, urgency=Notify.Urgency.NORMAL,
               timeout=5):
        n = Notify.Notification.new(self.name, msg, self.icon_name)
        n.set_urgency(urgency)
        try:
            n.show()
        except:
            return
        if self.fix_gnome3:
            GLib.timeout_add_seconds(timeout, self.close_notification, n)

    def close_notification(self, n):
        n.close()
        self.hide_icon()
        self.show_icon()

    def hide_icon(self):
        self.statusicon.set_visible(False)

    def show_icon(self):
        self.statusicon.set_visible(True)

    def update_active_zones(self):
        self.active_zones.clear()

        # remove all entries for the left menu
        left_menu_children = self.left_menu.get_children()
        for child in left_menu_children:
            self.left_menu.remove(child)
            child.destroy()

        # add connecitons entry
        item = Gtk.MenuItem.new()
        label = Gtk.Label()
        label.set_markup("<b>"+escape(_("Connections"))+"</b>")
        label.set_alignment(0, 0.5)
        item.add(label)
        item.connect("select", self.no_select)
        self.left_menu.append(item)

        if not self.connected:
            return

        active_zones = self.fw.getActiveZones()
        if active_zones:
            self.active_zones = active_zones

        # get all active connections (NM) and interfaces
        connections = { }
        interfaces = { }
        sources = { }
        for zone in sorted(self.active_zones):
            if "interfaces" in self.active_zones[zone]:
                for interface in sorted(self.active_zones[zone]["interfaces"]):
                    if interface not in self.connections:
                        interfaces[interface] = zone
                    else:
                        # NM controlled
                        connection = self.connections[interface]
                        if connection not in self.connections_uuid:
                            uuid = None
                        else:
                            uuid = self.connections_uuid[connection]
                        connections[connection] = [ zone, uuid ]
            if "sources" in self.active_zones[zone]:
                for source in sorted(self.active_zones[zone]["sources"]):
                    sources[source] = zone

        # add NM controlled entries
        for connection in sorted(connections):
            [ zone, uuid ] = connections[connection]
            
            item = Gtk.MenuItem.new()
            hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
            label = Gtk.Label()
            label.set_markup("%s\n<small>%s: %s</small>" % \
                                 (connection, escape(_("Zone")), zone))
            label.set_alignment(0, 0.5)
            label.set_padding(12, 0)
            hbox.pack_start(label, True, True, 0)
#            # only sensitive if nm_connection_editor exists
#            if NM_CONNECTION_EDITOR == "":
#                item.set_sensitive(False)
            item.add(hbox)
#            item.connect("activate", self.nm_connection_editor, uuid)
            item.connect("activate", self.change_zone_connection_editor, connection, zone)
            self.left_menu.append(item)

        if len(interfaces) > 0:
            item = Gtk.MenuItem.new()
            label = Gtk.Label()
            label.set_markup("<b>"+escape(_("Interfaces"))+"</b>")
            label.set_alignment(0, 0.5)
            item.add(label)
            item.connect("select", self.no_select)
            self.left_menu.append(item)

            # add other interfaces
            for interface in sorted(interfaces):
                zone = interfaces[interface]

                item = Gtk.MenuItem.new()
                hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
                label = Gtk.Label()
                label.set_markup("%s\n<small>%s: %s</small>" % \
                                     (interface, escape(_("Zone")), zone))
                label.set_alignment(0, 0.5)
                label.set_padding(12, 0)
                hbox.pack_start(label, True, True, 0)
                item.add(hbox)
                item.connect("activate", self.change_zone_interface_editor, interface, zone)
                self.left_menu.append(item)

        if len(sources) > 0:
            item = Gtk.MenuItem.new()
            label = Gtk.Label()
            label.set_markup("<b>"+escape(_("Sources"))+"</b>")
            label.set_alignment(0, 0.5)
            item.add(label)
            item.connect("select", self.no_select)
            self.left_menu.append(item)

            for source in sorted(sources):
                zone = sources[source]

                item = Gtk.MenuItem.new()
                hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
                label = Gtk.Label()
                label.set_markup("%s\n<small>%s: %s</small>" % \
                                     (source, escape(_("Zone")),
                                      zone))
                label.set_alignment(0, 0.5)
                label.set_padding(12, 0)
                hbox.pack_start(label, True, True, 0)
                item.add(hbox)
                item.connect("activate", self.change_zone_source_editor, source, zone)
                self.left_menu.append(item)

    def nm_connection_editor(self, item, uuid=None):
        if NM_CONNECTION_EDITOR == "":
            self._warning("NetworkManager connection editor is missing.")
            return

        if uuid:
            if "kde-" in NM_CONNECTION_EDITOR:
                os.system("%s %s &" % (NM_CONNECTION_EDITOR, uuid))
            else:
                os.system("%s --edit=%s &" % (NM_CONNECTION_EDITOR, uuid))
        else:
            os.system("%s &" % NM_CONNECTION_EDITOR)

    def change_zone_interface_editor(self, item, interface, zone):
        if interface in self.zone_interface_editors:
            return self.zone_interface_editors[interface].present()

        editor = ZoneInterfaceEditor(self.fw, interface, zone)
        editor.set_icon_name(self.icon_name)
        self.zone_interface_editors[interface] = editor

        editor.show_all()
        result = editor.run()
        editor.hide()
        if result == 2:
            self.fw.changeZoneOfInterface(editor.get_zone(), interface)
        del self.zone_interface_editors[interface]

    def change_zone_connection_editor(self, item, connection, zone):
        if connection in self.zone_connection_editors:
            return self.zone_connection_editors[connection].present()

        editor = ZoneConnectionEditor(self.fw, connection, zone)
        editor.set_icon_name(self.icon_name)
        self.zone_connection_editors[connection] = editor

        editor.show_all()
        editor.run()
        editor.hide()
        del self.zone_connection_editors[connection]

    def change_zone_source_editor(self, item, source, zone):
        if source in self.zone_source_editors:
            return self.zone_source_editors[source].present()

        editor = ZoneSourceEditor(self.fw, source, zone)
        editor.set_icon_name(self.icon_name)
        self.zone_source_editors[source] = editor

        editor.show_all()
        result = editor.run()
        editor.hide()
        if result == 2:
            self.fw.changeZoneOfSource(editor.get_zone(), source)
        del self.zone_source_editors[source]

    def connection_established(self, first=False):
        self.show_icon()
        self.set_mode("normal")
        self.default_zone = self.fw.getDefaultZone()
        self.connected = True
        self.update_active_zones()

        self.shields_up_combo.get_model().clear()
        self.shields_down_combo.get_model().clear()
        zones = self.fw.getZones()
        for zone in zones:
            self.shields_up_combo.append_text(zone)
            self.shields_down_combo.append_text(zone)

        if self.settings.get_string("shields-up") not in zones:
            self.warning("Shields Up zone '%s' not found" % \
                             self.settings.get_string("shields-up"), 
                         "Resetting to default value...")
            self.settings.reset("shields-up")
        if self.settings.get_string("shields-down") not in zones:
            self.warning("Shields Down zone '%s' not found" % \
                             self.settings.get_string("shields-down"), 
                         "Resetting to default value...")
            self.settings.reset("shields-down")

        self.shieldsup_check.handler_block(self.shieldsup_check_id)
        if self.default_zone == self.settings.get_string("shields-up"):
            self.shieldsup_check.set_active(True)
        else:
            self.shieldsup_check.set_active(False)
        self.shieldsup_check.handler_unblock(self.shieldsup_check_id)

        self.update_tooltip()
        if self.notification_check.get_active():
            self.notify(_("Connection to FirewallD established."))

    def connection_lost(self):
        self.connected = False
        self.default_zone = None
        self.set_mode("error")
        self.update_active_zones()
        self.update_tooltip()
        if self.notification_check.get_active():
            self.notify(_("Connection to FirewallD lost."))
        if not self.show_inactive:
            self.hide_icon()

    def panic_mode_cb(self, check):
        if not self.fw or not self.connected:
            return
        if check.get_active():
            self.fw.enablePanicMode()
        else:
            self.fw.disablePanicMode()
        
    def left_menu_cb(self, widget, menu):
        menu.show_all()
        menu.popup(None, None, self.position_function,
                   self.statusicon, 1, Gtk.get_current_event_time())

    def right_menu_cb(self, widget, button, time, menu):
        if button != 3:
            return
        menu.show_all()
        menu.popup(None, None, self.position_function,
                   self.statusicon, button, time)

    def no_select(self, item):
        item.deselect()

    def about_cb(self, widget):
        self.about_dialog.run()
        self.about_dialog.hide()

    def configure_cb(self, widget):
        os.system("firewall-config &")

    def combobox_select_text(self, combobox, value):
        model = combobox.get_model()
        iter = model.get_iter_first()
        while iter:
            if model.get_value(iter, 0) == value:
                combobox.set_active_iter(iter)
                return True
            iter = model.iter_next(iter)
        combobox.set_active(0)
        return False

    def reloaded(self):
        if self.notification_check.get_active():
            self.notify(_("FirewallD has been reloaded."))

    def shields_up_changed(self, *args):
        text = self.shields_up_combo.get_active_text()
        if text and text != self.settings.get_string("shields-up"):
            self.settings.set_string("shields-up", text)

    def shields_down_changed(self, *args):
        text = self.shields_down_combo.get_active_text()
        if text and text != self.settings.get_string("shields-down"):
            self.settings.set_string("shields-down", text)

    def settings_shields_up_changed(self, *args):
        self.combobox_select_text(self.shields_up_combo,
                                  self.settings.get_string("shields-up"))
        #TODO: apply zone if shields-up enabled

    def settings_shields_down_changed(self, *args):
        self.combobox_select_text(self.shields_down_combo,
                                  self.settings.get_string("shields-down"))
        #TODO: apply zone if shields-down enabled?

    def reset_shields_up_zone(self, *args):
        self.settings.reset("shields-up")

    def reset_shields_down_zone(self, *args):
        self.settings.reset("shields-down")

    def configure_shields_cb(self, widget):
        self.combobox_select_text(self.shields_up_combo,
                                  self.settings.get_string("shields-up"))
        self.combobox_select_text(self.shields_down_combo,
                                  self.settings.get_string("shields-down"))

        self.shields_dialog.show_all()
        self.shields_dialog.run()
        self.shields_dialog.hide()

    def __blink(self, arg=None):
        if self._blink_count != 0:
            if self._blink_count > 0 and self._blink:
                self._blink_count -= 1
            self._blink = not self._blink
            self.timer = GLib.timeout_add_seconds(1, self.__blink, None)
        if not self._blink:
            self.set_icon(self.mode)
        else:
            self.set_icon("normal")

    def get_mode(self):
        return self.mode

    def set_mode(self, mode):
        if self.mode != mode:
            if self.timer:
                GLib.source_remove(self.timer)
                self.timer = None
                self._blink = False
            self.mode = mode

        elif self.mode == mode and self.timer:
            if self._blink_count == 0:
                self._blink_count += 1
            return

        if mode == "normal":
            self.set_icon(mode)
            return

        if self.blink_enabled and self.blink_count != 0:
            self._blink = True
            self._blink_count = self.blink_count
            self.__blink()

    def update_tooltip(self):
        if self.get_mode() == "error":
            self.tooltip = "<span color='#FF0000'>" + \
                _("No connection to firewall daemon") + "</span>"
            self.statusicon.set_tooltip_markup(self.tooltip)
            return

        if self.panic_check.get_active():
            self.tooltip = "<big><b><span color='#FF0000'>" + \
                _("PANIC MODE") + "</span></b></big>"
            self.statusicon.set_tooltip_markup(self.tooltip)
            return

        messages = [ ]

        if self.default_zone:
            messages.append(_("Default Zone: '%s'" % self.default_zone))

        if len(self.active_zones) > 0:
            for zone in sorted(self.active_zones):
                if "interfaces" in self.active_zones[zone]:
                    for interface in sorted(self.active_zones[zone]["interfaces"]):
                        if interface in self.connections:
                            connection = self.connections[interface]
                            text = _("Zone '{zone}' active for connection "
                                     "'{connection}' on interface '{interface}'")
                        else:
                            text = _("Zone '{zone}' active for interface "
                                     "'{interface}'")
                            connection = None
                        messages.append(text.format(zone=zone,
                                                    connection=connection,
                                                    interface=interface))
                if "sources" in self.active_zones[zone]:
                    for source in sorted(self.active_zones[zone]["sources"]):
                        text = _("Zone '{zone}' active for source {source}")
                        connection = None
                        messages.append(text.format(zone=zone, source=source))
        else:
            messages.append(_("No Active Zones."))

        self.tooltip = "\n".join(messages)

        self.statusicon.set_tooltip_markup(self.tooltip)

    def default_zone_changed(self, zone):
        self.default_zone = zone
        if self.notification_check.get_active():
            self.notify(_("Default zone changed to '%s'.") % zone)
        self.update_active_zones()
        self.update_tooltip()

        # in case something else changes default zone
        if self.default_zone == self.settings.get_string("shields-up"):
            self.shieldsup_check.set_active(True)
        else:
            self.shieldsup_check.set_active(False)


    def _panic_mode(self, enable):
        self.panic_check.handler_block(self.panic_check_id)
        self.panic_check.set_active(enable)
        self.panic_check.handler_unblock(self.panic_check_id)

        self.update_tooltip()

        if enable:
            self.set_mode("panic")
        else:
            self.set_mode("normal")

        if self.notification_check.get_active():
            ed = { 1: _("All network traffic is blocked."),
                   0: _("Network traffic is not blocked anymore.") }
            self.notify(ed[enable])

    def panic_mode_enabled(self):
        self._panic_mode(True)

    def panic_mode_disabled(self):
        self._panic_mode(False)

    def _interface(self, zone, interface, enable):
        self.update_active_zones()
        self.update_tooltip()

        # send notification if enabled
        if self.notification_check.get_active():
            ed = { 1: _("activated"),
                   0: _("deactivated") }
            if interface in self.connections:
                connection = self.connections[interface]
                text = _("Zone '{zone}' {activated_deactivated} for "
                         "connection '{connection}' on "
                         "interface '{interface}'")
            else:
                connection = None
                text = _("Zone '{zone}' {activated_deactivated} for "
                         "interface '{interface}'")
            self.notify(text.format(zone=zone,
                                    activated_deactivated=ed[enable],
                                    connection=connection,
                                    interface=interface))

    def interface_added(self, zone, interface):
        self._interface(zone, interface, True)

    def interface_removed(self, zone, interface):
        self._interface(zone, interface, False)

    def zone_of_interface_changed(self, zone, interface):
        # update zone editor
        if interface in self.zone_interface_editors:
            self.zone_interface_editors[interface].set_zone(zone)

        self.update_active_zones()
        self.update_tooltip()

        if self.notification_check.get_active():
            self.notify(_("Zone '%s' activated for interface '%s'") % \
                            (zone, interface))

    def _source(self, zone, source, enable):
        self.update_active_zones()
        self.update_tooltip()

        # send notification if enabled
        if self.notification_check.get_active():
            ed = { 1: _("activated"),
                   0: _("deactivated") }
            connection = None
            text = _("Zone '{zone}' {activated_deactivated} for "
                     "source '{source}'")
            self.notify(text.format(zone=zone,
                                    activated_deactivated=ed[enable],
                                    source=source))

    def source_added(self, zone, source):
        self._source(zone, source, True)

    def source_removed(self, zone, source):
        self._source(zone, source, False)

    def zone_of_source_changed(self, zone, source):
        index = source

        # update zone editor
        if index in self.zone_interface_editors:
            self.zone_interface_editors[index].set_zone(zone)

        self.update_active_zones()
        self.update_tooltip()

        if self.notification_check.get_active():
            self.notify(_("Zone '%s' activated for source '%s'") % \
                            (zone, source))

    def nm_signal_receiver(self, *args, **kwargs):
        #print("nm_signal_receiver", args, kwargs)
        self.connections.clear()
        self.connections_uuid.clear()

        # do not use NMClient could result in python core dump

        NM_IF = NetworkManager.DBUS_INTERFACE
        NM_IF_D = NetworkManager.DBUS_INTERFACE+".Device"
        NM_IF_C_A = NetworkManager.DBUS_INTERFACE+".Connection.Active"
        NM_IF_S_C = NetworkManager.DBUS_INTERFACE+".Settings.Connection"
        NM_PATH = NetworkManager.DBUS_PATH
        DBUS_PROP = 'org.freedesktop.DBus.Properties'

        try:
            # get active connections
            obj = self.bus.get_object(NM_IF, NM_PATH)
            props = dbus.Interface(obj, dbus_interface=DBUS_PROP)
            connections = dbus_to_python(props.Get(NM_IF, "ActiveConnections"))

            # for all active connections:
            for active in connections:
                # get connection and devices from active connection
                obj = self.bus.get_object(NM_IF, active)
                props = dbus.Interface(obj, dbus_interface=DBUS_PROP)
                connection = dbus_to_python(props.Get(NM_IF_C_A, "Connection"))
                devices = dbus_to_python(props.Get(NM_IF_C_A, "Devices"))

                # get name (id) from connection
                obj = self.bus.get_object(NM_IF, connection)
                iface = dbus.Interface(obj, dbus_interface=NM_IF_S_C)
                settings = dbus_to_python(iface.GetSettings())
                name = settings["connection"]["id"]
                uuid = settings["connection"]["uuid"]
                self.connections_uuid[name] = uuid

                # for all devices:
                for device in devices:
                    obj = self.bus.get_object(NM_IF, device)
                    props = dbus.Interface(obj, dbus_interface=DBUS_PROP)
                    # get interface from device (first try: IpInterface)
                    iface = dbus_to_python(props.Get(NM_IF_D, "IpInterface"))
                    if iface == "":
                        iface = dbus_to_python(props.Get(NM_IF_D, "Interface"))
                    self.connections[iface] = name

        except Exception as msg:
            print(msg)

        self.update_tooltip()

    def warning(self, text, secondary_text=None):
        self.message(text, secondary_text=secondary_text,
                     msg_type=Gtk.MessageType.WARNING)

    def error(self, text, secondary_text=None):
        self.message(text, secondary_text=secondary_text,
                     msg_type=Gtk.MessageType.ERROR)

    def message(self, text, secondary_text=None, msg_type=Gtk.MessageType.INFO,
                buttons=[ ("gtk-close", Gtk.ResponseType.CLOSE) ]):
        dialog = Gtk.MessageDialog(None, 0, msg_type, None, text)
        dialog.set_title(_("Firewall-applet"))
        for (button, response) in buttons:
            dialog.add_button(button, response)
        if secondary_text != None:
            dialog.format_secondary_text(secondary_text)
        dialog.show_all()
        result = dialog.run()
        dialog.hide()
        return result

# MAIN

if len(sys.argv) > 1:
    print("""Usage: %s [options]

Options:
  -h, --help     show this help message and exit
"""  % sys.argv[0])
    sys.exit(1)

mainloop = GLib.MainLoop()
applet = TrayApplet()
mainloop.run()
