/*
 * Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package org.graalvm.visualvm.uisupport;

import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
import java.util.Hashtable;
import java.util.Map;
import java.util.ResourceBundle;
import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.StyleConstants;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.HTMLWriter;
import org.graalvm.visualvm.lib.ui.UIUtils;


/**
 * Copy of org.graalvm.visualvm.lib.ui.components.HTMLTextArea to be used in
 * VisualVM tool an plugins.
 * 
 * NOTE: out of date, the original code contains additional improvements and bugfixes
 *
 * @author Jiri Sedlacek
 */
public class HTMLTextArea extends JEditorPane implements HyperlinkListener, MouseListener {
    //~ Inner Classes ------------------------------------------------------------------------------------------------------------

    /** Private Writer that extracts correctly formatted string from HTMLDocument */
    private static class ExtendedHTMLWriter extends HTMLWriter {
        //~ Constructors ---------------------------------------------------------------------------------------------------------

        public ExtendedHTMLWriter(Writer w, HTMLDocument doc, int pos, int len) {
            super(w, doc, pos, len);
            setLineLength(Integer.MAX_VALUE);
        }

        //~ Methods --------------------------------------------------------------------------------------------------------------

        protected boolean isSupportedBreakFlowTag(AttributeSet attr) {
            Object o = attr.getAttribute(StyleConstants.NameAttribute);

            if (o instanceof HTML.Tag) {
                HTML.Tag tag = (HTML.Tag) o;

                if ((tag == HTML.Tag.HTML) || (tag == HTML.Tag.HEAD) || (tag == HTML.Tag.BODY) || (tag == HTML.Tag.HR)) {
                    return false;
                }

                return (tag).breaksFlow();
            }

            return false;
        }

        protected void emptyTag(Element elem) throws BadLocationException, IOException {
            if (isSupportedBreakFlowTag(elem.getAttributes())) {
                writeLineSeparator();
            }

            if (matchNameAttribute(elem.getAttributes(), HTML.Tag.CONTENT)) {
                text(elem);
            }
        }

        protected void endTag(Element elem) throws IOException {
            if (isSupportedBreakFlowTag(elem.getAttributes())) {
                writeLineSeparator();
            }
        }

        protected void startTag(Element elem) throws IOException, BadLocationException {
        }
    }

    // --- Private classes for copy/paste support --------------------------------
    //
    // NOTE: only vertical formatting is correctly copy/pasted,
    //       horizontal formatting (ul, li) is ignored.

    /** Private TransferHandler that copies correctly formatted string from HTMLDocument to system clipboard */
    private class HTMLTextAreaTransferHandler extends TransferHandler {
        //~ Methods --------------------------------------------------------------------------------------------------------------

        public void exportToClipboard(JComponent comp, Clipboard clip, int action) {
            try {
                int selStart = getSelectionStart();
                int selLength = getSelectionEnd() - selStart;

                StringWriter plainTextWriter = new StringWriter();

                try {
                    new ExtendedHTMLWriter(plainTextWriter, (HTMLDocument) getDocument(), selStart, selLength).write();
                } catch (Exception e) {
                }

                String plainText = NcrToUnicode.decode(plainTextWriter.toString());
                clip.setContents(new StringSelection(plainText), null);

                if (action == TransferHandler.MOVE) {
                    getDocument().remove(selStart, selLength);
                }
            } catch (BadLocationException ble) {
            }
        }
    }

    /** Class for decoding strings from NCR to Unicode */
    private static class NcrToUnicode {
        //~ Static fields/initializers -------------------------------------------------------------------------------------------

        private static Map entities;

        //~ Methods --------------------------------------------------------------------------------------------------------------

