public final class BrowserLauncher {

    /**
     * The Java virtual machine that we are running on.  Actually, in most
     * cases we only care about the operating system, but some operating
     * systems require us to switch on the VM.
     */
    private static int jvm;

    /**
     * The browser for the system.
     */
    private static Object browser;

    /**
     * Caches whether any classes, methods, and fields that are not part of the
     * JDK and need to be dynamically loaded at runtime loaded successfully.
     * <p>
     * Note that if this is <code>false</code>, <code>openURL()</code> will
     * always return an IOException.
     */
    private static boolean loadedWithoutErrors;

    /**
     * The com.apple.mrj.MRJFileUtils class.
     */
    private static Class mrjFileUtilsClass;

    /**
     * The com.apple.mrj.MRJOSType class.
     */
    private static Class mrjOSTypeClass;

    /**
     * The com.apple.MacOS.AEDesc class.
     */
    private static Class aeDescClass;

    /**
     * The <init>(int) method of com.apple.MacOS.AETarget.
     */
    private static Constructor aeTargetConstructor;

    /**
     * The <init>(int, int, int) method of com.apple.MacOS.AppleEvent.
     */
    private static Constructor appleEventConstructor;

    /**
     * The <init>(String) method of com.apple.MacOS.AEDesc.
     */
    private static Constructor aeDescConstructor;

    /**
     * The findFolder method of com.apple.mrj.MRJFileUtils.
     */
    private static Method findFolder;

    /**
     * The getFileCreator method of com.apple.mrj.MRJFileUtils.
     */
    private static Method getFileCreator;

    /**
     * The getFileType method of com.apple.mrj.MRJFileUtils.
     */
    private static Method getFileType;

    /**
     * The openURL method of com.apple.mrj.MRJFileUtils.
     */
    private static Method openURL;

    /**
     * The makeOSType method of com.apple.MacOS.OSUtils.
     */
    private static Method makeOSType;

    /**
     * The putParameter method of com.apple.MacOS.AppleEvent.
     */
    private static Method putParameter;

    /**
     * The sendNoReply method of com.apple.MacOS.AppleEvent.
     */
    private static Method sendNoReply;

    /**
     * Actually an MRJOSType pointing to the System Folder on a Macintosh.
     */
    private static Object kSystemFolderType;

    /**
     * The keyDirectObject AppleEvent parameter type.
     */
    private static Integer keyDirectObject;

    /**
     * The kAutoGenerateReturnID AppleEvent code.
     */
    private static Integer kAutoGenerateReturnID;

    /**
     * The kAnyTransactionID AppleEvent code.
     */
    private static Integer kAnyTransactionID;

    /**
     * The linkage object required for JDirect 3 on Mac OS X.
     */
    private static Object linkage;

    /**
     * The framework to reference on Mac OS X.
     */
    private static final String JDirect_MacOSX
            = "/System/Library/Frameworks/Carbon.framework/Frameworks/"
            + "HIToolbox.framework/HIToolbox";

    /**
     * JVM constant for MRJ 2.0.
     */
    private static final int MRJ_2_0 = 0;

    /**
     * JVM constant for MRJ 2.1 or later.
     */
    private static final int MRJ_2_1 = 1;

    /**
     * JVM constant for Java on Mac OS X 10.0 (MRJ 3.0).
     */
    private static final int MRJ_3_0 = 3;

    /**
     * JVM constant for MRJ 3.1.
     */
    private static final int MRJ_3_1 = 4;

    /**
     * JVM constant for any Windows NT JVM.
     */
    private static final int WINDOWS_NT = 5;

    /**
     * JVM constant for any Windows 9x JVM.
     */
    private static final int WINDOWS_9x = 6;

    /**
     * JVM constant for any other platform.
     */
    private static final int OTHER = -1;

    /**
     * The file type of the Finder on a Macintosh.  Hardcoding "Finder" would
     * keep non-U.S. English systems from working properly.
     */
    private static final String FINDER_TYPE = "FNDR";

    /**
     * The creator code of the Finder on a Macintosh, which is needed to send
     * AppleEvents to the application.
     */
    private static final String FINDER_CREATOR = "MACS";

    /**
     * The name for the AppleEvent type corresponding to a GetURL event.
     */
    private static final String GURL_EVENT = "GURL";

    /**
     * The first parameter that needs to be passed into Runtime.exec() to open
     * the default web browser on Windows.
     */
    private static final String FIRST_WINDOWS_PARAMETER = "/c";

    /** The second parameter for Runtime.exec() on Windows. */
    private static final String SECOND_WINDOWS_PARAMETER = "start";

    /**
     * The third parameter for Runtime.exec() on Windows.  This is a "title"
     * parameter that the command line expects.  Setting this parameter allows
     * URLs containing spaces to work.
     */
    private static final String THIRD_WINDOWS_PARAMETER = "\"\"";

    /**
     * The shell parameters for Netscape that opens a given URL in an
     * already-open copy of Netscape on many command-line systems.
     */
    private static final String NETSCAPE_REMOTE_PARAMETER = "-remote";
    private static final String NETSCAPE_OPEN_PARAMETER_START = "'openURL(";
    private static final String NETSCAPE_OPEN_PARAMETER_END = ")'";

    /**
     * The message from any exception thrown throughout the initialization
     * process.
     */
    private static String errorMessage;

    /**
     * An initialization block that determines the operating system and loads
     * the necessary runtime data.
     */
    static {
        loadedWithoutErrors = true;
        String osName = System.getProperty("os.name");
        if (osName.startsWith("Mac OS")) {
            String mrjVersion = System.getProperty("mrj.version");
            String majorMRJVersion = mrjVersion.substring(0, 3);
            try {
                double version = Double.valueOf(majorMRJVersion).doubleValue();
                if (version == 2) {
                    jvm = MRJ_2_0;
                } else if (version >= 2.1 && version < 3) {
                    // Assume that all 2.x versions of MRJ work the same.
                    // MRJ 2.1 actually works via Runtime.exec() and 2.2
                    // supports that but has an openURL() method as well that
                    // we currently ignore.
                    jvm = MRJ_2_1;
                } else if (version == 3.0) {
                    jvm = MRJ_3_0;
                } else if (version >= 3.1) {
                    // Assume that all 3.1 and later versions of MRJ work the
                    // same.
                    jvm = MRJ_3_1;
                } else {
                    loadedWithoutErrors = false;
                    errorMessage = "Unsupported MRJ version: " + version;
                }
            } catch (NumberFormatException nfe) {
                loadedWithoutErrors = false;
                errorMessage = "Invalid MRJ version: " + mrjVersion;
            }
        } else if (osName.startsWith("Windows")) {
            if (osName.indexOf("9") != -1) {
                jvm = WINDOWS_9x;
            } else {
                jvm = WINDOWS_NT;
            }
        } else {
            jvm = OTHER;
        }

        if (loadedWithoutErrors) {    // if we haven't hit any errors yet
            loadedWithoutErrors = loadClasses();
        }
    }

    /**
     * This class should be never be instantiated; this just ensures so.
     */
    private BrowserLauncher() {
    }

