iterator.Explorer.java Source code

Java tutorial

Introduction

Here is the source code for iterator.Explorer.java

Source

/*
 * Copyright 2012-2013 by Andrew Kennedy.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package iterator;

import static iterator.util.Config.*;
import iterator.model.IFS;
import iterator.util.Config;
import iterator.util.Config.Render;
import iterator.util.Platform;
import iterator.util.Subscriber;
import iterator.util.Version;
import iterator.view.Details;
import iterator.view.Editor;
import iterator.view.Viewer;

import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.List;
import java.util.Random;

import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.ButtonGroup;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

import com.google.common.base.CaseFormat;
import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.google.common.io.Resources;

/**
 * IFS Explorer main class.
 *
 * @author andrew.international@gmail.com
 */
public class Explorer extends JFrame implements KeyListener, UncaughtExceptionHandler, Subscriber {
    /** serialVersionUID */
    private static final long serialVersionUID = -2003170067188344917L;

    public static final String BANNER = "   ___ _____ ____    _____            _                     \n"
            + "  |_ _|  ___/ ___|  | ____|_  ___ __ | | ___  _ __ ___ _ __ \n"
            + "   | || |_  \\___ \\  |  _| \\ \\/ / '_ \\| |/ _ \\| '__/ _ \\ '__|\n"
            + "   | ||  _|  ___) | | |___ >  <| |_) | | (_) | | |  __/ |   \n"
            + "  |___|_|   |____/  |_____/_/\\_\\ .__/|_|\\___/|_|  \\___|_|   \n"
            + "                               |_|                          \n" + "\n"
            + "    Iterated Function System Explorer Version %s\n"
            + "    Copyright 2012-2013 by Andrew Donald Kennedy\n"
            + "    Licensed under the Apache Software License, Version 2.0\n"
            + "    https://grkvlt.github.io/iterator/\n" + "\n";

    private static final Version version = Version.instance();

    public static final String EXPLORER = "IFS Explorer";
    public static final String EDITOR = "Editor";
    public static final String VIEWER = "Viewer";
    public static final String DETAILS = "Details";

    public static final String FULLSCREEN_OPTION = "-f";
    public static final String FULLSCREEN_OPTION_LONG = "--fullscreen";
    public static final String COLOUR_OPTION = "-c";
    public static final String COLOUR_OPTION_LONG = "--colour";
    public static final String PALETTE_OPTION = "-p";
    public static final String PALETTE_OPTION_LONG = "--palette";
    public static final String CONFIG_OPTION_LONG = "--config";

    private Config config;
    private File override;

    private boolean fullScreen = false;

    private boolean colour = false;
    private boolean palette = false;
    private boolean stealing = false;
    private boolean ifscolour = false;

    private Render render = Render.STANDARD;

    private Platform platform = Platform.getPlatform();
    private BufferedImage icon, source;
    private Preferences prefs;
    private About about;

    private IFS ifs;
    private List<Color> colours;
    private int paletteSize;
    private String paletteFile;
    private long seed;
    private Dimension size, min = new Dimension(MIN_WINDOW_SIZE, MIN_WINDOW_SIZE);
    private File cwd;

    private JMenuBar menuBar;
    private Editor editor;
    private Viewer viewer;
    private Details details;
    private JScrollPane scroll;
    private JPanel view;
    private CardLayout cards;
    private String current;
    private JCheckBoxMenuItem showEditor, showViewer, showDetails;
    private JMenuItem export, save, saveAs;

    private EventBus bus;
    private Runnable postponed;

