#include "puList.hxx"
#include "AirportList.hxx"
+#include "layout.hxx"
int fgPopup::checkHit(int button, int updown, int x, int y)
{
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();
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);
} 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") {
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") {
void
FGDialog::setupObject (puObject * object, SGPropertyNode * props)
{
+ object->setLabelPlace(PUPLACE_CENTERED_RIGHT);
+
if (props->hasValue("legend"))
object->setLegend(props->getStringValue("legend"));
{
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++)
--- /dev/null
+#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);
+}
--- /dev/null
+#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;
+}