#30: Adaptives Layout (https://polymap.org/mosaic/ticket/30)
[polymap3:falkos-polymap3-rhei.git] / plugins / org.polymap.rhei.batik / src / org / polymap / rhei / batik / toolkit / ConstraintLayout.java
1 /* 
2  * polymap.org
3  * Copyright 2013, Polymap GmbH. All rights reserved.
4  *
5  * This is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU Lesser General Public License as
7  * published by the Free Software Foundation; either version 3.0 of
8  * the License, or (at your option) any later version.
9  *
10  * This software is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13  * Lesser General Public License for more details.
14  */
15 package org.polymap.rhei.batik.toolkit;
16
17 import java.util.ArrayList;
18 import java.util.Arrays;
19 import java.util.Collections;
20 import java.util.Comparator;
21 import java.util.Iterator;
22 import java.util.List;
23 import java.util.Random;
24 import org.apache.commons.logging.Log;
25 import org.apache.commons.logging.LogFactory;
26
27 import org.eclipse.swt.SWT;
28 import org.eclipse.swt.graphics.Point;
29 import org.eclipse.swt.graphics.Rectangle;
30 import org.eclipse.swt.widgets.Composite;
31 import org.eclipse.swt.widgets.Control;
32 import org.eclipse.swt.widgets.Layout;
33
34 import org.polymap.rhei.batik.internal.cp.BestFirstOptimizer;
35 import org.polymap.rhei.batik.internal.cp.IOptimizationGoal;
36 import org.polymap.rhei.batik.internal.cp.ISolution;
37 import org.polymap.rhei.batik.internal.cp.ISolver;
38 import org.polymap.rhei.batik.internal.cp.PercentScore;
39 import org.polymap.rhei.batik.internal.cp.Prioritized;
40 import org.polymap.rhei.batik.internal.cp.ISolver.ScoredSolution;
41
42 /**
43  * 
44  *
45  * @author <a href="http://www.polymap.de">Falko Bräutigam</a>
46  */
47 public class ConstraintLayout
48         extends Layout {
49
50     private static Log log = LogFactory.getLog( ConstraintLayout.class );
51
52     public int                  marginWidth = 10;
53
54     public int                  marginHeight = 10;
55     
56     public int                  spacing = 10;
57     
58     private LayoutSolution      solution;
59     
60     private Rectangle           clientArea;
61
62     
63     private boolean computeSolution( Composite composite, boolean flushCache ) {
64         assert solution == null || solution.composite == composite;
65         
66         if (solution == null || flushCache) {
67             clientArea = composite.getClientArea();
68             if (clientArea.width <= 0 || clientArea.height < 0) {
69                 return false;
70             }
71             if (clientArea.width > composite.getDisplay().getBounds().width) {
72                 log.warn( "Invalid client area: " + clientArea + ", display: " + composite.getDisplay().getBounds().width );
73                 return false;
74             }
75             
76             log.debug( "LAYOUT: " + composite.hashCode() + " -> " + clientArea );
77             
78             ISolver solver = new BestFirstOptimizer( 200, 10 );
79             solver.addGoal( new PriorityOnTopGoal( 1 ) );
80             solver.addGoal( new MinOverallHeightGoal( Math.max( 1000, clientArea.height ), 2 ) );
81 //            solver.addGoal( new ElementRelationGoal( this ), 1 );
82
83             for (Control child : composite.getChildren()) {
84                 ConstraintData data = (ConstraintData)child.getLayoutData();
85                 if (data != null) {
86                     data.fillSolver( solver );
87                 }
88             }
89             LayoutSolution start = new LayoutSolution( composite );
90             start.justifyElements();
91             
92             List<ScoredSolution> results = solver.solve( start );
93             solution = (LayoutSolution)results.get( results.size()-1 ).solution;
94         }
95         return solution != null;
96     }
97     
98     
99     @Override
100     protected void layout( Composite composite, boolean flushCache ) {
101         // compute solution
102         if (computeSolution( composite, flushCache )) {
103             // layout elements
104             int colX = marginWidth;
105
106             for (LayoutColumn column : solution.columns) {
107                 assert column.width > 0;
108                 int elmY = marginHeight;
109
110                 for (LayoutElement elm : column) {
111                     assert elm.height >= 0;
112                     elm.control.setBounds( colX, elmY, column.width, elm.height );
113                     elmY += elm.height + spacing;
114                 }
115                 colX += column.width + spacing;
116             }
117         }
118     }
119
120     
121     @Override
122     protected Point computeSize( Composite composite, int wHint, int hHint, boolean flushCache ) {
123         Point result = null;
124         
125         // compute solution
126         if (computeSolution( composite, flushCache )) {
127             // find heighest column
128             LayoutColumn maxColumn = null;
129             for (LayoutColumn column : solution.columns) {
130                 maxColumn = maxColumn == null || column.height >= maxColumn.height ? column : maxColumn;
131             }
132             int height = maxColumn.height + (2*marginHeight) + ((maxColumn.size()-1)*spacing);
133             result = new Point( SWT.DEFAULT, height );
134             //log.info( "    computeSize: " + result );
135         }
136         else {
137             result = new Point( wHint, hHint );
138         }
139         return result;
140     }
141
142     
143     /**
144      * 
145      */
146     public class LayoutSolution
147             implements ISolution {
148
149         public Composite                composite;
150         
151         public ArrayList<LayoutColumn>  columns = new ArrayList( 3 );
152         
153         
154         public LayoutSolution( Composite composite ) {
155             this.composite = composite;
156             assert clientArea.width > 0;
157             this.columns.add( new LayoutColumn( Arrays.asList( composite.getChildren() ) ) );
158         }
159         
160         public LayoutSolution( LayoutSolution other ) {
161             this.composite = other.composite;
162             for (LayoutColumn column : other.columns) {
163                 columns.add( new LayoutColumn( column ) );                
164             }
165         }
166
167         @Override
168         public String surrogate() {
169             int result = 1;
170             for (LayoutColumn column : columns) {
171                 result = 31 * result + column.width;
172                 for (LayoutElement elm : column) {
173                     result = 31 * result + elm.hashCode();
174                 }
175             }
176             return String.valueOf( result );
177         }
178
179         @Override
180         public LayoutSolution copy() {
181             return new LayoutSolution( this );
182         }
183
184         /** Returns a new List containing all elements. */
185         public List<LayoutElement> elements() {
186             List<LayoutElement> result = new ArrayList();
187             for (LayoutColumn column : columns) {
188                 result.addAll( column );
189             }
190             return result;
191         }
192         
193         public LayoutElement remove( int index ) {
194             int c = 0;
195             for (LayoutColumn column : columns) {
196                 for (Iterator<LayoutElement> it=column.iterator(); it.hasNext(); c++) {
197                     LayoutElement elm = it.next();
198                     if (c == index) {
199                         it.remove();
200                         return elm;
201                     }
202                 }
203             }
204             throw new IllegalArgumentException( "Invalid index: index=" + index + ", size=" + c );
205         }
206         
207         /** Initialize columns width/height. */
208         public void justifyElements() {
209             int columnWidth = defaultColumnWidth();
210             // compute columns width
211             for (LayoutColumn column : columns) {
212                 column.width = columnWidth;
213             }
214             // set element heights
215             for (LayoutColumn column : columns) {
216                 column.justifyElements();
217             }
218         }
219
220         /** Equal width for all columns. */
221         public int defaultColumnWidth() {
222             int result = (clientArea.width - (marginWidth*2) - ((columns.size()-1) * spacing)) / columns.size();
223             assert result > 0;
224             return result;
225         }
226     }
227     
228
229     /**
230      * 
231      */
232     public static class LayoutColumn
233             extends ArrayList<LayoutElement> {
234     
235         public int      width, height;
236         
237         public LayoutColumn( List<Control> controls ) {
238             super( controls.size() );
239             for (Control control : controls) {
240                 add( new LayoutElement( control ) );
241             }
242         }
243
244         public LayoutColumn( LayoutColumn other ) {
245             super( other );
246             this.width = other.width;
247             this.height = other.height;
248         }
249
250         public int computeMinWidth( int wHint ) {
251             // minimum: wHint == 0
252             int result = wHint;
253             for (LayoutElement elm : this) {
254                 result = Math.max( result, elm.computeWidth( wHint ) );
255             }
256             return result;
257         }
258         
259         public void justifyElements() {
260             assert width > 0;
261             height = 0;
262             for (LayoutElement elm : this) {
263                 elm.height = elm.control.computeSize( width, SWT.DEFAULT ).y;
264                 height += elm.height;
265             }
266         }
267     }
268
269
270     /**
271      * 
272      */
273     public static class LayoutElement {
274         
275         public Control  control;
276         
277         public int      height;
278         
279         public LayoutElement( Control control ) {
280             assert control != null;
281             this.control = control;
282         }
283
284         public <T extends LayoutConstraint> T constraint( Class<T> type, T defaultValue ) {
285             ConstraintData data = (ConstraintData)control.getLayoutData();
286             return data != null ? data.constraint( type, defaultValue ) : defaultValue;
287         }
288
289         public int computeWidth( int wHint ) {
290             int width = control.computeSize( wHint, SWT.DEFAULT ).x;
291             // min constraint
292             MinWidthConstraint minConstraint = constraint( MinWidthConstraint.class, null );
293             width = minConstraint != null ? Math.max( minConstraint.getValue(), width ) : width;
294             // max constraint
295             MaxWidthConstraint maxConstraint = constraint( MaxWidthConstraint.class, null );
296             width = maxConstraint != null ? Math.min( maxConstraint.getValue(), width ) : width;
297             return width;
298         }
299         
300         @Override
301         public int hashCode() {
302             return control.hashCode();
303         }
304
305         @Override
306         public boolean equals( Object obj ) {
307             return control.equals( ((LayoutElement)obj).control );
308         }
309     }
310     
311     
312     /**
313      * 
314      */
315     static class PriorityOnTopGoal
316             extends Prioritized
317             implements IOptimizationGoal<LayoutSolution,PercentScore> {
318
319         public PriorityOnTopGoal( int priority ) {
320             super( priority );
321         }
322
323         @Override
324         public boolean optimize( LayoutSolution solution ) {
325             for (LayoutColumn column : solution.columns) {
326                 LayoutElement prev = null;
327                 int index = 0;
328                 for (LayoutElement elm : column) {
329                     if (prev != null) {
330                         PriorityConstraint prevPrio = prev.constraint( PriorityConstraint.class, new PriorityConstraint( 0 ) );
331                         PriorityConstraint elmPrio = elm.constraint( PriorityConstraint.class, new PriorityConstraint( 0 ) );
332                         
333                         if (prevPrio.getValue() < elmPrio.getValue()) {
334                             column.set( index-1, elm );
335                             column.set( index, prev );
336                             return true;
337                         }
338                     }
339                     prev = elm;
340                     ++index;
341                 }
342             }
343             return false;
344         }
345
346         @Override
347         public PercentScore score( LayoutSolution solution ) {
348             // sort ascending height
349             ArrayList<LayoutElement> heightSortedElms = new ArrayList( solution.elements() );
350             Collections.sort( heightSortedElms, new Comparator<LayoutElement>() {
351                 public int compare( LayoutElement elm1, LayoutElement elm2 ) {
352                     assert elm1.height > 0 && elm2.height > 0;
353                     return elm1.height - elm2.height;
354                 }
355             });
356             
357             
358 //            // sort (descending) all elms using their y pos in column
359 //            Multimap<Integer,LayoutElement> heightSortedElms = TreeMultimap.create( Ordering.natural(), Ordering.arbitrary() );
360 //            for (LayoutColumn column : solution.columns) {
361 //                int y = 0; //Integer.MAX_VALUE;
362 //                for (LayoutElement elm : column) {
363 //                    heightSortedElms.put( y, elm );
364 //                    y += elm.height;
365 //                }
366 //            }
367
368             int elmPercent = 100 / solution.elements().size();
369             int result = 0;
370
371             LayoutElement prev = null;
372             for (LayoutElement elm : heightSortedElms) {
373                 if (prev != null) {
374                     PriorityConstraint prevPrio = prev.constraint( PriorityConstraint.class, new PriorityConstraint( 0 ) );
375                     PriorityConstraint elmPrio = elm.constraint( PriorityConstraint.class, new PriorityConstraint( 0 ) );
376
377                     if (prevPrio.getValue() > elmPrio.getValue()) {
378                         result += elmPercent;
379                     }
380                 }
381                 prev = elm;
382             }
383             return new PercentScore( result );
384         }
385     }
386
387
388     /**
389      * 
390      */
391     static class MinOverallHeightGoal
392             extends Prioritized
393             implements IOptimizationGoal<LayoutSolution,PercentScore> {
394
395         private static final Random rand = new Random();
396         
397         private int                 clientHeight;
398
399         public MinOverallHeightGoal( int clientHeight, Comparable priority ) {
400             super( priority );
401             assert clientHeight > 0;
402             this.clientHeight = clientHeight;
403         }
404
405         @Override
406         public boolean optimize( LayoutSolution solution ) {
407             // first idea: more than 1 column
408             if (solution.columns.size() == 1) {
409                 solution.columns.add( new LayoutColumn( Collections.EMPTY_LIST ) );
410             }
411
412             // find min/max height column
413             LayoutColumn minColumn = null, maxColumn = null;
414             for (LayoutColumn column : solution.columns) {
415                 minColumn = minColumn == null || column.height < minColumn.height ? column : minColumn;
416                 maxColumn = maxColumn == null || column.height >= maxColumn.height ? column : maxColumn;
417             }
418             assert minColumn != maxColumn : "minColumn:" + minColumn.height + ", maxColumn:" + maxColumn.height;
419             
420             // biggest column has just 1 elm -> nothing to optimize
421             if (maxColumn.size() <= 1) {
422                 return false;
423             }
424             
425             LayoutElement elm = maxColumn.remove( maxColumn.size() - 1 );
426             minColumn.add( 0, elm );
427
428             solution.justifyElements();
429             return true;
430         }
431
432         @Override
433         public PercentScore score( LayoutSolution solution ) {
434             int maxColumnHeight = 0, minColumnHeight = Integer.MAX_VALUE;
435             int columnWidth = solution.defaultColumnWidth();
436             
437             for (LayoutColumn column : solution.columns) {
438                 // avoid empty column produced by the random optimization
439                 if (column.size() == 0) {
440                     return PercentScore.INVALID;
441                 }
442                 // avoid columns to small for columnWidth
443                 if (column.computeMinWidth( 0 ) > columnWidth) {
444                     return PercentScore.INVALID;
445                 }
446                 
447                 assert column.height >= 0;
448                 maxColumnHeight = Math.max( column.height, maxColumnHeight );
449                 minColumnHeight = Math.min( column.height, minColumnHeight );
450             }
451             
452 //            // result: ratio = max/min
453 //            double ratio = (100d / maxColumnHeight * minColumnHeight);
454 //            PercentScore result = new PercentScore( (int)ratio );
455             
456             // might be > 100
457             int heightPercent = (int)(100d / clientHeight * maxColumnHeight);
458             PercentScore result = new PercentScore( 100 - Math.min( 100, heightPercent ) );
459             assert result.getValue() >= 0 && result.getValue() <= 100;
460             return result;
461         }
462         
463     }
464
465 }