blob: 61ab2cb3106423dd82368b850e7c5b882d0f332f [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2020 Original authors and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Original authors and others - initial API and implementation
* Dirk Fauth <dirk.fauth@googlemail.com> - Bug 461261
* Loris Securo <lorissek@gmail.com> - Bug 472668
******************************************************************************/
package org.eclipse.nebula.widgets.nattable.painter.cell;
import java.util.Map;
import java.util.WeakHashMap;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.data.convert.IDisplayConverter;
import org.eclipse.nebula.widgets.nattable.layer.cell.CellDisplayConversionUtils;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.style.CellStyleAttributes;
import org.eclipse.nebula.widgets.nattable.style.IStyle;
import org.eclipse.nebula.widgets.nattable.style.TextDecorationEnum;
import org.eclipse.nebula.widgets.nattable.util.GUIHelper;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
/**
* Abstract TextPainter the contains general methods for drawing text into a
* cell. Can handle word wrapping and/or word cutting and/or automatic
* calculation and resizing of the cell height or width if the text does not fit
* into the cell.
*/
public abstract class AbstractTextPainter extends BackgroundPainter {
public static final String EMPTY = ""; //$NON-NLS-1$
public static final String DOT = "..."; //$NON-NLS-1$
/**
* The regular expression to find predefined new lines in the text to show.
* Is used for word wrapping to preserve user defined new lines. To be
* platform independent \n and \r and the combination of both are used to
* find user defined new lines.
*/
public static final String NEW_LINE_REGEX = "\\n\\r|\\r\\n|\\n|\\r"; //$NON-NLS-1$
public static final String LINE_SEPARATOR = System.getProperty("line.separator"); //$NON-NLS-1$
/**
* @since 1.5
*/
protected boolean wordWrapping = false;
protected boolean wrapText;
protected final boolean paintBg;
protected boolean paintFg = true;
protected int spacing = 5;
/**
* @since 1.5
*/
protected int lineSpacing = 0;
// can only grow but will not calculate the minimal length
protected boolean calculateByTextLength;
protected boolean calculateByTextHeight;
private boolean underline;
private boolean strikethrough;
private boolean trimText = true;
private Color originalBackground;
private Color originalForeground;
private Font originalFont;
private static Map<String, Integer> temporaryMap = new WeakHashMap<>();
private static Map<org.eclipse.swt.graphics.Font, FontData[]> fontDataCache = new WeakHashMap<>();
public AbstractTextPainter() {
this(false, true);
}
/**
* @param wrapText
* split text over multiple lines
* @param paintBg
* skips painting the background if is FALSE
*/
public AbstractTextPainter(boolean wrapText, boolean paintBg) {
this(wrapText, paintBg, 0);
}
/**
* @param wrapText
* split text over multiple lines
* @param paintBg
* skips painting the background if is FALSE
* @param spacing
* The space between text and cell border
*/
public AbstractTextPainter(boolean wrapText, boolean paintBg, int spacing) {
this(wrapText, paintBg, spacing, false);
}
/**
* @param wrapText
* split text over multiple lines
* @param paintBg
* skips painting the background if is FALSE
* @param calculate
* tells the text painter to calculate the cell borders regarding
* the content
*/
public AbstractTextPainter(boolean wrapText, boolean paintBg,
boolean calculate) {
this(wrapText, paintBg, 0, calculate);
}
/**
* @param wrapText
* split text over multiple lines
* @param paintBg
* skips painting the background if is FALSE
* @param calculateByTextLength
* tells the text painter to calculate the cell border by
* containing text length. For horizontal text rendering, this
* means the width of the cell is calculated by content, for
* vertical text rendering the height is calculated
* @param calculateByTextHeight
* tells the text painter to calculate the cell border by
* containing text height. For horizontal text rendering, this
* means the height of the cell is calculated by content, for
* vertical text rendering the width is calculated
*/
public AbstractTextPainter(boolean wrapText, boolean paintBg,
boolean calculateByTextLength, boolean calculateByTextHeight) {
this(wrapText, paintBg, 0, calculateByTextLength, calculateByTextHeight);
}
/**
* @param wrapText
* split text over multiple lines
* @param paintBg
* skips painting the background if is FALSE
* @param spacing
* The space between text and cell border
* @param calculate
* tells the text painter to calculate the cell borders regarding
* the content
*/
public AbstractTextPainter(boolean wrapText, boolean paintBg, int spacing,
boolean calculate) {
this(wrapText, paintBg, spacing, calculate, calculate);
}
/**
* @param wrapText
* split text over multiple lines
* @param paintBg
* skips painting the background if is FALSE
* @param spacing
* The space between text and cell border
* @param calculateByTextLength
* tells the text painter to calculate the cell border by
* containing text length. For horizontal text rendering, this
* means the width of the cell is calculated by content, for
* vertical text rendering the height is calculated
* @param calculateByTextHeight
* tells the text painter to calculate the cell border by
* containing text height. For horizontal text rendering, this
* means the height of the cell is calculated by content, for
* vertical text rendering the width is calculated
*/
public AbstractTextPainter(boolean wrapText, boolean paintBg, int spacing,
boolean calculateByTextLength, boolean calculateByTextHeight) {
this.wrapText = wrapText;
this.paintBg = paintBg;
this.spacing = spacing;
this.calculateByTextLength = calculateByTextLength;
this.calculateByTextHeight = calculateByTextHeight;
}
/**
* Convert the data value of the cell using the {@link IDisplayConverter}
* from the {@link IConfigRegistry}
*
* @param cell
* The cell whose data value should be converted.
* @param configRegistry
* The {@link IConfigRegistry} to retrieve the converter.
* @return The data value converted to a String.
*/
protected String convertDataType(ILayerCell cell, IConfigRegistry configRegistry) {
return CellDisplayConversionUtils.convertDataType(cell, configRegistry);
}
/**
* Setup the GC by the values defined in the given cell style.
*
* @param gc
* The {@link GC} that is used for rendering.
* @param cellStyle
* The {@link IStyle} to retrieve the styling options.
*/
public void setupGCFromConfig(GC gc, IStyle cellStyle) {
Color fg = cellStyle.getAttributeValue(CellStyleAttributes.FOREGROUND_COLOR);
Color bg = cellStyle.getAttributeValue(CellStyleAttributes.BACKGROUND_COLOR);
Font font = cellStyle.getAttributeValue(CellStyleAttributes.FONT);
// remember previous settings
this.originalFont = gc.getFont();
this.originalForeground = gc.getForeground();
this.originalBackground = gc.getBackground();
gc.setAntialias(GUIHelper.DEFAULT_ANTIALIAS);
gc.setTextAntialias(GUIHelper.DEFAULT_TEXT_ANTIALIAS);
gc.setFont(font);
gc.setForeground(fg != null ? fg : GUIHelper.COLOR_LIST_FOREGROUND);
gc.setBackground(bg != null ? bg : GUIHelper.COLOR_LIST_BACKGROUND);
}
/**
* Reset the GC to the original values.
*
* @param gc
* The {@link GC} that is used for rendering.
*
* @since 1.4
*/
public void resetGC(GC gc) {
gc.setFont(this.originalFont);
gc.setForeground(this.originalForeground);
gc.setBackground(this.originalBackground);
}
/**
* Checks if there is a underline text decoration configured within the
* given cell style.
*
* @param cellStyle
* The cell style of the current cell to check for the text
* decoration.
* @return <code>true</code> if there is a underline text decoration
* configured, <code>false</code> otherwise.
*/
protected boolean renderUnderlined(IStyle cellStyle) {
TextDecorationEnum decoration = cellStyle.getAttributeValue(CellStyleAttributes.TEXT_DECORATION);
if (decoration != null) {
return (decoration.equals(TextDecorationEnum.UNDERLINE)
|| decoration.equals(TextDecorationEnum.UNDERLINE_STRIKETHROUGH));
}
return this.underline;
}
/**
* Checks if there is a strikethrough text decoration configured within the
* given cell style.
*
* @param cellStyle
* The cell style of the current cell to check for the text
* decoration.
* @return <code>true</code> if there is a strikethrough text decoration
* configured, <code>false</code> otherwise.
*/
protected boolean renderStrikethrough(IStyle cellStyle) {
TextDecorationEnum decoration = cellStyle.getAttributeValue(CellStyleAttributes.TEXT_DECORATION);
if (decoration != null) {
return (decoration.equals(TextDecorationEnum.STRIKETHROUGH)
|| decoration.equals(TextDecorationEnum.UNDERLINE_STRIKETHROUGH));
}
return this.strikethrough;
}
/**
* Scans for new line characters and counts the number of lines for the
* given text.
*
* @param text
* the text to scan
* @return the number of lines for the given text
*/
protected int getNumberOfNewLines(String text) {
String[] lines = text.split(NEW_LINE_REGEX);
return lines.length;
}
/**
* Calculates the length of a given text by using the GC. To minimize the
* count of calculations, the calculation result will be stored within a
* Map, so the next time the length of the same text is asked for, the
* result is only returned by cache and is not calculated again.
*
* @param gc
* the current GC
* @param text
* the text to get the length for
* @return the length of the text
*/
protected int getLengthFromCache(GC gc, String text) {
String originalString = text;
StringBuilder buffer = new StringBuilder();
buffer.append(text);
if (gc.getFont() != null) {
FontData[] datas = fontDataCache.get(gc.getFont());
if (datas == null) {
datas = gc.getFont().getFontData();
fontDataCache.put(gc.getFont(), datas);
}
if (datas != null && datas.length > 0) {
buffer.append(datas[0].getName());
buffer.append(","); //$NON-NLS-1$
buffer.append(datas[0].getHeight());
buffer.append(","); //$NON-NLS-1$
buffer.append(datas[0].getStyle());
}
}
text = buffer.toString();
Integer width = temporaryMap.computeIfAbsent(text, t -> Integer.valueOf(gc.textExtent(originalString).x));
return width.intValue();
}
/**
* Computes dependent on the configuration of the TextPainter the text to
* display. If word wrapping is enabled new lines are inserted if the
* available space is not enough. If calculation of available space is
* enabled, the space is automatically widened for the text to display, and
* if no calculation is enabled the text is cut and modified to end with
* "..." to fit into the available space
*
* @param cell
* the current cell to paint
* @param gc
* the current GC
* @param availableLength
* the available space for the text to display
* @param text
* the text that should be modified for display
* @return the modified text
*/
protected String getTextToDisplay(ILayerCell cell, GC gc, int availableLength, String text) {
StringBuilder output = new StringBuilder();
if (isTrimText()) {
text = text.trim();
}
// take the whole width of the text
int textLength = getLengthFromCache(gc, text);
if (this.wordWrapping || (!this.calculateByTextLength && this.wrapText)) {
String[] lines = text.split(NEW_LINE_REGEX);
for (String line : lines) {
if (output.length() > 0) {
output.append(LINE_SEPARATOR);
}
String[] words = line.split("\\s"); //$NON-NLS-1$
// concat the words with spaces and newlines
String computedText = ""; //$NON-NLS-1$
for (String word : words) {
computedText = computeTextToDisplay(computedText, word, gc, availableLength);
}
output.append(computedText);
}
} else if (this.calculateByTextLength && this.wrapText) {
if (availableLength < textLength) {
// calculate length by finding the longest word in text
textLength = (availableLength - (2 * this.spacing));
String[] lines = text.split(NEW_LINE_REGEX);
for (String line : lines) {
if (output.length() > 0) {
output.append(LINE_SEPARATOR);
}
String[] words = line.split("\\s"); //$NON-NLS-1$
for (String word : words) {
textLength = Math.max(textLength, getLengthFromCache(gc, word));
}
// concat the words with spaces and newlines to be always
// smaller then available
String computedText = ""; //$NON-NLS-1$
for (String word : words) {
computedText = computeTextToDisplay(computedText, word, gc, textLength);
}
output.append(computedText);
}
} else {
output.append(text);
}
setNewMinLength(cell, textLength + calculatePadding(cell, availableLength));
} else if (this.calculateByTextLength && !this.wrapText) {
output.append(modifyTextToDisplay(text, gc, textLength));
// add padding and spacing to textLength because they are needed for
// correct sizing
// padding can occur on using decorators like the
// BeveledBorderDecorator or the PaddingDecorator
setNewMinLength(cell, textLength + calculatePadding(cell, availableLength) + (2 * this.spacing));
} else if (!this.calculateByTextLength && !this.wrapText) {
output.append(modifyTextToDisplay(text, gc, availableLength + (2 * this.spacing)));
}
return output.toString();
}
/**
* This method gets only called if text wrapping is enabled. Concatenates
* the two given words by taking the availableSpace into account. If
* concatenating those two words with a space as delimiter does fit into the
* available space the return value is exactly this. Else instead of a space
* there will be a new line character used as delimiter.
*
* @param one
* the first word or the whole text before the next word
* @param two
* the next word to add to the first parameter
* @param gc
* the current GC
* @param availableSpace
* the available space
* @return the concatenated String of the first two parameters
*/
private String computeTextToDisplay(String one, String two, GC gc, int availableSpace) {
String result = one;
// if one is empty or one ends with newline just add two
if (one == null || one.length() == 0 || one.endsWith(LINE_SEPARATOR)) {
result += modifyTextToDisplay(two, gc, availableSpace);
} else {
// if one contains a newline
if (one.indexOf(LINE_SEPARATOR) != -1) {
// get the end of the last part after the last newline
one = one.substring(one.lastIndexOf(LINE_SEPARATOR) + LINE_SEPARATOR.length());
}
if (getLengthFromCache(gc, one) == availableSpace
|| getLengthFromCache(gc, one + " " + two) >= availableSpace) { //$NON-NLS-1$
result += LINE_SEPARATOR;
result += modifyTextToDisplay(two, gc, availableSpace);
} else {
result += ' ';
result += two;
}
}
return result;
}
/**
* Checks if the given text is bigger than the available space. If not the
* given text is simply returned without modification. If the text does not
* fit into the available space, it will be modified by cutting and adding
* three dots.
*
* @param text
* the text to compute
* @param gc
* the current GC
* @param availableLength
* the available space
* @return the modified text if it is bigger than the available space or the
* text as it was given if it fits into the available space
*/
private String modifyTextToDisplay(String text, GC gc, int availableLength) {
// length of the text on GC taking new lines into account
// this means the textLength is the value of the longest line
int textLength = getLengthFromCache(gc, text);
if (textLength > availableLength) {
// as looking at the text length without taking new lines into
// account we have to look at every line itself
StringBuilder result = new StringBuilder();
String[] lines = text.split(NEW_LINE_REGEX);
for (String line : lines) {
if (result.length() > 0) {
result.append(LINE_SEPARATOR);
}
// now modify every line if it is longer than the available
// space this way every line will get ... if it doesn't fit
int lineLength = getLengthFromCache(gc, line);
if (lineLength > availableLength) {
if (!this.wordWrapping) {
int numExtraChars = 0;
int newStringLength = line.length();
String trialLabelText = line + DOT;
int newTextExtent = getLengthFromCache(gc, trialLabelText);
while (newTextExtent > availableLength + 1 && newStringLength > 0) {
double avgWidthPerChar = (double) newTextExtent / trialLabelText.length();
numExtraChars += 1 + (int) ((newTextExtent - availableLength) / avgWidthPerChar);
newStringLength = line.length() - numExtraChars;
if (newStringLength > 0) {
trialLabelText = line.substring(0, newStringLength) + DOT;
newTextExtent = getLengthFromCache(gc, trialLabelText);
}
}
if (numExtraChars > line.length()) {
numExtraChars = line.length();
}
// now we have gone too short, lets add chars one at a
// time to exceed the width...
String testString = line;
for (int i = 0; i < line.length(); i++) {
testString = line.substring(0, line.length() + i - numExtraChars) + DOT;
textLength = getLengthFromCache(gc, testString);
if (textLength >= availableLength) {
// now roll back one as this was the first
// number that exceeded
if (line.length() + i - numExtraChars < 1) {
line = EMPTY;
} else {
line = line.substring(0, line.length() + i - numExtraChars - 1) + DOT;
}
break;
}
}
} else {
StringBuilder output = new StringBuilder();
StringBuilder wrap = new StringBuilder();
for (char c : line.toCharArray()) {
wrap.append(c);
int length = getLengthFromCache(gc, wrap.toString());
if (length >= availableLength) {
output.append(wrap.substring(0, wrap.length() - 1)).append(LINE_SEPARATOR);
wrap = new StringBuilder();
wrap.append(c);
}
}
line = output.append(wrap).toString();
}
}
result.append(line);
}
return result.toString();
}
return text;
}
/**
* This method gets only called if automatic length calculation is enabled.
* Calculate the new cell width/height by using the given content length and
* the difference from current cell width/height to available length. If the
* calculated cell is greater than the current set contentLength, update the
* contentLength and execute a corresponding resize command.
*
* @param cell
* the current cell that is painted
* @param contentLength
* the length of the content
*/
protected abstract void setNewMinLength(ILayerCell cell, int contentLength);
/**
* This method is used to determine the padding from the cell to the
* available length. A padding can occur for example by using a
* BeveledBorderDecorator or PaddingDecorator. This TextPainter is called
* with the available space rectangle which is calculated by the wrapping
* painters and decorators by subtracting paddings. As this TextPainter does
* not know his wrapping painters and decorators the existing padding needs
* to be calculated for automatic resizing. Abstract because a horizontal
* TextPainter uses the width while a VerticalTextPainter uses the height of
* the cell and the Rectangle.
*
* @param cell
* the current cell which should be resized
* @param availableLength
* the length value that is available and was given into
* paintCell() as Rectangle argument
* @return the padding between the current cell length - availableLength
*/
protected abstract int calculatePadding(ILayerCell cell, int availableLength);
/**
* Set if the text should be rendered underlined or not.
*
* @param underline
* <code>true</code> if the text should be printed underlined,
* <code>false</code> if not
*/
public void setUnderline(boolean underline) {
this.underline = underline;
}
/**
* Set if the text should be rendered strikethrough or not.
*
* @param strikethrough
* <code>true</code> if the text should be printed strikethrough,
* <code>false</code> if not
*/
public void setStrikethrough(boolean strikethrough) {
this.strikethrough = strikethrough;
}
/**
* @return <code>true</code> if this text painter is calculating the cell
* dimensions by containing text length. For horizontal text
* rendering, this means the <b>width</b> of the cell is calculated
* by content, for vertical text rendering the <b>height</b> is
* calculated.
*/
public boolean isCalculateByTextLength() {
return this.calculateByTextLength;
}
/**
* Configure whether the text painter should calculate the cell dimensions
* by containing text length. For horizontal text rendering, this means the
* <b>width</b> of the cell is calculated by content, for vertical text
* rendering the <b>height</b> is calculated.
*
* @param calculateByTextLength
* <code>true</code> to calculate and modify the cell dimension
* according to the text length, <code>false</code> to not
* modifying the cell dimensions.
*/
public void setCalculateByTextLength(boolean calculateByTextLength) {
this.calculateByTextLength = calculateByTextLength;
}
/**
* @return <code>true</code> if this text painter is calculating the cell
* dimensions by containing text height. For horizontal text
* rendering, this means the <b>height</b> of the cell is calculated
* by content, for vertical text rendering the <b>width</b> is
* calculated.
*/
public boolean isCalculateByTextHeight() {
return this.calculateByTextHeight;
}
/**
* Configure whether the text painter should calculate the cell dimensions
* by containing text height. For horizontal text rendering, this means the
* <b>height</b> of the cell is calculated by content, for vertical text
* rendering the <b>width</b> is calculated.
*
* @param calculateByTextHeight
* <code>true</code> to calculate and modify the cell dimension
* according to the text height, <code>false</code> to not
* modifying the cell dimensions.
*/
public void setCalculateByTextHeight(boolean calculateByTextHeight) {
this.calculateByTextHeight = calculateByTextHeight;
}
/**
* @return <code>true</code> if the text to display should be trimmed,
* <code>false</code> if not. Default is <code>true</code>
*
* @since 1.3
*/
public boolean isTrimText() {
return this.trimText;
}
/**
* @param trimText
* <code>true</code> if the text to display should be trimmed,
* <code>false</code> if not.
*
* @since 1.3
*/
public void setTrimText(boolean trimText) {
this.trimText = trimText;
}
/**
*
* @return <code>true</code> if the text will be wrapped, <code>false</code>
* if not.
*
* @since 1.4
*/
public boolean isWrapText() {
return this.wrapText;
}
/**
*
* @param wrapText
* <code>true</code> if the text should be wrapped,
* <code>false</code> if not.
*
* @since 1.4
*/
public void setWrapText(boolean wrapText) {
this.wrapText = wrapText;
}
/**
* Render a decoration to the text, e.g. underline and/or strikethrough
* lines.
*
* @param cellStyle
* The {@link IStyle} that contains the styling information for
* rendering.
* @param gc
* the {@link GC} used to paint
* @param x
* start x of the text
* @param y
* start y of the text
* @param length
* length of the text
* @param fontHeight
* The height of the current font
*
* @since 1.4
*/
protected void paintDecoration(IStyle cellStyle, GC gc, int x, int y, int length, int fontHeight) {
boolean ul = renderUnderlined(cellStyle);
boolean st = renderStrikethrough(cellStyle);
if ((ul || st) && length > 0) {
// check and draw underline and strikethrough separately
// so it is possible to combine both
if (ul) {
// y = start y of text + font height - half of the font
// descent so the underline is between baseline and bottom
int underlineY = y + fontHeight - (gc.getFontMetrics().getDescent() / 2);
gc.drawLine(x, underlineY, x + length, underlineY);
}
if (st) {
// y = start y of text + half of font height + ascent
// this way lower case characters are also strikethrough
int strikeY = y + (fontHeight / 2) + (gc.getFontMetrics().getLeading() / 2);
gc.drawLine(x, strikeY, x + length, strikeY);
}
}
}
/**
* Return whether word wrapping is enabled or not.
* <p>
* Word wrapping is the wrapping behavior similar to spreadsheet
* applications where words are wrapped if there is not enough space. Text
* wrapping on the other hand only wraps whole words.
* </p>
* <p>
* Enabling this feature could result in slow rendering performance. It is
* therefore disabled by default.
* </p>
* <p>
* <b>Note:</b> If word wrapping is enabled, features like automatic size
* calculation by text length and text wrapping are ignored.
* </p>
*
* @return <code>true</code> if word wrapping is enabled, <code>false</code>
* if not.
*
* @since 1.5
*/
public boolean isWordWrapping() {
return this.wordWrapping;
}
/**
* Configure whether word wrapping should be enabled or not.
* <p>
* Word wrapping is the wrapping behavior similar to spreadsheet
* applications where words are wrapped if there is not enough space. Text
* wrapping on the other hand only wraps whole words.
* </p>
* <p>
* Enabling this feature could result in slow rendering performance. It is
* therefore disabled by default.
* </p>
* <p>
* <b>Note:</b> If word wrapping is enabled, features like automatic size
* calculation by text length and text wrapping are ignored.
* </p>
*
* @param wordWrapping
* <code>true</code> to enable word wrapping, <code>false</code>
* to disable it.
*
* @since 1.5
*/
public void setWordWrapping(boolean wordWrapping) {
this.wordWrapping = wordWrapping;
}
/**
* Return the number of pixels that are added between lines. Default is 0,
* which means that the line height is defined by the font height only.
*
* @return The number of pixels that are added between lines
*
* @since 1.5
*/
public int getLineSpacing() {
return this.lineSpacing;
}
/**
* Specify the number of pixels that should be added between lines. Default
* is 0, which means that the line height is defined by the font height
* only.
*
* @param spacing
* The number of pixels that should be added between lines
*
* @since 1.5
*/
public void setLineSpacing(int spacing) {
this.lineSpacing = spacing;
}
}