/*
* This file is part of lanterna (http://code.google.com/p/lanterna/).
*
* lanterna is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*
* Copyright (C) 2010-2015 Martin
*/
package com.googlecode.lanterna.gui2;
import com.googlecode.lanterna.TerminalTextUtils;
import com.googlecode.lanterna.SGR;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.graphics.ThemeDefinition;
import java.util.EnumSet;
import java.util.List;
/**
* Label is a simple read-only text display component. It supports customized colors and multi-line text.
* @author Martin
*/
public class Label extends AbstractComponent {
private String[] lines;
private Integer labelWidth;
private TerminalSize labelSize;
private TextColor foregroundColor;
private TextColor backgroundColor;
private final EnumSet additionalStyles;
/**
* Main constructor, creates a new Label displaying a specific text.
* @param text Text the label will display
*/
public Label(String text) {
this.lines = null;
this.labelSize = TerminalSize.ZERO;
this.labelWidth = 0;
this.foregroundColor = null;
this.backgroundColor = null;
this.additionalStyles = EnumSet.noneOf(SGR.class);
setText(text);
}
/**
* Protected access to set the internal representation of the text in this label, to be used by sub-classes of label
* in certain cases where {@code setText(..)} doesn't work. In general, you probably want to stick to
* {@code setText(..)} instead of this method unless you have a good reason not to.
* @param lines New lines this label will display
*/
protected void setLines(String[] lines) {
this.lines = lines;
}
/**
* Updates the text this label is displaying
* @param text New text to display
*/
public synchronized void setText(String text) {
setLines(splitIntoMultipleLines(text));
this.labelSize = getBounds(lines, labelSize);
invalidate();
}
/**
* Returns the text this label is displaying. Multi-line labels will have their text concatenated with \n, even if
* they were originally set using multi-line text having \r\n as line terminators.
* @return String of the text this label is displaying
*/
public synchronized String getText() {
if(lines.length == 0) {
return "";
}
StringBuilder bob = new StringBuilder(lines[0]);
for(int i = 1; i < lines.length; i++) {
bob.append("\n").append(lines[i]);
}
return bob.toString();
}
/**
* Utility method for taking a string and turning it into an array of lines. This method is used in order to deal
* with line endings consistently.
* @param text Text to split
* @return Array of strings that forms the lines of the original string
*/
protected String[] splitIntoMultipleLines(String text) {
return text.replace("\r", "").split("\n");
}
/**
* Returns the area, in terminal columns and rows, required to fully draw the lines passed in.
* @param lines Lines to measure the size of
* @param currentBounds Optional (can pass {@code null}) terminal size to use for storing the output values. If the
* method is called many times and always returning the same value, passing in an external
* reference of this size will avoid creating new {@code TerminalSize} objects every time
* @return Size that is required to draw the lines
*/
protected TerminalSize getBounds(String[] lines, TerminalSize currentBounds) {
if(currentBounds == null) {
currentBounds = TerminalSize.ZERO;
}
currentBounds = currentBounds.withRows(lines.length);
if(labelWidth == null || labelWidth == 0) {
int preferredWidth = 0;
for(String line : lines) {
int lineWidth = TerminalTextUtils.getColumnWidth(line);
if(preferredWidth < lineWidth) {
preferredWidth = lineWidth;
}
}
currentBounds = currentBounds.withColumns(preferredWidth);
}
else {
List wordWrapped = TerminalTextUtils.getWordWrappedText(labelWidth, lines);
currentBounds = currentBounds.withColumns(labelWidth).withRows(wordWrapped.size());
}
return currentBounds;
}
/**
* Overrides the current theme's foreground color and use the one specified. If called with {@code null}, the
* override is cleared and the theme is used again.
* @param foregroundColor Foreground color to use when drawing the label, if {@code null} then use the theme's
* default
* @return Itself
*/
public synchronized Label setForegroundColor(TextColor foregroundColor) {
this.foregroundColor = foregroundColor;
return this;
}
/**
* Returns the foreground color used when drawing the label, or {@code null} if the color is read from the current
* theme.
* @return Foreground color used when drawing the label, or {@code null} if the color is read from the current
* theme.
*/
public TextColor getForegroundColor() {
return foregroundColor;
}
/**
* Overrides the current theme's background color and use the one specified. If called with {@code null}, the
* override is cleared and the theme is used again.
* @param backgroundColor Background color to use when drawing the label, if {@code null} then use the theme's
* default
* @return Itself
*/
public synchronized Label setBackgroundColor(TextColor backgroundColor) {
this.backgroundColor = backgroundColor;
return this;
}
/**
* Returns the background color used when drawing the label, or {@code null} if the color is read from the current
* theme.
* @return Background color used when drawing the label, or {@code null} if the color is read from the current
* theme.
*/
public TextColor getBackgroundColor() {
return backgroundColor;
}
/**
* Adds an additional SGR style to use when drawing the label, in case it wasn't enabled by the theme
* @param sgr SGR style to enable for this label
* @return Itself
*/
public synchronized Label addStyle(SGR sgr) {
additionalStyles.add(sgr);
return this;
}
/**
* Removes an additional SGR style used when drawing the label, previously added by {@code addStyle(..)}. If the
* style you are trying to remove is specified by the theme, calling this method will have no effect.
* @param sgr SGR style to remove
* @return Itself
*/
public synchronized Label removeStyle(SGR sgr) {
additionalStyles.remove(sgr);
return this;
}
/**
* Use this method to limit how wide the label can grow. If set to {@code null} there is no limit but if set to a
* positive integer then the preferred size will be calculated using word wrapping for lines that are longer than
* this label width. This may make the label increase in height as new rows may be requested. Please note that some
* layout managers might assign more space to the label and because of this the wrapping might not be as you expect
* it. If set to 0, the label will request the same space as if set to {@code null}, but when drawing it will apply
* word wrapping instead of truncation in order to fit the label inside the designated area if it's smaller than
* what was requested. By default this is set to 0.
*
* @param labelWidth Either {@code null} or 0 for no limit on how wide the label can be, where 0 indicates word
* wrapping should be used if the assigned area is smaller than the requested size, or a positive
* integer setting the requested maximum width at what point word wrapping will begin
* @return Itself
*/
public synchronized Label setLabelWidth(Integer labelWidth) {
this.labelWidth = labelWidth;
return this;
}
/**
* Returns the limit how wide the label can grow. If set to {@code null} or 0 there is no limit but if set to a
* positive integer then the preferred size will be calculated using word wrapping for lines that are longer than
* the label width. This may make the label increase in height as new rows may be requested. Please note that some
* layout managers might assign more space to the label and because of this the wrapping might not be as you expect
* it. If set to 0, the label will request the same space as if set to {@code null}, but when drawing it will apply
* word wrapping instead of truncation in order to fit the label inside the designated area if it's smaller than
* what was requested.
* @return Either {@code null} or 0 for no limit on how wide the label can be, where 0 indicates word
* wrapping should be used if the assigned area is smaller than the requested size, or a positive
* integer setting the requested maximum width at what point word wrapping will begin
*/
public Integer getLabelWidth() {
return labelWidth;
}
@Override
protected ComponentRenderer createDefaultRenderer() {
return new ComponentRenderer() {
@Override
public TerminalSize getPreferredSize(Label Label) {
return labelSize;
}
@Override
public void drawComponent(TextGUIGraphics graphics, Label component) {
ThemeDefinition themeDefinition = graphics.getThemeDefinition(Label.class);
graphics.applyThemeStyle(themeDefinition.getNormal());
if(foregroundColor != null) {
graphics.setForegroundColor(foregroundColor);
}
if(backgroundColor != null) {
graphics.setBackgroundColor(backgroundColor);
}
for(SGR sgr: additionalStyles) {
graphics.enableModifiers(sgr);
}
String[] linesToDraw;
if(component.getLabelWidth() == null) {
linesToDraw = component.lines;
}
else {
linesToDraw = TerminalTextUtils.getWordWrappedText(graphics.getSize().getColumns(), component.lines).toArray(new String[0]);
}
for(int row = 0; row < Math.min(graphics.getSize().getRows(), linesToDraw.length); row++) {
String line = linesToDraw[row];
if(graphics.getSize().getColumns() >= labelSize.getColumns()) {
graphics.putString(0, row, line);
}
else {
int availableColumns = graphics.getSize().getColumns();
String fitString = TerminalTextUtils.fitString(line, availableColumns);
graphics.putString(0, row, fitString);
}
}
}
};
}
}