Implement the flow table layout in the Details View.
[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.Float;
26 import com.google.gwt.dom.client.Style.Overflow;
27 import com.google.gwt.dom.client.Style.Unit;
28 import com.google.gwt.user.client.ui.ComplexPanel;
29 import com.google.gwt.user.client.ui.Composite;
30 import com.google.gwt.user.client.ui.FlowPanel;
31 import com.google.gwt.user.client.ui.Panel;
32 import com.google.gwt.user.client.ui.Widget;
33
34 /**
35  * A container widget that implements the Glom details view flow table behaviour. Child widgets are arranged using the
36  * least vertical space in the specified number of columns.
37  * 
38  * This class is currently implemented as a {@link Composite} widget. It would be more efficient to subclass
39  * {@link Panel} or {@link ComplexPanel} and implement this class at a lower level but we'd loose the ability to easily
40  * debug the code. This is something to consider when looking for optimisations.
41  * 
42  * @author Ben Konrath <ben@bagu.org>
43  */
44 public class FlowTable extends Composite {
45
46         // Represents an item to be inserted into the FlowTable. The primary reason for this class is to cache the vertical
47         // height of the widget being added to the FlowTable.
48         class FlowTableItem {
49                 Widget widget;
50                 int height;
51
52                 @SuppressWarnings("unused")
53                 private FlowTableItem() {
54                         // disable default constructor
55                 }
56
57                 FlowTableItem(Widget widget) {
58                         // Get the vertical height with decorations by temporarily adding the widget to the body element of the
59                         // document in a transparent container. This is required because the size information is only available when
60                         // the widget is attached to the DOM. The size information must be obtained before the widget is added to
61                         // column because adding a widget to a container automatically removes it from the previous container.
62                         Document doc = Document.get();
63                         com.google.gwt.dom.client.Element div = doc.createDivElement();
64                         div.getStyle().setOpacity(0.0);
65                         div.appendChild(widget.getElement().<com.google.gwt.user.client.Element> cast());
66                         doc.getBody().appendChild(div);
67                         height = widget.getOffsetHeight();
68                         doc.getBody().removeChild(div);
69                         this.widget = widget;
70                 }
71
72                 int getHeight() {
73                         return height;
74                 }
75
76                 Widget getWidget() {
77                         return widget;
78                 }
79         }
80
81         private FlowPanel mainPanel = new FlowPanel();
82         private ArrayList<FlowPanel> columns = new ArrayList<FlowPanel>();
83         private ArrayList<FlowTableItem> items = new ArrayList<FlowTableItem>();
84
85         @SuppressWarnings("unused")
86         private FlowTable() {
87                 // disable default constructor
88         }
89
90         public FlowTable(int columnCount) {
91                 // set the overflow properly so that the columns can be arranged properly
92                 mainPanel.getElement().getStyle().setOverflow(Overflow.HIDDEN);
93                 mainPanel.getElement().getStyle().setWidth(100, Unit.PCT);
94
95                 // create the columns
96                 for (int i = 0; i < columnCount; i++) {
97                         FlowPanel column = new FlowPanel();
98                         column.getElement().getStyle().setFloat(Float.LEFT);
99                         // The columns widths are evenly distributed but this doens't match how the flow table layout works in Glom.
100                         // TODO This might need to be fixed to match Glom.
101                         column.getElement().getStyle().setWidth(100 / columnCount, Unit.PCT);
102                         columns.add(column);
103                         mainPanel.add(column);
104                 }
105
106                 initWidget(mainPanel);
107         }
108
109         /**
110          * Adds a Widget to the FlowTable. The layout of the child widgets is adjusted to minimize the vertical height of
111          * the entire FlowTable.
112          * 
113          * @param widget
114          *            widget to add to the FlowTable
115          */
116         public void add(Widget widget) {
117
118                 // keep track for the child items
119                 items.add(new FlowTableItem(widget));
120
121                 // Discover the total amount of minimum space needed by this container widget, by examining its child widgets,
122                 // by examining every possible sequential arrangement of the widgets in this fixed number of columns:
123                 int minColumnHeight = getMinimumColumnHeight(0, columns.size()); // This calls itself recursively.
124
125                 // Rearrange the widgets taking the newly added widget into account.
126                 int currentColumnIndex = 0;
127                 int currentColumnHeight = 0;
128                 FlowPanel currentColumn = columns.get(currentColumnIndex);
129                 for (FlowTableItem item : items) {
130                         if (currentColumnHeight + item.getHeight() > minColumnHeight) {
131                                 // Ensure that we never try to add widgets to an existing column. This shouldn't happen so it's just a
132                                 // precaution. TODO: log a message if columnNumber is greater than columns.size()
133                                 if (currentColumnIndex < columns.size() - 1) {
134                                         currentColumn = columns.get(++currentColumnIndex);
135                                         currentColumnHeight = 0;
136                                 }
137                         }
138                         currentColumn.add(item.getWidget()); // adding the widget to the column removes it from its current
139                                                                                                         // container
140                         currentColumnHeight += item.getHeight();
141                 }
142         }
143
144         /*
145          * Discover how best (least column height) to arrange these widgets in these columns, keeping them in sequence, and
146          * then say how high the columns must be.
147          * 
148          * This method was ported from the FlowTable class of Glom.
149          */
150         private int getMinimumColumnHeight(int startWidget, int columnCount) {
151
152                 if (columnCount == 1) {
153                         // Just add the heights together:
154                         int widgetsCount = items.size() - startWidget;
155                         return getColumnHeight(startWidget, widgetsCount);
156
157                 } else {
158                         // Try each combination of widgets in the first column, combined with the the other combinations in the
159                         // following columns:
160                         int minimumColumnHeight = 0;
161                         boolean atLeastOneCombinationChecked = false;
162
163                         int countItemsRemaining = items.size() - startWidget;
164
165                         for (int firstColumnWidgetsCount = 1; firstColumnWidgetsCount <= countItemsRemaining; firstColumnWidgetsCount++) {
166                                 int firstColumnHeight = getColumnHeight(startWidget, firstColumnWidgetsCount);
167                                 int minimumColumnHeightSoFar = firstColumnHeight;
168                                 int othersColumnStartWidget = startWidget + firstColumnWidgetsCount;
169
170                                 // Call this function recursively to get the minimum column height in the other columns, when these
171                                 // widgets are in the first column:
172                                 int minimumColumnHeightNextColumns = 0;
173                                 if (othersColumnStartWidget < items.size()) {
174                                         minimumColumnHeightNextColumns = getMinimumColumnHeight(othersColumnStartWidget, columnCount - 1);
175                                         minimumColumnHeightSoFar = Math.max(firstColumnHeight, minimumColumnHeightNextColumns);
176                                 }
177
178                                 // See whether this is better than the last one:
179                                 if (atLeastOneCombinationChecked) {
180                                         if (minimumColumnHeightSoFar < minimumColumnHeight) {
181                                                 minimumColumnHeight = minimumColumnHeightSoFar;
182                                         }
183                                 } else {
184                                         minimumColumnHeight = minimumColumnHeightSoFar;
185                                         atLeastOneCombinationChecked = true;
186                                 }
187                         }
188
189                         return minimumColumnHeight;
190                 }
191         }
192
193         private int getColumnHeight(int startWidget, int widgetCount) {
194                 // Just add the heights together:
195                 int columnHeight = 0;
196                 for (int i = startWidget; i < (startWidget + widgetCount); i++) {
197                         FlowTableItem item = items.get(i);
198                         int itemHeight = item.getHeight();
199                         columnHeight += itemHeight;
200                 }
201                 return columnHeight;
202         }
203
204 }