        public static String decode(String str) {
            StringBuilder ostr = new StringBuilder();
            int i1 = 0;
            int i2 = 0;

            while (i2 < str.length()) {
                i1 = str.indexOf('&', i2); //NOI18N

                if (i1 == -1) {
                    ostr.append(str.substring(i2, str.length()));

                    break;
                }

                ostr.append(str.substring(i2, i1));
                i2 = str.indexOf(';', i1); //NOI18N

                if (i2 == -1) {
                    ostr.append(str.substring(i1, str.length()));

                    break;
                }

                String tok = str.substring(i1 + 1, i2);

                if (tok.charAt(0) == '#') { //NOI18N

                    if (tok.equals("#160")) { //NOI18N
                        ostr.append((String) getEntities().get("nbsp")); //NOI18N // Fixes Issue 92818, "&nbsp;" is resolved as "&#160;" before decoding, so redirecting back to "&nbsp;"
                    } else {
                        tok = tok.substring(1);

                        try {
                            int radix = 10;

                            if (tok.trim().charAt(0) == 'x') { //NOI18N
                                radix = 16;
                                tok = tok.substring(1, tok.length());
                            }

                            ostr.append((char) Integer.parseInt(tok, radix));
                        } catch (NumberFormatException exp) {
                            ostr.append('?'); //NOI18N
                        }
                    }
                } else {
                    tok = (String) getEntities().get(tok);

                    if (tok != null) {
                        ostr.append(tok);
                    } else {
                        ostr.append('?'); //NOI18N
                    }
                }

                i2++;
            }

            return ostr.toString();
        }

