utybo.branchingstorytree.swing.OpenBSTGUI.java Source code

Java tutorial

Introduction

Here is the source code for utybo.branchingstorytree.swing.OpenBSTGUI.java

Source

/**
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * This Source Code Form is "Incompatible With Secondary Licenses", as
 * defined by the Mozilla Public License, v. 2.0.
 */
package utybo.branchingstorytree.swing;

import static utybo.branchingstorytree.swing.OpenBST.LOG;
import static utybo.branchingstorytree.swing.VisualsUtils.invokeSwingAndWait;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dialog.ModalityType;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.Vector;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
import java.util.function.Supplier;

import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.ButtonGroup;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
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.JRadioButtonMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.LookAndFeel;
import javax.swing.ProgressMonitorInputStream;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.WindowConstants;
import javax.swing.plaf.metal.MetalLookAndFeel;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.pushingpixels.substance.api.SubstanceCortex;
import org.pushingpixels.substance.api.SubstanceLookAndFeel;
import org.pushingpixels.substance.api.SubstanceSkin;
import org.pushingpixels.substance.api.SubstanceSlices.DecorationAreaType;
import org.pushingpixels.substance.api.painter.overlay.SubstanceOverlayPainter;
import org.pushingpixels.substance.api.skin.BusinessSkin;
import org.pushingpixels.substance.api.skin.SubstanceAutumnLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceBusinessBlackSteelLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceBusinessBlueSteelLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceCeruleanLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceCremeCoffeeLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceCremeLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceDustCoffeeLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceDustLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceGeminiLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceGraphiteAquaLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceGraphiteChalkLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceGraphiteGlassLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceGraphiteGoldLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceGraphiteLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceMagellanLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceMarinerLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceModerateLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceNebulaBrickWallLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceNebulaLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceOfficeBlack2007LookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceOfficeBlue2007LookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceOfficeSilver2007LookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceRavenLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceSaharaLookAndFeel;
import org.pushingpixels.substance.api.skin.SubstanceTwilightLookAndFeel;
import org.pushingpixels.substance.swingx.SubstanceSwingxPlugin;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import net.miginfocom.swing.MigLayout;
import utybo.branchingstorytree.api.BSTException;
import utybo.branchingstorytree.api.BranchingStoryTreeParser;
import utybo.branchingstorytree.api.script.Dictionary;
import utybo.branchingstorytree.api.story.BranchingStory;
import utybo.branchingstorytree.swing.editor.StoryEditor;
import utybo.branchingstorytree.swing.impl.BRMFileClient;
import utybo.branchingstorytree.swing.impl.IMGClient;
import utybo.branchingstorytree.swing.impl.TabClient;
import utybo.branchingstorytree.swing.utils.BSTPackager;
import utybo.branchingstorytree.swing.utils.Lang;
import utybo.branchingstorytree.swing.utils.Lang.UnrespectedModelException;
import utybo.branchingstorytree.swing.visuals.AboutDialog;
import utybo.branchingstorytree.swing.visuals.JBackgroundPanel;
import utybo.branchingstorytree.swing.visuals.JBannerPanel;
import utybo.branchingstorytree.swing.visuals.PackageDialog;
import utybo.branchingstorytree.swing.visuals.StoryPanel;

@SuppressWarnings("serial")
public class OpenBSTGUI extends JFrame {
    private static OpenBSTGUI instance;
    private final BranchingStoryTreeParser parser = new BranchingStoryTreeParser();
    private static final Color DISCORD_COLOR = new Color(114, 137, 218);
    public static final Color OPENBST_BLUE = new Color(33, 150, 243);

    public static final SubstanceLookAndFeel DARK_THEME = new SubstanceGraphiteGoldLookAndFeel();
    public static final SubstanceLookAndFeel LIGHT_THEME;
    public static final LookAndFeel DEBUG_THEME = new MetalLookAndFeel();

