changeset 40:b28d7adc786b

Made GUI more robust to mutable data changes and improved much of GridLayoutWidget's code. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 08 May 2008 16:05:51 +0100
parents 5132301e9ed7
children b3a6ca4516b4
files codeDoc/jobs.txt data/conf/gui.mtt mde/gui/exception.d mde/gui/widget/Ifaces.d mde/gui/widget/Window.d mde/gui/widget/layout.d
diffstat 6 files changed, 176 insertions(+), 143 deletions(-) [+]
line wrap: on
line diff
--- a/codeDoc/jobs.txt	Wed May 07 13:07:03 2008 +0100
+++ b/codeDoc/jobs.txt	Thu May 08 16:05:51 2008 +0100
@@ -3,6 +3,8 @@
 
 
 In progress:
+Make a widget with multiple sizable rows/cols.
+Implement row/col sizing.
 
 
 
--- a/data/conf/gui.mtt	Wed May 07 13:07:03 2008 +0100
+++ b/data/conf/gui.mtt	Thu May 08 16:05:51 2008 +0100
@@ -7,4 +7,4 @@
 {W2}
 <int|x=150>
 <int|y=200>
-<int[][int]|widgetData=[0:[0xB004,1,3,3,1,5],3:[0x4010,160,160],5:[0xB004,3,1,6,1,6],6:[0x4010,60,60],1:[0x3001]]>
+<int[][int]|widgetData=[0:[0xB004,5,5,2,1,2,1,2,1,1,1,1,1,2,1,2,1,2,1,1,1,1,1,2,1,2,1,2],1:[0x3001],2:[0x4010,75,75]]>
--- a/mde/gui/exception.d	Wed May 07 13:07:03 2008 +0100
+++ b/mde/gui/exception.d	Thu May 08 16:05:51 2008 +0100
@@ -47,14 +47,3 @@
         super (msg);
     }
 }
-
-/// Thrown when a Widget class's adjust() is called with invalid data.
-class MutableDataException : WindowLoadException
-{
-    this () {   // Default, by Widget class's this
-        super ("Bad widget mutable data");
-    }
-    this (char[] msg) { // From createWidget
-        super (msg);
-    }
-}
--- a/mde/gui/widget/Ifaces.d	Wed May 07 13:07:03 2008 +0100
+++ b/mde/gui/widget/Ifaces.d	Thu May 08 16:05:51 2008 +0100
@@ -75,17 +75,27 @@
 //BEGIN Load and save
     /** Called after creating widgets to adjust size & other mutable attributes from saved data.
      *
+     * As for setSize, setPosition should be called afterwards.
+     *
      * Each widget should call adjust on each of its sub-widgets in turn with data, each time
      * replacing data by the return value of the call. It should then take its own mutable data
      * from the beginning of the array and return the remainder of the array.
      *
-     * Throws: on error, throw a MutableDataException. */
+     * Adjust should handle errors gracefully by reverting to default values and not throwing.
+     * This is because the creation data and the user's mutable data may be stored separately and
+     * become out-of-sync during an update. */
     int[] adjust (int[] data);
     
-    /** Output data suitible for recreating the widget (data to be passed to this()). */
+    /** Output data suitible for recreating the widget (data to be passed to this()).
+     *
+     * Creation data is data only changed when the gui is edited. */
     int[] getCreationData ();
     
     /** Output data containing the widget's current adjustments (data to be passed to adjust()).
+     *
+     * Mutable data is data which can be changed during normal gui use, such as the size of
+     * resizible widgets or current tab of a tab widget.
+     *
      * Should be a concatenation of each sub-widget's mutable data and the widget's own. */
     int[] getMutableData ();
 //END Load and save
