de.bwravencl.controllerbuddy.gui.Main.java Source code

Java tutorial

Introduction

Here is the source code for de.bwravencl.controllerbuddy.gui.Main.java

Source

/* Copyright (C) 2019  Matteo Hausner
 *
 * 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.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package de.bwravencl.controllerbuddy.gui;

import static de.bwravencl.controllerbuddy.gui.GuiUtils.invokeOnEventDispatchThreadIfRequired;
import static de.bwravencl.controllerbuddy.gui.GuiUtils.loadFrameLocation;
import static de.bwravencl.controllerbuddy.gui.GuiUtils.setEnabledRecursive;
import static org.lwjgl.glfw.GLFW.GLFW_DISCONNECTED;
import static org.lwjgl.glfw.GLFW.GLFW_JOYSTICK_1;
import static org.lwjgl.glfw.GLFW.GLFW_JOYSTICK_LAST;
import static org.lwjgl.glfw.GLFW.glfwGetGamepadName;
import static org.lwjgl.glfw.GLFW.glfwGetJoystickGUID;
import static org.lwjgl.glfw.GLFW.glfwInit;
import static org.lwjgl.glfw.GLFW.glfwJoystickIsGamepad;
import static org.lwjgl.glfw.GLFW.glfwJoystickPresent;
import static org.lwjgl.glfw.GLFW.glfwSetJoystickCallback;
import static org.lwjgl.glfw.GLFW.glfwTerminate;

import java.awt.AWTException;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Frame;
import java.awt.GraphicsEnvironment;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.MenuItem;
import java.awt.Point;
import java.awt.PopupMenu;
import java.awt.Rectangle;
import java.awt.SystemTray;
import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.TrayIcon.MessageType;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.IOException;
import java.lang.System.Logger;
import java.net.ServerSocket;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Timer;
import java.util.TimerTask;
import java.util.prefs.Preferences;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.ButtonGroup;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JColorChooser;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.WindowConstants;
import javax.swing.border.EtchedBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.text.DefaultFormatter;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.lwjgl.glfw.GLFWJoystickCallback;

import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.oracle.si.Singleton;
import com.oracle.si.Singleton.SingletonApp;
import com.sun.jna.Platform;
import com.sun.jna.platform.win32.WinDef.UINT;

import de.bwravencl.controllerbuddy.Version;
import de.bwravencl.controllerbuddy.gui.GuiUtils.FrameDragListener;
import de.bwravencl.controllerbuddy.input.Input;
import de.bwravencl.controllerbuddy.input.Input.VirtualAxis;
import de.bwravencl.controllerbuddy.input.Mode;
import de.bwravencl.controllerbuddy.input.OverlayAxis;
import de.bwravencl.controllerbuddy.input.Profile;
import de.bwravencl.controllerbuddy.input.action.IAction;
import de.bwravencl.controllerbuddy.json.ActionTypeAdapter;
import de.bwravencl.controllerbuddy.json.ModeAwareTypeAdapterFactory;
import de.bwravencl.controllerbuddy.output.ClientVJoyOutputThread;
import de.bwravencl.controllerbuddy.output.LocalVJoyOutputThread;
import de.bwravencl.controllerbuddy.output.OutputThread;
import de.bwravencl.controllerbuddy.output.ServerOutputThread;
import de.bwravencl.controllerbuddy.output.VJoyOutputThread;
import de.bwravencl.controllerbuddy.util.ResourceBundleUtil;

public final class Main implements SingletonApp {

    private class AddModeAction extends AbstractAction {

        private static final long serialVersionUID = -4881923833724315489L;

        private AddModeAction() {
            putValue(NAME, rb.getString("ADD_MODE_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("ADD_MODE_ACTION_DESCRIPTION"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            final var mode = new Mode();
            input.getProfile().getModes().add(mode);

            setUnsavedChanges(true);
            updateModesPanel();
        }

    }

    private class ChangeVJoyDirectoryAction extends AbstractAction {

        private static final long serialVersionUID = -7672382299595684105L;

        private ChangeVJoyDirectoryAction() {
            putValue(NAME, rb.getString("CHANGE_VJOY_DIRECTORY_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("CHANGE_VJOY_DIRECTORY_ACTION_DESCRIPTION"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            final var vJoyDirectoryFileChooser = new JFileChooser(
                    preferences.get(PREFERENCES_VJOY_DIRECTORY, VJoyOutputThread.getDefaultInstallationPath()));
            vJoyDirectoryFileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);

            if (vJoyDirectoryFileChooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) {
                final var path = vJoyDirectoryFileChooser.getSelectedFile().getAbsolutePath();
                final var file = new File(VJoyOutputThread.getLibraryFilePath(path));

                if (file.exists()) {
                    preferences.put(PREFERENCES_VJOY_DIRECTORY, path);
                    vJoyDirectoryLabel1.setText(path);
                } else
                    JOptionPane.showMessageDialog(frame,
                            rb.getString("INVALID_VJOY_DIRECTORY_DIALOG_TEXT_PREFIX")
                                    + VJoyOutputThread.getDefaultInstallationPath()
                                    + rb.getString("INVALID_VJOY_DIRECTORY_DIALOG_TEXT_SUFFIX"),
                            rb.getString("ERROR_DIALOG_TITLE"), JOptionPane.ERROR_MESSAGE);
            }
        }

    }

    private class DisplayIndicatorAction extends AbstractAction {

        private static final long serialVersionUID = 3316770144012465987L;

        private final VirtualAxis virtualAxis;

        private DisplayIndicatorAction(final VirtualAxis virtualAxis) {
            this.virtualAxis = virtualAxis;

            putValue(NAME, rb.getString("DISPLAY_INDICATOR_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("DISPLAY_INDICATOR_ACTION_DESCRIPTION"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            if (((JCheckBox) e.getSource()).isSelected())
                input.getProfile().getVirtualAxisToOverlayAxisMap().put(virtualAxis, new OverlayAxis());
            else
                input.getProfile().getVirtualAxisToOverlayAxisMap().remove(virtualAxis);

            setUnsavedChanges(true);
            updateOverlayPanel();
        }

    }

    private class InvertIndicatorAction extends AbstractAction {

        private static final long serialVersionUID = 3316770144012465987L;

        private final VirtualAxis virtualAxis;

        private InvertIndicatorAction(final VirtualAxis virtualAxis) {
            this.virtualAxis = virtualAxis;

            putValue(NAME, rb.getString("INVERT_INDICATOR_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("INVERT_INDICATOR_ACTION_DESCRIPTION"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            input.getProfile().getVirtualAxisToOverlayAxisMap()
                    .get(virtualAxis).inverted = ((JCheckBox) e.getSource()).isSelected();

            setUnsavedChanges(true);
            updateOverlayPanel();
        }

    }

    private class NewAction extends AbstractAction {

        private static final long serialVersionUID = 5703987691203427504L;

        private NewAction() {
            putValue(NAME, rb.getString("NEW_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("NEW_ACTION_DESCRIPTION"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            newProfile();
        }

    }

    private class OpenAction extends AbstractAction {

        private static final long serialVersionUID = -8932510785275935297L;

        private OpenAction() {
            putValue(NAME, rb.getString("OPEN_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("OPEN_ACTION_DESCRIPTION"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            if (fileChooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION)
                loadProfile(fileChooser.getSelectedFile());
        }

    }

    private enum OutputType {
        NONE, LOCAL, CLIENT, SERVER
    }

    private static final class ProfileFileChooser extends JFileChooser {

        private static final long serialVersionUID = -4669170626378955605L;

        private ProfileFileChooser() {
            setFileFilter(new FileNameExtensionFilter(rb.getString("PROFILE_FILE_DESCRIPTION"),
                    rb.getString("PROFILE_FILE_EXTENSION")));
            setSelectedFile(new File(rb.getString("PROFILE_FILE_SUFFIX")));
        }

        @Override
        public void approveSelection() {
            final var file = getSelectedFile();
            if (file.exists() && getDialogType() == SAVE_DIALOG) {
                final int result = JOptionPane.showConfirmDialog(this,
                        file.getName() + rb.getString("FILE_EXISTS_DIALOG_TEXT"),
                        rb.getString("FILE_EXISTS_DIALOG_TITLE"), JOptionPane.YES_NO_CANCEL_OPTION);
                switch (result) {
                case JOptionPane.NO_OPTION:
                    return;
                case JOptionPane.CLOSED_OPTION:
                    return;
                case JOptionPane.CANCEL_OPTION:
                    cancelSelection();
                    return;
                default:
                    break;
                }
            }
            super.approveSelection();
        }
    }

    private class QuitAction extends AbstractAction {

        private static final long serialVersionUID = 8952460723177800923L;

        private QuitAction() {
            putValue(NAME, rb.getString("QUIT_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("QUIT_ACTION_DESCRIPTION"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            quit();
        }

    }

    private class RemoveModeAction extends AbstractAction {

        private static final long serialVersionUID = -1056071724769862582L;

        private final Mode mode;

        private RemoveModeAction(final Mode mode) {
            this.mode = mode;

            putValue(NAME, rb.getString("REMOVE_MODE_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("REMOVE_MODE_ACTION_DESCRIPTION_PREFIX")
                    + mode.getDescription() + rb.getString("REMOVE_MODE_ACTION_DESCRIPTION_SUFFIX"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            input.getProfile().removeMode(input, mode);
            setUnsavedChanges(true);
            updateModesPanel();
        }

    }

    private class SaveAction extends AbstractAction {

        private static final long serialVersionUID = -8469921697479550983L;

        private SaveAction() {
            putValue(NAME, rb.getString("SAVE_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("SAVE_ACTION_DESCRIPTION"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            if (currentFile != null)
                saveProfile(currentFile);
            else
                saveProfileAs();
        }

    }

    private class SaveAsAction extends AbstractAction {

        private static final long serialVersionUID = -8469921697479550983L;

        private SaveAsAction() {
            putValue(NAME, rb.getString("SAVE_AS_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("SAVE_AS_ACTION_DESCRIPTION"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            saveProfileAs();
        }

    }

    private class SelectControllerAction extends AbstractAction {

        private static final long serialVersionUID = -2043467156713598592L;

        private final int jid;

        private SelectControllerAction(final int jid) {
            this.jid = jid;

            final var name = glfwGetGamepadName(jid);
            putValue(NAME, name);
            putValue(SHORT_DESCRIPTION, rb.getString("SELECT_CONTROLLER_ACTION_DESCRIPTION_PREFIX") + name
                    + rb.getString("SELECT_CONTROLLER_ACTION_DESCRIPTION_SUFFIX"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            setSelectedJid(jid);
        }

    }

    private class SelectIndicatorColorAction extends AbstractAction {

        private static final long serialVersionUID = 3316770144012465987L;

        private final VirtualAxis virtualAxis;

        private SelectIndicatorColorAction(final VirtualAxis virtualAxis) {
            this.virtualAxis = virtualAxis;

            putValue(NAME, rb.getString("CHANGE_INDICATOR_COLOR_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("CHANGE_INDICATOR_COLOR_ACTION_DESCRIPTION"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            final var overlayAxis = input.getProfile().getVirtualAxisToOverlayAxisMap().get(virtualAxis);

            final var newColor = JColorChooser.showDialog(frame, "Choose Background Color", overlayAxis.color);
            if (newColor != null)
                overlayAxis.color = newColor;

            setUnsavedChanges(true);
            updateOverlayPanel();
        }

    }

    private class SetHostAction extends AbstractAction implements FocusListener {

        private static final long serialVersionUID = -7674562782751876814L;

        private final JTextField hostTextField;

        private SetHostAction(final JTextField hostTextField) {
            this.hostTextField = hostTextField;
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            setHost();
        }

        @Override
        public void focusGained(final FocusEvent e) {
        }

        @Override
        public void focusLost(final FocusEvent e) {
            setHost();
        }

        private void setHost() {
            final var host = hostTextField.getText();

            if (host != null && host.length() > 0)
                preferences.put(PREFERENCES_HOST, host);
            else
                hostTextField.setText(preferences.get(PREFERENCES_HOST, ClientVJoyOutputThread.DEFAULT_HOST));
        }

    }

    private class SetModeDescriptionAction extends AbstractAction implements DocumentListener {

        private static final long serialVersionUID = -6706537047137827688L;

        private final Mode mode;
        private final JTextField modeDescriptionTextField;

        private SetModeDescriptionAction(final Mode mode, final JTextField modeDescriptionTextField) {
            this.mode = mode;
            this.modeDescriptionTextField = modeDescriptionTextField;
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            setModeDescription();
        }

        @Override
        public void changedUpdate(final DocumentEvent e) {
            setModeDescription();
        }

        @Override
        public void insertUpdate(final DocumentEvent e) {
            setModeDescription();
        }

        @Override
        public void removeUpdate(final DocumentEvent e) {
            setModeDescription();
        }

        private void setModeDescription() {
            final var description = modeDescriptionTextField.getText();

            if (description != null && description.length() > 0) {
                mode.setDescription(description);
                setUnsavedChanges(true);
            }
        }

    }

    private class ShowAboutDialogAction extends AbstractAction {

        private static final long serialVersionUID = -2578971543384483382L;

        private ShowAboutDialogAction() {
            putValue(NAME, rb.getString("SHOW_ABOUT_DIALOG_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("SHOW_ABOUT_DIALOG_ACTION_DESCRIPTION"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            final var icon = new ImageIcon(Main.class.getResource(Main.ICON_RESOURCE_PATHS[2]));
            JOptionPane.showMessageDialog(frame,
                    rb.getString("ABOUT_DIALOG_TEXT_PREFIX") + Version.getVersion()
                            + rb.getString("ABOUT_DIALOG_TEXT_SUFFIX"),
                    (String) getValue(NAME), JOptionPane.INFORMATION_MESSAGE, icon);
        }

    }

    private class ShowAction extends AbstractAction {

        private static final long serialVersionUID = 8578159622754054457L;

        private ShowAction() {
            putValue(NAME, rb.getString("SHOW_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("SHOW_ACTION_DESCRIPTION"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            final var openEvent = new WindowEvent(frame, WindowEvent.WINDOW_OPENED);
            Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(openEvent);
            frame.setVisible(true);
            frame.setExtendedState(Frame.NORMAL);
        }

    }

    private class StartClientAction extends AbstractAction {

        private static final long serialVersionUID = 3975574941559749481L;

        private StartClientAction() {
            putValue(NAME, rb.getString("START_CLIENT_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("START_CLIENT_ACTION_DESCRIPTION"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            startClient();
        }

    }

    private class StartLocalAction extends AbstractAction {

        private static final long serialVersionUID = -2003502124995392039L;

        private StartLocalAction() {
            putValue(NAME, rb.getString("START_LOCAL_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("START_LOCAL_ACTION_DESCRIPTION"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            startLocal();
        }

    }

    private class StartServerAction extends AbstractAction {

        private static final long serialVersionUID = 1758447420975631146L;

        private StartServerAction() {
            putValue(NAME, rb.getString("START_SERVER_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("START_SERVER_ACTION_DESCRIPTION"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            startServer();
        }

    }

    private class StopClientAction extends AbstractAction {

        private static final long serialVersionUID = -2863419586328503426L;

        private StopClientAction() {
            putValue(NAME, rb.getString("STOP_CLIENT_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("STOP_CLIENT_ACTION_DESCRIPTION"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            stopClient(true);
        }

    }

    private class StopLocalAction extends AbstractAction {

        private static final long serialVersionUID = -4859431944733030332L;

        private StopLocalAction() {
            putValue(NAME, rb.getString("STOP_LOCAL_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("STOP_LOCAL_ACTION_DESCRIPTION"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            stopLocal(true);
        }

    }

    private class StopServerAction extends AbstractAction {

        private static final long serialVersionUID = 6023207463370122769L;

        private StopServerAction() {
            putValue(NAME, rb.getString("STOP_SERVER_ACTION_NAME"));
            putValue(SHORT_DESCRIPTION, rb.getString("STOP_SERVER_ACTION_DESCRIPTION"));
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            stopServer(true);
        }

    }

    static {
        try {
            UIManager.setLookAndFeel(new javax.swing.plaf.metal.MetalLookAndFeel());
        } catch (final UnsupportedLookAndFeelException e) {
            throw new RuntimeException(e);
        }
    }

    private static final Logger log = System.getLogger(Main.class.getName());
    private static final String SINGLETON_ID = Version.class.getPackageName();
    public static final String STRING_RESOURCE_BUNDLE_BASENAME = "strings";
    private static final ResourceBundle rb = new ResourceBundleUtil()
            .getResourceBundle(STRING_RESOURCE_BUNDLE_BASENAME, Locale.getDefault());
    static final int DIALOG_BOUNDS_X = 100;
    static final int DIALOG_BOUNDS_Y = 100;
    static final int DIALOG_BOUNDS_WIDTH = 930;
    static final int DIALOG_BOUNDS_HEIGHT = 640;
    static final int DIALOG_BOUNDS_X_Y_OFFSET = 25;
    static final Dimension BUTTON_DIMENSION = new Dimension(110, 25);
    private static final String OPTION_AUTOSTART = "autostart";
    private static final String OPTION_TRAY = "tray";
    private static final String OPTION_VERSION = "version";
    private static final String OPTION_AUTOSTART_VALUE_LOCAL = "local";
    private static final String OPTION_AUTOSTART_VALUE_CLIENT = "client";
    private static final String OPTION_AUTOSTART_VALUE_SERVER = "server";
    private static final String PREFERENCES_POLL_INTERVAL = "poll_interval";
    private static final String PREFERENCES_LAST_CONTROLLER = "last_controller";
    private static final String PREFERENCES_LAST_PROFILE = "last_profile";
    public static final String PREFERENCES_VJOY_DIRECTORY = "vjoy_directory";
    private static final String PREFERENCES_VJOY_DEVICE = "vjoy_device";
    private static final String PREFERENCES_HOST = "host";
    private static final String PREFERENCES_PORT = "port";
    private static final String PREFERENCES_TIMEOUT = "timeout";
    private static final String PREFERENCES_SHOW_OVERLAY = "show_overlay";
    private static final String PREFERENCES_SHOW_VR_OVERLAY = "show_vr_overlay";
    private static final String PREFERENCES_PREVENT_POWER_SAVE_MODE = "prevent_power_save_mode";
    private static final long OVERLAY_POSITION_UPDATE_INTERVAL = 10000L;
    private static final String[] ICON_RESOURCE_PATHS = { "/icon_16.png", "/icon_32.png", "/icon_64.png",
            "/icon_128.png" };
    static final Color TRANSPARENT = new Color(255, 255, 255, 0);
    private static final int INVALID_JID = GLFW_JOYSTICK_1 - 1;

    private static boolean isModalDialogShowing() {
        final var windows = Window.getWindows();
        if (windows != null)
            for (final Window w : windows)
                if (w.isShowing() && w instanceof Dialog && ((Dialog) w).isModal())
                    return true;

        return false;
    }

    public static void main(final String[] args) {
        if (!Singleton.invoke(SINGLETON_ID, args))
            SwingUtilities.invokeLater(() -> {
                final var options = new Options();
                options.addOption(OPTION_AUTOSTART, true, rb.getString("AUTOSTART_OPTION_DESCRIPTION"));
                options.addOption(OPTION_TRAY, false, rb.getString("TRAY_OPTION_DESCRIPTION"));
                options.addOption(OPTION_VERSION, false, rb.getString("VERSION_OPTION_DESCRIPTION"));

                try {
                    final CommandLine commandLine = new DefaultParser().parse(options, args);
                    if (commandLine.hasOption(OPTION_VERSION))
                        System.out.println(rb.getString("APPLICATION_NAME") + ' ' + Version.getVersion());
                    else {
                        final var main = new Main();
                        if (!commandLine.hasOption(OPTION_TRAY))
                            main.frame.setVisible(true);
                        if (commandLine.hasOption(OPTION_AUTOSTART)) {
                            final var optionValue = commandLine.getOptionValue(OPTION_AUTOSTART);

                            if (OPTION_AUTOSTART_VALUE_LOCAL.equals(optionValue))
                                main.startLocal();
                            else if (OPTION_AUTOSTART_VALUE_CLIENT.equals(optionValue))
                                main.startClient();
                            else if (OPTION_AUTOSTART_VALUE_SERVER.equals(optionValue))
                                main.startServer();
                        }
                    }
                } catch (final ParseException e) {
                    final var helpFormatter = new HelpFormatter();
                    helpFormatter.printHelp(rb.getString("APPLICATION_NAME"), options, true);
                }
            });
    }

    private static void waitForThreadToFinish(final Thread thread) {
        while (thread != null && thread.isAlive())
            try {
                Thread.sleep(100L);
            } catch (final InterruptedException e) {
                Thread.currentThread().interrupt();
            }
    }

    private final boolean windows = Platform.isWindows() && !Platform.isWindowsCE();
    private final Preferences preferences = Preferences.userNodeForPackage(Version.class);
    private final Map<VirtualAxis, JProgressBar> virtualAxisToProgressBarMap = new HashMap<>();
    private volatile LocalVJoyOutputThread localThread;
    private volatile ClientVJoyOutputThread clientThread;
    private volatile ServerOutputThread serverThread;
    private int selectedJid = INVALID_JID;
    private Input input;
    private OutputType lastOutputType = OutputType.NONE;
    private final JFrame frame;
    private final OpenAction openAction = new OpenAction();
    private final JMenuBar menuBar = new JMenuBar();
    private final JMenu fileMenu = new JMenu(rb.getString("FILE_MENU"));
    private final JMenu deviceMenu = new JMenu(rb.getString("DEVICE_MENU"));
    private final JMenu localMenu = new JMenu(rb.getString("LOCAL_MENU"));
    private final JMenu clientMenu = new JMenu(rb.getString("CLIENT_MENU"));
    private final JMenu serverMenu = new JMenu(rb.getString("SERVER_MENU"));
    private final JMenuItem newMenuItem = fileMenu.add(new NewAction());
    private final JMenuItem openMenuItem = fileMenu.add(openAction);
    private final JMenuItem saveMenuItem = fileMenu.add(new SaveAction());
    private final JMenuItem saveAsMenuItem = fileMenu.add(new SaveAsAction());
    private JRadioButtonMenuItem startLocalRadioButtonMenuItem;
    private JRadioButtonMenuItem stopLocalRadioButtonMenuItem;
    private JRadioButtonMenuItem startClientRadioButtonMenuItem;
    private JRadioButtonMenuItem stopClientRadioButtonMenuItem;
    private final JRadioButtonMenuItem startServerRadioButtonMenuItem;
    private final JRadioButtonMenuItem stopServerRadioButtonMenuItem;
    private MenuItem showMenuItem;
    private final JTabbedPane tabbedPane = new JTabbedPane(SwingConstants.TOP);
    private JPanel modesPanel;
    private JScrollPane modesScrollPane;
    private JPanel modesListPanel;
    private JPanel addModePanel;
    private JPanel overlayPanel;
    private AssignmentsComponent assignmentsComponent;
    private final JScrollPane settingsScrollPane = new JScrollPane();
    private final JPanel settingsPanel;
    private JScrollPane indicatorsScrollPane;
    private JPanel indicatorsListPanel;
    private TimerTask overlayTimerTask;
    private JLabel vJoyDirectoryLabel1;
    private JTextField hostTextField;
    private final JLabel statusLabel = new JLabel(rb.getString("STATUS_READY"));
    private TrayIcon trayIcon;
    private boolean unsavedChanges = false;
    private String loadedProfile = null;
    private File currentFile;
    private ServerSocket serverSocket;
    private volatile boolean scheduleOnScreenKeyboardModeSwitch;
    private final JLabel labelCurrentMode = new JLabel();
    private final JFileChooser fileChooser = new ProfileFileChooser();
    private final Timer timer = new Timer();
    private volatile OpenVrOverlay openVrOverlay;
    private FrameDragListener overlayFrameDragListener;
    private FlowLayout indicatorPanelFlowLayout;
    private JPanel indicatorPanel;
    private Rectangle prevMaxWindowBounds;
    private volatile JFrame overlayFrame;
    private final OnScreenKeyboard onScreenKeyboard = new OnScreenKeyboard(this);

    private Main() {
        Singleton.start(this, SINGLETON_ID);

        frame = new JFrame();
        frame.addWindowListener(new WindowAdapter() {

            @Override
            public void windowClosing(final WindowEvent e) {
                super.windowClosing(e);

                if (showMenuItem != null)
                    showMenuItem.setEnabled(true);
            }

            @Override
            public void windowDeiconified(final WindowEvent e) {
                super.windowDeiconified(e);

                if (showMenuItem != null)
                    showMenuItem.setEnabled(false);
            }

            @Override
            public void windowIconified(final WindowEvent e) {
                super.windowIconified(e);

                if (showMenuItem != null)
                    showMenuItem.setEnabled(true);
            }

            @Override
            public void windowOpened(final WindowEvent e) {
                super.windowOpened(e);

                if (showMenuItem != null)
                    showMenuItem.setEnabled(false);
            }

        });

        frame.setBounds(DIALOG_BOUNDS_X, DIALOG_BOUNDS_Y, DIALOG_BOUNDS_WIDTH, DIALOG_BOUNDS_HEIGHT);
        frame.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);

        final var icons = new ArrayList<Image>();
        for (final var path : ICON_RESOURCE_PATHS) {
            final var icon = new ImageIcon(Main.class.getResource(path));
            icons.add(icon.getImage());
        }
        frame.setIconImages(icons);

        frame.setJMenuBar(menuBar);

        menuBar.add(fileMenu);
        final QuitAction quitAction = new QuitAction();
        fileMenu.add(quitAction);
        menuBar.add(deviceMenu);

        if (windows) {
            menuBar.add(localMenu, 2);

            final var buttonGroupLocalState = new ButtonGroup();
            startLocalRadioButtonMenuItem = new JRadioButtonMenuItem(rb.getString("START_MENU_ITEM"));
            startLocalRadioButtonMenuItem.setAction(new StartLocalAction());
            buttonGroupLocalState.add(startLocalRadioButtonMenuItem);
            localMenu.add(startLocalRadioButtonMenuItem);

            stopLocalRadioButtonMenuItem = new JRadioButtonMenuItem(rb.getString("STOP_MENU_ITEM"));
            stopLocalRadioButtonMenuItem.setAction(new StopLocalAction());
            buttonGroupLocalState.add(stopLocalRadioButtonMenuItem);
            localMenu.add(stopLocalRadioButtonMenuItem);

            menuBar.add(clientMenu);

            final var buttonGroupClientState = new ButtonGroup();

            startClientRadioButtonMenuItem = new JRadioButtonMenuItem(rb.getString("START_MENU_ITEM"));
            startClientRadioButtonMenuItem.setAction(new StartClientAction());
            buttonGroupClientState.add(startClientRadioButtonMenuItem);
            clientMenu.add(startClientRadioButtonMenuItem);

            stopClientRadioButtonMenuItem = new JRadioButtonMenuItem(rb.getString("STOP_MENU_ITEM"));
            stopClientRadioButtonMenuItem.setAction(new StopClientAction());
            buttonGroupClientState.add(stopClientRadioButtonMenuItem);
            clientMenu.add(stopClientRadioButtonMenuItem);
        }

        final var buttonGroupServerState = new ButtonGroup();
        startServerRadioButtonMenuItem = new JRadioButtonMenuItem(rb.getString("START_MENU_ITEM"));
        startServerRadioButtonMenuItem.setAction(new StartServerAction());
        buttonGroupServerState.add(startServerRadioButtonMenuItem);
        serverMenu.add(startServerRadioButtonMenuItem);

        stopServerRadioButtonMenuItem = new JRadioButtonMenuItem(rb.getString("STOP_MENU_ITEM"));
        stopServerRadioButtonMenuItem.setAction(new StopServerAction());
        buttonGroupServerState.add(stopServerRadioButtonMenuItem);
        serverMenu.add(stopServerRadioButtonMenuItem);

        final var helpMenu = new JMenu(rb.getString("HELP_MENU"));
        menuBar.add(helpMenu);
        helpMenu.add(new ShowAboutDialogAction());

        frame.getContentPane().add(tabbedPane);

        settingsPanel = new JPanel();
        settingsPanel.setLayout(new GridBagLayout());

        settingsScrollPane.setViewportView(settingsPanel);
        tabbedPane.addTab(rb.getString("SETTINGS_TAB"), null, settingsScrollPane);

        final var panelGridBagConstraints = new GridBagConstraints(0, GridBagConstraints.RELATIVE, 1, 1, 0.0, 0.0,
                GridBagConstraints.FIRST_LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 5);

        final var panelFlowLayout = new FlowLayout(FlowLayout.LEADING, 10, 10);

        final var pollIntervalPanel = new JPanel(panelFlowLayout);
        settingsPanel.add(pollIntervalPanel, panelGridBagConstraints);

        final var pollIntervalLabel = new JLabel(rb.getString("POLL_INTERVAL_LABEL"));
        pollIntervalLabel.setPreferredSize(new Dimension(120, 15));
        pollIntervalPanel.add(pollIntervalLabel);

        final var pollIntervalSpinner = new JSpinner(new SpinnerNumberModel(
                preferences.getInt(PREFERENCES_POLL_INTERVAL, OutputThread.DEFAULT_POLL_INTERVAL), 10, 500, 1));
        final JSpinner.DefaultEditor pollIntervalSpinnerEditor = new JSpinner.NumberEditor(pollIntervalSpinner,
                "#");
        ((DefaultFormatter) pollIntervalSpinnerEditor.getTextField().getFormatter()).setCommitsOnValidEdit(true);
        pollIntervalSpinner.setEditor(pollIntervalSpinnerEditor);
        pollIntervalSpinner.addChangeListener(
                e -> preferences.putInt(PREFERENCES_POLL_INTERVAL, (int) ((JSpinner) e.getSource()).getValue()));
        pollIntervalPanel.add(pollIntervalSpinner);

        if (windows) {
            final var vJoyDirectoryPanel = new JPanel(panelFlowLayout);
            settingsPanel.add(vJoyDirectoryPanel, panelGridBagConstraints);

            final var vJoyDirectoryLabel = new JLabel(rb.getString("VJOY_DIRECTORY_LABEL"));
            vJoyDirectoryLabel.setPreferredSize(new Dimension(120, 15));
            vJoyDirectoryPanel.add(vJoyDirectoryLabel);

            vJoyDirectoryLabel1 = new JLabel(
                    preferences.get(PREFERENCES_VJOY_DIRECTORY, VJoyOutputThread.getDefaultInstallationPath()));
            vJoyDirectoryPanel.add(vJoyDirectoryLabel1);

            final var vJoyDirectoryButton = new JButton(new ChangeVJoyDirectoryAction());
            vJoyDirectoryPanel.add(vJoyDirectoryButton);

            final var vJoyDevicePanel = new JPanel(panelFlowLayout);
            settingsPanel.add(vJoyDevicePanel, panelGridBagConstraints);

            final var vJoyDeviceLabel = new JLabel(rb.getString("VJOY_DEVICE_LABEL"));
            vJoyDeviceLabel.setPreferredSize(new Dimension(120, 15));
            vJoyDevicePanel.add(vJoyDeviceLabel);

            final var vJoyDeviceSpinner = new JSpinner(new SpinnerNumberModel(
                    preferences.getInt(PREFERENCES_VJOY_DEVICE, VJoyOutputThread.DEFAULT_VJOY_DEVICE), 1, 16, 1));
            final JSpinner.DefaultEditor vJoyDeviceSpinnerEditor = new JSpinner.NumberEditor(vJoyDeviceSpinner,
                    "#");
            ((DefaultFormatter) vJoyDeviceSpinnerEditor.getTextField().getFormatter()).setCommitsOnValidEdit(true);
            vJoyDeviceSpinner.setEditor(vJoyDeviceSpinnerEditor);
            vJoyDeviceSpinner.addChangeListener(
                    e -> preferences.putInt(PREFERENCES_VJOY_DEVICE, (int) ((JSpinner) e.getSource()).getValue()));
            vJoyDevicePanel.add(vJoyDeviceSpinner);

            final var hostPanel = new JPanel(panelFlowLayout);
            settingsPanel.add(hostPanel, panelGridBagConstraints);

            final var hostLabel = new JLabel(rb.getString("HOST_LABEL"));
            hostLabel.setPreferredSize(new Dimension(120, 15));
            hostPanel.add(hostLabel);

            hostTextField = new JTextField(preferences.get(PREFERENCES_HOST, ClientVJoyOutputThread.DEFAULT_HOST),
                    10);
            final var setHostAction = new SetHostAction(hostTextField);
            hostTextField.addActionListener(setHostAction);
            hostTextField.addFocusListener(setHostAction);
            hostPanel.add(hostTextField);
        }

        final var portPanel = new JPanel(panelFlowLayout);
        settingsPanel.add(portPanel, panelGridBagConstraints);

        final var portLabel = new JLabel(rb.getString("PORT_LABEL"));
        portLabel.setPreferredSize(new Dimension(120, 15));
        portPanel.add(portLabel);

        final var portSpinner = new JSpinner(new SpinnerNumberModel(
                preferences.getInt(PREFERENCES_PORT, ServerOutputThread.DEFAULT_PORT), 1024, 65535, 1));
        final JSpinner.DefaultEditor portSpinnerEditor = new JSpinner.NumberEditor(portSpinner, "#");
        ((DefaultFormatter) portSpinnerEditor.getTextField().getFormatter()).setCommitsOnValidEdit(true);
        portSpinner.setEditor(portSpinnerEditor);
        portSpinner.addChangeListener(
                e -> preferences.putInt(PREFERENCES_PORT, (int) ((JSpinner) e.getSource()).getValue()));
        portPanel.add(portSpinner);

        final var timeoutPanel = new JPanel(panelFlowLayout);
        settingsPanel.add(timeoutPanel, panelGridBagConstraints);

        final var timeoutLabel = new JLabel(rb.getString("TIMEOUT_LABEL"));
        timeoutLabel.setPreferredSize(new Dimension(120, 15));
        timeoutPanel.add(timeoutLabel);

        final var timeoutSpinner = new JSpinner(new SpinnerNumberModel(
                preferences.getInt(PREFERENCES_TIMEOUT, ServerOutputThread.DEFAULT_TIMEOUT), 10, 60000, 1));
        final JSpinner.DefaultEditor timeoutSpinnerEditor = new JSpinner.NumberEditor(timeoutSpinner, "#");
        ((DefaultFormatter) timeoutSpinnerEditor.getTextField().getFormatter()).setCommitsOnValidEdit(true);
        timeoutSpinner.setEditor(timeoutSpinnerEditor);
        timeoutSpinner.addChangeListener(
                e -> preferences.putInt(PREFERENCES_TIMEOUT, (int) ((JSpinner) e.getSource()).getValue()));
        timeoutPanel.add(timeoutSpinner);

        final var alwaysOnTopSupported = Toolkit.getDefaultToolkit().isAlwaysOnTopSupported();
        if (alwaysOnTopSupported || preferences.getBoolean(PREFERENCES_SHOW_OVERLAY, alwaysOnTopSupported)) {
            final var overlaySettingsPanel = new JPanel(panelFlowLayout);
            settingsPanel.add(overlaySettingsPanel, panelGridBagConstraints);

            final var overlayLabel = new JLabel(rb.getString("OVERLAY_LABEL"));
            overlayLabel.setPreferredSize(new Dimension(120, 15));
            overlaySettingsPanel.add(overlayLabel);

            final var showOverlayCheckBox = new JCheckBox(rb.getString("SHOW_OVERLAY_CHECK_BOX"));
            showOverlayCheckBox.setSelected(preferences.getBoolean(PREFERENCES_SHOW_OVERLAY, true));
            showOverlayCheckBox.addActionListener(e -> {
                final boolean showOverlay = ((JCheckBox) e.getSource()).isSelected();

                preferences.putBoolean(PREFERENCES_SHOW_OVERLAY, showOverlay);
            });
            overlaySettingsPanel.add(showOverlayCheckBox);
        }

        if (windows) {
            if (preferences.getBoolean(PREFERENCES_SHOW_VR_OVERLAY, true)) {
                final var vrOverlaySettingsPanel = new JPanel(panelFlowLayout);
                settingsPanel.add(vrOverlaySettingsPanel, panelGridBagConstraints);

                final var vrOverlayLabel = new JLabel(rb.getString("VR_OVERLAY_LABEL"));
                vrOverlayLabel.setPreferredSize(new Dimension(120, 15));
                vrOverlaySettingsPanel.add(vrOverlayLabel);

                final var showVrOverlayCheckBox = new JCheckBox(rb.getString("SHOW_VR_OVERLAY_CHECK_BOX"));
                showVrOverlayCheckBox.setSelected(preferences.getBoolean(PREFERENCES_SHOW_VR_OVERLAY, true));
                showVrOverlayCheckBox.addActionListener(e -> {
                    final var showVrOverlay = ((JCheckBox) e.getSource()).isSelected();

                    preferences.putBoolean(PREFERENCES_SHOW_VR_OVERLAY, showVrOverlay);
                });
                vrOverlaySettingsPanel.add(showVrOverlayCheckBox);
            }

            final var preventPowerSaveModeSettingsPanel = new JPanel(panelFlowLayout);
            settingsPanel.add(preventPowerSaveModeSettingsPanel, panelGridBagConstraints);

            final var preventPowerSaveModeLabel = new JLabel(rb.getString("POWER_SAVE_MODE_LABEL"));
            preventPowerSaveModeLabel.setPreferredSize(new Dimension(120, 15));
            preventPowerSaveModeSettingsPanel.add(preventPowerSaveModeLabel);

            final var preventPowerSaveModeCheckBox = new JCheckBox(
                    rb.getString("PREVENT_POWER_SAVE_MODE_CHECK_BOX"));
            preventPowerSaveModeCheckBox
                    .setSelected(preferences.getBoolean(PREFERENCES_PREVENT_POWER_SAVE_MODE, true));
            preventPowerSaveModeCheckBox.addActionListener(e -> {
                final var preventPowerSaveMode = ((JCheckBox) e.getSource()).isSelected();

                preferences.putBoolean(PREFERENCES_PREVENT_POWER_SAVE_MODE, preventPowerSaveMode);
            });
            preventPowerSaveModeSettingsPanel.add(preventPowerSaveModeCheckBox);
        }

        if (SystemTray.isSupported()) {
            final var popupMenu = new PopupMenu();

            final var showAction = new ShowAction();
            showMenuItem = new MenuItem((String) showAction.getValue(Action.NAME));
            showMenuItem.addActionListener(showAction);
            popupMenu.add(showMenuItem);

            popupMenu.addSeparator();

            final var openMenuItem = new MenuItem((String) openAction.getValue(Action.NAME));
            openMenuItem.addActionListener(openAction);
            popupMenu.add(openMenuItem);

            popupMenu.addSeparator();

            final var quitMenuItem = new MenuItem((String) quitAction.getValue(Action.NAME));
            quitMenuItem.addActionListener(quitAction);
            popupMenu.add(quitMenuItem);

            trayIcon = new TrayIcon(frame.getIconImage());
            trayIcon.addActionListener(showAction);
            trayIcon.setPopupMenu(popupMenu);
            try {
                SystemTray.getSystemTray().add(trayIcon);
            } catch (final AWTException e) {
                log.log(Logger.Level.ERROR, e.getMessage(), e);
            }
        }

        updateTitleAndTooltip();

        settingsPanel.add(Box.createGlue(), new GridBagConstraints(0, GridBagConstraints.RELATIVE, 1, 1, 1.0, 1.0,
                GridBagConstraints.FIRST_LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));

        final var outsideBorder = BorderFactory.createEtchedBorder(EtchedBorder.RAISED);
        final var insideBorder = BorderFactory.createEmptyBorder(0, 5, 0, 5);
        statusLabel.setBorder(BorderFactory.createCompoundBorder(outsideBorder, insideBorder));
        frame.add(statusLabel, BorderLayout.SOUTH);

        final var glfwInitialized = glfwInit();
        if (!glfwInitialized)
            if (windows)
                JOptionPane.showMessageDialog(frame, rb.getString("COULD_NOT_INITIALIZE_GLFW_DIALOG_TEXT_WINDOWS"),
                        rb.getString("ERROR_DIALOG_TITLE"), JOptionPane.ERROR_MESSAGE);
            else {
                JOptionPane.showMessageDialog(frame, rb.getString("COULD_NOT_INITIALIZE_GLFW_DIALOG_TEXT"),
                        rb.getString("ERROR_DIALOG_TITLE"), JOptionPane.ERROR_MESSAGE);
                quit();
            }

        final var presentJids = new HashSet<Integer>();
        for (var jid = GLFW_JOYSTICK_1; jid <= GLFW_JOYSTICK_LAST; jid++)
            if (glfwJoystickPresent(jid) && glfwJoystickIsGamepad(jid))
                presentJids.add(jid);

        final var lastControllerGuid = preferences.get(PREFERENCES_LAST_CONTROLLER, null);
        for (final var jid : presentJids) {
            final var lastControllerFound = lastControllerGuid != null
                    ? lastControllerGuid.equals(glfwGetJoystickGUID(jid))
                    : false;

            if (!isSelectedJidValid() || lastControllerFound)
                selectedJid = jid;

            if (lastControllerFound)
                break;
        }

        newProfile();

        onControllersChanged(true);

        glfwSetJoystickCallback(new GLFWJoystickCallback() {

            @Override
            public void invoke(final int jid, final int event) {
                final var disconnected = event == GLFW_DISCONNECTED;
                if (disconnected || glfwJoystickIsGamepad(jid)) {
                    if (disconnected && selectedJid == jid)
                        selectedJid = INVALID_JID;

                    invokeOnEventDispatchThreadIfRequired(() -> onControllersChanged(false));
                }

            }
        });

        if (glfwInitialized && presentJids.isEmpty()) {
            if (windows)
                JOptionPane.showMessageDialog(frame, rb.getString("NO_CONTROLLER_CONNECTED_DIALOG_TEXT_WINDOWS"),
                        rb.getString("INFORMATION_DIALOG_TITLE"), JOptionPane.INFORMATION_MESSAGE);
            else
                JOptionPane.showMessageDialog(frame, rb.getString("NO_CONTROLLER_CONNECTED_DIALOG_TEXT"),
                        rb.getString("INFORMATION_DIALOG_TITLE"), JOptionPane.INFORMATION_MESSAGE);
        } else {
            final String path = preferences.get(PREFERENCES_LAST_PROFILE, null);
            if (path != null)
                loadProfile(new File(path));
        }
    }

    private void deInitOverlay() {
        if (openVrOverlay != null) {
            openVrOverlay.stop();
            openVrOverlay = null;
        }

        if (overlayFrame != null) {
            overlayFrame.dispose();
            overlayFrame = null;
        }

        virtualAxisToProgressBarMap.clear();

        onScreenKeyboard.setVisible(false);
    }

    public void displayChargingStateInfo(final boolean charging) {
        if (trayIcon != null && input != null)
            trayIcon.displayMessage(rb.getString("CHARGING_STATE_CAPTION"),
                    (charging ? rb.getString("CHARGING_STATE_CHARGING_PREFIX")
                            : rb.getString("CHARGING_STATE_DISCHARGING_PREFIX")) + input.getBatteryState()
                            + rb.getString("CHARGING_STATE_SUFFIX"),
                    MessageType.INFO);
    }

    public void displayLowBatteryWarning(final int batteryCharge) {
        SwingUtilities.invokeLater(() -> {
            if (trayIcon != null)
                trayIcon.displayMessage(rb.getString("LOW_BATTERY_CAPTION"), batteryCharge + "%",
                        MessageType.WARNING);
        });
    }

    public JFrame getFrame() {
        return frame;
    }

    Input getInput() {
        return input;
    }

    public OnScreenKeyboard getOnScreenKeyboard() {
        return onScreenKeyboard;
    }

    JFrame getOverlayFrame() {
        return overlayFrame;
    }

    public Preferences getPreferences() {
        return preferences;
    }

    public Timer getTimer() {
        return timer;
    }

    public void handleOnScreenKeyboardModeChange() {
        if (scheduleOnScreenKeyboardModeSwitch) {
            for (final var buttonToModeActions : input.getProfile().getButtonToModeActionsMap().values())
                for (final var buttonToModeAction : buttonToModeActions)
                    if (OnScreenKeyboard.onScreenKeyboardMode.equals(buttonToModeAction.getMode(input))) {
                        buttonToModeAction.doAction(input, Byte.MAX_VALUE);
                        break;
                    }

            scheduleOnScreenKeyboardModeSwitch = false;
        }
    }

    private void initOverlay() {
        if (!preferences.getBoolean(PREFERENCES_SHOW_OVERLAY, Toolkit.getDefaultToolkit().isAlwaysOnTopSupported()))
            return;

        var longestDescription = "";
        for (final var mode : input.getProfile().getModes()) {
            final var description = mode.getDescription();
            if (description.length() > longestDescription.length())
                longestDescription = description;
        }

        final var fontMetrics = labelCurrentMode.getFontMetrics(labelCurrentMode.getFont());
        labelCurrentMode.setPreferredSize(
                new Dimension(fontMetrics.stringWidth(longestDescription), fontMetrics.getHeight()));
        labelCurrentMode.setForeground(Color.RED);
        labelCurrentMode.setText(input.getProfile().getActiveMode().getDescription());

        overlayFrame = new JFrame("Overlay");
        overlayFrame.setType(JFrame.Type.UTILITY);
        overlayFrame.setLayout(new BorderLayout());
        overlayFrame.setFocusableWindowState(false);
        overlayFrame.setUndecorated(true);
        overlayFrame.setBackground(TRANSPARENT);

        overlayFrame.add(labelCurrentMode, BorderLayout.PAGE_END);
        overlayFrame.setAlwaysOnTop(true);

        indicatorPanelFlowLayout = new FlowLayout();
        indicatorPanel = new JPanel(indicatorPanelFlowLayout);
        indicatorPanel.setBackground(TRANSPARENT);

        final var virtualAxisToOverlayAxisMap = input.getProfile().getVirtualAxisToOverlayAxisMap();
        for (final var virtualAxis : Input.VirtualAxis.values()) {
            final var overlayAxis = virtualAxisToOverlayAxisMap.get(virtualAxis);
            if (overlayAxis != null) {
                final var progressBar = new JProgressBar(SwingConstants.VERTICAL) {

                    private static final long serialVersionUID = 8167193907929992395L;

                    @Override
                    public void setMaximum(final int n) {
                        if (overlayAxis.inverted)
                            super.setMinimum(-n);
                        else
                            super.setMaximum(n);
                    }

                    @Override
                    public void setMinimum(final int n) {
                        if (overlayAxis.inverted)
                            super.setMaximum(-n);
                        else
                            super.setMinimum(n);
                    }

                    @Override
                    public void setValue(final int n) {
                        super.setValue(overlayAxis.inverted ? -n : n);
                    }
                };
                progressBar.setPreferredSize(new Dimension(21, 149));
                progressBar.setBorder(BorderFactory.createDashedBorder(Color.BLACK,
                        (float) progressBar.getPreferredSize().getWidth(),
                        (float) progressBar.getPreferredSize().getWidth()));
                progressBar.setBackground(Color.LIGHT_GRAY);
                progressBar.setForeground(overlayAxis.color);
                progressBar.setValue(1);
                indicatorPanel.add(progressBar);
                virtualAxisToProgressBarMap.put(virtualAxis, progressBar);
            }
        }

        overlayFrame.add(indicatorPanel);

        overlayFrameDragListener = new FrameDragListener(this, overlayFrame) {

            @Override
            public void mouseDragged(final MouseEvent e) {
                super.mouseDragged(e);
                final var maxWindowBounds = GraphicsEnvironment.getLocalGraphicsEnvironment()
                        .getMaximumWindowBounds();
                updateOverlayAlignment(maxWindowBounds);
            }

        };
        overlayFrame.addMouseListener(overlayFrameDragListener);
        overlayFrame.addMouseMotionListener(overlayFrameDragListener);

        prevMaxWindowBounds = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
        updateOverlayLocation(prevMaxWindowBounds);

        overlayFrame.setVisible(true);
    }

    private void initVrOverlay() {
        if (!windows || !preferences.getBoolean(PREFERENCES_SHOW_VR_OVERLAY, true))
            return;

        try {
            openVrOverlay = new OpenVrOverlay(this);
        } catch (final Exception e) {
            openVrOverlay = null;
        }
    }

    private boolean isSelectedJidValid() {
        return selectedJid >= GLFW_JOYSTICK_1 && selectedJid <= GLFW_JOYSTICK_LAST;
    }

    public boolean isWindows() {
        return windows;
    }

    private void loadProfile(final File file) {
        stopAll();

        var profileLoaded = false;

        try {
            final var jsonString = Files.readString(file.toPath());
            final var actionAdapter = new ActionTypeAdapter();
            final var gson = new GsonBuilder().registerTypeAdapterFactory(new ModeAwareTypeAdapterFactory())
                    .registerTypeAdapter(IAction.class, actionAdapter).create();

            try {
                final var profile = gson.fromJson(jsonString, Profile.class);

                final var unknownActionClasses = actionAdapter.getUnknownActionClasses();
                if (!unknownActionClasses.isEmpty())
                    JOptionPane.showMessageDialog(frame,
                            rb.getString("UNKNOWN_ACTION_TYPES_DIALOG_TEXT")
                                    + String.join("\n", unknownActionClasses),
                            rb.getString("WARNING_DIALOG_TITLE"), JOptionPane.WARNING_MESSAGE);

                profileLoaded = input.setProfile(profile, input.getJid());
                if (profileLoaded) {
                    saveLastProfile(file);
                    updateModesPanel();
                    updateOverlayPanel();
                    loadedProfile = file.getName();
                    setUnsavedChanges(false);
                    setStatusBarText(rb.getString("STATUS_PROFILE_LOADED") + file.getAbsolutePath());
                    scheduleStatusBarText(rb.getString("STATUS_READY"));
                    fileChooser.setSelectedFile(file);

                    restartLast();
                }
            } catch (final JsonParseException e) {
                log.log(Logger.Level.ERROR, e.getMessage(), e);
            }
        } catch (final IOException e) {
            log.log(Logger.Level.ERROR, e.getMessage(), e);
        }

        if (!profileLoaded)
            JOptionPane.showMessageDialog(frame, rb.getString("COULD_NOT_LOAD_PROFILE_DIALOG_TEXT"),
                    rb.getString("ERROR_DIALOG_TITLE"), JOptionPane.ERROR_MESSAGE);
    }

    @Override
    public void newActivation(final String... args) {
        SwingUtilities
                .invokeLater(() -> JOptionPane.showMessageDialog(frame, rb.getString("ALREADY_RUNNING_DIALOG_TEXT"),
                        rb.getString("ERROR_DIALOG_TITLE"), JOptionPane.ERROR_MESSAGE));
    }

    private void newProfile() {
        stopAll();

        currentFile = null;

        if (input != null)
            input.deInit();

        input = new Input(this, selectedJid);

        loadedProfile = null;
        updateTitleAndTooltip();
        updateModesPanel();
        updateOverlayPanel();
        setStatusBarText(rb.getString("STATUS_READY"));
        fileChooser.setSelectedFile(new File(rb.getString("PROFILE_FILE_SUFFIX")));
    }

    private void onControllersChanged(final boolean selectFirstTab) {
        final var presentJids = new HashSet<Integer>();
        for (var jid = GLFW_JOYSTICK_1; jid <= GLFW_JOYSTICK_LAST; jid++)
            if (glfwJoystickPresent(jid) && glfwJoystickIsGamepad(jid)) {
                presentJids.add(jid);

                if (!isSelectedJidValid())
                    setSelectedJid(jid);
            }

        final var controllerConnected = !presentJids.isEmpty();
        if (!controllerConnected)
            selectedJid = INVALID_JID;

        final var previousSelectedTabIndex = tabbedPane.getSelectedIndex();
        fileMenu.remove(newMenuItem);
        fileMenu.remove(openMenuItem);
        fileMenu.remove(saveMenuItem);
        fileMenu.remove(saveAsMenuItem);
        if (fileMenu.getItemCount() > 1)
            fileMenu.remove(0);
        deviceMenu.removeAll();
        menuBar.remove(deviceMenu);
        menuBar.remove(localMenu);
        menuBar.remove(serverMenu);
        tabbedPane.remove(modesPanel);
        tabbedPane.remove(assignmentsComponent);
        tabbedPane.remove(overlayPanel);

        if (controllerConnected) {
            fileMenu.insert(newMenuItem, 0);
            fileMenu.insert(openMenuItem, 1);
            fileMenu.insert(saveMenuItem, 2);
            fileMenu.insert(saveAsMenuItem, 3);
            fileMenu.insertSeparator(4);

            for (final var jid : presentJids)
                deviceMenu.add(new SelectControllerAction(jid));
            menuBar.add(deviceMenu, 1);

            if (windows)
                menuBar.add(localMenu, 2);

            menuBar.add(serverMenu, windows ? 4 : 2);

            modesPanel = new JPanel(new BorderLayout());
            tabbedPane.insertTab(rb.getString("MODES_TAB"), null, modesPanel, null,
                    tabbedPane.indexOfComponent(settingsScrollPane));

            modesListPanel = new JPanel();
            modesListPanel.setLayout(new GridBagLayout());

            modesScrollPane = new JScrollPane();
            modesScrollPane.setViewportBorder(
                    BorderFactory.createMatteBorder(10, 10, 0, 10, modesListPanel.getBackground()));
            modesPanel.add(modesScrollPane, BorderLayout.CENTER);

            addModePanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
            final var addButton = new JButton(new AddModeAction());
            addButton.setPreferredSize(BUTTON_DIMENSION);
            addModePanel.add(addButton);
            modesPanel.add(addModePanel, BorderLayout.SOUTH);

            assignmentsComponent = new AssignmentsComponent(this);
            tabbedPane.insertTab(rb.getString("ASSIGNMENTS_TAB"), null, assignmentsComponent, null,
                    tabbedPane.indexOfComponent(settingsScrollPane));

            overlayPanel = new JPanel(new BorderLayout());

            indicatorsListPanel = new JPanel();
            indicatorsListPanel.setLayout(new GridBagLayout());

            indicatorsScrollPane = new JScrollPane();
            indicatorsScrollPane.setViewportBorder(
                    BorderFactory.createMatteBorder(10, 10, 0, 10, indicatorsListPanel.getBackground()));
            overlayPanel.add(indicatorsScrollPane, BorderLayout.CENTER);
            tabbedPane.insertTab(rb.getString("OVERLAY_TAB"), null, overlayPanel, null,
                    tabbedPane.indexOfComponent(settingsScrollPane));
        }

        if (selectFirstTab || !controllerConnected)
            tabbedPane.setSelectedIndex(0);
        else if (previousSelectedTabIndex < tabbedPane.getTabCount())
            tabbedPane.setSelectedIndex(previousSelectedTabIndex);

        updateModesPanel();
        updateOverlayPanel();
        updatePanelAccess();

        frame.getContentPane().invalidate();
        frame.getContentPane().repaint();
    }

    private void onOutputThreadsChanged() {
        final var localActive = localThread != null && localThread.isAlive();
        final var clientActive = clientThread != null && clientThread.isAlive();
        final var serverActive = serverThread != null && serverThread.isAlive();

        final var noneActive = !localActive && !clientActive && !serverActive;

        if (startLocalRadioButtonMenuItem != null) {
            startLocalRadioButtonMenuItem.setSelected(localActive);
            startLocalRadioButtonMenuItem.setEnabled(noneActive);
        }

        if (stopLocalRadioButtonMenuItem != null) {
            stopLocalRadioButtonMenuItem.setSelected(!localActive);
            stopLocalRadioButtonMenuItem.setEnabled(localActive);
        }

        if (startClientRadioButtonMenuItem != null) {
            startClientRadioButtonMenuItem.setSelected(clientActive);
            startClientRadioButtonMenuItem.setEnabled(noneActive);
        }

        if (stopClientRadioButtonMenuItem != null) {
            stopClientRadioButtonMenuItem.setSelected(!clientActive);
            stopClientRadioButtonMenuItem.setEnabled(clientActive);
        }

        if (startServerRadioButtonMenuItem != null) {
            startServerRadioButtonMenuItem.setSelected(serverActive);
            startServerRadioButtonMenuItem.setEnabled(noneActive);
        }

        if (stopServerRadioButtonMenuItem != null) {
            stopServerRadioButtonMenuItem.setSelected(!serverActive);
            stopServerRadioButtonMenuItem.setEnabled(serverActive);
        }

        updatePanelAccess();
    }

    public boolean preventPowerSaveMode() {
        return preferences.getBoolean(PREFERENCES_PREVENT_POWER_SAVE_MODE, true);
    }

    public void quit() {
        if (serverSocket != null)
            try {
                serverSocket.close();
            } catch (final IOException e) {
                log.log(Logger.Level.ERROR, e.getMessage(), e);
            }

        if (input != null)
            input.deInit();

        stopAll();
        glfwTerminate();
        Singleton.stop();
        System.exit(0);
    }

    private void repaintOverlay() {
        if (overlayFrame != null) {
            overlayFrame.getContentPane().validate();
            overlayFrame.getContentPane().repaint();
        }

        if (onScreenKeyboard.isVisible()) {
            onScreenKeyboard.getContentPane().validate();
            onScreenKeyboard.getContentPane().repaint();
        }
    }

    public void restartLast() {
        switch (lastOutputType) {
        case LOCAL:
            startLocal();
            break;
        case CLIENT:
            startClient();
            break;
        case SERVER:
            startServer();
            break;
        case NONE:
            break;
        }
    }

    private void saveLastProfile(final File file) {
        currentFile = file;
        preferences.put(PREFERENCES_LAST_PROFILE, file.getAbsolutePath());
    }

    private void saveProfile(File file) {
        input.reset();

        final String profileFileSuffix = rb.getString("PROFILE_FILE_SUFFIX");
        if (!file.getName().toLowerCase(Locale.getDefault()).endsWith(profileFileSuffix))
            file = new File(file.getAbsoluteFile() + profileFileSuffix);

        final var gson = new GsonBuilder().registerTypeAdapterFactory(new ModeAwareTypeAdapterFactory())
                .registerTypeAdapter(IAction.class, new ActionTypeAdapter()).setPrettyPrinting().create();
        final var jsonString = gson.toJson(input.getProfile());
        try {
            Files.writeString(file.toPath(), jsonString);
            saveLastProfile(file);
            loadedProfile = file.getName();
            setUnsavedChanges(false);
            setStatusBarText(rb.getString("STATUS_PROFILE_SAVED") + file.getAbsolutePath());
            scheduleStatusBarText(rb.getString("STATUS_READY"));
        } catch (final IOException e) {
            log.log(Logger.Level.ERROR, e.getMessage(), e);
            JOptionPane.showMessageDialog(frame, rb.getString("COULD_NOT_SAVE_PROFILE"),
                    rb.getString("ERROR_DIALOG_TITLE"), JOptionPane.ERROR_MESSAGE);
        }
    }

    private void saveProfileAs() {
        fileChooser.setSelectedFile(currentFile);
        if (fileChooser.showSaveDialog(frame) == JFileChooser.APPROVE_OPTION)
            saveProfile(fileChooser.getSelectedFile());
    }

    public void scheduleStatusBarText(final String text) {
        class StatusBarTextTimerTask extends TimerTask {

            private final String newText;
            private final String originalText;

            public StatusBarTextTimerTask(final String newText) {
                this.newText = newText;
                originalText = statusLabel.getText();
            }

            @Override
            public void run() {
                SwingUtilities.invokeLater(() -> {
                    if (statusLabel.getText().equals(originalText))
                        setStatusBarText(newText);
                });
            }
        }

        timer.schedule(new StatusBarTextTimerTask(text), 5000L);
    }

    public void setOverlayText(final String text) {
        invokeOnEventDispatchThreadIfRequired(() -> labelCurrentMode.setText(text));
    }

    public void setSelectedJid(final int jid) {
        if (selectedJid == jid)
            return;

        selectedJid = jid;

        final var guid = glfwGetJoystickGUID(jid);
        if (guid != null)
            preferences.put(PREFERENCES_LAST_CONTROLLER, guid);

        stopAll();

        Profile previousProfile = null;
        if (input != null) {
            input.deInit();
            previousProfile = input.getProfile();
        }

        input = new Input(this, selectedJid);

        if (previousProfile != null)
            input.setProfile(previousProfile, selectedJid);
    }

    public void setStatusBarText(final String text) {
        if (statusLabel != null) {
            statusLabel.setText(text);
            repaintOverlay();
        }
    }

    void setUnsavedChanges(final boolean unsavedChanges) {
        this.unsavedChanges = unsavedChanges;
        updateTitleAndTooltip();
    }

    private void startClient() {
        lastOutputType = OutputType.CLIENT;
        clientThread = new ClientVJoyOutputThread(Main.this, input);
        clientThread.setvJoyDevice(
                new UINT(preferences.getInt(PREFERENCES_VJOY_DEVICE, VJoyOutputThread.DEFAULT_VJOY_DEVICE)));
        clientThread.setHost(hostTextField.getText());
        clientThread.setPort(preferences.getInt(PREFERENCES_PORT, ServerOutputThread.DEFAULT_PORT));
        clientThread.setTimeout(preferences.getInt(PREFERENCES_TIMEOUT, ServerOutputThread.DEFAULT_TIMEOUT));
        clientThread.start();

        onOutputThreadsChanged();
    }

    private void startLocal() {
        lastOutputType = OutputType.LOCAL;
        localThread = new LocalVJoyOutputThread(Main.this, input);
        localThread.setvJoyDevice(
                new UINT(preferences.getInt(PREFERENCES_VJOY_DEVICE, VJoyOutputThread.DEFAULT_VJOY_DEVICE)));
        localThread
                .setPollInterval(preferences.getInt(PREFERENCES_POLL_INTERVAL, OutputThread.DEFAULT_POLL_INTERVAL));
        localThread.start();

        onOutputThreadsChanged();

        initOverlay();
        initVrOverlay();
        startOverlayTimerTask();
    }

    private void startOverlayTimerTask() {
        stopOverlayTimerTask();

        overlayTimerTask = new TimerTask() {

            @Override
            public void run() {
                SwingUtilities.invokeLater(() -> {
                    if (!isModalDialogShowing()) {
                        if (overlayFrame != null) {
                            overlayFrame.setAlwaysOnTop(false);
                            overlayFrame.setAlwaysOnTop(true);
                        }

                        if (onScreenKeyboard.isVisible()) {
                            onScreenKeyboard.setAlwaysOnTop(false);
                            onScreenKeyboard.setAlwaysOnTop(true);
                        }
                    }

                    final var maxWindowBounds = GraphicsEnvironment.getLocalGraphicsEnvironment()
                            .getMaximumWindowBounds();
                    if (!maxWindowBounds.equals(prevMaxWindowBounds)) {
                        prevMaxWindowBounds = maxWindowBounds;

                        updateOverlayLocation(maxWindowBounds);
                        onScreenKeyboard.updateLocation();
                    }

                    repaintOverlay();
                });
            }
        };

        timer.schedule(overlayTimerTask, OVERLAY_POSITION_UPDATE_INTERVAL, OVERLAY_POSITION_UPDATE_INTERVAL);
    }

    private void startServer() {
        lastOutputType = OutputType.SERVER;
        serverThread = new ServerOutputThread(Main.this, input);
        serverThread.setPort(preferences.getInt(PREFERENCES_PORT, ServerOutputThread.DEFAULT_PORT));
        serverThread.setTimeout(preferences.getInt(PREFERENCES_TIMEOUT, ServerOutputThread.DEFAULT_TIMEOUT));
        serverThread
                .setPollInterval(preferences.getInt(PREFERENCES_POLL_INTERVAL, OutputThread.DEFAULT_POLL_INTERVAL));
        serverThread.start();

        onOutputThreadsChanged();

        initOverlay();
        startOverlayTimerTask();
    }

    public void stopAll() {
        if (windows) {
            stopLocal(false);
            stopClient(false);
        }
        stopServer(false);

        System.gc();
    }

    private void stopClient(final boolean resetLastOutputType) {
        if (clientThread != null)
            clientThread.stopOutput();

        if (resetLastOutputType)
            lastOutputType = OutputType.NONE;

        waitForThreadToFinish(clientThread);

        invokeOnEventDispatchThreadIfRequired(() -> {
            onOutputThreadsChanged();
        });
    }

    private void stopLocal(final boolean resetLastOutputType) {
        if (localThread != null)
            localThread.stopOutput();

        if (resetLastOutputType)
            lastOutputType = OutputType.NONE;

        invokeOnEventDispatchThreadIfRequired(() -> {
            stopOverlayTimerTask();
            deInitOverlay();
        });

        waitForOverlayDeInit();

        waitForThreadToFinish(localThread);

        invokeOnEventDispatchThreadIfRequired(() -> {
            onOutputThreadsChanged();
        });
    }

    private void stopOverlayTimerTask() {
        if (overlayTimerTask != null)
            overlayTimerTask.cancel();
    }

    private void stopServer(final boolean resetLastOutputType) {
        if (serverThread != null)
            serverThread.stopOutput();

        if (resetLastOutputType)
            lastOutputType = OutputType.NONE;

        invokeOnEventDispatchThreadIfRequired(() -> {
            stopOverlayTimerTask();
            deInitOverlay();
        });

        waitForOverlayDeInit();

        waitForThreadToFinish(serverThread);

        invokeOnEventDispatchThreadIfRequired(() -> {
            onOutputThreadsChanged();
        });
    }

    public void toggleOnScreenKeyboard() {
        if (localThread != null && localThread.isAlive() || clientThread != null && clientThread.isAlive()
                || serverThread != null && serverThread.isAlive())
            SwingUtilities.invokeLater(() -> {
                onScreenKeyboard.setVisible(!onScreenKeyboard.isVisible());
                repaintOverlay();
            });
    }

    void updateModesPanel() {
        if (modesListPanel == null)
            return;

        modesListPanel.removeAll();

        final var modes = input.getProfile().getModes();
        for (var i = 0; i < modes.size(); i++) {
            final var mode = modes.get(i);

            final var modePanel = new JPanel(new GridBagLayout());
            modesListPanel.add(modePanel,
                    new GridBagConstraints(0, GridBagConstraints.RELATIVE, 1, 1, 0.0, 0.0,
                            GridBagConstraints.FIRST_LINE_START, GridBagConstraints.HORIZONTAL,
                            new Insets(0, 0, 0, 0), 0, 5));

            final var modeNoLabel = new JLabel(rb.getString("MODE_NO_LABEL_PREFIX") + (i + 1));
            modeNoLabel.setPreferredSize(new Dimension(100, 15));
            modePanel.add(modeNoLabel, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.BASELINE,
                    GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));

            modePanel.add(Box.createGlue(), new GridBagConstraints(1, GridBagConstraints.RELATIVE, 1, 1, 1.0, 1.0,
                    GridBagConstraints.BASELINE, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));

            final var descriptionTextField = new JTextField(mode.getDescription(), 20);
            modePanel.add(descriptionTextField, new GridBagConstraints(2, 0, 1, 1, 1.0, 1.0,
                    GridBagConstraints.BASELINE, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));

            final var setModeDescriptionAction = new SetModeDescriptionAction(mode, descriptionTextField);
            descriptionTextField.addActionListener(setModeDescriptionAction);
            descriptionTextField.getDocument().addDocumentListener(setModeDescriptionAction);

            modePanel.add(Box.createGlue(), new GridBagConstraints(3, GridBagConstraints.RELATIVE, 1, 1, 1.0, 1.0,
                    GridBagConstraints.BASELINE, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));

            if (Profile.defaultMode.equals(mode) || OnScreenKeyboard.onScreenKeyboardMode.equals(mode)) {
                descriptionTextField.setEditable(false);
                modePanel.add(Box.createHorizontalStrut(BUTTON_DIMENSION.width));
            } else {
                final var deleteButton = new JButton(new RemoveModeAction(mode));
                deleteButton.setPreferredSize(BUTTON_DIMENSION);
                modePanel.add(deleteButton, new GridBagConstraints(4, GridBagConstraints.RELATIVE, 1, 1, 0.0, 0.0,
                        GridBagConstraints.BASELINE, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
            }
        }

        modesListPanel.add(Box.createGlue(), new GridBagConstraints(0, GridBagConstraints.RELATIVE, 1, 1, 1.0, 1.0,
                GridBagConstraints.FIRST_LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));

        modesScrollPane.setViewportView(modesListPanel);
    }

    private void updateOverlayAlignment(final Rectangle maxWindowBounds) {
        final var inLowerHalf = overlayFrame.getY() + overlayFrame.getHeight() / 2 < maxWindowBounds.height / 2;

        overlayFrame.remove(labelCurrentMode);
        overlayFrame.add(labelCurrentMode, inLowerHalf ? BorderLayout.PAGE_START : BorderLayout.PAGE_END);

        var alignment = SwingConstants.RIGHT;
        var flowLayoutAlignment = FlowLayout.RIGHT;
        if (overlayFrame.getX() + overlayFrame.getWidth() / 2 < maxWindowBounds.width / 2) {
            alignment = SwingConstants.LEFT;
            flowLayoutAlignment = FlowLayout.LEFT;
        }

        labelCurrentMode.setHorizontalAlignment(alignment);

        indicatorPanelFlowLayout.setAlignment(flowLayoutAlignment);
        indicatorPanel.invalidate();

        overlayFrame.setBackground(TRANSPARENT);
        overlayFrame.pack();
    }

    public void updateOverlayAxisIndicators() {
        for (final var virtualAxis : Input.VirtualAxis.values())
            if (virtualAxisToProgressBarMap.containsKey(virtualAxis)) {
                OutputThread outputThread = null;
                if (localThread != null && localThread.isAlive())
                    outputThread = localThread;
                else if (clientThread != null && clientThread.isAlive())
                    outputThread = clientThread;
                else if (serverThread != null && serverThread.isAlive())
                    outputThread = serverThread;

                if (outputThread != null) {
                    final var progressBar = virtualAxisToProgressBarMap.get(virtualAxis);
                    progressBar.setMinimum(-outputThread.getMaxAxisValue());
                    progressBar.setMaximum(outputThread.getMinAxisValue());

                    final var newValue = -input.getAxes().get(virtualAxis);
                    if (progressBar.getValue() != newValue)
                        progressBar.setValue(newValue);
                }
            }
    }

    private void updateOverlayLocation(final Rectangle maxWindowBounds) {
        if (overlayFrame != null && overlayFrameDragListener != null && !overlayFrameDragListener.isDragging()) {
            overlayFrame.pack();
            final var x = maxWindowBounds.width - overlayFrame.getWidth();
            final var y = maxWindowBounds.height - overlayFrame.getHeight();
            final var defaultLocation = new Point(x, y);
            loadFrameLocation(preferences, overlayFrame, defaultLocation, maxWindowBounds);
            updateOverlayAlignment(maxWindowBounds);
        }
    }

    private void updateOverlayPanel() {
        if (indicatorsListPanel == null)
            return;

        indicatorsListPanel.removeAll();

        for (final var virtualAxis : Input.VirtualAxis.values()) {
            final var indicatorPanel = new JPanel(new GridBagLayout());
            indicatorsListPanel.add(indicatorPanel,
                    new GridBagConstraints(0, GridBagConstraints.RELATIVE, 1, 1, 0.0, 0.0,
                            GridBagConstraints.FIRST_LINE_START, GridBagConstraints.HORIZONTAL,
                            new Insets(0, 0, 0, 0), 0, 5));

            final var virtualAxisLabel = new JLabel(virtualAxis.toString() + rb.getString("AXIS_LABEL_SUFFIX"));
            virtualAxisLabel.setPreferredSize(new Dimension(100, 15));
            indicatorPanel.add(virtualAxisLabel, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0,
                    GridBagConstraints.BASELINE, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));

            final var virtualAxisToOverlayAxisMap = input.getProfile().getVirtualAxisToOverlayAxisMap();
            final var overlayAxis = virtualAxisToOverlayAxisMap.get(virtualAxis);
            final var enabled = overlayAxis != null;

            final var colorLabel = new JLabel();
            if (enabled) {
                colorLabel.setOpaque(true);
                colorLabel.setBackground(overlayAxis.color);
            } else
                colorLabel.setText(rb.getString("INDICATOR_DISABLED_LABEL"));
            colorLabel.setHorizontalAlignment(SwingConstants.CENTER);

            colorLabel.setPreferredSize(new Dimension(100, 15));
            colorLabel.setBorder(BorderFactory.createLineBorder(Color.BLACK));
            indicatorPanel.add(colorLabel, new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.BASELINE,
                    GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));

            final var colorButton = new JButton(new SelectIndicatorColorAction(virtualAxis));
            colorButton.setPreferredSize(BUTTON_DIMENSION);
            colorButton.setEnabled(enabled);
            indicatorPanel.add(colorButton, new GridBagConstraints(2, 0, 1, 1, 1.0, 0.0,
                    GridBagConstraints.BASELINE, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));

            final var invertedCheckBox = new JCheckBox(new InvertIndicatorAction(virtualAxis));
            invertedCheckBox.setSelected(enabled && overlayAxis.inverted);
            invertedCheckBox.setEnabled(enabled);
            indicatorPanel.add(invertedCheckBox, new GridBagConstraints(3, 0, 1, 1, 1.0, 0.0,
                    GridBagConstraints.BASELINE, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));

            final var displayCheckBox = new JCheckBox(new DisplayIndicatorAction(virtualAxis));
            displayCheckBox.setSelected(enabled);
            indicatorPanel.add(displayCheckBox, new GridBagConstraints(4, GridBagConstraints.RELATIVE, 1, 1, 0.0,
                    0.0, GridBagConstraints.BASELINE, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
        }

        indicatorsListPanel.add(Box.createGlue(), new GridBagConstraints(0, GridBagConstraints.RELATIVE, 1, 1, 1.0,
                1.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));

        indicatorsScrollPane.setViewportView(indicatorsListPanel);
    }

    private void updatePanelAccess() {
        final var localActive = localThread != null && localThread.isAlive();
        final var serverActive = serverThread != null && serverThread.isAlive();

        final var panelsEnabled = !localActive && !serverActive;
        setEnabledRecursive(modesListPanel, panelsEnabled);
        setEnabledRecursive(addModePanel, panelsEnabled);
        if (assignmentsComponent != null)
            assignmentsComponent.setEnabled(panelsEnabled);
        setEnabledRecursive(indicatorsListPanel, panelsEnabled);
        setEnabledRecursive(settingsPanel, panelsEnabled);
    }

    public void updateTitleAndTooltip() {
        final var sb = new StringBuilder();

        if (!isSelectedJidValid())
            sb.append(rb.getString("APPLICATION_NAME"));
        else if (loadedProfile == null)
            sb.append(rb.getString("MAIN_FRAME_TITLE_UNSAVED_PROFILE"));
        else {
            if (unsavedChanges)
                sb.append(rb.getString("MAIN_FRAME_TITLE_PREFIX"));

            sb.append(loadedProfile);
            sb.append(rb.getString("MAIN_FRAME_TITLE_SUFFIX"));
        }

        frame.setTitle(sb.toString());

        if (trayIcon != null && input != null) {
            if (input.isDualShock4Controller())
                sb.append(rb.getString("BATTERY_TOOLTIP_PREFIX") + input.getBatteryState()
                        + (input.isCharging() ? rb.getString("BATTERY_TOOLTIP_CHARGING_SUFFIX")
                                : rb.getString("BATTERY_TOOLTIP_DISCHARGING_SUFFIX")));

            trayIcon.setToolTip(sb.toString());
        }
    }

    private void waitForOverlayDeInit() {
        while (overlayFrame != null || openVrOverlay != null)
            try {
                Thread.sleep(100L);
            } catch (final InterruptedException e) {
                Thread.currentThread().interrupt();
            }
    }

}