    public static final Map<String, LookAndFeel> ADDITIONAL_LIGHT_THEMES, ADDITIONAL_DARK_THEMES;
    static {
        SubstanceSkin skin = new BusinessSkin();
        for (SubstanceOverlayPainter op : new ArrayList<>(skin.getOverlayPainters(DecorationAreaType.TOOLBAR))) {
            skin.removeOverlayPainter(op, DecorationAreaType.TOOLBAR);
        }
        LIGHT_THEME = new SubstanceLookAndFeel(skin) {
            private static final long serialVersionUID = 1L;
        };

        TreeMap<String, LookAndFeel> mapl = new TreeMap<>();
        TreeMap<String, LookAndFeel> mapd = new TreeMap<>();
        mapl.put("Autumn", new SubstanceAutumnLookAndFeel());
        mapl.put("Business Black Steel", new SubstanceBusinessBlackSteelLookAndFeel());
        mapl.put("Business Blue Steel", new SubstanceBusinessBlueSteelLookAndFeel());
        mapl.put("Cerulean", new SubstanceCeruleanLookAndFeel());
        mapl.put("Creme Coffee", new SubstanceCremeCoffeeLookAndFeel());
        mapl.put("Creme", new SubstanceCremeLookAndFeel());
        mapl.put("Dust Coffee", new SubstanceDustCoffeeLookAndFeel());
        mapl.put("Dust", new SubstanceDustLookAndFeel());
        mapl.put("Gemini", new SubstanceGeminiLookAndFeel());
        mapd.put("Graphite Aqua", new SubstanceGraphiteAquaLookAndFeel());
        mapd.put("Graphite Chalk", new SubstanceGraphiteChalkLookAndFeel());
        mapd.put("Graphite Glass", new SubstanceGraphiteGlassLookAndFeel());
        mapd.put("Graphite", new SubstanceGraphiteLookAndFeel());
        mapd.put("Magellan", new SubstanceMagellanLookAndFeel());
        mapl.put("Mariner", new SubstanceMarinerLookAndFeel());
        mapl.put("Moderate", new SubstanceModerateLookAndFeel());
        mapl.put("Nebula Brick Wall", new SubstanceNebulaBrickWallLookAndFeel());
        mapl.put("Nebula", new SubstanceNebulaLookAndFeel());
        mapl.put("Office 2007 (Black)", new SubstanceOfficeBlack2007LookAndFeel());
        mapl.put("Office 2007 (Blue)", new SubstanceOfficeBlue2007LookAndFeel());
        mapl.put("Office 2007 (Silver)", new SubstanceOfficeSilver2007LookAndFeel());
        mapd.put("Raven", new SubstanceRavenLookAndFeel());
        mapl.put("Sahara", new SubstanceSaharaLookAndFeel());
        mapd.put("Twilight", new SubstanceTwilightLookAndFeel());
        ADDITIONAL_LIGHT_THEMES = Collections.unmodifiableMap(mapl);
        ADDITIONAL_DARK_THEMES = Collections.unmodifiableMap(mapd);
    }

    /**
     * Container for all the tabs
     */
    private final JTabbedPane container;

    private final JBackgroundPanel background;

    private final JPanel bannersPanel;

    private int selectedTheme = 1;
    private boolean dark = false;
    private final LinkedList<Consumer<Boolean>> darkModeCallbacks = new LinkedList<>();

    public static void launch() {
        VisualsUtils.invokeSwingAndWait(() -> {
            instance = new OpenBSTGUI();
            instance.setVisible(true);
        });
    }

    public static OpenBSTGUI getInstance() {
        return instance;
    }

    protected static void initializeLaF() {
        invokeSwingAndWait(() -> {
            try {
                UIManager.setLookAndFeel(LIGHT_THEME);
                SubstanceCortex.GlobalScope.setColorizationFactor(1.0D);
                SubstanceCortex.GlobalScope.registerComponentPlugin(new SubstanceSwingxPlugin());

                if (System.getProperty("os.name").toLowerCase().equals("linux")) {
                    // Try to apply GNOME Shell fix
                    try {
                        final Toolkit xToolkit = Toolkit.getDefaultToolkit();
                        java.lang.reflect.Field awtAppClassNameField = xToolkit.getClass()
                                .getDeclaredField("awtAppClassName");
                        awtAppClassNameField.setAccessible(true);
                        awtAppClassNameField.set(xToolkit, Lang.get("title"));
                        awtAppClassNameField.setAccessible(false);
                    } catch (final Exception e) {
                        LOG.warn("Could not apply X fix", e);
                    }
                }

            } catch (final Exception e) {
                LOG.warn("Could not apply Substance LaF, falling back to system LaF", e);
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (final Exception e1) {
                    LOG.warn("Failed to load System LaF as well, falling back to keeping the default LaF", e1);
                }
            }
        });
    }

    ///////// FRAME CREATION AND COMPONENTS

