/*
* 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.TerminalSize;
import com.googlecode.lanterna.input.KeyStroke;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* This class is the basic building block for creating user interfaces, being the standard implementation of
* {@code Container} that supports multiple children. A {@code Panel} is a component that can contain one or more
* other components, including nested panels. The panel itself doesn't have any particular appearance and isn't
* interactable by itself, although you can set a border for the panel and interactable components inside the panel will
* receive input focus as expected.
*
* @author Martin
*/
public class Panel extends AbstractComponent<Panel> implements Container {
private final List<Component> components;
private LayoutManager layoutManager;
private TerminalSize cachedPreferredSize;
/**
* Default constructor, creates a new panel with no child components and by default set to a vertical
* {@code LinearLayout} layout manager.
*/
public Panel() {
this(new LinearLayout());
}
public Panel(LayoutManager layoutManager) {
if(layoutManager == null) {
layoutManager = new AbsoluteLayout();
}
this.components = new ArrayList<Component>();
this.layoutManager = layoutManager;
this.cachedPreferredSize = null;
}
/**
* Adds a new child component to the panel. Where within the panel the child will be displayed is up to the layout
* manager assigned to this panel. If the component has already been added to another panel, it will first be
* removed from that panel before added to this one.
* @param component Child component to add to this panel
* @return Itself
*/
public Panel addComponent(Component component) {
if(component == null) {
throw new IllegalArgumentException("Cannot add null component");
}
synchronized(components) {
if(components.contains(component)) {
return this;
}
if(component.getParent() != null) {
component.getParent().removeComponent(component);
}
components.add(component);
}
component.onAdded(this);
invalidate();
return this;
}
/**
* This method is a shortcut for calling:
* <pre>
* {@code
* component.setLayoutData(layoutData);
* panel.addComponent(component);
* }
* </pre>
* @param component Component to add to the panel
* @param layoutData Layout data to assign to the component
* @return Itself
*/
public Panel addComponent(Component component, LayoutData layoutData) {
if(component != null) {
component.setLayoutData(layoutData);
addComponent(component);
}
return this;
}
@Override
public boolean containsComponent(Component component) {
return component != null && component.hasParent(this);
}
@Override
public boolean removeComponent(Component component) {
if(component == null) {
throw new IllegalArgumentException("Cannot remove null component");
}
synchronized(components) {
int index = components.indexOf(component);
if(index == -1) {
return false;
}
if(getBasePane() != null && getBasePane().getFocusedInteractable() == component) {
getBasePane().setFocusedInteractable(null);
}
components.remove(index);
}
component.onRemoved(this);
invalidate();
return true;
}
/**
* Removes all child components from this panel
* @return Itself
*/
public Panel removeAllComponents() {
synchronized(components) {
for(Component component : new ArrayList<Component>(components)) {
removeComponent(component);
}
}
return this;
}
/**
* Assigns a new layout manager to this panel, replacing the previous layout manager assigned. Please note that if
* the panel is not empty at the time you assign a new layout manager, the existing components might not show up
* where you expect them and their layout data property might need to be re-assigned.
* @param layoutManager New layout manager this panel should be using
* @return Itself
*/
public synchronized Panel setLayoutManager(LayoutManager layoutManager) {
if(layoutManager == null) {
layoutManager = new AbsoluteLayout();
}
this.layoutManager = layoutManager;
invalidate();
return this;
}
/**
* Returns the layout manager assigned to this panel
* @return Layout manager assigned to this panel
*/
public LayoutManager getLayoutManager() {
return layoutManager;
}
@Override
public int getChildCount() {
synchronized(components) {
return components.size();
}
}
@Override
public Collection<Component> getChildren() {
synchronized(components) {
return new ArrayList<Component>(components);
}
}
@Override
protected ComponentRenderer<Panel> createDefaultRenderer() {
return new ComponentRenderer<Panel>() {
@Override
public TerminalSize getPreferredSize(Panel component) {
synchronized(components) {
cachedPreferredSize = layoutManager.getPreferredSize(components);
}
return cachedPreferredSize;
}
@Override
public void drawComponent(TextGUIGraphics graphics, Panel component) {
if(isInvalid()) {
layout(graphics.getSize());
}
// Reset the area
graphics.applyThemeStyle(getThemeDefinition().getNormal());
graphics.fill(' ');
synchronized(components) {
for(Component child: components) {
TextGUIGraphics componentGraphics = graphics.newTextGraphics(child.getPosition(), child.getSize());
child.draw(componentGraphics);
}
}
}
};
}
@Override
public TerminalSize calculatePreferredSize() {
if(cachedPreferredSize != null && !isInvalid()) {
return cachedPreferredSize;
}
return super.calculatePreferredSize();
}
@Override
public boolean isInvalid() {
synchronized(components) {
for(Component component: components) {
if(component.isInvalid()) {
return true;
}
}
}
return super.isInvalid() || layoutManager.hasChanged();
}
@Override
public Interactable nextFocus(Interactable fromThis) {
boolean chooseNextAvailable = (fromThis == null);
synchronized(components) {
for(Component component : components) {
if(chooseNextAvailable) {
if(component instanceof Interactable && ((Interactable) component).isEnabled() && ((Interactable) component).isFocusable()) {
return (Interactable) component;
}
else if(component instanceof Container) {
Interactable firstInteractable = ((Container) (component)).nextFocus(null);
if(firstInteractable != null) {
return firstInteractable;
}
}
continue;
}
if(component == fromThis) {
chooseNextAvailable = true;
continue;
}
if(component instanceof Container) {
Container container = (Container) component;
if(fromThis.isInside(container)) {
Interactable next = container.nextFocus(fromThis);
if(next == null) {
chooseNextAvailable = true;
}
else {
return next;
}
}
}
}
return null;
}
}
@Override
public Interactable previousFocus(Interactable fromThis) {
boolean chooseNextAvailable = (fromThis == null);
List<Component> revComponents = new ArrayList<Component>();
synchronized(components) {
revComponents.addAll(components);
}
Collections.reverse(revComponents);
for (Component component : revComponents) {
if (chooseNextAvailable) {
if (component instanceof Interactable && ((Interactable)component).isEnabled() && ((Interactable)component).isFocusable()) {
return (Interactable) component;
}
if (component instanceof Container) {
Interactable lastInteractable = ((Container)(component)).previousFocus(null);
if (lastInteractable != null) {
return lastInteractable;
}
}
continue;
}
if (component == fromThis) {
chooseNextAvailable = true;
continue;
}
if (component instanceof Container) {
Container container = (Container) component;
if (fromThis.isInside(container)) {
Interactable next = container.previousFocus(fromThis);
if (next == null) {
chooseNextAvailable = true;
} else {
return next;
}
}
}
}
return null;
}
@Override
public boolean handleInput(KeyStroke key) {
return false;
}
@Override
public void updateLookupMap(InteractableLookupMap interactableLookupMap) {
synchronized(components) {
for(Component component: components) {
if(component instanceof Container) {
((Container)component).updateLookupMap(interactableLookupMap);
}
else if(component instanceof Interactable && ((Interactable)component).isEnabled() && ((Interactable)component).isFocusable()) {
interactableLookupMap.add((Interactable)component);
}
}
}
}
@Override
public void invalidate() {
super.invalidate();
synchronized(components) {
//Propagate
for(Component component: components) {
component.invalidate();
}
}
}
private void layout(TerminalSize size) {
synchronized(components) {
layoutManager.doLayout(size, components);
}
}
}