/* * 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.TerminalPosition; import com.googlecode.lanterna.TerminalSize; /** * 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 { private ComponentRenderer renderer; 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 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; renderer = null; //Will be set on the first call to getRenderer() } /** * 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(); /** * This will attempt to dynamically construct a {@code ComponentRenderer} class from a string, assumed to be passed * in from a theme. This makes it possible to create themes that supplies their own {@code ComponentRenderers} that * can even replace the ones built into lanterna and used for the bundled components. * * @param className Fully qualified name of the {@code ComponentRenderer} we want to instatiate * @return {@code null} if {@code className} was null, otherwise the {@code ComponentRenderer} instance * @throws RuntimeException If there were any problems instatiating the class */ @SuppressWarnings("unchecked") protected ComponentRenderer getRendererFromTheme(String className) { if(className == null) { return null; } try { return (ComponentRenderer)Class.forName(className).newInstance(); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } /** * 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. * @param renderer {@code ComponentRenderer} to be used when drawing this component * @return Itself */ public T setRenderer(ComponentRenderer renderer) { this.renderer = renderer; return self(); } @Override public synchronized ComponentRenderer getRenderer() { if(renderer == null) { renderer = createDefaultRenderer(); if(renderer == null) { throw new IllegalStateException(getClass() + " returns a null default renderer"); } } return renderer; } @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) { if(getRenderer() == null) { ComponentRenderer renderer = getRendererFromTheme(graphics.getThemeDefinition(getClass()).getRenderer()); if(renderer == null) { renderer = createDefaultRenderer(); if(renderer == null) { throw new IllegalStateException(getClass() + " returned a null default renderer"); } } setRenderer(renderer); } //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 */ 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 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) { parent = container; } @Override public synchronized void onRemoved(Container container) { parent = null; } /** * 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; } }