/*
 * Copyright (C) 2009-2017 Alistair Neil <info@dazzleships.net>
 *
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package client;

import java.awt.Color;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ItemEvent;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JFileChooser;
import javax.swing.JPopupMenu;
import javax.swing.JProgressBar;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.event.TableModelEvent;
import javax.swing.table.TableRowSorter;
import lib.AboutDialog;
import lib.DesktopNotify;
import lib.ExtensionFileFilter;
import lib.GTKFixes;
import lib.InfoDialog;
import lib.TextResourceDialog;
import lib.Localisation;
import lib.NetFunctions;
import lib.OSFunction;
import lib.SimpleFile;
import lib.SimpleProps;
import lib.SwingTrayIcon;
import lib.TrayPopupMenu;
import lib.Utilities;

/**
 *
 * @author Alistair Neil <info@dazzleships.net>
 */
public class SelekTOR extends javax.swing.JFrame {

    private static final Localisation LOCAL = new Localisation("resources/MessagesBundle");
    private static final String EMPTYSTR = "";
    private static final String PREFSVER = "1.01";
    private static final String APPNAME = "SelekTOR";
    private static final String APPVER = "3.13.73";
    private static final String APPSERIES = "3xx";
    private static final String WEBDOMAIN = "www.dazzleships.net";
    private static final String FILEDOMAIN = "http://dazzle.000webhostapp.com";
    private static final String HOMEPAGE = "http://" + WEBDOMAIN;
    private static final String HOMEPAGE_SSL = "https://" + WEBDOMAIN;
    private static final String CONTACT = HOMEPAGE_SSL + "/?page_id=247";
    private static final String APPUPDATE = FILEDOMAIN + "/anapps/linux/selektor/";
    private static final String TESTURL = "http://google.com";
    private static final String TORPROJECT = "https://torproject.org/";
    private static final String TORCHECK = "https://check.torproject.org/";
    private static final String ATLAS = "https://atlas.torproject.org/";
    private static final String PATREON = "https://www.patreon.com/SelekTOR";
    private static final String DOCSPATH = "/usr/share/doc/selektor/";
    private static final String EXITFAVOURITES = "favourites.txt";
    private static final String GUARDWHITELIST = "guardwhitelist.txt";
    private static final String GEOIPZIP = "geoip.zip";
    private static final String GEOIPSHA = "geoip.sha";
    private static final String ICONFILE = "selektor.png";
    private static final int TESTING_DISABLED = 0;
    private static final int TESTING_ACTIVE = -1;
    private static final long MAXTHRESHOLD = 5000;
    private String clientLocation;
    private String settingsPath;
    private String cachePath;
    private String defaultGeoipPath;
    private String geoipPath;
    private String geoip6Path;
    private String iconPath = EMPTYSTR;
    private TorController tcMain;
    private NodeList nodeList;
    private PacFactory pacFactory;
    private String strExitISOCode = "GB";
    private String strRequestedExit = EMPTYSTR;
    private NodeItem niActiveGuard;
    private NodeItem niActiveMiddle;
    private NodeItem niActiveExit;
    private int intRowSelected = -1;
    private ExitNodeTableModel entm;
    private SimpleProps sp;
    private final ArrayList<TorController> alTestThreads = new ArrayList<>();
    private int intActiveNodeTests = 0;
    private int intBestNodeIndex = 0;
    private String[] strArrBestNodes;
    private long lngThreshold = MAXTHRESHOLD;
    private int intThresholdExceeded = 0;
    private NetFunctions nf;
    private boolean boolNetEnabled = true;
    private String strSecret;
    private String strHash;
    private final ArrayList<String> alTestQueue = new ArrayList<>();
    private Timer tmr;
    private int intBeginTesting;
    private int intLatencyCheck;
    private int intAutoSelectCheck;
    private long lngHibernateTimeStamp;
    private volatile boolean boolFirstRun;
    private ArrayList<String> alArgs;
    private TorMonFrame tmf;
    private long startupDelay;
    private DesktopNotify dn;
    private SwingTrayIcon sti;
    private String desktopname = EMPTYSTR;
    private String compositor = EMPTYSTR;
    private boolean updateCheckDisabled;

    /**
     * Initialises SelekTOR UI Frame
     *
     * @param args
     */
    public SelekTOR(String[] args) {

        boolFirstRun = true;

        // Initialises UI theme to system default
        Utilities.loadUIStyle("System");

        // Check for multiple instances of this app
        if (OSFunction.isMultipleInstance(APPNAME)) {
            openInstanceDialog();
            cleanupExit();
            return;
        }

        // Initialise some essentialvariables
        // I know this looks a bit odd but trust me this allows us to install and run from anywhere
        String runPath = OSFunction.getUsersCurrentFolder() + OSFunction.getFileSeparator();
        if (runPath.startsWith("/usr/share")) {
            runPath = "/usr/share/";
        }
        iconPath = runPath + "icons" + OSFunction.getFileSeparator();
        settingsPath = OSFunction.getAppSettingsPath(APPNAME, APPSERIES);
        cachePath = settingsPath + "cache" + OSFunction.getFileSeparator();

        // Initialise property settings
        initProperties();

        // Initialise pac factory
        initPacFactory();

        // Get the desktop
        desktopname = OSFunction.getActiveDesktop();
        compositor = OSFunction.getCompositor();

        // Process any supplied cli arguments 
        if (processCLIArgs(args)) {
            cleanupExit();
            return;
        }
        // Ensure any old proxy settings are cleaned up
        pacCleanup();

        // Verify Tor client is installed
        if (OSFunction.isWindows()) {
            clientLocation = OSFunction.findFile("tor.exe", "Tor/");
        } else {
            clientLocation = OSFunction.findFile("tor", "/usr/sbin/", "/usr/bin/");
        }
        if (clientLocation == null) {
            Logger.getGlobal().log(Level.SEVERE, "Tor client not found.");
            openTorClientDialog();
            cleanupExit();
            return;
        }

        // Verify if geoip data is installed
        defaultGeoipPath = "/usr/share/tor/";
        geoipPath = OSFunction.findFile("geoip", settingsPath, defaultGeoipPath);
        geoip6Path = OSFunction.findFile("geoip6", settingsPath, defaultGeoipPath);
        if (geoipPath == null || geoip6Path == null) {
            Logger.getGlobal().log(Level.SEVERE, "Geoip not found.");
            openTorClientDialog();
            cleanupExit();
            return;
        }

        // Verify if gsettings is installed
        if (OSFunction.getGsettingsPath() == null) {
            Logger.getGlobal().log(Level.SEVERE, "Gsettings not found.");
            openGsettingsDialog();
            cleanupExit();
            return;
        }

        // Netbeans components init
        initComponents();
        Utilities.preloadFileChooser();
        updateProgramStatus(LOCAL.getString("progstatus_initial"));

        // Process any startup delay
        try {
            Thread.sleep(startupDelay);
        } catch (InterruptedException ex) {
        }
        startupDelay = -1;

        // Backup proxy preferences
        pacFactory.backupProxyPrefs();
        pacFactory.setProxyAutoConfigURL(null);

        // Initialise node table
        initNodeTables();

        // Initialise tor main and test threads
        initTorMain();

        // Do a Tor version check
        float torversion = tcMain.getVersion();
        Logger.getGlobal().logp(Level.INFO, this.getClass().getName(), "", "Tor version = " + torversion);
        if (torversion < 0.2420) {
            openToOldDialog();
            cleanupExit();
            return;
        }

        // Create testing threads
        initTestThreads(sp.getInt("PREF_TOR_TESTHREADS"));

        // Final Initialisation  of other components which need to be done last
        initFinalComponents();

        // System tray
        initSystemTray();

        // Main program timer
        initTimer();

        // Do tor main start
        applyMainTorSettings();
        tcMain.setStartupDelay(1000);
        tcMain.start();

        initGUIVisibility();
        Logger.getGlobal().log(Level.INFO, "Environment = {0}", System.getenv().toString());
        Logger.getGlobal().log(Level.INFO, "Active desktop = {0}", desktopname);
        Logger.getGlobal().log(Level.INFO, "Compositor = {0}", compositor);
        if (Logger.getGlobal().isLoggable(Level.FINE)) {
            for (String s : OSFunction.getCachedProcesses()) {
                Logger.getGlobal().log(Level.FINE, "Process = {0}", s);
            }
        }
    }

    /**
     * Initialise properties
     */
    private void initProperties() {

        // Initialise application title
        setTitle(APPNAME);
        // Application icon
        setIconImage(new javax.swing.ImageIcon(iconPath + ICONFILE).getImage());

        // Initial logging settings
        Logger.getGlobal().setLevel(Level.SEVERE);
        addLoggingFile(settingsPath + "status.log", false);

        // Init protperties
        sp = new SimpleProps(settingsPath + APPNAME + ".xml", APPNAME + " Settings");
        // Set up our defaults
        sp.setDefaultModeEnabled(true);
        sp.setString("PREF_ISOCOUNTRY", "GB");
        sp.setInt("PREF_LISTENPORT", 9054);
        sp.setInt("PREF_TOR_TESTHREADS", 2);
        sp.setInt("PREF_TORLOGLEV", 2);
        sp.setInt("PREF_PROXY_MODE", 2);
        sp.setLong("PREF_GEODATE", 0);
        sp.setBool("PREF_GEOCHECK", true);
        sp.setBool("PREF_AUTOSTART", false);
        sp.setBool("PREF_NOSYSTRAY", false);
        sp.setBool("PREF_HIDETOTRAY", false);
        sp.setBool("PREF_MINONCLOSE", false);
        sp.setBool("PREF_HIDE_MIN", false);
        sp.setBool("PREF_UPDATECHECK", true);
        sp.setBool("PREF_SAFESOCKS", true);
        sp.setBool("PREF_TESTSOCKS", false);
        sp.setBool("PREF_AVOIDDISK", true);
        sp.setBool("PREF_SAFELOG", true);
        sp.setBool("PREF_CACHEDELETE", false);
        sp.setBool("PREF_DISABLE_NOTIFY", false);
        sp.setString("PREF_HTTP_PROXY", EMPTYSTR);
        sp.setString("PREF_DONOT_PROXY", "localhost,127.0.0.1");
        sp.setString("PREF_TORBRIDGE", EMPTYSTR);
        sp.setString("PREF_TORARGS", EMPTYSTR);
        sp.setDefaultModeEnabled(false);
        sp.resetToDefaults();
        sp.load();
        // This will reset the preferences file to its default if we dont get a preferences version match
        if (!sp.getString("prefsver").contentEquals(PREFSVER)) {
            sp.resetToDefaults();
            sp.setString("prefsver", PREFSVER);
            sp.delete();
            sp.save();
        }
        // Dont allow listen ports below 9054, prevents any conflicts with the Tor default service.
        // Which can be used along side SelekTOR
        if (sp.getInt("PREF_LISTENPORT") < 9054) {
            sp.setInt("PREF_LISTENPORT", 9054);
            sp.save();
        }
        strExitISOCode = sp.getString("PREF_ISOCOUNTRY");
    }

    /**
     * Initialise logging facilities
     *
     * @param logpath
     * @param append
     */
    private void addLoggingFile(String logpath, boolean append) {
        try {
            if (logpath != null) {
                FileHandler fh = new FileHandler(logpath, 10000000, 1, append);
                fh.setFormatter(new SimpleFormatter());
                Logger.getGlobal().addHandler(fh);
            }
        } catch (SecurityException | IOException ex) {
        }
    }

    /**
     * Processes any supplied arguments post UI component build
     *
     * @param args Startup arguments
     */
    private boolean processCLIArgs(String... args) {
        alArgs = new ArrayList<>();
        alArgs.addAll(Arrays.asList(args));
        for (String s : args) {
            if (s.contentEquals("--start-delay")) {
                alArgs.remove(s);
                if (alArgs.size() > 0) {
                    String strDelay = alArgs.get(0);
                    try {
                        startupDelay = Long.valueOf(strDelay) * 1000;
                        alArgs.remove(0);
                    } catch (NumberFormatException ex) {
                    }
                }
            }
            if (s.contentEquals("--debug-all")) {
                alArgs.remove(s);
                if (!alArgs.isEmpty()) {
                    addLoggingFile(alArgs.get(0), false);
                    alArgs.remove(0);
                }
                Logger.getGlobal().setLevel(Level.ALL);
            }
            if (s.contentEquals("--debug-warn")) {
                alArgs.remove(s);
                if (!alArgs.isEmpty()) {
                    addLoggingFile(alArgs.get(0), false);
                    alArgs.remove(0);
                }
                Logger.getGlobal().setLevel(Level.WARNING);
            }
            if (s.contentEquals("--debug-info")) {
                alArgs.remove(s);
                if (!alArgs.isEmpty()) {
                    addLoggingFile(alArgs.get(0), false);
                }
                Logger.getGlobal().setLevel(Level.INFO);
            }
            if (s.contentEquals("--debug-fine")) {
                alArgs.remove(s);
                if (!alArgs.isEmpty()) {
                    addLoggingFile(alArgs.get(0), false);
                    alArgs.remove(0);
                }
                Logger.getGlobal().setLevel(Level.FINE);
            }
            if (s.contentEquals("--debug-finer")) {
                alArgs.remove(s);
                if (!alArgs.isEmpty()) {
                    addLoggingFile(alArgs.get(0), false);
                    alArgs.remove(0);
                }
                Logger.getGlobal().setLevel(Level.FINER);
            }
            if (s.contentEquals("--debug-finest")) {
                alArgs.remove(s);
                if (!alArgs.isEmpty()) {
                    addLoggingFile(alArgs.get(0), false);
                    alArgs.remove(0);
                }
                Logger.getGlobal().setLevel(Level.FINEST);
            }
            if (s.contentEquals("--reset-defaults")) {
                alArgs.remove(s);
                sp.resetToDefaults();
                sp.save();
                sp.load();
                return true;
            }
            if (s.contentEquals("--clear-cache")) {
                alArgs.remove(s);
                SimpleFile.deleteFolder(cachePath);
            }
            if (s.contentEquals("--help")) {
                alArgs.remove(s);
                outputHelp();
                // Disable proxy restore on exit
                pacFactory = null;
                return true;
            }
            if (s.contentEquals("--cleanup")) {
                alArgs.remove(s);
                if (!sp.getBool("PREF_AUTOSTART")) {
                    return true;
                }
            }
            if (s.contentEquals("--default-proxy-quiet")) {
                alArgs.remove(s);
                pacFactory.setProxyAutoConfigURL(null);
                pacFactory.deleteProxyPrefs();
                // Disable proxy restore on exit
                pacFactory = null;
                return true;
            }
            if (s.contentEquals("--restore-proxy-quiet")) {
                alArgs.remove(s);
                return true;
            }
            if (s.contentEquals("--update-disable")) {
                alArgs.remove(s);
                updateCheckDisabled = true;
            }
            if (s.contentEquals("--restore-proxy")) {
                alArgs.remove(s);
                InfoDialog id = createInfoDialog();
                id.createWarn(LOCAL.getString("dlg_restoreproxy_title"),
                        LOCAL.getString("dlg_restoreproxy_body"));
                id.pack();
                id.setVisible(true);
                if (id.getReturnStatus() == InfoDialog.OK) {
                    return true;
                }
                // Disable proxy restore on exit
                pacFactory = null;
                return true;
            }
            if (s.contentEquals("--default-proxy")) {
                alArgs.remove(s);
                InfoDialog id = createInfoDialog();
                id.createWarn(LOCAL.getString("dlg_resetproxy_title"),
                        LOCAL.getString("dlg_resetproxy_body"));
                id.pack();
                id.setVisible(true);
                if (id.getReturnStatus() == InfoDialog.OK) {
                    pacFactory.setProxyAutoConfigURL(null);
                    pacFactory.deleteProxyPrefs();
                }
                // Disable proxy restore on exit
                pacFactory = null;
                return true;
            }
            if (s.contentEquals("--ignore-desktop")) {
                alArgs.remove(s);
                desktopname = EMPTYSTR;
            }
        }
        if (alArgs.size() > 0) {
            outputHelp();
            // Disable proxy restore on exit
            pacFactory = null;
            return true;
        }
        return false;
    }

