| 1 | /* |
| 2 | * This file is part of lanterna (http://code.google.com/p/lanterna/). |
| 3 | * |
| 4 | * lanterna is free software: you can redistribute it and/or modify |
| 5 | * it under the terms of the GNU Lesser General Public License as published by |
| 6 | * the Free Software Foundation, either version 3 of the License, or |
| 7 | * (at your option) any later version. |
| 8 | * |
| 9 | * This program is distributed in the hope that it will be useful, |
| 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | * GNU Lesser General Public License for more details. |
| 13 | * |
| 14 | * You should have received a copy of the GNU Lesser General Public License |
| 15 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | * |
| 17 | * Copyright (C) 2010-2015 Martin |
| 18 | */ |
| 19 | package com.googlecode.lanterna.gui2; |
| 20 | |
| 21 | import com.googlecode.lanterna.TerminalPosition; |
| 22 | import com.googlecode.lanterna.TerminalSize; |
| 23 | |
| 24 | import java.util.List; |
| 25 | |
| 26 | /** |
| 27 | * Simple layout manager the puts all components on a single line, either horizontally or vertically. |
| 28 | */ |
| 29 | public class LinearLayout implements LayoutManager { |
| 30 | /** |
| 31 | * This enum type will decide the alignment of a component on the counter-axis, meaning the horizontal alignment on |
| 32 | * vertical {@code LinearLayout}s and vertical alignment on horizontal {@code LinearLayout}s. |
| 33 | */ |
| 34 | public enum Alignment { |
| 35 | /** |
| 36 | * The component will be placed to the left (for vertical layouts) or top (for horizontal layouts) |
| 37 | */ |
| 38 | Beginning, |
| 39 | /** |
| 40 | * The component will be placed horizontally centered (for vertical layouts) or vertically centered (for |
| 41 | * horizontal layouts) |
| 42 | */ |
| 43 | Center, |
| 44 | /** |
| 45 | * The component will be placed to the right (for vertical layouts) or bottom (for horizontal layouts) |
| 46 | */ |
| 47 | End, |
| 48 | /** |
| 49 | * The component will be forced to take up all the horizontal space (for vertical layouts) or vertical space |
| 50 | * (for horizontal layouts) |
| 51 | */ |
| 52 | Fill, |
| 53 | } |
| 54 | |
| 55 | private static class LinearLayoutData implements LayoutData { |
| 56 | private final Alignment alignment; |
| 57 | |
| 58 | public LinearLayoutData(Alignment alignment) { |
| 59 | this.alignment = alignment; |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | /** |
| 64 | * Creates a {@code LayoutData} for {@code LinearLayout} that assigns a component to a particular alignment on its |
| 65 | * counter-axis, meaning the horizontal alignment on vertical {@code LinearLayout}s and vertical alignment on |
| 66 | * horizontal {@code LinearLayout}s. |
| 67 | * @param alignment Alignment to store in the {@code LayoutData} object |
| 68 | * @return {@code LayoutData} object created for {@code LinearLayout}s with the specified alignment |
| 69 | * @see Alignment |
| 70 | */ |
| 71 | public static LayoutData createLayoutData(Alignment alignment) { |
| 72 | return new LinearLayoutData(alignment); |
| 73 | } |
| 74 | |
| 75 | private final Direction direction; |
| 76 | private int spacing; |
| 77 | private boolean changed; |
| 78 | |
| 79 | /** |
| 80 | * Default constructor, creates a vertical {@code LinearLayout} |
| 81 | */ |
| 82 | public LinearLayout() { |
| 83 | this(Direction.VERTICAL); |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * Standard constructor that creates a {@code LinearLayout} with a specified direction to position the components on |
| 88 | * @param direction Direction for this {@code Direction} |
| 89 | */ |
| 90 | public LinearLayout(Direction direction) { |
| 91 | this.direction = direction; |
| 92 | this.spacing = direction == Direction.HORIZONTAL ? 1 : 0; |
| 93 | this.changed = true; |
| 94 | } |
| 95 | |
| 96 | /** |
| 97 | * Sets the amount of empty space to put in between components. For horizontal layouts, this is number of columns |
| 98 | * (by default 1) and for vertical layouts this is number of rows (by default 0). |
| 99 | * @param spacing Spacing between components, either in number of columns or rows depending on the direction |
| 100 | * @return Itself |
| 101 | */ |
| 102 | public LinearLayout setSpacing(int spacing) { |
| 103 | this.spacing = spacing; |
| 104 | this.changed = true; |
| 105 | return this; |
| 106 | } |
| 107 | |
| 108 | /** |
| 109 | * Returns the amount of empty space to put in between components. For horizontal layouts, this is number of columns |
| 110 | * (by default 1) and for vertical layouts this is number of rows (by default 0). |
| 111 | * @return Spacing between components, either in number of columns or rows depending on the direction |
| 112 | */ |
| 113 | public int getSpacing() { |
| 114 | return spacing; |
| 115 | } |
| 116 | |
| 117 | @Override |
| 118 | public TerminalSize getPreferredSize(List<Component> components) { |
| 119 | if(direction == Direction.VERTICAL) { |
| 120 | return getPreferredSizeVertically(components); |
| 121 | } |
| 122 | else { |
| 123 | return getPreferredSizeHorizontally(components); |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | private TerminalSize getPreferredSizeVertically(List<Component> components) { |
| 128 | int maxWidth = 0; |
| 129 | int height = 0; |
| 130 | for(Component component: components) { |
| 131 | TerminalSize preferredSize = component.getPreferredSize(); |
| 132 | if(maxWidth < preferredSize.getColumns()) { |
| 133 | maxWidth = preferredSize.getColumns(); |
| 134 | } |
| 135 | height += preferredSize.getRows(); |
| 136 | } |
| 137 | height += spacing * (components.size() - 1); |
| 138 | return new TerminalSize(maxWidth, height); |
| 139 | } |
| 140 | |
| 141 | private TerminalSize getPreferredSizeHorizontally(List<Component> components) { |
| 142 | int maxHeight = 0; |
| 143 | int width = 0; |
| 144 | for(Component component: components) { |
| 145 | TerminalSize preferredSize = component.getPreferredSize(); |
| 146 | if(maxHeight < preferredSize.getRows()) { |
| 147 | maxHeight = preferredSize.getRows(); |
| 148 | } |
| 149 | width += preferredSize.getColumns(); |
| 150 | } |
| 151 | width += spacing * (components.size() - 1); |
| 152 | return new TerminalSize(width, maxHeight); |
| 153 | } |
| 154 | |
| 155 | @Override |
| 156 | public boolean hasChanged() { |
| 157 | return changed; |
| 158 | } |
| 159 | |
| 160 | @Override |
| 161 | public void doLayout(TerminalSize area, List<Component> components) { |
| 162 | if(direction == Direction.VERTICAL) { |
| 163 | doVerticalLayout(area, components); |
| 164 | } |
| 165 | else { |
| 166 | doHorizontalLayout(area, components); |
| 167 | } |
| 168 | this.changed = false; |
| 169 | } |
| 170 | |
| 171 | private void doVerticalLayout(TerminalSize area, List<Component> components) { |
| 172 | int remainingVerticalSpace = area.getRows(); |
| 173 | int availableHorizontalSpace = area.getColumns(); |
| 174 | for(Component component: components) { |
| 175 | if(remainingVerticalSpace <= 0) { |
| 176 | component.setPosition(TerminalPosition.TOP_LEFT_CORNER); |
| 177 | component.setSize(TerminalSize.ZERO); |
| 178 | } |
| 179 | else { |
| 180 | LinearLayoutData layoutData = (LinearLayoutData)component.getLayoutData(); |
| 181 | Alignment alignment = Alignment.Beginning; |
| 182 | if(layoutData != null) { |
| 183 | alignment = layoutData.alignment; |
| 184 | } |
| 185 | |
| 186 | TerminalSize preferredSize = component.getPreferredSize(); |
| 187 | TerminalSize decidedSize = new TerminalSize( |
| 188 | Math.min(availableHorizontalSpace, preferredSize.getColumns()), |
| 189 | Math.min(remainingVerticalSpace, preferredSize.getRows())); |
| 190 | if(alignment == Alignment.Fill) { |
| 191 | decidedSize = decidedSize.withColumns(availableHorizontalSpace); |
| 192 | alignment = Alignment.Beginning; |
| 193 | } |
| 194 | |
| 195 | TerminalPosition position = component.getPosition(); |
| 196 | position = position.withRow(area.getRows() - remainingVerticalSpace); |
| 197 | switch(alignment) { |
| 198 | case End: |
| 199 | position = position.withColumn(availableHorizontalSpace - decidedSize.getColumns()); |
| 200 | break; |
| 201 | case Center: |
| 202 | position = position.withColumn((availableHorizontalSpace - decidedSize.getColumns()) / 2); |
| 203 | break; |
| 204 | case Beginning: |
| 205 | default: |
| 206 | position = position.withColumn(0); |
| 207 | break; |
| 208 | } |
| 209 | component.setPosition(position); |
| 210 | component.setSize(component.getSize().with(decidedSize)); |
| 211 | remainingVerticalSpace -= decidedSize.getRows() + spacing; |
| 212 | } |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | private void doHorizontalLayout(TerminalSize area, List<Component> components) { |
| 217 | int remainingHorizontalSpace = area.getColumns(); |
| 218 | int availableVerticalSpace = area.getRows(); |
| 219 | for(Component component: components) { |
| 220 | if(remainingHorizontalSpace <= 0) { |
| 221 | component.setPosition(TerminalPosition.TOP_LEFT_CORNER); |
| 222 | component.setSize(TerminalSize.ZERO); |
| 223 | } |
| 224 | else { |
| 225 | LinearLayoutData layoutData = (LinearLayoutData)component.getLayoutData(); |
| 226 | Alignment alignment = Alignment.Beginning; |
| 227 | if(layoutData != null) { |
| 228 | alignment = layoutData.alignment; |
| 229 | } |
| 230 | TerminalSize preferredSize = component.getPreferredSize(); |
| 231 | TerminalSize decidedSize = new TerminalSize( |
| 232 | Math.min(remainingHorizontalSpace, preferredSize.getColumns()), |
| 233 | Math.min(availableVerticalSpace, preferredSize.getRows())); |
| 234 | if(alignment == Alignment.Fill) { |
| 235 | decidedSize = decidedSize.withRows(availableVerticalSpace); |
| 236 | alignment = Alignment.Beginning; |
| 237 | } |
| 238 | |
| 239 | TerminalPosition position = component.getPosition(); |
| 240 | position = position.withColumn(area.getColumns() - remainingHorizontalSpace); |
| 241 | switch(alignment) { |
| 242 | case End: |
| 243 | position = position.withRow(availableVerticalSpace - decidedSize.getRows()); |
| 244 | break; |
| 245 | case Center: |
| 246 | position = position.withRow((availableVerticalSpace - decidedSize.getRows()) / 2); |
| 247 | break; |
| 248 | case Beginning: |
| 249 | default: |
| 250 | position = position.withRow(0); |
| 251 | break; |
| 252 | } |
| 253 | component.setPosition(position); |
| 254 | component.setSize(component.getSize().with(decidedSize)); |
| 255 | remainingHorizontalSpace -= decidedSize.getColumns() + spacing; |
| 256 | } |
| 257 | } |
| 258 | } |
| 259 | } |