com.googlecode.lanterna.gui2.AbstractComponent Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of lanterna Show documentation
Show all versions of lanterna Show documentation
Java library for creating text-based terminal GUIs
/*
* 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-2020 Martin Berglund
*/
package com.googlecode.lanterna.gui2;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.bundle.LanternaThemes;
import com.googlecode.lanterna.graphics.Theme;
import com.googlecode.lanterna.graphics.ThemeDefinition;
/**
* AbstractComponent provides some good default behaviour for a {@code Component}, all components in Lanterna extends
* from this class in some way. If you want to write your own component that isn't interactable or theme:able, you
* probably want to extend from this class.
*
* The way you want to declare your new {@code Component} is to pass in itself as the generic parameter, like this:
*
* {@code
* public class MyComponent extends AbstractComponent {
* ...
* }
* }
*
* This was, the component renderer will be correctly setup type-wise and you will need to do fewer typecastings when
* you implement the drawing method your new component.
*
* @author Martin
* @param Should always be itself, this value will be used for the {@code ComponentRenderer} declaration
*/
public abstract class AbstractComponent implements Component {
/**
* Manually set renderer
*/
private ComponentRenderer overrideRenderer;
/**
* If overrideRenderer is not set, this is used instead if not null, set by the theme
*/
private ComponentRenderer themeRenderer;
/**
* To keep track of the theme that created the themeRenderer, so we can reset it if the theme changes
*/
private Theme themeRenderersTheme;
/**
* If the theme had nothing for this component and no override is set, this is the third fallback
*/
private ComponentRenderer defaultRenderer;
private Container parent;
private TerminalSize size;
private TerminalSize explicitPreferredSize; //This is keeping the value set by the user (if setPreferredSize() is used)
private TerminalPosition position;
private Theme themeOverride;
private LayoutData layoutData;
private boolean invalid;
/**
* Default constructor
*/
public AbstractComponent() {
size = TerminalSize.ZERO;
position = TerminalPosition.TOP_LEFT_CORNER;
explicitPreferredSize = null;
layoutData = null;
invalid = true;
parent = null;
overrideRenderer = null;
themeRenderer = null;
themeRenderersTheme = null;
defaultRenderer = null;
}
/**
* When you create a custom component, you need to implement this method and return a Renderer which is responsible
* for taking care of sizing the component, rendering it and choosing where to place the cursor (if Interactable).
* This value is intended to be overridden by custom themes.
* @return Renderer to use when sizing and drawing this component
*/
protected abstract ComponentRenderer createDefaultRenderer();
/**
* Takes a {@code Runnable} and immediately executes it if this is called on the designated GUI thread, otherwise
* schedules it for later invocation.
* @param runnable {@code Runnable} to execute on the GUI thread
*/
protected void runOnGUIThreadIfExistsOtherwiseRunDirect(Runnable runnable) {
if(getTextGUI() != null && getTextGUI().getGUIThread() != null) {
getTextGUI().getGUIThread().invokeLater(runnable);
}
else {
runnable.run();
}
}
/**
* Explicitly sets the {@code ComponentRenderer} to be used when drawing this component. This will override whatever
* the current theme is suggesting or what the default renderer is. If you call this with {@code null}, the override
* is cleared.
* @param renderer {@code ComponentRenderer} to be used when drawing this component
* @return Itself
*/
public T setRenderer(ComponentRenderer renderer) {
this.overrideRenderer = renderer;
return self();
}
@Override
public synchronized ComponentRenderer getRenderer() {
// First try the override
if(overrideRenderer != null) {
return overrideRenderer;
}
// Then try to create and return a renderer from the theme
Theme currentTheme = getTheme();
if((themeRenderer == null && getBasePane() != null) ||
// Check if the theme has changed
themeRenderer != null && currentTheme != themeRenderersTheme) {
themeRenderer = currentTheme.getDefinition(getClass()).getRenderer(selfClass());
if(themeRenderer != null) {
themeRenderersTheme = currentTheme;
}
}
if(themeRenderer != null) {
return themeRenderer;
}
// Finally, fallback to the default renderer
if(defaultRenderer == null) {
defaultRenderer = createDefaultRenderer();
if(defaultRenderer == null) {
throw new IllegalStateException(getClass() + " returned a null default renderer");
}
}
return defaultRenderer;
}
@Override
public void invalidate() {
invalid = true;
}
@Override
public synchronized T setSize(TerminalSize size) {
this.size = size;
return self();
}
@Override
public TerminalSize getSize() {
return size;
}
@Override
public final TerminalSize getPreferredSize() {
if(explicitPreferredSize != null) {
return explicitPreferredSize;
}
else {
return calculatePreferredSize();
}
}
@Override
public final synchronized T setPreferredSize(TerminalSize explicitPreferredSize) {
this.explicitPreferredSize = explicitPreferredSize;
return self();
}
/**
* Invokes the component renderer's size calculation logic and returns the result. This value represents the
* preferred size and isn't necessarily what it will eventually be assigned later on.
* @return Size that the component renderer believes the component should be
*/
protected synchronized TerminalSize calculatePreferredSize() {
return getRenderer().getPreferredSize(self());
}
@Override
public synchronized T setPosition(TerminalPosition position) {
this.position = position;
return self();
}
@Override
public TerminalPosition getPosition() {
return position;
}
@Override
public boolean isInvalid() {
return invalid;
}
@Override
public final synchronized void draw(final TextGUIGraphics graphics) {
//Delegate drawing the component to the renderer
setSize(graphics.getSize());
onBeforeDrawing();
getRenderer().drawComponent(graphics, self());
onAfterDrawing(graphics);
invalid = false;
}
/**
* This method is called just before the component's renderer is invoked for the drawing operation. You can use this
* hook to do some last-minute adjustments to the component, as an alternative to coding it into the renderer
* itself. The component should have the correct size and position at this point, if you call {@code getSize()} and
* {@code getPosition()}.
*/
protected void onBeforeDrawing() {
//No operation by default
}
/**
* This method is called immediately after the component's renderer has finished the drawing operation. You can use
* this hook to do some post-processing if you need, as an alternative to coding it into the renderer. The
* {@code TextGUIGraphics} supplied is the same that was fed into the renderer.
* @param graphics Graphics object you can use to manipulate the appearance of the component
*/
@SuppressWarnings("EmptyMethod")
protected void onAfterDrawing(TextGUIGraphics graphics) {
//No operation by default
}
@Override
public synchronized T setLayoutData(LayoutData data) {
if(layoutData != data) {
layoutData = data;
invalidate();
}
return self();
}
@Override
public LayoutData getLayoutData() {
return layoutData;
}
@Override
public Container getParent() {
return parent;
}
@Override
public boolean hasParent(Container parent) {
if(this.parent == null) {
return false;
}
Container recursiveParent = this.parent;
while(recursiveParent != null) {
if(recursiveParent == parent) {
return true;
}
recursiveParent = recursiveParent.getParent();
}
return false;
}
@Override
public TextGUI getTextGUI() {
if(parent == null) {
return null;
}
return parent.getTextGUI();
}
@Override
public synchronized Theme getTheme() {
if(themeOverride != null) {
return themeOverride;
}
else if(parent != null) {
return parent.getTheme();
}
else if(getBasePane() != null) {
return getBasePane().getTheme();
}
else {
return LanternaThemes.getDefaultTheme();
}
}
@Override
public ThemeDefinition getThemeDefinition() {
return getTheme().getDefinition(getClass());
}
@Override
public synchronized Component setTheme(Theme theme) {
themeOverride = theme;
invalidate();
return this;
}
@Override
public boolean isInside(Container container) {
Component test = this;
while(test.getParent() != null) {
if(test.getParent() == container) {
return true;
}
test = test.getParent();
}
return false;
}
@Override
public BasePane getBasePane() {
if(parent == null) {
return null;
}
return parent.getBasePane();
}
@Override
public TerminalPosition toBasePane(TerminalPosition position) {
Container parent = getParent();
if(parent == null) {
return null;
}
return parent.toBasePane(getPosition().withRelative(position));
}
@Override
public TerminalPosition toGlobal(TerminalPosition position) {
Container parent = getParent();
if(parent == null) {
return null;
}
return parent.toGlobal(getPosition().withRelative(position));
}
@Override
public synchronized Border withBorder(Border border) {
border.setComponent(this);
return border;
}
@Override
public synchronized T addTo(Panel panel) {
panel.addComponent(this);
return self();
}
@Override
public synchronized void onAdded(Container container) {
if (parent != container && parent != null) {
// first inform current parent:
parent.removeComponent(this);
}
parent = container;
}
@Override
public synchronized void onRemoved(Container container) {
if (parent == container) {
parent = null;
themeRenderer = null;
} else {
throw new IllegalStateException(this + " is not " + container +"'s child.");
}
}
/**
* This is a little hack to avoid doing typecasts all over the place when having to return {@code T}. Credit to
* avl42 for this one!
* @return Itself, but as type T
*/
@SuppressWarnings("unchecked")
protected T self() {
return (T)this;
}
@SuppressWarnings("unchecked")
private Class selfClass() {
return (Class)getClass();
}
}