--- a/mde/gui/widget/Window.d	Wed May 07 13:07:03 2008 +0100
+++ b/mde/gui/widget/Window.d	Thu May 08 16:05:51 2008 +0100
@@ -58,8 +58,8 @@
         assert (gui !is null, "Window.finalise ("~name~"): gui is null");
     } body {
         // Check data was loaded:
-        if (widgetData is null || mutableData is null)
-            throw new WindowLoadException ("No widget/mutable data");
+        if (widgetData is null)
+            throw new WindowLoadException ("No widget creation data");
         
         gui_ = gui;
         rend = gui.renderer;
@@ -72,6 +72,7 @@
         widget = makeWidget (0);    // primary widget always has ID 0.
         widgetData = null;  // data is no longer needed: allow GC to collect (cannot safely delete)
         
+        // Note: this should return an empty array, but nothing much should happen if it's not empty:
         widget.adjust (mutableData);    // adjust/set size, etc.
         mutableData = null;             // no longer needed
         
@@ -111,7 +112,7 @@
     } body {
         /+ NOTE: currently editing is impossible...
         if (edited) {   // only save the widget creation data if it's been adjusted:
-            addSaveData (widget);       // generate widget save data
+            addCreationData (widget);   // generate widget save data
             dlg (INTAA, WDGD, parseFrom!(int[][int]) (widgetData));
         }+/
         // Save mutable data:
--- a/mde/gui/widget/layout.d	Wed May 07 13:07:03 2008 +0100
+++ b/mde/gui/widget/layout.d	Thu May 08 16:05:51 2008 +0100
@@ -21,7 +21,12 @@
 
 /** Encapsulates a grid of Widgets.
 *
-* Since a grid with either dimension zero is not useful, there must be at least one sub-widget. */
+* Currently there is no support for changing number of cells, sub-widgets or sub-widget properties
+* (namely isW/HSizable and minimal size) after this() has run.
+*
+* Since a grid with either dimension zero is not useful, there must be at least one sub-widget.
+*
+* The grid has no border but has spacing between widgets. */
 class GridLayoutWidget : Widget
 {
     this (IWindow wind, int[] data) {
@@ -43,26 +48,43 @@
         foreach (i, inout subWidget; subWidgets) {
             subWidget = window.makeWidget (data[i+3]);
         }
+        
+        // Calculate cached construction data
+        genCachedConstructionData;
     }
     
     int[] adjust (int[] data) {
         // Give all sub-widgets their data:
         foreach (widget; subWidgets)
             data = widget.adjust (data);
-        if (data.length < rows + cols) throw new MutableDataException;
         
         /* We basically short-cut setSize by loading previous col/row sizes and doing the final
-         * calculations. There isn't checks that the data is valid/up-to-date... worst case is too
-         * small overlapping widgets or huge ones?
+         * calculations.
          * Note: if setSize gets called afterwards, it should have same dimensions and so not do
          * anything. */
-        colW = data[0..cols];
-        rowH = data[cols..rows+cols];
-        setColRowSizes;
+        
+        int lenUsed = 0;
+        if (data.length < rows + cols) {    // data error; use defaults
+            colW = colWMin.dup;
+            rowH = rowHMin.dup;
+        } else {                            // sufficient data
+            lenUsed = rows+cols;
+            colW = data[0..cols];
+            rowH = data[cols..lenUsed];
+            
+            // Check row sizes are valid:
+            //NOTE: this could be made optional
+            foreach (i, inout w; colW)
+                if (w < colWMin[i]) w = colWMin[i];
+            foreach (i, inout h; rowH)
+                if (h < rowHMin[i]) h = rowHMin[i];
+        }
+        
+        genCachedMutableData;
         w = colW[$-1] + colX[$-1];
         h = rowY[$-1] + rowH[$-1];
         
-        return data[rows+cols..$];
+        return data[lenUsed..$];
     }
     
     int[] getCreationData () {
@@ -86,113 +108,28 @@
     }
     
     bool isWSizable () {
-        if (colSizable == -2) {     // check whether any columns are resizable
-            for1:
-            for (uint i = 0; i < cols; ++i) {                       // for each column
-                for (uint j = 0; j < subWidgets.length; j += cols)  // for each row
-                    if (!subWidgets[i+j].isWSizable)        // column not resizable
-                        continue for1;                      // continue the outer for loop
-                
-                // column is resizable if we get to here
-                colSizable = i;
-                goto break1;        // use goto in lieu of for...else
-            }
-            
-            // if we get here, no resizable column was found
-            colSizable = -1;
-            
-            break1:;
-        }
-        
-        if (colSizable >= 0) return true;
-        else return false;
+        return (sizableCols.length != 0);
     }
     
     bool isHSizable () {
-        if (rowSizable == -2) {     // check whether any columns are resizable
-            for2:
-            for (uint i = 0; i < subWidgets.length; i += cols) {    // for each row
-                for (uint j = 0; j < cols; ++j)                     // for each column
-                    if (!subWidgets[i+j].isHSizable)
-                        continue for2;
-                
-                rowSizable = i / cols;  // the current row
-                goto break2;
-            }
-            
-            rowSizable = -1;
-            
-            break2:;
-        }
-        
-        if (rowSizable >= 0) return true;
-        else return false;
+        return (sizableRows.length != 0);
     }
     
     /* Calculates the minimal size from all rows and columns of widgets. */
     void getMinimalSize (out int mw, out int mh) {
-        // If rowHMin & colWMin are null, calculate them. They are set null whenever the contents
-        // or the contents' minimal size change, as well as when this widget is created.
-        if (rowHMin is null)
-            genMinRowColSizes;
-        
-        // Calculate the size, starting with the spacing:
-        mh = window.renderer.layoutSpacing;     // use temporarily
-        mw = mh * (cols - 1);
-        mh *= (rows - 1);
-        
-        foreach (x; colWMin)            // add the column/row's dimensions
-            mw += x;
-        foreach (x; rowHMin)
-            mh += x;
+        mw = this.mw;
+        mh = this.mh;
     }
     
     void setSize (int nw, int nh) {
-        /* For each of width and height, there are several cases:
-         *  [new value is] more than old value
-         *  ->  enlarge any row/column
-         *  same as old value
-         *  ->  do nothing
-         *  more than min but less than current value
-         *  ->  find an enlarged row/col and reduce size
-         *  ->  repeat if necessary
-         *  minimal value or less
-         *  -> clamp to min and use min col/row sizes
-         */
-        // FIXME: implement!
-        
-        // Step 1: calculate the minimal row/column sizes.
-        alias w mw;             // no need for extra vars, just use these
-        alias h mh;
-        getMinimalSize (mw, mh);
-        colW = colWMin.dup;     // start with these dimensions, and increase if necessary
-        rowH = rowHMin.dup;     // duplicate, because we may want to resize without recalculating *Min
+        // Step 1: calculate the row/column sizes.
+        setSizeImpl!(true)  (nw);
+        setSizeImpl!(false) (nh);
         
-        // Step 2: clamp nw/nh or expand a column/row to achieve the required size
-        if (nw <= mw) nw = mw;  // clamp to minimal size
-        else {
-            if (isWSizable)     // calculates colSizable; true if any is resizable
-                colW[colSizable] += nw - mw; // new width
-            else                // no resizable column; so force the last one
-                colW[$-1] += nw - mw;
-        }
+        // Step 2: calculate the row/column offsets (positions) and set the sub-widgets sizes.
+        genCachedMutableData;
         
-        if (nh <= mh) nh = mh;
-        else {
-            if (isHSizable)
-                rowH[rowSizable] += nh - mh;
-            else
-                rowH[$-1] += nh - mh;
-        }
-        
-        w = nw;
-        h = nh;
-        
-        // Step 3: set each sub-widget's size.
-        // Step 4: calculate the column and row positions
-        setColRowSizes;
-        
-        // Step 5: position needs resetting
+        // Step 3: position needs to be set
         // Currently this happens by specifying that setPosition should be run after setSize.
     }
     
@@ -243,26 +180,62 @@
     }
     
 private:
-    void genMinRowColSizes () {
-        // Find the sizes of all subWidgets
-        int[] widgetW = new int[subWidgets.length]; // dimensions
-        int[] widgetH = new int[subWidgets.length];
-        foreach (i,widget; subWidgets)
-            widget.getMinimalSize (widgetW[i],widgetH[i]);
+    /* Calculations which need to be run whenever a new sub-widget structure is set
+     * (i.e. to produce cached data calculated from construction data). */
+    void genCachedConstructionData () {
+        // Calculate the minimal column and row sizes:
+        colWMin = new int[cols];    // set length, making sure the arrays are initialised to zero
+        rowHMin = new int[rows];
+        int ww, wh;     // sub-widget minimal sizes
+        foreach (i,widget; subWidgets) {
+            widget.getMinimalSize (ww, wh);
             
-        // Find the minimal row heights and column widths (non cumulative)
-        colWMin = new int[cols];    // set length
-        rowHMin = new int[rows];
-        for (uint i = 0; i < subWidgets.length; ++i) {
-            uint n;
-            n = i % cols;           // column
-            if (colWMin[n] < widgetW[i]) colWMin[n] = widgetW[i];
+            // Increase dimensions if current minimal size is larger:
+            uint n = i % cols;      // column
+            if (colWMin[n] < ww) colWMin[n] = ww;
             n = i / cols;           // row
-            if (rowHMin[n] < widgetH[i]) rowHMin[n] = widgetH[i];
+            if (rowHMin[n] < wh) rowHMin[n] = wh;
+        }
+        
+        
+        // Calculate the overall minimal size, starting with the spacing:
+        mh = window.renderer.layoutSpacing;     // use temporarily
+        mw = mh * (cols - 1);
+        mh *= (rows - 1);
+        
+        foreach (x; colWMin)            // add the column/row's dimensions
+            mw += x;
+        foreach (x; rowHMin)
+            mh += x;
+        
+        
+        // Find which cols/rows are resizable:
+        sizableCols = null;     // clear these because we append to them
+        sizableRows = null;
+        
+        forCols:
+        for (uint i = 0; i < cols; ++i) {                       // for each column
+            for (uint j = 0; j < subWidgets.length; j += cols)  // for each row
+                if (!subWidgets[i+j].isWSizable)        // column not resizable
+                    continue forCols;                   // continue the outer for loop
+                
+            // column is resizable if we get to here
+            sizableCols ~= i;
+        }
+        
+        forRows:
+        for (uint i = 0; i < subWidgets.length; i += cols) {    // for each row
+            for (uint j = 0; j < cols; ++j)                     // for each column
+                if (!subWidgets[i+j].isHSizable)
+                    continue forRows;
+                
+            sizableRows ~= i / cols;    // the current row
         }
     }
     
-    void setColRowSizes () {
+    /* Calculations which need to be run whenever resizing occurs (or deeper alterations)
+     * (i.e. to produce cached data calculated from construction and mutable data). */
+    void genCachedMutableData () {
         // Calculate column and row locations:
         colX.length = cols;
         rowY.length = rows;
@@ -285,21 +258,79 @@
             widget.setSize (colW[i % cols], rowH[i / cols]);
     }
     
-protected:
-    int cols, rows;     // number of cells in grid
+    /* setSize generalised for either dimension; w/h is renamed d, col/row renamed cell. */
+    void setSizeImpl(bool W) (int nd) {
+        static if (W) {
+            alias w         d;
+            alias mw        md;
+            alias colW      cellD;
+            alias colWMin   cellDMin;
+            alias sizableCols sizableCells;
+        } else {
+            alias h         d;
+            alias mh        md;
+            alias rowH      cellD;
+            alias rowHMin   cellDMin;
+            alias sizableRows sizableCells;
+        }
+        // Could occur if adjust isn't called first, but this would be a code error:
+        assert (cellD !is null, "setSizeImpl: cellD is null");
+        
+        /* For each of width and height, there are several cases:
+        *  [new value is] more than old value
+        *  ->  enlarge any row/column
+        *  same as old value
+        *  ->  do nothing
+        *  more than min but less than current value
+        *  ->  find an enlarged row/col and reduce size
+        *  ->  repeat if necessary
+        *  minimal value or less
+        *  -> clamp to min and use min col/row sizes
+        */
+        if (nd > d) {       // expand (d < nd)
+            if (sizableCells.length) {               // check there is a resizable col/row
+                cellD[sizableCells[$-1]] += nd - d;  // new size
+                d = nd;
+            }
+        } else if (nd < d) {
+            if (nd > md) {  // shrink (md < nd < d)
+                int toReduce = nd - d;  // negative
+                foreach_reverse (cell; sizableCells) {
+                    cellD[cell] += toReduce;
+                    toReduce = cellD[cell] - cellDMin[cell];
+                    if (toReduce < 0)   // reduced too far
+                        cellD[cell] = cellDMin[cell];
+                    else                // ok; cellD >= cellDMin
+                        break;
+                }
+                d = nd;
+            } else {        // clamp  (nd <= md)
+                cellD = cellDMin.dup;   // duplicate so future edits don't affect cellDMin
+                d = md;
+            }
+        }                   // third possibility: nd = d
+    }
     
-    int colSizable = -2;// 0..cols-1 means this column is resizable
-    int rowSizable = -2;// -2 means not calculated yet, -1 means not resizable
+protected:
+    // Construction data (saved):
+    int cols, rows;     // number of cells in grid
+    IWidget[] subWidgets;   // all widgets in the grid (by row):
+    /* SubWidget order:    [ 0 1 ]
+    *                      [ 2 3 ] */
     
+    // Cached data calculated from construction data:
     int[] colWMin;      // minimal column width
     int[] rowHMin;      // minimal row height
+    int mw, mh;         // minimal dimensions
+    
+    int[] sizableCols;   // all resizable columns / rows, empty if not resizable
+    int[] sizableRows;   // resizing is done from last entry
+    
+    // Mutable data (saved):
     int[] colW;         // column width (widest widget)
     int[] rowH;         // row height (highest widget in the row)
     
+    // Cached data calculated from construction and mutable data:
     int[] colX;         // cumulative colW[i-1] + padding (add x to get column's left x-coord)
     int[] rowY;         // cumulative rowH[i-1] + padding
-    
-    IWidget[] subWidgets;   // all widgets in the grid (by row):
-    /* SubWidget order:    [ 0 1 ]
-    *                      [ 2 3 ] */
 }