]> git.mxchange.org Git - flightgear.git/commitdiff
GUI layout management and a few visual/eye-candy modifications. See
authorandy <andy>
Wed, 12 May 2004 15:36:07 +0000 (15:36 +0000)
committerandy <andy>
Wed, 12 May 2004 15:36:07 +0000 (15:36 +0000)
DOCS/README.layout in the base package for details, along with the
modified dialog files.

src/GUI/Makefile.am
src/GUI/dialog.cxx
src/GUI/gui.cxx
src/GUI/layout-props.cxx [new file with mode: 0644]
src/GUI/layout-test.cxx [new file with mode: 0644]
src/GUI/layout.cxx [new file with mode: 0644]
src/GUI/layout.hxx [new file with mode: 0644]

index 267f5414fc89850f60fe905bda127dde00e4b3c7..e6979c0b7830a731bb779e818408da0b02959986 100644 (file)
@@ -1,4 +1,5 @@
 noinst_LIBRARIES = libGUI.a
+noinst_PROGRAMS = layout-test
 
 libGUI_a_SOURCES = \
         new_gui.cxx new_gui.hxx \
@@ -12,6 +13,13 @@ libGUI_a_SOURCES = \
        sgVec3Slider.cxx sgVec3Slider.hxx \
        trackball.c trackball.h \
        puList.cxx puList.hxx \
-       AirportList.cxx AirportList.hxx
+       AirportList.cxx AirportList.hxx \
+        layout.cxx layout-props.cxx layout.hxx
 
 INCLUDES = -I$(top_srcdir) -I$(top_srcdir)/src
+
+layout_test_SOURCES = layout-test.cxx
+
+layout_test_LDADD = libGUI.a \
+                    -lsgprops -lsgdebug -lsgstructure -lsgmisc -lsgxml \
+                    -lplibpw -lplibpu -lplibfnt -lplibul $(opengl_LIBS)
\ No newline at end of file
index 6d3c7c7cb2c5c7354faa75f456bb8d67bf1fa0ec..ae1f962701bbe89d4ae19ae3b0ded36ac9e7a1fe 100644 (file)
@@ -7,6 +7,7 @@
 
 #include "puList.hxx"
 #include "AirportList.hxx"
+#include "layout.hxx"
 
 int fgPopup::checkHit(int button, int updown, int x, int y)
 {
@@ -235,9 +236,20 @@ FGDialog::display (SGPropertyNode * props)
         return;
     }
 
