src/de/matthiasmann/twl/DialogLayout.java
author Matthias Mann
Sat May 19 19:02:37 2012 +0200 (22 hours ago)
changeset 1038 0dda4b118c36
parent 922 4a539d4da117
permissions -rw-r--r--
TextArea: added CSS attribute "tab-size" with "-moz-tab-size" alias
     1 /*
     2  * Copyright (c) 2008-2012, Matthias Mann
     3  *
     4  * All rights reserved.
     5  *
     6  * Redistribution and use in source and binary forms, with or without
     7  * modification, are permitted provided that the following conditions are met:
     8  *
     9  *     * Redistributions of source code must retain the above copyright notice,
    10  *       this list of conditions and the following disclaimer.
    11  *     * Redistributions in binary form must reproduce the above copyright
    12  *       notice, this list of conditions and the following disclaimer in the
    13  *       documentation and/or other materials provided with the distribution.
    14  *     * Neither the name of Matthias Mann nor the names of its contributors may
    15  *       be used to endorse or promote products derived from this software
    16  *       without specific prior written permission.
    17  *
    18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
    21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
    22  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
    23  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
    24  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
    25  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
    26  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
    27  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    28  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    29  */
    30 package de.matthiasmann.twl;
    31 
    32 import de.matthiasmann.twl.renderer.AnimationState.StateKey;
    33 import java.util.ArrayList;
    34 import java.util.Arrays;
    35 import java.util.HashMap;
    36 import java.util.HashSet;
    37 import java.util.logging.Level;
    38 import java.util.logging.Logger;
    39 
    40 /**
    41  * A layout manager similar to Swing's GroupLayout
    42  *
    43  * This layout manager uses two independant layout groups:
    44  *   one for the horizontal axis
    45  *   one for the vertical axis.
    46  * Every widget must be added to both the horizontal and the vertical group.
    47  *
    48  * When a widget is added to a group it will also be added as a child widget
    49  * if it was not already added. You can add widgets to DialogLayout before
    50  * adding them to a group to set the focus order.
    51  *
    52  * There are two kinds of groups:
    53  *   a sequential group which which behaves similar to BoxLayout
    54  *   a parallel group which alignes the start and size of each child
    55  *
    56  * Groups can be cascaded as a tree without restrictions.
    57  *
    58  * It is also possible to add widgets to DialogLayout without adding them
    59  * to the layout groups. These widgets are then not touched by DialogLayout's
    60  * layout system.
    61  *
    62  * When a widget is only added to either the horizontal or vertical groups
    63  * and not both, then an IllegalStateException exception is created on layout.
    64  *
    65  * To help debugging the group construction you can set the system property
    66  * "debugLayoutGroups" to "true" which will collect additional stack traces
    67  * to help locate the source of the error.
    68  *
    69  * @author Matthias Mann
    70  * @see #createParallelGroup() 
    71  * @see #createSequentialGroup()
    72  */
    73 public class DialogLayout extends Widget {
    74 
    75     /**
    76      * Symbolic constant to refer to "small gap".
    77      * @see #getSmallGap()
    78      * @see Group#addGap(int)
    79      * @see Group#addGap(int, int, int)
    80      */
    81     public static final int SMALL_GAP   = -1;
    82 
    83     /**
    84      * Symbolic constant to refer to "medium gap".
    85      * @see #getMediumGap()
    86      * @see Group#addGap(int)
    87      * @see Group#addGap(int, int, int)
    88      */
    89     public static final int MEDIUM_GAP  = -2;
    90 
    91     /**
    92      * Symbolic constant to refer to "large gap".
    93      * @see #getLargeGap()
    94      * @see Group#addGap(int)
    95      * @see Group#addGap(int, int, int)
    96      */
    97     public static final int LARGE_GAP   = -3;
    98 
    99     /**
   100      * Symbolic constant to refer to "default gap".
   101      * The default gap is added (when enabled) between widgets.
   102      *
   103      * @see #getDefaultGap()
   104      * @see #setAddDefaultGaps(boolean)
   105      * @see #isAddDefaultGaps()
   106      * @see Group#addGap(int)
   107      * @see Group#addGap(int, int, int)
   108      */
   109     public static final int DEFAULT_GAP = -4;
   110 
   111     private static final boolean DEBUG_LAYOUT_GROUPS = Widget.getSafeBooleanProperty("debugLayoutGroups");
   112     
   113     protected Dimension smallGap;
   114     protected Dimension mediumGap;
   115     protected Dimension largeGap;
   116     protected Dimension defaultGap;
   117     protected ParameterMap namedGaps;
   118 
   119     protected boolean addDefaultGaps = true;
   120     protected boolean includeInvisibleWidgets = true;
   121     protected boolean redoDefaultGaps;
   122     protected boolean isPrepared;
   123     protected boolean blockInvalidateLayoutTree;
   124     protected boolean warnOnIncomplete;
   125 
   126     private Group horz;
   127     private Group vert;
   128 
   129     /**
   130      * Debugging aid. Captures the stack trace where one of the group was last assigned.
   131      */
   132     Throwable debugStackTrace;
   133 
   134     final HashMap<Widget, WidgetSpring> widgetSprings;
   135 
   136     /**
   137      * Creates a new DialogLayout widget.
   138      *
   139      * Initially both the horizontal and the vertical group are null.
   140      * 
   141      * @see #setHorizontalGroup(de.matthiasmann.twl.DialogLayout.Group)
   142      * @see #setVerticalGroup(de.matthiasmann.twl.DialogLayout.Group)
   143      */
   144     public DialogLayout() {
   145         widgetSprings = new HashMap<Widget, WidgetSpring>();
   146         collectDebugStack();
   147     }
   148 
   149     public Group getHorizontalGroup() {
   150         return horz;
   151     }
   152 
   153     /**
   154      * The horizontal group controls the position and size of all child
   155      * widgets along the X axis.
   156      *
   157      * Every widget must be part of both horizontal and vertical group.
   158      * Otherwise a IllegalStateException is thrown at layout time.
   159      *
   160      * If you want to change both horizontal and vertical group then
   161      * it's recommended to set the other group first to null:
   162      * <pre>
   163      * setVerticalGroup(null);
   164      * setHorizontalGroup(newHorzGroup);
   165      * setVerticalGroup(newVertGroup);
   166      * </pre>
   167      *
   168      * @param g the group used for the X axis
   169      * @see #setVerticalGroup(de.matthiasmann.twl.DialogLayout.Group)
   170      */
   171     public void setHorizontalGroup(Group g) {
   172         if(g != null) {
   173             g.checkGroup(this);
   174         }
   175         this.horz = g;
   176         collectDebugStack();
   177         layoutGroupsChanged();
   178     }
   179 
   180     public Group getVerticalGroup() {
   181         return vert;
   182     }
   183 
   184     /**
   185      * The vertical group controls the position and size of all child
   186      * widgets along the Y axis.
   187      *
   188      * Every widget must be part of both horizontal and vertical group.
   189      * Otherwise a IllegalStateException is thrown at layout time.
   190      *
   191      * @param g the group used for the Y axis
   192      * @see #setHorizontalGroup(de.matthiasmann.twl.DialogLayout.Group) 
   193      */
   194     public void setVerticalGroup(Group g) {
   195         if(g != null) {
   196             g.checkGroup(this);
   197         }
   198         this.vert = g;
   199         collectDebugStack();
   200         layoutGroupsChanged();
   201     }
   202 
   203     public Dimension getSmallGap() {
   204         return smallGap;
   205     }
   206 
   207     public void setSmallGap(Dimension smallGap) {
   208         this.smallGap = smallGap;
   209         maybeInvalidateLayoutTree();
   210     }
   211 
   212     public Dimension getMediumGap() {
   213         return mediumGap;
   214     }
   215 
   216     public void setMediumGap(Dimension mediumGap) {
   217         this.mediumGap = mediumGap;
   218         maybeInvalidateLayoutTree();
   219     }
   220 
   221     public Dimension getLargeGap() {
   222         return largeGap;
   223     }
   224 
   225     public void setLargeGap(Dimension largeGap) {
   226         this.largeGap = largeGap;
   227         maybeInvalidateLayoutTree();
   228     }
   229 
   230     public Dimension getDefaultGap() {
   231         return defaultGap;
   232     }
   233 
   234     public void setDefaultGap(Dimension defaultGap) {
   235         this.defaultGap = defaultGap;
   236         maybeInvalidateLayoutTree();
   237     }
   238 
   239     public boolean isAddDefaultGaps() {
   240         return addDefaultGaps;
   241     }
   242 
   243     /**
   244      * Determine whether default gaps should be added from the theme or not.
   245      * 
   246      * @param addDefaultGaps if true then default gaps are added.
   247      */
   248     public void setAddDefaultGaps(boolean addDefaultGaps) {
   249         this.addDefaultGaps = addDefaultGaps;
   250     }
   251 
   252     /**
   253      * removes all default gaps from all groups.
   254      */
   255     public void removeDefaultGaps() {
   256         if(horz != null && vert != null) {
   257             horz.removeDefaultGaps();
   258             vert.removeDefaultGaps();
   259             maybeInvalidateLayoutTree();
   260         }
   261     }
   262 
   263     /**
   264      * Adds theme dependant default gaps to all groups.
   265      */
   266     public void addDefaultGaps() {
   267         if(horz != null && vert != null) {
   268             horz.addDefaultGap();
   269             vert.addDefaultGap();
   270             maybeInvalidateLayoutTree();
   271         }
   272     }
   273 
   274     public boolean isIncludeInvisibleWidgets() {
   275         return includeInvisibleWidgets;
   276     }
   277 
   278     /**
   279      * Controls whether invisible widgets should be included in the layout or
   280      * not. If they are not included then the layout is recomputed when the
   281      * visibility of a child widget changes.
   282      *
   283      * The default is true
   284      *
   285      * @param includeInvisibleWidgets If true then invisible widgets are included,
   286      *      if false they don't contribute to the layout.
   287      */
   288     public void setIncludeInvisibleWidgets(boolean includeInvisibleWidgets) {
   289         if(this.includeInvisibleWidgets != includeInvisibleWidgets) {
   290             this.includeInvisibleWidgets = includeInvisibleWidgets;
   291             layoutGroupsChanged();
   292         }
   293     }
   294 
   295     private void collectDebugStack() {
   296         warnOnIncomplete = true;
   297         if(DEBUG_LAYOUT_GROUPS) {
   298             debugStackTrace = new Throwable("DialogLayout created/used here").fillInStackTrace();
   299         }
   300     }
   301 
   302     private void warnOnIncomplete() {
   303         warnOnIncomplete = false;
   304         getLogger().log(Level.WARNING, "Dialog layout has incomplete state", debugStackTrace);
   305     }
   306 
   307     static Logger getLogger() {
   308         return Logger.getLogger(DialogLayout.class.getName());
   309     }
   310 
   311     protected void applyThemeDialogLayout(ThemeInfo themeInfo) {
   312         try {
   313             blockInvalidateLayoutTree = true;
   314             setSmallGap(themeInfo.getParameterValue("smallGap", true, Dimension.class, Dimension.ZERO));
   315             setMediumGap(themeInfo.getParameterValue("mediumGap", true, Dimension.class, Dimension.ZERO));
   316             setLargeGap(themeInfo.getParameterValue("largeGap", true, Dimension.class, Dimension.ZERO));
   317             setDefaultGap(themeInfo.getParameterValue("defaultGap", true, Dimension.class, Dimension.ZERO));
   318             namedGaps = themeInfo.getParameterMap("namedGaps");
   319         } finally {
   320             blockInvalidateLayoutTree = false;
   321         }
   322         invalidateLayout();
   323     }
   324 
   325     @Override
   326     protected void applyTheme(ThemeInfo themeInfo) {
   327         super.applyTheme(themeInfo);
   328         applyThemeDialogLayout(themeInfo);
   329     }
   330 
   331     @Override
   332     public int getMinWidth() {
   333         if(horz != null) {
   334             prepare();
   335             return horz.getMinSize(AXIS_X) + getBorderHorizontal();
   336         }
   337         return super.getMinWidth();
   338     }
   339 
   340     @Override
   341     public int getMinHeight() {
   342         if(vert != null) {
   343             prepare();
   344             return vert.getMinSize(AXIS_Y) + getBorderVertical();
   345         }
   346         return super.getMinHeight();
   347     }
   348 
   349     @Override
   350     public int getPreferredInnerWidth() {
   351         if(horz != null) {
   352             prepare();
   353             return horz.getPrefSize(AXIS_X);
   354         }
   355         return super.getPreferredInnerWidth();
   356     }
   357 
   358     @Override
   359     public int getPreferredInnerHeight() {
   360         if(vert != null) {
   361             prepare();
   362             return vert.getPrefSize(AXIS_Y);
   363         }
   364         return super.getPreferredInnerHeight();
   365     }
   366 
   367     @Override
   368     public void adjustSize() {
   369         if(horz != null && vert != null) {
   370             prepare();
   371             int minWidth = horz.getMinSize(AXIS_X);
   372             int minHeight = vert.getMinSize(AXIS_Y);
   373             int prefWidth = horz.getPrefSize(AXIS_X);
   374             int prefHeight = vert.getPrefSize(AXIS_Y);
   375             int maxWidth = getMaxWidth();
   376             int maxHeight = getMaxHeight();
   377             setInnerSize(
   378                     computeSize(minWidth, prefWidth, maxWidth),
   379                     computeSize(minHeight, prefHeight, maxHeight));
   380             doLayout();
   381         }
   382     }
   383 
   384     @Override
   385     protected void layout() {
   386         if(horz != null && vert != null) {
   387             prepare();
   388             doLayout();
   389         } else if(warnOnIncomplete) {
   390             warnOnIncomplete();
   391         }
   392     }
   393     
   394     protected void prepare() {
   395         if(redoDefaultGaps) {
   396             if(addDefaultGaps) {
   397                 try {
   398                     blockInvalidateLayoutTree = true;
   399                     removeDefaultGaps();
   400                     addDefaultGaps();
   401                 } finally {
   402                     blockInvalidateLayoutTree = false;
   403                 }
   404             }
   405             redoDefaultGaps = false;
   406             isPrepared = false;
   407         }
   408         if(!isPrepared) {
   409             for(WidgetSpring s : widgetSprings.values()) {
   410                 if(includeInvisibleWidgets || s.w.isVisible()) {
   411                     s.prepare();
   412                 }
   413             }
   414             isPrepared = true;
   415         }
   416     }
   417 
   418     protected void doLayout() {
   419         horz.setSize(AXIS_X, getInnerX(), getInnerWidth());
   420         vert.setSize(AXIS_Y, getInnerY(), getInnerHeight());
   421         try{
   422             for(WidgetSpring s : widgetSprings.values()) {
   423                 if(includeInvisibleWidgets || s.w.isVisible()) {
   424                     s.apply();
   425                 }
   426             }
   427         }catch(IllegalStateException ex) {
   428             if(debugStackTrace != null && ex.getCause() == null) {
   429                 ex.initCause(debugStackTrace);
   430             }
   431             throw ex;
   432         }
   433     }
   434 
   435     @Override
   436     public void invalidateLayout() {
   437         isPrepared = false;
   438         super.invalidateLayout();
   439     }
   440 
   441     @Override
   442     protected void paintWidget(GUI gui) {
   443         isPrepared = false;
   444         // super.paintWidget() is empty
   445     }
   446 
   447     @Override
   448     protected void sizeChanged() {
   449         isPrepared = false;
   450         super.sizeChanged();
   451     }
   452 
   453     @Override
   454     protected void afterAddToGUI(GUI gui) {
   455         isPrepared = false;
   456         super.afterAddToGUI(gui);
   457     }
   458 
   459     /**
   460      * Creates a new parallel group.
   461      * All children in a parallel group share the same position and size of it's axis.
   462      *
   463      * @return the new parallel Group.
   464      */
   465     public Group createParallelGroup() {
   466         return new ParallelGroup();
   467     }
   468 
   469     /**
   470      * Creates a parallel group and adds the specified widgets.
   471      *
   472      * @see #createParallelGroup()
   473      * @param widgets the widgets to add
   474      * @return a new parallel Group.
   475      */
   476     public Group createParallelGroup(Widget ... widgets) {
   477         return createParallelGroup().addWidgets(widgets);
   478     }
   479 
   480     /**
   481      * Creates a parallel group and adds the specified groups.
   482      *
   483      * @see #createParallelGroup()
   484      * @param groups the groups to add
   485      * @return a new parallel Group.
   486      */
   487     public Group createParallelGroup(Group ... groups) {
   488         return createParallelGroup().addGroups(groups);
   489     }
   490 
   491     /**
   492      * Creates a new sequential group.
   493      * All children in a sequential group are ordered with increasing coordinates
   494      * along it's axis in the order they are added to the group. The available
   495      * size is distributed among the children depending on their min/preferred/max
   496      * sizes.
   497      * 
   498      * @return a new sequential Group.
   499      */
   500     public Group createSequentialGroup() {
   501         return new SequentialGroup();
   502     }
   503 
   504     /**
   505      * Creates a sequential group and adds the specified widgets.
   506      *
   507      * @see #createSequentialGroup()
   508      * @param widgets the widgets to add
   509      * @return a new sequential Group.
   510      */
   511     public Group createSequentialGroup(Widget ... widgets) {
   512         return createSequentialGroup().addWidgets(widgets);
   513     }
   514 
   515     /**
   516      * Creates a sequential group and adds the specified groups.
   517      *
   518      * @see #createSequentialGroup()
   519      * @param groups the groups to add
   520      * @return a new sequential Group.
   521      */
   522     public Group createSequentialGroup(Group ... groups) {
   523         return createSequentialGroup().addGroups(groups);
   524     }
   525 
   526     @Override
   527     public void insertChild(Widget child, int index) throws IndexOutOfBoundsException {
   528         super.insertChild(child, index);
   529         widgetSprings.put(child, new WidgetSpring(child));
   530     }
   531 
   532     @Override
   533     public void removeAllChildren() {
   534         super.removeAllChildren();
   535         widgetSprings.clear();
   536         recheckWidgets();
   537         layoutGroupsChanged();
   538     }
   539 
   540     @Override
   541     public Widget removeChild(int index) throws IndexOutOfBoundsException {
   542         final Widget widget = super.removeChild(index);
   543         widgetSprings.remove(widget);
   544         recheckWidgets();
   545         layoutGroupsChanged();
   546         return widget;
   547     }
   548 
   549     /**
   550      * Sets the alignment of the specified widget.
   551      * The widget must have already been added to this container for this method to work.
   552      *
   553      * <p>The default alignment of a widget is {@link Alignment#FILL}</p>
   554      * 
   555      * @param widget the widget for which the alignment should be set
   556      * @param alignment the new alignment
   557      * @return true if the widget's alignment was changed, false otherwise
   558      */
   559     public boolean setWidgetAlignment(Widget widget, Alignment alignment) {
   560         if(widget == null) {
   561             throw new NullPointerException("widget");
   562         }
   563         if(alignment == null) {
   564             throw new NullPointerException("alignment");
   565         }
   566         WidgetSpring ws = widgetSprings.get(widget);
   567         if(ws != null) {
   568             assert widget.getParent() == this;
   569             ws.alignment = alignment;
   570             return true;
   571         }
   572         return false;
   573     }
   574 
   575     protected void recheckWidgets() {
   576         if(horz != null) {
   577             horz.recheckWidgets();
   578         }
   579         if(vert != null) {
   580             vert.recheckWidgets();
   581         }
   582     }
   583     
   584     protected void layoutGroupsChanged() {
   585         redoDefaultGaps = true;
   586         maybeInvalidateLayoutTree();
   587     }
   588     
   589     protected void maybeInvalidateLayoutTree() {
   590         if(horz != null && vert != null && !blockInvalidateLayoutTree) {
   591             invalidateLayout();
   592         }
   593     }
   594 
   595     @Override
   596     protected void childVisibilityChanged(Widget child) {
   597         if(!includeInvisibleWidgets) {
   598             layoutGroupsChanged(); // this will also clear isPrepared
   599         }
   600     }
   601 
   602     void removeChild(WidgetSpring widgetSpring) {
   603         Widget widget = widgetSpring.w;
   604         int idx = getChildIndex(widget);
   605         assert idx >= 0;
   606         super.removeChild(idx);
   607         widgetSprings.remove(widget);
   608     }
   609 
   610     public static class Gap {
   611         public final int min;
   612         public final int preferred;
   613         public final int max;
   614 
   615         public Gap() {
   616             this(0,0,32767);
   617         }
   618         public Gap(int size) {
   619             this(size, size, size);
   620         }
   621         public Gap(int min, int preferred) {
   622             this(min, preferred, 32767);
   623         }
   624         public Gap(int min, int preferred, int max) {
   625             if(min < 0) {
   626                 throw new IllegalArgumentException("min");
   627             }
   628             if(preferred < min) {
   629                 throw new IllegalArgumentException("preferred");
   630             }
   631             if(max < 0 || (max > 0 && max < preferred)) {
   632                 throw new IllegalArgumentException("max");
   633             }
   634             this.min = min;
   635             this.preferred = preferred;
   636             this.max = max;
   637         }
   638     }
   639     
   640     static final int AXIS_X = 0;
   641     static final int AXIS_Y = 1;
   642 
   643     static abstract class Spring {
   644         abstract int getMinSize(int axis);
   645         abstract int getPrefSize(int axis);
   646         abstract int getMaxSize(int axis);
   647         abstract void setSize(int axis, int pos, int size);
   648 
   649         Spring() {
   650         }
   651         
   652         void collectAllSprings(HashSet<Spring> result) {
   653             result.add(this);
   654         }
   655 
   656         boolean isVisible() {
   657             return true;
   658         }
   659     }
   660 
   661     private static class WidgetSpring extends Spring {
   662         final Widget w;
   663         Alignment alignment;
   664         int x;
   665         int y;
   666         int width;
   667         int height;
   668         int minWidth;
   669         int minHeight;
   670         int maxWidth;
   671         int maxHeight;
   672         int prefWidth;
   673         int prefHeight;
   674         int flags;
   675 
   676         WidgetSpring(Widget w) {
   677             this.w = w;
   678             this.alignment = Alignment.FILL;
   679         }
   680 
   681         void prepare() {
   682             this.x = w.getX();
   683             this.y = w.getY();
   684             this.width = w.getWidth();
   685             this.height = w.getHeight();
   686             this.minWidth = w.getMinWidth();
   687             this.minHeight = w.getMinHeight();
   688             this.maxWidth = w.getMaxWidth();
   689             this.maxHeight = w.getMaxHeight();
   690             this.prefWidth = computeSize(minWidth, w.getPreferredWidth(), maxWidth);
   691             this.prefHeight = computeSize(minHeight, w.getPreferredHeight(), maxHeight);
   692             this.flags = 0;
   693         }
   694 
   695         @Override
   696         int getMinSize(int axis) {
   697             switch(axis) {
   698             case AXIS_X: return minWidth;
   699             case AXIS_Y: return minHeight;
   700             default: throw new IllegalArgumentException("axis");
   701             }
   702         }
   703 
   704         @Override
   705         int getPrefSize(int axis) {
   706             switch(axis) {
   707             case AXIS_X: return prefWidth;
   708             case AXIS_Y: return prefHeight;
   709             default: throw new IllegalArgumentException("axis");
   710             }
   711         }
   712 
   713         @Override
   714         int getMaxSize(int axis) {
   715             switch(axis) {
   716             case AXIS_X: return maxWidth;
   717             case AXIS_Y: return maxHeight;
   718             default: throw new IllegalArgumentException("axis");
   719             }
   720         }
   721 
   722         @Override
   723         void setSize(int axis, int pos, int size) {
   724             this.flags |= 1 << axis;
   725             switch(axis) {
   726             case AXIS_X:
   727                 this.x = pos;
   728                 this.width = size;
   729                 break;
   730             case AXIS_Y:
   731                 this.y = pos;
   732                 this.height = size;
   733                 break;
   734             default:
   735                 throw new IllegalArgumentException("axis");
   736             }
   737         }
   738 
   739         void apply() {
   740             if(flags != 3) {
   741                 invalidState();
   742             }
   743             if(alignment != Alignment.FILL) {
   744                 int newWidth = Math.min(width, prefWidth);
   745                 int newHeight = Math.min(height, prefHeight);
   746                 w.setPosition(
   747                         x + alignment.computePositionX(width, newWidth),
   748                         y + alignment.computePositionY(height, newHeight));
   749                 w.setSize(newWidth, newHeight);
   750             } else {
   751                 w.setPosition(x, y);
   752                 w.setSize(width, height);
   753             }
   754         }
   755 
   756         @Override
   757         boolean isVisible() {
   758             return w.isVisible();
   759         }
   760 
   761         @SuppressWarnings("PointlessBitwiseExpression")
   762         void invalidState() {
   763             StringBuilder sb = new StringBuilder();
   764             sb.append("Widget ").append(w)
   765                     .append(" with theme ").append(w.getTheme())
   766                     .append(" is not part of the following groups:");
   767             if((flags & (1 << AXIS_X)) == 0) {
   768                 sb.append(" horizontal");
   769             }
   770             if((flags & (1 << AXIS_Y)) == 0) {
   771                 sb.append(" vertical");
   772             }
   773             throw new IllegalStateException(sb.toString());
   774         }
   775     }
   776 
   777     private class GapSpring extends Spring {
   778         final int min;
   779         final int pref;
   780         final int max;
   781         final boolean isDefault;
   782 
   783         GapSpring(int min, int pref, int max, boolean isDefault) {
   784             convertConstant(AXIS_X, min);
   785             convertConstant(AXIS_X, pref);
   786             convertConstant(AXIS_X, max);
   787             this.min = min;
   788             this.pref = pref;
   789             this.max = max;
   790             this.isDefault = isDefault;
   791         }
   792 
   793         @Override
   794         int getMinSize(int axis) {
   795             return convertConstant(axis, min);
   796         }
   797 
   798         @Override
   799         int getPrefSize(int axis) {
   800             return convertConstant(axis, pref);
   801         }
   802 
   803         @Override
   804         int getMaxSize(int axis) {
   805             return convertConstant(axis, max);
   806         }
   807 
   808         @Override
   809         void setSize(int axis, int pos, int size) {
   810         }
   811 
   812         private int convertConstant(int axis, int value) {
   813             if(value >= 0) {
   814                 return value;
   815             }
   816             Dimension dim;
   817             switch(value) {
   818             case SMALL_GAP:
   819                 dim = smallGap;
   820                 break;
   821             case MEDIUM_GAP:
   822                 dim = mediumGap;
   823                 break;
   824             case LARGE_GAP:
   825                 dim = largeGap;
   826                 break;
   827             case DEFAULT_GAP:
   828                 dim = defaultGap;
   829                 break;
   830             default:
   831                 throw new IllegalArgumentException("Invalid gap size: " + value);
   832             }
   833             if(dim == null) {
   834                 return 0;
   835             } else if(axis == AXIS_X) {
   836                 return dim.getX();
   837             } else {
   838                 return dim.getY();
   839             }
   840         }
   841     }
   842 
   843     static final Gap NO_GAP = new Gap(0,0,32767);
   844 
   845     private class NamedGapSpring extends Spring {
   846         final String name;
   847 
   848         public NamedGapSpring(String name) {
   849             this.name = name;
   850         }
   851 
   852         @Override
   853         int getMaxSize(int axis) {
   854             return getGap().max;
   855         }
   856 
   857         @Override
   858         int getMinSize(int axis) {
   859             return getGap().min;
   860         }
   861 
   862         @Override
   863         int getPrefSize(int axis) {
   864             return getGap().preferred;
   865         }
   866 
   867         @Override
   868         void setSize(int axis, int pos, int size) {
   869         }
   870 
   871         private Gap getGap() {
   872             if(namedGaps != null) {
   873                 return namedGaps.getParameterValue(name, true, Gap.class, NO_GAP);
   874             }
   875             return NO_GAP;
   876         }
   877     }
   878 
   879     public abstract class Group extends Spring {
   880         final ArrayList<Spring> springs = new ArrayList<Spring>();
   881         boolean alreadyAdded;
   882 
   883         void checkGroup(DialogLayout owner) {
   884             if(DialogLayout.this != owner) {
   885                 throw new IllegalArgumentException("Can't add group from different layout");
   886             }
   887             if(alreadyAdded) {
   888                 throw new IllegalArgumentException("Group already added to another group");
   889             }
   890         }
   891 
   892         /**
   893          * Adds another group. A group can only be added once.
   894          *
   895          * WARNING: No check is made to prevent cycles.
   896          * 
   897          * @param g the child Group
   898          * @return this Group
   899          */
   900         public Group addGroup(Group g) {
   901             g.checkGroup(DialogLayout.this);
   902             g.alreadyAdded = true;
   903             addSpring(g);
   904             return this;
   905         }
   906 
   907         /**
   908          * Adds several groups. A group can only be added once.
   909          *
   910          * WARNING: No check is made to prevent cycles.
   911          *
   912          * @param groups the groups to add
   913          * @return this Group
   914          */
   915         public Group addGroups(Group ... groups) {
   916             for(Group g : groups) {
   917                 addGroup(g);
   918             }
   919             return this;
   920         }
   921 
   922         /**
   923          * Adds a widget to this group.
   924          *
   925          * <p>If the widget is already a child widget of the DialogLayout then it
   926          * keeps it current settings, otherwise it is added the alignment is set
   927          * to {@link Alignment#FILL}.</p>
   928          *
   929          * @param w the child widget.
   930          * @return this Group
   931          * @see Widget#add(de.matthiasmann.twl.Widget)
   932          */
   933         public Group addWidget(Widget w) {
   934             if(w.getParent() != DialogLayout.this) {
   935                 DialogLayout.this.add(w);
   936             }
   937             WidgetSpring s = widgetSprings.get(w);
   938             if(s == null) {
   939                 throw new IllegalStateException("WidgetSpring for Widget not found: " + w);
   940             }
   941             addSpring(s);
   942             return this;
   943         }
   944 
   945         /**
   946          * Adds a widget to this group.
   947          *
   948          * <p>If the widget is already a child widget of the DialogLayout then it
   949          * it's alignment is set to the specified value overwriting any current
   950          * alignment setting, otherwise it is added to the DialogLayout.</p>
   951          *
   952          * @param w the child widget.
   953          * @param alignment the alignment of the child widget.
   954          * @return this Group
   955          * @see Widget#add(de.matthiasmann.twl.Widget) 
   956          * @see #setWidgetAlignment(de.matthiasmann.twl.Widget, de.matthiasmann.twl.Alignment)
   957          */
   958         public Group addWidget(Widget w, Alignment alignment) {
   959             addWidget(w);
   960             setWidgetAlignment(w, alignment);
   961             return this;
   962         }
   963 
   964         /**
   965          * Adds several widgets to this group. The widget is automatically added as child widget.
   966          * 
   967          * @param widgets The widgets which should be added.
   968          * @return this Group
   969          */
   970         public Group addWidgets(Widget ... widgets) {
   971             for(Widget w : widgets) {
   972                 addWidget(w);
   973             }
   974             return this;
   975         }
   976 
   977         /**
   978          * Adds several widgets to this group, inserting the specified gap in between.
   979          * Each widget also gets an animation state set depending on it's position.
   980          *
   981          * The state gapName+"NotFirst" is set to false for widgets[0] and true for all others
   982          * The state gapName+"NotLast" is set to false for widgets[n-1] and true for all others
   983          *
   984          * @param gapName the name of the gap to insert between widgets
   985          * @param widgets The widgets which should be added.
   986          * @return this Group
   987          */
   988         public Group addWidgetsWithGap(String gapName, Widget ... widgets) {
   989             StateKey stateNotFirst = StateKey.get(gapName.concat("NotFirst"));
   990             StateKey stateNotLast = StateKey.get(gapName.concat("NotLast"));
   991             for(int i=0,n=widgets.length ; i<n ;i++) {
   992                 if(i > 0) {
   993                     addGap(gapName);
   994                 }
   995                 Widget w = widgets[i];
   996                 addWidget(w);
   997                 AnimationState as = w.getAnimationState();
   998                 as.setAnimationState(stateNotFirst, i > 0);
   999                 as.setAnimationState(stateNotLast, i < n-1);
  1000             }
  1001             return this;
  1002         }
  1003         
  1004         /**
  1005          * Adds a generic gap. Can use symbolic gap names.
  1006          *
  1007          * @param min the minimum size in pixels or a symbolic constant
  1008          * @param pref the preferred size in pixels or a symbolic constant
  1009          * @param max the maximum size in pixels or a symbolic constant
  1010          * @return this Group
  1011          * @see DialogLayout#SMALL_GAP
  1012          * @see DialogLayout#MEDIUM_GAP
  1013          * @see DialogLayout#LARGE_GAP
  1014          * @see DialogLayout#DEFAULT_GAP
  1015          */
  1016         public Group addGap(int min, int pref, int max) {
  1017             addSpring(new GapSpring(min, pref, max, false));
  1018             return this;
  1019         }
  1020 
  1021         /**
  1022          * Adds a fixed sized gap. Can use symbolic gap names.
  1023          *
  1024          * @param size the size in pixels or a symbolic constant
  1025          * @return this Group
  1026          * @see DialogLayout#SMALL_GAP
  1027          * @see DialogLayout#MEDIUM_GAP
  1028          * @see DialogLayout#LARGE_GAP
  1029          * @see DialogLayout#DEFAULT_GAP
  1030          */
  1031         public Group addGap(int size) {
  1032             addSpring(new GapSpring(size, size, size, false));
  1033             return this;
  1034         }
  1035 
  1036         /**
  1037          * Adds a gap with minimum size. Can use symbolic gap names.
  1038          *
  1039          * @param minSize the minimum size in pixels or a symbolic constant
  1040          * @return this Group
  1041          * @see DialogLayout#SMALL_GAP
  1042          * @see DialogLayout#MEDIUM_GAP
  1043          * @see DialogLayout#LARGE_GAP
  1044          * @see DialogLayout#DEFAULT_GAP
  1045          */
  1046         public Group addMinGap(int minSize) {
  1047             addSpring(new GapSpring(minSize, minSize, Short.MAX_VALUE, false));
  1048             return this;
  1049         }
  1050 
  1051         /**
  1052          * Adds a flexible gap with no minimum size.
  1053          *
  1054          * <p>This is equivalent to {@code addGap(0, 0, Short.MAX_VALUE) }</p>
  1055          * @return this Group
  1056          */
  1057         public Group addGap() {
  1058             addSpring(new GapSpring(0, 0, Short.MAX_VALUE, false));
  1059             return this;
  1060         }
  1061 
  1062         /**
  1063          * Adds a named gap.
  1064          * 
  1065          * <p>Named gaps are configured via the theme parameter "namedGaps" which
  1066          * maps from names to &lt;gap&gt; objects.</p>
  1067          * 
  1068          * <p>They behave equal to {@link #addGap(int, int, int) }.</p>
  1069          * 
  1070          * @param name the name of the gap (vcase sensitive)
  1071          * @return this Group
  1072          */
  1073         public Group addGap(String name) {
  1074             if(name.length() == 0) {
  1075                 throw new IllegalArgumentException("name");
  1076             }
  1077             addSpring(new NamedGapSpring(name));
  1078             return this;
  1079         }
  1080 
  1081         /**
  1082          * Remove all default gaps from this and child groups
  1083          */
  1084         public void removeDefaultGaps() {
  1085             for(int i=springs.size() ; i-->0 ;) {
  1086                 Spring s = springs.get(i);
  1087                 if(s instanceof GapSpring) {
  1088                     if(((GapSpring)s).isDefault) {
  1089                         springs.remove(i);
  1090                     }
  1091                 } else if(s instanceof Group) {
  1092                     ((Group)s).removeDefaultGaps();
  1093                 }
  1094             }
  1095         }
  1096 
  1097         /**
  1098          * Add a default gap between all children except if the neighbour is already a Gap.
  1099          */
  1100         public void addDefaultGap() {
  1101             for(int i=0 ; i<springs.size() ; i++) {
  1102                 Spring s = springs.get(i);
  1103                 if(s instanceof Group) {
  1104                     ((Group)s).addDefaultGap();
  1105                 }
  1106             }
  1107         }
  1108 
  1109         /**
  1110          * Removes the specified group from this group.
  1111          * 
  1112          * @param g the group to remove
  1113          * @param removeWidgets if true all widgets in the specified group
  1114          *      should be removed from the {@code DialogLayout}
  1115          * @return true if it was found and removed, false otherwise
  1116          */
  1117         public boolean removeGroup(Group g, boolean removeWidgets) {
  1118             for(int i=0 ; i<springs.size() ; i++) {
  1119                 if(springs.get(i) == g) {
  1120                     springs.remove(i);
  1121                     if(removeWidgets) {
  1122                         g.removeWidgets();
  1123                         DialogLayout.this.recheckWidgets();
  1124                     }
  1125                     DialogLayout.this.layoutGroupsChanged();
  1126                     return true;
  1127                 }
  1128             }
  1129             return false;
  1130         }
  1131 
  1132         /**
  1133          * Removes all elements from this group
  1134          *
  1135          * @param removeWidgets if true all widgets in this group are removed
  1136          *      from the {@code DialogLayout}
  1137          */
  1138         public void clear(boolean removeWidgets) {
  1139             if(removeWidgets) {
  1140                 removeWidgets();
  1141             }
  1142             springs.clear();
  1143             if(removeWidgets) {
  1144                 DialogLayout.this.recheckWidgets();
  1145             }
  1146             DialogLayout.this.layoutGroupsChanged();
  1147         }
  1148 
  1149         void addSpring(Spring s) {
  1150             springs.add(s);
  1151             DialogLayout.this.layoutGroupsChanged();
  1152         }
  1153 
  1154         void recheckWidgets() {
  1155             for(int i=springs.size() ; i-->0 ;) {
  1156                 Spring s = springs.get(i);
  1157                 if(s instanceof WidgetSpring) {
  1158                     if(!widgetSprings.containsKey(((WidgetSpring)s).w)) {
  1159                         springs.remove(i);
  1160                     }
  1161                 } else if(s instanceof Group) {
  1162                     ((Group)s).recheckWidgets();
  1163                 }
  1164             }
  1165         }
  1166         
  1167         void removeWidgets() {
  1168             for(int i=springs.size() ; i-->0 ;) {
  1169                 Spring s = springs.get(i);
  1170                 if(s instanceof WidgetSpring) {
  1171                     removeChild((WidgetSpring)s);
  1172                 } else if(s instanceof Group) {
  1173                     ((Group)s).removeWidgets();
  1174                 }
  1175             }
  1176         }
  1177     }
  1178 
  1179     static class SpringDelta implements Comparable<SpringDelta> {
  1180         final int idx;
  1181         final int delta;
  1182 
  1183         SpringDelta(int idx, int delta) {
  1184             this.idx = idx;
  1185             this.delta = delta;
  1186         }
  1187 
  1188         public int compareTo(SpringDelta o) {
  1189             return delta - o.delta;
  1190         }
  1191     }
  1192 
  1193     class SequentialGroup extends Group {
  1194         SequentialGroup() {
  1195         }
  1196 
  1197         @Override
  1198         int getMinSize(int axis) {
  1199             int size = 0;
  1200             for(int i=0,n=springs.size() ; i<n ; i++) {
  1201                 Spring s = springs.get(i);
  1202                 if(includeInvisibleWidgets || s.isVisible()) {
  1203                     size += s.getMinSize(axis);
  1204                 }
  1205             }
  1206             return size;
  1207         }
  1208 
  1209         @Override
  1210         int getPrefSize(int axis) {
  1211             int size = 0;
  1212             for(int i=0,n=springs.size() ; i<n ; i++) {
  1213                 Spring s = springs.get(i);
  1214                 if(includeInvisibleWidgets || s.isVisible()) {
  1215                     size += s.getPrefSize(axis);
  1216                 }
  1217             }
  1218             return size;
  1219         }
  1220 
  1221         @Override
  1222         int getMaxSize(int axis) {
  1223             int size = 0;
  1224             boolean hasMax = false;
  1225             for(int i=0,n=springs.size() ; i<n ; i++) {
  1226                 Spring s = springs.get(i);
  1227                 if(includeInvisibleWidgets || s.isVisible()) {
  1228                     int max = s.getMaxSize(axis);
  1229                     if(max > 0) {
  1230                         size += max;
  1231                         hasMax = true;
  1232                     } else {
  1233                         size += s.getPrefSize(axis);
  1234                     }
  1235                 }
  1236             }
  1237             return hasMax ? size : 0;
  1238         }
  1239         
  1240         /**
  1241          * Add a default gap between all children except if the neighbour is already a Gap.
  1242          */
  1243         @Override
  1244         public void addDefaultGap() {
  1245             if(springs.size() > 1) {
  1246                 boolean wasGap = true;
  1247                 for(int i=0 ; i<springs.size() ; i++) {
  1248                     Spring s = springs.get(i);
  1249                     if(includeInvisibleWidgets || s.isVisible()) {
  1250                         boolean isGap = (s instanceof GapSpring) || (s instanceof NamedGapSpring);
  1251                         if(!isGap && !wasGap) {
  1252                             springs.add(i++, new GapSpring(DEFAULT_GAP, DEFAULT_GAP, DEFAULT_GAP, true));
  1253                         }
  1254                         wasGap = isGap;
  1255                     }
  1256                 }
  1257             }
  1258             super.addDefaultGap();
  1259         }
  1260 
  1261         @Override
  1262         void setSize(int axis, int pos, int size) {
  1263             int prefSize = getPrefSize(axis);
  1264             if(size == prefSize) {
  1265                 for(Spring s : springs) {
  1266                     if(includeInvisibleWidgets || s.isVisible()) {
  1267                         int spref = s.getPrefSize(axis);
  1268                         s.setSize(axis, pos, spref);
  1269                         pos += spref;
  1270                     }
  1271                 }
  1272             } else if(springs.size() == 1) {
  1273                 // no need to check visibility flag
  1274                 Spring s = springs.get(0);
  1275                 s.setSize(axis, pos, size);
  1276             } else if(springs.size() > 1) {
  1277                 setSizeNonPref(axis, pos, size, prefSize);
  1278             }
  1279         }
  1280 
  1281         private void setSizeNonPref(int axis, int pos, int size, int prefSize) {
  1282             int delta = size - prefSize;
  1283             boolean useMin = delta < 0;
  1284             if(useMin) {
  1285                 delta = -delta;
  1286             }
  1287 
  1288             SpringDelta[] deltas = new SpringDelta[springs.size()];
  1289             int resizeable = 0;
  1290             for(int i=0 ; i<springs.size() ; i++) {
  1291                 Spring s = springs.get(i);
  1292                 if(includeInvisibleWidgets || s.isVisible()) {
  1293                     int sdelta = useMin
  1294                             ? s.getPrefSize(axis) - s.getMinSize(axis)
  1295                             : s.getMaxSize(axis) - s.getPrefSize(axis);
  1296                     if(sdelta > 0)  {
  1297                         deltas[resizeable++] = new SpringDelta(i, sdelta);
  1298                     }
  1299                 }
  1300             }
  1301             if(resizeable > 0) {
  1302                 if(resizeable > 1) {
  1303                     Arrays.sort(deltas, 0, resizeable);
  1304                 }
  1305                 
  1306                 int sizes[] = new int[springs.size()];
  1307 
  1308                 int remaining = resizeable;
  1309                 for(int i=0 ; i<resizeable ; i++) {
  1310                     SpringDelta d = deltas[i];
  1311                     
  1312                     int sdelta = delta / remaining;
  1313                     int ddelta = Math.min(d.delta, sdelta);
  1314                     delta -= ddelta;
  1315                     remaining--;
  1316                     
  1317                     if(useMin) {
  1318                         ddelta = -ddelta;
  1319                     }
  1320                     sizes[d.idx] = ddelta;
  1321                 }
  1322 
  1323                 for(int i=0 ; i<springs.size() ; i++) {
  1324                     Spring s = springs.get(i);
  1325                     if(includeInvisibleWidgets || s.isVisible()) {
  1326                         int ssize = s.getPrefSize(axis) + sizes[i];
  1327                         s.setSize(axis, pos, ssize);
  1328                         pos += ssize;
  1329                     }
  1330                 }
  1331             } else {
  1332                 for(Spring s : springs) {
  1333                     if(includeInvisibleWidgets || s.isVisible()) {
  1334                         int ssize;
  1335                         if(useMin) {
  1336                             ssize = s.getMinSize(axis);
  1337                         } else {
  1338                             ssize = s.getMaxSize(axis);
  1339                             if(ssize == 0) {
  1340                                 ssize = s.getPrefSize(axis);
  1341                             }
  1342                         }
  1343                         s.setSize(axis, pos, ssize);
  1344                         pos += ssize;
  1345                     }
  1346                 }
  1347             }
  1348         }
  1349     }
  1350 
  1351     class ParallelGroup extends Group {
  1352         ParallelGroup() {
  1353         }
  1354 
  1355         @Override
  1356         int getMinSize(int axis) {
  1357             int size = 0;
  1358             for(int i=0,n=springs.size() ; i<n ; i++) {
  1359                 Spring s = springs.get(i);
  1360                 if(includeInvisibleWidgets || s.isVisible()) {
  1361                     size = Math.max(size, s.getMinSize(axis));
  1362                 }
  1363             }
  1364             return size;
  1365         }
  1366 
  1367         @Override
  1368         int getPrefSize(int axis) {
  1369             int size = 0;
  1370             for(int i=0,n=springs.size() ; i<n ; i++) {
  1371                 Spring s = springs.get(i);
  1372                 if(includeInvisibleWidgets || s.isVisible()) {
  1373                     size = Math.max(size, s.getPrefSize(axis));
  1374                 }
  1375             }
  1376             return size;
  1377         }
  1378 
  1379         @Override
  1380         int getMaxSize(int axis) {
  1381             int size = 0;
  1382             for(int i=0,n=springs.size() ; i<n ; i++) {
  1383                 Spring s = springs.get(i);
  1384                 if(includeInvisibleWidgets || s.isVisible()) {
  1385                     size = Math.max(size, s.getMaxSize(axis));
  1386                 }
  1387             }
  1388             return size;
  1389         }
  1390 
  1391         @Override
  1392         void setSize(int axis, int pos, int size) {
  1393             for(int i=0,n=springs.size() ; i<n ; i++) {
  1394                 Spring s = springs.get(i);
  1395                 if(includeInvisibleWidgets || s.isVisible()) {
  1396                     s.setSize(axis, pos, size);
  1397                 }
  1398             }
  1399         }
  1400 
  1401         @Override
  1402         public Group addGap() {
  1403             getLogger().log(Level.WARNING, "Useless call to addGap() on ParallelGroup", new Throwable());
  1404             return this;
  1405         }
  1406     }
  1407 }