package com.baselet.element.facet.common; import java.util.Collections; import java.util.List; import com.baselet.control.StringStyle; import com.baselet.control.basics.XValues; import com.baselet.control.enums.AlignHorizontal; import com.baselet.control.enums.AlignVertical; import com.baselet.control.enums.ElementStyle; import com.baselet.control.enums.Priority; import com.baselet.diagram.draw.DrawHandler; import com.baselet.diagram.draw.DrawHandler.Layer; import com.baselet.diagram.draw.TextSplitter; import com.baselet.element.facet.Facet; import com.baselet.element.facet.PropertiesParserState; import com.baselet.gui.AutocompletionText; public class TextPrintFacet extends Facet { public static final TextPrintFacet INSTANCE = new TextPrintFacet(); private TextPrintFacet() {} @Override public boolean checkStart(String line, PropertiesParserState state) { return true; // every line which has not been processed yet gets printed } @Override public void handleLine(String line, PropertiesParserState state) { DrawHandler drawer = state.getDrawer(); drawer.setLayer(Layer.Foreground); // should be always on top of background setupAtFirstLine(line, drawer, state); if (state.getElementStyle() == ElementStyle.WORDWRAP && !line.trim().isEmpty()) { // empty lines are skipped (otherwise they would get lost) printLineWithWordWrap(line, drawer, state); } else { printLine(StringStyle.analyzeFormatLabels(StringStyle.replaceNotEscaped(line)), drawer, state); } drawer.setLayer(Layer.Background); } private static void printLineWithWordWrap(String line, DrawHandler drawer, PropertiesParserState state) { double spaceForText = state.getXLimitsForArea(state.getTextPrintPosition(), drawer.textHeightMax(), false).getSpace() - drawer.getDistanceBorderToText() * 2; StringStyle[] wrappedLine = TextSplitter.splitStringAlgorithm(line, spaceForText, drawer); int lineIndex = 0; while (state.getTextPrintPosition() < state.getGridElementSize().height && lineIndex < wrappedLine.length) { double currentSpaceForText = state.getXLimitsForArea(state.getTextPrintPosition(), drawer.textHeightMax(), false).getSpace() - drawer.getDistanceBorderToText() * 2; // if the space for the text has changed recalculate the remaining word wrap if (Math.abs(currentSpaceForText - spaceForText) >= .0000001) { // compare with small range (findbugs FE_FLOATING_POINT_EQUALITY) // we can not use the length of the printed lines to calculate the substring start, because the number of whitespace chars is unknown line = line.substring(line.indexOf(wrappedLine[lineIndex - 1].getStringWithoutMarkup()) + wrappedLine[lineIndex - 1].getStringWithoutMarkup().length()).trim(); wrappedLine = TextSplitter.splitStringAlgorithm(line, spaceForText, drawer); lineIndex = 0; } printLine(wrappedLine[lineIndex++], drawer, state); } } private static void printLine(StringStyle line, DrawHandler drawer, PropertiesParserState state) { XValues xLimitsForText = state.getXLimitsForArea(state.getTextPrintPosition(), drawer.textHeightMax(), false); Double spaceNotUsedForText = state.getGridElementSize().width - xLimitsForText.getSpace(); if (!spaceNotUsedForText.equals(Double.NaN)) { // NaN is possible if xlimits calculation contains e.g. a division by zero state.updateMinimumWidth(spaceNotUsedForText + drawer.textWidth(line)); } AlignHorizontal hAlign = state.getAlignment().getHorizontal(); drawer.print(line, calcHorizontalTextBoundaries(xLimitsForText, drawer.getDistanceBorderToText(), hAlign), state.getTextPrintPosition(), hAlign); state.increaseTextPrintPosition(drawer.textHeightMaxWithSpace()); } /** * before the first line is printed, some space-setup is necessary to make sure the text position is correct */ private static void setupAtFirstLine(String line, DrawHandler drawer, PropertiesParserState state) { boolean isFirstPrintedLine = state.getFacetResponse(TextPrintFacet.class, true); if (isFirstPrintedLine) { state.getBuffer().setTopMin(calcStartPointFromVAlign(drawer, state)); state.getBuffer().setTopMin(calcTopDisplacementToFitLine(line, state, drawer)); state.setFacetResponse(TextPrintFacet.class, false); } } private static double calcStartPointFromVAlign(DrawHandler drawer, PropertiesParserState state) { double returnVal = drawer.textHeightMax(); // print method is located at the bottom of the text therefore add text height (important for UseCase etc where text must not reach out of the border) if (state.getElementStyle() == ElementStyle.AUTORESIZE) { // #291: if autoresize is enabled, valign is not used, because the element shouldn't have unused space returnVal += 2 * drawer.getDistanceBorderToText() + state.getBuffer().getTop(); // the same as TOP but with 2x border distance because it looks better (example from #291) } else if (state.getAlignment().getVertical() == AlignVertical.TOP) { returnVal += drawer.getDistanceBorderToText() + state.getBuffer().getTop(); } else if (state.getAlignment().getVertical() == AlignVertical.CENTER) { returnVal += (state.getGridElementSize().height - state.getTotalTextBlockHeight()) / 2 + state.getBuffer().getTop() / 2; } else /* if (state.getvAlign() == AlignVertical.BOTTOM) */ { returnVal += state.getGridElementSize().height - state.getTotalTextBlockHeight() - drawer.textHeightMax() / 4; // 1/4 of textheight is a good value for large fontsizes and "deep" characters like "y" } return returnVal; } /** * Calculates the necessary y-pos space to make the first line fit the xLimits of the element * Currently only used by UseCase element to make sure the first line is moved down as long as it doesn't fit into the available space */ private static double calcTopDisplacementToFitLine(String firstLine, PropertiesParserState state, DrawHandler drawer) { double displacement = state.getTextPrintPosition(); boolean wordwrap = state.getElementStyle() == ElementStyle.WORDWRAP; if (!wordwrap) { // in case of wordwrap or no text, there is no top displacement int BUFFER = 2; // a small buffer between text and outer border double textHeight = drawer.textHeightMax(); double addedSpacePerIteration = textHeight / 2; double availableWidthSpace = state.getXLimitsForArea(displacement, textHeight, true).getSpace() - BUFFER; double accumulator = displacement; int maxLoops = 1000; while (accumulator < state.getGridElementSize().height && !TextSplitter.checkifStringFitsNoWordwrap(firstLine, availableWidthSpace, drawer)) { if (maxLoops-- < 0) { throw new RuntimeException("Endless loop during calculation of top displacement"); } accumulator += addedSpacePerIteration; double previousWidthSpace = availableWidthSpace; availableWidthSpace = state.getXLimitsForArea(accumulator, textHeight, true).getSpace() - BUFFER; // only set displacement if the last iteration resulted in a space gain (eg: for UseCase until the middle, for Class: stays on top because on a rectangle there is never a width-space gain) if (availableWidthSpace > previousWidthSpace) { displacement = accumulator; } } } return displacement; } private static double calcHorizontalTextBoundaries(XValues xLimitsForText, double distanceBorderToText, AlignHorizontal hAlign) { double x; if (hAlign == AlignHorizontal.LEFT) { x = xLimitsForText.getLeft() + distanceBorderToText; } else if (hAlign == AlignHorizontal.CENTER) { x = xLimitsForText.getSpace() / 2.0 + xLimitsForText.getLeft(); } else /* if (state.gethAlign() == AlignHorizontal.RIGHT) */ { x = xLimitsForText.getRight() - distanceBorderToText; } return x; } @Override public List<AutocompletionText> getAutocompletionStrings() { return Collections.emptyList(); // no autocompletion text for this facet } @Override public Priority getPriority() { return Priority.LOWEST; // only text not used by other facets should be printed } @Override public void parsingFinished(PropertiesParserState state, List<String> handledLines) { // adjust height only if autoresize is active, becauce other elements use the // height of the text to position the text in the center e.g. UseCase if (state.getElementStyle() == ElementStyle.AUTORESIZE) { double heightDiff = -state.getDrawer().textHeightMax(); // subtract 1xtextheight to avoid making element too high (because the print-text pos is always on the bottom) heightDiff = heightDiff + state.getDrawer().textHeightMax() / 2; // add a vertical border padding // since the height is textPrintPosition + buffer.getTop() we need to adjust the textPrintPosition and not the buffer (which is set with the updateMinimumSize method) state.increaseTextPrintPosition(heightDiff); // add a horizontal border padding double hSpaceLeftAndRight = state.getDrawer().getDistanceBorderToText() * 2; state.updateMinimumWidth(state.getMinimumWidth() + hSpaceLeftAndRight); } } }