    public Explorer(String... argv) {
        super(EXPLORER);
        Thread.setDefaultUncaughtExceptionHandler(this);

        // Parse arguments
        if (argv.length != 0) {
            for (int i = 0; i < argv.length; i++) {
                if (argv[i].charAt(0) == '-') {
                    // Argument is a program option
                    if (argv[i].equalsIgnoreCase(FULLSCREEN_OPTION)
                            || argv[i].equalsIgnoreCase(FULLSCREEN_OPTION_LONG)) {
                        fullScreen = true;
                    } else if (argv[i].equalsIgnoreCase(COLOUR_OPTION)
                            || argv[i].equalsIgnoreCase(COLOUR_OPTION_LONG)) {
                        colour = true;
                    } else if (argv[i].equalsIgnoreCase(PALETTE_OPTION)
                            || argv[i].equalsIgnoreCase(PALETTE_OPTION_LONG)) {
                        colour = true;
                        palette = true;
                    } else if (argv[i].equalsIgnoreCase(CONFIG_OPTION_LONG)) {
                        if (argv.length >= i + 1) {
                            override = new File(argv[++i]);
                            if (!override.exists()) {
                                throw new IllegalArgumentException(
                                        "Configuration file does not exist: " + override);
                            }
                        }
                    } else {
                        throw new IllegalArgumentException("Cannot parse option: " + argv[i]);
                    }
                } else if (i == argv.length - 1) {
                    // Last argument is a file
                    final File file = new File(argv[i]);
                    if (file.canRead()) {
                        // Add a task to load the file
                        postponed = new Runnable() {
                            public void run() {
                                IFS loaded = load(file);
                                loaded.setSize(size);
                                bus.post(loaded);
                            }
                        };
                    } else {
                        throw new IllegalArgumentException("Cannot load XML data file: " + argv[i]);
                    }
                } else {
                    throw new IllegalArgumentException("Unknown argument: " + argv[i]);
                }
            }
        }

        // Load configuration
        config = Config.loadProperties(override);

        // Check colour mode configuration
        if (config.containsKey(MODE_PROPERTY)) {
            String mode = config.get(MODE_PROPERTY);
            if (MODE_COLOUR.equalsIgnoreCase(mode)) {
                colour = true;
                palette = false;
                stealing = false;
                ifscolour = false;
            } else if (MODE_PALETTE.equalsIgnoreCase(mode)) {
                colour = true;
                palette = true;
                stealing = false;
                ifscolour = false;
            } else if (MODE_STEALING.equalsIgnoreCase(mode)) {
                colour = true;
                palette = true;
                stealing = true;
                ifscolour = false;
            } else if (MODE_IFS_COLOUR.equalsIgnoreCase(mode)) {
                colour = true;
                palette = false;
                stealing = false;
                ifscolour = true;
            } else if (MODE_GRAY.equalsIgnoreCase(mode)) {
                colour = false;
                palette = false;
                stealing = false;
                ifscolour = false;
            } else {
                throw new IllegalArgumentException("Cannot set colour mode: " + mode);
            }
        }

        // Check rendering configuration
        if (config.containsKey(RENDER_PROPERTY)) {
            String value = config.get(RENDER_PROPERTY);
            render = Render.valueOf(value.toUpperCase());
        }

        // Get window size configuration
        int w = Math.max(MIN_WINDOW_SIZE, config.get(WINDOW_PROPERTY + ".width", DEFAULT_WINDOW_SIZE));
        int h = Math.max(MIN_WINDOW_SIZE, config.get(WINDOW_PROPERTY + ".height", DEFAULT_WINDOW_SIZE));
        size = new Dimension(w, h);

        // Load icon resources
        icon = loadImage(Resources.getResource("icon.png"));
        setIconImage(icon);

        // Load colour palette
        if (palette) {
            seed = config.get(PALETTE_PROPERTY + ".seed", DEFAULT_PALETTE_SEED);
            paletteFile = config.get(PALETTE_PROPERTY + ".file", DEFAULT_PALETTE_FILE);
            paletteSize = config.get(PALETTE_PROPERTY + ".size", DEFAULT_PALETTE_SIZE);
            loadColours();
        }
        printf("Configured %s: %s",
                colour ? palette ? stealing ? "stealing" : "palette" : ifscolour ? "ifscolour" : "colour"
                        : "grayscale",
                palette ? paletteFile : colour ? "hsb" : "black");

        // Setup event bus
        bus = new EventBus(EXPLORER);
        bus.register(this);

        // Setup full-screen mode if required
        if (fullScreen) {
            setUndecorated(true);
            setResizable(false);
            Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
            Insets insets = Toolkit.getDefaultToolkit().getScreenInsets(getGraphicsConfiguration());
            setBounds(insets.left, insets.top, screen.width - (insets.left + insets.right),
                    screen.height - (insets.top + insets.bottom));
        }

        // Load dialogs
        prefs = new Preferences(bus, this);
        about = new About(bus, this);

        // Setup platform specifics
        if (platform == Platform.MAC_OS_X) {
            try {
                Class<?> support = Class.forName("iterator.AppleSupport");
                Constructor<?> ctor = support.getConstructor(EventBus.class, Explorer.class);
                Method setup = support.getDeclaredMethod("setup");
                Object apple = ctor.newInstance(bus, this);
                setup.invoke(apple);
            } catch (InvocationTargetException ite) {
                printf("Error while configuring OSX support: %s\n", ite.getCause().getMessage());
                System.exit(1);
            } catch (Exception e) {
                printf("Unable to configure OSX support: %s\n", e.getMessage());
            }
        }
    }