    /**
     * Called by a static initializer to load any classes, fields, and methods
     * required at runtime to locate the user's web browser.
     * @return <code>true</code> if all intialization succeeded
     * <code>false</code> if any portion of the initialization failed
     */
    private static boolean loadClasses() {
        switch (jvm) {
            case MRJ_2_0:
                try {
                    Class aeTargetClass = Class.forName(
                            "com.apple.MacOS.AETarget");
                    Class osUtilsClass = Class.forName(
                            "com.apple.MacOS.OSUtils");
                    Class appleEventClass = Class.forName(
                            "com.apple.MacOS.AppleEvent");
                    Class aeClass = Class.forName(
                            "com.apple.MacOS.ae");
                    aeDescClass = Class.forName(
                            "com.apple.MacOS.AEDesc");

                    aeTargetConstructor = aeTargetClass.getDeclaredConstructor(
                            new Class [] {int.class });
                    appleEventConstructor
                            = appleEventClass.getDeclaredConstructor(
                            new Class[] {int.class, int.class, aeTargetClass,
                            int.class, int.class });
                    aeDescConstructor = aeDescClass.getDeclaredConstructor(
                            new Class[] {String.class });

                    makeOSType = osUtilsClass.getDeclaredMethod("makeOSType",
                            new Class [] {String.class });
                    putParameter = appleEventClass.getDeclaredMethod(
                            "putParameter",
                            new Class[] {int.class, aeDescClass });
                    sendNoReply = appleEventClass.getDeclaredMethod(
                            "sendNoReply", new Class[] {});

                    Field keyDirectObjectField = aeClass.getDeclaredField(
                            "keyDirectObject");
                    keyDirectObject = (Integer) keyDirectObjectField.get(null);
                    Field autoGenerateReturnIDField
                            = appleEventClass.getDeclaredField(
                            "kAutoGenerateReturnID");
                    kAutoGenerateReturnID
                            = (Integer) autoGenerateReturnIDField.get(null);
                    Field anyTransactionIDField
                            = appleEventClass.getDeclaredField(
                            "kAnyTransactionID");
                    kAnyTransactionID = (Integer) anyTransactionIDField.get(
                            null);
                } catch (ClassNotFoundException cnfe) {
                    errorMessage = cnfe.getMessage();
                    return false;
                } catch (NoSuchMethodException nsme) {
                    errorMessage = nsme.getMessage();
                    return false;
                } catch (NoSuchFieldException nsfe) {
                    errorMessage = nsfe.getMessage();
                    return false;
                } catch (IllegalAccessException iae) {
                    errorMessage = iae.getMessage();
                    return false;
                }
                break;
            case MRJ_2_1:
                try {
                    mrjFileUtilsClass = Class.forName(
                            "com.apple.mrj.MRJFileUtils");
                    mrjOSTypeClass = Class.forName("com.apple.mrj.MRJOSType");
                    Field systemFolderField
                            = mrjFileUtilsClass.getDeclaredField(
                            "kSystemFolderType");
                    kSystemFolderType = systemFolderField.get(null);
                    findFolder = mrjFileUtilsClass.getDeclaredMethod(
                            "findFolder", new Class[] {mrjOSTypeClass });
                    getFileCreator = mrjFileUtilsClass.getDeclaredMethod(
                            "getFileCreator", new Class[] {File.class });
                    getFileType = mrjFileUtilsClass.getDeclaredMethod(
                            "getFileType", new Class[] {File.class });
                } catch (ClassNotFoundException cnfe) {
                    errorMessage = cnfe.getMessage();
                    return false;
                } catch (NoSuchFieldException nsfe) {
                    errorMessage = nsfe.getMessage();
                    return false;
                } catch (NoSuchMethodException nsme) {
                    errorMessage = nsme.getMessage();
                    return false;
                } catch (SecurityException se) {
                    errorMessage = se.getMessage();
                    return false;
                } catch (IllegalAccessException iae) {
                    errorMessage = iae.getMessage();
                    return false;
                }
                break;
            case MRJ_3_0:
                try {
                    Class linker = Class.forName(
                            "com.apple.mrj.jdirect.Linker");
                    Constructor constructor = linker.getConstructor(
                            new Class[]{Class.class });
                    linkage = constructor.newInstance(new Object[] {
                        BrowserLauncher.class });
                } catch (ClassNotFoundException cnfe) {
                    errorMessage = cnfe.getMessage();
                    return false;
                } catch (NoSuchMethodException nsme) {
                    errorMessage = nsme.getMessage();
                    return false;
                } catch (InvocationTargetException ite) {
                    errorMessage = ite.getMessage();
                    return false;
                } catch (InstantiationException ie) {
                    errorMessage = ie.getMessage();
                    return false;
                } catch (IllegalAccessException iae) {
                    errorMessage = iae.getMessage();
                    return false;
                }
                break;
            case MRJ_3_1:
                try {
                    mrjFileUtilsClass = Class.forName(
                            "com.apple.mrj.MRJFileUtils");
                    openURL = mrjFileUtilsClass.getDeclaredMethod("openURL",
                            new Class[] {String.class });
                } catch (ClassNotFoundException cnfe) {
                    errorMessage = cnfe.getMessage();
                    return false;
                } catch (NoSuchMethodException nsme) {
                    errorMessage = nsme.getMessage();
                    return false;
                }
                break;
            default:
                break;
        }
        return true;
    }

    /**
     * Attempts to locate the default web browser on the local system.  Caches
     * results so it only locates the browser once for each use of this class
     * per JVM instance.
     * @return The browser for the system.  Note that this may not be what you
     * would consider to be a standard web browser; instead, it's the
     * application that gets called to open the default web browser.  In some
     * cases, this will be a non-String object that provides the means of
     * calling the default browser.
     */
    private static Object locateBrowser() {
        if (browser != null) {
            return browser;
        }
        switch (jvm) {
            case MRJ_2_0:
                try {
                    Integer finderCreatorCode = (Integer) makeOSType.invoke(
                            null, new Object[] {FINDER_CREATOR });
                    Object aeTarget = aeTargetConstructor.newInstance(
                            new Object[] {finderCreatorCode });
                    Integer gurlType = (Integer) makeOSType.invoke(null,
                            new Object[] {GURL_EVENT });
                    Object appleEvent = appleEventConstructor.newInstance(
                            new Object[] {gurlType, gurlType, aeTarget,
                            kAutoGenerateReturnID, kAnyTransactionID });
                    // Don't set browser = appleEvent because then the next
                    // time we call locateBrowser(), we'll get the same
                    // AppleEvent, to which we'll already have added the
                    // relevant parameter. Instead, regenerate the AppleEvent
                    // every time.
                    // There's probably a way to do this better; if any has any
                    // ideas, please let me know.
                    return appleEvent;
                } catch (IllegalAccessException iae) {
                    browser = null;
                    errorMessage = iae.getMessage();
                    return browser;
                } catch (InstantiationException ie) {
                    browser = null;
                    errorMessage = ie.getMessage();
                    return browser;
                } catch (InvocationTargetException ite) {
                    browser = null;
                    errorMessage = ite.getMessage();
                    return browser;
                }
            case MRJ_2_1:
                File systemFolder;
                try {
                    systemFolder = (File) findFolder.invoke(null,
                            new Object[] {kSystemFolderType });
                } catch (IllegalArgumentException iare) {
                    browser = null;
                    errorMessage = iare.getMessage();
                    return browser;
                } catch (IllegalAccessException iae) {
                    browser = null;
                    errorMessage = iae.getMessage();
                    return browser;
                } catch (InvocationTargetException ite) {
                    browser = null;
                    errorMessage = ite.getTargetException().getClass() + ": "
                            + ite.getTargetException().getMessage();
                    return browser;
                }
                String[] systemFolderFiles = systemFolder.list();
                // Avoid a FilenameFilter because that can't be stopped
                // mid-list
                for(int i = 0; i < systemFolderFiles.length; i++) {
                    try {
                        File file = new File(systemFolder,
                                systemFolderFiles[i]);
                        if (!file.isFile()) {
                            continue;
                        }
                        // We're looking for a file with a creator code of
                        // 'MACS' and a type of 'FNDR'.  Only requiring the
                        // type results in non-Finder applications being picked
                        // up on certain Mac OS 9 systems, especially German
                        // ones, and sending a GURL event to those applications
                        // results in a logout under Multiple Users.
                        Object fileType = getFileType.invoke(null,
                                new Object[] {file });
                        if (FINDER_TYPE.equals(fileType.toString())) {
                            Object fileCreator = getFileCreator.invoke(null,
                                    new Object[] {file });
                            if (FINDER_CREATOR.equals(fileCreator.toString())) {
                                browser = file.toString(); // Actually the
                                                           // Finder, but
                                                           // that's OK
                                return browser;
                            }
                        }
                    } catch (IllegalArgumentException iare) {
                        browser = browser;
                        errorMessage = iare.getMessage();
                        return null;
                    } catch (IllegalAccessException iae) {
                        browser = null;
                        errorMessage = iae.getMessage();
                        return browser;
                    } catch (InvocationTargetException ite) {
                        browser = null;
                        errorMessage = ite.getTargetException().getClass()
                        + ": " + ite.getTargetException().getMessage();
                        return browser;
                    }
                }
                browser = null;
                break;
            case MRJ_3_0:
            case MRJ_3_1:
                browser = "";    // Return something non-null
                break;
            case WINDOWS_NT:
                browser = "cmd.exe";
                break;
            case WINDOWS_9x:
                browser = "command.com";
                break;
            case OTHER:
            default:
                browser = "firefox";
                break;
        }
        return browser;
    }

