/*
* 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 <http://www.gnu.org/licenses/>.
*
* Copyright (C) 2010-2017 Martin Berglund
*/
package com.googlecode.lanterna.gui2;
import com.googlecode.lanterna.Symbols;
import com.googlecode.lanterna.TerminalTextUtils;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.graphics.ThemeDefinition;
import com.googlecode.lanterna.input.KeyStroke;
import com.googlecode.lanterna.input.KeyType;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Simple labeled button that the user can trigger by pressing the Enter or the Spacebar key on the keyboard when the
* component is in focus. You can specify an initial action through one of the constructors and you can also add
* additional actions to the button using {@link #addListener(Listener)}. To remove a previously attached action, use
* {@link #removeListener(Listener)}.
* @author Martin
*/
public class Button extends AbstractInteractableComponent<Button> {
/**
* Listener interface that can be used to catch user events on the button
*/
public interface Listener {
/**
* This is called when the user has triggered the button
* @param button Button which was triggered
*/
void onTriggered(Button button);
}
private final List<Listener> listeners;
private String label;
/**
* Creates a new button with a specific label and no initially attached action.
* @param label Label to put on the button
*/
public Button(String label) {
this.listeners = new CopyOnWriteArrayList<Listener>();
setLabel(label);
}
/**
* Creates a new button with a label and an associated action to fire when triggered by the user
* @param label Label to put on the button
* @param action Action to fire when the user triggers the button by pressing the enter or the space key
*/
public Button(String label, final Runnable action) {
this(label);
listeners.add(new Listener() {
@Override
public void onTriggered(Button button) {
action.run();
}
});
}
@Override
protected ButtonRenderer createDefaultRenderer() {
return new DefaultButtonRenderer();
}
@Override
public synchronized TerminalPosition getCursorLocation() {
return getRenderer().getCursorLocation(this);
}
@Override
public synchronized Result handleKeyStroke(KeyStroke keyStroke) {
if(keyStroke.getKeyType() == KeyType.Enter ||
(keyStroke.getKeyType() == KeyType.Character && keyStroke.getCharacter() == ' ')) {
triggerActions();
return Result.HANDLED;
}
return super.handleKeyStroke(keyStroke);
}
protected synchronized void triggerActions() {
for(Listener listener: listeners) {
listener.onTriggered(this);
}
}
/**
* Updates the label on the button to the specified string
* @param label New label to use on the button
*/
public final synchronized void setLabel(String label) {
if(label == null) {
throw new IllegalArgumentException("null label to a button is not allowed");
}
if(label.isEmpty()) {
label = " ";
}
this.label = label;
invalidate();
}
/**
* Adds a listener to notify when the button is triggered; the listeners will be called serially in the order they
* were added
* @param listener Listener to call when the button is triggered
*/
public void addListener(Listener listener) {
if(listener == null) {
throw new IllegalArgumentException("null listener to a button is not allowed");
}
listeners.add(listener);
}
/**
* Removes a listener from the button's list of listeners to call when the button is triggered. If the listener list
* doesn't contain the listener specified, this call do with do nothing.
* @param listener Listener to remove from this button's listener list
* @return {@code true} if this button contained the specified listener
*/
public boolean removeListener(Listener listener) {
return listeners.remove(listener);
}
/**
* Returns the label current assigned to the button
* @return Label currently used by the button
*/
public String getLabel() {
return label;
}
@Override
public String toString() {
return "Button{" + label + "}";
}
/**
* Helper interface that doesn't add any new methods but makes coding new button renderers a little bit more clear
*/
public interface ButtonRenderer extends InteractableRenderer<Button> {
}
/**
* This is the default button renderer that is used if you don't override anything. With this renderer, buttons are
* drawn on a single line, with the label inside of "<" and ">".
*/
public static class DefaultButtonRenderer implements ButtonRenderer {
@Override
public TerminalPosition getCursorLocation(Button button) {
if(button.getThemeDefinition().isCursorVisible()) {
return new TerminalPosition(1 + getLabelShift(button, button.getSize()), 0);
}
else {
return null;
}
}
@Override
public TerminalSize getPreferredSize(Button button) {
return new TerminalSize(Math.max(8, TerminalTextUtils.getColumnWidth(button.getLabel()) + 2), 1);
}
@Override
public void drawComponent(TextGUIGraphics graphics, Button button) {
ThemeDefinition themeDefinition = button.getThemeDefinition();
if(button.isFocused()) {
graphics.applyThemeStyle(themeDefinition.getActive());
}
else {
graphics.applyThemeStyle(themeDefinition.getInsensitive());
}
graphics.fill(' ');
graphics.setCharacter(0, 0, themeDefinition.getCharacter("LEFT_BORDER", '<'));
graphics.setCharacter(graphics.getSize().getColumns() - 1, 0, themeDefinition.getCharacter("RIGHT_BORDER", '>'));
if(button.isFocused()) {
graphics.applyThemeStyle(themeDefinition.getActive());
}
else {
graphics.applyThemeStyle(themeDefinition.getPreLight());
}
int labelShift = getLabelShift(button, graphics.getSize());
graphics.setCharacter(1 + labelShift, 0, button.getLabel().charAt(0));
if(TerminalTextUtils.getColumnWidth(button.getLabel()) == 1) {
return;
}
if(button.isFocused()) {
graphics.applyThemeStyle(themeDefinition.getSelected());
}
else {
graphics.applyThemeStyle(themeDefinition.getNormal());
}
graphics.putString(1 + labelShift + 1, 0, button.getLabel().substring(1));
}
private int getLabelShift(Button button, TerminalSize size) {
int availableSpace = size.getColumns() - 2;
if(availableSpace <= 0) {
return 0;
}
int labelShift = 0;
int widthInColumns = TerminalTextUtils.getColumnWidth(button.getLabel());
if(availableSpace > widthInColumns) {
labelShift = (size.getColumns() - 2 - widthInColumns) / 2;
}
return labelShift;
}
}
/**
* Alternative button renderer that displays buttons with just the label and minimal decoration
*/
public static class FlatButtonRenderer implements ButtonRenderer {
@Override
public TerminalPosition getCursorLocation(Button component) {
return null;
}
@Override
public TerminalSize getPreferredSize(Button component) {
return new TerminalSize(TerminalTextUtils.getColumnWidth(component.getLabel()), 1);
}
@Override
public void drawComponent(TextGUIGraphics graphics, Button button) {
ThemeDefinition themeDefinition = button.getThemeDefinition();
if(button.isFocused()) {
graphics.applyThemeStyle(themeDefinition.getActive());
}
else {
graphics.applyThemeStyle(themeDefinition.getInsensitive());
}
graphics.fill(' ');
if(button.isFocused()) {
graphics.applyThemeStyle(themeDefinition.getSelected());
}
else {
graphics.applyThemeStyle(themeDefinition.getNormal());
}
graphics.putString(0, 0, button.getLabel());
}
}
public static class BorderedButtonRenderer implements ButtonRenderer {
@Override
public TerminalPosition getCursorLocation(Button component) {
return null;
}
@Override
public TerminalSize getPreferredSize(Button component) {
return new TerminalSize(TerminalTextUtils.getColumnWidth(component.getLabel()) + 5, 4);
}
@Override
public void drawComponent(TextGUIGraphics graphics, Button button) {
ThemeDefinition themeDefinition = button.getThemeDefinition();
graphics.applyThemeStyle(themeDefinition.getNormal());
TerminalSize size = graphics.getSize();
graphics.drawLine(1, 0, size.getColumns() - 3, 0, Symbols.SINGLE_LINE_HORIZONTAL);
graphics.drawLine(1, size.getRows() - 2, size.getColumns() - 3, size.getRows() - 2, Symbols.SINGLE_LINE_HORIZONTAL);
graphics.drawLine(0, 1, 0, size.getRows() - 3, Symbols.SINGLE_LINE_VERTICAL);
graphics.drawLine(size.getColumns() - 2, 1, size.getColumns() - 2, size.getRows() - 3, Symbols.SINGLE_LINE_VERTICAL);
graphics.setCharacter(0, 0, Symbols.SINGLE_LINE_TOP_LEFT_CORNER);
graphics.setCharacter(size.getColumns() - 2, 0, Symbols.SINGLE_LINE_TOP_RIGHT_CORNER);
graphics.setCharacter(size.getColumns() - 2, size.getRows() - 2, Symbols.SINGLE_LINE_BOTTOM_RIGHT_CORNER);
graphics.setCharacter(0, size.getRows() - 2, Symbols.SINGLE_LINE_BOTTOM_LEFT_CORNER);
// Fill the inner part of the box
graphics.drawLine(1, 1, size.getColumns() - 3, 1, ' ');
// Draw the text inside the button
if(button.isFocused()) {
graphics.applyThemeStyle(themeDefinition.getActive());
}
graphics.putString(2, 1, TerminalTextUtils.fitString(button.getLabel(), size.getColumns() - 5));
// Draw the shadow
graphics.applyThemeStyle(themeDefinition.getInsensitive());
graphics.drawLine(1, size.getRows() - 1, size.getColumns() - 1, size.getRows() - 1, ' ');
graphics.drawLine(size.getColumns() - 1, 1, size.getColumns() - 1, size.getRows() - 2, ' ');
}
}
}