    public static BufferedImage loadImage(URL url) {
        try {
            return ImageIO.read(url);
        } catch (IOException ioe) {
            throw Throwables.propagate(ioe);
        }
    }

    public void loadColours() {
        if (paletteFile.contains(".")) {
            try {
                source = loadImage(URI.create(paletteFile).toURL());
            } catch (MalformedURLException mue) {
                Throwables.propagate(mue);
            }
        } else {
            source = loadImage(Resources.getResource("palette/" + paletteFile + ".png"));
        }
        colours = Lists.newArrayList();
        Random random = new Random(seed);
        while (colours.size() < paletteSize) {
            int x = random.nextInt(source.getWidth());
            int y = random.nextInt(source.getHeight());
            Color c = new Color(source.getRGB(x, y));
            if (!colours.contains(c)) {
                colours.add(c);
            }
        }
    }

    public Color getPixel(double x, double y) {
        int sx = (int) Math.max(0, Math.min(source.getWidth() - 1, (x / getWidth()) * source.getWidth()));
        int sy = (int) Math.max(0, Math.min(source.getHeight() - 1, (y / getHeight()) * source.getHeight()));
        return new Color(source.getRGB(sx, sy));
    }

    @SuppressWarnings("serial")
    public void start() {
        JPanel content = new JPanel(new BorderLayout());

        menuBar = new JMenuBar();
        JMenu file = new JMenu("File");
        if (platform != Platform.MAC_OS_X) {
            file.add(new AbstractAction("About...") {
                @Override
                public void actionPerformed(ActionEvent e) {
                    about.showDialog();
                }
            });
        }
        JMenuItem newIfs = new JMenuItem(new AbstractAction("New IFS") {
            @Override
            public void actionPerformed(ActionEvent e) {
                IFS untitled = new IFS();
                show(EDITOR);
                bus.post(untitled);
            }
        });
        newIfs.setAccelerator(
                KeyStroke.getKeyStroke(KeyEvent.VK_N, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        file.add(newIfs);
        JMenuItem open = new JMenuItem(new AbstractAction("Open...") {
            @Override
            public void actionPerformed(ActionEvent e) {
                FileNameExtensionFilter filter = new FileNameExtensionFilter("XML Files", "xml");
                JFileChooser chooser = new JFileChooser();
                chooser.setCurrentDirectory(cwd);
                chooser.setFileFilter(filter);
                int result = chooser.showOpenDialog(null);
                if (result == JFileChooser.APPROVE_OPTION) {
                    IFS loaded = load(chooser.getSelectedFile());
                    loaded.setSize(size);
                    show(EDITOR);
                    bus.post(loaded);
                }
            }
        });
        open.setAccelerator(
                KeyStroke.getKeyStroke(KeyEvent.VK_O, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        file.add(open);
        save = new JMenuItem(new AbstractAction("Save") {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (ifs.getName() == null) {
                    saveAs.doClick();
                } else {
                    File saveAs = new File(cwd, ifs.getName() + ".xml");
                    save(saveAs);
                    save.setEnabled(false);
                    bus.post(ifs);
                }
            }
        });
        save.setAccelerator(
                KeyStroke.getKeyStroke(KeyEvent.VK_S, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        save.setEnabled(false);
        file.add(save);
        saveAs = new JMenuItem(new AbstractAction("Save As...") {
            @Override
            public void actionPerformed(ActionEvent e) {
                FileNameExtensionFilter filter = new FileNameExtensionFilter("XML Files", "xml");
                JFileChooser chooser = new JFileChooser();
                chooser.setCurrentDirectory(cwd);
                chooser.setFileFilter(filter);
                chooser.setSelectedFile(new File(Optional.fromNullable(ifs.getName()).or(IFS.UNTITLED) + ".xml"));
                int result = chooser.showSaveDialog(null);
                if (result == JFileChooser.APPROVE_OPTION) {
                    File saveAs = chooser.getSelectedFile();
                    String name = saveAs.getName();
                    if (!name.toLowerCase().endsWith(".xml")) {
                        saveAs = new File(saveAs.getParent(), name + ".xml");
                    }
                    ifs.setName(saveAs.getName().replace(".xml", ""));
                    save(saveAs);
                    bus.post(ifs);
                }
            }
        });
        saveAs.setEnabled(false);
        file.add(saveAs);
        export = new JMenuItem(new AbstractAction("Export...") {
            @Override
            public void actionPerformed(ActionEvent e) {
                FileNameExtensionFilter filter = new FileNameExtensionFilter("PNG Image Files", "png");
                JFileChooser chooser = new JFileChooser();
                chooser.setCurrentDirectory(cwd);
                chooser.setFileFilter(filter);
                chooser.setSelectedFile(new File(Optional.fromNullable(ifs.getName()).or(IFS.UNTITLED) + ".png"));
                int result = chooser.showSaveDialog(null);
                if (result == JFileChooser.APPROVE_OPTION) {
                    File export = chooser.getSelectedFile();
                    String name = export.getName();
                    if (!name.toLowerCase().endsWith(".png")) {
                        export = new File(export.getParent(), name + ".png");
                    }
                    viewer.save(export);
                    cwd = export.getParentFile();
                }
            }
        });
        export.setAccelerator(
                KeyStroke.getKeyStroke(KeyEvent.VK_E, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        file.add(export);
        JMenuItem print = new JMenuItem(new AbstractAction("Print...") {
            @Override
            public void actionPerformed(ActionEvent e) {
            }
        });
        print.setAccelerator(
                KeyStroke.getKeyStroke(KeyEvent.VK_P, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        file.add(print);
        if (platform != Platform.MAC_OS_X) {
            file.add(new AbstractAction("Preferences...") {
                @Override
                public void actionPerformed(ActionEvent e) {
                }
            });
            JMenuItem quit = new JMenuItem(new AbstractAction("Quit") {
                @Override
                public void actionPerformed(ActionEvent e) {
                    System.exit(0);
                }
            });
            quit.setAccelerator(
                    KeyStroke.getKeyStroke(KeyEvent.VK_Q, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
            file.add(quit);
        }
        menuBar.add(file);

        JMenu system = new JMenu("Display");
        showEditor = new JCheckBoxMenuItem(new AbstractAction("Editor") {
            @Override
            public void actionPerformed(ActionEvent e) {
                show(EDITOR);
            }
        });
        showViewer = new JCheckBoxMenuItem(new AbstractAction("Viewer") {
            @Override
            public void actionPerformed(ActionEvent e) {
                show(VIEWER);
            }
        });
        showDetails = new JCheckBoxMenuItem(new AbstractAction("Details") {
            @Override
            public void actionPerformed(ActionEvent e) {
                show(DETAILS);
            }
        });
        system.add(showEditor);
        system.add(showViewer);
        system.add(showDetails);
        ButtonGroup displayGroup = new ButtonGroup();
        displayGroup.add(showEditor);
        displayGroup.add(showViewer);
        displayGroup.add(showDetails);
        menuBar.add(system);
        setJMenuBar(menuBar);

        setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        addWindowListener(new WindowAdapter() {
            /** @see WindowListener#windowClosed(WindowEvent) */
            @Override
            public void windowClosed(WindowEvent e) {
                System.exit(0);
            }
        });

        editor = new Editor(bus, this);
        viewer = new Viewer(bus, this);
        details = new Details(bus, this);
        scroll = new JScrollPane(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        scroll.setViewportView(details);

        cards = new CardLayout();
        view = new JPanel(cards);
        view.add(editor, EDITOR);
        view.add(viewer, VIEWER);
        view.add(scroll, DETAILS);
        content.add(view, BorderLayout.CENTER);
        show(EDITOR);
        showEditor.setSelected(true);

        setContentPane(content);

        if (!fullScreen) {
            editor.setMinimumSize(min);
            editor.setSize(size);
            viewer.setMinimumSize(min);
            viewer.setSize(size);
            pack();
            final int top = getInsets().top + menuBar.getHeight();
            Dimension actual = new Dimension(size.width, size.height + top);
            setSize(actual);
            setPreferredSize(actual);
            setMinimumSize(new Dimension(min.width, min.height + top));
            Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
            setLocation((screen.width / 2) - (actual.width / 2), (screen.height / 2) - (actual.height / 2));
            addComponentListener(new ComponentAdapter() {
                @Override
                public void componentResized(ComponentEvent e) {
                    Dimension suggested = getSize();
                    int side = Math.max(MIN_WINDOW_SIZE, Math.min(suggested.width, suggested.height - top));
                    setSize(side, side + top);
                    bus.post(new Dimension(side, side));
                }
            });
        }

        setFocusable(true);
        requestFocusInWindow();
        setFocusTraversalKeysEnabled(false);
        addKeyListener(this);
        addKeyListener(editor);
        addKeyListener(viewer);

        IFS untitled = new IFS();
        bus.post(untitled);

        // Check for post-startup task
        if (postponed != null) {
            SwingUtilities.invokeLater(postponed);
        }

        setVisible(true);
    }

    public void show(String name) {
        cards.show(view, name);
        current = name;
        if (name.equals(VIEWER)) {
            export.setEnabled(true);
            viewer.reset();
            viewer.start();
        } else {
            export.setEnabled(false);
            viewer.stop();
        }
    }

    /** @see Subscriber#resized(Dimension) */
    @Override
    @Subscribe
    public void resized(Dimension resized) {
        size = resized.getSize();
        System.err.println("Resized: " + size.width + ", " + size.height);
    }

    /** @see Subscriber#updated(IFS) */
    @Override
    @Subscribe
    public void updated(IFS updated) {
        ifs = updated;
        System.err.println("Updated: " + ifs);
        String name = CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL,
                Optional.fromNullable(ifs.getName()).or(IFS.UNTITLED));
        setTitle(name);
        if (!ifs.isEmpty()) {
            save.setEnabled(true);
            saveAs.setEnabled(true);
        }
        repaint();
    }

    public void save(File file) {
        try (FileWriter writer = new FileWriter(file)) {
            JAXBContext context = JAXBContext.newInstance(IFS.class);
            Marshaller marshaller = context.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
            marshaller.marshal(ifs, writer);
            cwd = file.getParentFile();
        } catch (Exception e) {
            throw Throwables.propagate(e);
        }
    }

    public IFS load(File file) {
        try (FileReader reader = new FileReader(file)) {
            JAXBContext context = JAXBContext.newInstance(IFS.class);
            Unmarshaller unmarshaller = context.createUnmarshaller();
            IFS ifs = (IFS) unmarshaller.unmarshal(reader);
            cwd = file.getParentFile();
            return ifs;
        } catch (Exception e) {
            throw Throwables.propagate(e);
        }
    }

    public boolean isFullScreen() {
        return fullScreen;
    }

    public boolean isColour() {
        return colour;
    }

    public boolean hasPalette() {
        return palette && colours != null;
    }

    public boolean isStealing() {
        return stealing && source != null;
    }

    public boolean isIFSColour() {
        return ifscolour;
    }

    public List<Color> getColours() {
        return colours;
    }

    public int getPaletteSize() {
        return paletteSize;
    }

    public Render getRenderMode() {
        return render;
    }

    public long getSeed() {
        return seed;
    }

    /** Debug information shown. */
    public boolean isDebug() {
        return config.get(DEBUG_PROPERTY, Boolean.FALSE);
    }

    /** Small grid spacing. */
    public int getMinGrid() {
        return config.get(GRID_PROPERTY + ".min", DEFAULT_GRID_MIN);
    }

    /** Large grid spacing. */
    public int getMaxGrid() {
        return config.get(GRID_PROPERTY + ".max", DEFAULT_GRID_MAX);
    }

    /** Snap to grid distance. */
    public int getSnapGrid() {
        return config.get(GRID_PROPERTY + ".snap", DEFAULT_GRID_SNAP);
    }

    public BufferedImage getIcon() {
        return icon;
    }

    public JScrollPane getScroll() {
        return scroll;
    }

    public Viewer getViewer() {
        return viewer;
    }

    public Editor getEditor() {
        return editor;
    }

    public About getAbout() {
        return about;
    }

    public Preferences getPreferences() {
        return prefs;
    }

    public IFS getIFS() {
        return ifs;
    }

    public EventBus getEventBus() {
        return bus;
    }

    public int getThreads() {
        Integer threads = Math.max(Runtime.getRuntime().availableProcessors() / 2, MIN_THREADS);
        return config.get(THREADS_PROPERTY, threads);
    }

    /** @see java.awt.event.KeyListener#keyTyped(java.awt.event.KeyEvent) */
    @Override
    public void keyTyped(KeyEvent e) {
    }

    /** @see java.awt.event.KeyListener#keyPressed(java.awt.event.KeyEvent) */
    @Override
    public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_TAB) {
            if (e.isShiftDown()) {
                if (current.equals(EDITOR)) {
                    showDetails.setSelected(true);
                    show(DETAILS);
                } else if (current.equals(DETAILS)) {
                    showViewer.setSelected(true);
                    show(VIEWER);
                } else if (current.equals(VIEWER)) {
                    showEditor.setSelected(true);
                    show(EDITOR);
                }
            } else {
                if (current.equals(EDITOR)) {
                    showViewer.setSelected(true);
                    show(VIEWER);
                } else if (current.equals(VIEWER)) {
                    showDetails.setSelected(true);
                    show(DETAILS);
                } else if (current.equals(DETAILS)) {
                    showEditor.setSelected(true);
                    show(EDITOR);
                }
            }
        } else if (e.getKeyCode() == KeyEvent.VK_S) {
            if (viewer.isVisible() || details.isVisible()) {
                if (e.isShiftDown()) {
                    seed--;
                } else {
                    seed++;
                }
                loadColours();
                bus.post(ifs);
            }
        }
    }

    /** @see java.awt.event.KeyListener#keyReleased(java.awt.event.KeyEvent) */
    @Override
    public void keyReleased(KeyEvent e) {
    }

    public void printf(String format, Object... varargs) {
        String output = String.format(format, varargs);
        if (!output.endsWith("\n"))
            output = output.concat("\n");
        System.err.print(output);
        System.out.print(output);
    }

    /** @see java.lang.Thread.UncaughtExceptionHandler#uncaughtException(Thread, Throwable) */
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        printf("Error: Thread %s (%d) caused %s: %s", t.getName(), t.getId(), e.getClass().getName(),
                e.getMessage());
        if (isDebug()) {
            e.printStackTrace(System.err);
        }
        System.exit(1);
    }

    /**
     * Explorer.
     */
    public static void main(final String... argv) throws Exception {
        // Print text banner
        System.out.printf(BANNER, version.get());

        // Load splash screen first
        Splash splashScreen = new Splash();
        splashScreen.showDialog();

        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                // Start application
                Explorer explorer = new Explorer(argv);
                explorer.start();
            }
        });
    }

}