package org.archstudio.bna.things.labels; import java.awt.Dimension; import java.awt.Font; import java.text.BreakIterator; import java.util.Arrays; import java.util.List; import org.archstudio.bna.IBNAView; import org.archstudio.bna.ICoordinate; import org.archstudio.bna.ICoordinateMapper; import org.archstudio.bna.things.AbstractThingPeer; import org.archstudio.bna.ui.IUIResources; import org.archstudio.bna.ui.IUIResources.FontMetrics; import org.archstudio.bna.utils.BNAUtils; import org.archstudio.sysutils.RegExBreakIterator; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.graphics.Rectangle; import com.google.common.collect.Lists; public class BoundedLabelThingPeer<T extends BoundedLabelThing> extends AbstractThingPeer<T> { private static final int MIN_FONT_SIZE = 2; private static final int MIN_LOCAL_WIDTH = 4; private static final int MIN_LOCAL_HEIGHT = MIN_FONT_SIZE + 1; private static final class LayoutData { Font font = null; List<String> lines = Lists.newArrayList(); List<Float> lineWidths = Lists.newArrayList(); float lineLeading = 0; float lineAscent = 0; float lineDescent = 0; //float totalWidth = 0; float totalHeight = 0; } private static final String trimRight(String string) { int end = string.length(); while (end > 0 && Character.isWhitespace(string.charAt(end - 1))) { end--; } return string.substring(0, end); } Font breakWidthsFont; float[] breakWidths; List<Object> breakWidthsCacheConditions; public BoundedLabelThingPeer(T thing, IBNAView view, ICoordinateMapper cm) { super(thing, view, cm); } protected LayoutData calculateLayout(BoundedLabelThing t, Dimension localSize, IUIResources r) { LayoutData layoutData = new LayoutData(); if (localSize.width > MIN_LOCAL_WIDTH && localSize.height > MIN_LOCAL_HEIGHT) { Font font = null; List<String> lines = Lists.newArrayList(); List<Float> lineWidths = Lists.newArrayList(); float lineLeading = 0; float lineAscent = 0; float lineDescent = 0; float totalWidth = 0; float totalHeight = 0; String text = t.getText(); int minFontSize = MIN_FONT_SIZE; int originalMinFontSize = minFontSize; int maxFontSize = t.getFontSize(); breakWidthsFont = r.getFont(t.getFontName(), t.getFontStyle(), maxFontSize); /* * Here, we list the set of conditions under which the cache is valid. When one of the conditions changes, * the cache must be recalculated. This is done by creating a list of the conditions then comparing it to * the conditions stored alongside the cached value. If the stored conditions do not match, we recalculate * the cache and store the new conditions. */ List<Object> breakWidthsCacheConditions = Lists.newArrayList(); breakWidthsCacheConditions.add(text); breakWidthsCacheConditions.add(breakWidthsFont); breakWidthsCacheConditions.add(r.isAntialiasText()); breakWidthsCacheConditions.add(r.getClass()); if (!breakWidthsCacheConditions.equals(this.breakWidthsCacheConditions)) { this.breakWidthsCacheConditions = breakWidthsCacheConditions; breakWidths = new float[text.length()]; BreakIterator breakIterator = new RegExBreakIterator(null, "\\s+|-"); breakIterator.setText(text); int start = breakIterator.first(); for (int end = breakIterator.next(); end != BreakIterator.DONE; start = end, end = breakIterator.next()) { String lineText = text.substring(0, end); String leftLineText = trimRight(lineText); int width = r.getTextSize(breakWidthsFont, leftLineText).width; Arrays.fill(breakWidths, start, end, width); } } RESIZING: while (minFontSize <= maxFontSize) { int trialFontSize; if (minFontSize == maxFontSize) { trialFontSize = minFontSize; } else { // must round up otherwise minFontSize may keep getting set to the same value trialFontSize = (maxFontSize + minFontSize + 1) / 2; } font = r.getFont(t.getFontName(), t.getFontStyle(), trialFontSize); lines.clear(); lineWidths.clear(); FontMetrics metrics = r.getFontMetrics(font); lineLeading = metrics.getLeading(); lineAscent = metrics.getAscent(); lineDescent = metrics.getDescent(); totalWidth = 0; totalHeight = 0; float lineHeight = lineLeading + lineAscent + lineDescent; float adjustedSize = (float) localSize.width * breakWidthsFont.getSize() / font.getSize(); int start = 0; float startingOffset = 0; while (start < text.length()) { if (totalHeight == 0) { // for the first line, only add the ascent totalHeight += lineAscent; } else { // for the remaining lines, add the descent too totalHeight += lineHeight; } if (totalHeight > localSize.height) { maxFontSize = trialFontSize - 1; continue RESIZING; } int end = Arrays .binarySearch(breakWidths, start, breakWidths.length, startingOffset + adjustedSize); if (end < 0) { end = -end - 1; } else { while (++end < breakWidths.length) { if (breakWidths[end] != breakWidths[end - 1]) { break; } } } if (start == end) { maxFontSize = trialFontSize - 1; continue RESIZING; } lines.add(trimRight(text.substring(start, end))); float width = (breakWidths[end - 1] - startingOffset) * font.getSize() / breakWidthsFont.getSize(); lineWidths.add(width); totalWidth = Math.max(totalWidth, width); start = end; startingOffset = breakWidths[end - 1]; } if (minFontSize == maxFontSize) { break RESIZING; } minFontSize = trialFontSize; } if (maxFontSize >= originalMinFontSize) { layoutData.font = font; layoutData.lines = lines; layoutData.lineWidths = lineWidths; layoutData.lineLeading = lineLeading; layoutData.lineAscent = lineAscent; layoutData.lineDescent = lineDescent; //layoutData.totalWidth = totalWidth; layoutData.totalHeight = totalHeight; } } return layoutData; } LayoutData layoutData; List<Object> layoutDataCacheConditions; @Override public boolean draw(Rectangle localBounds, IUIResources r) { Rectangle lbb = cm.worldToLocal(t.getBoundingBox()); if (!lbb.intersects(localBounds)) { return false; } if (t.getText().trim().length() == 0) { return false; } RGB color = t.getColor(); if (color != null) { List<Object> layoutDataCacheConditions = Lists.newArrayList(); layoutDataCacheConditions.add(t.getText()); layoutDataCacheConditions.add(t.getFontName()); layoutDataCacheConditions.add(t.getFontStyle()); layoutDataCacheConditions.add(t.getFontSize()); layoutDataCacheConditions.add(t.isDontIncreaseFontSize()); layoutDataCacheConditions.add(r.isAntialiasText()); layoutDataCacheConditions.add(r.getClass()); // don't use the local bounding box as it can vary slightly depending on location layoutDataCacheConditions.add(BNAUtils.toDimension(t.getBoundingBox())); layoutDataCacheConditions.add(cm.getLocalScale()); if (!layoutDataCacheConditions.equals(this.layoutDataCacheConditions)) { this.layoutDataCacheConditions = layoutDataCacheConditions; layoutData = calculateLayout(t, BNAUtils.getSize(lbb), r); } if (layoutData.font != null) { float scale = 1.0f; float y = lbb.y; switch (t.getVerticalAlignment()) { case BOTTOM: y += lbb.height - layoutData.totalHeight * scale; break; case MIDDLE: y += Math.floor((lbb.height - layoutData.totalHeight * scale) / 2); break; case TOP: break; } for (int i = 0; i < layoutData.lines.size(); i++) { String line = layoutData.lines.get(i); float x = lbb.x; switch (t.getHorizontalAlignment()) { case RIGHT: x += lbb.width - layoutData.lineWidths.get(i) * scale; break; case CENTER: x += (lbb.width - layoutData.lineWidths.get(i) * scale) / 2; break; case LEFT: break; } r.drawText(layoutData.font, line, x, y - layoutData.lineLeading, color, 1); y += (layoutData.lineLeading + layoutData.lineAscent + layoutData.lineDescent) * scale; } } } return true; } @Override public boolean isInThing(ICoordinate location) { return t.getBoundingBox().contains(location.getWorldPoint()); } }