    /**
     * Attempts to open the default web browser to the given URL.
     * @param url The URL to open
     * @throws IOException If the web browser could not be located or does not
     * run
     */
    public static void openURL(String url) throws IOException {
        if (!loadedWithoutErrors) {
            throw new IOException("Exception in finding browser: "
                    + errorMessage);
        }
        Object browser = locateBrowser();
        if (browser == null) {
            throw new IOException("Unable to locate browser: "
                    + errorMessage);
        }

        switch (jvm) {
            case MRJ_2_0:
                Object aeDesc = null;
                try {
                    aeDesc = aeDescConstructor.newInstance(new Object[] {
                        url });
                    putParameter.invoke(browser, new Object[] {
                        keyDirectObject, aeDesc });
                    sendNoReply.invoke(browser, new Object[] {});
                } catch (InvocationTargetException ite) {
                    throw new IOException(
                            "InvocationTargetException while creating AEDesc: "
                            + ite.getMessage());
                } catch (IllegalAccessException iae) {
                    throw new IOException(
                            "IllegalAccessException while building "
                            + "AppleEvent: " + iae.getMessage());
                } catch (InstantiationException ie) {
                    throw new IOException("InstantiationException while "
                            + "creating AEDesc: " + ie.getMessage());
                } finally {
                    aeDesc = null;    // Encourage it to get disposed if it
                                        // was created
                    browser = null;    // Ditto
                }
                break;
            case MRJ_2_1:
                Runtime.getRuntime().exec(new String[] {(String) browser,
                url } );
                break;
            case MRJ_3_0:
                int[] instance = new int[1];
                int result = ICStart(instance, 0);
                if (result == 0) {
                    int[] selectionStart = new int[] {0 };
                    byte[] urlBytes = url.getBytes();
                    int[] selectionEnd = new int[] {urlBytes.length };
                    result = ICLaunchURL(instance[0], new byte[] {0 },
                            urlBytes, urlBytes.length, selectionStart,
                            selectionEnd);
                    if (result == 0) {
                        // Ignore the return value; the URL was launched
                        // successfully regardless of what happens here.
                        ICStop(instance);
                    } else {
                        throw new IOException("Unable to launch URL: "
                                + result);
                    }
                } else {
                    throw new IOException("Unable to create an Internet Config"
                            + " instance: " + result);
                }
                break;
            case MRJ_3_1:
                try {
                    openURL.invoke(null, new Object[] {url });
                } catch (InvocationTargetException ite) {
                    throw new IOException(
                            "InvocationTargetException while calling openURL: "
                            + ite.getMessage());
                } catch (IllegalAccessException iae) {
                    throw new IOException(
                            "IllegalAccessException while calling openURL: "
                            + iae.getMessage());
                }
                break;
            case WINDOWS_NT:
            case WINDOWS_9x:
                // Add quotes around the URL to allow ampersands and other
                // special characters to work.
                Process process = Runtime.getRuntime().exec(new String[] {
                    (String) browser,
                FIRST_WINDOWS_PARAMETER,
                SECOND_WINDOWS_PARAMETER,
                THIRD_WINDOWS_PARAMETER,
                '"' + url + '"' });
                // This avoids a memory leak on some versions of Java on
                // Windows.
                // That's hinted at in
                // <http://developer.java.sun.com/developer/qow/archive/68/>.
                try {
                    process.waitFor();
                    process.exitValue();
                } catch (InterruptedException ie) {
                    throw new IOException(
                            "InterruptedException while launching browser: "
                            + ie.getMessage());
                }
                break;
            case OTHER:
                // Assume that we're on Unix and that Netscape is installed

                // First, attempt to open the URL in a currently running
                // session of Netscape
                process = Runtime.getRuntime().exec(new String[] {
                    (String) browser,
                NETSCAPE_REMOTE_PARAMETER,
                NETSCAPE_OPEN_PARAMETER_START +
                        url +
                        NETSCAPE_OPEN_PARAMETER_END });
                try {
                    int exitCode = process.waitFor();
                    if (exitCode != 0) {    // if Netscape was not open
                        Runtime.getRuntime().exec(new String[] {
                            (String) browser, url });
                    }
                } catch (InterruptedException ie) {
                    throw new IOException(
                            "InterruptedException while launching browser: "
                            + ie.getMessage());
                }
                break;
            default:
                // This should never occur, but if it does, we'll try the
                // simplest thing possible
                Runtime.getRuntime().exec(new String[] {(String) browser,
                url });
                break;
        }
    }

