Use an HTML table instead of CSS for the FlowTable layout.
[online-glom:gwt-glom.git] / src / main / java / org / glom / web / client / ui / details / FlowTable.java
1 /*
2  * Copyright (C) 2011 Openismus GmbH
3  *
4  * This file is part of GWT-Glom.
5  *
6  * GWT-Glom is free software: you can redistribute it and/or modify it
7  * under the terms of the GNU Lesser General Public License as published by the
8  * Free Software Foundation, either version 3 of the License, or (at your
9  * option) any later version.
10  *
11  * GWT-Glom is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
14  * for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with GWT-Glom.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package org.glom.web.client.ui.details;
21
22 import java.util.ArrayList;
23
24 import com.google.gwt.dom.client.Document;
25 import com.google.gwt.dom.client.Style.Unit;
26 import com.google.gwt.user.client.ui.Composite;
27 import com.google.gwt.user.client.ui.FlexTable;
28 import com.google.gwt.user.client.ui.FlowPanel;
29 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
30 import com.google.gwt.user.client.ui.HTMLTable.ColumnFormatter;
31 import com.google.gwt.user.client.ui.HTMLTable.RowFormatter;
32 import com.google.gwt.user.client.ui.HasVerticalAlignment;
33 import com.google.gwt.user.client.ui.IsWidget;
34 import com.google.gwt.user.client.ui.Widget;
35
36 /**
37  * A container widget that implements the Glom details view flow table behaviour. Child widgets are arranged using the
38  * least vertical space in the specified number of columns.
39  * 
40  * @author Ben Konrath <ben@bagu.org>
41  */
42 public class FlowTable extends Composite {
43
44         // Represents an item to be inserted into the FlowTable. The primary reason for this class is to cache the vertical
45         // height of the widget being added to the FlowTable.
46         class FlowTableItem implements IsWidget {
47
48                 Widget widget;
49                 int height;
50
51                 @SuppressWarnings("unused")
52                 private FlowTableItem() {
53                         // disable default constructor
54                 }
55
56                 FlowTableItem(Widget widget) {
57                         // Get the vertical height with decorations by temporarily adding the widget to the body element of the
58                         // document in a transparent container. This is required because the size information is only available when
59                         // the widget is attached to the DOM. The size information must be obtained before the widget is added to
60                         // column because adding a widget to a container automatically removes it from the previous container.
61                         Document doc = Document.get();
62                         com.google.gwt.dom.client.Element div = doc.createDivElement();
63                         div.getStyle().setOpacity(0.0);
64                         div.appendChild(widget.getElement().<com.google.gwt.user.client.Element> cast());
65                         doc.getBody().appendChild(div);
66                         height = widget.getOffsetHeight();
67                         doc.getBody().removeChild(div);
68                         this.widget = widget;
69                 }
70
71                 int getHeight() {
72                         return height;
73                 }
74
75                 /*
76                  * (non-Javadoc)
77                  * 
78                  * @see com.google.gwt.user.client.ui.IsWidget#asWidget()
79                  */
80                 @Override
81                 public Widget asWidget() {
82                         return widget;
83                 }
84         }
85
86         private FlexTable table = new FlexTable();
87         private ArrayList<FlowPanel> columns = new ArrayList<FlowPanel>();
88         private ArrayList<FlowTableItem> items = new ArrayList<FlowTableItem>();
89
90         @SuppressWarnings("unused")
91         private FlowTable() {
92                 // disable default constructor
93         }
94
95         public FlowTable(int columnCount) {
96                 // get the formatters
97                 CellFormatter cellFormatter = table.getFlexCellFormatter();
98                 ColumnFormatter columnFormatter = table.getColumnFormatter();
99                 RowFormatter rowFormater = table.getRowFormatter();
100
101                 // align the Cells to the top of the row
102                 rowFormater.setVerticalAlign(0, HasVerticalAlignment.ALIGN_TOP);
103
104                 // take up all available horizontal space and remove the border
105                 table.setWidth("100%");
106                 table.getElement().getStyle().setProperty("borderCollapse", "collapse");
107                 table.setBorderWidth(0);
108
109                 // The columns widths are evenly distributed amongst the number of columns with 1% padding between the columns.
110                 double columnWidth = (100 - (columnCount - 1)) / columnCount;
111                 for (int i = 0; i < columnCount; i++) {
112                         // create and add a column
113                         FlowPanel column = new FlowPanel();
114                         table.setWidget(0, i, column);
115
116                         // set the column with from the calucation above
117                         columnFormatter.setWidth(i, columnWidth + "%");
118
119                         // Add space between the columns.
120                         // Don't set the left padding on the first column.
121                         if (i != 0)
122                                 cellFormatter.getElement(0, i).getStyle().setPaddingLeft(0.5, Unit.PCT);
123                         // Don't set the right padding on the last column.
124                         if (i != columnCount - 1)
125                                 cellFormatter.getElement(0, i).getStyle().setPaddingRight(0.5, Unit.PCT);
126
127                         // TODO The style name should be placed on the column FlexTable when I add it. - Ben
128                         cellFormatter.addStyleName(0, i, "group-column");
129
130                         // Keep track of the columns so it can be accessed later
131                         columns.add(column);
132                 }
133
134                 initWidget(table);
135         }
136
137         /**
138          * Adds a Widget to the FlowTable. The layout of the child widgets is adjusted to minimize the vertical height of
139          * the entire FlowTable.
140          * 
141          * @param widget
142          *            widget to add to the FlowTable
143          */
144         public void add(Widget widget) {
145
146                 // keep track for the child items
147                 items.add(new FlowTableItem(widget));
148
149                 // Discover the total amount of minimum space needed by this container widget, by examining its child widgets,
150                 // by examining every possible sequential arrangement of the widgets in this fixed number of columns:
151                 int minColumnHeight = getMinimumColumnHeight(0, columns.size()); // This calls itself recursively.
152
153                 // Rearrange the widgets taking the newly added widget into account.
154                 int currentColumnIndex = 0;
155                 int currentColumnHeight = 0;
156                 FlowPanel currentColumn = columns.get(currentColumnIndex);
157                 for (FlowTableItem item : items) {
158                         if (currentColumnHeight + item.getHeight() > minColumnHeight) {
159                                 // Ensure that we never try to add widgets to an existing column. This shouldn't happen so it's just a
160                                 // precaution. TODO: log a message if columnNumber is greater than columns.size()
161                                 if (currentColumnIndex < columns.size() - 1) {
162                                         currentColumn = columns.get(++currentColumnIndex);
163                                         currentColumnHeight = 0;
164                                 }
165                         }
166                         currentColumn.add(item.asWidget()); // adding the widget to the column removes it from its current container
167                         currentColumnHeight += item.getHeight();
168                 }
169         }
170
171         /*
172          * Discover how best (least column height) to arrange these widgets in these columns, keeping them in sequence, and
173          * then say how high the columns must be.
174          * 
175          * This method was ported from the FlowTable class of Glom.
176          */
177         private int getMinimumColumnHeight(int startWidget, int columnCount) {
178
179                 if (columnCount == 1) {
180                         // Just add the heights together:
181                         int widgetsCount = items.size() - startWidget;
182                         return getColumnHeight(startWidget, widgetsCount);
183
184                 } else {
185                         // Try each combination of widgets in the first column, combined with the the other combinations in the
186                         // following columns:
187                         int minimumColumnHeight = 0;
188                         boolean atLeastOneCombinationChecked = false;
189
190                         int countItemsRemaining = items.size() - startWidget;
191
192                         for (int firstColumnWidgetsCount = 1; firstColumnWidgetsCount <= countItemsRemaining; firstColumnWidgetsCount++) {
193                                 int firstColumnHeight = getColumnHeight(startWidget, firstColumnWidgetsCount);
194                                 int minimumColumnHeightSoFar = firstColumnHeight;
195                                 int othersColumnStartWidget = startWidget + firstColumnWidgetsCount;
196
197                                 // Call this function recursively to get the minimum column height in the other columns, when these
198                                 // widgets are in the first column:
199                                 int minimumColumnHeightNextColumns = 0;
200                                 if (othersColumnStartWidget < items.size()) {
201                                         minimumColumnHeightNextColumns = getMinimumColumnHeight(othersColumnStartWidget, columnCount - 1);
202                                         minimumColumnHeightSoFar = Math.max(firstColumnHeight, minimumColumnHeightNextColumns);
203                                 }
204
205                                 // See whether this is better than the last one:
206                                 if (atLeastOneCombinationChecked) {
207                                         if (minimumColumnHeightSoFar < minimumColumnHeight) {
208                                                 minimumColumnHeight = minimumColumnHeightSoFar;
209                                         }
210                                 } else {
211                                         minimumColumnHeight = minimumColumnHeightSoFar;
212                                         atLeastOneCombinationChecked = true;
213                                 }
214                         }
215
216                         return minimumColumnHeight;
217                 }
218         }
219
220         private int getColumnHeight(int startWidget, int widgetCount) {
221                 // Just add the heights together:
222                 int columnHeight = 0;
223                 for (int i = startWidget; i < (startWidget + widgetCount); i++) {
224                         FlowTableItem item = items.get(i);
225                         int itemHeight = item.getHeight();
226                         columnHeight += itemHeight;
227                 }
228                 return columnHeight;
229         }
230
231 }