From eeeee3a53eaf9ae51af307ca64f78530d1df0ff6 Mon Sep 17 00:00:00 2001 From: andy Date: Wed, 12 May 2004 15:36:07 +0000 Subject: [PATCH] GUI layout management and a few visual/eye-candy modifications. See DOCS/README.layout in the base package for details, along with the modified dialog files. --- src/GUI/Makefile.am | 10 +- src/GUI/dialog.cxx | 41 ++++- src/GUI/gui.cxx | 8 +- src/GUI/layout-props.cxx | 88 ++++++++++ src/GUI/layout-test.cxx | 56 ++++++ src/GUI/layout.cxx | 371 +++++++++++++++++++++++++++++++++++++++ src/GUI/layout.hxx | 56 ++++++ 7 files changed, 620 insertions(+), 10 deletions(-) create mode 100644 src/GUI/layout-props.cxx create mode 100644 src/GUI/layout-test.cxx create mode 100644 src/GUI/layout.cxx create mode 100644 src/GUI/layout.hxx diff --git a/src/GUI/Makefile.am b/src/GUI/Makefile.am index 267f5414f..e6979c0b7 100644 --- a/src/GUI/Makefile.am +++ b/src/GUI/Makefile.am @@ -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 diff --git a/src/GUI/dialog.cxx b/src/GUI/dialog.cxx index 6d3c7c7cb..ae1f96270 100644 --- a/src/GUI/dialog.cxx +++ b/src/GUI/dialog.cxx @@ -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++) diff --git a/src/GUI/gui.cxx b/src/GUI/gui.cxx index 48deee30c..571588e9c 100644 --- a/src/GUI/gui.cxx +++ b/src/GUI/gui.cxx @@ -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 index 000000000..68574045d --- /dev/null +++ b/src/GUI/layout-props.cxx @@ -0,0 +1,88 @@ +#include +#include + +#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 index 000000000..feb8f207f --- /dev/null +++ b/src/GUI/layout-test.cxx @@ -0,0 +1,56 @@ +#include + +#include +#include +#include +#include +#include + +#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 index 000000000..40f16a326 --- /dev/null +++ b/src/GUI/layout.cxx @@ -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 +// property, or per widget with . +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 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; ichild = 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; irow = rows - cell->row - cell->rspan; + } + + // Pass 2: get sizes for single-cell children + for(i=0; irspan < 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; irspan > 1) { + int total = 0; + for(j=0; jrspan; j++) + total += rowSizes[cell->row + j]; + int extra = total - cell->h; + if(extra > 0) { + for(j=0; jrspan; j++) { + int chunk = extra / (cell->rspan - j); + rowSizes[cell->row + j] += chunk; + extra -= chunk; + } + } + } + if(cell->cspan > 1) { + int total = 0; + for(j=0; jcspan; j++) + total += colSizes[cell->col + j]; + int extra = total - cell->w; + if(extra > 0) { + for(j=0; jcspan; 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; irspan; j++) h += rowSizes[cell->row + j]; + for(j=0; jcspan; 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 index 000000000..1819d8b4e --- /dev/null +++ b/src/GUI/layout.hxx @@ -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 -- 2.39.5