public void mouseMoved(MouseEvent e) {
        Element elem = textPane.getStyledDocument().getCharacterElement(textPane.viewToModel(e.getPoint()));
        AttributeSet as = elem.getAttributes();
        if (StyleConstants.isUnderline(as))
            textPane.setCursor(new Cursor(Cursor.HAND_CURSOR));
        else
            textPane.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
    }
}




/** Extracts the hyperlink text when clicking a hyperlink */
class TextClickListener extends MouseAdapter {
    JTextPane textPane;
    Connection connection;

    public TextClickListener(JTextPane textPane, Connection connection) {
        this.textPane = textPane;
        this.connection = connection;
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        try {
            Element elem = textPane.getStyledDocument().getCharacterElement(textPane.viewToModel(e.getPoint()));
            boolean contains = elem.getAttributes().containsAttributes(GUI.hyperlinkUnclickedStyle);
            if (contains) {
                StyledDocument document = textPane.getStyledDocument();
                Point pt = new Point(e.getX(), e.getY());
                int originalClickPoint = textPane.viewToModel(pt);
                boolean isURL = true;
                String URLString = "";
                int backwardsClickPoint = originalClickPoint - 1;
                while (true) {
                    elem = document.getCharacterElement(originalClickPoint);
                    if (elem.getAttributes().containsAttributes(GUI.hyperlinkUnclickedStyle)) {
                        URLString = URLString + (document.getText(originalClickPoint, 1));
                    } else break;
                    originalClickPoint++;
                }

                while (isURL) {
                    elem = document.getCharacterElement(backwardsClickPoint);
                    if (elem.getAttributes().containsAttributes(GUI.hyperlinkUnclickedStyle)) {
                        URLString = document.getText(backwardsClickPoint, 1) + URLString;
                    } else isURL = false;
                    backwardsClickPoint--;
                }
                URLLinkAction linkAction = new URLLinkAction(URLString, connection);
                linkAction.execute();
            }
        } catch (BadLocationException | IOException ignored) {
        }
    }
}



/** Handles the action to take when a hyperlink is clicked. i.e. open a new channel, open browser, etc */
class URLLinkAction extends AbstractAction {
    private String url;
    private Connection connection;

    URLLinkAction(String bac, Connection connection) {
        url = bac;
        this.connection = connection;
    }

    protected void execute() throws IOException {
        if (url.startsWith("#"))
            try {
                connection.send("JOIN " + url);
            } catch (IOException | BadLocationException ex) {
                Logger.getLogger(URLLinkAction.class.getName()).log(Level.SEVERE, null, ex);
            }
        else {
            String osName = System.getProperty("os.name").toLowerCase();
            Runtime rt = Runtime.getRuntime();
            if (osName.contains("win")) rt.exec("rundll32 url.dll,FileProtocolHandler " + url);
            else if (osName.contains("mac")) rt.exec("open " + url);
            else if (osName.contains("ix") || osName.contains("ux") || osName.contains("sun")) {
                String[] browsers = {"epiphany", "firefox", "mozilla", "konqueror", "netscape", "opera", "links", "lynx"};

                // Build a command string which looks like "browser1 "url" || browser2 "url" ||..."
                StringBuilder cmd = new StringBuilder();
                for (int i = 0; i < browsers.length; i++)
                    cmd.append(i == 0 ? "" : " || ").append(browsers[i]).append(" \"").append(url).append("\" ");

                rt.exec(new String[]{"sh", "-c", cmd.toString()});
            }
        }
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        try {
            execute();
        } catch (IOException e1) {
            e1.printStackTrace();
        }
    }
}






/*
 *  A class to control the maximum number of lines to be stored in a Document
 *
 *  Excess lines can be removed from the start or end of the Document
 *  depending on your requirement.
 *
 *  a) if you append text to the Document, then you would want to remove lines
 *     from the start.
 *  b) if you insert text at the beginning of the Document, then you would
 *     want to remove lines from the end.
 */
class LimitLinesDocumentListener implements DocumentListener
{
    private int maximumLines;
    private boolean isRemoveFromStart;