    /**
     * Output on standard out command line help info
     */
    private void outputHelp() {
        System.out.println(APPNAME + " " + APPVER + " valid command line arguments.");
        System.out.println();
        System.out.println("--help : This help information");
        System.out.println("--debug-all [filename] : All debug output.");
        System.out.println("--debug-warn [filename] : Warnings level debug output.");
        System.out.println("--debug-info [filename] : Information level debug output.");
        System.out.println("--debug-fine [filename] : Fine level debug output.");
        System.out.println("--debug-finer [filename] : Finer level debug output.");
        System.out.println("--debug-finest [filename] : Finest level debug output.");
        System.out.println("--reset-defaults : Return user settings to their safe defaults.");
        System.out.println("--clear-cache : Clears the Tor cache on startup.");
        System.out.println("--ignore-desktop : Ignore desktop detection information.");
        System.out.println("--start-delay : Delay startup in seconds, eg. --start-delay 5");
        System.out.println("--cleanup : Cleans up after a bad shutdown which may have left system proxy in an unknown state.");
        System.out.println("--default-proxy-quiet : Set system proxy back to its defaults quietly, without GUI dialog.");
        System.out.println("--restore-proxy-quiet : Attempt to restore proxy to its last known settings using a previously saved backup, "
                + "without GUI dialog.");
        System.out.println("--default-proxy : Set system proxy back to its defaults accepts verification input via a GUI dialog..");
        System.out.println("--restore-proxy : Attempt to restore proxy to its last known settings using a previously saved backup, "
                + "with input verification via a  GUI dialog.");
        System.out.println("--update-disable : Disables the version update checking at startup and removes "
                + "the option from preferences.");
        System.out.println();
    }

    /**
     * Sets the enabled status of various GUI objects
     *
     * @param enabled
     */
    private void setComponentsEnabled(boolean enabled) {
        jButtonExitNodeDetails.setEnabled(enabled);
        jButtonMiddleNodeDetails.setEnabled(enabled);
        jButtonGuardNodeDetails.setEnabled(enabled);
        jButtonSetGuards.setEnabled(enabled);
        jMenuItemPatternEdit.setEnabled(enabled);
        jMenuItemAddPattern.setEnabled(enabled);
        jTableExitNodes.setEnabled(enabled);
        jComboCountry.setEnabled(enabled);
        jMenuItemTorCheck.setEnabled(enabled);
        jCheckProxyDisable.setEnabled(enabled);
        jCheckProxyByPattern.setEnabled(enabled);
        jCheckProxyAll.setEnabled(enabled);
        jCheckTrayProxyDisable.setEnabled(enabled);
        jCheckTrayProxyByPattern.setEnabled(enabled);
        jCheckTrayProxyAll.setEnabled(enabled);
        jButtonClearFavs.setEnabled(enabled);
        if (!enabled) {
            jMenuItemGeoip.setEnabled(enabled);
        }
    }

    /**
     * Final initialisation of objects that need to be done near the end of
     * startup
     *
     */
    private void initFinalComponents() {

        // Apply fixes for linux gtk issues in java
        GTKFixes.fixMenubarHeight(jMenuBar, jMenuItemGUI);
        GTKFixes.fixMenuSelectionColor(jMenuMenu, jMenuProxy, jMenuHelp);
        GTKFixes.fixJMenuPopupBorder(jMenuMenu, jMenuProxy, jMenuHelp);
        GTKFixes.fixMenuItemFgColor(jCheckProxyDisable, jCheckProxyByPattern, jCheckProxyAll);
        GTKFixes.fixMenuItemFgColor(jCheckTrayProxyDisable, jCheckTrayProxyByPattern, jCheckTrayProxyAll);

        // Disable certain components until first node is activated
        setComponentsEnabled(false);

        // Add proxy buttons to proxy button groups
        buttonGroupProxy.add(jCheckProxyDisable);
        buttonGroupProxy.add(jCheckProxyByPattern);
        buttonGroupProxy.add(jCheckProxyAll);
        buttonGroupProxyTray.add(jCheckTrayProxyDisable);
        buttonGroupProxyTray.add(jCheckTrayProxyByPattern);
        buttonGroupProxyTray.add(jCheckTrayProxyAll);

        // Update initial proxy selection
        switch (sp.getInt("PREF_PROXY_MODE")) {
            case 0:
                jCheckProxyDisable.setSelected(true);
                break;
            case 1:
                jCheckProxyByPattern.setSelected(true);
                break;
            case 2:
                jCheckProxyAll.setSelected(true);
                break;
        }

        // Make debug log menu item visible if required
        jMenuItemDebugLog.setVisible(Logger.getGlobal().getLevel() != Level.SEVERE);

    }

