/*
 * 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.TerminalPosition;
import com.googlecode.lanterna.TerminalSize;

import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

Simple layout manager the puts all components on a single line, either horizontally or vertically.
/** * Simple layout manager the puts all components on a single line, either horizontally or vertically. */
public class LinearLayout implements LayoutManager {
This enum type will decide the alignment of a component on the counter-axis, meaning the horizontal alignment on vertical LinearLayouts and vertical alignment on horizontal LinearLayouts.
/** * This enum type will decide the alignment of a component on the counter-axis, meaning the horizontal alignment on * vertical {@code LinearLayout}s and vertical alignment on horizontal {@code LinearLayout}s. */
public enum Alignment {
The component will be placed to the left (for vertical layouts) or top (for horizontal layouts)
/** * The component will be placed to the left (for vertical layouts) or top (for horizontal layouts) */
Beginning,
The component will be placed horizontally centered (for vertical layouts) or vertically centered (for horizontal layouts)
/** * The component will be placed horizontally centered (for vertical layouts) or vertically centered (for * horizontal layouts) */
Center,
The component will be placed to the right (for vertical layouts) or bottom (for horizontal layouts)
/** * The component will be placed to the right (for vertical layouts) or bottom (for horizontal layouts) */
End,
The component will be forced to take up all the horizontal space (for vertical layouts) or vertical space (for horizontal layouts)
/** * The component will be forced to take up all the horizontal space (for vertical layouts) or vertical space * (for horizontal layouts) */
Fill, }
This enum type will what to do with a component if the container has extra space to offer. This can happen if the window runs in full screen or the window has been programmatically set to a fixed size, above the preferred size of the window.
/** * This enum type will what to do with a component if the container has extra space to offer. This can happen if the * window runs in full screen or the window has been programmatically set to a fixed size, above the preferred size * of the window. */
public enum GrowPolicy {
This is the default grow policy, the component will not become larger than the preferred size, even if the container can offer more.
/** * This is the default grow policy, the component will not become larger than the preferred size, even if the * container can offer more. */
None,
With this grow policy, if the container has more space available then this component will be grown to fill the extra space.
/** * With this grow policy, if the container has more space available then this component will be grown to fill * the extra space. */
CanGrow, } private static class LinearLayoutData implements LayoutData { private final Alignment alignment; private final GrowPolicy growPolicy; public LinearLayoutData(Alignment alignment, GrowPolicy growPolicy) { this.alignment = alignment; this.growPolicy = growPolicy; } }
Creates a LayoutData for LinearLayout that assigns a component to a particular alignment on its counter-axis, meaning the horizontal alignment on vertical LinearLayouts and vertical alignment on horizontal LinearLayouts.
Params:
  • alignment – Alignment to store in the LayoutData object
See Also:
Returns:LayoutData object created for LinearLayouts with the specified alignment
/** * Creates a {@code LayoutData} for {@code LinearLayout} that assigns a component to a particular alignment on its * counter-axis, meaning the horizontal alignment on vertical {@code LinearLayout}s and vertical alignment on * horizontal {@code LinearLayout}s. * @param alignment Alignment to store in the {@code LayoutData} object * @return {@code LayoutData} object created for {@code LinearLayout}s with the specified alignment * @see Alignment */
public static LayoutData createLayoutData(Alignment alignment) { return createLayoutData(alignment, GrowPolicy.None); }
Creates a LayoutData for LinearLayout that assigns a component to a particular alignment on its counter-axis, meaning the horizontal alignment on vertical LinearLayouts and vertical alignment on horizontal LinearLayouts.
Params:
  • alignment – Alignment to store in the LayoutData object
  • growPolicy – When policy to apply to the component if the parent container has more space available along the main axis.
See Also:
Returns:LayoutData object created for LinearLayouts with the specified alignment
/** * Creates a {@code LayoutData} for {@code LinearLayout} that assigns a component to a particular alignment on its * counter-axis, meaning the horizontal alignment on vertical {@code LinearLayout}s and vertical alignment on * horizontal {@code LinearLayout}s. * @param alignment Alignment to store in the {@code LayoutData} object * @param growPolicy When policy to apply to the component if the parent container has more space available along * the main axis. * @return {@code LayoutData} object created for {@code LinearLayout}s with the specified alignment * @see Alignment */
public static LayoutData createLayoutData(Alignment alignment, GrowPolicy growPolicy) { return new LinearLayoutData(alignment, growPolicy); } private final Direction direction; private int spacing; private boolean changed;
Default constructor, creates a vertical LinearLayout
/** * Default constructor, creates a vertical {@code LinearLayout} */
public LinearLayout() { this(Direction.VERTICAL); }
Standard constructor that creates a LinearLayout with a specified direction to position the components on
Params:
  • direction – Direction for this Direction
/** * Standard constructor that creates a {@code LinearLayout} with a specified direction to position the components on * @param direction Direction for this {@code Direction} */
public LinearLayout(Direction direction) { this.direction = direction; this.spacing = direction == Direction.HORIZONTAL ? 1 : 0; this.changed = true; }
Sets the amount of empty space to put in between components. For horizontal layouts, this is number of columns (by default 1) and for vertical layouts this is number of rows (by default 0).
Params:
  • spacing – Spacing between components, either in number of columns or rows depending on the direction
Returns:Itself
/** * Sets the amount of empty space to put in between components. For horizontal layouts, this is number of columns * (by default 1) and for vertical layouts this is number of rows (by default 0). * @param spacing Spacing between components, either in number of columns or rows depending on the direction * @return Itself */
public LinearLayout setSpacing(int spacing) { this.spacing = spacing; this.changed = true; return this; }
Returns the amount of empty space to put in between components. For horizontal layouts, this is number of columns (by default 1) and for vertical layouts this is number of rows (by default 0).
Returns:Spacing between components, either in number of columns or rows depending on the direction
/** * Returns the amount of empty space to put in between components. For horizontal layouts, this is number of columns * (by default 1) and for vertical layouts this is number of rows (by default 0). * @return Spacing between components, either in number of columns or rows depending on the direction */
public int getSpacing() { return spacing; } @Override public TerminalSize getPreferredSize(List<Component> components) { // Filter out invisible components components = components.stream().filter(Component::isVisible).collect(Collectors.toList()); if(direction == Direction.VERTICAL) { return getPreferredSizeVertically(components); } else { return getPreferredSizeHorizontally(components); } } private TerminalSize getPreferredSizeVertically(List<Component> components) { int maxWidth = 0; int height = 0; for(Component component: components) { TerminalSize preferredSize = component.getPreferredSize(); if(maxWidth < preferredSize.getColumns()) { maxWidth = preferredSize.getColumns(); } height += preferredSize.getRows(); } height += spacing * (components.size() - 1); return new TerminalSize(maxWidth, Math.max(0, height)); } private TerminalSize getPreferredSizeHorizontally(List<Component> components) { int maxHeight = 0; int width = 0; for(Component component: components) { TerminalSize preferredSize = component.getPreferredSize(); if(maxHeight < preferredSize.getRows()) { maxHeight = preferredSize.getRows(); } width += preferredSize.getColumns(); } width += spacing * (components.size() - 1); return new TerminalSize(Math.max(0,width), maxHeight); } @Override public boolean hasChanged() { return changed; } @Override public void doLayout(TerminalSize area, List<Component> components) { // Filter out invisible components components = components.stream().filter(Component::isVisible).collect(Collectors.toList()); if(direction == Direction.VERTICAL) { if (Boolean.getBoolean("com.googlecode.lanterna.gui2.LinearLayout.useOldNonFlexLayout")) { doVerticalLayout(area, components); } else { doFlexibleVerticalLayout(area, components); } } else { if (Boolean.getBoolean("com.googlecode.lanterna.gui2.LinearLayout.useOldNonFlexLayout")) { doHorizontalLayout(area, components); } else { doFlexibleHorizontalLayout(area, components); } } this.changed = false; } @Deprecated private void doVerticalLayout(TerminalSize area, List<Component> components) { int remainingVerticalSpace = area.getRows(); int availableHorizontalSpace = area.getColumns(); for(Component component: components) { if(remainingVerticalSpace <= 0) { component.setPosition(TerminalPosition.TOP_LEFT_CORNER); component.setSize(TerminalSize.ZERO); } else { Alignment alignment = Alignment.Beginning; LayoutData layoutData = component.getLayoutData(); if (layoutData instanceof LinearLayoutData) { alignment = ((LinearLayoutData)layoutData).alignment; } TerminalSize preferredSize = component.getPreferredSize(); TerminalSize decidedSize = new TerminalSize( Math.min(availableHorizontalSpace, preferredSize.getColumns()), Math.min(remainingVerticalSpace, preferredSize.getRows())); if(alignment == Alignment.Fill) { decidedSize = decidedSize.withColumns(availableHorizontalSpace); alignment = Alignment.Beginning; } TerminalPosition position = component.getPosition(); position = position.withRow(area.getRows() - remainingVerticalSpace); switch(alignment) { case End: position = position.withColumn(availableHorizontalSpace - decidedSize.getColumns()); break; case Center: position = position.withColumn((availableHorizontalSpace - decidedSize.getColumns()) / 2); break; case Beginning: default: position = position.withColumn(0); break; } component.setPosition(position); component.setSize(component.getSize().with(decidedSize)); remainingVerticalSpace -= decidedSize.getRows() + spacing; } } } private void doFlexibleVerticalLayout(TerminalSize area, List<Component> components) { int availableVerticalSpace = area.getRows(); int availableHorizontalSpace = area.getColumns(); List<Component> copyOfComponenets = new ArrayList<>(components); final Map<Component, TerminalSize> fittingMap = new IdentityHashMap<>(); int totalRequiredVerticalSpace = 0; for (Component component: components) { Alignment alignment = Alignment.Beginning; LayoutData layoutData = component.getLayoutData(); if (layoutData instanceof LinearLayoutData) { alignment = ((LinearLayoutData)layoutData).alignment; } TerminalSize preferredSize = component.getPreferredSize(); TerminalSize fittingSize = new TerminalSize( Math.min(availableHorizontalSpace, preferredSize.getColumns()), preferredSize.getRows()); if(alignment == Alignment.Fill) { fittingSize = fittingSize.withColumns(availableHorizontalSpace); } fittingMap.put(component, fittingSize); totalRequiredVerticalSpace += fittingSize.getRows() + spacing; } if (!components.isEmpty()) { // Remove the last spacing totalRequiredVerticalSpace -= spacing; } // If we can't fit everything, trim the down the size of the largest components until it fits if (availableVerticalSpace < totalRequiredVerticalSpace) { copyOfComponenets.sort((o1, o2) -> { // Reverse sort return -Integer.compare(fittingMap.get(o1).getRows(), fittingMap.get(o2).getRows()); }); while (availableVerticalSpace < totalRequiredVerticalSpace) { int largestSize = fittingMap.get(copyOfComponenets.get(0)).getRows(); for (Component largeComponent: copyOfComponenets) { TerminalSize currentSize = fittingMap.get(largeComponent); if (largestSize > currentSize.getRows()) { break; } fittingMap.put(largeComponent, currentSize.withRelativeRows(-1)); totalRequiredVerticalSpace--; } } } // If we have more space available than we need, grow components to fill if (availableVerticalSpace > totalRequiredVerticalSpace) { boolean resizedOneComponent = false; while (availableVerticalSpace > totalRequiredVerticalSpace) { for(Component component: components) { final LinearLayoutData layoutData = (LinearLayoutData)component.getLayoutData(); final TerminalSize currentSize = fittingMap.get(component); if (layoutData != null && layoutData.growPolicy == GrowPolicy.CanGrow) { fittingMap.put(component, currentSize.withRelativeRows(1)); availableVerticalSpace--; resizedOneComponent = true; } if (availableVerticalSpace <= totalRequiredVerticalSpace) { break; } } if (!resizedOneComponent) { break; } } } // Assign the sizes and positions int topPosition = 0; for(Component component: components) { Alignment alignment = Alignment.Beginning; LayoutData layoutData = component.getLayoutData(); if (layoutData instanceof LinearLayoutData) { alignment = ((LinearLayoutData)layoutData).alignment; } TerminalSize decidedSize = fittingMap.get(component); TerminalPosition position = component.getPosition(); position = position.withRow(topPosition); switch(alignment) { case End: position = position.withColumn(availableHorizontalSpace - decidedSize.getColumns()); break; case Center: position = position.withColumn((availableHorizontalSpace - decidedSize.getColumns()) / 2); break; case Beginning: default: position = position.withColumn(0); break; } component.setPosition(component.getPosition().with(position)); component.setSize(component.getSize().with(decidedSize)); topPosition += decidedSize.getRows() + spacing; } } @Deprecated private void doHorizontalLayout(TerminalSize area, List<Component> components) { int remainingHorizontalSpace = area.getColumns(); int availableVerticalSpace = area.getRows(); for(Component component: components) { if(remainingHorizontalSpace <= 0) { component.setPosition(TerminalPosition.TOP_LEFT_CORNER); component.setSize(TerminalSize.ZERO); } else { Alignment alignment = Alignment.Beginning; LayoutData layoutData = component.getLayoutData(); if (layoutData instanceof LinearLayoutData) { alignment = ((LinearLayoutData)layoutData).alignment; } TerminalSize preferredSize = component.getPreferredSize(); TerminalSize decidedSize = new TerminalSize( Math.min(remainingHorizontalSpace, preferredSize.getColumns()), Math.min(availableVerticalSpace, preferredSize.getRows())); if(alignment == Alignment.Fill) { decidedSize = decidedSize.withRows(availableVerticalSpace); alignment = Alignment.Beginning; } TerminalPosition position = component.getPosition(); position = position.withColumn(area.getColumns() - remainingHorizontalSpace); switch(alignment) { case End: position = position.withRow(availableVerticalSpace - decidedSize.getRows()); break; case Center: position = position.withRow((availableVerticalSpace - decidedSize.getRows()) / 2); break; case Beginning: default: position = position.withRow(0); break; } component.setPosition(position); component.setSize(component.getSize().with(decidedSize)); remainingHorizontalSpace -= decidedSize.getColumns() + spacing; } } } private void doFlexibleHorizontalLayout(TerminalSize area, List<Component> components) { int availableVerticalSpace = area.getRows(); int availableHorizontalSpace = area.getColumns(); List<Component> copyOfComponenets = new ArrayList<>(components); final Map<Component, TerminalSize> fittingMap = new IdentityHashMap<>(); int totalRequiredHorizontalSpace = 0; for (Component component: components) { Alignment alignment = Alignment.Beginning; LayoutData layoutData = component.getLayoutData(); if (layoutData instanceof LinearLayoutData) { alignment = ((LinearLayoutData)layoutData).alignment; } TerminalSize preferredSize = component.getPreferredSize(); TerminalSize fittingSize = new TerminalSize( preferredSize.getColumns(), Math.min(availableVerticalSpace, preferredSize.getRows())); if(alignment == Alignment.Fill) { fittingSize = fittingSize.withRows(availableVerticalSpace); } fittingMap.put(component, fittingSize); totalRequiredHorizontalSpace += fittingSize.getColumns() + spacing; } if (!components.isEmpty()) { // Remove the last spacing totalRequiredHorizontalSpace -= spacing; } // If we can't fit everything, trim the down the size of the largest components until it fits if (availableHorizontalSpace < totalRequiredHorizontalSpace) { copyOfComponenets.sort((o1, o2) -> { // Reverse sort return -Integer.compare(fittingMap.get(o1).getColumns(), fittingMap.get(o2).getColumns()); }); while (availableHorizontalSpace < totalRequiredHorizontalSpace) { int largestSize = fittingMap.get(copyOfComponenets.get(0)).getColumns(); for (Component largeComponent: copyOfComponenets) { TerminalSize currentSize = fittingMap.get(largeComponent); if (largestSize > currentSize.getColumns()) { break; } fittingMap.put(largeComponent, currentSize.withRelativeColumns(-1)); totalRequiredHorizontalSpace--; } } } // If we have more space available than we need, grow components to fill if (availableHorizontalSpace > totalRequiredHorizontalSpace) { boolean resizedOneComponent = false; while (availableHorizontalSpace > totalRequiredHorizontalSpace) { for(Component component: components) { final LinearLayoutData layoutData = (LinearLayoutData)component.getLayoutData(); final TerminalSize currentSize = fittingMap.get(component); if (layoutData != null && layoutData.growPolicy == GrowPolicy.CanGrow) { fittingMap.put(component, currentSize.withRelativeColumns(1)); availableHorizontalSpace--; resizedOneComponent = true; } if (availableHorizontalSpace <= totalRequiredHorizontalSpace) { break; } } if (!resizedOneComponent) { break; } } } // Assign the sizes and positions int leftPosition = 0; for(Component component: components) { Alignment alignment = Alignment.Beginning; LayoutData layoutData = component.getLayoutData(); if (layoutData instanceof LinearLayoutData) { alignment = ((LinearLayoutData)layoutData).alignment; } TerminalSize decidedSize = fittingMap.get(component); TerminalPosition position = component.getPosition(); position = position.withColumn(leftPosition); switch(alignment) { case End: position = position.withRow(availableVerticalSpace - decidedSize.getRows()); break; case Center: position = position.withRow((availableVerticalSpace - decidedSize.getRows()) / 2); break; case Beginning: default: position = position.withRow(0); break; } component.setPosition(component.getPosition().with(position)); component.setSize(component.getSize().with(decidedSize)); leftPosition += decidedSize.getColumns() + spacing; } } }