/*
* Chunk.java - A syntax token with extra information required for painting it
* on screen
* :tabSize=4:indentSize=4:noTabs=false:
* :folding=explicit:collapseFolds=1:
*
* Copyright (C) 2001, 2002 Slava Pestov
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or any later version.
*
* This program 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.gjt.sp.jedit.syntax;
//{{{ Imports
import javax.swing.text.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.lang.ref.SoftReference;
import org.gjt.sp.jedit.Debug;
import org.gjt.sp.jedit.IPropertyManager;
//}}}
/**
* A syntax token with extra information required for painting it
* on screen.
* @since jEdit 4.1pre1
*/
public class Chunk extends Token
{
//{{{ paintChunkList() method
/**
* Paints a chunk list.
* @param chunks The chunk list
* @param gfx The graphics context
* @param x The x co-ordinate
* @param y The y co-ordinate
* @param glyphVector true if we want to use glyphVector, false if we
* want to use drawString
* @return The width of the painted text
* @since jEdit 4.2pre1
*/
public static float paintChunkList(Chunk chunks,
Graphics2D gfx, float x, float y, boolean glyphVector)
{
Rectangle clipRect = gfx.getClipBounds();
float _x = 0.0f;
while(chunks != null)
{
// only paint visible chunks
if(x + _x + chunks.width > clipRect.x
&& x + _x < clipRect.x + clipRect.width)
{
// Useful for debugging purposes
if(Debug.CHUNK_PAINT_DEBUG)
{
gfx.draw(new Rectangle2D.Float(x + _x,y - 10,
chunks.width,10));
}
if(chunks.isAccessible() && chunks.glyphs != null)
{
gfx.setFont(chunks.style.getFont());
gfx.setColor(chunks.style.getForegroundColor());
if (glyphVector)
chunks.drawGlyphs(gfx, x + _x, y);
else if(chunks.str != null)
{
gfx.drawString(chunks.str,
(int)(x + _x),(int)y);
}
}
}
_x += chunks.width;
chunks = (Chunk)chunks.next;
}
return _x;
} //}}}
//{{{ paintChunkBackgrounds() method
/**
* Paints the background highlights of a chunk list.
* @param chunks The chunk list
* @param gfx The graphics context
* @param x The x co-ordinate
* @param y The y co-ordinate
* @return The width of the painted backgrounds
* @since jEdit 4.2pre1
*/
public static float paintChunkBackgrounds(Chunk chunks,
Graphics2D gfx, float x, float y, int lineHeight)
{
Rectangle clipRect = gfx.getClipBounds();
float _x = 0.0f;
FontMetrics forBackground = gfx.getFontMetrics();
int ascent = forBackground.getAscent();
int height = lineHeight;
while(chunks != null)
{
// only paint visible chunks
if(x + _x + chunks.width > clipRect.x
&& x + _x < clipRect.x + clipRect.width)
{
if(chunks.isAccessible())
{
//{{{ Paint token background color if necessary
Color bgColor = chunks.background;
if(bgColor != null)
{
gfx.setColor(bgColor);
gfx.fill(new Rectangle2D.Float(
x + _x,y - ascent,
_x + chunks.width - _x,
height));
} //}}}
}
}
_x += chunks.width;
chunks = (Chunk)chunks.next;
}
return _x;
} //}}}
//{{{ offsetToX() method
/**
* Converts an offset in a chunk list into an x co-ordinate.
* @param chunks The chunk list
* @param offset The offset
* @since jEdit 4.1pre1
*/
public static float offsetToX(Chunk chunks, int offset)
{
if(chunks != null && offset < chunks.offset)
{
throw new ArrayIndexOutOfBoundsException(offset + " < "
+ chunks.offset);
}
float x = 0.0f;
while(chunks != null)
{
if(chunks.isAccessible() && offset < chunks.offset + chunks.length)
return x + chunks.offsetToX(offset - chunks.offset);
x += chunks.width;
chunks = (Chunk)chunks.next;
}
return x;
} //}}}
//{{{ xToOffset() method
/**
* Converts an x co-ordinate in a chunk list into an offset.
* @param chunks The chunk list
* @param x The x co-ordinate
* @param round Round up to next letter if past the middle of a letter?
* @return The offset within the line, or -1 if the x co-ordinate is too
* far to the right
* @since jEdit 4.1pre1
*/
public static int xToOffset(Chunk chunks, float x, boolean round)
{
float _x = 0.0f;
while(chunks != null)
{
if(chunks.isAccessible() && x < _x + chunks.width)
return chunks.xToOffset(x - _x,round);
_x += chunks.width;
chunks = (Chunk)chunks.next;
}
return -1;
} //}}}
//{{{ propertiesChanged() method
/**
* Reload internal configuration based on the given properties.
*
* @param props Configuration properties.
*
* @since jEdit 4.4pre1
*/
public static void propertiesChanged(IPropertyManager props)
{
fontSubstList = null;
if (props == null)
{
fontSubstEnabled = false;
fontSubstSystemFontsEnabled = true;
preferredFonts = null;
}
else
{
fontSubstEnabled = Boolean.parseBoolean(props.getProperty("view.enableFontSubst"));
fontSubstSystemFontsEnabled = Boolean.parseBoolean(props.getProperty("view.enableFontSubstSystemFonts"));
}
List<Font> userFonts = new ArrayList<Font>();
String family;
int i = 0;
if (props != null)
{
while ((family = props.getProperty("view.fontSubstList." + i)) != null)
{
/*
* The default font is Font.DIALOG if the family
* doesn't match any installed fonts. The following
* check skips fonts that don't exist.
*/
Font f = new Font(family, Font.PLAIN, 12);
if (!"dialog".equalsIgnoreCase(f.getFamily()) ||
"dialog".equalsIgnoreCase(family))
userFonts.add(f);
i++;
}
}
preferredFonts = userFonts.toArray(new Font[userFonts.size()]);
// Clear cache, not to hold reference to old fonts which
// might become unused after properties changed.
glyphCache = null;
} //}}}
//{{{ Package private members
//{{{ Instance variables
SyntaxStyle style;
// set up after init()
float width;
//}}}
//{{{ Chunk constructor
/**
* Constructs a virtual indent appears at the beggining of a wrapped line.
*/
Chunk(float width, int offset, ParserRuleSet rules)
{
super(Token.NULL,offset,0,rules);
this.width = width;
assert !isAccessible();
assert isInitialized();
} //}}}
//{{{ Chunk constructor
Chunk(byte id, int offset, int length, ParserRuleSet rules,
SyntaxStyle[] styles, byte defaultID)
{
super(id,offset,length,rules);
style = styles[id];
background = style.getBackgroundColor();
if(background == null)
background = styles[defaultID].getBackgroundColor();
assert isAccessible();
assert !isInitialized();
} //}}}
//{{{ Chunk constructor
Chunk(byte id, int offset, int length, ParserRuleSet rules,
SyntaxStyle style, Color background)
{
super(id,offset,length,rules);
this.style = style;
this.background = background;
assert isAccessible();
assert !isInitialized();
} //}}}
//{{{ isAccessible() method
/**
* Returns true if this chunk has accesible text.
*/
final boolean isAccessible()
{
return length > 0;
} //}}}
//{{{ isInitialized() method
/**
* Returns true if this chunk is ready for painting.
*/
final boolean isInitialized()
{
return !isAccessible() // virtual indent
|| (glyphs != null) // normal text
|| (width > 0); // tab
} //}}}
//{{{ isTab() method
/**
* Returns true if this chunk represents a tab.
*/
final boolean isTab(Segment lineText)
{
return length == 1
&& lineText.array[lineText.offset + offset] == '\t';
} //}}}
//{{{ snippetBefore() method
/**
* Returns a shorten uninitialized chunk before specific offset.
*/
final Chunk snippetBefore(int snipOffset)
{
assert 0 <= snipOffset && snipOffset < length;
return new Chunk(id, offset, snipOffset,
rules, style, background);
} //}}}
//{{{ snippetAfter() method
/**
* Returns a shorten uninitialized chunk after specific offset.
*/
final Chunk snippetAfter(int snipOffset)
{
assert 0 <= snipOffset && snipOffset < length;
return new Chunk(id, offset + snipOffset, length - snipOffset,
rules, style, background);
} //}}}
//{{{ snippetBeforeLineOffset() method
/**
* Returns a shorten uninitialized chunk before specific offset.
* The offset is it in the line text, instead of in chunk.
*/
final Chunk snippetBeforeLineOffset(int lineOffset)
{
return snippetBefore(lineOffset - this.offset);
} //}}}
//{{{ offsetToX() method
final float offsetToX(int offset)
{
if(glyphs == null)
return 0.0f;
float x = 0.0f;
for (GlyphVector gv : glyphs)
{
if (offset < gv.getNumGlyphs())
{
x += (float) gv.getGlyphPosition(offset).getX();
return x;
}
x += (float) gv.getLogicalBounds().getWidth();
offset -= gv.getNumGlyphs();
}
/* Shouldn't reach this. */
assert false : "Shouldn't reach this.";
return -1;
} //}}}
//{{{ xToOffset() method
final int xToOffset(float x, boolean round)
{
if (glyphs == null)
{
if (round && width - x < x)
return offset + length;
else
return offset;
}
int off = offset;
float myx = 0.0f;
for (GlyphVector gv : glyphs)
{
float gwidth = (float) gv.getLogicalBounds().getWidth();
if (myx + gwidth >= x)
{
float[] pos = gv.getGlyphPositions(0, gv.getNumGlyphs(), null);
for (int i = 0; i < gv.getNumGlyphs(); i++)
{
float glyphX = myx + pos[i << 1];
float nextX = (i == gv.getNumGlyphs() - 1)
? width
: myx + pos[(i << 1) + 2];
if (nextX > x)
{
if (!round || nextX - x > x - glyphX)
return off + i;
else
return off + i + 1;
}
}
}
myx += gwidth;
off += gv.getNumGlyphs();
}
/* Shouldn't reach this. */
assert false : "Shouldn't reach this.";
return -1;
} //}}}
//{{{ init() method
void init(Segment lineText, TabExpander expander, float x,
FontRenderContext fontRenderContext, int physicalLineOffset)
{
if(!isAccessible())
{
// do nothing
}
else if(isTab(lineText))
{
float newX = expander.nextTabStop(x,physicalLineOffset+offset);
width = newX - x;
}
else
{
str = new String(lineText.array,lineText.offset + offset,length);
GlyphKey cacheKey = new GlyphKey(str,
style.getFont(), fontRenderContext);
GlyphCache cache = getGlyphCache();
GlyphVector[] cachedGlyphs = cache.get(cacheKey);
if (cachedGlyphs != null)
{
glyphs = cachedGlyphs;
}
else
{
int textStart = lineText.offset + offset;
int textEnd = textStart + length;
glyphs = layoutGlyphs(style.getFont(),
fontRenderContext,
lineText.array, textStart, textEnd);
cache.put(cacheKey, glyphs);
}
float w = 0.0f;
for (GlyphVector gv: glyphs)
{
w += (float)gv.getLogicalBounds().getWidth();
}
width = w;
}
assert isInitialized();
} //}}}
//}}}
//{{{ Private members
//{{{ Static variables
private static final char[] EMPTY_TEXT = new char[0];
private static boolean fontSubstEnabled;
private static boolean fontSubstSystemFontsEnabled;
private static Font[] preferredFonts;
private static Font[] fontSubstList;
// This cache is meant to reduce calls of layoutGlyphVector(),
// which was an outclassing CPU bottleneck (profiled on jProfiler,
// Sun JDK 6, Windows XP).
//
// The capacity is roughly tuned so that the effect is clearly
// noticeable on very large random int table in C mode; like
// following:
// int table000[100] = { 232, 190, 69, ..., 80, 246, 78 };
// int table000[100] = { 69, 84, 206, ..., 160, 197, 161 };
// ...
// int table099[100] = { 219, 100, 60, ..., 100, 203, 8 };
// int table100[100] = { 159, 189, 159, ..., 76, 9, 239, };
// and the additional heap usage is lower than 1 MB.
//
// Heap usage was measured as about 400 KB / 256 entries (JRE 7u3,
// Windows XP).
private static int glyphCacheCapacity = 256;
private static SoftReference<GlyphCache> glyphCache;
//}}}
//{{{ Instance variables
// this is either style.getBackgroundColor() or
// styles[defaultID].getBackgroundColor()
private Color background;
private String str;
private GlyphVector[] glyphs;
//}}}
//{{{ getFontSubstList() method
private static Font[] getFontSubstList()
{
if (fontSubstList == null)
{
if (fontSubstSystemFontsEnabled)
{
Font[] systemFonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts();
fontSubstList = new Font[preferredFonts.length +
systemFonts.length];
System.arraycopy(preferredFonts, 0, fontSubstList, 0,
preferredFonts.length);
System.arraycopy(systemFonts, 0, fontSubstList,
preferredFonts.length,
systemFonts.length);
}
else
{
fontSubstList = new Font[preferredFonts.length];
System.arraycopy(preferredFonts, 0, fontSubstList, 0,
preferredFonts.length);
}
}
return fontSubstList;
} //}}}
//{{{ getSubstFont() method
/**
* Returns the first font which can display a character from
* configured substitution candidates, or null if there is no
* such font.
*/
private static Font getSubstFont(int codepoint)
{
// Workaround for a problem reported in SF.net patch #3480246
// > If font substitution with system fonts is enabled,
// > I get for inserted control characters strange mathematical
// > symbols from a non-unicode font in my system.
if (Character.isISOControl(codepoint))
return null;
for (Font candidate: getFontSubstList())
{
if (candidate.canDisplay(codepoint))
{
return candidate;
}
}
return null;
} //}}}
//{{{ drawGlyphs() method
/**
* Draws the internal list of glyph vectors into the given
* graphics object.
*
* @param gfx Where to draw the glyphs.
* @param x Starting horizontal position.
* @param y Vertical position.
*/
private void drawGlyphs(Graphics2D gfx,
float x,
float y)
{
for (GlyphVector gv : glyphs)
{
gfx.drawGlyphVector(gv, x, y);
x += (float) gv.getLogicalBounds().getWidth();
}
} //}}}
//{{{ layoutGlyphVector() method
/**
* A wrapper of Font.layoutGlyphVector() to simplify the calls.
*/
private static GlyphVector layoutGlyphVector(Font font,
FontRenderContext frc,
char[] text, int start, int end)
{
// FIXME: Need BiDi support.
int flags = Font.LAYOUT_LEFT_TO_RIGHT
| Font.LAYOUT_NO_START_CONTEXT
| Font.LAYOUT_NO_LIMIT_CONTEXT;
GlyphVector result =
font.layoutGlyphVector(frc, text, start, end, flags);
// This is necessary to work around a memory leak in Sun Java 6 where
// the sun.font.GlyphLayout is cached and reused while holding an
// instance to the char array.
font.layoutGlyphVector(frc, EMPTY_TEXT, 0, 0, flags);
return result;
} // }}}
//{{{ layoutGlyphs() method
/**
* Layout the glyphs to render the given text, applying font
* substitution if configured.
*
* Font substitution works in the following manner:
* - All characters that can be rendered with the main
* font will be.
* - For characters that can't be handled by the main
* font, iterate over the list of available fonts to
* find an appropriate one. The first match is chosen.
*
* The user can define his list of preferred fonts, which will
* be tried before the system fonts.
*/
private static GlyphVector[] layoutGlyphs(Font mainFont,
FontRenderContext frc,
char[] text, int start, int end)
{
int substStart = !fontSubstEnabled ? -1
: mainFont.canDisplayUpTo(text, start, end);
if (substStart == -1)
{
GlyphVector gv = layoutGlyphVector(mainFont, frc,
text, start, end);
return new GlyphVector[] {gv};
}
else
{
FontSubstitution subst = new FontSubstitution(
mainFont, frc, text, start);
subst.addNonSubstRange(substStart - start);
doFontSubstitution(subst, mainFont,
text, substStart, end);
subst.finish();
return subst.getGlyphs();
}
} //}}}
//{{{ doFontSubstitution() method
private static void doFontSubstitution(FontSubstitution subst,
Font mainFont,
char[] text, int start, int end)
{
for (;;)
{
assert start < end;
int nextChar = Character.codePointAt(text, start);
int charCount = Character.charCount(nextChar);
assert !mainFont.canDisplay(nextChar);
Font substFont = getSubstFont(nextChar);
if (substFont != null)
{
subst.addRange(substFont, charCount);
}
else
{
subst.addNonSubstRange(charCount);
}
start += charCount;
if (start >= end)
{
break;
}
int nextSubstStart =
mainFont.canDisplayUpTo(text, start, end);
if (nextSubstStart == -1)
{
subst.addNonSubstRange(end - start);
break;
}
subst.addNonSubstRange(nextSubstStart - start);
start = nextSubstStart;
}
} //}}}
//{{{ class FontSubstitution
// A helper class to build GlyphVector[] with least calls to
// layoutGlyphVector() no matter how many the font substitution
// logic find intermediate boundaries.
private static class FontSubstitution
{
public FontSubstitution(Font mainFont, FontRenderContext frc,
char[] text, int start)
{
this.mainFont = mainFont;
this.frc = frc;
this.text = text;
rangeStart = start;
rangeFont = null;
rangeLength = 0;
glyphs = new ArrayList<GlyphVector>();
}
public void addNonSubstRange(int length)
{
addRange(null, length);
}
private void addRange(Font font, int length)
{
assert length >= 0;
if (length == 0)
{
return;
}
if (font == rangeFont)
{
rangeLength += length;
}
else
{
addGlyphVectorOfLastRange();
rangeFont = font;
rangeStart += rangeLength;
rangeLength = length;
}
}
public void finish()
{
addGlyphVectorOfLastRange();
rangeFont = null;
rangeStart += rangeLength;
rangeLength = 0;
}
public GlyphVector[] getGlyphs()
{
return glyphs.toArray(new GlyphVector[glyphs.size()]);
}
private final Font mainFont;
private final FontRenderContext frc;
private final char[] text;
private int rangeStart;
private Font rangeFont;
private int rangeLength;
private final ArrayList<GlyphVector> glyphs;
private void addGlyphVectorOfLastRange()
{
if (rangeLength == 0)
{
return;
}
Font font = (rangeFont == null) ?
mainFont :
rangeFont.deriveFont(mainFont.getStyle(),
mainFont.getSize());
GlyphVector gv = layoutGlyphVector(font, frc,
text, rangeStart, rangeStart + rangeLength);
glyphs.add(gv);
}
} //}}}
//{{{ getGlyphCache() method
private static GlyphCache getGlyphCache()
{
if (glyphCache != null)
{
GlyphCache cache = glyphCache.get();
if (cache != null)
{
return cache;
}
}
GlyphCache newOne = new GlyphCache(glyphCacheCapacity);
glyphCache = new SoftReference<GlyphCache>(newOne);
return newOne;
} //}}}
//{{{ class GlyphKey
private static class GlyphKey
{
public final String token;
public final Font font;
public final FontRenderContext context;
GlyphKey(String token, Font font, FontRenderContext context)
{
assert token != null;
assert font != null;
assert context != null;
this.token = token;
this.font = font;
this.context = context;
}
@Override
public final int hashCode()
{
return token.hashCode()
+ font.hashCode()
+ context.hashCode();
}
@Override
public final boolean equals(Object otherObject)
{
// should be called only from GlyphCache to
// compare with other keys, then explicit type
// checking and null checking are not necessary.
GlyphKey other = (GlyphKey)otherObject;
return token.equals(other.token)
&& font.equals(other.font)
&& context.equals(other.context);
}
@Override
public final String toString()
{
return token;
}
} //}}}
//{{{ class GlyphCache
private static class GlyphCache extends LinkedHashMap<GlyphKey, GlyphVector[]>
{
public GlyphCache(int capacity)
{
// Avoid rehashing with known limit.
super(capacity + 1, 1.0f, true/*accessOrder*/);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<GlyphKey, GlyphVector[]> eldest)
{
return size() > capacity;
}
private final int capacity;
} //}}}
//}}}
}