/* * 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; } //}}} //}}} }