/* * This file is part of lanterna (https://github.com/mabe02/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 <http://www.gnu.org/licenses/>. * * Copyright (C) 2010-2020 Martin Berglund */ package com.googlecode.lanterna.gui2; import com.googlecode.lanterna.Symbols; import com.googlecode.lanterna.TerminalSize; import com.googlecode.lanterna.TerminalTextUtils; import com.googlecode.lanterna.graphics.ThemeDefinition; /** * This GUI element gives a visual indication of how far a process of some sort has progressed at any given time. It's * a classic user interface component that most people are familiar with. It works based on a scale expressed as having * a <i>minimum</i>, a <i>maximum</i> and a current <i>value</i> somewhere along that range. When the current * <i>value</i> is the same as the <i>minimum</i>, the progress indication is empty, at 0%. If the <i>value</i> is the * same as the <i>maximum</i>, the progress indication is filled, at 100%. Any <i>value</i> in between the * <i>minimum</i> and the <i>maximum</i> will be indicated proportionally to where on this range between <i>minimum</i> * and <i>maximum</i> it is. * <p> * In order to add a label to the progress bar, for example to print the % completed, this class supports adding a * format specification. This label format, before drawing, will be passed in through a {@code String.format(..)} with * the current progress of <i>value</i> from <i>minimum</i> to <i>maximum</i> expressed as a {@code float} passed in as * a single vararg parameter. This parameter will be scaled from 0.0f to 100.0f. By default, the label format is set to * "%2.0f%%" which becomes a simple percentage string when formatted. * @author Martin */ public class ProgressBar extends AbstractComponent<ProgressBar> { private int min; private int max; private int value; private int preferredWidth; private String labelFormat; /** * Creates a new progress bar initially defined with a range from 0 to 100. The */ public ProgressBar() { this(0, 100); } /** * Creates a new progress bar with a defined range of minimum to maximum * @param min The minimum value of this progress bar * @param max The maximum value of this progress bar */ public ProgressBar(int min, int max) { this(min, max, 0); } /** * Creates a new progress bar with a defined range of minimum to maximum and also with a hint as to how wide the * progress bar should be drawn * @param min The minimum value of this progress bar * @param max The maximum value of this progress bar * @param preferredWidth Width size hint, in number of columns, for this progress bar. The renderer may choose to * not use this hint. 0 or less means that there is no hint. */ public ProgressBar(int min, int max, int preferredWidth) { if(min > max) { min = max; } this.min = min; this.max = max; this.value = min; this.labelFormat = "%2.0f%%"; if(preferredWidth < 1) { preferredWidth = 1; } this.preferredWidth = preferredWidth; } /** * Returns the current <i>minimum</i> value for this progress bar * @return The <i>minimum</i> value of this progress bar */ public int getMin() { return min; } /** * Updates the <i>minimum</i> value of this progress bar. If the current <i>maximum</i> and/or <i>value</i> are * smaller than this new <i>minimum</i>, they are automatically adjusted so that the range is still valid. * @param min New <i>minimum</i> value to assign to this progress bar * @return Itself */ public synchronized ProgressBar setMin(int min) { if(min > max) { setMax(min); } if(min > value) { setValue(min); } if(this.min != min) { this.min = min; invalidate(); } return this; } /** * Returns the current <i>maximum</i> value for this progress bar * @return The <i>maximum</i> value of this progress bar */ public int getMax() { return max; } /** * Updates the <i>maximum</i> value of this progress bar. If the current <i>minimum</i> and/or <i>value</i> are * greater than this new <i>maximum</i>, they are automatically adjusted so that the range is still valid. * @param max New <i>maximum</i> value to assign to this progress bar * @return Itself */ public synchronized ProgressBar setMax(int max) { if(max < min) { setMin(max); } if(max < value) { setValue(max); } if(this.max != max) { this.max = max; invalidate(); } return this; } /** * Returns the current <i>value</i> of this progress bar, which represents how complete the progress indication is. * @return The progress value of this progress bar */ public int getValue() { return value; } /** * Updates the <i>value</i> of this progress bar, which will update the visual state. If the value passed in is * outside the <i>minimum-maximum</i> range, it is automatically adjusted. * @param value New value of the progress bar * @return Itself */ public synchronized ProgressBar setValue(int value) { if(value < min) { value = min; } if(value > max) { value = max; } if(this.value != value) { this.value = value; invalidate(); } return this; } /** * Returns the preferred width of the progress bar component, in number of columns. If 0 or less, it should be * interpreted as no preference on width and it's up to the renderer to decide. * @return Preferred width this progress bar would like to have, or 0 (or less) if no preference */ public int getPreferredWidth() { return preferredWidth; } /** * Updated the preferred width hint, which tells the renderer how wide this progress bar would like to be. If called * with 0 (or less), it means no preference on width and the renderer will have to figure out on its own how wide * to make it. * @param preferredWidth New preferred width in number of columns, or 0 if no preference */ public void setPreferredWidth(int preferredWidth) { this.preferredWidth = preferredWidth; } /** * Returns the current label format string which is the template for what the progress bar would like to be the * label printed. Exactly how this label is printed depends on the renderer, but the default renderer will print * the label centered in the middle of the progress indication. * @return The label format template string this progress bar is currently using */ public String getLabelFormat() { return labelFormat; } /** * Sets the label format this progress bar should use when the component is drawn. The string would be compatible * with {@code String.format(..)}, the class will pass the string through that method and pass in the current * progress as a single vararg parameter (passed in as a {@code float} in the range of 0.0f to 100.0f). Setting this * format string to null or empty string will turn off the label rendering. * @param labelFormat Label format to use when drawing the progress bar, or {@code null} to disable * @return Itself */ public synchronized ProgressBar setLabelFormat(String labelFormat) { this.labelFormat = labelFormat; invalidate(); return this; } /** * Returns the current progress of this progress bar's <i>value</i> from <i>minimum</i> to <i>maximum</i>, expressed * as a float from 0.0f to 1.0f. * @return current progress of this progress bar expressed as a float from 0.0f to 1.0f. */ public synchronized float getProgress() { return (float)(value - min) / (float)max; } /** * Returns the label of this progress bar formatted through {@code String.format(..)} with the current progress * value. * @return The progress bar label formatted with the current progress */ public synchronized String getFormattedLabel() { if(labelFormat == null) { return ""; } return String.format(labelFormat, getProgress() * 100.0f); } @Override protected ComponentRenderer<ProgressBar> createDefaultRenderer() { return new DefaultProgressBarRenderer(); } /** * Default implementation of the progress bar GUI component renderer. This renderer will draw the progress bar * on a single line and gradually fill up the space with a different color as the progress is increasing. */ public static class DefaultProgressBarRenderer implements ComponentRenderer<ProgressBar> { @Override public TerminalSize getPreferredSize(ProgressBar component) { int preferredWidth = component.getPreferredWidth(); if(preferredWidth > 0) { return new TerminalSize(preferredWidth, 1); } else if(component.getLabelFormat() != null && !component.getLabelFormat().trim().isEmpty()) { return new TerminalSize(TerminalTextUtils.getColumnWidth(String.format(component.getLabelFormat(), 100.0f)) + 2, 1); } else { return new TerminalSize(10, 1); } } @Override public void drawComponent(TextGUIGraphics graphics, ProgressBar component) { TerminalSize size = graphics.getSize(); if(size.getRows() == 0 || size.getColumns() == 0) { return; } ThemeDefinition themeDefinition = component.getThemeDefinition(); int columnOfProgress = (int)(component.getProgress() * size.getColumns()); String label = component.getFormattedLabel(); int labelRow = size.getRows() / 2; // Adjust label so it fits inside the component int labelWidth = TerminalTextUtils.getColumnWidth(label); // Can't be too smart about this, because of CJK characters if(labelWidth > size.getColumns()) { boolean tail = true; while (labelWidth > size.getColumns()) { if(tail) { label = label.substring(0, label.length() - 1); } else { label = label.substring(1); } tail = !tail; labelWidth = TerminalTextUtils.getColumnWidth(label); } } int labelStartPosition = (size.getColumns() - labelWidth) / 2; for(int row = 0; row < size.getRows(); row++) { graphics.applyThemeStyle(themeDefinition.getActive()); for(int column = 0; column < size.getColumns(); column++) { if(column == columnOfProgress) { graphics.applyThemeStyle(themeDefinition.getNormal()); } if(row == labelRow && column >= labelStartPosition && column < labelStartPosition + labelWidth) { char character = label.charAt(TerminalTextUtils.getStringCharacterIndex(label, column - labelStartPosition)); graphics.setCharacter(column, row, character); if(TerminalTextUtils.isCharDoubleWidth(character)) { column++; if(column == columnOfProgress) { graphics.applyThemeStyle(themeDefinition.getNormal()); } } } else { graphics.setCharacter(column, row, themeDefinition.getCharacter("FILLER", ' ')); } } } } } /** * This progress bar renderer implementation takes slightly more space (three rows) and draws a slightly more * complicates progress bar with fixed measurers to mark 25%, 50% and 75%. Maybe you have seen this one before * somewhere? */ public static class LargeProgressBarRenderer implements ComponentRenderer<ProgressBar> { @Override public TerminalSize getPreferredSize(ProgressBar component) { int preferredWidth = component.getPreferredWidth(); if(preferredWidth > 0) { return new TerminalSize(preferredWidth, 3); } else { return new TerminalSize(42, 3); } } @Override public void drawComponent(TextGUIGraphics graphics, ProgressBar component) { TerminalSize size = graphics.getSize(); if(size.getRows() == 0 || size.getColumns() == 0) { return; } ThemeDefinition themeDefinition = component.getThemeDefinition(); int columnOfProgress = (int)(component.getProgress() * (size.getColumns() - 4)); int mark25 = -1; int mark50 = -1; int mark75 = -1; if(size.getColumns() > 9) { mark50 = (size.getColumns() - 2) / 2; } if(size.getColumns() > 16) { mark25 = (size.getColumns() - 2) / 4; mark75 = mark50 + mark25; } // Draw header, if there are at least 3 rows available int rowOffset = 0; if(size.getRows() >= 3) { graphics.applyThemeStyle(themeDefinition.getNormal()); graphics.drawLine(0, 0, size.getColumns(), 0, ' '); if(size.getColumns() > 1) { graphics.setCharacter(1, 0, '0'); } if(mark25 != -1) { if(component.getProgress() < 0.25f) { graphics.applyThemeStyle(themeDefinition.getInsensitive()); } graphics.putString(1 + mark25, 0, "25"); } if(mark50 != -1) { if(component.getProgress() < 0.50f) { graphics.applyThemeStyle(themeDefinition.getInsensitive()); } graphics.putString(1 + mark50, 0, "50"); } if(mark75 != -1) { if(component.getProgress() < 0.75f) { graphics.applyThemeStyle(themeDefinition.getInsensitive()); } graphics.putString(1 + mark75, 0, "75"); } if(size.getColumns() >= 7) { if(component.getProgress() < 1.0f) { graphics.applyThemeStyle(themeDefinition.getInsensitive()); } graphics.putString(size.getColumns() - 3, 0, "100"); } rowOffset++; } // Draw the main indicator for(int i = 0; i < Math.max(1, size.getRows() - 2); i++) { graphics.applyThemeStyle(themeDefinition.getNormal()); graphics.drawLine(0, rowOffset, size.getColumns(), rowOffset, ' '); if (size.getColumns() > 2) { graphics.setCharacter(1, rowOffset, Symbols.SINGLE_LINE_VERTICAL); } if (size.getColumns() > 3) { graphics.setCharacter(size.getColumns() - 2, rowOffset, Symbols.SINGLE_LINE_VERTICAL); } if (size.getColumns() > 4) { graphics.applyThemeStyle(themeDefinition.getActive()); for(int columnOffset = 2; columnOffset < size.getColumns() - 2; columnOffset++) { if(columnOfProgress + 2 == columnOffset) { graphics.applyThemeStyle(themeDefinition.getNormal()); } if(mark25 == columnOffset - 1) { graphics.setCharacter(columnOffset, rowOffset, Symbols.SINGLE_LINE_VERTICAL); } else if(mark50 == columnOffset - 1) { graphics.setCharacter(columnOffset, rowOffset, Symbols.SINGLE_LINE_VERTICAL); } else if(mark75 == columnOffset - 1) { graphics.setCharacter(columnOffset, rowOffset, Symbols.SINGLE_LINE_VERTICAL); } else { graphics.setCharacter(columnOffset, rowOffset, ' '); } } } if(((int)(component.getProgress() * ((size.getColumns() - 4) * 2))) % 2 == 1) { graphics.applyThemeStyle(themeDefinition.getPreLight()); graphics.setCharacter(columnOfProgress + 2, rowOffset, '|'); } rowOffset++; } // Draw footer if there are at least 2 rows, this one is easy if(size.getRows() >= 2) { graphics.applyThemeStyle(themeDefinition.getNormal()); graphics.drawLine(0, rowOffset, size.getColumns(), rowOffset, Symbols.SINGLE_LINE_T_UP); graphics.setCharacter(0, rowOffset, ' '); if (size.getColumns() > 1) { graphics.setCharacter(size.getColumns() - 1, rowOffset, ' '); } if (size.getColumns() > 2) { graphics.setCharacter(1, rowOffset, Symbols.SINGLE_LINE_BOTTOM_LEFT_CORNER); } if (size.getColumns() > 3) { graphics.setCharacter(size.getColumns() - 2, rowOffset, Symbols.SINGLE_LINE_BOTTOM_RIGHT_CORNER); } } } } }