        private static synchronized Map getEntities() {
            if (entities == null) {
                entities = new Hashtable();
                //Quotation mark
                entities.put("quot", "\""); //NOI18N
                                            //Ampersand

                entities.put("amp", "\u0026"); //NOI18N
                                               //Less than

                entities.put("lt", "\u003C"); //NOI18N
                                              //Greater than

                entities.put("gt", "\u003E"); //NOI18N
                                              //Nonbreaking space

                entities.put("nbsp", "\u0020"); //NOI18N // Fixes Issue 92818, "\u00A0" (&nbsp; equivalent) is resolved as incorrect character, thus mapping to standard space
                                                //Inverted exclamation point

                entities.put("iexcl", "\u00A1"); //NOI18N
                                                 //Cent sign

                entities.put("cent", "\u00A2"); //NOI18N
                                                //Pound sign

                entities.put("pound", "\u00A3"); //NOI18N
                                                 //General currency sign

                entities.put("curren", "\u00A4"); //NOI18N
                                                  //Yen sign

                entities.put("yen", "\u00A5"); //NOI18N
                                               //Broken vertical bar

                entities.put("brvbar", "\u00A6"); //NOI18N
                                                  //Section sign

                entities.put("sect", "\u00A7"); //NOI18N
                                                //Umlaut

                entities.put("uml", "\u00A8"); //NOI18N
                                               //Copyright

                entities.put("copy", "\u00A9"); //NOI18N
                                                //Feminine ordinal

                entities.put("ordf", "\u00AA"); //NOI18N
                                                //Left angle quote

                entities.put("laquo", "\u00AB"); //NOI18N
                                                 //Not sign

                entities.put("not", "\u00AC"); //NOI18N
                                               //Soft hyphen

                entities.put("shy", "\u00AD"); //NOI18N
                                               //Registered trademark

                entities.put("reg", "\u00AE"); //NOI18N
                                               //Macron accent

                entities.put("macr", "\u00AF"); //NOI18N
                                                //Degree sign

                entities.put("deg", "\u00B0"); //NOI18N
                                               //Plus or minus

                entities.put("plusmn", "\u00B1"); //NOI18N
                                                  //Superscript 2

                entities.put("sup2", "\u00B2"); //NOI18N
                                                //Superscript 3

                entities.put("sup3", "\u00B3"); //NOI18N
                                                //Acute accent

                entities.put("acute", "\u00B4"); //NOI18N
                                                 //Micro sign (Greek mu)

                entities.put("micro", "\u00B5"); //NOI18N
                                                 //Paragraph sign

                entities.put("para", "\u00B6"); //NOI18N
                                                //Middle dot

                entities.put("middot", "\u00B7"); //NOI18N
                                                  //Cedilla

                entities.put("cedil", "\u00B8"); //NOI18N
                                                 //Superscript 1

                entities.put("sup1", "\u00B9"); //NOI18N
                                                //Masculine ordinal

                entities.put("ordm", "\u00BA"); //NOI18N
                                                //Right angle quote

                entities.put("raquo", "\u00BB"); //NOI18N
                                                 //Fraction one-fourth

                entities.put("frac14", "\u00BC"); //NOI18N
                                                  //Fraction one-half

                entities.put("frac12", "\u00BD"); //NOI18N
                                                  //Fraction three-fourths

                entities.put("frac34", "\u00BE"); //NOI18N
                                                  //Inverted question mark

                entities.put("iquest", "\u00BF"); //NOI18N
                                                  //Capital A, grave accent

                entities.put("Agrave", "\u00C0"); //NOI18N
                                                  //Capital A, acute accent

                entities.put("Aacute", "\u00C1"); //NOI18N
                                                  //Capital A, circumflex accent

                entities.put("Acirc", "\u00C2"); //NOI18N
                                                 //Capital A, tilde

                entities.put("Atilde", "\u00C3"); //NOI18N
                                                  //Capital A, umlaut

                entities.put("Auml", "\u00C4"); //NOI18N
                                                //Capital A, ring

                entities.put("Aring", "\u00C5"); //NOI18N
                                                 //Capital AE ligature

                entities.put("AElig", "\u00C6"); //NOI18N
                                                 //Capital C, cedilla

                entities.put("Ccedil", "\u00C7"); //NOI18N
                                                  //Capital E, grave accent

                entities.put("Egrave", "\u00C8"); //NOI18N
                                                  //Capital E, acute accent

                entities.put("Eacute", "\u00C9"); //NOI18N
                                                  //Capital E, circumflex accent

                entities.put("Ecirc", "\u00CA"); //NOI18N
                                                 //Capital E, umlaut

                entities.put("Euml", "\u00CB"); //NOI18N
                                                //Capital I, grave accent

                entities.put("Igrave", "\u00CC"); //NOI18N
                                                  //Capital I, acute accent

                entities.put("Iacute", "\u00CD"); //NOI18N
                                                  //Capital I, circumflex accent

                entities.put("Icirc", "\u00CE"); //NOI18N
                                                 //Capital I, umlaut

                entities.put("Iuml", "\u00CF"); //NOI18N
                                                //Capital eth, Icelandic

                entities.put("ETH", "\u00D0"); //NOI18N
                                               //Capital N, tilde

                entities.put("Ntilde", "\u00D1"); //NOI18N
                                                  //Capital O, grave accent

                entities.put("Ograve", "\u00D2"); //NOI18N
                                                  //Capital O, acute accent

                entities.put("Oacute", "\u00D3"); //NOI18N
                                                  //Capital O, circumflex accent

                entities.put("Ocirc", "\u00D4"); //NOI18N
                                                 //Capital O, tilde

                entities.put("Otilde", "\u00D5"); //NOI18N
                                                  //Capital O, umlaut

                entities.put("Ouml", "\u00D6"); //NOI18N
                                                //Multiply sign

                entities.put("times", "\u00D7"); //NOI18N
                                                 //Capital O, slash

                entities.put("Oslash", "\u00D8"); //NOI18N
                                                  //Capital U, grave accent

                entities.put("Ugrave", "\u00D9"); //NOI18N
                                                  //Capital U, acute accent

                entities.put("Uacute", "\u00DA"); //NOI18N
                                                  //Capital U, circumflex accent

                entities.put("Ucirc", "\u00DB"); //NOI18N
                                                 //Capital U, umlaut

                entities.put("Uuml", "\u00DC"); //NOI18N
                                                //Capital Y, acute accent

                entities.put("Yacute", "\u00DD"); //NOI18N
                                                  //Capital thorn, Icelandic

                entities.put("THORN", "\u00DE"); //NOI18N
                                                 //Small sz ligature, German

                entities.put("szlig", "\u00DF"); //NOI18N
                                                 //Small a, grave accent

                entities.put("agrave", "\u00E0"); //NOI18N
                                                  //Small a, acute accent

                entities.put("aacute", "\u00E1"); //NOI18N
                                                  //Small a, circumflex accent

                entities.put("acirc", "\u00E2"); //NOI18N
                                                 //Small a, tilde

                entities.put("atilde", "\u00E3"); //NOI18N
                                                  //Small a, umlaut

                entities.put("auml", "\u00E4"); //NOI18N
                                                //Small a, ring

                entities.put("aring", "\u00E5"); //NOI18N
                                                 //Small ae ligature

                entities.put("aelig", "\u00E6"); //NOI18N
                                                 //Small c, cedilla

                entities.put("ccedil", "\u00E7"); //NOI18N
                                                  //Small e, grave accent

                entities.put("egrave", "\u00E8"); //NOI18N
                                                  //Small e, acute accent

                entities.put("eacute", "\u00E9"); //NOI18N
                                                  //Small e, circumflex accent

                entities.put("ecirc", "\u00EA"); //NOI18N
                                                 //Small e, umlaut

                entities.put("euml", "\u00EB"); //NOI18N
                                                //Small i, grave accent

                entities.put("igrave", "\u00EC"); //NOI18N
                                                  //Small i, acute accent

                entities.put("iacute", "\u00ED"); //NOI18N
                                                  //Small i, circumflex accent

                entities.put("icirc", "\u00EE"); //NOI18N
                                                 //Small i, umlaut

                entities.put("iuml", "\u00EF"); //NOI18N
                                                //Small eth, Icelandic

                entities.put("eth", "\u00F0"); //NOI18N
                                               //Small n, tilde

                entities.put("ntilde", "\u00F1"); //NOI18N
                                                  //Small o, grave accent

                entities.put("ograve", "\u00F2"); //NOI18N
                                                  //Small o, acute accent

                entities.put("oacute", "\u00F3"); //NOI18N
                                                  //Small o, circumflex accent

                entities.put("ocirc", "\u00F4"); //NOI18N
                                                 //Small o, tilde

                entities.put("otilde", "\u00F5"); //NOI18N
                                                  //Small o, umlaut

                entities.put("ouml", "\u00F6"); //NOI18N
                                                //Division sign

                entities.put("divide", "\u00F7"); //NOI18N
                                                  //Small o, slash

                entities.put("oslash", "\u00F8"); //NOI18N
                                                  //Small u, grave accent

                entities.put("ugrave", "\u00F9"); //NOI18N
                                                  //Small u, acute accent

                entities.put("uacute", "\u00FA"); //NOI18N
                                                  //Small u, circumflex accent

                entities.put("ucirc", "\u00FB"); //NOI18N
                                                 //Small u, umlaut

                entities.put("uuml", "\u00FC"); //NOI18N
                                                //Small y, acute accent

                entities.put("yacute", "\u00FD"); //NOI18N
                                                  //Small thorn, Icelandic

                entities.put("thorn", "\u00FE"); //NOI18N
                                                 //Small y, umlaut

                entities.put("yuml", "\u00FF"); //NOI18N
            }

            return entities;
        }
    }