    /**
     * Methods required for Mac OS X.  The presence of native methods does
     * not cause any problems on other platforms.
     */
    private native static int ICStart(int[] instance, int signature);
    private native static int ICStop(int[] instance);
    private native static int ICLaunchURL(int instance, byte[] hint,
            byte[] data, int len,
            int[] selectionStart, int[] selectionEnd);
} public final class Main {
    /**
     * The single instance of the application that will be created.
     */
    private static Main main;
    /**
     * The main window.
     */
    private Mainframe frame;
    /**
     * The &quot;About...&quot; box.
     */
    private AboutBox aboutBox = new AboutBox();
    /**
     * Internationalization strings.
     */
    private static ResourceBundle resBundle = ResourceBundle.getBundle(
            "dg/hipster/resource/strings");

    static {
        System.setProperty("apple.laf.useScreenMenuBar", "true");
    }

    /**
     * The method executed at application start.
     * @param args the command line arguments
     */
    public static void main(final String[] args) {
        main = new Main();
        main.initView();
        main.initControllers();
        IdeaDocument document = new IdeaDocument();
        if (args.length > 0) {
            try {
                document = ReaderFactory.getInstance().read(new File(args[0]));
            } catch (ReaderException re) {
                GuiUtilities.showDebug(re.getMessage());
            }
        }
        getMainframe().setDocument(document);
        main.setVisible(true);
    }

    /**
     * Default constructor of the application. Left as package-private
     * for unit-testing.
     */
    Main() {
    }

    /**
     * Set up the interface.
     */
    private void initView() {
        frame = new Mainframe();
    }

    /**
     * Connect the controllers to the interface.
     */
    private void initControllers() {
        if (frame != null) {
            frame.setDefaultCloseOperation(
                    JFrame.DO_NOTHING_ON_CLOSE);
            frame.addWindowListener(new WindowAdapter() {
                public void windowClosing(final WindowEvent e) {
                    if (handleQuit()) {
                        System.exit(0);
                    }
                }
            });
        }
        if (isMac()) {
            loadMacPlugins();
        }
    }

    /**
     * Load plugins for integration with the Macintosh environment.
     */
    private void loadMacPlugins() {
        try {
            Class.forName("dg.hipster.controller.MacAppListener");
        } catch (ClassNotFoundException cnfe) {
            cnfe.printStackTrace();
        }
    }

    /**
     * Make the application visible. Will put the app into edit
     * mode.
     * @param visible true to become visible, false otherwise.
     */
    public void setVisible(final boolean visible) {
        if (frame != null) {
            frame.setVisible(visible);
            frame.editSelected();
        }
    }

    /**
     * True if we are running on a Mac.
     * @return true if the current platform is a Mac, false otherwise
     */
    public static boolean isMac() {
        String osName = System.getProperty("os.name");
        return ((osName != null) && (osName.indexOf("Mac") != -1));
    }

    /**
     * True if we are running on Windows.
     * @return true if the current platform is Windows, false otherwise
     */
    public static boolean isWindows() {
        String osName = System.getProperty("os.name");
        return ((osName != null) && (osName.indexOf("Windows") != -1));
    }

    /**
     * Display the about box.
     */
    public static void showAbout() {
        main.aboutBox.setVisible(true);
    }

    /**
     * Display the preferences dialog.
     */
    public static void showPreferences() {
        GuiUtilities.showInfo("preferences.placeholder");
    }

    /**
     * Called when the application being closed down.
     *@return true if OK to continue with shutdown.
     */
    public static boolean handleQuit() {
        Rectangle bounds = main.frame.getBounds();
        Settings.getInstance().setWindowLeft(bounds.x);
        Settings.getInstance().setWindowTop(bounds.y);
        Settings.getInstance().setWindowWidth(bounds.width);
        Settings.getInstance().setWindowHeight(bounds.height);
        try {
            return getMainframe().checkIfSave();
        } catch (Exception e) {
            e.printStackTrace();
            return true;
        }
    }

    /**
     * Get the main application frme.
     * @return main application frame.
     */
    public static Mainframe getMainframe() {
        return main.frame;
    }
} public final class Utilities {

    /**
     * Creates a new instance of Utilities.
     */
    private Utilities() {
    }

    /**
     * Whether two objects are equal, without throwing
     * a null pointer exception.
     * @param object0 first object we are comparing
     * @param object1 second object we are comparing
     * @return true if they are the same, false otherwise
     */
    public static boolean areEqual(final Object object0, final Object object1) {
        if ((object0 == null) && (object1 == null)) {
            return true;
        }
        if ((object0 == null) || (object1 == null)) {
            return false;
        }
        return object0.equals(object1);
    }

    /**
     * Whether a point is on a line of a given thickness.
     * @param p point in question.
     * @param fromPoint one end of line.
     * @param toPoint other end of line.
     * @param thickness thickness of the line.
     * @return true if on the line, false otherwise.
     */
    public static boolean hitsLine(final Point2D p, final Point2D fromPoint,
            final Point2D toPoint, final double thickness) {
        if ((fromPoint == null) || (toPoint == null)) {
            return false;
        }
        double vx0 = fromPoint.getX();
        double vy0 = fromPoint.getY();
        double vx1 = toPoint.getX();
        double vy1 = toPoint.getY();
        double vx2 = p.getX();
        double vy2 = p.getY();

        double minX = Math.min(vx0, vx1) - thickness / 2;
        double maxX = Math.max(vx0, vx1) + thickness / 2;
        double minY = Math.min(vy0, vy1) - thickness / 2;
        double maxY = Math.max(vy0, vy1) + thickness / 2;

        if ((vx2 > maxX) || (vx2 < minX)) {
            return false;
        }
        if ((vy2 > maxY) || (vy2 < minY)) {
            return false;
        }

        // Calculate magnitude of the normal to the line-segment
        double magNormal = Math.sqrt(
                ((vx1 - vx0) * (vx1 - vx0)) + ((vy1 - vy0) * (vy1 - vy0))
                );

        // Calculate (signed) distance of the point from the line-segment
        double distance = (
                ((vx2 - vx0) * (vy0 - vy1)) + ((vy2 - vy0) * (vx1 - vx0))
                ) / magNormal;

        // Check if the
        if (Math.abs(distance) <= (thickness / 2)) {
            return true;
        }
        return false;
    }

    /**
     * Convert a file into a string URL.
     * @return string version of a URL, with spaces converted.
     * @param f file to get URL for.
     * @throws java.net.MalformedURLException if the file cannot be converted
     * to a URL - should not happen.
     */
    public static String toStringUrl(final File f)
    throws MalformedURLException {
        String url = f.toURL().toString();
        url = url.trim();
        while (url.indexOf(' ') != -1) {
            int pos = url.indexOf(' ');
            url = url.substring(0, pos) + "%20"
                    + url.substring(pos + 1);
        }
        return url;
    }
} public final class ReaderException extends Exception {
    private String message;
    private Throwable cause;

    /** Creates a new instance of ReaderException */
    public ReaderException(String aMessage, Throwable aCause) {
        this.message = aMessage;
        this.cause = aCause;
    }

    public String getMessage() {
        return message;
    }

    public Throwable getCause() {
        return cause;
    }

} public final class ReaderFactory {
    private static ReaderFactory instance = new ReaderFactory();

    /** Creates a new instance of ReaderFactory */
    private ReaderFactory() {
    }

    public static ReaderFactory getInstance() {
        return instance;
    }

    public IdeaDocument read(File f) throws ReaderException {
        try {
            IdeaReader reader = null;
            if (f.getName().toLowerCase().endsWith(".opml")) {
                reader = new OPMLReader(new FileInputStream(f));
            }
            if (reader != null) {
                IdeaDocument document = reader.getDocument();
                document.setCurrentFile(f);
                return document;
            }
        } catch (Exception e) {
            throw new ReaderException("Unable to read file " + f, e);
        }
        throw new ReaderException("Unknown file type for " + f, null);
    }
} public final class WriterFactory {
    private static WriterFactory instance = new WriterFactory();

    /** Creates a new instance of ReaderFactory */
    private WriterFactory() {
    }

    public static WriterFactory getInstance() {
        return instance;
    }

    public void write(File f, IdeaDocument document, boolean export) throws ReaderException {
        try {
            IdeaWriter writer = null;
            if (f.getName().toLowerCase().endsWith(".opml")) {
                writer = new OPMLWriter(new OutputStreamWriter(
                        new FileOutputStream(f), "UTF-8"));
            } else if (f.getName().toLowerCase().endsWith(".txt")) {
                writer = new WikiWriter(new OutputStreamWriter(
                        new FileOutputStream(f), "UTF-8"));
            }
            if (writer != null) {
                try {
                    writer.write(document);
                    if (!export) {
                        document.setCurrentFile(f);
                        document.setDirty(false);
                    }
                } catch(Exception e) {
                    e.printStackTrace();
                }
            } else {
                throw new ReaderException("Unknown file type for " + f, null);
            }
        } catch (Exception e) {
            throw new ReaderException("Unable to write file " + f, e);
        }
    }
} public final class WikiWriter implements IdeaWriter {
    /**
     * Date format to use for string conversion.
     */
    public static final DateFormat DATE_FORMAT = new SimpleDateFormat(
            "E, dd MMM yyyy hh:mm:ss z");
    private Writer out;

    public WikiWriter(Writer out) {
        this.out = out;
    }

    public void write(IdeaDocument document) throws IOException {
        save(document);
        out.flush();
        out.close();
    }

    private void save(final IdeaDocument document) throws IOException {
        Idea idea = document.getIdea();
        index(idea);
        out.write(wikiIdea(0, idea));
    }

    public static String wikiIdea(int indent, Idea idea) {
        String title = (blank(idea.getDescription()) ? idea.getText() : idea.getDescription());
        String url = idea.getUrl();
        String result = "";
        if ((url != null) && (url.length() != 0)) {
            title = "[" + url + " " + title + "]";
        }
        String heading = "";
        if ((indent < 3) && (title.length() < 60)) {
            heading = "====================".substring(0, indent + 2);
            result += heading + title + heading + "\n\n";
        } else {
            heading = "***********".substring(0, Math.max(indent - 2, 1));
            result += heading + title + "\n";
        }
        if (!blank(idea.getNotes())) {
            result += idea.getNotes() + "\n\n";
        }
        for (Idea subIdea: idea.getSubIdeas()) {
            result += wikiIdea(indent + 1, subIdea);
        }
        if (idea.getLinks().size() > 0) {
            result += "See also:" + "\n\n";
        }
        for (IdeaLink link: idea.getLinks()) {
            result += "* [[#" + link.getTo().getText() + "]]\n";
        }
        return result;
    }
    
    private static boolean blank(String s) {
        return (s == null) || (s.length() == 0);
    }

    private Map<Idea, Integer> ideaIndex;
    int count;

    private void index(Idea idea) {
        ideaIndex = new HashMap<Idea, Integer>();
        count = 0;
        indexWithSubs(idea);
    }

    private void indexWithSubs(Idea idea) {
        ideaIndex.put(idea, count++);
        for (Idea subIdea : idea.getSubIdeas()) {
            indexWithSubs(subIdea);
        }
    }
} public static String wikiIdea(int indent, Idea idea) {
    String title = (blank(idea.getDescription()) ? idea.getText() : idea.getDescription());
    String url = idea.getUrl();
    String result = "";
    if ((url != null) && (url.length() != 0)) {
        title = "[" + url + " " + title + "]";
    }
    String heading = "";
    if ((indent < 3) && (title.length() < 60)) {
        heading = "====================".substring(0, indent + 2);
        result += heading + title + heading + "\n\n";
    } else {
        heading = "***********".substring(0, Math.max(indent - 2, 1));
        result += heading + title + "\n";
    }
    if (!blank(idea.getNotes())) {
        result += idea.getNotes() + "\n\n";
    }
    for (Idea subIdea: idea.getSubIdeas()) {
        result += wikiIdea(indent + 1, subIdea);
    }
    if (idea.getLinks().size() > 0) {
        result += "See also:" + "\n\n";
    }
    for (IdeaLink link: idea.getLinks()) {
        result += "* [[#" + link.getTo().getText() + "]]\n";
    }
    return result;
} public class Idea extends AbstractModel implements IdeaListener {
    /**
     * Length of a branch idea.
     */
    private double length;
    /**
     * Clockwise angle from the angle defined by the parent.
     */
    private double angle;
    /**
     * Angular velocity.
     */
    private double v;
    /**
     * Short text version of the idea. Used for the text
     * on idea maps.
     */
    private String text = "";
    /**
     * Description - probably used to over-ride shorter name in exports.
     */
    private String description = "";
    /**
     * URL to hyperlink to.
     */
    private String url = "";
    /**
     * Start date for this idea.
     */
    private Date startDate;
    /**
     * Start date for this idea.
     */
    private Date endDate;
    /**
     * List of sub-ideas.
     */
    private Vector<Idea> subIdeas = new Vector<Idea>();
    /**
     * List of sub-ideas.
     */
    private Vector<IdeaLink> links = new Vector<IdeaLink>();
    /**
     * List of objects that are observing this idea.
     */
    private List<IdeaListener> listeners = new Vector<IdeaListener>();
    /**
     * Longer notes.
     */
    private String notes = "";
    /**
     * Whether this idea is currently selected.
     */
    private boolean selected;

    /**
     * No args constructor.
     */
    public Idea() {

    }

    /**
     * Constructor for an idea with the given text.
     * @param text zhort text description of the idea
     */
    public Idea(String text) {
        setText(text);
    }

    //
    // PROPERTIES
    //

    /**
     * Get the short text description of this idea.
     * @return short text string, using for idea maps
     */
    public String getText() {
        return text;
    }

    /**
     * Specify the short newText description of this idea.
     *
     * @param newText string to use
     */
    public void setText(String newText) {
        String oldText = this.text;
        if ((this.text == null) || (!this.text.equals(newText))) {
            this.text = newText;
            this.firePropertyChange("text", oldText, newText);
            notify(new IdeaEvent(this, IdeaEvent.CHANGED, "CHANGED"));
        }
    }

    /**
     * The length of this idea.
     *@return length in points.
     */
    public double getLength() {
        return length;
    }

    /**
     * The length of this idea.
     * @param newLength length of the idea's representation.
     */
    public void setLength(double newLength) {
        double oldLength = this.length;
        this.length = newLength;
        if (Math.abs(oldLength - newLength) > 0.0000001) {
            this.firePropertyChange("length", oldLength, newLength);
        }
    }

    /**
     * The angle of this idea relative to its parent.
     *@return angle in radians.
     */
    public double getAngle() {
        return angle;
    }

    /**
     * The angle of this idea relative to its parent.
     * @param newAngle angle in radians.
     */
    public void setAngle(double newAngle) {
        double oldAngle = this.angle;
        this.angle = newAngle;
        if (Math.abs(oldAngle - newAngle) > 0.0000001) {
            this.firePropertyChange("angle", oldAngle, newAngle);
        }
    }

    /**
     * The velocity of this idea's end-point during animation.
     *@return the velocity.
     */
    public double getV() {
        return v;
    }

    /**
     * The velocity of this idea's end-point during animation.
     * @param newV the velocity.
     */
    public void setV(double newV) {
        double oldV = this.v;
        this.v = newV;
        if (Math.abs(oldV - newV) > 0.0000001) {
            this.firePropertyChange("v", oldV, newV);
        }
    }

    /**
     * Longer notes fields.
     * @return Long text string for use in the notes
     * field of this idea's properties palette.
     */
    public String getNotes() {
        return notes;
    }

    /**
     * Longer notes fields.
     * @param newNotes Long text string for use in the notes
     * field of this idea's properties palette.
     */
    public void setNotes(String newNotes) {
        String oldNotes = this.notes;
        if ((this.notes == null) || (!this.notes.equals(newNotes))) {
            this.notes = newNotes;
            this.firePropertyChange("notes", oldNotes, newNotes);
            notify(new IdeaEvent(this, IdeaEvent.CHANGED, "CHANGED"));
        }
    }

    /**
     * URL associated with this idea.
     * @return URL of more info.
     */
    public String getUrl() {
        return url;
    }

    /**
     * URL connected with this idea.
     * @param newUrl associated URL.
     */
    public void setUrl(String newUrl) {
        String oldUrl = this.url;
        if ((this.url == null) || (!this.url.equals(newUrl))) {
            this.url = newUrl;
            this.firePropertyChange("url", oldUrl, newUrl);
        }
    }

    /**
     * Longer text field. Might be used in exports in
     * preference to the text field.
     * @return Longer text field.
     */
    public String getDescription() {
        return description;
    }

    /**
     * Longer text field. Might be used in exports in
     * preference to the text field.
     * @param newDescription Longer text field.
     */
    public void setDescription(String newDescription) {
        String oldDescription = this.description;
        if ((this.description == null)
        || (!this.description.equals(newDescription))) {
            this.description = newDescription;
            this.firePropertyChange("description", oldDescription,
                    newDescription);
        }
    }

    /**
     * End date for the period this idea is valid.
     * @return idea's end date.
     */
    public Date getEndDate() {
        return endDate;
    }

    /**
     * End date for the period this idea is valid.
     * @param newEndDate idea's end date.
     */
    public void setEndDate(Date newEndDate) {
        Date oldEndDate = this.endDate;
        if ((this.endDate == null)
        || (!this.endDate.equals(newEndDate))) {
            this.endDate = newEndDate;
            this.firePropertyChange("endDate", oldEndDate,
                    newEndDate);
        }
    }

    /**
     * Start date for the period this idea is valid.
     * @return idea's start date.
     */
    public Date getStartDate() {
        return startDate;
    }

    /**
     * Start date for the period this idea is valid.
     * @param newStartDate idea's start date.
     */
    public void setStartDate(Date newStartDate) {
        Date oldStartDate = this.startDate;
        if ((this.startDate == null)
        || (!this.startDate.equals(newStartDate))) {
            this.startDate = newStartDate;
            this.firePropertyChange("startDate", oldStartDate,
                    newStartDate);
        }
    }

    public boolean isSelected() {
        return selected;
    }

    public void setSelected(boolean newSelected) {
        boolean oldSelected = this.selected;
        this.selected = newSelected;
        if (oldSelected != newSelected) {
            this.firePropertyChange("selected", oldSelected, newSelected);
        }
    }

    /**
     * Get a list of the sub-ideas for this idea.
     *@return list of idea objects
     */
    public List<Idea> getSubIdeas() {
        return (List<Idea>)subIdeas.clone();
    }

    /**
     * Get the list of links that this idea is connected to.
     * @return list of links to related ideas.
     */
    public List<IdeaLink> getLinks() {
        return (List<IdeaLink>)links.clone();
    }

    //
    // Collection methods.
    //

    /**
     * Add a sub-idea to this idea.
     * @param subIdea sub-idea to add
     */
    public synchronized void add(Idea subIdea) {
        subIdeas.add(subIdea);
        subIdea.addIdeaListener(this);
        notify(new IdeaEvent(this, IdeaEvent.ADDED, "ADDED",
                subIdea, subIdeas.size() - 1));
    }

    /**
     * Add a sub-idea to this idea at the given position.
     * @param pos position in the list of sub-ideas to insert
     * @param subIdea sub-idea to add
     */
    public synchronized void add(int pos, Idea subIdea) {
        subIdeas.add(pos, subIdea);
        subIdea.addIdeaListener(this);
        notify(new IdeaEvent(this, IdeaEvent.ADDED, "ADDED",
                subIdea, pos));
    }

    /**
     * Remove the given sub-idea from this idea. No
     * exception is raised if the idea given is
     * <em>not</em> a sub-idea of this idea.
     * @param subIdea sub-idea to be removed.
     */
    public synchronized void remove(Idea subIdea) {
        int pos = subIdeas.indexOf(subIdea);
        subIdea.removeLinks();
        subIdeas.remove(subIdea);
        subIdea.removeIdeaListener(this);
        subIdea.notify(new IdeaEvent(subIdea, IdeaEvent.REMOVED, "REMOVED",
                subIdea, pos));
        notify(new IdeaEvent(this, IdeaEvent.REMOVED, "REMOVED",
                subIdea, pos));
    }

    /**
     * Add a one-way link to another idea.
     *@param other idea to link to.
     */
    public void addLink(IdeaLink other) {
        if ((other != null) && (!this.equals(other.getTo()))
        && (this.equals(other.getFrom()))) {
            links.add(other);
            notify(new IdeaEvent(this, IdeaEvent.ADDED_LINK, "ADDED_LINK",
                    other));
        }
    }

    /**
     * Remove a link to another idea.
     *@param other idea to link to.
     */
    public void removeLink(IdeaLink other) {
        if (other == null) {
            return;
        }
        if (links.contains(other)) {
            links.remove(other);
            notify(new IdeaEvent(this, IdeaEvent.REMOVED_LINK, "REMOVED_LINK",
                    other));
        }
    }

    /**
     * Remove all links from the idea
     */
    public void removeLinks() {
        for (int i = links.size() - 1; i >= 0; i--) {
            IdeaLink link = links.get(i);
            removeLink(link);
        }
    }

    //
    // Idea-listener methods.
    //

    /**
     * Add an idea listener to this idea. The idea listener
     * will be informed when anything changes about this
     * idea.
     * @param ideaListener idea listener in question.
     */
    public void addIdeaListener(IdeaListener ideaListener) {
        if (!listeners.contains(ideaListener)) {
            listeners.add(ideaListener);
        }
    }

    /**
     * The given idea-listener will no longer be
     * notified of changes to this idea. No exception
     * will be thrown if the given idea-listener is
     * not currently listening to this idea.
     * @param ideaListener idea-listener in question
     */
    public void removeIdeaListener(IdeaListener ideaListener) {
        listeners.remove(ideaListener);
    }

    /**
     * Notify all listeners that something has changed. The parameters
     * provide more detail about the change.
     * @param command Text string describing the command. See source for
     * details.
     * @param paras objects providing more information about the change
     */
    private void notify(IdeaEvent ideaEvent) {
        for (IdeaListener listener: listeners) {
            listener.ideaChanged(ideaEvent);
        }
    }

    public void ideaChanged(IdeaEvent ideaEvent) {
        notify(ideaEvent);
    }

    //
    // Other methods.
    //

    /**
     * String version of this idea. It will be the short text
     * description, followed by a recursive list of the
     * sub-ideas.
     * @return string describing this idea and its sub-ideas.
     */
    public String toString() {
        return this.text + subIdeas.toString();
    }

    /**
     * Create a clone of this idea.
     */
    public Idea clone() {
        Idea idea = new Idea(getText());
        idea.setAngle(getAngle());
        idea.setLength(getLength());
        idea.setNotes(getNotes());
        idea.setDescription(getDescription());
        idea.setStartDate(getStartDate());
        idea.setEndDate(getEndDate());
        idea.setUrl(getUrl());
        idea.setV(getV());
        for (Idea subIdea : this.subIdeas) {
            idea.add(subIdea.clone());
        }
        return idea;
    }

    /**
     * Find the parent for the given idea, starting at this idea.
     *@param idea - idea we are looking for.
     *@return parent of the given idea, if it is in the tree of ideas
     * growing from this one.
     */
    public Idea getParentFor(Idea idea) {
        if (this == idea) {
            return null;
        }
        for (Idea subIdea : this.subIdeas) {
            if (subIdea == idea) {
                return this;
            }
            Idea descendent = subIdea.getParentFor(idea);
            if (descendent != null) {
                return descendent;
            }
        }
        for (IdeaLink link : this.links) {
            if (link == idea) {
                return this;
            }
        }
        return null;
    }
} public class IdeaDocument extends AbstractModel implements IdeaListener {
    /**
     * Internationalization strings.
     */
    protected static ResourceBundle resBundle = ResourceBundle.getBundle(
            "dg/hipster/resource/strings");
    /**
     * Undo controller.
     */
    private UndoManager undoManager = new UndoManager();

    private Idea idea;
    private File currentFile;
    private boolean dirty;
    private String title;
    private boolean needsAdjustment;
    private Idea selected;

    public IdeaDocument() {
        this.setCurrentFile(null);
        setTitle("Untitled");
        this.setIdea(new Idea(this.getTitle()));
        this.setDirty(false);
        this.setSelected(this.getIdea());
    }

    public void setIdea(Idea newIdea) {
        Idea oldIdea = this.idea;
        if (oldIdea != null) {
            oldIdea.removeIdeaListener(this);
        }
        this.idea = newIdea;
        if (this.idea != null) {
            newIdea.addIdeaListener(this);
        }
        undoManager.setIdea(newIdea);
        firePropertyChange("idea", oldIdea, this.idea);
        setSelected(newIdea);
    }

    public Idea getIdea() {
        return this.idea;
    }

    public File getCurrentFile() {
        return currentFile;
    }

    public void setCurrentFile(File newCurrentFile) {
        File oldFile = this.currentFile;
        this.currentFile = newCurrentFile;
        if (this.currentFile == null) {
            this.setTitle(resBundle.getString("untitled"));
        } else {
            this.setTitle(newCurrentFile.getAbsolutePath());
        }
        firePropertyChange("currentFile", oldFile, this.currentFile);
    }

    public boolean isDirty() {
        return dirty;
    }

    public void setDirty(boolean dirty) {
        boolean oldDirty = this.dirty;
        this.dirty = dirty;
        firePropertyChange("dirty", oldDirty, this.dirty);
    }

    public String getTitle() {
        return this.title;
    }

    public void setTitle(String newTitle) {
        String oldTitle = this.title;
        this.title = newTitle;
        firePropertyChange("title", oldTitle, this.title);
    }

    public void ideaChanged(IdeaEvent ideaEvent) {
        this.setDirty(true);
        if (this.currentFile == null) {
            setTitle(this.getIdea().getText());
        }
        setNeedsAdjustment(true);
    }

    public void setNeedsAdjustment(boolean adjustment) {
        this.needsAdjustment = adjustment;
    }

    public boolean isNeedsAdjustment() {
        return this.needsAdjustment;
    }

    public Idea getSelected() {
        return selected;
    }

    public void setSelected(Idea newSelected) {
        Idea oldSelected = this.selected;
        if (this.selected != null) {
            this.selected.setSelected(false);
        }
        this.selected = newSelected;
        if (this.selected != null) {
            this.selected.setSelected(true);
        }
        firePropertyChange("selected", oldSelected, this.selected);
    }

    /**
     * Delete the currently selected idea (and consequently
     * its child ideas).
     */
    public void deleteSelected() {
        if (selected == null) {
            return;
        }
        Idea parent = this.idea.getParentFor(selected);
        if (parent == null) {
            return;
        }
        if (selected instanceof IdeaLink) {
            parent.removeLink((IdeaLink)selected);
            this.setSelected(parent);
            return;
        }
        Idea nextToSelect = null;
        Idea nextSibling = getNextSibling(selected);
        Idea previousSibling = getPreviousSibling(selected);
        if (nextSibling != null) {
            nextToSelect = nextSibling;
        } else if (previousSibling != null) {
            nextToSelect = previousSibling;
        } else {
            nextToSelect = parent;
        }
        parent.remove(selected);
        this.setSelected(nextToSelect);
    }

    public Idea getPreviousSibling(Idea i) {
        return getSibling(i, -1);
    }

    public Idea getNextSibling(Idea i) {
        return getSibling(i, +1);
    }

    public Idea getSibling(Idea i, int difference) {
        Idea parent = idea.getParentFor(i);
        if (parent == null) {
            return null;
        }
        int pos = parent.getSubIdeas().indexOf(i);
        int subCount = parent.getSubIdeas().size();
        int diff = difference % subCount;
        int siblingPos = pos + diff;
        if ((diff == 0) || (siblingPos < 0) || (siblingPos > (subCount - 1))) {
            return null;
        }
        siblingPos = (siblingPos + subCount) % subCount;
        return parent.getSubIdeas().get(siblingPos);
    }

    /**
     * Undo the last change.
     */
    public void undo() {
        if (undoManager != null) {
            undoManager.undo();
            this.setSelected(null);
        }
    }

    /**
     * Redo the last change undone.
     */
    public void redo() {
        if (undoManager != null) {
            undoManager.redo();
            this.setSelected(null);
        }
    }
} public class IdeaLink extends Idea {
    private Idea from;
    private Idea to;

    /**
     * Creates a new instance of IdeaLink
     */
    public IdeaLink(Idea fromIdea, Idea toIdea) {
        super();
        this.from = fromIdea;
        this.to = toIdea;
        if (this.to != null) {
            this.to.addIdeaListener(this);
        }
    }

    public void ideaChanged(IdeaEvent ideaEvent) {
        if ((ideaEvent.getIdea().equals(this.to)) && (ideaEvent.getActionCommand().equals("REMOVED"))) {
            this.from.removeLink(this);
        }
        super.ideaChanged(ideaEvent);
    }

    public Idea getFrom() {
        return from;
    }

    public Idea getTo() {
        return to;
    }

    public String toString() {
        return from + " -> " + to;
    }
}
 public final class Settings {
    private static Preferences prefs = Preferences.userNodeForPackage(
            Settings.class);
    private final static String WINDOW_TOP = "windowTop";
    private final static String WINDOW_LEFT = "windowLeft";
    private final static String WINDOW_WIDTH = "windowWidth";
    private final static String WINDOW_HEIGHT = "windowHeight";

    private static Settings settings = new Settings();

    private static Dimension screenSize = Toolkit.getDefaultToolkit(
            ).getScreenSize();
    private PropertyChangeSupport pcs = new PropertyChangeSupport(this);

    public void addPropertyChangeListener(
            PropertyChangeListener listener
            ) {
        pcs.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(
            PropertyChangeListener listener
            ) {
        pcs.removePropertyChangeListener(listener);
    }

    public synchronized void addPropertyChangeListener(
            String propertyName,
            PropertyChangeListener listener
            ) {
        pcs.addPropertyChangeListener(propertyName, listener);
    }

    public synchronized void removePropertyChangeListener(
            String propertyName,
            PropertyChangeListener listener
            ) {
        pcs.removePropertyChangeListener(propertyName, listener);
    }

    public void firePropertyChange(String propertyName,
            Object oldValue, Object newValue) {
        pcs.firePropertyChange(propertyName, oldValue, newValue);
    }

    public void firePropertyChange(String propertyName,
            int oldValue, int newValue) {
        pcs.firePropertyChange(propertyName, oldValue, newValue);
    }

    /** Creates a new instance of Settings */
    private Settings() {
    }

    /**
     * Get an instance of the settings bean.
     * @return instance of the settings bean
     */
    public static Settings getInstance() {
        return settings;
    }

    /**
     * y-coordinate of the top-left corner of the main window.
     * @return y-coordinate of the top-left corner of the main window.
     */
    public int getWindowTop() {
        int dflt = (screenSize.height - getWindowHeight()) / 8;
        return prefs.getInt(WINDOW_TOP, dflt);
    }

    /**
     * Set y-coordinate of the top-left corner of the main window.
     * @param value y-coordinate of the top-left corner of the main window.
     */
    public void setWindowTop(int value) {
        int oldValue = getWindowTop();
        prefs.putInt(WINDOW_TOP, value);
        firePropertyChange("windowTop", oldValue, value);
    }

    /**
     * x-coordinate of the top-left corner of the main window.
     * @return x-coordinate of the top-left corner of the main window.
     */
    public int getWindowLeft() {
        int dflt = (screenSize.width - getWindowWidth()) / 5;
        return prefs.getInt(WINDOW_LEFT, dflt);
    }

    /**
     * Set the x-coordinate of the top-left corner of the main window.
     * @param value x-coordinate of the top-left corner of the main window.
     */
    public void setWindowLeft(int value) {
        int oldValue = getWindowLeft();
        prefs.putInt(WINDOW_LEFT, value);
        firePropertyChange("windowLeft", oldValue, value);
    }

    /**
     * Width of the main window.
     * @return width of the main window.
     */
    public int getWindowWidth() {
        int dflt = 3 * screenSize.width / 5;
        return prefs.getInt(WINDOW_WIDTH, dflt);
    }

    /**
     * Set the width of the main window.
     * @param value width of the main window.
     */
    public void setWindowWidth(int value) {
        int oldValue = getWindowWidth();
        prefs.putInt(WINDOW_WIDTH, value);
        firePropertyChange("windowWidth", oldValue, value);
    }

    /**
     * Height of the main window.
     * @return height of the main window.
     */
    public int getWindowHeight() {
        int dflt = 3 * screenSize.height / 4;
        return prefs.getInt(WINDOW_HEIGHT, dflt);
    }

    /**
     * Set the height of the main window.
     * @param value height of the main window.
     */
    public void setWindowHeight(int value) {
        int oldValue = getWindowHeight();
        prefs.putInt(WINDOW_HEIGHT, value);
        firePropertyChange("windowHeight", oldValue, value);
    }
} public final class XMLMenuBar extends JMenuBar {
    /**
     * Internationalization strings.
     */
    private ResourceBundle resBundle;

    private Map<String, JMenu> menus = new HashMap<String, JMenu>();
    private Map<String, JMenuItem> menuItems = new HashMap<String, JMenuItem>();
    private Controller controller;
    private Object backingBean;
//    private JMenuBar menuBar;

    public XMLMenuBar(Object aBean, String viewXML, ResourceBundle resBundle) {
        super();
        this.backingBean = aBean;
        controller = new Controller(null);
        this.resBundle = resBundle;

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        InputStream in = null;
        try {
            in = XMLMenuBar.class.getResourceAsStream(viewXML);
            Document document = factory.newDocumentBuilder().parse(in);

            Element docMenuBar = (Element) document.getElementsByTagName(
                    "menuBar").item(0);

            //menuBar = new JMenuBar();

            NodeList menuList = docMenuBar.getElementsByTagName("menu");
            for (int i = 0; i < menuList.getLength(); i++) {
                Element menuElement = (Element) menuList.item(i);
                String menuName = menuElement.getTagName();
                String menuTextName = menuElement.getAttribute("textName");
                String menuText = resBundle.getString("menu." + menuTextName);
                JMenu aMenu = new JMenu(menuText);
                menus.put(menuTextName, aMenu);
                add(aMenu);
                NodeList itemList = menuElement.getChildNodes();
                int col = 0;
                for (int j = 0; j < itemList.getLength(); j++) {
                    Node itemNode = (Node) itemList.item(j);
                    if (itemNode instanceof Element) {
                        Element itemElement = (Element) itemNode;
                        String itemName = itemElement.getTagName();
                        String itemTextName = itemElement.getAttribute("textName");
                        String action = itemElement.getAttribute("action");
                        int keyStroke = 0;
                        String key = itemElement.getAttribute("key");
                        if ((key != null) && (key.length() > 0)) {
                            Class keyEventClass = KeyEvent.class;
                            Field ks = keyEventClass.getDeclaredField("VK_" + key);
                            keyStroke = ks.getInt(new Integer(0));
                        }
                        createItem(itemTextName, aMenu, action, keyStroke);
                    }
                }
            }

        } catch(Exception e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch(IOException ioe) {
                }
            }
        }
    }

    public JMenuBar getMenuBar() {
        return this;
    }

    public void createMenus(final JMenuBar menu, Object[][] params) {
        for(Object[] p: params) {
            String menuName = (String) p[0];
            createMenu(menuName, menu, (Object[][])p[1]);
        }
    }

    public void createMenu(final String name, final JMenuBar menu, Object[][] params) {
        String text = resBundle.getString("menu." + name);
        JMenu aMenu = new JMenu(text);
        menus.put(name, aMenu);
        menu.add(aMenu);
        for (Object[] p: params) {
            if (p.length == 3) {
                createItem((String) p[0], aMenu, (String) p[1], (Integer) p[2]);
            } else if ("-".equals(p[0])) {
                aMenu.addSeparator();
            } else {
                createItem((String) p[0], aMenu, (String) p[1]);
            }
        }
    }

    public void createItem(String name, JMenu menu, String action) {
        createItem(name, menu, action, 0);
    }

    public void createItem(String name, JMenu menu, String action, Integer keyStroke) {
        KeyStroke ks = null;
        if (keyStroke != 0) {
            ks = KeyStroke.getKeyStroke(keyStroke,
                    Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
        }
        createItem(name, menu, action, ks);
    }

    public void createItem(String name, JMenu menu, String action, KeyStroke keyStroke) {
        String text = "-";
        if (!"-".equals(name)) {
            text = resBundle.getString("menu." + name);
        } else {
            menu.addSeparator();
            return;
        }
        JMenuItem item = new JMenuItem(text);
        menu.add(item);
        if (keyStroke != null) {
            item.setAccelerator(keyStroke);
        }
        menuItems.put(name, item);
        controller.bindMethod(backingBean, action, item);
    }

    public JMenu getMenu(String name) {
        return menus.get(name);
    }

    public JMenuItem getItem(String name) {
        return menuItems.get(name);
    }
}
import dg.hipster.BrowserLauncher;
import dg.hipster.Main;
import dg.hipster.Utilities;
import dg.hipster.io.ReaderException;
import dg.hipster.io.ReaderFactory;
import dg.hipster.io.WriterFactory;
import dg.hipster.io.WikiWriter;
import static dg.hipster.io.WikiWriter.wikiIdea;
import dg.hipster.model.Idea;
import dg.hipster.model.IdeaDocument;
import dg.hipster.model.IdeaLink;
import dg.hipster.model.Settings;
import dg.inx.XMLMenuBar;
import java.awt.BorderLayout;
import java.awt.FileDialog;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URLEncoder;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
    }
    
    public void saveDocument() throws IOException, ReaderException {
        saveDocument(this.getDocument(), this.getDocument().getCurrentFile());
    }
    
    public void saveDocument(IdeaDocument document, File f)
    throws IOException, ReaderException {
        File file = f;
        if (file == null) {
            String filename = document.getTitle();
            if (!filename.toUpperCase().endsWith(".OPML")) {
                filename += ".opml";
            }
            int pos = filename.lastIndexOf(File.separatorChar);
            FileDialog chooser = new FileDialog(this,
                    resBundle.getString("save.opml.file"),
                    FileDialog.SAVE);
            chooser.setFile(filename.substring(pos + 1));
            
            chooser.setVisible(true);
            
            if (chooser.getFile() != null) {
                file = new File(chooser.getDirectory() + chooser.getFile());
            }
        }
        
        
        if (file != null) {
            Idea idea = document.getIdea();
            WriterFactory.getInstance().write(file, document, false);
        }
    }
    
    public void exportWiki()
    throws IOException, ReaderException {
        File file = null;
        if (file == null) {
            String filename = this.getDocument().getTitle();
            if (!filename.toUpperCase().endsWith(".TXT")) {
                filename += ".txt";
            }
            int pos = filename.lastIndexOf(File.separatorChar);
            FileDialog chooser = new FileDialog(this,
                    resBundle.getString("export.wiki.file"),
                    FileDialog.SAVE);
            chooser.setFile(filename.substring(pos + 1));
            
            chooser.setVisible(true);
            
            if (chooser.getFile() != null) {
                file = new File(chooser.getDirectory() + chooser.getFile());
            }
        }
        
        
        if (file != null) {
            Idea idea = this.getDocument().getIdea();
            WriterFactory.getInstance().write(file, this.getDocument(), true);
        }
    }
    
    public boolean checkIfSave() throws IOException, ReaderException {
        ideaMap.stopAdjust();
        IdeaDocument doc = getDocument();
        if (doc != null) {
            if (doc.isDirty()) {
                int answer = JOptionPane.showConfirmDialog(this,
                        resBundle.getString("save_file_question"),
                        resBundle.getString("app.name"),
                        JOptionPane.YES_NO_CANCEL_OPTION);
                if (answer == JOptionPane.YES_OPTION) {
                    saveDocument();
                } else if (answer == JOptionPane.CANCEL_OPTION) {
                    return false;
                }
            }
        }
        return true;
    }
    
    public void fileExit() {
        System.exit(0);
    }
    
    public void editPreferences() {
        Main.showPreferences();
    }
    
    public void helpAbout() {
        Main.showAbout();
    }
    
    public void helpManual() {
        if (Main.isMac()) {
            try {
                Class HelpBook = Class.forName("dg.hipster.HelpBook");
                Method launchHelpViewer = HelpBook.getMethod(
                        "launchHelpViewer");
                launchHelpViewer.invoke(new Integer(10));
            } catch(Exception cnfe) {
                cnfe.printStackTrace();
            }
        } else {
            String pwd = System.getProperty("launch4j.exedir");
            if ((pwd == null) || (pwd.length() == 0)) {
                pwd = System.getProperty("hipster.library.dir");
            }
            if ((pwd == null) || (pwd.length() == 0)) {
                pwd = System.getProperty("user.dir");
            }
            String manualIndex = pwd + File.separatorChar + "manual"
                    + File.separatorChar + "English" + File.separatorChar
                    + "index.html";
            showUrlInWindows((new File(manualIndex)).toString());
        }
    }
    
    private void showUrlInWindows(String u) {
        try {
            BrowserLauncher.openURL((new File(u)).toURL().toString());