Files
ByteEdit/src/org/fife/ui/rsyntaxtextarea/RtfGenerator.java
T

461 lines
13 KiB
Java

/*
* 07/28/2008
* RtfGenerator.java - Generates RTF via a simple Java API.
* This library is distributed under a modified BSD license. See the included
* RSyntaxTextArea.License.txt file for details.
*/
package org.fife.ui.rsyntaxtextarea;
import java.awt.Color;
import java.awt.Font;
import java.awt.Toolkit;
import java.util.ArrayList;
import java.util.List;
import org.fife.ui.rtextarea.RTextArea;
/**
* Generates RTF text via a simple Java API.
* <p>
*
* The following RTF features are supported:
* <ul>
* <li>Fonts
* <li>Font sizes
* <li>Foreground and background colors
* <li>Bold, italic, and underline
* </ul>
*
* The RTF generated isn't really "optimized," but it will do, especially for
* small amounts of text, such as what's common when copy-and-pasting. It
* tries to be sufficient for the use case of copying syntax highlighted
* code:
* <ul>
* <li>It assumes that tokens changing foreground color often is fairly
* common.
* <li>It assumes that background highlighting is fairly uncommon.
* </ul>
*
* @author Robert Futrell
* @version 1.0
*/
public class RtfGenerator {
private List<Font> fontList;
private List<Color> colorList;
private StringBuilder document;
private boolean lastWasControlWord;
private int lastFontIndex;
private int lastFGIndex;
private boolean lastBold;
private boolean lastItalic;
private int lastFontSize;
/**
* Java2D assumes a 72 dpi screen resolution, but on Windows the screen
* resolution is either 96 dpi or 120 dpi, depending on your font display
* settings. This is an attempt to make the RTF generated match the
* size of what's displayed in the RSyntaxTextArea.
*/
private int screenRes;
/**
* The default font size for RTF. This is point size, in half
* points.
*/
private static final int DEFAULT_FONT_SIZE = 12;// 24;
/**
* Constructor.
*/
public RtfGenerator() {
fontList = new ArrayList<Font>(1); // Usually only 1.
colorList = new ArrayList<Color>(1); // Usually only 1.
document = new StringBuilder();
reset();
}
/**
* Adds a newline to the RTF document.
*
* @see #appendToDoc(String, Font, Color, Color)
*/
public void appendNewline() {
document.append("\\par");
document.append('\n'); // Just for ease of reading RTF.
lastWasControlWord = false;
}
/**
* Appends styled text to the RTF document being generated.
*
* @param text
* The text to append.
* @param f
* The font of the text. If this is <code>null</code>, the
* default font is used.
* @param fg
* The foreground of the text. If this is <code>null</code>,
* the default foreground color is used.
* @param bg
* The background color of the text. If this is
* <code>null</code>, the default background color is used.
* @see #appendNewline()
*/
public void appendToDoc(String text, Font f, Color fg, Color bg) {
appendToDoc(text, f, fg, bg, false);
}
/**
* Appends styled text to the RTF document being generated.
*
* @param text
* The text to append.
* @param f
* The font of the text. If this is <code>null</code>, the
* default font is used.
* @param bg
* The background color of the text. If this is
* <code>null</code>, the default background color is used.
* @param underline
* Whether the text should be underlined.
* @see #appendNewline()
*/
public void appendToDocNoFG(String text, Font f, Color bg, boolean underline) {
appendToDoc(text, f, null, bg, underline, false);
}
/**
* Appends styled text to the RTF document being generated.
*
* @param text
* The text to append.
* @param f
* The font of the text. If this is <code>null</code>, the
* default font is used.
* @param fg
* The foreground of the text. If this is <code>null</code>,
* the default foreground color is used.
* @param bg
* The background color of the text. If this is
* <code>null</code>, the default background color is used.
* @param underline
* Whether the text should be underlined.
* @see #appendNewline()
*/
public void appendToDoc(String text, Font f, Color fg, Color bg, boolean underline) {
appendToDoc(text, f, fg, bg, underline, true);
}
/**
* Appends styled text to the RTF document being generated.
*
* @param text
* The text to append.
* @param f
* The font of the text. If this is <code>null</code>, the
* default font is used.
* @param fg
* The foreground of the text. If this is <code>null</code>,
* the default foreground color is used.
* @param bg
* The background color of the text. If this is
* <code>null</code>, the default background color is used.
* @param underline
* Whether the text should be underlined.
* @param setFG
* Whether the foreground specified by <code>fg</code> should
* be honored (if it is non-<code>null</code>).
* @see #appendNewline()
*/
public void appendToDoc(String text, Font f, Color fg, Color bg, boolean underline, boolean setFG) {
if (text != null) {
// Set font to use, if different from last addition.
int fontIndex = f == null ? 0 : (getFontIndex(fontList, f) + 1);
if (fontIndex != lastFontIndex) {
document.append("\\f").append(fontIndex);
lastFontIndex = fontIndex;
lastWasControlWord = true;
}
// Set styles to use.
if (f != null) {
int fontSize = fixFontSize(f.getSize2D() * 2); // Half points!
if (fontSize != lastFontSize) {
document.append("\\fs").append(fontSize);
lastFontSize = fontSize;
lastWasControlWord = true;
}
if (f.isBold() != lastBold) {
document.append(lastBold ? "\\b0" : "\\b");
lastBold = !lastBold;
lastWasControlWord = true;
}
if (f.isItalic() != lastItalic) {
document.append(lastItalic ? "\\i0" : "\\i");
lastItalic = !lastItalic;
lastWasControlWord = true;
}
} else { // No font specified - assume neither bold nor italic.
if (lastFontSize != DEFAULT_FONT_SIZE) {
document.append("\\fs").append(DEFAULT_FONT_SIZE);
lastFontSize = DEFAULT_FONT_SIZE;
lastWasControlWord = true;
}
if (lastBold) {
document.append("\\b0");
lastBold = false;
lastWasControlWord = true;
}
if (lastItalic) {
document.append("\\i0");
lastItalic = false;
lastWasControlWord = true;
}
}
if (underline) {
document.append("\\ul");
lastWasControlWord = true;
}
// Set the foreground color.
if (setFG) {
int fgIndex = 0;
if (fg != null) { // null => fg color index 0
fgIndex = getColorIndex(colorList, fg) + 1;
}
if (fgIndex != lastFGIndex) {
document.append("\\cf").append(fgIndex);
lastFGIndex = fgIndex;
lastWasControlWord = true;
}
}
// Set the background color.
if (bg != null) {
int pos = getColorIndex(colorList, bg);
document.append("\\highlight").append(pos + 1);
lastWasControlWord = true;
}
if (lastWasControlWord) {
document.append(' '); // Delimiter
lastWasControlWord = false;
}
escapeAndAdd(document, text);
// Reset everything that was set for this text fragment.
if (bg != null) {
document.append("\\highlight0");
lastWasControlWord = true;
}
if (underline) {
document.append("\\ul0");
lastWasControlWord = true;
}
}
}
/**
* Appends some text to a buffer, with special care taken for special
* characters as defined by the RTF spec.
*
* <ul>
* <li>All tab characters are replaced with the string
* "<code>\tab</code>"
* <li>'\', '{' and '}' are changed to "\\", "\{" and "\}"
* </ul>
*
* @param text
* The text to append (with tab chars substituted).
* @param sb
* The buffer to append to.
*/
private void escapeAndAdd(StringBuilder sb, String text) {
int count = text.length();
for (int i = 0; i < count; i++) {
char ch = text.charAt(i);
switch (ch) {
case '\t':
// Micro-optimization: for syntax highlighting with
// tab indentation, there are often multiple tabs
// back-to-back at the start of lines, so don't put
// spaces between each "\tab".
sb.append("\\tab");
while ((++i < count) && text.charAt(i) == '\t') {
sb.append("\\tab");
}
sb.append(' ');
i--; // We read one too far.
break;
case '\\':
case '{':
case '}':
sb.append('\\').append(ch);
break;
default:
if (ch <= 127) {
sb.append(ch);
} else {
sb.append("\\u").append((int) ch);
}
break;
}
}
}
/**
* Returns a font point size, adjusted for the current screen resolution.
* <p>
*
* Java2D assumes 72 dpi. On systems with larger dpi (Windows, GTK, etc.),
* font rendering will appear too small if we simply return a Java "Font"
* object's getSize() value. We need to adjust it for the screen
* resolution.
*
* @param pointSize
* A Java Font's point size, as returned from
* <code>getSize2D()</code>.
* @return The font point size, adjusted for the current screen resolution.
* This will allow other applications to render fonts the same
* size as they appear in the Java application.
*/
private int fixFontSize(float pointSize) {
if (screenRes != 72) { // Java2D assumes 72 dpi
pointSize = Math.round(pointSize * 72f / screenRes);
}
return (int) pointSize;
}
/**
* Returns the index of the specified item in a list. If the item
* is not in the list, it is added, and its new index is returned.
*
* @param list
* The list (possibly) containing the item.
* @param item
* The item to get the index of.
* @return The index of the item.
*/
private static int getColorIndex(List<Color> list, Color item) {
int pos = list.indexOf(item);
if (pos == -1) {
list.add(item);
pos = list.size() - 1;
}
return pos;
}
private String getColorTableRtf() {
// Example:
// "{\\colortbl ;\\red255\\green0\\blue0;\\red0\\green0\\blue255; }"
StringBuilder sb = new StringBuilder();
sb.append("{\\colortbl ;");
for (Color c : colorList) {
sb.append("\\red").append(c.getRed());
sb.append("\\green").append(c.getGreen());
sb.append("\\blue").append(c.getBlue());
sb.append(';');
}
sb.append("}");
return sb.toString();
}
/**
* Returns the index of the specified font in a list of fonts. This
* method only checks for a font by its family name; its attributes such
* as bold and italic are ignored.
* <p>
*
* If the font is not in the list, it is added, and its new index is
* returned.
*
* @param list
* The list (possibly) containing the font.
* @param font
* The font to get the index of.
* @return The index of the font.
*/
private static int getFontIndex(List<Font> list, Font font) {
String fontName = font.getFamily();
for (int i = 0; i < list.size(); i++) {
Font font2 = list.get(i);
if (font2.getFamily().equals(fontName)) {
return i;
}
}
list.add(font);
return list.size() - 1;
}
private String getFontTableRtf() {
// Example:
// "{\\fonttbl{\\f0\\fmodern\\fcharset0 Courier;}}"
StringBuilder sb = new StringBuilder();
// Workaround for text areas using the Java logical font "Monospaced"
// by default. There's no way to know what it's mapped to, so we
// just search for a monospaced font on the system.
String monoFamilyName = getMonospacedFontFamily();
sb.append("{\\fonttbl{\\f0\\fnil\\fcharset0 " + monoFamilyName + ";}");
for (int i = 0; i < fontList.size(); i++) {
Font f = fontList.get(i);
String familyName = f.getFamily();
if (familyName.equals("Monospaced")) {
familyName = monoFamilyName;
}
sb.append("{\\f").append(i + 1).append("\\fnil\\fcharset0 ");
sb.append(familyName).append(";}");
}
sb.append('}');
return sb.toString();
}
/**
* Returns a good "default" monospaced font to use when Java's logical
* font "Monospaced" is found.
*
* @return The monospaced font family to use.
*/
private static String getMonospacedFontFamily() {
String family = RTextArea.getDefaultFont().getFamily();
if ("Monospaced".equals(family)) {
family = "Courier";
}
return family;
}
/**
* Returns the RTF document created by this generator.
*
* @return The RTF document, as a <code>String</code>.
*/
public String getRtf() {
StringBuilder sb = new StringBuilder();
sb.append("{");
// Header
sb.append("\\rtf1\\ansi\\ansicpg1252");
sb.append("\\deff0"); // First font in font table is the default
sb.append("\\deflang1033");
sb.append("\\viewkind4"); // "Normal" view
sb.append("\\uc\\pard\\f0");
sb.append("\\fs20"); // Font size in half-points (default 24)
sb.append(getFontTableRtf()).append('\n');
sb.append(getColorTableRtf()).append('\n');
// Content
sb.append(document);
sb.append("}");
// System.err.println("*** " + sb.length());
return sb.toString();
}
/**
* Resets this generator. All document information and content is
* cleared.
*/
public void reset() {
fontList.clear();
colorList.clear();
document.setLength(0);
lastWasControlWord = false;
lastFontIndex = 0;
lastFGIndex = 0;
lastBold = false;
lastItalic = false;
lastFontSize = DEFAULT_FONT_SIZE;
screenRes = Toolkit.getDefaultToolkit().getScreenResolution();
}
}