    //~ Static fields/initializers -----------------------------------------------------------------------------------------------

    // -----
    // I18N String constants
    private static final ResourceBundle messages = ResourceBundle.getBundle("org.graalvm.visualvm.lib.ui.components.Bundle"); // NOI18N
    private static final String CUT_STRING = messages.getString("HTMLTextArea_CutString"); // NOI18N
    private static final String COPY_STRING = messages.getString("HTMLTextArea_CopyString"); // NOI18N
    private static final String PASTE_STRING = messages.getString("HTMLTextArea_PasteString"); // NOI18N
    private static final String DELETE_STRING = messages.getString("HTMLTextArea_DeleteString"); // NOI18N
    private static final String SELECT_ALL_STRING = messages.getString("HTMLTextArea_SelectAllString"); // NOI18N
                                                                                                        // -----

    //~ Instance fields ----------------------------------------------------------------------------------------------------------

    private ActionListener popupListener;
    private JMenuItem itemCopy;
    private JMenuItem itemCut;
    private JMenuItem itemDelete;
    private JMenuItem itemPaste;
    private JMenuItem itemSelectAll;

    // --- Popup menu support ----------------------------------------------------
    private JPopupMenu popupMenu;
    private boolean showPopup = true;

    // --- Lazy setting text ---------------------------------------------------
    private String pendingText;
    private String currentText;
    private boolean forceSetText;
    