    public OpenBSTGUI() {
        instance = this;
        UIManager.put("OptionPane.errorIcon", new ImageIcon(Icons.getImage("Cancel", 48)));
        UIManager.put("OptionPane.informationIcon", new ImageIcon(Icons.getImage("About", 48)));
        UIManager.put("OptionPane.questionIcon", new ImageIcon(Icons.getImage("Rename", 48)));
        UIManager.put("OptionPane.warningIcon", new ImageIcon(Icons.getImage("Error", 48)));

        BorderLayout borderLayout = new BorderLayout();
        borderLayout.setVgap(4);
        getContentPane().setLayout(borderLayout);
        setIconImage(Icons.getImage("Logo", 48));
        setTitle("OpenBST " + OpenBST.VERSION);
        setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        addWindowListener(new WindowAdapter() {

            @Override
            public void windowClosing(WindowEvent e) {
                boolean cancelled = false;
                int i = 0;
                for (Component c : container.getComponents()) {
                    if (c instanceof StoryPanel) {
                        i++;
                    } else if (c instanceof StoryEditor) {
                        container.setSelectedComponent(c);
                        if (((StoryEditor) c).askClose()) {
                            continue;
                        } else {
                            cancelled = true;
                            break;
                        }
                    }
                }
                if (!cancelled) {
                    if (i > 0) {
                        int j = Messagers.showConfirm(OpenBSTGUI.this,
                                "You are about to close " + i + " file(s). Are you sure you wish to exit OpenBST?",
                                Messagers.OPTIONS_YES_NO, Messagers.TYPE_WARNING, "Closing OpenBST");
                        if (j != Messagers.OPTION_YES)
                            cancelled = true;
                    }
                    if (!cancelled)
                        System.exit(0);
                }
            }

        });

        JMenuBar jmb = new JMenuBar();
        jmb.setBackground(OPENBST_BLUE);
        jmb.add(Box.createHorizontalGlue());
        jmb.add(createShortMenu());
        jmb.add(Box.createHorizontalGlue());
        this.setJMenuBar(jmb);

        addDarkModeCallback(b -> {
            jmb.setBackground(b ? OPENBST_BLUE.darker().darker() : OPENBST_BLUE);
        });

        container = new JTabbedPane();
        container.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
        container.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(final MouseEvent e) {
                if (SwingUtilities.isMiddleMouseButton(e)) {
                    final int i = container.indexAtLocation(e.getX(), e.getY());
                    System.out.println(i);
                    if (i > -1) {
                        Component c = container.getComponentAt(i);
                        if (c instanceof StoryPanel) {
                            container.setSelectedComponent(c);
                            ((StoryPanel) c).askClose();
                        } else if (c instanceof StoryEditor) {
                            container.setSelectedComponent(c);
                            ((StoryEditor) c).askClose();
                        }
                    }
                }
            }
        });
        getContentPane().add(container, BorderLayout.CENTER);

        final JBackgroundPanel welcomeContentPanel = new JBackgroundPanel(Icons.getRandomBackground(),
                Image.SCALE_FAST);
        background = welcomeContentPanel;

        welcomeContentPanel.setLayout(new MigLayout("hidemode 2", "[grow,center]", "[][grow][]"));
        container.add(welcomeContentPanel);
        container.setTitleAt(0, Lang.get("welcome"));

        bannersPanel = new JPanel(new MigLayout("hidemode 2, gap 0px, fill, wrap 1, ins 0"));
        bannersPanel.setBackground(new Color(0, 0, 0, 0));
        welcomeContentPanel.add(bannersPanel, "cell 0 0,grow");

        if (OpenBST.VERSION.endsWith("u")) {
            JButton btnReportBugs = new JButton(Lang.get("welcome.reportbugs"));
            btnReportBugs.addActionListener(e -> {
                VisualsUtils.browse("https://github.com/utybo/BST/issues");
            });
            bannersPanel.add(new JBannerPanel(new ImageIcon(Icons.getImage("Experiment", 32)), Color.YELLOW,
                    Lang.get("welcome.ontheedge"), btnReportBugs, false), "grow");
        } else if (OpenBST.VERSION.contains("SNAPSHOT")) {
            bannersPanel.add(new JBannerPanel(new ImageIcon(Icons.getImage("Experiment", 32)), Color.ORANGE,
                    Lang.get("welcome.snapshot"), null, false), "grow");
        }

        if (System.getProperty("java.specification.version").equals("9")) {
            bannersPanel.add(new JBannerPanel(new ImageIcon(Icons.getImage("Attention", 32)),
                    new Color(255, 50, 50), Lang.get("welcome.java9warning"), null, false), "grow");
        }
        if (System.getProperty("java.specification.version").equals("10")) {
            bannersPanel.add(new JBannerPanel(new ImageIcon(Icons.getImage("Attention", 32)),
                    new Color(255, 50, 50), Lang.get("welcome.java10warning"), null, false), "grow");
        }

        JButton btnJoinDiscord = new JButton(Lang.get("openbst.discordjoin"));
        btnJoinDiscord.addActionListener(e -> {
            VisualsUtils.browse("https://discord.gg/6SVDCMM");
        });
        bannersPanel.add(new JBannerPanel(new ImageIcon(Icons.getImage("Discord", 48)), DISCORD_COLOR,
                Lang.get("openbst.discord"), btnJoinDiscord, true), "grow");

        JPanel panel = new JPanel();
        panel.setBackground(new Color(0, 0, 0, 0));
        welcomeContentPanel.add(panel, "flowx,cell 0 1,growx,aligny center");
        panel.setLayout(new MigLayout("", "[40%][][][][60%,growprio 50]", "[][grow]"));

        final JLabel lblOpenbst = new JLabel(new ImageIcon(Icons.getImage("FullLogo", 48)));
        addDarkModeCallback(b -> lblOpenbst
                .setIcon(new ImageIcon(b ? Icons.getImage("FullLogoWhite", 48) : Icons.getImage("FullLogo", 48))));
        panel.add(lblOpenbst, "flowx,cell 0 0 1 2,alignx trailing,aligny center");

        JSeparator separator = new JSeparator();
        separator.setOrientation(SwingConstants.VERTICAL);
        panel.add(separator, "cell 2 0 1 2,growy");

        final JLabel lblWelcomeToOpenbst = new JLabel("<html>" + Lang.get("welcome.intro"));
        lblWelcomeToOpenbst.setMaximumSize(new Dimension(350, 999999));
        panel.add(lblWelcomeToOpenbst, "cell 4 0");

        Component horizontalStrut = Box.createHorizontalStrut(10);
        panel.add(horizontalStrut, "cell 1 1");

        Component horizontalStrut_1 = Box.createHorizontalStrut(10);
        panel.add(horizontalStrut_1, "cell 3 1");

        final JButton btnOpenAFile = new JButton(Lang.get("welcome.open"));
        panel.add(btnOpenAFile, "flowx,cell 4 1");
        btnOpenAFile.setIcon(new ImageIcon(Icons.getImage("Open", 40)));
        btnOpenAFile.addActionListener(e -> {
            openStory(VisualsUtils.askForFile(this, Lang.get("file.title")));
        });

        final JButton btnOpenEditor = new JButton(Lang.get("welcome.openeditor"));
        panel.add(btnOpenEditor, "cell 4 1");
        btnOpenEditor.setIcon(new ImageIcon(Icons.getImage("Edit Property", 40)));
        btnOpenEditor.addActionListener(e -> {
            openEditor(VisualsUtils.askForFile(this, Lang.get("file.title")));
        });

        JButton btnChangeBackground = new JButton(Lang.get("welcome.changebackground"),
                new ImageIcon(Icons.getImage("Change Theme", 16)));
        btnChangeBackground.addActionListener(e -> {
            BufferedImage prev = background.getImage();
            BufferedImage next;
            do {
                next = Icons.getRandomBackground();
            } while (prev == next);
            background.setImage(next);
        });
        welcomeContentPanel.add(btnChangeBackground, "flowx,cell 0 2,alignx left");

        JButton btnWelcomepixabay = new JButton(Lang.get("welcome.pixabay"),
                new ImageIcon(Icons.getImage("External Link", 16)));
        btnWelcomepixabay.addActionListener(e -> {
            VisualsUtils.browse("https://pixabay.com");

        });
        welcomeContentPanel.add(btnWelcomepixabay, "cell 0 2");

        JLabel creds = new JLabel(Lang.get("welcome.credits"));
        creds.setEnabled(false);
        welcomeContentPanel.add(creds, "cell 0 2, gapbefore 10px");

        setSize((int) (830 * Icons.getScale()), (int) (480 * Icons.getScale()));
        setLocationRelativeTo(null);
    }

    private JMenu createShortMenu() {
        JMenu shortMenu = new JMenu();
        addDarkModeCallback(b -> {
            shortMenu.setBackground(b ? OPENBST_BLUE.darker().darker() : OPENBST_BLUE.brighter());
            shortMenu.setForeground(b ? Color.WHITE : OPENBST_BLUE);
        });
        shortMenu.setBackground(OPENBST_BLUE.brighter());
        shortMenu.setForeground(OPENBST_BLUE);
        shortMenu.setText(Lang.get("banner.title"));
        shortMenu.setIcon(new ImageIcon(Icons.getImage("Logo", 16)));
        JMenuItem label = new JMenuItem(Lang.get("menu.title"));
        label.setEnabled(false);
        shortMenu.add(label);
        shortMenu.addSeparator();
        shortMenu.add(
                new JMenuItem(new AbstractAction(Lang.get("menu.open"), new ImageIcon(Icons.getImage("Open", 16))) {
                    private static final long serialVersionUID = 1L;

                    @Override
                    public void actionPerformed(ActionEvent e) {
                        openStory(VisualsUtils.askForFile(OpenBSTGUI.this, Lang.get("file.title")));
                    }
                }));

        shortMenu.addSeparator();

        shortMenu.add(new JMenuItem(
                new AbstractAction(Lang.get("menu.create"), new ImageIcon(Icons.getImage("Add Property", 16))) {
                    private static final long serialVersionUID = 1L;

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

        JMenu additionalMenu = new JMenu(Lang.get("menu.advanced"));
        shortMenu.add(additionalMenu);

        additionalMenu.add(new JMenuItem(
                new AbstractAction(Lang.get("menu.package"), new ImageIcon(Icons.getImage("Open Archive", 16))) {
                    private static final long serialVersionUID = 1L;

                    @Override
                    public void actionPerformed(ActionEvent e) {
                        new PackageDialog(instance).setVisible(true);
                    }
                }));
        additionalMenu.add(new JMenuItem(
                new AbstractAction(Lang.get("langcheck"), new ImageIcon(Icons.getImage("LangCheck", 16))) {
                    private static final long serialVersionUID = 1L;

                    @Override
                    public void actionPerformed(ActionEvent e) {
                        final Map<String, String> languages = new Gson()
                                .fromJson(new InputStreamReader(
                                        OpenBST.class.getResourceAsStream(
                                                "/utybo/branchingstorytree/swing/lang/langs.json"),
                                        StandardCharsets.UTF_8), new TypeToken<Map<String, String>>() {
                                        }.getType());
                        languages.remove("en");
                        languages.remove("default");
                        JComboBox<String> jcb = new JComboBox<>(new Vector<>(languages.keySet()));
                        JPanel panel = new JPanel();
                        panel.add(new JLabel(Lang.get("langcheck.choose")));
                        panel.add(jcb);
                        int result = JOptionPane.showOptionDialog(OpenBSTGUI.this, panel, Lang.get("langcheck"),
                                JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null);
                        if (result == JOptionPane.OK_OPTION) {
                            Locale selected = new Locale((String) jcb.getSelectedItem());
                            if (!Lang.getMap().keySet().contains(selected)) {
                                try {
                                    Lang.loadTranslationsFromFile(selected,
                                            OpenBST.class
                                                    .getResourceAsStream("/utybo/branchingstorytree/swing/lang/"
                                                            + languages.get(jcb.getSelectedItem().toString())));
                                } catch (UnrespectedModelException | IOException e1) {
                                    LOG.warn("Failed to load translation file", e1);
                                }
                            }
                            ArrayList<String> list = new ArrayList<>();
                            Lang.getLocaleMap(Locale.ENGLISH).forEach((k, v) -> {
                                if (!Lang.getLocaleMap(selected).containsKey(k)) {
                                    list.add(k + "\n");
                                }
                            });
                            StringBuilder sb = new StringBuilder();
                            Collections.sort(list);
                            list.forEach(s -> sb.append(s));
                            JDialog dialog = new JDialog(OpenBSTGUI.this, Lang.get("langcheck"));
                            dialog.getContentPane().setLayout(new MigLayout());
                            dialog.getContentPane().add(new JLabel(Lang.get("langcheck.result")),
                                    "pushx, growx, wrap");
                            JTextArea area = new JTextArea();
                            area.setLineWrap(true);
                            area.setWrapStyleWord(true);
                            area.setText(sb.toString());
                            area.setEditable(false);
                            area.setBorder(BorderFactory.createLoweredBevelBorder());
                            JScrollPane jsp = new JScrollPane(area);
                            jsp.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
                            dialog.getContentPane().add(jsp, "pushx, pushy, growx, growy");
                            dialog.setSize((int) (Icons.getScale() * 300), (int) (Icons.getScale() * 300));
                            dialog.setLocationRelativeTo(OpenBSTGUI.this);
                            dialog.setModalityType(ModalityType.APPLICATION_MODAL);
                            dialog.setVisible(true);
                        }
                    }
                }));

        additionalMenu.add(new JMenuItem(
                new AbstractAction(Lang.get("menu.debug"), new ImageIcon(Icons.getImage("Code", 16))) {
                    private static final long serialVersionUID = 1L;

                    @Override
                    public void actionPerformed(ActionEvent e) {
                        DebugInfo.launch(OpenBSTGUI.this);
                    }
                }));

        JMenu includedFiles = new JMenu("Included BST files");

        for (Entry<String, String> entry : OpenBST.getInternalFiles().entrySet()) {
            JMenuItem jmi = new JMenuItem(entry.getKey());
            jmi.addActionListener(ev -> {
                String path = "/bst/" + entry.getValue();
                InputStream is = OpenBSTGUI.class.getResourceAsStream(path);
                ProgressMonitorInputStream pmis = new ProgressMonitorInputStream(OpenBSTGUI.this, "Extracting...",
                        is);
                new Thread(() -> {
                    try {
                        File f = File.createTempFile("openbstinternal", ".bsp");
                        FileOutputStream fos = new FileOutputStream(f);
                        IOUtils.copy(pmis, fos);
                        openStory(f);
                    } catch (final IOException e) {
                        LOG.error("IOException caught", e);
                        showException(Lang.get("file.error").replace("$e", e.getClass().getSimpleName())
                                .replace("$m", e.getMessage()), e);
                    }

                }).start();

            });
            includedFiles.add(jmi);
        }
        additionalMenu.add(includedFiles);

        shortMenu.addSeparator();

        JMenu themesMenu = new JMenu(Lang.get("menu.themes"));
        shortMenu.add(themesMenu);
        themesMenu.setIcon(new ImageIcon(Icons.getImage("Color Wheel", 16)));
        ButtonGroup themesGroup = new ButtonGroup();
        JRadioButtonMenuItem jrbmi;

        jrbmi = new JRadioButtonMenuItem(Lang.get("menu.themes.dark"));
        if (0 == selectedTheme) {
            jrbmi.setSelected(true);
        }
        jrbmi.addActionListener(e -> switchLaF(0, DARK_THEME));
        themesMenu.add(jrbmi);
        themesGroup.add(jrbmi);

        jrbmi = new JRadioButtonMenuItem(Lang.get("menu.themes.light"));
        if (1 == selectedTheme) {
            jrbmi.setSelected(true);
        }
        jrbmi.addActionListener(e -> switchLaF(1, LIGHT_THEME));
        themesMenu.add(jrbmi);
        themesGroup.add(jrbmi);

        jrbmi = new JRadioButtonMenuItem(Lang.get("menu.themes.debug"));
        if (2 == selectedTheme) {
            jrbmi.setSelected(true);
        }
        jrbmi.addActionListener(e -> switchLaF(2, DEBUG_THEME));
        themesMenu.add(jrbmi);
        themesGroup.add(jrbmi);

        JMenu additionalLightThemesMenu = new JMenu(Lang.get("menu.themes.morelight"));
        int j = 3;
        for (Map.Entry<String, LookAndFeel> entry : ADDITIONAL_LIGHT_THEMES.entrySet()) {
            int jf = j;
            jrbmi = new JRadioButtonMenuItem(entry.getKey());
            if (j == selectedTheme)
                jrbmi.setSelected(true);
            jrbmi.addActionListener(e -> switchLaF(jf, entry.getValue()));
            additionalLightThemesMenu.add(jrbmi);
            themesGroup.add(jrbmi);
            j++;
        }
        themesMenu.add(additionalLightThemesMenu);

        JMenu additionalDarkThemesMenu = new JMenu(Lang.get("menu.themes.moredark"));
        for (Map.Entry<String, LookAndFeel> entry : ADDITIONAL_DARK_THEMES.entrySet()) {
            int jf = j;
            jrbmi = new JRadioButtonMenuItem(entry.getKey());
            if (j == selectedTheme)
                jrbmi.setSelected(true);
            jrbmi.addActionListener(e -> switchLaF(jf, entry.getValue()));
            additionalDarkThemesMenu.add(jrbmi);
            themesGroup.add(jrbmi);
            j++;
        }
        themesMenu.add(additionalDarkThemesMenu);

        shortMenu.add(new JMenuItem(
                new AbstractAction(Lang.get("menu.about"), new ImageIcon(Icons.getImage("About", 16))) {
                    /**
                     *
                     */
                    private static final long serialVersionUID = 1L;

                    @Override
                    public void actionPerformed(ActionEvent e) {
                        new AboutDialog(instance).setVisible(true);
                    }
                }));

        return shortMenu;
    }

    ///////// FRAME MODIFICATION
    public void removeTab(final JPanel panel) {
        container.remove(panel);
    }

    /**
     * Add a story by creating a tab and initializing its panel. Also triggers
     * post-creation events (such as NSFW warnings)
     *
     * @param story
     *            The story to create a tab for
     * @param file
     *            The file the story was loaded from
     * @param client
     *            The client to use
     * @return
     */
    private StoryPanel addStory(final BranchingStory story, final File file, final TabClient client) {
        LOG.trace("Creating tab");
        final StoryPanel sp = new StoryPanel(story, this, file, client);
        container.addTab(sp.getTitle(), null, sp, null);
        container.setSelectedIndex(container.getTabCount() - 1);
        if (!sp.postCreation()) {
            container.removeTabAt(container.getTabCount() - 1);
            return null;
        } else {
            return sp;
        }
    }

    ///////// ACTIONS ON CLICKS
    private void doNewEditor() {
        try {
            StoryEditor se = new StoryEditor(new BranchingStory());
            container.addTab("Editor", se);
            container.setSelectedComponent(se);
        } catch (Exception e) {
            LOG.error("Error on story editor init", e);
            Messagers.showException(this, "Error while creating the Story Editor", e);
        }
    }

    ///////// OPENING STORIES
    public void openStory(File f) {
        if (f != null) {
            final TabClient client = new TabClient(instance);
            loadFile(f, client, new Consumer<BranchingStory>() {
                private StoryPanel sp;

                @Override
                public void accept(BranchingStory bs) {
                    if (bs != null) {
                        try {
                            SwingUtilities.invokeAndWait(() -> sp = addStory(bs, f, client));
                            if (sp != null) {
                                try {
                                    client.getBRMHandler().load();
                                    if (Boolean.parseBoolean(bs.getTagOrDefault("img_requireinternal", "false")))
                                        IMGClient.initInternal();
                                } catch (BSTException e) {
                                    LOG.error("Exception caught while loading resources", e);
                                    showException(Lang.get("file.resourceerror").replace("$e", whichCause(e))
                                            .replace("$m", whichMessage(e)), e);
                                }
                                SwingUtilities.invokeAndWait(() -> sp.setupStory());
                            }
                        } catch (InvocationTargetException | InterruptedException e) {
                            LOG.warn("Swing invocation exception", e);
                        }

                    }
                }

                private String whichMessage(BSTException e) {
                    if (e.getCause() != null) {
                        return e.getCause().getMessage();
                    } else {
                        return e.getMessage();
                    }
                }

                private String whichCause(BSTException e) {
                    if (e.getCause() != null) {
                        return e.getCause().getClass().getSimpleName();
                    } else {
                        return e.getClass().getSimpleName();
                    }
                }
            });
        }
    }

    private void openEditor(File f) {
        if (f != null) {
            final TabClient client = new TabClient(instance);
            loadFile(f, client, new Consumer<BranchingStory>() {
                @Override
                public void accept(BranchingStory bs) {
                    try {
                        SwingUtilities.invokeAndWait(() -> {
                            try {
                                StoryEditor se = new StoryEditor(bs);
                                container.addTab(se.getTitle(), se);
                                container.setSelectedComponent(se);
                            } catch (Exception e) {
                                LOG.error("Error on story editor init", e);
                                Messagers.showException(OpenBSTGUI.this, "Error while creating the Story Editor ("
                                        + e.getClass().getSimpleName() + " : " + e.getMessage() + ")", e);
                            }
                        });
                    } catch (Exception e) {
                        LOG.error(e);
                    }
                }
            });
        }
    }

    /**
     * Load and parse a file, using appropriate dialogs if an error occurs to
     * inform the user and even give him the option to reload the file
     *
     * @param file
     *            The file to load
     * @param client
     *            The BST Client. This is required for parsing the file
     * @return
     */
    public void loadFile(final File file, final TabClient client, Consumer<BranchingStory> callback) {
        SwingWorker<BranchingStory, Object> worker = new SwingWorker<BranchingStory, Object>() {
            @Override
            protected BranchingStory doInBackground() throws Exception {
                try {
                    LOG.trace("Parsing story");
                    String ext = FilenameUtils.getExtension(file.getName());
                    BranchingStory bs = null;
                    if (ext.equals("bsp")) {
                        bs = BSTPackager.fromPackage(new ProgressMonitorInputStream(instance,
                                "Opening " + file.getName() + "...", new FileInputStream(file)), client);
                    } else {
                        bs = parser
                                .parse(new BufferedReader(new InputStreamReader(
                                        new ProgressMonitorInputStream(instance,
                                                "Opening " + file.getName() + "...", new FileInputStream(file)),
                                        StandardCharsets.UTF_8)), new Dictionary(), client, "<main>");
                        client.setBRMHandler(new BRMFileClient(file, client, bs));
                    }
                    callback.accept(bs);
                    return bs;
                } catch (final IOException e) {
                    LOG.error("IOException caught", e);
                    showException(Lang.get("file.error").replace("$e", e.getClass().getSimpleName()).replace("$m",
                            e.getMessage()), e);
                    return null;
                } catch (final BSTException e) {
                    LOG.error("BSTException caught", e);
                    String s = "<html>" + Lang.get("file.bsterror.1");
                    s += Lang.get("file.bsterror.2");
                    s += Lang.get("file.bsterror.3").replace("$l", "" + e.getWhere()).replace("$f", "[main]");
                    if (e.getCause() != null) {
                        s += Lang.get("file.bsterror.4").replace("$e", e.getCause().getClass().getSimpleName())
                                .replace("$m", e.getCause().getMessage());
                    }
                    s += Lang.get("file.bsterror.5").replace("$m", "" + e.getMessage());
                    s += Lang.get("file.bsterror.6");
                    String s2 = s;
                    if (doAndReturn(() -> Messagers.showConfirm(instance, s2, Messagers.OPTIONS_YES_NO,
                            Messagers.TYPE_ERROR, Lang.get("bsterror"))) == Messagers.OPTION_YES) {
                        LOG.debug("Reloading");
                        return doInBackground();
                    }
                    return null;
                } catch (final Exception e) {
                    LOG.error("Random exception caught", e);
                    showException(Lang.get("file.crash"), e);
                    return null;
                }

            }

            private <T> T doAndReturn(Supplier<T> supplier) {
                ArrayList<T> l = new ArrayList<>();
                invokeSwingAndWait(() -> {
                    l.add(supplier.get());
                });
                return l.size() == 0 ? null : l.get(0);
            }

            @Override
            protected void done() {
                try {
                    get();
                } catch (InterruptedException e) {
                    // Shouldn't happen
                } catch (ExecutionException e) {
                    LOG.error("Random exception caught", e);
                    Messagers.showException(instance, Lang.get("file.crash"), e);
                }
            }
        };
        worker.execute();
    }

    ////// UTILITIES

    private void switchLaF(int id, LookAndFeel laf) {
        try {
            dark = id == 0 || ADDITIONAL_DARK_THEMES.containsValue(laf);
            UIManager.setLookAndFeel(laf);
            SwingUtilities.updateComponentTreeUI(instance);
            background.setDark(dark);
            darkModeCallbacks.forEach(a -> a.accept(dark));
            selectedTheme = id;
        } catch (UnsupportedLookAndFeelException e) {
            LOG.warn("Unsupported LaF", e);
        }
    }

    protected void showMessage(String msg, int type) {
        invokeSwingAndWait(() -> Messagers.showMessage(this, msg, type));
    }

    protected void showException(String msg, Exception e) {
        invokeSwingAndWait(() -> Messagers.showException(this, msg, e));
    }

    public void addDarkModeCallback(Consumer<Boolean> callback) {
        darkModeCallbacks.add(callback);
    }

    public void removeDarkModeCallbback(Consumer<Boolean> callback) {
        darkModeCallbacks.remove(callback);
    }

    public Boolean isDark() {
        return dark;
    }

    public void setTabName(JPanel panel, String string) {
        if (container.indexOfComponent(panel) != -1)
            container.setTitleAt(container.indexOfComponent(panel), string);
    }

    public void addBanner(JBannerPanel banner) {
        bannersPanel.add(banner, "grow");
        if (bannersPanel.getComponents().length > 2) {
            Component[] toScan = Arrays.copyOf(bannersPanel.getComponents(), bannersPanel.getComponents().length);
            for (Component c : toScan) {
                if (c instanceof JBannerPanel) {
                    if (((JBannerPanel) c).isHideable())
                        bannersPanel.remove(c);
                    if (bannersPanel.getComponents().length <= 2)
                        break;
                }
            }
        }
        banner.revalidate();
        banner.repaint();

        // So... There's a weird bug where the background will keep some bits of older
        // banners that were present at first paint time.
        // This makes sure that after everything is rendered correctly and ready, the
        // background gets repainted fully
        // (swing is painful sometimes)
        SwingUtilities.invokeLater(() -> {
            background.revalidate();
            background.repaint();
        });
    }
}