package org.netbeans.modules.specialcopypaste;
import java.awt.Color;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.io.CharConversionException;
import javax.swing.JEditorPane;
import javax.swing.text.AttributeSet;
import javax.swing.text.StyleConstants;
import org.netbeans.api.editor.mimelookup.*;
import org.netbeans.api.editor.settings.FontColorSettings;
import org.openide.cookies.EditorCookie;
import org.openide.nodes.Node;
import org.openide.util.HelpCtx;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.actions.CookieAction;
import org.netbeans.api.lexer.*;
import org.openide.util.Exceptions;
import org.openide.xml.XMLUtil;
public final class CopyAsHTMLCSS extends CookieAction
{
protected final void performAction(final Node[] activatedNodes)
{
final Lookup lookup = activatedNodes[0].getLookup();
final EditorCookie editorCookie = lookup.lookup(EditorCookie.class);
// Bug lurking here. Surely there's a better way of getting the pane which currently has focus, but this seems to work
for(final JEditorPane pane : editorCookie.getOpenedPanes())
if (pane.isShowing())
try
{
StringSelection content = new StringSelection( getSelectedSourceAsHTMLCSS(pane) );
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(content, content);
return;
}
catch (Throwable e)
{
org.openide.ErrorManager.getDefault().notify(e);
}
}
private static final String getSelectedSourceAsHTMLCSS(final JEditorPane pane)
{
final MimePath mimePath = MimePath.get( pane.getContentType() );
final Lookup lookup = MimeLookup.getLookup( mimePath );
final FontColorSettings fontColorSettings = lookup.lookup( FontColorSettings.class );
final TokenHierarchy tokenHierarchy = TokenHierarchy.get( pane.getDocument() );
final TokenSequence tokenSeq = tokenHierarchy.tokenSequence();
final StringBuilder stringBuilder = new StringBuilder();
// Forward to start of selected text
tokenSeq.move( pane.getSelectionStart() );
// Process each token between start and end of selection
while(tokenSeq.moveNext() && tokenSeq.offset() < pane.getSelectionEnd() )
processToken( tokenSeq.token(), fontColorSettings, stringBuilder );
// Wrap everything in a preformated tag with a font family (also add monospace as fallback) and color span
return String.format("<pre>\n<span style=\"font-family:%s,monospace;color:%s\">\n%s\n</span>\n</pre>",
pane.getFont().getFamily(),
getHtmlColorString(pane.getForeground()),
stringBuilder.toString());
}
private static final void processToken(final Token token, final FontColorSettings fontColorSettings, final StringBuilder stringBuilder)
{
final String tokenText = escapeHtmlContent( token.text().toString() );
String category = token.id().name();
AttributeSet attribs = fontColorSettings.getTokenFontColors(category);
if (attribs == null)
{
category = token.id().primaryCategory();
attribs = fontColorSettings.getTokenFontColors(category);
}
if (attribs == null || isWhiteSpace(tokenText))
stringBuilder.append(tokenText);
else
styleAndEmitToken(tokenText, stringBuilder, attribs);
}
private static final void styleAndEmitToken(final String tokenText, final StringBuilder stringBuilder, final AttributeSet attribs)
{
final StringBuilder styleString = new StringBuilder();
final Color fg = (Color) attribs.getAttribute(StyleConstants.Foreground);
if (fg != null)
emit(styleString, "color:", getHtmlColorString(fg), ";");
final Color bg = (Color) attribs.getAttribute(StyleConstants.Background);
if (bg != null)
emit(styleString, "background:", getHtmlColorString(bg), ";");
Boolean b = (Boolean) attribs.getAttribute(StyleConstants.Bold);
if ((b != null) && b.booleanValue())
styleString.append("font-weight:bold;");
b = (Boolean) attribs.getAttribute(StyleConstants.Italic);
if ((b != null) && b.booleanValue())
styleString.append("font-style:italic;");
if (styleString.length() > 0)
emit(stringBuilder, "<span style=\"", styleString.toString(), "\">", tokenText, "</span>");
else
stringBuilder.append(tokenText);
}
/**
* Language agnostic whitespace token detector
* Nessesary since apparently there is no universal WHITESPACE tokenId
*
* @param tokenText
* @return
*/
private static final boolean isWhiteSpace(String tokenText)
{
tokenText = tokenText.replaceAll("\\n", "");
tokenText = tokenText.replaceAll("\\t", "");
return (tokenText.trim().length() == 0);
}
private static final void emit(final StringBuilder stringBuilder, final String... strings)
{
for(String string:strings)
stringBuilder.append(string);
}
private static final String escapeHtmlContent(final String content)
{
try
{
return XMLUtil.toElementContent(content);
}
catch (CharConversionException cce)
{
Exceptions.printStackTrace(cce);
}
return content;
}
private static final String getHtmlColorString(final Color color)
{
final StringBuffer result = new StringBuffer("#");
result.append( getColorComponentAsHexString( color.getRed() ));
result.append( getColorComponentAsHexString( color.getGreen() ));
result.append( getColorComponentAsHexString( color.getBlue() ));
return result.toString();
}
private static final String getColorComponentAsHexString(final int colorComponent)
{
if(colorComponent < 0x10)
return "0" + Integer.toHexString(colorComponent);
else
return Integer.toHexString(colorComponent);
}
protected final int mode()
{
return CookieAction.MODE_EXACTLY_ONE;
}
public final String getName()
{
return NbBundle.getMessage(CopyAsHTMLCSS.class, "CTL_CopyAsHTMLCSS");
}
protected final Class[] cookieClasses()
{
return new Class[]
{
EditorCookie.class
};
}
@Override
protected final void initialize()
{
super.initialize();
putValue("noIconInMenu", Boolean.TRUE);
}
public final HelpCtx getHelpCtx()
{
return HelpCtx.DEFAULT_HELP;
}
@Override
protected final boolean asynchronous()
{
return false;
}
}