    //~ Constructors -------------------------------------------------------------------------------------------------------------

    public HTMLTextArea() {
        setEditorKit(new HTMLEditorKit());
        setEditable(false);
        setOpaque(true);
        setAutoscrolls(true);
        addHyperlinkListener(this);
        setTransferHandler(new HTMLTextAreaTransferHandler());
        setFont(UIManager.getFont("Label.font")); //NOI18N
        setBackground(UIUtils.getProfilerResultsBackground());
        addMouseListener(this);
        
        addKeyListener(new KeyAdapter() {
            public void keyPressed(KeyEvent e) {
                if (e.getKeyCode() == KeyEvent.VK_SPACE)
                    invokeSelectedLink();
            }
        });

        // Bugfix #185777, update text only if visible
        addHierarchyListener(new HierarchyListener() {
            public void hierarchyChanged(HierarchyEvent e) {
                if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) {
                    if (isShowing() && pendingText != null) setText(pendingText);
                }
            }
        });
    }

    public HTMLTextArea(String text) {
        this();
        setText(text);
    }

    //~ Methods ------------------------------------------------------------------------------------------------------------------
    
    public void setOpaque(boolean o) {
        super.setOpaque(o);
        if (UIUtils.isNimbusLookAndFeel() && !o)
            setBackground(new Color(0, 0, 0, 0));
    }

    public void setForeground(Color color) {
        Color foreground = getForeground();
        if (foreground != null && foreground.equals(color)) return;

        super.setForeground(color);
        forceSetText = true;
        setText(getText());
    }

    public void setShowPopup(boolean showPopup) {
        this.showPopup = showPopup;
    }

    public boolean getShowPopup() {
        return showPopup;
    }

    public void setText(String value) {
        if (value == null) return;

        if (!isShowing() && !forceSetText) {

            pendingText = value;

        } else {

            if (!forceSetText && value.equals(currentText)) return;

            currentText = value;
            pendingText = null;

            Font font = getFont();
            Color textColor = getForeground();
            value = value.replaceAll("\\n\\r|\\r\\n|\\n|\\r", "<br>"); //NOI18N
            value = value.replace("<code>", "<code style=\"font-size: " + font.getSize() + "pt;\">"); //NOI18N

            String colorText = "rgb(" + textColor.getRed() + "," + textColor.getGreen() + "," + textColor.getBlue() + ")"; //NOI18N
            String newText = "<html><body text=\"" + colorText + "\" style=\"font-size: " + font.getSize() + //NOI18N
                             "pt; font-family: " + font.getName() + ";\">" + value + "</body></html>"; //NOI18N

            setDocument(getEditorKit().createDefaultDocument()); // Workaround for http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5042872
            super.setText(newText);

        }

        forceSetText = false;
    }

    public String getText() {
        return pendingText != null ? pendingText : currentText;
    }

    public Dimension getMinimumSize() {
        if (pendingText != null) {
            forceSetText = true;
            setText(pendingText);
        }
        return super.getMinimumSize();
    }

    public Dimension getPreferredSize() {
        if (pendingText != null) {
            forceSetText = true;
            setText(pendingText);
        }
        return super.getPreferredSize();
    }

    public Dimension getMaximumSize() {
        if (pendingText != null) {
            forceSetText = true;
            setText(pendingText);
        }
        return super.getMaximumSize();
    }

    public void deleteSelection() {
        try {
            getDocument().remove(getSelectionStart(), getSelectionEnd() - getSelectionStart());
        } catch (Exception ex) {}
    }
    
    private void invokeSelectedLink() {
        for (Action action : getEditorKit().getActions()) {
            if ("activate-link-action".equals(action.getValue(Action.NAME))) {  // NOI18N
                action.actionPerformed(new ActionEvent(this, 0, null));
                return;
            }
        }
    }

    public void hyperlinkUpdate(HyperlinkEvent e) {
        if (!isEnabled()) {
            return;
        }

        if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
            showURL(e.getURL());
        } else if (e.getEventType() == HyperlinkEvent.EventType.ENTERED) {
            setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        } else if (e.getEventType() == HyperlinkEvent.EventType.EXITED) {
            setCursor(Cursor.getDefaultCursor());
        }
    }

    public void mouseClicked(MouseEvent e) {
        if (e.getModifiers() == InputEvent.BUTTON3_MASK) {
            if (isEnabled() && isFocusable() && showPopup) {
                JPopupMenu popup = getPopupMenu();

                if (popup != null) {
                    updatePopupMenu();

                    if (!hasFocus()) {
                        requestFocus(); // required for Select All functionality
                    }

                    popup.show(this, e.getX(), e.getY());
                }
            }
        }
    }

    public void mouseEntered(MouseEvent e) {
    }

    public void mouseExited(MouseEvent e) {
    }

    public void mousePressed(MouseEvent e) {
    }

    public void mouseReleased(MouseEvent e) {
    }

    public void paste() {
        try {
            replaceSelection(Toolkit.getDefaultToolkit().getSystemClipboard().getContents(this)
                                    .getTransferData(DataFlavor.stringFlavor).toString());
        } catch (Exception ex) {
        }

        ;
    }

    protected JPopupMenu getPopupMenu() {
        if (popupMenu == null) {
            popupMenu = createPopupMenu();
        }

        return popupMenu;
    }

    protected JPopupMenu createPopupMenu() {
        JPopupMenu popup = new JPopupMenu();

        popupListener = createPopupListener();

        itemCut = new JMenuItem(CUT_STRING);
        itemCopy = new JMenuItem(COPY_STRING);
        itemPaste = new JMenuItem(PASTE_STRING);
        itemDelete = new JMenuItem(DELETE_STRING);
        itemSelectAll = new JMenuItem(SELECT_ALL_STRING);

        itemCut.addActionListener(popupListener);
        itemCopy.addActionListener(popupListener);
        itemPaste.addActionListener(popupListener);
        itemDelete.addActionListener(popupListener);
        itemSelectAll.addActionListener(popupListener);

        popup.add(itemCut);
        popup.add(itemCopy);
        popup.add(itemPaste);
        popup.add(itemDelete);
        popup.addSeparator();
        popup.add(itemSelectAll);

        return popup;
    }

    protected void showURL(URL url) {
        // override to react to URL clicks
    }

    protected void updatePopupMenu() {
        // Cut
        itemCut.setEnabled(isEditable() && (getSelectedText() != null));

        // Copy
        itemCopy.setEnabled(getSelectedText() != null);

        // Paste
        try {
            Transferable clipboardContent = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(this);
            itemPaste.setEnabled(isEditable() && (clipboardContent != null)
                                 && clipboardContent.isDataFlavorSupported(DataFlavor.stringFlavor));
        } catch (Exception e) {
            itemPaste.setEnabled(false);
        }

        // Delete
        if (isEditable()) {
            itemDelete.setVisible(true);
            itemDelete.setEnabled(getSelectedText() != null);
        } else {
            itemDelete.setVisible(false);
        }

        // Select All
        // always visible and enabled...
    }

    private ActionListener createPopupListener() {
        return new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    if (e.getSource() == itemCut) {
                        cut();
                    } else if (e.getSource() == itemCopy) {
                        copy();
                    } else if (e.getSource() == itemPaste) {
                        paste();
                    } else if (e.getSource() == itemDelete) {
                        deleteSelection();
                    } else if (e.getSource() == itemSelectAll) {
                        selectAll();
                    }
                }
            };
    }
}