    /*
     *  Specify the number of lines to be stored in the Document.
     *  Extra lines will be removed from the start of the Document.
     */
    public LimitLinesDocumentListener(int maximumLines)
    {
        this(maximumLines, true);
    }

    /*
     *  Specify the number of lines to be stored in the Document.
     *  Extra lines will be removed from the start or end of the Document,
     *  depending on the boolean value specified.
     */
    public LimitLinesDocumentListener(int maximumLines, boolean isRemoveFromStart)
    {
        setLimitLines(maximumLines);
        this.isRemoveFromStart = isRemoveFromStart;
    }

    /*
     *  Return the maximum number of lines to be stored in the Document
     */
    public int getLimitLines()
    {
        return maximumLines;
    }

    /*
     *  Set the maximum number of lines to be stored in the Document
     */
    public void setLimitLines(int maximumLines)
    {
        if (maximumLines < 1)
        {
            String message = "Maximum lines must be greater than 0";
            throw new IllegalArgumentException(message);
        }

        this.maximumLines = maximumLines;
    }

    //  Handle insertion of new text into the Document

    public void insertUpdate(final DocumentEvent e)
    {
        //  Changes to the Document can not be done within the listener
        //  so we need to add the processing to the end of the EDT

        SwingUtilities.invokeLater( new Runnable()
        {
            public void run()
            {
                removeLines(e);
            }
        });
    }

    public void removeUpdate(DocumentEvent e) {}
    public void changedUpdate(DocumentEvent e) {}

    /*
     *  Remove lines from the Document when necessary
     */
    private void removeLines(DocumentEvent e)
    {
        //  The root Element of the Document will tell us the total number
        //  of line in the Document.

        Document document = e.getDocument();
        Element root = document.getDefaultRootElement();

        while (root.getElementCount() > maximumLines)
        {
            if (isRemoveFromStart)
            {
                removeFromStart(document, root);
            }
            else
            {
                removeFromEnd(document, root);
            }
        }
    }

    /*
     *  Remove lines from the start of the Document
     */
    private void removeFromStart(Document document, Element root)
    {
        Element line = root.getElement(0);
        int end = line.getEndOffset();

        try
        {
            document.remove(0, end);
        }
        catch(BadLocationException ble)
        {
            System.out.println(ble);
        }
    }

    /*
     *  Remove lines from the end of the Document
     */
    private void removeFromEnd(Document document, Element root)
    {
        //  We use start minus 1 to make sure we remove the newline
        //  character of the previous line

        Element line = root.getElement(root.getElementCount() - 1);
        int start = line.getStartOffset();
        int end = line.getEndOffset();

        try
        {
            document.remove(start - 1, end - start);
        }
        catch(BadLocationException ble)
        {
            System.out.println(ble);
        }
    }
}


/**
 * From http://stackoverflow.com/questions/8666727/wrap-long-words-in-jtextpane-java-7
 */
class WrapEditorKit extends StyledEditorKit {
    ViewFactory defaultFactory=new WrapColumnFactory();
    public ViewFactory getViewFactory() {
        return defaultFactory;
    }

}

class WrapColumnFactory implements ViewFactory {
    public View create(Element elem) {
        String kind = elem.getName();
        if (kind != null) {
            if (kind.equals(AbstractDocument.ContentElementName)) {
                return new WrapLabelView(elem);
            } else if (kind.equals(AbstractDocument.ParagraphElementName)) {
                return new ParagraphView(elem);
            } else if (kind.equals(AbstractDocument.SectionElementName)) {
                return new BoxView(elem, View.Y_AXIS);
            } else if (kind.equals(StyleConstants.ComponentElementName)) {
                return new ComponentView(elem);
            } else if (kind.equals(StyleConstants.IconElementName)) {
                return new IconView(elem);
            }
        }

        // default to text display
        return new LabelView(elem);
    }
}

class WrapLabelView extends LabelView {
    public WrapLabelView(Element elem) {
        super(elem);
    }

    public float getMinimumSpan(int axis) {
        switch (axis) {
            case View.X_AXIS:
                return 0;
            case View.Y_AXIS:
                return super.getMinimumSpan(axis);
            default:
                throw new IllegalArgumentException("Invalid axis: " + axis);
        }
    }