-    _object = makeObject(props,
-                      globals->get_props()->getIntValue("/sim/startup/xsize"),
-                      globals->get_props()->getIntValue("/sim/startup/ysize"));
+    int screenw = globals->get_props()->getIntValue("/sim/startup/xsize");
+    int screenh = globals->get_props()->getIntValue("/sim/startup/ysize");
+
+    LayoutWidget wid(props);
+    int pw=0, ph=0;
+    if(!props->hasValue("width") || !props->hasValue("height"))
+        wid.calcPrefSize(&pw, &ph);
+    pw = props->getIntValue("width", pw);
+    ph = props->getIntValue("height", ph);
+    int px = props->getIntValue("x", (screenw - pw) / 2);
+    int py = props->getIntValue("y", (screenh - ph) / 2);
+    wid.layout(px, py, pw, ph);
+
+    _object = makeObject(props, screenw, screenh);
 
     if (_object != 0) {
         _object->reveal();
@@ -251,9 +263,9 @@ FGDialog::display (SGPropertyNode * props)
 puObject *
 FGDialog::makeObject (SGPropertyNode * props, int parentWidth, int parentHeight)
 {
+    bool presetSize = props->hasValue("width") && props->hasValue("height");
     int width = props->getIntValue("width", parentWidth);
     int height = props->getIntValue("height", parentHeight);
-
     int x = props->getIntValue("x", (parentWidth - width) / 2);
     int y = props->getIntValue("y", (parentHeight - height) / 2);
 
@@ -288,10 +300,21 @@ FGDialog::makeObject (SGPropertyNode * props, int parentWidth, int parentHeight)
     } else if (type == "text") {
         puText * text = new puText(x, y);
         setupObject(text, props);
+        // Layed-out objects need their size set, and non-layout ones
+        // get a different placement.
+        if(presetSize) text->setSize(width, height);
+        else text->setLabelPlace(PUPLACE_LABEL_DEFAULT);
         return text;
     } else if (type == "checkbox") {
+        puButton * b;
+        b = new puButton(x, y, x + width, y + height, PUBUTTON_XCHECK);
+        b->setColourScheme(.8, .7, .7); // matches "PUI input pink"
+        setupObject(b, props);
+        return b;
+    } else if (type == "radio") {
         puButton * b;
         b = new puButton(x, y, x + width, y + height, PUBUTTON_CIRCLE);
+        b->setColourScheme(.8, .7, .7); // matches "PUI input pink"
         setupObject(b, props);
         return b;
     } else if (type == "button") {
@@ -301,6 +324,8 @@ FGDialog::makeObject (SGPropertyNode * props, int parentWidth, int parentHeight)
             b = new puOneShot(x, y, legend);
         else
             b = new puButton(x, y, legend);
+        if(presetSize)
+            b->setSize(width, height);
         setupObject(b, props);
         return b;
     } else if (type == "combo") {
@@ -354,6 +379,8 @@ FGDialog::makeObject (SGPropertyNode * props, int parentWidth, int parentHeight)
 void
 FGDialog::setupObject (puObject * object, SGPropertyNode * props)
 {
+    object->setLabelPlace(PUPLACE_CENTERED_RIGHT);
+
     if (props->hasValue("legend"))
         object->setLegend(props->getStringValue("legend"));
 
@@ -391,8 +418,10 @@ FGDialog::setupGroup (puGroup * group, SGPropertyNode * props,
 {
     setupObject(group, props);
 
-    if (makeFrame)
-        new puFrame(0, 0, width, height);
+    if (makeFrame) {
+        puFrame* f = new puFrame(0, 0, width, height);
+        f->setColorScheme(0.8, 0.8, 0.9, 0.85);
+    }
 
     int nChildren = props->nChildren();
     for (int i = 0; i < nChildren; i++)
index 48deee30cd54fa5ac836d0b3b20391e99f7830fb..571588e9c35adcad105547b9efe5d309a25c44ef 100644 (file)
@@ -49,7 +49,7 @@
 #include "gui.h"
 #include "gui_local.hxx"
 #include "preset_dlg.hxx"
-
+#include "layout.hxx"
 
 extern void initDialog (void);
 extern void mkDialogInit (void);
@@ -71,8 +71,8 @@ void guiInit()
 
     // Initialize PUI
     puInit();
-    puSetDefaultStyle         ( PUSTYLE_SMALL_BEVELLED ); //PUSTYLE_DEFAULT
-    puSetDefaultColourScheme  (0.8, 0.8, 0.9, 0.8);
+    puSetDefaultStyle         ( PUSTYLE_SMALL_SHADED ); //PUSTYLE_DEFAULT
+    puSetDefaultColourScheme  (0.8, 0.8, 0.9, 1);
 
     initDialog();
 
@@ -94,6 +94,8 @@ void guiInit()
     puFont GuiFont ( guiFntHandle, 15 ) ;
     puSetDefaultFonts( GuiFont, GuiFont ) ;
     guiFnt = puGetDefaultLabelFont();
+
+    LayoutWidget::setDefaultFont(&GuiFont, 15);
   
     if (!fgHasNode("/sim/startup/mouse-pointer")) {
         // no preference specified for mouse pointer, attempt to autodetect...
diff --git a/src/GUI/layout-props.cxx b/src/GUI/layout-props.cxx
new file mode 100644 (file)
index 0000000..6857404
--- /dev/null
@@ -0,0 +1,88 @@
+#include <plib/pu.h>
+#include <simgear/props/props.hxx>
+
+#include "layout.hxx"
+
+// This file contains the code implementing the LayoutWidget class in
+// terms of a PropertyNode (plus a tiny bit of PUI glue).  See
+// layout.cxx for the actual layout engine.
+
+puFont LayoutWidget::FONT;
+
+void LayoutWidget::setDefaultFont(puFont* font, int pixels)
+{
+    UNIT = (int)(pixels * (1/3.) + 0.999);
+    FONT = *font;
+}
+
+int LayoutWidget::stringLength(const char* s)
+{
+    return (int)(FONT.getFloatStringWidth(s) + 0.999);
+}
+
+const char* LayoutWidget::type()
+{
+    const char* t = _prop->getName();
+    return (*t == 0) ? "dialog" : t;
+}
+
+bool LayoutWidget::hasParent()
+{
+    return _prop->getParent() ? true : false;
+}
+
+LayoutWidget LayoutWidget::parent()
+{
+    return LayoutWidget(_prop->getParent());
+}
+
+int LayoutWidget::nChildren()
+{
+    // Hack: assume that any non-leaf nodes are widgets...
+    int n = 0;
+    for(int i=0; i<_prop->nChildren(); i++)
+        if(_prop->getChild(i)->nChildren() != 0)
+            n++;
+    return n;
+}
+
+LayoutWidget LayoutWidget::getChild(int idx)
+{
+    // Same hack.  Note that access is linear time in the number of
+    // children...
+    int n = 0;
+    for(int i=0; i<_prop->nChildren(); i++) {
+        SGPropertyNode* p = _prop->getChild(i);
+        if(p->nChildren() != 0) {
+            if(idx == n) return LayoutWidget(p);
+            n++;
+        }
+    }
+    return LayoutWidget(0);
+}
+
+bool LayoutWidget::hasField(const char* f)
+{
+    return _prop->hasChild(f);
+}
+
+int LayoutWidget::getNum(const char* f)
+{
+    return _prop->getIntValue(f);
+}
+
+bool LayoutWidget::getBool(const char* f)
+{
+    return _prop->getBoolValue(f);
+}
+
+const char* LayoutWidget::getStr(const char* f)
+{
+    return _prop->getStringValue(f);
+}
+
+void LayoutWidget::setNum(const char* f, int num)
+{
+    _prop->setIntValue(f, num);
+}
+
diff --git a/src/GUI/layout-test.cxx b/src/GUI/layout-test.cxx
new file mode 100644 (file)
index 0000000..feb8f20
--- /dev/null
@@ -0,0 +1,56 @@
+#include <iostream>
+
+#include <GL/gl.h>
+#include <plib/pw.h>
+#include <plib/pu.h>
+#include <simgear/props/props.hxx>
+#include <simgear/props/props_io.hxx>
+
+#include "layout.hxx"
+
+// Takes a property file on the command line, lays it out, and writes
+// the resulting tree back to stdout.  Requires that the
+// "Helvetica.txf" font file from the base package be in the current
+// directory.
+
+// g++ -Wall -g -o layout layout.cxx layout-props.cxx layout-test.cxx
+// -I/fg/include -L/fg/lib -I.. -lsgprops -lsgdebug -lsgstructure
+// -lsgmisc -lsgxml -lplibpw -lplibpu -lplibfnt -lplibul -lGL
+
+// We can't load a plib fntTexFont without a GL context, so we use the
+// PW library to initialize things.  The callbacks are required, but
+// just stubs.
+void exitCB(){ pwCleanup(); exit(0); }
+void resizeCB(int w, int h){ }
+void mouseMotionCB(int x, int y){ puMouse(x, y); }
+void mouseButtonCB(int button, int updn, int x, int y){ puMouse(button, updn, x, y); }
+void keyboardCB(int key, int updn, int x, int y){ puKeyboard(key, updn, x, y); }
+
+const char* FONT_FILE = "Helvetica.txf";
+
+int main(int argc, char** argv)
+{
+    FILE* tmp;
+    if(!(tmp = fopen(FONT_FILE, "r"))) {
+        fprintf(stderr, "Could not open %s for reading.\n", FONT_FILE);
+        exit(1);
+    }
+    fclose(tmp);
+
+    pwInit(0, 0, 600, 400, 0, "Layout Test", true, 0);
+    pwSetCallbacks(keyboardCB, mouseButtonCB, mouseMotionCB,
+                   resizeCB, exitCB);
+
+    fntTexFont helv;
+    helv.load(FONT_FILE);
+    puFont puhelv(&helv);
+
+    LayoutWidget::setDefaultFont(&puhelv, 15);
+    SGPropertyNode props;
+    readProperties(argv[1], &props);
+    LayoutWidget w(&props);
+    int pw=0, ph=0;
+    w.calcPrefSize(&pw, &ph);
+    w.layout(0, 0, pw, ph);
+    writeProperties(cout, &props, true);
+}
diff --git a/src/GUI/layout.cxx b/src/GUI/layout.cxx
new file mode 100644 (file)
index 0000000..40f16a3
--- /dev/null
@@ -0,0 +1,371 @@
+#include "layout.hxx"
+
+// This file contains the actual layout engine.  It has no dependence
+// on outside libraries; see layout-props.cxx for the glue code.
+
+// Note, property names with leading double-underscores (__bx, etc...)
+// are debugging information, and can be safely removed.
+
+const int DEFAULT_PADDING = 2;
+
+int LayoutWidget::UNIT = 5;
+
+bool LayoutWidget::eq(const char* a, const char* b)
+{
+    while(*a && (*a == *b)) { a++; b++; }
+    return *a == *b;
+}
+
+// Normal widgets get a padding of 4 pixels.  Layout groups shouldn't
+// have visible padding by default, except for top-level dialog groups
+// which need to leave two pixels for the puFrame's border.  This
+// value can, of course, be overriden by the parent groups
+// <default-padding> property, or per widget with <padding>.
+int LayoutWidget::padding()
+{
+    int pad = isType("group") ? 0 : 4;
+    if(isType("dialog")) pad = 2;
+    if(hasParent() && parent().hasField("default-padding"))
+        pad = parent().getNum("default-padding");
+    if(hasField("padding"))
+        pad = getNum("padding");
+    return pad;
+}
+
+void LayoutWidget::calcPrefSize(int* w, int* h)
+{
+    *w = *h = 0; // Ask for nothing by default
+
+    int legw = stringLength(getStr("legend"));
+    int labw = stringLength(getStr("label"));
+
+    if(isType("dialog") || isType("group")) {
+        if(!hasField("layout")) {
+            // Legacy support for groups without layout managers.
+            if(hasField("width")) *w = getNum("width");
+            if(hasField("height")) *h = getNum("height");
+        } else {
+            const char* layout = getStr("layout");
+            if     (eq(layout, "hbox" )) doHVBox(false, false, w, h);
+            else if(eq(layout, "vbox" )) doHVBox(false, true, w, h);
+            else if(eq(layout, "table")) doTable(false, w, h);
+        }
+    } else if (isType("text")) {
+        *w = labw;
+        *h = 4*UNIT; // FIXME: multi line height?
+    } else if (isType("button")) {
+        *w = legw + 6*UNIT + (labw ? labw + UNIT : 0);
+        *h = 6*UNIT;
+    } else if (isType("checkbox") || isType("radio")) {
+        *w = 3*UNIT + (labw ? (3*UNIT + labw) : 0);
+        *h = 3*UNIT;
+    } else if (isType("input") || isType("combo") || isType("select")) {
+        *w = 17*UNIT;
+        *h = 6*UNIT;
+    } else if (isType("slider")) {
+        if(getBool("vertical")) *w = 3*UNIT;
+        else                    *h = 3*UNIT;
+    } else if (isType("list") || isType("airport-list") || isType("dial")) {
+        *w = *h = 12*UNIT;
+    }
+
+    // Throw it all out if the user specified a fixed preference
+    if(hasField("pref-width"))  *w = getNum("pref-width");
+    if(hasField("pref-height")) *h = getNum("pref-height");
+
+    // And finally correct for cell padding
+    int pad = 2*padding();
+    *w += pad;
+    *h += pad;
+
+    // Store what we calculated
+    setNum("__pw", *w);
+    setNum("__ph", *h);
+}
+
+// Set up geometry such that the widget lives "inside" the specified 
+void LayoutWidget::layout(int x, int y, int w, int h)
+{
+    setNum("__bx", x);
+    setNum("__by", y);
+    setNum("__bw", w);
+    setNum("__bh", h);
+
+    // Correct for padding.
+    int pad = padding();
+    x += pad;
+    y += pad;
+    w -= 2*pad;
+    h -= 2*pad;
+
+    int prefw = 0, prefh = 0;
+    calcPrefSize(&prefw, &prefh);
+    prefw -= 2*pad;
+    prefh -= 2*pad;
+
+    // "Parent Set" values override widget preferences
+    if(hasField("_psw")) prefw = getNum("_psw");
+    if(hasField("_psh")) prefh = getNum("_psh");
+
+    bool isGroup = isType("dialog") || isType("group");
+
+    // Correct our box for alignment.  The values above correspond to
+    // a "fill" alignment.
+    const char* halign = isGroup ? "fill" : "center";
+    if(hasField("halign")) halign = getStr("halign");
+    if(eq(halign, "left")) {
+        w = prefw;
+    } else if(eq(halign, "right")) {
+        x += w - prefw;
+        w = prefw;
+    } else if(eq(halign, "center")) {
+        x += (w - prefw)/2;
+        w = prefw;
+    }
+    const char* valign = isGroup ? "fill" : "center";
+    if(hasField("valign")) valign = getStr("valign");
+    if(eq(valign, "bottom")) {
+        h = prefh;
+    } else if(eq(valign, "top")) {
+        y += h - prefh;
+        h = prefh;
+    } else if(eq(valign, "center")) {
+        y += (h - prefh)/2;
+        h = prefh;
+    }
+
+    // PUI widgets interpret their size differently depending on
+    // type, so diddle the values as needed to fit the widget into
+    // the x/y/w/h box we have calculated.
+    if (isType("text")) {
+        // puText labels are layed out to the right of the box, so
+        // zero the width.
+        w = 0;
+    } else if (isType("input") || isType("combo") || isType("select")) {
+        // Fix the height to a constant
+        y += (h - 6*UNIT) / 2;
+        h = 6*UNIT;
+    } else if (isType("checkbox") || isType("radio")) {
+        // The PUI dimensions are of the check area only.  Center it
+        // on the left side of our box.
+        y += (h - 3*UNIT) / 2;
+        w = h = 3*UNIT;
+    } else if (isType("slider")) {
+        // Fix the thickness to a constant
+        if(getBool("vertical")) { x += (w-3*UNIT)/2; w = 3*UNIT; }
+        else                    { y += (h-3*UNIT)/2; h = 3*UNIT; }
+    }
+
+    // Set out output geometry
+    setNum("x", x);
+    setNum("y", y);
+    setNum("width", w);
+    setNum("height", h);
+
+    // Finally, if we are ourselves a layout object, do the actual layout.
+    if(isGroup && hasField("layout")) {
+        const char* layout = getStr("layout");
+        if     (eq(layout, "hbox" )) doHVBox(true, false);
+        else if(eq(layout, "vbox" )) doHVBox(true, true);
+        else if(eq(layout, "table")) doTable(true);
+    }
+}
+
+// Convention: the "A" cooridinate refers to the major axis of the
+// container (width, for an hbox), "B" is minor.
+void LayoutWidget::doHVBox(bool doLayout, bool vertical, int* w, int* h)
+{
+    int nc = nChildren();
+    int* prefA = doLayout ? new int[nc] : 0;
+    int i, totalA = 0, maxB = 0, nStretch = 0;
+    int nEq = 0, eqA = 0, eqB = 0, eqTotalA = 0;
+    for(i=0; i<nc; i++) {
+        LayoutWidget child = getChild(i);
+        int a, b;
+        child.calcPrefSize(vertical ? &b : &a, vertical ? &a : &b);
+        if(doLayout) prefA[i] = a;
+        totalA += a;
+        if(b > maxB) maxB = b;
+        if(child.getBool("stretch")) {
+            nStretch++;
+        } else if(child.getBool("equal")) {
+            int pad = child.padding();
+            nEq++;
+            eqTotalA += a - 2*pad;
+            if(a-2*pad > eqA) eqA = a - 2*pad;
+            if(b-2*pad > eqB) eqB = b - 2*pad;
+        }
+    }
+    if(nStretch == 0) nStretch = nc;
+    totalA += nEq * eqA - eqTotalA; 
+
+    if(!doLayout) {
+        if(vertical) { *w = maxB;   *h = totalA; }
+        else         { *w = totalA; *h = maxB; }
+        return;
+    }
+
+    int currA = 0;
+    int availA = getNum(vertical ? "height" : "width");
+    int availB = getNum(vertical ? "width" : "height");
+    bool stretchAll = nStretch == nc ? true : false;
+    int stretch = availA - totalA;
+    if(stretch < 0) stretch = 0;
+    for(i=0; i<nc; i++) {
+        // Swap the child order for vertical boxes, so we lay out
+        // from top to bottom instead of along the cartesian Y axis.
+        int idx = vertical ? (nc-i-1) : i;
+        LayoutWidget child = getChild(idx);
+        if(child.getBool("equal")) {
+            int pad = child.padding();
+            prefA[idx] = eqA + 2*pad;
+            // Use "parent set" values to communicate the setting to
+            // the child.
+            child.setNum(vertical ? "_psh" : "_psw", eqA);
+            child.setNum(vertical ? "_psw" : "_psh", eqB);
+        }
+        if(stretchAll || child.getBool("stretch")) {
+            int chunk = stretch / nStretch;
+            stretch -= chunk;
+            nStretch--;
+            prefA[idx] += chunk;
+            child.setNum("__stretch", chunk);
+        }
+        if(vertical) child.layout(    0, currA,   availB, prefA[idx]);
+        else         child.layout(currA,     0, prefA[idx],   availB);
+        currA += prefA[idx];
+    }
+
+    delete[] prefA;
+}
+
+struct TabCell {
+    TabCell() {}
+    LayoutWidget child;
+    int w, h, row, col, rspan, cspan;
+};
+
+void LayoutWidget::doTable(bool doLayout, int* w, int* h)
+{
+    int i, j, nc = nChildren();
+    TabCell* children = new TabCell[nc];
+    
+    // Pass 1: initialize bookeeping structures
+    int rows = 0, cols = 0;
+    for(i=0; i<nc; i++) {
+        TabCell* cell = &children[i];
+        cell->child = getChild(i);
+        cell->child.calcPrefSize(&cell->w, &cell->h);
+        cell->row = cell->child.getNum("row");
+        cell->col = cell->child.getNum("col");
+        cell->rspan = cell->child.hasField("rowspan") ? cell->child.getNum("rowspan") : 1;
+        cell->cspan = cell->child.hasField("colspan") ? cell->child.getNum("colspan") : 1;
+        if(cell->row + cell->rspan > rows) rows = cell->row + cell->rspan;
+        if(cell->col + cell->cspan > cols) cols = cell->col + cell->cspan;
+    }
+    int* rowSizes = new int[rows];
+    int* colSizes = new int[cols];
+    for(i=0; i<rows; i++) rowSizes[i] = 0;
+    for(i=0; i<cols; i++) colSizes[i] = 0;
+
+    // Pass 1a (hack): we want row zero to be the top, not the
+    // (cartesian: y==0) bottom, so reverse the sense of the row
+    // numbers.
+    for(i=0; i<nc; i++) {
+        TabCell* cell = &children[i];
+        cell->row = rows - cell->row - cell->rspan;
+    }
+    
+    // Pass 2: get sizes for single-cell children
+    for(i=0; i<nc; i++) {
+        TabCell* cell = &children[i];
+        if(cell->rspan < 2 && cell->h > rowSizes[cell->row])
+            rowSizes[cell->row] = cell->h;
+        if(cell->cspan < 2 && cell->w > colSizes[cell->col])
+            colSizes[cell->col] = cell->w;
+    }
+    
+    // Pass 3: multi-cell children, make space as needed
+    for(i=0; i<nc; i++) {
+        TabCell* cell = &children[i];
+        if(cell->rspan > 1) {
+            int total = 0;
+            for(j=0; j<cell->rspan; j++)
+                total += rowSizes[cell->row + j];
+            int extra = total - cell->h;
+            if(extra > 0) {
+                for(j=0; j<cell->rspan; j++) {
+                    int chunk = extra / (cell->rspan - j);
+                    rowSizes[cell->row + j] += chunk;
+                    extra -= chunk;
+                }
+            }
+        }
+        if(cell->cspan > 1) {
+            int total = 0;
+            for(j=0; j<cell->cspan; j++)
+                total += colSizes[cell->col + j];
+            int extra = total - cell->w;
+            if(extra > 0) {
+                for(j=0; j<cell->cspan; j++) {
+                    int chunk = extra / (cell->cspan - j);
+                    colSizes[cell->col + j] += chunk;
+                    extra -= chunk;
+                }
+            }
+        }
+    }
+
+    // Calculate our preferred sizes, and return if we aren't doing layout
+    int prefw=0, prefh=0;
+    for(i=0; i<cols; i++) prefw += colSizes[i];
+    for(i=0; i<rows; i++) prefh += rowSizes[i];
+
+    if(!doLayout) {
+        *w = prefw; *h = prefh;
+        delete[] children; delete[] rowSizes; delete[] colSizes;
+        return;
+    }
+
+    // Allocate extra space
+    int pad = 2*padding();
+    int extra = getNum("height") - pad - prefh;
+    for(i=0; i<rows; i++) {
+        int chunk = extra / (rows - i);
+        rowSizes[i] += chunk;
+        extra -= chunk;
+    }
+    extra = getNum("width") - pad - prefw;
+    for(i=0; i<cols; i++) {
+        int chunk = extra / (cols - i);
+        colSizes[i] += chunk;
+        extra -= chunk;
+    }
+
+    // Finally, lay out the children (with just two more temporary
+    // arrays for calculating coordinates)
+    int* rowY = new int[rows];
+    int total = 0;
+    for(i=0; i<rows; i++) { rowY[i] = total; total += rowSizes[i]; }
+
+    int* colX = new int[cols];
+    total = 0;
+    for(i=0; i<cols; i++) { colX[i] = total; total += colSizes[i]; }
+
+    for(i=0; i<nc; i++) {
+        TabCell* cell = &children[i];
+        int w = 0, h = 0;
+        for(j=0; j<cell->rspan; j++) h += rowSizes[cell->row + j];
+        for(j=0; j<cell->cspan; j++) w += colSizes[cell->col + j];
+        int x = colX[cell->col];
+        int y = rowY[cell->row];
+        cell->child.layout(x, y, w, h);
+    }    
+
+    // Clean up
+    delete[] children;
+    delete[] rowSizes;
+    delete[] colSizes;
+    delete[] rowY;
+    delete[] colX;
+}
diff --git a/src/GUI/layout.hxx b/src/GUI/layout.hxx
new file mode 100644 (file)
index 0000000..1819d8b
--- /dev/null
@@ -0,0 +1,56 @@
+#ifndef __LAYOUT_HXX
+#define __LAYOUT_HXX
+
+class SGPropertyNode;
+class puFont;
+
+// For the purposes of doing layout management, widgets have a type,
+// zero or more children, and string-indexed "fields" which can be
+// constraints, parameters or x/y/width/height geometry values.  It
+// can provide a "preferred" width and height to its parent, and is
+// capable of laying itself out into a specified x/y/w/h box.  The
+// widget "type" is not a field for historical reasons having to do
+// with the way the dialog property format works.
+//
+// Note that this is a simple wrapper around an SGPropertyNode
+// pointer.  The intent is that these objects will be created on the
+// stack as needed and passed by value.  All persistent data is stored
+// in the wrapped properties.
+class LayoutWidget {
+public:
+    static void setDefaultFont(puFont* font, int pixels);
+
+    LayoutWidget() { _prop = 0; }
+    LayoutWidget(SGPropertyNode* p) { _prop = p; }
+
+    const char*  type();
+    bool         hasParent();
+    LayoutWidget parent();
+    int          nChildren();
+    LayoutWidget getChild(int i);
+    bool         hasField(const char* f);
+    int          getNum(const char* f);
+    bool         getBool(const char* f);
+    const char*  getStr(const char* f);
+    void         setNum(const char* f, int num);
+
+    void calcPrefSize(int* w, int* h);
+    void layout(int x, int y, int w, int h);
+
+private:
+    static int UNIT;
+    static puFont FONT;
+
+    static bool eq(const char* a, const char* b);
+    bool isType(const char* t) { return eq(t, type()); }
+
+    int padding();
+    int stringLength(const char* s); // must handle null argument
+
+    void doHVBox(bool doLayout, bool vertical, int* w=0, int* h=0);
+    void doTable(bool doLayout, int* w=0, int* h=0);
+
+    SGPropertyNode* _prop;
+};
+
+#endif // __LAYOUT_HXX