    /**
     * Initialise our internal 1 second timer tick
     *
     */
    private void initTimer() {
        tmr = new Timer(1000, new java.awt.event.ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    // Do a hibernate check, check to see if computer has been asleep for more than 60 secs
                    if ((System.currentTimeMillis() - lngHibernateTimeStamp) > 60000) {
                        Logger.getGlobal().logp(Level.INFO, SelekTOR.class.getName(),
                                "initTimer()", "Hibernate Recovery");
                        lngHibernateTimeStamp = System.currentTimeMillis();
                        setComponentsEnabled(false);
                        stopAllTorThreads();
                        boolFirstRun = true;
                        tcMain.start();
                    }
                    lngHibernateTimeStamp = System.currentTimeMillis();

                    if (!boolNetEnabled) {
                        return;
                    }

                    // Do Latency checking
                    if (intLatencyCheck > 0) {
                        intLatencyCheck--;
                        if (intLatencyCheck == 0) {
                            intLatencyCheck = 5;
                            getFreshNodeInfo();
                            // Tor latency check, only when node active
                            if (niActiveExit != null) {
                                // Do latency check
                                tcMain.doLatencyCheck(4950);
                            }
                        }
                    }

                    // Activate a node selection check
                    if (intAutoSelectCheck > 0) {
                        intAutoSelectCheck--;
                        if (intAutoSelectCheck == 0) {
                            autoSelectNode();
                        }
                    }

                    // Activate a testing cycle
                    if (intBeginTesting > 0) {
                        intBeginTesting--;
                        if (intBeginTesting == 0) {
                            runTestingCycle();
                        }
                    }
                } catch (Exception ex) {
                    Logger.getGlobal().logp(Level.WARNING, this.getClass().getName(), "initTimer() actionPerformed", EMPTYSTR, ex);
                }
            }
        });
        lngHibernateTimeStamp = System.currentTimeMillis();
        tmr.setInitialDelay(0);
        tmr.setRepeats(true);
        tmr.start();

    }

    /**
     * Initialise our pacfactory object
     *
     */
    private void initPacFactory() {
        pacFactory = new PacFactory();
        pacFactory.setTempFolder(cachePath);
        pacFactory.setBackupFolder(settingsPath + "backup");
        pacFactory.setPatternsFolder(settingsPath + "patterns");
        pacFactory.setPACFolder(settingsPath);
        pacFactory.setDefaultProxy(sp.getString("PREF_HTTP_PROXY"));
        pacFactory.setDoNotProxy(sp.getString("PREF_DONOT_PROXY"));
    }

    /**
     * Open the preferences dialog
     */
    private void openPreferencesDialog() {

        // open dialog
        PrefsDialog pd;
        String disableditems = EMPTYSTR;
        if (!sti.isSupported()) {
            sp.setBool("PREF_NOSYSTRAY", true);
            disableditems += "jCheckDisableTray";
        }
        if (updateCheckDisabled) {
            disableditems += ",jCheckAppUpdate";
        }
        pd = new PrefsDialog(this, sp, disableditems);
        pd.setTitle(LOCAL.getString("wintitle_prefs").replace("$appname", APPNAME));
        pd.pack();
        pd.setLocationRelativeTo(this);
        pd.setVisible(true);

        // Check for changes
        String changed = sp.getChangedProperties();
        if (pd.getReturnStatus() == PrefsDialog.APPLY) {
            sp.save();
        }

        if (changed.contains("PREF_HTTP_PROXY")) {
            pacFactory.setDefaultProxy(sp.getString("PREF_HTTP_PROXY"));
            rebuildPatternPac();
        }

        if (changed.contains("PREF_NOSYSTRAY")) {
            if (sp.getBool("PREF_NOSYSTRAY")) {
                sti.unload();
            } else {
                sti.load();
            }
        }

        if (changed.contains("PREF_DISABLE_NOTIFY")) {
            dn.setEnabled(!sp.getBool("PREF_DISABLE_NOTIFY"));
            if (dn.isEnabled() && !dn.isSupported()) {
                openLibnotifyDialog();
            }
        }

        if (!boolNetEnabled) {
            return;
        }

        // Check to see if bridges entry was changed
        if (changed.contains("TORBRIDGE")) {
            // Update status message
            updateProgramStatus(LOCAL.getString("progstatus_applychanges"));
            // Validate bridge entry
            if (!tcMain.validateBridges(sp.getString("PREF_TORBRIDGE"))) {
                // Validation failed
                sp.setString("PREF_TORBRIDGE", EMPTYSTR);
                sp.save();
                // Apply default bridge settings by doing hard Tor restart
                stopAllTorThreads();
                applyMainTorSettings();
                tcMain.start();
                // Indicate validation failure to user
                openBridgeErrorDialog();
            } else {
                // Apply default bridge settings by doing hard Tor restart
                stopAllTorThreads();
                applyMainTorSettings();
                tcMain.start();
            }
            return;
        }

        // Check for any parameters that have changed that require full restart     
        if (changed.contains("LISTENPORT") || changed.contains("SAFE")
                || changed.contains("AVOID")
                || changed.contains("LOGLEV") || changed.contains("TORARGS")
                || changed.contains("TESTSOCKS")) {
            // Update status message
            updateProgramStatus(LOCAL.getString("progstatus_applychanges"));
            // Apply default bridge settings by doing hard Tor restart
            stopAllTorThreads();
            applyMainTorSettings();
            tcMain.start();
        }
    }

    private void applyMainTorSettings() {

        // Check to see if bridges are active
        String bridge = sp.getString("PREF_TORBRIDGE");
        if (!tcMain.validateBridges(bridge)) {
            bridge = EMPTYSTR;
            sp.setString("PREF_TORBRIDGE", bridge);
            sp.save();
        }
        // Update entry node label on UI
        if (bridge.isEmpty()) {
            jLabelGuard.setText(LOCAL.getString("label_guardnode"));
        } else {
            jLabelGuard.setText(LOCAL.getString("label_bridgenode"));
        }

        tcMain.setBridges(bridge);
        tcMain.setTestingURL(TESTURL);
        tcMain.setListenPort(sp.getInt("PREF_LISTENPORT"));
        tcMain.setEntryNodes(nodeList.getGuardFavouritesAsCSV());
        tcMain.setBoolTorOption("SafeSocks", sp.getBool("PREF_SAFESOCKS"));
        tcMain.setBoolTorOption("TestSocks", sp.getBool("PREF_TESTSOCKS"));
        tcMain.setBoolTorOption("AvoidDiskWrites", sp.getBool("PREF_AVOIDDISK"));
        tcMain.setBoolTorOption("SafeLogging", sp.getBool("PREF_SAFELOG"));
        tcMain.setExternalArgs(sp.getString("PREF_TORARGS"));
        tcMain.setLogLevel(sp.getInt("PREF_TORLOGLEV"));
        tcMain.setGeoIP4(geoipPath);
        tcMain.setGeoIP6(geoip6Path);
        tmf.setLaunchString("# Via commandline\n"
                + tcMain.getCLIOptionsAsString() + "\n\n"
                + "# Via torrc configuration file\n"
                + tcMain.getTorrcOptionsAsString()
        );
    }

    /**
     * Abort actions on all tor client threads
     */
    private void abortAllTorThreads() {
        if (niActiveExit != null) {
            niActiveExit.setCircuitHops(null);
        }
        if (tcMain != null) {
            tcMain.abortActions();
        }
        abortTesting();
    }

    /**
     * Bring all tor threads to a stop
     */
    private void stopAllTorThreads() {
        abortTesting();
        stopTestClients();
        if (nodeList != null) {
            nodeList.terminate();
        }
        if (tcMain != null) {
            tcMain.stop();
            tcMain.clearStdout();
            tcMain.setStartupDelay(2000);
        }
    }

    /**
     * Do nodelist refresh
     */
    private void regenNodelist() {
        Thread t = new Thread(new java.lang.Runnable() {

            @Override
            public void run() {
                if (boolFirstRun) {
                    updateProgramStatus(LOCAL.getString("progstatus_generate"));
                    nodeList.refreshNodelist(tcMain);
                    // App update check
                    checkForAppUpdates();
                }
                switch (nodeList.getCurrentStatus()) {
                    case NodeList.NODELIST_BUILT:
                        SwingUtilities.invokeLater(new java.lang.Runnable() {
                            @Override
                            public void run() {
                                if (boolFirstRun) {
                                    dn.setNotificationBody(LOCAL.getString("traymenu_proxymode" + (sp.getInt("PREF_PROXY_MODE") + 1)));
                                    applyProxyChanges(sp.getInt("PREF_PROXY_MODE"));
                                }
                                // Update countries combo box
                                String[] countries = nodeList.getValidatedCountries();
                                DefaultComboBoxModel<String> dcbm = new DefaultComboBoxModel<>(countries);
                                jComboCountry.setModel(dcbm);
                                jComboCountry.setSelectedIndex(-1);
                                String strSelection = strExitISOCode + "," + LOCAL.getDisplayCountry(strExitISOCode);
                                // Check selection exists
                                if (dcbm.getIndexOf(strSelection) < 0) {
                                    // Does not exist so default to the first country entry
                                    jComboCountry.setSelectedIndex(0);
                                } else {
                                    jComboCountry.setSelectedItem(strSelection);
                                }
                                // Check to make shure we have enough guard nodes
                                int activeguards = nodeList.saveGuardWhitelist();
                                if (activeguards < 3 && activeguards > 0) {
                                    openNotEnoughGuardsDialog();
                                }
                            }
                        });
                        break;
                    case NodeList.NODELIST_FAILED:
                        SwingUtilities.invokeLater(new java.lang.Runnable() {
                            @Override
                            public void run() {
                                Logger.getGlobal().logp(Level.WARNING, SelekTOR.class
                                        .getName(), "fetchRemoteData()", "Nodelist Failed");
                                openFatalErrorDialog(LOCAL.getString("dlg_nodelistfail_body"));
                                cleanupExit();
                            }
                        });
                        break;
                }

            }
        });
        t.start();
    }

    /**
     * Check remoteversion to determine if their is a newer application
     * available
     *
     */
    private void checkForAppUpdates() {

        // App update
        if (!sp.getBool("PREF_UPDATECHECK") || updateCheckDisabled) {
            Logger.getGlobal().log(Level.WARNING, "Update check is disabled.");
            return;
        }
        TorCircuit tc = tcMain.getLatestCircuit();
        if (tc != null) {
            String finger = tc.getExit(TorCircuit.FINGER);
            NodeItem ni = nodeList.getNode(finger);
            if (ni != null) {
                Logger.getGlobal().log(Level.INFO, "Active Exit is {0}({1}) in {2}",
                        new Object[]{ni.getNickName(), finger, ni.getCountryName()});
            }
        }
        String url = APPUPDATE + "version.html";
        Socket s = tcMain.createTorSocketToURL(url, false);
        nf.setSocketTimeout(5000);
        String version = nf.getURLContentAsString(url, s);
        if (version == null) {
            Logger.getGlobal().log(Level.WARNING, "Failed to retrieve version info.");
            return;
        }
        Logger.getGlobal().log(Level.INFO, "Sucessfully retrieved version info.");
        final String remoteversion = version.trim();
        SwingUtilities.invokeLater(new java.lang.Runnable() {
            @Override
            public void run() {
                if (Utilities.isNewerVersion(APPVER, remoteversion)) {
                    InfoDialog id = createInfoDialog();
                    id.createInfo(LOCAL.getString("dlg_update_title"),
                            LOCAL.getString("dlg_update_body").replace("$version", remoteversion));
                    id.setAutoClose(30000);
                    id.pack();
                    id.setVisible(true);
                }
            }
        });
    }

    /**
     * Check to see if we need to update geoip
     *
     */
    private void updateGeoipCheck() {
        jMenuItemGeoip.setEnabled(true);
        if (!sp.getBool("PREF_GEOCHECK")) {
            return;
        }
        long lngDate = sp.getLong("PREF_GEODATE");
        if (System.currentTimeMillis() > lngDate) {
            updateGeoip();
        }

    }

    /**
     * Pops up the GEOIP update dialog
     */
    private void updateGeoip() {

        InfoDialog id = new InfoDialog(this) {

            private final InfoDialog id = this;

            @Override
            public void progressTask(int retstatus, final JProgressBar jpb) {
                Thread t = new Thread(new java.lang.Runnable() {

                    @Override
                    public void run() {
                        String url = APPUPDATE + GEOIPSHA;
                        String destfile = OSFunction.getTempFolder(EMPTYSTR) + APPNAME
                                + "_" + GEOIPZIP;
                        try {
                            setAckEnabled(false);
                            jpb.setString(LOCAL.getString("dload_status_contact"));
                            Socket s;
                            // Grab checksum file from dazzleships
                            if (isCheckBoxSelected()) {
                                if (niActiveExit == null) {
                                    // Indicate download failure
                                    Logger.getGlobal().log(Level.INFO, "Invalid exit node");
                                    throw new IOException();
                                }
                                if (niActiveExit.getLatency() > 4000) {
                                    // Indicate download failure
                                    Logger.getGlobal().log(Level.INFO, "Poor exit node");
                                    throw new IOException();
                                }
                                s = tcMain.createTorSocketToURL(url, false);
                            } else {
                                s = new Socket(FILEDOMAIN, 80);
                            }
                            nf.setSocketTimeout(5000);
                            nf.setProgressBar(jpb);
                            jpb.setStringPainted(true);
                            if (!id.isVisible()) {
                                throw new IOException();
                            }
                            Logger.getGlobal().log(Level.INFO, "Starting geoip.sha download");
                            String chksum = nf.getURLContentAsString(url, s);
                            if (chksum == null || chksum.isEmpty()) {
                                // Indicate download failure
                                Logger.getGlobal().log(Level.INFO, "geoip.sha download failed");
                                throw new IOException();
                            }
                            // Grab geodata zip file from dazzleships
                            if (!id.isVisible()) {
                                throw new IOException();
                            }
                            Logger.getGlobal().log(Level.INFO, "Starting geoip.zip download");
                            url = APPUPDATE + GEOIPZIP;
                            if (isCheckBoxSelected()) {
                                s = tcMain.createTorSocketToURL(url, false);
                            } else {
                                s = new Socket(FILEDOMAIN, 80);
                            }
                            final SimpleFile sf = new SimpleFile(destfile);
                            // Delete old/previous file
                            if (sf.exists()) {
                                sf.delete();
                            }
                            // Ensure zip file downloaded
                            nf.saveURLContentToFile(destfile, url, s, true);
                            if (!sf.exists()) {
                                // Indicate download failure
                                Logger.getGlobal().log(Level.INFO, "geoip.zip download failed");
                                throw new IOException();
                            }
                            // Do checksum verification
                            String fchk = Utilities.getSha1Sum(destfile);
                            if (!fchk.contentEquals(chksum.trim())) {
                                // Delete corrupt zip file
                                sf.delete();
                                // Indicate download failure
                                Logger.getGlobal().log(Level.INFO, "geoip.zip checksum does not match");
                                throw new IOException();
                            }
                            if (!id.isVisible()) {
                                throw new IOException();
                            }
                            if (sf.extractZipTo(settingsPath)) {
                                SwingUtilities.invokeLater(new java.lang.Runnable() {
                                    @Override
                                    public void run() {
                                        // Set new check time
                                        updateGeoipCheckDate();
                                        // Update geoip paths
                                        geoipPath = OSFunction.findFile("geoip", settingsPath, defaultGeoipPath);
                                        geoip6Path = OSFunction.findFile("geoip6", settingsPath, defaultGeoipPath);
                                        // Restart Tor client, to take advantage of new geoip
                                        jComboCountry.setEnabled(false);
                                        setComponentsEnabled(false);
                                        boolFirstRun = true;
                                        stopAllTorThreads();
                                        applyMainTorSettings();
                                        tcMain.start();
                                        setVisible(false);
                                        dispose();
                                    }
                                });
                            }
                        } catch (final Exception ex) {
                            // Exits here in case of download fail
                            SwingUtilities.invokeLater(new java.lang.Runnable() {
                                @Override
                                public void run() {
                                    Logger.getGlobal().log(Level.WARNING, ex.getMessage());
                                    jpb.setString(LOCAL.getString("dload_status_failed"));
                                    setAckEnabled(true);
                                }
                            });
                        }
                    }
                });
                if (retstatus == InfoDialog.OK) {
                    t.start();
                } else {
                    updateGeoipCheckDate();
                    setVisible(false);
                    dispose();
                }

            }
        };
        id.setTitle(APPNAME);
        id.setCancelButtonText(LOCAL.getString("button_cancel"));
        id.setContinueButtonText(LOCAL.getString("button_continue"));
        id.setCloseButtonText(LOCAL.getString("button_close"));
        id.setCheckBoxText(LOCAL.getString("chkbox_viator"));
        id.setCheckBoxVisible(true);
        id.setCheckBoxEnabled(true);
        id.pack();
        id.createProgress(LOCAL.getString("dlg_geo_title"),
                LOCAL.getString("dlg_geo_body"));
        id.pack();
        id.setVisible(true);
    }

    private void updateGeoipCheckDate() {
        // Set new check time
        sp.setLong("PREF_GEODATE", System.currentTimeMillis() + ((long) 60000 * 60 * 24 * 90));
        sp.save();
    }

    /**
     * System tray initialisation
     */
    private void initSystemTray() {

        // Initialise tray utility
        sti = new SwingTrayIcon(this.getGraphicsConfiguration(), "resources/trayicon.png");
        if (desktopname.contentEquals("gnome3")
                || desktopname.contentEquals("enlightenment")
                || desktopname.contentEquals("kde5")) {
            sti.disable();
        }

        // Initialise desktop notification
        dn = new DesktopNotify(sti, iconPath + ICONFILE);
        dn.setNotificationTitle(APPNAME);
        dn.setEnabled(!sp.getBool("PREF_DISABLE_NOTIFY"));
        if (dn.isEnabled() && !dn.isSupported()) {
            openLibnotifyDialog();
        }

        // If trayIcon valid add mouse listener
        TrayPopupMenu tpm = new TrayPopupMenu();
        GTKFixes.fixTrayMenuPopupBorder(tpm);
        tpm.add(jCheckTrayProxyDisable);
        tpm.add(jCheckTrayProxyByPattern);
        tpm.add(jCheckTrayProxyAll);
        tpm.add(new JPopupMenu.Separator());
        tpm.add(jMenuItemGUI);
        tpm.add(jMenuItemQuitx);
        sti.addMouseListener(new javax.swing.event.MouseInputListener() {

            @Override
            public void mouseClicked(MouseEvent e) {
            }

            @Override
            public void mousePressed(MouseEvent e) {
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                if (e.getButton() == MouseEvent.BUTTON1) {
                    toggleGUIVisibility();
                }
            }

            @Override
            public void mouseEntered(MouseEvent e) {
            }

            @Override
            public void mouseExited(MouseEvent e) {
            }

            @Override
            public void mouseDragged(MouseEvent e) {
            }

            @Override
            public void mouseMoved(MouseEvent e) {
            }
        });
        sti.setTrayPopupMenu(tpm);
        if (!sp.getBool("PREF_NOSYSTRAY")) {
            sti.load();
        }
    }

    private InfoDialog createInfoDialog() {
        InfoDialog id = new InfoDialog(this);
        id.setTitle(APPNAME);
        id.setCancelButtonText(LOCAL.getString("button_cancel"));
        id.setContinueButtonText(LOCAL.getString("button_continue"));
        id.setCloseButtonText(LOCAL.getString("button_close"));
        return id;
    }

    private void openInstanceDialog() {
        InfoDialog id = createInfoDialog();
        id.createError(LOCAL.getString("dlg_instancefail_title"),
                LOCAL.getString("dlg_instancefail_body").replace("$appname", APPNAME));
        id.pack();
        id.setVisible(true);
    }

    private void openNotEnoughGuardsDialog() {
        InfoDialog id = createInfoDialog();
        id.createWarn(LOCAL.getString("dlg_guardwarn_title"),
                LOCAL.getString("dlg_notenoughguards_body").replace("$appname", APPNAME));
        id.setCancelVisible(false);
        id.setContinueButtonText(LOCAL.getString("button_setguards"));
        id.pack();
        id.setVisible(true);
        if (id.getReturnStatus() == InfoDialog.OK) {
            openGuardsSettingDialog();
        }
    }

    private void openGsettingsDialog() {
        InfoDialog id = createInfoDialog();
        id.createError(LOCAL.getString("dlg_gsetting_title"),
                LOCAL.getString("dlg_gsetting_body").replace("$appname", APPNAME));
        id.pack();
        id.setVisible(true);
    }

    private void openTorClientDialog() {
        InfoDialog id = createInfoDialog();
        id.createError(LOCAL.getString("dlg_torclient_title"),
                LOCAL.getString("dlg_torclient_body").replace("$appname", APPNAME));
        id.pack();
        id.setVisible(true);
    }

    private void openLibnotifyDialog() {
        InfoDialog id = createInfoDialog();
        id.createError(LOCAL.getString("dlg_libnotify_title"),
                LOCAL.getString("dlg_libnotify_body").replace("$appname", APPNAME));
        id.setModal(false);
        id.setAutoClose(10000);
        id.pack();
        id.setVisible(true);
    }

    private void openBridgeErrorDialog() {
        InfoDialog id = createInfoDialog();
        id.createError(LOCAL.getString("dlg_bridgerr_title"),
                LOCAL.getString("dlg_bridgerr_body").replace("$appname", APPNAME));
        id.pack();
        id.setVisible(true);
    }

    /**
     * Node table and node list initialisation
     */
    private void initNodeTables() {
        // Create our node list storage object           
        nodeList = new NodeList(settingsPath, EXITFAVOURITES, GUARDWHITELIST);
        entm = new ExitNodeTableModel();
        entm.setColumnName(LOCAL.getString("exittable_col1"), 0);
        entm.setColumnName(LOCAL.getString("exittable_col2"), 1);
        entm.setColumnName(LOCAL.getString("exittable_col3"), 2);
        entm.setColumnName(LOCAL.getString("exittable_col4"), 3);
        entm.setColumnName(LOCAL.getString("exittable_col5"), 4);
        entm.addTableModelListener(new javax.swing.event.TableModelListener() {
            @Override
            public void tableChanged(TableModelEvent e) {
                if (e.getType() == TableModelEvent.UPDATE) {
                    if (e.getColumn() == 4) {
                        nodeList.saveExitFavourites();
                        // Force favourites recalc
                        intThresholdExceeded = 255;
                        // Do node auto selection in 5 secs
                        intAutoSelectCheck = 5;
                    }
                }
            }
        });
        nodeList.setExitNodeTableModel(entm);
        jTableExitNodes.setModel(entm);
        // Adjust table column widths
        adjustExitTableColumns();
    }

    private void clearFavourites() {
        for (int i = 0; i < entm.getRowCount(); i++) {
            entm.setValueAt(false, i, 4);
        }
    }

    /**
     * Main tor process initialisation
     */
    private void initTorMain() {

        intBeginTesting = TESTING_DISABLED;
        nf = new NetFunctions();
        tmf = new TorMonFrame();
        tmf.setTitle(LOCAL.getString("wintitle_tormonitor"));
        tmf.setIconImage(new javax.swing.ImageIcon(iconPath + ICONFILE).getImage());

        // Initialise the Tor main client event handler
        tcMain = new TorController(clientLocation, cachePath) {
            private int intNodeFailCount;

            @Override
            public void controllerEventFired(int event, Object data) {
                Logger.getGlobal().logp(Level.INFO, SelekTOR.class.getName(),
                        "controllerEventFired() on Port=" + getListenPort(), getEventMessage(event) + ", Data=" + data);
                switch (event) {
                    case TOR_BRIDGE:
                        nodeList.addBridge((String) data);
                        break;
                    case TOR_NOROUTE:
                        if (boolNetEnabled) {
                            boolNetEnabled = false;
                            boolFirstRun = true;
                            updateProgramStatus(LOCAL.getString("progstatus_nonet"));
                            dn.setNotificationBody(LOCAL.getString("progstatus_nonet"));
                            dn.raiseNotification();
                            setComponentsEnabled(false);
                        }
                        break;
                    case TOR_DIRINFO_STALE:
                    // Deliberate fallthrough here
                    case TOR_NOEXITS:
                        // Ensure client is fully running as multiple TOR_NOEXITS can
                        // be generated at startup especially if folks are torrenting via Tor
                        if (getStatus() < STATUS_IDLE) {
                            break;
                        }
                        updateProgramStatus(LOCAL.getString("circuit_status_none"));
                        intNodeFailCount = 0;
                        resetBestNode();
                        strRequestedExit = getBestNode();
                        if (strRequestedExit == null) {
                            strRequestedExit = EMPTYSTR;
                        }
                        stop();
                        setSilentBootEnabled(true);
                        setStartupDelay(2000);
                        start(TOR_RESTARTED);
                        break;
                    case TOR_CLOCK_ERROR:
                    // Deliberate fallthrough here
                    case TOR_BOOT_FATAL:
                        openFatalErrorDialog((String) data);
                        cleanupExit();
                        break;
                    case TOR_BOOT_TIMEOUT:
                        stop();
                        setStartupDelay(2000);
                        start(TOR_BOOTED);
                        break;
                    case TOR_MESSAGE:
                        updateProgramStatus((String) data);
                        break;
                    case TOR_BOOTED:
                        boolNetEnabled = true;
                        regenNodelist();
                        break;
                    case TOR_RESTARTED:
                        boolNetEnabled = true;
                        activateNode(strRequestedExit);
                        break;
                    case EVENT_CIRCUIT_FAILED:
                    // Deliberate fallthrough here                   
                    case EVENT_CIRCUITS_FAILED:
                        switch (++intNodeFailCount) {
                            case 1:
                                // Retry
                                updateProgramStatus(LOCAL.getString("progstatus_nodefailretry"));
                                intThresholdExceeded = 254;
                                intAutoSelectCheck = 2;
                                break;
                            case 2:
                                // Get the next best node
                                updateProgramStatus(LOCAL.getString("progstatus_nodefail"));
                                intThresholdExceeded = 4;
                                intAutoSelectCheck = 2;
                                break;
                            case 3:
                                // Get the next best node and trigger test cycle
                                updateProgramStatus(LOCAL.getString("progstatus_nodefail"));
                                intNodeFailCount = 0;
                                intThresholdExceeded = 4;
                                intAutoSelectCheck = 2;
                                if (intBeginTesting > 2) {
                                    intBeginTesting = 2;
                                }
                                break;
                        }
                        break;
                    case EVENT_CIRCUITS_BUILT:
                        if (!strRequestedExit.isEmpty()) {
                            NodeItem ni = nodeList.getNode(strRequestedExit);
                            if (ni == null) {
                                activateNode(strRequestedExit = EMPTYSTR);
                                break;
                            }
                            if (ni.getCircuitHops() != null) {
                                tcMain.activateCircuit(ni.getCircuitHops());
                                break;
                            }
                        }
                    case EVENT_CIRCUIT_BUILT:
                        intNodeFailCount = 0;
                        intThresholdExceeded = 0;
                        intLatencyCheck = 1;
                        // Trigger node testing
                        if (intBeginTesting == TESTING_DISABLED) {
                            intBeginTesting = 2;
                        }
                        if (boolFirstRun) {
                            boolFirstRun = false;
                            // Update geoip check
                            updateGeoipCheck();
                        }
                        break;
                    case EVENT_LATENCY_DONE:
                        if (niActiveExit != null) {
                            niActiveExit.setStreams(getActiveStreams().size());
                            niActiveExit.setLatency(getLatency());
                            updateGUINodeInfo(niActiveGuard, niActiveMiddle, niActiveExit);
                            // Check for threshold override
                            if (intThresholdExceeded >= 100) {
                                break;
                            }
                            if (niActiveExit.getLatency() > lngThreshold) {
                                intThresholdExceeded++;
                                if (niActiveExit.getLatency() > MAXTHRESHOLD) {
                                    // If we have 3 (15 sec) of outright latency failures then trigger test cycle
                                    if (intThresholdExceeded > 2) {
                                        intAutoSelectCheck = 2;
                                        if (intBeginTesting > 2) {
                                            intBeginTesting = 2;
                                        }
                                    }
                                    break;
                                }
                                // If we have only have one favourite then effectively 
                                // ignore latency
                                if (nodeList.getNumberOfFavs() == 1) {
                                    intThresholdExceeded = 0;
                                    break;
                                }
                                if (niActiveExit.getStreams() > 0) {
                                    // If we have active streams then do new selection after 1 minute
                                    // of threshold excesses
                                    if (intThresholdExceeded > 11) {
                                        intAutoSelectCheck = 2;
                                    }
                                } else {
                                    // If we have no active streams then do new selection after 15 secs
                                    // of threshold excesses
                                    if (intThresholdExceeded > 2) {
                                        intAutoSelectCheck = 2;
                                    }
                                }
                                break;
                            }
                            intThresholdExceeded = 0;
                        }
                        break;
                }
            }
        };

        // Tor main initial startup conditions
        tcMain.setTorrcOption("ClientOnly", "1");
        tcMain.setTorrcOption("StrictNodes", "1");
        tcMain.setTorrcOption("UseMicrodescriptors", "0");
        tcMain.setTorrcOption("UseEntryGuards", "1");
        tcMain.setTorrcOption("NumEntryGuards", "3");
        tcMain.setTorrcOption("ExitNodes", "{FI}");
        tcMain.setStdoutTextArea(tmf.getStdoutTextArea());
        tcMain.setListenPort(sp.getInt("PREF_LISTENPORT"));
        tcMain.setOwnershipID(OSFunction.getOurProcessID());
        strSecret = Utilities.generateSecretKey(16);
        strHash = Utilities.getTorHashPassword(strSecret);
        tcMain.setControlPassword(strSecret, strHash);
    }

    /**
     * Test thread initialisation
     *
     * @param nothreads
     */
    private void initTestThreads(int nothreads) {

        TorController tcTest;

        // Main testing procedure here, event driven this just handles process declaration
        // Initialise our tor testing thread prior to booting it
        while (alTestThreads.size() < nothreads) {
            tcTest = new TorController(clientLocation, cachePath) {
                private NodeItem ni;

                @Override
                public void controllerEventFired(int event, Object data) {

                    Logger.getGlobal().logp(Level.INFO, SelekTOR.class.getName(),
                            "controllerEventFired() on Port=" + getListenPort(), getEventMessage(event) + ", Data=" + data);

                    switch (event) {
                        case TOR_NOROUTE:
                            if (boolNetEnabled) {
                                abortTesting();
                                stopTestClients();
                                updateGUINodeInfo(niActiveGuard = null, niActiveMiddle = null, niActiveExit = null);
                                tcMain.stop();
                                tcMain.setSilentBootEnabled(true);
                                tcMain.setStartupDelay(2000);
                                tcMain.start(TOR_BOOTED);
                            }
                            break;
                        case TOR_BOOT_TIMEOUT:
                        case TOR_BOOT_FATAL:
                            ni = null;
                            stop();
                            setSilentBootEnabled(true);
                            setStartupDelay(2000);
                            start(TOR_BOOTED);
                            break;
                        case TOR_RESTARTED:
                        // Deliberate fallthrough here
                        case TOR_BOOTED:
                            // Deliberate fallthrough here
                            ni = null;
                        case EVENT_TESTING_DONE:
                            if (ni != null) {
                                if (intActiveNodeTests > 0) {
                                    intActiveNodeTests--;
                                }
                                Logger.getGlobal().logp(Level.INFO, SelekTOR.class.getName(),
                                        "controllerEventFired() on Port=" + getListenPort(), "Completed Testing of " + ni.getNickName());
                                if (getBestLatency() < LATENCY_FAIL) {
                                    ni.setTestStatus(NodeItem.TESTSTATUS_PASSED);
                                    // Store best circuit using its latency as a key
                                    entm.setTestFieldValues(ni.getFingerprint(),
                                            getBestLatency(),
                                            LOCAL.getString("circuit_status_passed"),
                                            getBestHops());
                                } else if ((Integer) data > 0) {
                                    ni.setTestStatus(NodeItem.TESTSTATUS_FAILED);
                                    // If we have circuits that failed testing
                                    entm.setTestFieldValues(ni.getFingerprint(),
                                            LATENCY_FAIL,
                                            LOCAL.getString("circuit_status_failed"), null);
                                } else {
                                    // If we have no circuits
                                    ni.setTestStatus(NodeItem.TESTSTATUS_FAILED);
                                    entm.setTestFieldValues(ni.getFingerprint(),
                                            LATENCY_FAIL,
                                            LOCAL.getString("circuit_status_none"), null);
                                    stop();
                                    setSilentBootEnabled(true);
                                    setStartupDelay(2000);
                                    start(TOR_RESTARTED);
                                    break;
                                }
                            }
                            ni = null;
                            // Ensure test thread is ready
                            if (getStatus() != STATUS_IDLE) {
                                break;
                            }
                            // No more nodes to test
                            if (alTestQueue.isEmpty()) {
                                // Check to see that all testing threads have completed
                                if (intActiveNodeTests <= 0) {
                                    stopTestClients();
                                    // Re-Enable testing menu button 
                                    jMenuTesting.setEnabled(true);
                                    // intActiveNodeTests will be -1 if testing was aborted
                                    if (intActiveNodeTests == 0) {
                                        // Start next test in 10 minutes
                                        intBeginTesting = 600;
                                        // Force change on next Autoselection check
                                        intThresholdExceeded = 255;
                                        // Force Autoselection check in 1 second
                                        intAutoSelectCheck = 1;
                                    }
                                    return;
                                }
                                return;
                            }
                            // Fetch node item from queue
                            while (!alTestQueue.isEmpty()) {
                                String finger = alTestQueue.get(0);
                                alTestQueue.remove(0);
                                ni = nodeList.getNode(finger);
                                if (ni != null) {
                                    intActiveNodeTests++;
                                    entm.setValueAt(LOCAL.getString("circuit_status_creating_3hop"),
                                            ni.getFingerprint(), 3);
                                    Logger.getGlobal().logp(Level.INFO, SelekTOR.class.getName(),
                                            "controllerEventFired() on Port=" + getListenPort(), "Begun testing node " + ni.getNickName());
                                    testNode(ni.getFingerprint());
                                } else {
                                    Logger.getGlobal().logp(Level.INFO, SelekTOR.class.getName(),
                                            "controllerEventFired() on Port=" + getListenPort(), finger + " node not found in nodelist ");
                                    entm.setValueAt(LOCAL.getString("circuit_status_failed"),
                                            finger, 3);
                                }
                                break;
                            }
                            break;
                        case EVENT_CIRCUIT_BUILT:
                            if (ni != null) {
                                entm.setValueAt(LOCAL.getString("circuit_status_testing"), ni.getFingerprint(), 3);
                            }
                            break;
                        case EVENT_ABORTED:
                            if (intActiveNodeTests > 0) {
                                intActiveNodeTests--;
                            }
                            if (ni != null) {
                                entm.setValueAt(LOCAL.getString("circuit_status_aborted"),
                                        ni.getFingerprint(), 3);
                                ni.setTestStatus(NodeItem.TESTSTATUS_UNKNOWN);
                                ni = null;
                            }
                            break;
                        case TOR_DIRINFO_STALE:
                        case TOR_NOEXITS:
                            if (intActiveNodeTests > 0) {
                                intActiveNodeTests--;
                            }
                            if (ni != null) {
                                entm.setTestFieldValues(ni.getFingerprint(),
                                        LATENCY_FAIL,
                                        LOCAL.getString("circuit_status_noroute"), null);
                                ni.setTestStatus(NodeItem.TESTSTATUS_FAILED);
                                ni = null;
                                stop();
                                setSilentBootEnabled(true);
                                setStartupDelay(2000);
                                start(TOR_RESTARTED);
                            }
                            break;
                    }
                }

            };
            tcTest.setTorrcOption("ClientOnly", "1");
            tcTest.setTorrcOption("StrictNodes", "1");
            tcTest.setTorrcOption("UseMicrodescriptors", "0");
            tcTest.setTorrcOption("UseEntryGuards", "1");
            tcTest.setTorrcOption("NumEntryGuards", "3");
            tcTest.setOwnershipID(tcMain.getOwnershipID());
            tcTest.setControlPassword(strSecret, strHash);
            tcTest.setCLIOption("quiet", EMPTYSTR);

            // Add this this tor process thread to our thread queue
            alTestThreads.add(tcTest);
        }
        strSecret = strHash = null;
    }

    /**
     * Auto select next best node based on latency
     *
     */
    private void autoSelectNode() {

        String finger;
        switch (intThresholdExceeded) {
            case 255:
                intThresholdExceeded = 0;
                // Recaculate best nodes
                calcBestNodes();
                resetBestNode();
                finger = getBestNode();
                break;
            case 254:
                // this will re-apply previously selected node
                finger = strRequestedExit;
                strRequestedExit = EMPTYSTR;
                break;
            default:
                // Indicate node has failed testing
                NodeItem ni = null;
                if (niActiveExit != null && nodeList.getNumberOfFavs() != 1) {
                    ni = nodeList.getNode(niActiveExit.getFingerprint());
                    if (ni != null) {
                        ni.setTestStatus(NodeItem.TESTSTATUS_FAILED);
                    }
                }
                // Get next best node
                finger = getBestNode();
                break;
        }
        intThresholdExceeded = 0;

        // Check to see if finger is valid
        if (finger != null && !finger.isEmpty()) {
            if (!finger.contentEquals(strRequestedExit)) {
                strRequestedExit = finger;
                updateProgramStatus(LOCAL.getString("progstatus_autoswitch")
                        + " " + activateNode(strRequestedExit) + ".");
            }
        } else {
            updateProgramStatus(LOCAL.getString("progstatus_nodefailtorchoose"));
            strRequestedExit = EMPTYSTR;
            activateNode(strRequestedExit);
        }

    }

    /**
     * Set latency threshold value and update its corresponding label
     *
     * @param threshold
     */
    private void setThreshold(long threshold) {
        lngThreshold = threshold;
        jTableExitNodes.setToolTipText(LOCAL.getString("ttip_threshold")
                + ": " + lngThreshold + " ms");
    }

    /**
     * Calculate best nodes based on latency and also generate the latency
     * threshold value
     */
    private void calcBestNodes() {

        long l;
        long summation = 0;
        long mean = TorController.LATENCY_FAIL;
        int i = 0;
        ArrayList<String> alBestNodes = new ArrayList<>();
        ArrayList<String> alBestFavs = new ArrayList<>();
        TableRowSorter trs = new TableRowSorter<>(entm);
        trs.toggleSortOrder(2);

        for (int idx = 0; idx < entm.getRowCount(); idx++) {
            NodeItem ni = entm.getNodeItemAt(trs.convertRowIndexToModel(idx));
            if (ni.getTestStatus() == NodeItem.TESTSTATUS_FAILED) {
                continue;
            }
            if (ni.isFavourite()) {
                alBestFavs.add(ni.getFingerprint());
            }
            // Calculate our new weighted threshold value
            l = ni.getTestLatency();
            if (l >= mean) {
                continue;
            }
            summation += l;
            mean = (summation / (i + 1)) * 3;
            i++;
            alBestNodes.add(ni.getFingerprint());
        }
        // New threshold result, prevent divide by zero
        if (i == 0) {
            setThreshold(MAXTHRESHOLD);
        } else {
            setThreshold((summation / i) + 100);
        }
        if (alBestFavs.isEmpty()) {
            while (alBestNodes.size() > 5) {
                alBestNodes.remove(alBestNodes.remove(alBestNodes.size() - 1));
            }
            strArrBestNodes = alBestNodes.toArray(new String[alBestNodes.size()]);
        } else {
            strArrBestNodes = alBestFavs.toArray(new String[alBestFavs.size()]);
        }
    }

    private void resetTestFields() {
        for (int i = 0; i < entm.getRowCount(); i++) {
            entm.setValueAt(TorController.LATENCY_FAIL, i, 2);
            entm.setValueAt(LOCAL.getString("textfield_unknown"), i, 3);
            entm.setValueAt(null, i, 6);
        }
    }

    /**
     * Begin the test cycle
     */
    private void runTestingCycle() {

        // Setting intBeginTesting to -1 prevents re-triggering of testing cycle until it has completed
        intBeginTesting = TESTING_ACTIVE;
        // Update table status column to show unknown and reset the latency column to 9999
        intActiveNodeTests = 0;
        jMenuTesting.setEnabled(false);
        jTableExitNodes.getRowSorter().setSortKeys(null);
        resetTestFields();
        // Add all nodes to the individual test thread queues
        alTestQueue.addAll(nodeList.getExitNodes(true, false));

        // Some more inititalisation of test process threads
        long delay = 0;
        int listenport = tcMain.getListenPort();
        for (TorController tc : alTestThreads) {
            listenport += 2;
            tc.setListenPort(listenport);
            tc.setCachedDataFolder(tcMain.getListenPort());
            tc.setBridges(sp.getString("PREF_TORBRIDGE"));
            tc.setEntryNodes(tcMain.getEntryGuardsAsCSV());
            tc.setTestingURL(TESTURL);
            tc.setBoolTorOption("SafeSocks", sp.getBool("PREF_SAFESOCKS"));
            tc.setBoolTorOption("TestSocks", sp.getBool("PREF_TESTSOCKS"));
            tc.setBoolTorOption("AvoidDiskWrites", sp.getBool("PREF_AVOIDDISK"));
            tc.setBoolTorOption("SafeLogging", sp.getBool("PREF_SAFELOG"));
            tc.setGeoIP4(geoipPath);
            tc.setGeoIP6(geoip6Path);
            tc.setExternalArgs(sp.getString("PREF_TORARGS"));
            tc.setLogLevel(sp.getInt("PREF_TORLOGLEV"));
            tc.setSilentBootEnabled(true);
            tc.setStartupDelay(delay);
            delay += 5000;
            tc.start();
        }
    }

    /**
     * Cleanly abort any node testing
     */
    private void abortTesting() {
        intBeginTesting = TESTING_DISABLED;
        for (TorController tcTest : alTestThreads) {
            tcTest.abortActions();
        }
        intActiveNodeTests = -1;
        alTestQueue.clear();
        jMenuTesting.setEnabled(true);
    }

    /**
     * Terminate all test threads cleanly, this is permanent and would require a
     * call to initTestThreads prior to starting them again
     */
    private void stopTestClients() {
        for (TorController tc : alTestThreads) {
            tc.stop();
        }
    }

    /**
     * Gets the node with the best circuit hops
     *
     * @return String with the best circuit hops or empty string if we don't
     * have any or null if we have run out of good nodes
     */
    private String getBestNode() {
        // Check for no table or empty table
        if (strArrBestNodes == null || strArrBestNodes.length == 0) {
            intBestNodeIndex = 0;
            return EMPTYSTR;
        }
        // Check for end of table
        if (intBestNodeIndex > (strArrBestNodes.length - 1)) {
            intBestNodeIndex = 0;
            if (intBeginTesting > 2) {
                intBeginTesting = 2;
            }
            return null;
        }
        return strArrBestNodes[intBestNodeIndex++];
    }

    /**
     * Reset to the best node
     */
    private void resetBestNode() {
        intBestNodeIndex = 0;
    }

    /**
     * Update program status message
     *
     * @param message
     */
    private synchronized void updateProgramStatus(String message) {
        if (message != null) {
            jLabStatus.setText(message);
            Logger.getGlobal().log(Level.INFO, "updateProgramStatus {0}", message);
        }
    }

    private void getFreshNodeInfo() {
        TorCircuit tc = tcMain.getLatestCircuit();
        if (tc == null) {
            return;
        }
        // Get active guard node item 
        String finger = tc.getGuard(TorCircuit.FINGER);
        niActiveGuard = nodeList.getNode(finger).clone();
        // Get exitnode info
        finger = tc.getExit(TorCircuit.FINGER);
        niActiveExit = (NodeItem) nodeList.getNode(finger).clone();
        niActiveExit.setCircuitHops(tc.getHops());
        niActiveExit.setLatency(TorController.LATENCY_FAIL);
        // Get active middle node item 
        finger = tc.getMiddleMan(TorCircuit.FINGER);
        niActiveMiddle = nodeList.getNode(finger).clone();
    }

    /**
     * Convenience method to refresh all the GUI labels related to Node
     * information
     *
     * @param niexit Requires the active node item to be provided
     */
    private void updateGUINodeInfo(NodeItem niguard, NodeItem nimiddle, NodeItem niexit) {

        if (nimiddle == null) {
            jLabelMiddleStable.setText(LOCAL.getString("textfield_unknown"));
            jLabelMiddleBw.setText(LOCAL.getString("textfield_unknown"));
            jLabelMiddleNick.setText(LOCAL.getString("textfield_unknown"));
            jLabelMiddleIP.setText(LOCAL.getString("textfield_unknown"));
            jLabelMiddleCountry.setText(LOCAL.getString("textfield_unknown"));
            jButtonMiddleNodeDetails.setEnabled(false);
        } else {
            jLabelMiddleNick.setText(nimiddle.getNickName());
            jLabelMiddleIP.setText(nimiddle.getIPAddress());
            jLabelMiddleCountry.setText(nimiddle.getCountryName());
            jLabelMiddleBw.setText(String.valueOf(nimiddle.getBandwidth()) + " MB\\s");
            jLabelMiddleStable.setText(nimiddle.getStability());
            jButtonMiddleNodeDetails.setEnabled(true);
        }

        if (niexit == null) {
            intLatencyCheck = 0;
            intAutoSelectCheck = 0;
            jLabelExitNick.setText(LOCAL.getString("textfield_unknown"));
            jLabelExitIP.setText(LOCAL.getString("textfield_unknown"));
            jLabelExitCountry.setText(LOCAL.getString("textfield_unknown"));
            jLabelExitBw.setText(LOCAL.getString("textfield_unknown"));
            jLabelExitStable.setText(LOCAL.getString("textfield_unknown"));
            jLabelTorLatency.setText(LOCAL.getString("textfield_unknown"));
            jLabelStreams.setText(LOCAL.getString("textfield_unknown"));
            jLabelGuardStable.setText(LOCAL.getString("textfield_unknown"));
            jLabelGuardBw.setText(LOCAL.getString("textfield_unknown"));
            jLabelGuardNick.setText(LOCAL.getString("textfield_unknown"));
            jLabelGuardIP.setText(LOCAL.getString("textfield_unknown"));
            jLabelGuardCountry.setText(LOCAL.getString("textfield_unknown"));
            jButtonExitNodeDetails.setEnabled(false);
            jButtonGuardNodeDetails.setEnabled(false);
            jMenuItemTorCheck.setEnabled(false);
        } else {
            jLabelExitNick.setText(niexit.getNickName());
            jLabelExitIP.setText(niexit.getIPAddress());
            jLabelExitCountry.setText(niexit.getCountryName());
            jLabelExitBw.setText(String.valueOf(niexit.getBandwidth()) + " MB\\s");
            jLabelExitStable.setText(niexit.getStability());
            jButtonExitNodeDetails.setEnabled(true);
            jLabelGuardNick.setText(niguard.getNickName());
            jLabelGuardIP.setText(niguard.getIPAddress());
            jLabelGuardCountry.setText(niguard.getCountryName());
            jLabelGuardBw.setText(String.valueOf(niguard.getBandwidth()) + " MB\\s");
            jLabelGuardStable.setText(niguard.getStability());
            jMenuItemTorCheck.setEnabled(true);
            jButtonGuardNodeDetails.setEnabled(true);
            if (niexit.getLatency() > 5000) {
                jLabelTorLatency.setText("> 5000 ms");
            } else {
                jLabelTorLatency.setText(String.valueOf(niexit.getLatency()) + " ms");
            }
            jLabelStreams.setText(String.valueOf(niexit.getStreams()));
            updateProgramStatus(LOCAL.getString("progstatus_nodeactive3hop").replace("$nodename", niActiveExit.getNickName()));
            dn.raiseNotification();
        }
        String text = LOCAL.getString("label_country") + " : " + sp.getString("PREF_ISOCOUNTRY");
        text += "\n" + LOCAL.getString("traymenu_proxymode" + (sp.getInt("PREF_PROXY_MODE") + 1));
        text += "\n" + LOCAL.getString("traytool_guardnode") + " : " + jLabelGuardNick.getText();
        text += "\n" + LOCAL.getString("traytool_exitnode") + " : " + jLabelExitNick.getText();
        text += "\n" + LOCAL.getString("label_torlatency") + " : " + jLabelTorLatency.getText();
        text += "\n" + LOCAL.getString("label_streams") + " : " + jLabelStreams.getText();
        sti.setInfoTip(text);
    }

    /**
     * Convenience method to update the exit node table
     *
     */
    private void updateExitNodeTable() {
        try {
            strArrBestNodes = null;
            intRowSelected = -1;
            // Set in an empty model while we do the update of its model to avoid event collision issues
            jTableExitNodes.setModel(new ExitNodeTableModel());
            nodeList.refreshExitTableModel(strExitISOCode);
            jTableExitNodes.setModel(entm);
            resetTestFields();
            calcBestNodes();
        } catch (Exception ex) {
            Logger.getGlobal().logp(Level.WARNING, this.getClass().getName(), "updateExitNodeTable()", EMPTYSTR, ex);
        }
        updateGUINodeInfo(null, null, null);
        setComponentsEnabled(true);
    }

    /**
     * Adjust exit table column widths
     */
    private void adjustExitTableColumns() {
        // Adjust table column widths
        String col3 = LOCAL.getString("circuit_status_creating_2hop");
        if (col3.length() > 6) {
            col3 = col3.substring(0, col3.length() - 2);
        } else {
            col3 = "XXXXXXXXXXXXXXXXXX";
        }
        Utilities.adjustTableColumnWidth(jTableExitNodes,
                "XXXXXXXXXXXXXXXXXX",
                jTableExitNodes.getColumnName(1) + "X",
                jTableExitNodes.getColumnName(2) + "X",
                col3,
                jTableExitNodes.getColumnName(4) + "X");
    }

    /**
     * Returns the selected fingerprint
     *
     * @param row
     * @return Fingerprint as String
     */
    private String getSelectedFinger(int row) {
        return (String) entm.getValueAt(row, 5);
    }

    /**
     * Tests first to see if node is in the exitnode list
     *
     * @param finger
     * @return true if specified node is in the exit node table
     */
    private boolean isValidExit(String finger) {
        return nodeList.getExitNodes(true, false).contains(finger);
    }

    /**
     * Activates specified node with fingerprint finger, also returns the node
     * name
     *
     * @param finger
     * @return Node name
     */
    private String activateNode(String fingerprint) {
        String result = EMPTYSTR;
        updateGUINodeInfo(niActiveGuard = null, niActiveMiddle = null, niActiveExit = null);
        if (boolFirstRun) {
            tcMain.closeCircuitsExcept("", true);
        }
        if (fingerprint.isEmpty()) {
            // Get a list of exit nodes based on auto node selection mode
            ArrayList<String> alFingers = nodeList.getExitNodes(false, true);
            if (alFingers.isEmpty()) {
                // Get a list of all exit nodes
                alFingers = nodeList.getExitNodes(true, true);
            }
            // Randomly select from given list of nodes
            Random rand = new Random();
            int idx = rand.nextInt(alFingers.size());
            fingerprint = alFingers.get(idx);
            tcMain.activateNodes(fingerprint, 1);
        } else if (!fingerprint.startsWith("$")) {
            tcMain.activateNodes(fingerprint, 1);
        } else {
            NodeItem ni = nodeList.getNode(fingerprint);
            tcMain.activateNodes(fingerprint, 1);
            if (ni != null) {
                ni.setTestStatus(NodeItem.TESTSTATUS_PASSED);
                result = ni.getNickName();
            } else {
                result = fingerprint;
            }
        }
        return result;
    }

    /**
     * Called when the country is changed via the combo box change event
     *
     */
    private void countryChanged() {

        // Some essential resets of variables
        niActiveExit = null;

        // Get selected country
        String iso = (String) jComboCountry.getSelectedItem();
        iso = iso.substring(0, 2);
        if (!strExitISOCode.contentEquals(iso)) {
            // Save new country to properties file
            sp.setString("PREF_ISOCOUNTRY", strExitISOCode = iso);
            sp.save();
        }

        rebuildPatternPac();

        updateExitNodeTable();
        adjustExitTableColumns();

        updateProgramStatus(LOCAL.getString("progstatus_waitfortor"));
        activateNode(strRequestedExit = EMPTYSTR);
    }

    /**
     * Cleans up pac files and proxies
     */
    private void pacCleanup() {
        if (pacFactory != null) {
            pacFactory.restoreProxyPrefs(false);
            pacFactory.deleteProxyPrefs();
            pacFactory.deleteAllPAC();
        }
    }

    /**
     * The main application exit routine also does cleanup before exiting
     *
     */
    public final void cleanupExit() {
        try {
            setVisible(false);
            // Need to ensure various processes are cleanly terminated before we can exit
            if (tmr != null) {
                tmr.stop();
            }
            if (sti != null) {
                sti.dispose();
            }
            if (nodeList != null) {
                nodeList.terminate();
            }
            if (tmf != null) {
                tmf.dispose();
            }
            // Terminate all tor threads
            if (tcMain != null) {
                tcMain.stop();
            }
            stopTestClients();
            pacCleanup();
            if (sp.getBool("PREF_CACHEDELETE")) {
                SimpleFile.secureWipeFolder(cachePath);
            }
        } catch (Exception ex) {
        }
        Logger.getGlobal().log(Level.INFO, "Cleanup completed, shutting down");
        dispose();
        System.exit(0);
    }

    /**
     * Change visibilty of gUI
     *
     */
    private void initGUIVisibility() {
        // Bring up gui
        pack();
        setLocationRelativeTo(this);
        Utilities.registerWindow();
        if (sp.getBool("PREF_HIDETOTRAY")) {
            if (sp.getBool("PREF_HIDE_MIN") && sti.isLoaded()) {
                setVisible(false);
            } else {
                setVisible(true);
                SwingUtilities.invokeLater(new java.lang.Runnable() {

                    @Override
                    public void run() {
                        repaint();
                        setExtendedState(Frame.ICONIFIED);
                    }
                });
            }
        } else {
            setVisible(true);
        }
    }

    /**
     * Toggle GUI visibility
     */
    private void toggleGUIVisibility() {

        if (OSFunction.isLinux()) {
            requestFocus();
        }
        SwingUtilities.invokeLater(new java.lang.Runnable() {

            @Override
            public void run() {
                if (!isVisible()) {
                    setVisible(true);
                    setExtendedState(Frame.NORMAL);
                    return;
                }
                if ((getExtendedState() & Frame.ICONIFIED) != 0) {
                    setExtendedState(Frame.NORMAL);
                    toFront();
                } else {
                    if (OSFunction.isLinux()) {
                        switch (desktopname) {
                            case "kde4":
                                if (compositor.contentEquals("kwin")) {
                                    if (getFocusOwner() == null) {
                                        setVisible(false);
                                        setVisible(true);
                                        return;
                                    }
                                }
                                break;
                            case "mate":
                                if (compositor.contentEquals("compiz")) {
                                    if (getFocusOwner() == null) {
                                        setExtendedState(Frame.NORMAL);
                                        toFront();
                                        return;
                                    }
                                } else if (getFocusOwner() == null) {
                                    setVisible(false);
                                    setVisible(true);
                                    return;
                                }
                                break;
                            default:
                                if (getFocusOwner() == null) {
                                    setExtendedState(Frame.NORMAL);
                                    toFront();
                                    return;
                                }
                                break;
                        }
                    }
                    if (sp.getBool("PREF_HIDE_MIN") && sti.isLoaded()) {
                        setVisible(false);
                    } else {
                        setExtendedState(Frame.ICONIFIED);
                    }
                }
            }
        });
    }

    /**
     * Open the about dialog
     */
    private void openAboutDialog() {
        AboutDialog ad = new AboutDialog(this, true);
        ad.setButtonCloseText(LOCAL.getString("button_close"));
        ad.setButtonVisitText(LOCAL.getString("button_visitus"));
        ad.setButtonContactText(LOCAL.getString("button_contactus"));
        ad.setTitle(LOCAL.getString("wintitle_about").replace("$appname", APPNAME));
        ad.setContactURL(CONTACT);
        ad.setHomeURL(HOMEPAGE_SSL);
        ad.setAppLogo(new javax.swing.ImageIcon(iconPath + ICONFILE));
        String desc = LOCAL.getString("appdesc").replace("$appname", APPNAME);
        desc = desc.replace("$appver", APPVER) + "\n\nCopyright 2009-2017\nAlistair Neil";
        ad.setAppDescription(desc);
        ad.setLocationRelativeTo(this);
        ad.pack();
        ad.setVisible(true);
    }

    /**
     * Open the licensing dialog
     */
    private void openLicenseDialog() {
        TextResourceDialog trd = new TextResourceDialog(this, true);
        trd.setTitle(LOCAL.getString("dlg_license_title"));
        trd.setURLButtonText(LOCAL.getString("button_translations"));
        trd.setButtonURL(LOCAL.getString("gpltransurl"));
        trd.setCloseButtonText(LOCAL.getString("button_close"));
        trd.setTitle(LOCAL.getString("dlg_license_title").replace("$appname", APPNAME));
        trd.setText(Utilities.getTextFromResource("/resources/license.txt"));
        trd.setLocationRelativeTo(this);
        trd.setVisible(true);
    }

    /**
     * Open the licensing dialog
     */
    private void openCreditsDialog() {
        TextResourceDialog trd = new TextResourceDialog(this, true);
        trd.setTitle(LOCAL.getString("dlg_credits_title"));
        trd.setButtonURL(PATREON);
        trd.setURLButtonText(LOCAL.getString("button_patreon"));
        trd.setCloseButtonText(LOCAL.getString("button_close"));
        trd.setTitle(LOCAL.getString("dlg_credits_title").replace("$appname", APPNAME));
        trd.setText(LOCAL.getString("dlg_credits_body") + "\n\n"
                + Utilities.getTextFromResource("/resources/credits.txt"));
        trd.setLocationRelativeTo(this);
        trd.setVisible(true);
    }

    /**
     * Open the user pattern export file chooser
     */
    private void openPatternExport() {
        File[] files = pacFactory.getPatternsFiles();
        if (files.length == 0) {
            InfoDialog id = createInfoDialog();
            id.createWarn(LOCAL.getString("dlg_exportuser_title"),
                    LOCAL.getString("dlg_exportuser_body"));
            id.setContinueButtonText(LOCAL.getString("button_close"));
            id.setCancelVisible(false);
            id.pack();
            id.setVisible(true);
            return;
        }
        // Lets choose our zip filename or overwrite an existing zip file
        JFileChooser jfc = new JFileChooser();
        jfc.setDialogTitle(LOCAL.getString("dlg_saveuser_title"));
        if (jfc.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) {
            return;
        }
        pacFactory.exportUserPatterns(jfc.getSelectedFile().getAbsolutePath(), files);
    }

    /**
     * Open the pattern import file chooser
     */
    private void openPatternImport() {
        JFileChooser jfc = new JFileChooser();
        ExtensionFileFilter filter = new ExtensionFileFilter(LOCAL.getString("fileext_pattern"), new String[]{"zip"});
        jfc.setFileFilter(filter);
        jfc.setDialogTitle(LOCAL.getString("dlg_import_title"));
        if (jfc.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) {
            return;
        }
        InfoDialog id = createInfoDialog();
        if (pacFactory.importPatterns(jfc.getSelectedFile().getAbsolutePath())) {
            rebuildPatternPac();
            id.createWarn(LOCAL.getString("dlg_import_success_title"),
                    LOCAL.getString("dlg_import_success_body"));
            id.setCancelVisible(false);
            id.pack();
            id.setVisible(true);
        } else {
            id.createWarn(LOCAL.getString("dlg_import_fail_title"),
                    LOCAL.getString("dlg_import_fail_body"));
            id.setCancelVisible(false);
            id.pack();
            id.setVisible(true);
        }
    }

    /**
     * Open the pattern editor
     */
    private void openPatternEditorDialog() {
        PatternsEditor pe = new PatternsEditor(this, true, pacFactory);
        pe.populateCountryComboBox(nodeList.getValidatedCountries());
        pe.setSelectedCountry((String) jComboCountry.getSelectedItem());
        pe.setTitle(LOCAL.getString("wintitle_patternedit").replace("$appname", APPNAME));
        pe.pack();
        pe.setLocationRelativeTo(this);
        pe.setVisible(true);
        sp.setString("PREF_DONOT_PROXY", pacFactory.getDoNotProxy());
        if (sp.getChangedProperties().contains("PREF_DONOT_PROXY")) {
            sp.save();
        }
        if (pe.getReturnStatus() == PatternsEditor.APPLY) {
            rebuildPatternPac();
        }
    }

    /**
     * Open Quick Add pattern dialog
     */
    private void openQuickAddDialog() {
        QuickAddDialog qad = new QuickAddDialog(this, true);
        qad.pack();
        qad.setLocationRelativeTo(this);
        qad.setVisible(true);
        if (qad.getReturnStatus() == QuickAddDialog.APPLY && strExitISOCode != null) {
            pacFactory.addToPatternFile(strExitISOCode, qad.getDescription(), qad.getPattern());
            rebuildPatternPac();
        }
    }

    /**
     * Open Tor client to old dialog
     */
    private void openToOldDialog() {
        openFatalErrorDialog(LOCAL.getString("dlg_toold_body"));
    }

    /**
     * Open the fatal error dialog
     *
     * @param errmessage
     */
    private void openFatalErrorDialog(String errmessage) {
        String text = "\n\n" + LOCAL.getString("dlg_error_body").replace("$appname", APPNAME);
        InfoDialog id = createInfoDialog();
        id.setSize(500, 450);
        id.createInfo(LOCAL.getString("dlg_error_title"), errmessage + text);
        id.setModal(true);
        id.pack();
        id.setLocationRelativeTo(this);
        id.pack();
        id.setVisible(true);
    }

    /**
     * Open the guards node setting dialog
     */
    private void openGuardsSettingDialog() {
        GuardNodeDialog gnd = new GuardNodeDialog(this, true);
        gnd.setNodeList(nodeList);
        gnd.pack();
        gnd.setLocationRelativeTo(this);
        gnd.setVisible(true);
        if (gnd.getReturnStatus() == GuardNodeDialog.APPLY) {
            if (sp.getString("PREF_TORBRIDGE").isEmpty()) {
                updateProgramStatus(LOCAL.getString("progstatus_applychanges"));
                abortAllTorThreads();
                applyMainTorSettings();
                tcMain.start();
            }
        }
    }

    /**
     * Open the standard out dialog
     */
    private void openTorMonitor() {
        tmf.setLocationRelativeTo(this);
        tmf.setVisible(true);
    }

    /**
     * Called when proxy mode is changed by the user
     *
     * @param mode Proxy mode
     */
    private void proxyModeChanged(int mode) {
        sp.setInt("PREF_PROXY_MODE", mode);
        sp.save();
        dn.setNotificationBody(LOCAL.getString("traymenu_proxymode" + (sp.getInt("PREF_PROXY_MODE") + 1)));
        applyProxyChanges(mode);
    }

    /**
     * Apply any proxy changes based on current proxy mode
     *
     * @param mode Proxy mode
     */
    private void applyProxyChanges(int mode) {
        if (tcMain == null) {
            return;
        }
        switch (mode) {
            case PacFactory.PROXY_DISABLED:
                pacFactory.restoreProxyPrefs(true);
                pacFactory.deleteAllPAC();
                break;
            case PacFactory.PROXY_PATTERN:
                pacFactory.createPacFile(strExitISOCode, tcMain.getListenPort());
                break;
            case PacFactory.PROXY_ALL:
                pacFactory.createPacFile("proxyall", tcMain.getListenPort());
                break;
        }
    }

    /**
     * Rebuilds pattern pacfile but only if we are in pattern mode
     */
    private void rebuildPatternPac() {
        if (sp.getInt("PREF_PROXY_MODE") == PacFactory.PROXY_PATTERN) {
            applyProxyChanges(sp.getInt("PREF_PROXY_MODE"));
        }
    }

    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        popupTable = new javax.swing.JPopupMenu();
        jMenuNodeDetails = new javax.swing.JMenuItem();
        jMenuWhois = new javax.swing.JMenuItem();
        jMenuTesting = new javax.swing.JMenuItem();
        jMenuItemGUI = new javax.swing.JMenuItem();
        jMenuItemQuitx = new javax.swing.JMenuItem();
        buttonGroupProxy = new javax.swing.ButtonGroup();
        buttonGroupProxyTray = new javax.swing.ButtonGroup();
        jCheckTrayProxyDisable = new javax.swing.JCheckBoxMenuItem();
        jCheckTrayProxyByPattern = new javax.swing.JCheckBoxMenuItem();
        jCheckTrayProxyAll = new javax.swing.JCheckBoxMenuItem();
        jPanelMain = new javax.swing.JPanel();
        jLabelCountry = new javax.swing.JLabel();
        jComboCountry = new javax.swing.JComboBox<>();
        jScrollPaneExitNodes = new javax.swing.JScrollPane();
        jTableExitNodes = new javax.swing.JTable();
        jPanelNodeInfo = new javax.swing.JPanel();
        jLabel5 = new javax.swing.JLabel();
        jLabelGuardBw = new javax.swing.JLabel();
        jLabel10 = new javax.swing.JLabel();
        jLabelGuardStable = new javax.swing.JLabel();
        jLabelGuard = new javax.swing.JLabel();
        jLabel8 = new javax.swing.JLabel();
        jLabelGuardNick = new javax.swing.JLabel();
        jLabelGuardIP = new javax.swing.JLabel();
        jLabel9 = new javax.swing.JLabel();
        jLabelGuardCountry = new javax.swing.JLabel();
        jLabel12 = new javax.swing.JLabel();
        jLabelExitNick = new javax.swing.JLabel();
        jLabelExitIP = new javax.swing.JLabel();
        jLabelMiddle = new javax.swing.JLabel();
        jLabelExit = new javax.swing.JLabel();
        jLabelMiddleNick = new javax.swing.JLabel();
        jLabelMiddleIP = new javax.swing.JLabel();
        jLabelMiddleCountry = new javax.swing.JLabel();
        jButtonExitNodeDetails = new javax.swing.JButton();
        jButtonGuardNodeDetails = new javax.swing.JButton();
        jButtonMiddleNodeDetails = new javax.swing.JButton();
        jLabelMiddleBw = new javax.swing.JLabel();
        jLabelExitBw = new javax.swing.JLabel();
        jLabelMiddleStable = new javax.swing.JLabel();
        jLabelExitStable = new javax.swing.JLabel();
        jLabelExitCountry = new javax.swing.JLabel();
        jLabel4 = new javax.swing.JLabel();
        jLabelTorLatency = new javax.swing.JLabel();
        jLabelStreams = new javax.swing.JLabel();
        jLabel6 = new javax.swing.JLabel();
        jButtonSetGuards = new javax.swing.JButton();
        jLabStatus = new javax.swing.JLabel();
        jButtonClearFavs = new javax.swing.JButton();
        jMenuBar = new javax.swing.JMenuBar();
        jMenuMenu = new javax.swing.JMenu();
        jMenuItemPrefs = new javax.swing.JMenuItem();
        jMenuItemAddPattern = new javax.swing.JMenuItem();
        jMenuItemPatternEdit = new javax.swing.JMenuItem();
        jMenuItemStdout = new javax.swing.JMenuItem();
        jMenuItemExport = new javax.swing.JMenuItem();
        jMenuItemImport = new javax.swing.JMenuItem();
        jMenuItemGeoip = new javax.swing.JMenuItem();
        jMenuItemQuit = new javax.swing.JMenuItem();
        jMenuProxy = new javax.swing.JMenu();
        jCheckProxyDisable = new javax.swing.JCheckBoxMenuItem();
        jCheckProxyByPattern = new javax.swing.JCheckBoxMenuItem();
        jCheckProxyAll = new javax.swing.JCheckBoxMenuItem();
        jMenuHelp = new javax.swing.JMenu();
        jMenuItemDebugLog = new javax.swing.JMenuItem();
        jMenuItemHelp = new javax.swing.JMenuItem();
        jMenuItemTorMan = new javax.swing.JMenuItem();
        jMenuItemTorCheck = new javax.swing.JMenuItem();
        jMenuItemAbout = new javax.swing.JMenuItem();
        jMenuItemLicense = new javax.swing.JMenuItem();

        popupTable.addPopupMenuListener(new javax.swing.event.PopupMenuListener() {
            public void popupMenuWillBecomeVisible(javax.swing.event.PopupMenuEvent evt) {
                popupTablePopupMenuWillBecomeVisible(evt);
            }
            public void popupMenuWillBecomeInvisible(javax.swing.event.PopupMenuEvent evt) {
            }
            public void popupMenuCanceled(javax.swing.event.PopupMenuEvent evt) {
            }
        });

        jMenuNodeDetails.setText(LOCAL.getString("table_popup_details")); // NOI18N
        jMenuNodeDetails.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuNodeDetailsActionPerformed(evt);
            }
        });
        popupTable.add(jMenuNodeDetails);

        jMenuWhois.setText(LOCAL.getString("table_popup_whois")); // NOI18N
        jMenuWhois.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuWhoisActionPerformed(evt);
            }
        });
        popupTable.add(jMenuWhois);

        jMenuTesting.setText(LOCAL.getString("table_popup_begintest")); // NOI18N
        jMenuTesting.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuTestingActionPerformed(evt);
            }
        });
        popupTable.add(jMenuTesting);

        jMenuItemGUI.setBackground(new Color(jMenuBar.getBackground().getRGB()));
        jMenuItemGUI.setForeground(new Color(jMenuBar.getForeground().getRGB()));
        jMenuItemGUI.setText(LOCAL.getString("traymenu_showgui")); // NOI18N
        jMenuItemGUI.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemGUIActionPerformed(evt);
            }
        });

        jMenuItemQuitx.setText(LOCAL.getString("menu_quit")); // NOI18N
        jMenuItemQuitx.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemQuitxActionPerformed(evt);
            }
        });

        jCheckTrayProxyDisable.setText(LOCAL.getString("combo_proxymode1")); // NOI18N
        jCheckTrayProxyDisable.addItemListener(new java.awt.event.ItemListener() {
            public void itemStateChanged(java.awt.event.ItemEvent evt) {
                jCheckTrayProxyDisableItemStateChanged(evt);
            }
        });

        jCheckTrayProxyByPattern.setText(LOCAL.getString("combo_proxymode2")); // NOI18N
        jCheckTrayProxyByPattern.addItemListener(new java.awt.event.ItemListener() {
            public void itemStateChanged(java.awt.event.ItemEvent evt) {
                jCheckTrayProxyByPatternItemStateChanged(evt);
            }
        });

        jCheckTrayProxyAll.setText(LOCAL.getString("combo_proxymode3")); // NOI18N
        jCheckTrayProxyAll.addItemListener(new java.awt.event.ItemListener() {
            public void itemStateChanged(java.awt.event.ItemEvent evt) {
                jCheckTrayProxyAllItemStateChanged(evt);
            }
        });

        setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE);
        setResizable(false);
        addWindowListener(new java.awt.event.WindowAdapter() {
            public void windowClosing(java.awt.event.WindowEvent evt) {
                formWindowClosing(evt);
            }
        });

        jPanelMain.setFont(jPanelMain.getFont().deriveFont(jPanelMain.getFont().getStyle() | java.awt.Font.BOLD, jPanelMain.getFont().getSize()+1));

        jLabelCountry.setFont(jLabelCountry.getFont().deriveFont(jLabelCountry.getFont().getStyle() | java.awt.Font.BOLD));
        jLabelCountry.setText(LOCAL.getString("label_activecountry")); // NOI18N

        jComboCountry.addItemListener(new java.awt.event.ItemListener() {
            public void itemStateChanged(java.awt.event.ItemEvent evt) {
                jComboCountryItemStateChanged(evt);
            }
        });

        jTableExitNodes.setAutoCreateRowSorter(true);
        jTableExitNodes.setModel(new javax.swing.table.DefaultTableModel(
            new Object [][] {
                {},
                {},
                {},
                {}
            },
            new String [] {

            }
        ));
        jTableExitNodes.setToolTipText(LOCAL.getString("ttip_threshold") +" : "+ LOCAL.getString("textfield_unknown")
        );
        jTableExitNodes.setComponentPopupMenu(popupTable);
        jTableExitNodes.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
        jTableExitNodes.addMouseListener(new java.awt.event.MouseAdapter() {
            public void mouseClicked(java.awt.event.MouseEvent evt) {
                jTableExitNodesMouseClicked(evt);
            }
        });
        jScrollPaneExitNodes.setViewportView(jTableExitNodes);

        jPanelNodeInfo.setBorder(javax.swing.BorderFactory.createTitledBorder(null, LOCAL.getString("panel_info"), javax.swing.border.TitledBorder.DEFAULT_JUSTIFICATION, javax.swing.border.TitledBorder.DEFAULT_POSITION, jPanelMain.getFont())); // NOI18N
        jPanelNodeInfo.setFont(jPanelNodeInfo.getFont().deriveFont(jPanelNodeInfo.getFont().getStyle() | java.awt.Font.BOLD));

        jLabel5.setFont(jLabel5.getFont().deriveFont(jLabel5.getFont().getStyle() | java.awt.Font.BOLD));
        jLabel5.setText(LOCAL.getString("label_bandwidth")); // NOI18N

        jLabelGuardBw.setText(LOCAL.getString("textfield_unknown")); // NOI18N
        jLabelGuardBw.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        jLabel10.setFont(jLabel10.getFont().deriveFont(jLabel10.getFont().getStyle() | java.awt.Font.BOLD));
        jLabel10.setText(LOCAL.getString("label_stable")); // NOI18N

        jLabelGuardStable.setText(LOCAL.getString("textfield_unknown")); // NOI18N
        jLabelGuardStable.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        jLabelGuard.setFont(jLabelGuard.getFont().deriveFont(jLabelGuard.getFont().getStyle() | java.awt.Font.BOLD));
        jLabelGuard.setText(LOCAL.getString("label_guard")); // NOI18N

        jLabel8.setFont(jLabel8.getFont().deriveFont(jLabel8.getFont().getStyle() | java.awt.Font.BOLD));
        jLabel8.setText(LOCAL.getString("label_ip")); // NOI18N

        jLabelGuardNick.setText(LOCAL.getString("textfield_unknown")); // NOI18N
        jLabelGuardNick.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("resources/MessagesBundle"); // NOI18N
        jLabelGuardIP.setText(bundle.getString("textfield_unknown")); // NOI18N
        jLabelGuardIP.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        jLabel9.setFont(jLabel9.getFont().deriveFont(jLabel9.getFont().getStyle() | java.awt.Font.BOLD));
        jLabel9.setText(LOCAL.getString("label_country")); // NOI18N

        jLabelGuardCountry.setText(bundle.getString("textfield_unknown")); // NOI18N
        jLabelGuardCountry.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        jLabel12.setFont(jLabel12.getFont().deriveFont(jLabel12.getFont().getStyle() | java.awt.Font.BOLD));
        jLabel12.setText(LOCAL.getString("label_nickname")); // NOI18N

        jLabelExitNick.setText(LOCAL.getString("textfield_unknown")); // NOI18N
        jLabelExitNick.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        jLabelExitIP.setText(bundle.getString("textfield_unknown")); // NOI18N
        jLabelExitIP.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        jLabelMiddle.setFont(jLabelMiddle.getFont().deriveFont(jLabelMiddle.getFont().getStyle() | java.awt.Font.BOLD));
        jLabelMiddle.setText(LOCAL.getString("label_middle")); // NOI18N

        jLabelExit.setFont(jLabelExit.getFont().deriveFont(jLabelExit.getFont().getStyle() | java.awt.Font.BOLD));
        jLabelExit.setText(LOCAL.getString("label_exit")); // NOI18N

        jLabelMiddleNick.setText(bundle.getString("textfield_unknown")); // NOI18N
        jLabelMiddleNick.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        jLabelMiddleIP.setText(bundle.getString("textfield_unknown")); // NOI18N
        jLabelMiddleIP.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        jLabelMiddleCountry.setText(bundle.getString("textfield_unknown")); // NOI18N
        jLabelMiddleCountry.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        jButtonExitNodeDetails.setText(LOCAL.getString("button_details")); // NOI18N
        jButtonExitNodeDetails.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButtonExitNodeDetailsActionPerformed(evt);
            }
        });

        jButtonGuardNodeDetails.setText(LOCAL.getString("button_details")); // NOI18N
        jButtonGuardNodeDetails.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButtonGuardNodeDetailsActionPerformed(evt);
            }
        });

        jButtonMiddleNodeDetails.setText(LOCAL.getString("button_details")); // NOI18N
        jButtonMiddleNodeDetails.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButtonMiddleNodeDetailsActionPerformed(evt);
            }
        });

        jLabelMiddleBw.setText(LOCAL.getString("textfield_unknown")); // NOI18N
        jLabelMiddleBw.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        jLabelExitBw.setText(LOCAL.getString("textfield_unknown")); // NOI18N
        jLabelExitBw.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        jLabelMiddleStable.setText(LOCAL.getString("textfield_unknown")); // NOI18N
        jLabelMiddleStable.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        jLabelExitStable.setText(LOCAL.getString("textfield_unknown")); // NOI18N
        jLabelExitStable.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        jLabelExitCountry.setText(bundle.getString("textfield_unknown")); // NOI18N
        jLabelExitCountry.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        jLabel4.setFont(jLabel4.getFont().deriveFont(jLabel4.getFont().getStyle() | java.awt.Font.BOLD));
        jLabel4.setText(LOCAL.getString("label_torlatency")); // NOI18N

        jLabelTorLatency.setText(LOCAL.getString("textfield_unknown")); // NOI18N
        jLabelTorLatency.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        jLabelStreams.setText(LOCAL.getString("textfield_unknown")); // NOI18N
        jLabelStreams.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        jLabel6.setFont(jLabel6.getFont().deriveFont(jLabel6.getFont().getStyle() | java.awt.Font.BOLD));
        jLabel6.setText(LOCAL.getString("label_streams")); // NOI18N

        javax.swing.GroupLayout jPanelNodeInfoLayout = new javax.swing.GroupLayout(jPanelNodeInfo);
        jPanelNodeInfo.setLayout(jPanelNodeInfoLayout);
        jPanelNodeInfoLayout.setHorizontalGroup(
            jPanelNodeInfoLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanelNodeInfoLayout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanelNodeInfoLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jLabel10)
                    .addComponent(jLabel9)
                    .addComponent(jLabel8)
                    .addComponent(jLabel12)
                    .addComponent(jLabel5))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                .addGroup(jPanelNodeInfoLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
                    .addGroup(jPanelNodeInfoLayout.createSequentialGroup()
                        .addComponent(jLabelGuard)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                        .addComponent(jButtonGuardNodeDetails))
                    .addComponent(jLabelGuardIP, javax.swing.GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE)
                    .addComponent(jLabelGuardCountry, javax.swing.GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE)
                    .addComponent(jLabelGuardBw, javax.swing.GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE)
                    .addComponent(jLabelGuardStable, javax.swing.GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE)
                    .addComponent(jLabelGuardNick, javax.swing.GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                .addGroup(jPanelNodeInfoLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
                    .addGroup(jPanelNodeInfoLayout.createSequentialGroup()
                        .addComponent(jLabel4)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(jLabelTorLatency, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
                    .addGroup(jPanelNodeInfoLayout.createSequentialGroup()
                        .addComponent(jLabelMiddle)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                        .addComponent(jButtonMiddleNodeDetails))
                    .addComponent(jLabelMiddleIP, javax.swing.GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE)
                    .addComponent(jLabelMiddleCountry, javax.swing.GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE)
                    .addComponent(jLabelMiddleBw, javax.swing.GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE)
                    .addComponent(jLabelMiddleStable, javax.swing.GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE)
                    .addComponent(jLabelMiddleNick, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                .addGroup(jPanelNodeInfoLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false)
                    .addComponent(jLabelExitStable, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE)
                    .addComponent(jLabelExitBw, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE)
                    .addComponent(jLabelExitCountry, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE)
                    .addComponent(jLabelExitIP, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE)
                    .addGroup(javax.swing.GroupLayout.Alignment.LEADING, jPanelNodeInfoLayout.createSequentialGroup()
                        .addComponent(jLabel6)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(jLabelStreams, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
                    .addGroup(javax.swing.GroupLayout.Alignment.LEADING, jPanelNodeInfoLayout.createSequentialGroup()
                        .addComponent(jLabelExit)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                        .addComponent(jButtonExitNodeDetails))
                    .addComponent(jLabelExitNick, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE))
                .addContainerGap())
        );

        jPanelNodeInfoLayout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {jLabel10, jLabel12, jLabel5, jLabel8, jLabel9});

        jPanelNodeInfoLayout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {jLabelExitBw, jLabelExitCountry, jLabelExitIP, jLabelExitNick, jLabelExitStable, jLabelMiddleBw, jLabelMiddleCountry, jLabelMiddleIP, jLabelMiddleStable});

        jPanelNodeInfoLayout.setVerticalGroup(
            jPanelNodeInfoLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanelNodeInfoLayout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanelNodeInfoLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER)
                    .addComponent(jLabelGuard)
                    .addComponent(jButtonGuardNodeDetails)
                    .addComponent(jLabelMiddle)
                    .addComponent(jButtonMiddleNodeDetails)
                    .addComponent(jLabelExit)
                    .addComponent(jButtonExitNodeDetails))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(jPanelNodeInfoLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(jPanelNodeInfoLayout.createSequentialGroup()
                        .addComponent(jLabelExitNick)
                        .addGap(0, 0, 0)
                        .addComponent(jLabelExitIP)
                        .addGap(0, 0, 0)
                        .addComponent(jLabelExitCountry)
                        .addGap(0, 0, 0)
                        .addComponent(jLabelExitBw)
                        .addGap(0, 0, 0)
                        .addComponent(jLabelExitStable))
                    .addGroup(jPanelNodeInfoLayout.createSequentialGroup()
                        .addComponent(jLabelMiddleNick)
                        .addGap(0, 0, 0)
                        .addComponent(jLabelMiddleIP)
                        .addGap(0, 0, 0)
                        .addComponent(jLabelMiddleCountry)
                        .addGap(0, 0, 0)
                        .addComponent(jLabelMiddleBw)
                        .addGap(0, 0, 0)
                        .addComponent(jLabelMiddleStable))
                    .addGroup(jPanelNodeInfoLayout.createSequentialGroup()
                        .addGroup(jPanelNodeInfoLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                            .addComponent(jLabelGuardNick)
                            .addComponent(jLabel12))
                        .addGap(0, 0, 0)
                        .addGroup(jPanelNodeInfoLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                            .addComponent(jLabelGuardIP)
                            .addComponent(jLabel8))
                        .addGap(0, 0, 0)
                        .addGroup(jPanelNodeInfoLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                            .addComponent(jLabel9)
                            .addComponent(jLabelGuardCountry))
                        .addGroup(jPanelNodeInfoLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                            .addComponent(jLabel5)
                            .addComponent(jLabelGuardBw))
                        .addGap(0, 0, 0)
                        .addGroup(jPanelNodeInfoLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                            .addComponent(jLabelGuardStable)
                            .addComponent(jLabel10))))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addGroup(jPanelNodeInfoLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jLabelStreams, javax.swing.GroupLayout.Alignment.TRAILING)
                    .addComponent(jLabel6, javax.swing.GroupLayout.Alignment.TRAILING)
                    .addComponent(jLabelTorLatency, javax.swing.GroupLayout.Alignment.TRAILING)
                    .addComponent(jLabel4, javax.swing.GroupLayout.Alignment.TRAILING))
                .addContainerGap())
        );

        jPanelNodeInfoLayout.linkSize(javax.swing.SwingConstants.VERTICAL, new java.awt.Component[] {jLabelExitBw, jLabelExitCountry, jLabelExitIP, jLabelExitNick, jLabelExitStable, jLabelGuardBw, jLabelGuardCountry, jLabelGuardIP, jLabelGuardNick, jLabelGuardStable, jLabelMiddleBw, jLabelMiddleCountry, jLabelMiddleIP, jLabelMiddleNick, jLabelMiddleStable});

        jButtonSetGuards.setText(LOCAL.getString("button_setguards")); // NOI18N
        jButtonSetGuards.setToolTipText(LOCAL.getString("button_setguards")); // NOI18N
        jButtonSetGuards.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButtonSetGuardsActionPerformed(evt);
            }
        });

        jLabStatus.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
        jLabStatus.setText(LOCAL.getString("textfield_unknown")); // NOI18N

        jButtonClearFavs.setText(LOCAL.getString("button_clearfavs")); // NOI18N
        jButtonClearFavs.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButtonClearFavsActionPerformed(evt);
            }
        });

        javax.swing.GroupLayout jPanelMainLayout = new javax.swing.GroupLayout(jPanelMain);
        jPanelMain.setLayout(jPanelMainLayout);
        jPanelMainLayout.setHorizontalGroup(
            jPanelMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanelMainLayout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanelMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jLabStatus, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(jScrollPaneExitNodes)
                    .addComponent(jPanelNodeInfo, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addGroup(jPanelMainLayout.createSequentialGroup()
                        .addGroup(jPanelMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addGroup(jPanelMainLayout.createSequentialGroup()
                                .addComponent(jComboCountry, javax.swing.GroupLayout.PREFERRED_SIZE, 260, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                                .addComponent(jButtonSetGuards)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                                .addComponent(jButtonClearFavs))
                            .addComponent(jLabelCountry))
                        .addGap(0, 0, Short.MAX_VALUE)))
                .addContainerGap())
        );
        jPanelMainLayout.setVerticalGroup(
            jPanelMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanelMainLayout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jLabelCountry)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(jPanelMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(jComboCountry, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(jButtonSetGuards)
                    .addComponent(jButtonClearFavs))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jScrollPaneExitNodes, javax.swing.GroupLayout.DEFAULT_SIZE, 182, Short.MAX_VALUE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jPanelNodeInfo, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jLabStatus)
                .addContainerGap())
        );

        jMenuMenu.setMnemonic(LOCAL.getString("menu_menu").charAt(0));
        jMenuMenu.setText(LOCAL.getString("menu_menu")); // NOI18N

        jMenuItemPrefs.setMnemonic('P');
        jMenuItemPrefs.setText(LOCAL.getString("menu_prefs")); // NOI18N
        jMenuItemPrefs.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemPrefsActionPerformed(evt);
            }
        });
        jMenuMenu.add(jMenuItemPrefs);

        jMenuItemAddPattern.setText(LOCAL.getString("menu_quickadd")); // NOI18N
        jMenuItemAddPattern.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemAddPatternActionPerformed(evt);
            }
        });
        jMenuMenu.add(jMenuItemAddPattern);

        jMenuItemPatternEdit.setText(LOCAL.getString("menu_patternedit")); // NOI18N
        jMenuItemPatternEdit.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemPatternEditActionPerformed(evt);
            }
        });
        jMenuMenu.add(jMenuItemPatternEdit);

        jMenuItemStdout.setText(LOCAL.getString("menu_tormonitor")); // NOI18N
        jMenuItemStdout.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemStdoutActionPerformed(evt);
            }
        });
        jMenuMenu.add(jMenuItemStdout);

        jMenuItemExport.setText(LOCAL.getString("menu_export")); // NOI18N
        jMenuItemExport.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemExportActionPerformed(evt);
            }
        });
        jMenuMenu.add(jMenuItemExport);

        jMenuItemImport.setText(LOCAL.getString("menu_import")); // NOI18N
        jMenuItemImport.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemImportActionPerformed(evt);
            }
        });
        jMenuMenu.add(jMenuItemImport);

        jMenuItemGeoip.setText(LOCAL.getString("menu_geoip")); // NOI18N
        jMenuItemGeoip.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemGeoipActionPerformed(evt);
            }
        });
        jMenuMenu.add(jMenuItemGeoip);

        jMenuItemQuit.setMnemonic('Q');
        jMenuItemQuit.setText(LOCAL.getString("menu_quit")); // NOI18N
        jMenuItemQuit.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemQuitActionPerformed(evt);
            }
        });
        jMenuMenu.add(jMenuItemQuit);

        jMenuBar.add(jMenuMenu);

        jMenuProxy.setMnemonic(LOCAL.getString("menu_proxy").charAt(0));
        jMenuProxy.setText(LOCAL.getString("menu_proxy")); // NOI18N

        jCheckProxyDisable.setText(LOCAL.getString("combo_proxymode1")); // NOI18N
        jCheckProxyDisable.addItemListener(new java.awt.event.ItemListener() {
            public void itemStateChanged(java.awt.event.ItemEvent evt) {
                jCheckProxyDisableItemStateChanged(evt);
            }
        });
        jMenuProxy.add(jCheckProxyDisable);

        jCheckProxyByPattern.setText(LOCAL.getString("combo_proxymode2")); // NOI18N
        jCheckProxyByPattern.addItemListener(new java.awt.event.ItemListener() {
            public void itemStateChanged(java.awt.event.ItemEvent evt) {
                jCheckProxyByPatternItemStateChanged(evt);
            }
        });
        jMenuProxy.add(jCheckProxyByPattern);

        jCheckProxyAll.setText(LOCAL.getString("combo_proxymode3")); // NOI18N
        jCheckProxyAll.addItemListener(new java.awt.event.ItemListener() {
            public void itemStateChanged(java.awt.event.ItemEvent evt) {
                jCheckProxyAllItemStateChanged(evt);
            }
        });
        jMenuProxy.add(jCheckProxyAll);

        jMenuBar.add(jMenuProxy);

        jMenuHelp.setMnemonic(LOCAL.getString("menu_help").charAt(0));
        jMenuHelp.setText(LOCAL.getString("menu_help")); // NOI18N

        jMenuItemDebugLog.setVisible(false);
        jMenuItemDebugLog.setText(LOCAL.getString("menu_debuglog")); // NOI18N
        jMenuItemDebugLog.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemDebugLogActionPerformed(evt);
            }
        });
        jMenuHelp.add(jMenuItemDebugLog);

        jMenuItemHelp.setMnemonic('C');
        jMenuItemHelp.setText(LOCAL.getString("menu_helpcontents")); // NOI18N
        jMenuItemHelp.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemHelpActionPerformed(evt);
            }
        });
        jMenuHelp.add(jMenuItemHelp);

        jMenuItemTorMan.setText(LOCAL.getString("menu_tormanual")); // NOI18N
        jMenuItemTorMan.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemTorManActionPerformed(evt);
            }
        });
        jMenuHelp.add(jMenuItemTorMan);

        jMenuItemTorCheck.setMnemonic('T');
        jMenuItemTorCheck.setText(LOCAL.getString("menu_torcheck")); // NOI18N
        jMenuItemTorCheck.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemTorCheckActionPerformed(evt);
            }
        });
        jMenuHelp.add(jMenuItemTorCheck);

        jMenuItemAbout.setMnemonic('A');
        jMenuItemAbout.setText(LOCAL.getString("menu_about")); // NOI18N
        jMenuItemAbout.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemAboutActionPerformed(evt);
            }
        });
        jMenuHelp.add(jMenuItemAbout);

        jMenuItemLicense.setText(LOCAL.getString("menu_license")); // NOI18N
        jMenuItemLicense.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemLicenseActionPerformed(evt);
            }
        });
        jMenuHelp.add(jMenuItemLicense);

        jMenuBar.add(jMenuHelp);

        setJMenuBar(jMenuBar);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addComponent(jPanelMain, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addComponent(jPanelMain, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
        );

        pack();
    }// </editor-fold>//GEN-END:initComponents

    private void formWindowClosing(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowClosing
        if (sp.getBool("PREF_MINONCLOSE")) {
            // Used to provide clean shut down when a WM_CLOSE and Taskkill are called
            // posted externally
            if (!isVisible() && OSFunction.isWindows()) {
                cleanupExit();
                return;
            }
            if (sp.getBool("PREF_HIDE_MIN") && sti.isLoaded()) {
                setVisible(false);
            } else {
                setExtendedState(Frame.ICONIFIED);
            }
        } else {
            cleanupExit();
        }
    }//GEN-LAST:event_formWindowClosing

    private void jComboCountryItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_jComboCountryItemStateChanged

        switch (evt.getStateChange()) {
            case ItemEvent.DESELECTED:
                abortTesting();
                break;
            case ItemEvent.SELECTED:
                countryChanged();
                break;
        }

    }//GEN-LAST:event_jComboCountryItemStateChanged

    private void jMenuItemPrefsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemPrefsActionPerformed
        SwingUtilities.invokeLater(new java.lang.Runnable() {
            @Override
            public void run() {
                openPreferencesDialog();
            }
        });
    }//GEN-LAST:event_jMenuItemPrefsActionPerformed

    private void jMenuItemAddPatternActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemAddPatternActionPerformed
        openQuickAddDialog();
    }//GEN-LAST:event_jMenuItemAddPatternActionPerformed

    private void jMenuItemPatternEditActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemPatternEditActionPerformed
        SwingUtilities.invokeLater(new java.lang.Runnable() {
            @Override
            public void run() {
                openPatternEditorDialog();
            }
        });
    }//GEN-LAST:event_jMenuItemPatternEditActionPerformed

    private void jMenuItemImportActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemImportActionPerformed
        SwingUtilities.invokeLater(new java.lang.Runnable() {
            @Override
            public void run() {
                openPatternImport();
            }
        });
    }//GEN-LAST:event_jMenuItemImportActionPerformed

    private void jMenuItemQuitActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemQuitActionPerformed
        cleanupExit();
    }//GEN-LAST:event_jMenuItemQuitActionPerformed

    private void jMenuItemDebugLogActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemDebugLogActionPerformed
        Utilities.editFile(settingsPath + "status.log");
    }//GEN-LAST:event_jMenuItemDebugLogActionPerformed

    private void jMenuItemHelpActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemHelpActionPerformed
        SwingUtilities.invokeLater(new java.lang.Runnable() {
            @Override
            public void run() {
                Utilities.openFileExternally(DOCSPATH + "help-" + OSFunction.getLangCode() + ".html");
            }
        });
    }//GEN-LAST:event_jMenuItemHelpActionPerformed

    private void jMenuItemTorCheckActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemTorCheckActionPerformed
        SwingUtilities.invokeLater(new java.lang.Runnable() {
            @Override
            public void run() {
                String link = TORCHECK + "?lang="
                        + LOCAL.toWebLanguageTag();
                Utilities.openFileExternally(link);
            }
        });
    }//GEN-LAST:event_jMenuItemTorCheckActionPerformed

    private void jMenuItemAboutActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemAboutActionPerformed
        SwingUtilities.invokeLater(new java.lang.Runnable() {
            @Override
            public void run() {
                openAboutDialog();
            }
        });
    }//GEN-LAST:event_jMenuItemAboutActionPerformed

    private void jMenuItemLicenseActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemLicenseActionPerformed
        openLicenseDialog();
    }//GEN-LAST:event_jMenuItemLicenseActionPerformed

    private void jMenuWhoisActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuWhoisActionPerformed
        String finger = (String) entm.getValueAt(intRowSelected, 5);
        NodeItem ni = nodeList.getNode(finger);
        if (ni != null) {
            Utilities.openFileExternally("https://www.networksolutions.com/whois/results.jsp?ip=" + ni.getIPAddress());
        }
    }//GEN-LAST:event_jMenuWhoisActionPerformed

    private void jMenuTestingActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuTestingActionPerformed
        abortTesting();
        intBeginTesting = 4;
    }//GEN-LAST:event_jMenuTestingActionPerformed

    private void popupTablePopupMenuWillBecomeVisible(javax.swing.event.PopupMenuEvent evt) {//GEN-FIRST:event_popupTablePopupMenuWillBecomeVisible
        jMenuWhois.setEnabled(intRowSelected > -1);
        jMenuNodeDetails.setEnabled(intRowSelected > -1);
    }//GEN-LAST:event_popupTablePopupMenuWillBecomeVisible

    private void jMenuNodeDetailsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuNodeDetailsActionPerformed
        String finger = (String) entm.getValueAt(intRowSelected, 5);
        finger = finger.replace("$", EMPTYSTR);
        Utilities.openFileExternally(ATLAS + "#details/" + finger);
    }//GEN-LAST:event_jMenuNodeDetailsActionPerformed

    private void jButtonExitNodeDetailsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButtonExitNodeDetailsActionPerformed
        String finger = niActiveExit.getFingerprint();
        finger = finger.replace("$", EMPTYSTR);
        Utilities.openFileExternally(ATLAS + "#details/" + finger);
    }//GEN-LAST:event_jButtonExitNodeDetailsActionPerformed

    private void jMenuItemStdoutActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemStdoutActionPerformed
        SwingUtilities.invokeLater(new java.lang.Runnable() {
            @Override
            public void run() {
                openTorMonitor();
            }
        });
    }//GEN-LAST:event_jMenuItemStdoutActionPerformed

    private void jMenuItemTorManActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemTorManActionPerformed
        SwingUtilities.invokeLater(new java.lang.Runnable() {
            @Override
            public void run() {
                Utilities.openFileExternally(TORPROJECT + "docs/tor-manual.html");
            }
        });
    }//GEN-LAST:event_jMenuItemTorManActionPerformed

    private void jButtonGuardNodeDetailsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButtonGuardNodeDetailsActionPerformed
        String finger = niActiveGuard.getFingerprint();
        finger = finger.replace("$", EMPTYSTR);
        Utilities.openFileExternally(ATLAS + "#details/" + finger);
    }//GEN-LAST:event_jButtonGuardNodeDetailsActionPerformed

    private void jButtonSetGuardsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButtonSetGuardsActionPerformed
        SwingUtilities.invokeLater(new java.lang.Runnable() {
            @Override
            public void run() {
                openGuardsSettingDialog();
            }
        });
    }//GEN-LAST:event_jButtonSetGuardsActionPerformed

    private void jMenuItemGUIActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemGUIActionPerformed
        toggleGUIVisibility();
    }//GEN-LAST:event_jMenuItemGUIActionPerformed

    private void jMenuItemQuitxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemQuitxActionPerformed
        cleanupExit();
    }//GEN-LAST:event_jMenuItemQuitxActionPerformed

    private void jButtonMiddleNodeDetailsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButtonMiddleNodeDetailsActionPerformed
        String finger = niActiveMiddle.getFingerprint();
        finger = finger.replace("$", EMPTYSTR);
        Utilities.openFileExternally(ATLAS + "#details/" + finger);
    }//GEN-LAST:event_jButtonMiddleNodeDetailsActionPerformed

    private void jMenuItemExportActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemExportActionPerformed
        SwingUtilities.invokeLater(new java.lang.Runnable() {
            @Override
            public void run() {
                openPatternExport();
            }
        });
    }//GEN-LAST:event_jMenuItemExportActionPerformed

    private void jCheckProxyDisableItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_jCheckProxyDisableItemStateChanged
        if (evt.getStateChange() == ItemEvent.SELECTED) {
            jCheckTrayProxyDisable.setSelected(true);
            if (jCheckProxyDisable.isEnabled()) {
                proxyModeChanged(PacFactory.PROXY_DISABLED);
            }
        }
    }//GEN-LAST:event_jCheckProxyDisableItemStateChanged

    private void jCheckProxyByPatternItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_jCheckProxyByPatternItemStateChanged
        if (evt.getStateChange() == ItemEvent.SELECTED) {
            jCheckTrayProxyByPattern.setSelected(true);
            if (jCheckProxyByPattern.isEnabled()) {
                proxyModeChanged(PacFactory.PROXY_PATTERN);
            }
        }
    }//GEN-LAST:event_jCheckProxyByPatternItemStateChanged

    private void jCheckProxyAllItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_jCheckProxyAllItemStateChanged
        if (evt.getStateChange() == ItemEvent.SELECTED) {
            jCheckTrayProxyAll.setSelected(true);
            if (jCheckProxyAll.isEnabled()) {
                proxyModeChanged(PacFactory.PROXY_ALL);
            }
        }
    }//GEN-LAST:event_jCheckProxyAllItemStateChanged

    private void jCheckTrayProxyDisableItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_jCheckTrayProxyDisableItemStateChanged
        if (evt.getStateChange() == ItemEvent.SELECTED) {
            jCheckProxyDisable.setSelected(true);
        }
    }//GEN-LAST:event_jCheckTrayProxyDisableItemStateChanged

    private void jCheckTrayProxyByPatternItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_jCheckTrayProxyByPatternItemStateChanged
        if (evt.getStateChange() == ItemEvent.SELECTED) {
            jCheckProxyByPattern.setSelected(true);
        }
    }//GEN-LAST:event_jCheckTrayProxyByPatternItemStateChanged

    private void jCheckTrayProxyAllItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_jCheckTrayProxyAllItemStateChanged
        if (evt.getStateChange() == ItemEvent.SELECTED) {
            jCheckProxyAll.setSelected(true);
        }
    }//GEN-LAST:event_jCheckTrayProxyAllItemStateChanged

    private void jMenuItemGeoipActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemGeoipActionPerformed
        SwingUtilities.invokeLater(new java.lang.Runnable() {
            @Override
            public void run() {
                updateGeoip();
            }
        });
    }//GEN-LAST:event_jMenuItemGeoipActionPerformed

    private void jButtonClearFavsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButtonClearFavsActionPerformed
        clearFavourites();
    }//GEN-LAST:event_jButtonClearFavsActionPerformed

    private void jTableExitNodesMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_jTableExitNodesMouseClicked
        if (evt.getButton() != MouseEvent.BUTTON1) {
            return;
        }
        try {
            // Get row selection
            intRowSelected = jTableExitNodes.getSelectedRow();
            if (intRowSelected > -1) {
                intRowSelected = jTableExitNodes.convertRowIndexToModel(intRowSelected);
            }
        } catch (Exception ex) {
        }
    }//GEN-LAST:event_jTableExitNodesMouseClicked

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.ButtonGroup buttonGroupProxy;
    private javax.swing.ButtonGroup buttonGroupProxyTray;
    private javax.swing.JButton jButtonClearFavs;
    private javax.swing.JButton jButtonExitNodeDetails;
    private javax.swing.JButton jButtonGuardNodeDetails;
    private javax.swing.JButton jButtonMiddleNodeDetails;
    private javax.swing.JButton jButtonSetGuards;
    private javax.swing.JCheckBoxMenuItem jCheckProxyAll;
    private javax.swing.JCheckBoxMenuItem jCheckProxyByPattern;
    private javax.swing.JCheckBoxMenuItem jCheckProxyDisable;
    private javax.swing.JCheckBoxMenuItem jCheckTrayProxyAll;
    private javax.swing.JCheckBoxMenuItem jCheckTrayProxyByPattern;
    private javax.swing.JCheckBoxMenuItem jCheckTrayProxyDisable;
    private javax.swing.JComboBox<String> jComboCountry;
    private javax.swing.JLabel jLabStatus;
    private javax.swing.JLabel jLabel10;
    private javax.swing.JLabel jLabel12;
    private javax.swing.JLabel jLabel4;
    private javax.swing.JLabel jLabel5;
    private javax.swing.JLabel jLabel6;
    private javax.swing.JLabel jLabel8;
    private javax.swing.JLabel jLabel9;
    private javax.swing.JLabel jLabelCountry;
    private javax.swing.JLabel jLabelExit;
    private javax.swing.JLabel jLabelExitBw;
    private javax.swing.JLabel jLabelExitCountry;
    private javax.swing.JLabel jLabelExitIP;
    private javax.swing.JLabel jLabelExitNick;
    private javax.swing.JLabel jLabelExitStable;
    private javax.swing.JLabel jLabelGuard;
    private javax.swing.JLabel jLabelGuardBw;
    private javax.swing.JLabel jLabelGuardCountry;
    private javax.swing.JLabel jLabelGuardIP;
    private javax.swing.JLabel jLabelGuardNick;
    private javax.swing.JLabel jLabelGuardStable;
    private javax.swing.JLabel jLabelMiddle;
    private javax.swing.JLabel jLabelMiddleBw;
    private javax.swing.JLabel jLabelMiddleCountry;
    private javax.swing.JLabel jLabelMiddleIP;
    private javax.swing.JLabel jLabelMiddleNick;
    private javax.swing.JLabel jLabelMiddleStable;
    private javax.swing.JLabel jLabelStreams;
    private javax.swing.JLabel jLabelTorLatency;
    private javax.swing.JMenuBar jMenuBar;
    private javax.swing.JMenu jMenuHelp;
    private javax.swing.JMenuItem jMenuItemAbout;
    private javax.swing.JMenuItem jMenuItemAddPattern;
    private javax.swing.JMenuItem jMenuItemDebugLog;
    private javax.swing.JMenuItem jMenuItemExport;
    private javax.swing.JMenuItem jMenuItemGUI;
    private javax.swing.JMenuItem jMenuItemGeoip;
    private javax.swing.JMenuItem jMenuItemHelp;
    private javax.swing.JMenuItem jMenuItemImport;
    private javax.swing.JMenuItem jMenuItemLicense;
    private javax.swing.JMenuItem jMenuItemPatternEdit;
    private javax.swing.JMenuItem jMenuItemPrefs;
    private javax.swing.JMenuItem jMenuItemQuit;
    private javax.swing.JMenuItem jMenuItemQuitx;
    private javax.swing.JMenuItem jMenuItemStdout;
    private javax.swing.JMenuItem jMenuItemTorCheck;
    private javax.swing.JMenuItem jMenuItemTorMan;
    private javax.swing.JMenu jMenuMenu;
    private javax.swing.JMenuItem jMenuNodeDetails;
    private javax.swing.JMenu jMenuProxy;
    private javax.swing.JMenuItem jMenuTesting;
    private javax.swing.JMenuItem jMenuWhois;
    private javax.swing.JPanel jPanelMain;
    private javax.swing.JPanel jPanelNodeInfo;
    private javax.swing.JScrollPane jScrollPaneExitNodes;
    private javax.swing.JTable jTableExitNodes;
    private javax.swing.JPopupMenu popupTable;
    // End of variables declaration//GEN-END:variables
}
