Commit | Line | Data |
---|---|---|
a3b510ab NR |
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.*; | |
25 | ||
26 | /** | |
27 | * This emulates the behaviour of the GridLayout in SWT (as opposed to the one in AWT/Swing). I originally ported the | |
28 | * SWT class itself but due to licensing concerns (the eclipse license is not compatible with LGPL) I was advised not to | |
29 | * do that. This is a partial implementation and some of the semantics have changed, but in general it works the same | |
30 | * way so the SWT documentation will generally match. | |
31 | * <p> | |
32 | * You use the {@code GridLayout} by specifying a number of columns you want your grid to have and then when you add | |
33 | * components, you assign {@code LayoutData} to these components using the different static methods in this class | |
34 | * ({@code createLayoutData(..)}). You can set components to span both rows and columns, as well as defining how to | |
35 | * distribute the available space. | |
36 | */ | |
37 | public class GridLayout implements LayoutManager { | |
38 | /** | |
39 | * The enum is used to specify where in a grid cell a component should be placed, in the case that the preferred | |
40 | * size of the component is smaller than the space in the cell. This class will generally use two alignments, one | |
41 | * for horizontal and one for vertical. | |
42 | */ | |
43 | public enum Alignment { | |
44 | /** | |
45 | * Place the component at the start of the cell (horizontally or vertically) and leave whatever space is left | |
46 | * after the preferred size empty. | |
47 | */ | |
48 | BEGINNING, | |
49 | /** | |
50 | * Place the component at the middle of the cell (horizontally or vertically) and leave the space before and | |
51 | * after empty. | |
52 | */ | |
53 | CENTER, | |
54 | /** | |
55 | * Place the component at the end of the cell (horizontally or vertically) and leave whatever space is left | |
56 | * before the preferred size empty. | |
57 | */ | |
58 | END, | |
59 | /** | |
60 | * Force the component to be the same size as the table cell | |
61 | */ | |
62 | FILL, | |
63 | ; | |
64 | } | |
65 | ||
66 | static class GridLayoutData implements LayoutData { | |
67 | final Alignment horizontalAlignment; | |
68 | final Alignment verticalAlignment; | |
69 | final boolean grabExtraHorizontalSpace; | |
70 | final boolean grabExtraVerticalSpace; | |
71 | final int horizontalSpan; | |
72 | final int verticalSpan; | |
73 | ||
74 | private GridLayoutData( | |
75 | Alignment horizontalAlignment, | |
76 | Alignment verticalAlignment, | |
77 | boolean grabExtraHorizontalSpace, | |
78 | boolean grabExtraVerticalSpace, | |
79 | int horizontalSpan, | |
80 | int verticalSpan) { | |
81 | ||
82 | if(horizontalSpan < 1 || verticalSpan < 1) { | |
83 | throw new IllegalArgumentException("Horizontal/Vertical span must be 1 or greater"); | |
84 | } | |
85 | ||
86 | this.horizontalAlignment = horizontalAlignment; | |
87 | this.verticalAlignment = verticalAlignment; | |
88 | this.grabExtraHorizontalSpace = grabExtraHorizontalSpace; | |
89 | this.grabExtraVerticalSpace = grabExtraVerticalSpace; | |
90 | this.horizontalSpan = horizontalSpan; | |
91 | this.verticalSpan = verticalSpan; | |
92 | } | |
93 | } | |
94 | ||
95 | private static GridLayoutData DEFAULT = new GridLayoutData( | |
96 | Alignment.BEGINNING, | |
97 | Alignment.BEGINNING, | |
98 | false, | |
99 | false, | |
100 | 1, | |
101 | 1); | |
102 | ||
103 | /** | |
104 | * Creates a layout data object for {@code GridLayout}:s that specify the horizontal and vertical alignment for the | |
105 | * component in case the cell space is larger than the preferred size of the component | |
106 | * @param horizontalAlignment Horizontal alignment strategy | |
107 | * @param verticalAlignment Vertical alignment strategy | |
108 | * @return The layout data object containing the specified alignments | |
109 | */ | |
110 | public static LayoutData createLayoutData(Alignment horizontalAlignment, Alignment verticalAlignment) { | |
111 | return createLayoutData(horizontalAlignment, verticalAlignment, false, false); | |
112 | } | |
113 | ||
114 | /** | |
115 | * Creates a layout data object for {@code GridLayout}:s that specify the horizontal and vertical alignment for the | |
116 | * component in case the cell space is larger than the preferred size of the component. This method also has fields | |
117 | * for indicating that the component would like to take more space if available to the container. For example, if | |
118 | * the container is assigned is assigned an area of 50x15, but all the child components in the grid together only | |
119 | * asks for 40x10, the remaining 10 columns and 5 rows will be empty. If just a single component asks for extra | |
120 | * space horizontally and/or vertically, the grid will expand out to fill the entire area and the text space will be | |
121 | * assigned to the component that asked for it. | |
122 | * | |
123 | * @param horizontalAlignment Horizontal alignment strategy | |
124 | * @param verticalAlignment Vertical alignment strategy | |
125 | * @param grabExtraHorizontalSpace If set to {@code true}, this component will ask to be assigned extra horizontal | |
126 | * space if there is any to assign | |
127 | * @param grabExtraVerticalSpace If set to {@code true}, this component will ask to be assigned extra vertical | |
128 | * space if there is any to assign | |
129 | * @return The layout data object containing the specified alignments and size requirements | |
130 | */ | |
131 | public static LayoutData createLayoutData( | |
132 | Alignment horizontalAlignment, | |
133 | Alignment verticalAlignment, | |
134 | boolean grabExtraHorizontalSpace, | |
135 | boolean grabExtraVerticalSpace) { | |
136 | ||
137 | return createLayoutData(horizontalAlignment, verticalAlignment, grabExtraHorizontalSpace, grabExtraVerticalSpace, 1, 1); | |
138 | } | |
139 | ||
140 | /** | |
141 | * Creates a layout data object for {@code GridLayout}:s that specify the horizontal and vertical alignment for the | |
142 | * component in case the cell space is larger than the preferred size of the component. This method also has fields | |
143 | * for indicating that the component would like to take more space if available to the container. For example, if | |
144 | * the container is assigned is assigned an area of 50x15, but all the child components in the grid together only | |
145 | * asks for 40x10, the remaining 10 columns and 5 rows will be empty. If just a single component asks for extra | |
146 | * space horizontally and/or vertically, the grid will expand out to fill the entire area and the text space will be | |
147 | * assigned to the component that asked for it. It also puts in data on how many rows and/or columns the component | |
148 | * should span. | |
149 | * | |
150 | * @param horizontalAlignment Horizontal alignment strategy | |
151 | * @param verticalAlignment Vertical alignment strategy | |
152 | * @param grabExtraHorizontalSpace If set to {@code true}, this component will ask to be assigned extra horizontal | |
153 | * space if there is any to assign | |
154 | * @param grabExtraVerticalSpace If set to {@code true}, this component will ask to be assigned extra vertical | |
155 | * space if there is any to assign | |
156 | * @param horizontalSpan How many "cells" this component wants to span horizontally | |
157 | * @param verticalSpan How many "cells" this component wants to span vertically | |
158 | * @return The layout data object containing the specified alignments, size requirements and cell spanning | |
159 | */ | |
160 | public static LayoutData createLayoutData( | |
161 | Alignment horizontalAlignment, | |
162 | Alignment verticalAlignment, | |
163 | boolean grabExtraHorizontalSpace, | |
164 | boolean grabExtraVerticalSpace, | |
165 | int horizontalSpan, | |
166 | int verticalSpan) { | |
167 | ||
168 | return new GridLayoutData( | |
169 | horizontalAlignment, | |
170 | verticalAlignment, | |
171 | grabExtraHorizontalSpace, | |
172 | grabExtraVerticalSpace, | |
173 | horizontalSpan, | |
174 | verticalSpan); | |
175 | } | |
176 | ||
177 | /** | |
178 | * This is a shortcut method that will create a grid layout data object that will expand its cell as much as is can | |
179 | * horizontally and make the component occupy the whole area horizontally and center it vertically | |
180 | * @param horizontalSpan How many cells to span horizontally | |
181 | * @return Layout data object with the specified span and horizontally expanding as much as it can | |
182 | */ | |
183 | public static LayoutData createHorizontallyFilledLayoutData(int horizontalSpan) { | |
184 | return createLayoutData( | |
185 | Alignment.FILL, | |
186 | Alignment.CENTER, | |
187 | true, | |
188 | false, | |
189 | horizontalSpan, | |
190 | 1); | |
191 | } | |
192 | ||
193 | /** | |
194 | * This is a shortcut method that will create a grid layout data object that will expand its cell as much as is can | |
195 | * vertically and make the component occupy the whole area vertically and center it horizontally | |
196 | * @param horizontalSpan How many cells to span vertically | |
197 | * @return Layout data object with the specified span and vertically expanding as much as it can | |
198 | */ | |
199 | public static LayoutData createHorizontallyEndAlignedLayoutData(int horizontalSpan) { | |
200 | return createLayoutData( | |
201 | Alignment.END, | |
202 | Alignment.CENTER, | |
203 | true, | |
204 | false, | |
205 | horizontalSpan, | |
206 | 1); | |
207 | } | |
208 | ||
209 | private final int numberOfColumns; | |
210 | private int horizontalSpacing; | |
211 | private int verticalSpacing; | |
212 | private int topMarginSize; | |
213 | private int bottomMarginSize; | |
214 | private int leftMarginSize; | |
215 | private int rightMarginSize; | |
216 | ||
217 | private boolean changed; | |
218 | ||
219 | /** | |
220 | * Creates a new {@code GridLayout} with the specified number of columns. Initially, this layout will have a | |
221 | * horizontal spacing of 1 and vertical spacing of 0, with a left and right margin of 1. | |
222 | * @param numberOfColumns Number of columns in this grid | |
223 | */ | |
224 | public GridLayout(int numberOfColumns) { | |
225 | this.numberOfColumns = numberOfColumns; | |
226 | this.horizontalSpacing = 1; | |
227 | this.verticalSpacing = 0; | |
228 | this.topMarginSize = 0; | |
229 | this.bottomMarginSize = 0; | |
230 | this.leftMarginSize = 1; | |
231 | this.rightMarginSize = 1; | |
232 | this.changed = true; | |
233 | } | |
234 | ||
235 | /** | |
236 | * Returns the horizontal spacing, i.e. the number of empty columns between each cell | |
237 | * @return Horizontal spacing | |
238 | */ | |
239 | public int getHorizontalSpacing() { | |
240 | return horizontalSpacing; | |
241 | } | |
242 | ||
243 | /** | |
244 | * Sets the horizontal spacing, i.e. the number of empty columns between each cell | |
245 | * @param horizontalSpacing New horizontal spacing | |
246 | * @return Itself | |
247 | */ | |
248 | public GridLayout setHorizontalSpacing(int horizontalSpacing) { | |
249 | if(horizontalSpacing < 0) { | |
250 | throw new IllegalArgumentException("Horizontal spacing cannot be less than 0"); | |
251 | } | |
252 | this.horizontalSpacing = horizontalSpacing; | |
253 | this.changed = true; | |
254 | return this; | |
255 | } | |
256 | ||
257 | /** | |
258 | * Returns the vertical spacing, i.e. the number of empty columns between each row | |
259 | * @return Vertical spacing | |
260 | */ | |
261 | public int getVerticalSpacing() { | |
262 | return verticalSpacing; | |
263 | } | |
264 | ||
265 | /** | |
266 | * Sets the vertical spacing, i.e. the number of empty columns between each row | |
267 | * @param verticalSpacing New vertical spacing | |
268 | * @return Itself | |
269 | */ | |
270 | public GridLayout setVerticalSpacing(int verticalSpacing) { | |
271 | if(verticalSpacing < 0) { | |
272 | throw new IllegalArgumentException("Vertical spacing cannot be less than 0"); | |
273 | } | |
274 | this.verticalSpacing = verticalSpacing; | |
275 | this.changed = true; | |
276 | return this; | |
277 | } | |
278 | ||
279 | /** | |
280 | * Returns the top margin, i.e. number of empty rows above the first row in the grid | |
281 | * @return Top margin, in number of rows | |
282 | */ | |
283 | public int getTopMarginSize() { | |
284 | return topMarginSize; | |
285 | } | |
286 | ||
287 | /** | |
288 | * Sets the top margin, i.e. number of empty rows above the first row in the grid | |
289 | * @param topMarginSize Top margin, in number of rows | |
290 | * @return Itself | |
291 | */ | |
292 | public GridLayout setTopMarginSize(int topMarginSize) { | |
293 | if(topMarginSize < 0) { | |
294 | throw new IllegalArgumentException("Top margin size cannot be less than 0"); | |
295 | } | |
296 | this.topMarginSize = topMarginSize; | |
297 | this.changed = true; | |
298 | return this; | |
299 | } | |
300 | ||
301 | /** | |
302 | * Returns the bottom margin, i.e. number of empty rows below the last row in the grid | |
303 | * @return Bottom margin, in number of rows | |
304 | */ | |
305 | public int getBottomMarginSize() { | |
306 | return bottomMarginSize; | |
307 | } | |
308 | ||
309 | /** | |
310 | * Sets the bottom margin, i.e. number of empty rows below the last row in the grid | |
311 | * @param bottomMarginSize Bottom margin, in number of rows | |
312 | * @return Itself | |
313 | */ | |
314 | public GridLayout setBottomMarginSize(int bottomMarginSize) { | |
315 | if(bottomMarginSize < 0) { | |
316 | throw new IllegalArgumentException("Bottom margin size cannot be less than 0"); | |
317 | } | |
318 | this.bottomMarginSize = bottomMarginSize; | |
319 | this.changed = true; | |
320 | return this; | |
321 | } | |
322 | ||
323 | /** | |
324 | * Returns the left margin, i.e. number of empty columns left of the first column in the grid | |
325 | * @return Left margin, in number of columns | |
326 | */ | |
327 | public int getLeftMarginSize() { | |
328 | return leftMarginSize; | |
329 | } | |
330 | ||
331 | /** | |
332 | * Sets the left margin, i.e. number of empty columns left of the first column in the grid | |
333 | * @param leftMarginSize Left margin, in number of columns | |
334 | * @return Itself | |
335 | */ | |
336 | public GridLayout setLeftMarginSize(int leftMarginSize) { | |
337 | if(leftMarginSize < 0) { | |
338 | throw new IllegalArgumentException("Left margin size cannot be less than 0"); | |
339 | } | |
340 | this.leftMarginSize = leftMarginSize; | |
341 | this.changed = true; | |
342 | return this; | |
343 | } | |
344 | ||
345 | /** | |
346 | * Returns the right margin, i.e. number of empty columns right of the last column in the grid | |
347 | * @return Right margin, in number of columns | |
348 | */ | |
349 | public int getRightMarginSize() { | |
350 | return rightMarginSize; | |
351 | } | |
352 | ||
353 | /** | |
354 | * Sets the right margin, i.e. number of empty columns right of the last column in the grid | |
355 | * @param rightMarginSize Right margin, in number of columns | |
356 | * @return Itself | |
357 | */ | |
358 | public GridLayout setRightMarginSize(int rightMarginSize) { | |
359 | if(rightMarginSize < 0) { | |
360 | throw new IllegalArgumentException("Right margin size cannot be less than 0"); | |
361 | } | |
362 | this.rightMarginSize = rightMarginSize; | |
363 | this.changed = true; | |
364 | return this; | |
365 | } | |
366 | ||
367 | @Override | |
368 | public boolean hasChanged() { | |
369 | return this.changed; | |
370 | } | |
371 | ||
372 | @Override | |
373 | public TerminalSize getPreferredSize(List<Component> components) { | |
374 | TerminalSize preferredSize = TerminalSize.ZERO; | |
375 | if(components.isEmpty()) { | |
376 | return preferredSize.withRelative( | |
377 | leftMarginSize + rightMarginSize, | |
378 | topMarginSize + bottomMarginSize); | |
379 | } | |
380 | ||
381 | Component[][] table = buildTable(components); | |
382 | table = eliminateUnusedRowsAndColumns(table); | |
383 | ||
384 | //Figure out each column first, this can be done independently of the row heights | |
385 | int preferredWidth = 0; | |
386 | int preferredHeight = 0; | |
387 | for(int width: getPreferredColumnWidths(table)) { | |
388 | preferredWidth += width; | |
389 | } | |
390 | for(int height: getPreferredRowHeights(table)) { | |
391 | preferredHeight += height; | |
392 | } | |
393 | preferredSize = preferredSize.withRelative(preferredWidth, preferredHeight); | |
394 | preferredSize = preferredSize.withRelativeColumns(leftMarginSize + rightMarginSize + (table[0].length - 1) * horizontalSpacing); | |
395 | preferredSize = preferredSize.withRelativeRows(topMarginSize + bottomMarginSize + (table.length - 1) * verticalSpacing); | |
396 | return preferredSize; | |
397 | } | |
398 | ||
399 | @Override | |
400 | public void doLayout(TerminalSize area, List<Component> components) { | |
401 | //Sanity check, if the area is way too small, just return | |
402 | Component[][] table = buildTable(components); | |
403 | table = eliminateUnusedRowsAndColumns(table); | |
404 | ||
405 | if(area.equals(TerminalSize.ZERO) || | |
406 | table.length == 0 || | |
407 | area.getColumns() <= leftMarginSize + rightMarginSize + ((table[0].length - 1) * horizontalSpacing) || | |
408 | area.getRows() <= bottomMarginSize + topMarginSize + ((table.length - 1) * verticalSpacing)) { | |
409 | return; | |
410 | } | |
411 | ||
412 | //Adjust area to the margins | |
413 | area = area.withRelative(-leftMarginSize - rightMarginSize, -topMarginSize - bottomMarginSize); | |
414 | ||
415 | Map<Component, TerminalSize> sizeMap = new IdentityHashMap<Component, TerminalSize>(); | |
416 | Map<Component, TerminalPosition> positionMap = new IdentityHashMap<Component, TerminalPosition>(); | |
417 | ||
418 | //Figure out each column first, this can be done independently of the row heights | |
419 | int[] columnWidths = getPreferredColumnWidths(table); | |
420 | ||
421 | //Take notes of which columns we can expand if the usable area is larger than what the components want | |
422 | Set<Integer> expandableColumns = getExpandableColumns(table); | |
423 | ||
424 | //Next, start shrinking to make sure it fits the size of the area we are trying to lay out on. | |
425 | //Notice we subtract the horizontalSpacing to take the space between components into account | |
426 | TerminalSize areaWithoutHorizontalSpacing = area.withRelativeColumns(-horizontalSpacing * (table[0].length - 1)); | |
427 | int totalWidth = shrinkWidthToFitArea(areaWithoutHorizontalSpacing, columnWidths); | |
428 | ||
429 | //Finally, if there is extra space, make the expandable columns larger | |
430 | while(areaWithoutHorizontalSpacing.getColumns() > totalWidth && !expandableColumns.isEmpty()) { | |
431 | totalWidth = grabExtraHorizontalSpace(areaWithoutHorizontalSpacing, columnWidths, expandableColumns, totalWidth); | |
432 | } | |
433 | ||
434 | //Now repeat for rows | |
435 | int[] rowHeights = getPreferredRowHeights(table); | |
436 | Set<Integer> expandableRows = getExpandableRows(table); | |
437 | TerminalSize areaWithoutVerticalSpacing = area.withRelativeRows(-verticalSpacing * (table.length - 1)); | |
438 | int totalHeight = shrinkHeightToFitArea(areaWithoutVerticalSpacing, rowHeights); | |
439 | while(areaWithoutVerticalSpacing.getRows() > totalHeight && !expandableRows.isEmpty()) { | |
440 | totalHeight = grabExtraVerticalSpace(areaWithoutVerticalSpacing, rowHeights, expandableRows, totalHeight); | |
441 | } | |
442 | ||
443 | //Ok, all constraints are in place, we can start placing out components. To simplify, do it horizontally first | |
444 | //and vertically after | |
445 | TerminalPosition tableCellTopLeft = TerminalPosition.TOP_LEFT_CORNER; | |
446 | for(int y = 0; y < table.length; y++) { | |
447 | tableCellTopLeft = tableCellTopLeft.withColumn(0); | |
448 | for(int x = 0; x < table[y].length; x++) { | |
449 | Component component = table[y][x]; | |
450 | if(component != null && !positionMap.containsKey(component)) { | |
451 | GridLayoutData layoutData = getLayoutData(component); | |
452 | TerminalSize size = component.getPreferredSize(); | |
453 | TerminalPosition position = tableCellTopLeft; | |
454 | ||
455 | int availableHorizontalSpace = 0; | |
456 | int availableVerticalSpace = 0; | |
457 | for (int i = 0; i < layoutData.horizontalSpan; i++) { | |
458 | availableHorizontalSpace += columnWidths[x + i] + (i > 0 ? horizontalSpacing : 0); | |
459 | } | |
460 | for (int i = 0; i < layoutData.verticalSpan; i++) { | |
461 | availableVerticalSpace += rowHeights[y + i] + (i > 0 ? verticalSpacing : 0); | |
462 | } | |
463 | ||
464 | //Make sure to obey the size restrictions | |
465 | size = size.withColumns(Math.min(size.getColumns(), availableHorizontalSpace)); | |
466 | size = size.withRows(Math.min(size.getRows(), availableVerticalSpace)); | |
467 | ||
468 | switch (layoutData.horizontalAlignment) { | |
469 | case CENTER: | |
470 | position = position.withRelativeColumn((availableHorizontalSpace - size.getColumns()) / 2); | |
471 | break; | |
472 | case END: | |
473 | position = position.withRelativeColumn(availableHorizontalSpace - size.getColumns()); | |
474 | break; | |
475 | case FILL: | |
476 | size = size.withColumns(availableHorizontalSpace); | |
477 | break; | |
478 | default: | |
479 | break; | |
480 | } | |
481 | switch (layoutData.verticalAlignment) { | |
482 | case CENTER: | |
483 | position = position.withRelativeRow((availableVerticalSpace - size.getRows()) / 2); | |
484 | break; | |
485 | case END: | |
486 | position = position.withRelativeRow(availableVerticalSpace - size.getRows()); | |
487 | break; | |
488 | case FILL: | |
489 | size = size.withRows(availableVerticalSpace); | |
490 | break; | |
491 | default: | |
492 | break; | |
493 | } | |
494 | ||
495 | sizeMap.put(component, size); | |
496 | positionMap.put(component, position); | |
497 | } | |
498 | tableCellTopLeft = tableCellTopLeft.withRelativeColumn(columnWidths[x] + horizontalSpacing); | |
499 | } | |
500 | tableCellTopLeft = tableCellTopLeft.withRelativeRow(rowHeights[y] + verticalSpacing); | |
501 | } | |
502 | ||
503 | //Apply the margins here | |
504 | for(Component component: components) { | |
505 | component.setPosition(positionMap.get(component).withRelative(leftMarginSize, topMarginSize)); | |
506 | component.setSize(sizeMap.get(component)); | |
507 | } | |
508 | this.changed = false; | |
509 | } | |
510 | ||
511 | private int[] getPreferredColumnWidths(Component[][] table) { | |
512 | //actualNumberOfColumns may be different from this.numberOfColumns since some columns may have been eliminated | |
513 | int actualNumberOfColumns = table[0].length; | |
514 | int columnWidths[] = new int[actualNumberOfColumns]; | |
515 | ||
516 | //Start by letting all span = 1 columns take what they need | |
517 | for(Component[] row: table) { | |
518 | for(int i = 0; i < actualNumberOfColumns; i++) { | |
519 | Component component = row[i]; | |
520 | if(component == null) { | |
521 | continue; | |
522 | } | |
523 | GridLayoutData layoutData = getLayoutData(component); | |
524 | if (layoutData.horizontalSpan == 1) { | |
525 | columnWidths[i] = Math.max(columnWidths[i], component.getPreferredSize().getColumns()); | |
526 | } | |
527 | } | |
528 | } | |
529 | ||
530 | //Next, do span > 1 and enlarge if necessary | |
531 | for(Component[] row: table) { | |
532 | for(int i = 0; i < actualNumberOfColumns; ) { | |
533 | Component component = row[i]; | |
534 | if(component == null) { | |
535 | i++; | |
536 | continue; | |
537 | } | |
538 | GridLayoutData layoutData = getLayoutData(component); | |
539 | if(layoutData.horizontalSpan > 1) { | |
540 | int accumWidth = 0; | |
541 | for(int j = i; j < i + layoutData.horizontalSpan; j++) { | |
542 | accumWidth += columnWidths[j]; | |
543 | } | |
544 | ||
545 | int preferredWidth = component.getPreferredSize().getColumns(); | |
546 | if(preferredWidth > accumWidth) { | |
547 | int columnOffset = 0; | |
548 | do { | |
549 | columnWidths[i + columnOffset++]++; | |
550 | accumWidth++; | |
551 | if(columnOffset == layoutData.horizontalSpan) { | |
552 | columnOffset = 0; | |
553 | } | |
554 | } | |
555 | while(preferredWidth > accumWidth); | |
556 | } | |
557 | } | |
558 | i += layoutData.horizontalSpan; | |
559 | } | |
560 | } | |
561 | return columnWidths; | |
562 | } | |
563 | ||
564 | private int[] getPreferredRowHeights(Component[][] table) { | |
565 | int numberOfRows = table.length; | |
566 | int rowHeights[] = new int[numberOfRows]; | |
567 | ||
568 | //Start by letting all span = 1 rows take what they need | |
569 | int rowIndex = 0; | |
570 | for(Component[] row: table) { | |
571 | for(int i = 0; i < row.length; i++) { | |
572 | Component component = row[i]; | |
573 | if(component == null) { | |
574 | continue; | |
575 | } | |
576 | GridLayoutData layoutData = getLayoutData(component); | |
577 | if(layoutData.verticalSpan == 1) { | |
578 | rowHeights[rowIndex] = Math.max(rowHeights[rowIndex], component.getPreferredSize().getRows()); | |
579 | } | |
580 | } | |
581 | rowIndex++; | |
582 | } | |
583 | ||
584 | //Next, do span > 1 and enlarge if necessary | |
585 | for(int x = 0; x < numberOfColumns; x++) { | |
586 | for(int y = 0; y < numberOfRows && y < table.length; ) { | |
587 | if(x >= table[y].length) { | |
588 | y++; | |
589 | continue; | |
590 | } | |
591 | Component component = table[y][x]; | |
592 | if(component == null) { | |
593 | y++; | |
594 | continue; | |
595 | } | |
596 | GridLayoutData layoutData = getLayoutData(component); | |
597 | if(layoutData.verticalSpan > 1) { | |
598 | int accumulatedHeight = 0; | |
599 | for(int i = y; i < y + layoutData.verticalSpan; i++) { | |
600 | accumulatedHeight += rowHeights[i]; | |
601 | } | |
602 | ||
603 | int preferredHeight = component.getPreferredSize().getRows(); | |
604 | if(preferredHeight > accumulatedHeight) { | |
605 | int rowOffset = 0; | |
606 | do { | |
607 | rowHeights[y + rowOffset++]++; | |
608 | accumulatedHeight++; | |
609 | if(rowOffset == layoutData.verticalSpan) { | |
610 | rowOffset = 0; | |
611 | } | |
612 | } | |
613 | while(preferredHeight > accumulatedHeight); | |
614 | } | |
615 | } | |
616 | y += layoutData.verticalSpan; | |
617 | } | |
618 | } | |
619 | return rowHeights; | |
620 | } | |
621 | ||
622 | private Set<Integer> getExpandableColumns(Component[][] table) { | |
623 | Set<Integer> expandableColumns = new TreeSet<Integer>(); | |
624 | for(Component[] row: table) { | |
625 | for (int i = 0; i < row.length; i++) { | |
626 | if(row[i] == null) { | |
627 | continue; | |
628 | } | |
629 | GridLayoutData layoutData = getLayoutData(row[i]); | |
630 | if(layoutData.grabExtraHorizontalSpace) { | |
631 | expandableColumns.add(i); | |
632 | } | |
633 | } | |
634 | } | |
635 | return expandableColumns; | |
636 | } | |
637 | ||
638 | private Set<Integer> getExpandableRows(Component[][] table) { | |
639 | Set<Integer> expandableRows = new TreeSet<Integer>(); | |
640 | for(int rowIndex = 0; rowIndex < table.length; rowIndex++) { | |
641 | Component[] row = table[rowIndex]; | |
642 | for (int columnIndex = 0; columnIndex < row.length; columnIndex++) { | |
643 | if(row[columnIndex] == null) { | |
644 | continue; | |
645 | } | |
646 | GridLayoutData layoutData = getLayoutData(row[columnIndex]); | |
647 | if(layoutData.grabExtraVerticalSpace) { | |
648 | expandableRows.add(rowIndex); | |
649 | } | |
650 | } | |
651 | } | |
652 | return expandableRows; | |
653 | } | |
654 | ||
655 | private int shrinkWidthToFitArea(TerminalSize area, int[] columnWidths) { | |
656 | int totalWidth = 0; | |
657 | for(int width: columnWidths) { | |
658 | totalWidth += width; | |
659 | } | |
660 | if(totalWidth > area.getColumns()) { | |
661 | int columnOffset = 0; | |
662 | do { | |
663 | if(columnWidths[columnOffset] > 0) { | |
664 | columnWidths[columnOffset]--; | |
665 | totalWidth--; | |
666 | } | |
667 | if(++columnOffset == numberOfColumns) { | |
668 | columnOffset = 0; | |
669 | } | |
670 | } | |
671 | while(totalWidth > area.getColumns()); | |
672 | } | |
673 | return totalWidth; | |
674 | } | |
675 | ||
676 | private int shrinkHeightToFitArea(TerminalSize area, int[] rowHeights) { | |
677 | int totalHeight = 0; | |
678 | for(int height: rowHeights) { | |
679 | totalHeight += height; | |
680 | } | |
681 | if(totalHeight > area.getRows()) { | |
682 | int rowOffset = 0; | |
683 | do { | |
684 | if(rowHeights[rowOffset] > 0) { | |
685 | rowHeights[rowOffset]--; | |
686 | totalHeight--; | |
687 | } | |
688 | if(++rowOffset == rowHeights.length) { | |
689 | rowOffset = 0; | |
690 | } | |
691 | } | |
692 | while(totalHeight > area.getRows()); | |
693 | } | |
694 | return totalHeight; | |
695 | } | |
696 | ||
697 | private int grabExtraHorizontalSpace(TerminalSize area, int[] columnWidths, Set<Integer> expandableColumns, int totalWidth) { | |
698 | for(int columnIndex: expandableColumns) { | |
699 | columnWidths[columnIndex]++; | |
700 | totalWidth++; | |
701 | if(area.getColumns() == totalWidth) { | |
702 | break; | |
703 | } | |
704 | } | |
705 | return totalWidth; | |
706 | } | |
707 | ||
708 | private int grabExtraVerticalSpace(TerminalSize area, int[] rowHeights, Set<Integer> expandableRows, int totalHeight) { | |
709 | for(int rowIndex: expandableRows) { | |
710 | rowHeights[rowIndex]++; | |
711 | totalHeight++; | |
712 | if(area.getColumns() == totalHeight) { | |
713 | break; | |
714 | } | |
715 | } | |
716 | return totalHeight; | |
717 | } | |
718 | ||
719 | private Component[][] buildTable(List<Component> components) { | |
720 | List<Component[]> rows = new ArrayList<Component[]>(); | |
721 | List<int[]> hspans = new ArrayList<int[]>(); | |
722 | List<int[]> vspans = new ArrayList<int[]>(); | |
723 | ||
724 | int rowCount = 0; | |
725 | int rowsExtent = 1; | |
726 | Queue<Component> toBePlaced = new LinkedList<Component>(components); | |
727 | while(!toBePlaced.isEmpty() || rowCount < rowsExtent) { | |
728 | //Start new row | |
729 | Component[] row = new Component[numberOfColumns]; | |
730 | int[] hspan = new int[numberOfColumns]; | |
731 | int[] vspan = new int[numberOfColumns]; | |
732 | ||
733 | for(int i = 0; i < numberOfColumns; i++) { | |
734 | if(i > 0 && hspan[i - 1] > 1) { | |
735 | row[i] = row[i-1]; | |
736 | hspan[i] = hspan[i - 1] - 1; | |
737 | vspan[i] = vspan[i - 1]; | |
738 | } | |
739 | else if(rowCount > 0 && vspans.get(rowCount - 1)[i] > 1) { | |
740 | row[i] = rows.get(rowCount - 1)[i]; | |
741 | hspan[i] = hspans.get(rowCount - 1)[i]; | |
742 | vspan[i] = vspans.get(rowCount - 1)[i] - 1; | |
743 | } | |
744 | else if(!toBePlaced.isEmpty()) { | |
745 | Component component = toBePlaced.poll(); | |
746 | GridLayoutData gridLayoutData = getLayoutData(component); | |
747 | ||
748 | row[i] = component; | |
749 | hspan[i] = gridLayoutData.horizontalSpan; | |
750 | vspan[i] = gridLayoutData.verticalSpan; | |
751 | rowsExtent = Math.max(rowsExtent, rowCount + gridLayoutData.verticalSpan); | |
752 | } | |
753 | else { | |
754 | row[i] = null; | |
755 | hspan[i] = 1; | |
756 | vspan[i] = 1; | |
757 | } | |
758 | } | |
759 | ||
760 | rows.add(row); | |
761 | hspans.add(hspan); | |
762 | vspans.add(vspan); | |
763 | rowCount++; | |
764 | } | |
765 | return rows.toArray(new Component[rows.size()][]); | |
766 | } | |
767 | ||
768 | private Component[][] eliminateUnusedRowsAndColumns(Component[][] table) { | |
769 | if(table.length == 0) { | |
770 | return table; | |
771 | } | |
772 | //Could make this into a Set, but I doubt there will be any real gain in performance as these are probably going | |
773 | //to be very small. | |
774 | List<Integer> rowsToRemove = new ArrayList<Integer>(); | |
775 | List<Integer> columnsToRemove = new ArrayList<Integer>(); | |
776 | ||
777 | final int tableRows = table.length; | |
778 | final int tableColumns = table[0].length; | |
779 | ||
780 | //Scan for unnecessary columns | |
781 | columnLoop: | |
782 | for(int column = tableColumns - 1; column > 0; column--) { | |
783 | for(int row = 0; row < tableRows; row++) { | |
784 | if(table[row][column] != table[row][column - 1]) { | |
785 | continue columnLoop; | |
786 | } | |
787 | } | |
788 | columnsToRemove.add(column); | |
789 | } | |
790 | ||
791 | //Scan for unnecessary rows | |
792 | rowLoop: | |
793 | for(int row = tableRows - 1; row > 0; row--) { | |
794 | for(int column = 0; column < tableColumns; column++) { | |
795 | if(table[row][column] != table[row - 1][column]) { | |
796 | continue rowLoop; | |
797 | } | |
798 | } | |
799 | rowsToRemove.add(row); | |
800 | } | |
801 | ||
802 | //If there's nothing to remove, just return the same | |
803 | if(rowsToRemove.isEmpty() && columnsToRemove.isEmpty()) { | |
804 | return table; | |
805 | } | |
806 | ||
807 | //Build a new table with rows & columns eliminated | |
808 | Component[][] newTable = new Component[tableRows - rowsToRemove.size()][]; | |
809 | int insertedRowCounter = 0; | |
810 | for(int row = 0; row < tableRows; row++) { | |
811 | Component[] newColumn = new Component[tableColumns - columnsToRemove.size()]; | |
812 | int insertedColumnCounter = 0; | |
813 | for(int column = 0; column < tableColumns; column++) { | |
814 | if(columnsToRemove.contains(column)) { | |
815 | continue; | |
816 | } | |
817 | newColumn[insertedColumnCounter++] = table[row][column]; | |
818 | } | |
819 | newTable[insertedRowCounter++] = newColumn; | |
820 | } | |
821 | return newTable; | |
822 | } | |
823 | ||
824 | private GridLayoutData getLayoutData(Component component) { | |
825 | LayoutData layoutData = component.getLayoutData(); | |
826 | if(layoutData == null || !(layoutData instanceof GridLayoutData)) { | |
827 | return DEFAULT; | |
828 | } | |
829 | else { | |
830 | return (GridLayoutData)layoutData; | |
831 | } | |
832 | } | |
833 | } |