diff --git a/rsrc/dialogs/edit-party.xml b/rsrc/dialogs/edit-party.xml
index a153d30b..d2ae1caf 100644
--- a/rsrc/dialogs/edit-party.xml
+++ b/rsrc/dialogs/edit-party.xml
@@ -1,58 +1,58 @@
diff --git a/rsrc/dialogs/preferences.xml b/rsrc/dialogs/preferences.xml
index 4d82eebe..a37526bd 100644
--- a/rsrc/dialogs/preferences.xml
+++ b/rsrc/dialogs/preferences.xml
@@ -1,46 +1,45 @@
diff --git a/rsrc/schemas/dialog.xsd b/rsrc/schemas/dialog.xsd
index 51574bbc..58f291fb 100644
--- a/rsrc/schemas/dialog.xsd
+++ b/rsrc/schemas/dialog.xsd
@@ -71,6 +71,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -81,6 +100,11 @@
+
+
+
+
+
@@ -136,6 +160,7 @@
+
@@ -157,6 +182,7 @@
+
@@ -170,6 +196,7 @@
+
@@ -185,6 +212,7 @@
+
@@ -199,6 +227,7 @@
+
@@ -236,6 +265,7 @@
+
@@ -278,5 +308,9 @@
+
+
+
+
diff --git a/src/dialogxml/dialogs/dialog.cpp b/src/dialogxml/dialogs/dialog.cpp
index e14cc19a..3d7922dc 100644
--- a/src/dialogxml/dialogs/dialog.cpp
+++ b/src/dialogxml/dialogs/dialog.cpp
@@ -216,44 +216,133 @@ void cDialog::loadFromFile(std::string path){
}
vector specificTabs, reverseTabs;
+ std::pair prevCtrl{"", nullptr};
for(node = node.begin(xml.FirstChildElement()); node != node.end(); node++){
node->GetValue(&type);
+ ctrlIter inserted;
// Yes, I'm using insert instead of [] to add elements to the map.
// In this situation, it's actually easier that way; the reason being, the
// map key is obtained from the name attribute of each element.
if(type == "field") {
auto field = parse(*node);
- controls.insert(field);
+ inserted = controls.insert(field).first;
tabOrder.push_back(field);
if(field.second->tabOrder > 0)
specificTabs.push_back(field.second->tabOrder);
else if(field.second->tabOrder < 0)
reverseTabs.push_back(field.second->tabOrder);
} else if(type == "text")
- controls.insert(parse(*node));
+ inserted = controls.insert(parse(*node)).first;
else if(type == "pict")
- controls.insert(parse(*node));
+ inserted = controls.insert(parse(*node)).first;
else if(type == "slider")
- controls.insert(parse(*node));
+ inserted = controls.insert(parse(*node)).first;
else if(type == "button")
- controls.insert(parse(*node));
+ inserted = controls.insert(parse(*node)).first;
else if(type == "led")
- controls.insert(parse(*node));
+ inserted = controls.insert(parse(*node)).first;
else if(type == "group")
- controls.insert(parse(*node));
+ inserted = controls.insert(parse(*node)).first;
else if(type == "stack") {
auto parsed = parse(*node);
- controls.insert(parsed);
+ inserted = controls.insert(parsed).first;
// Now, if it contains any fields, their tab order must be accounted for
parsed.second->fillTabOrder(specificTabs, reverseTabs);
} else if(type == "pane") {
auto parsed = parse(*node);
- controls.insert(parsed);
+ inserted = controls.insert(parsed).first;
// TODO: Now, if it contains any fields, their tab order must be accounted for
//parsed.second->fillTabOrder(specificTabs, reverseTabs);
} else throw xBadNode(type,node->Row(),node->Column(),fname);
+ if(prevCtrl.second) {
+ if(inserted->second->anchor == "$$prev$$" && prevCtrl.second->anchor == "$$next$$") {
+ throw xBadVal(type, "anchor", "", node->Row(), node->Column(), fname);
+ } else if(inserted->second->anchor == "$$prev$$") {
+ inserted->second->anchor = prevCtrl.first;
+ } else if(prevCtrl.second->anchor == "$$next$$") {
+ prevCtrl.second->anchor = inserted->first;
+ }
+ }
+ prevCtrl = *inserted;
}
+ // Resolve relative positioning
+ bool all_resolved = true;
+ do {
+ all_resolved = true;
+ for(auto& p : controls) {
+ auto ctrl = p.second;
+ if(!ctrl->anchor.empty()) {
+ auto anchor = controls[ctrl->anchor];
+ if(!anchor->anchor.empty()) {
+ // Make sure it's not a loop!
+ std::vector refs{ctrl->anchor};
+ while(!anchor->anchor.empty()) {
+ refs.push_back(anchor->anchor);
+ anchor = controls[anchor->anchor];
+ if(std::find(refs.begin(), refs.end(), anchor->anchor) != refs.end()) {
+ std::string ctrlType;
+ switch(ctrl->getType()) {
+ case CTRL_UNKNOWN: ctrlType = "???"; break;
+ case CTRL_BTN: ctrlType = "button"; break;
+ case CTRL_LED: ctrlType = "led"; break;
+ case CTRL_PICT: ctrlType = "pict"; break;
+ case CTRL_FIELD: ctrlType = "field"; break;
+ case CTRL_TEXT: ctrlType = "text"; break;
+ case CTRL_GROUP: ctrlType = "group"; break;
+ case CTRL_STACK: ctrlType = "stack"; break;
+ case CTRL_SCROLL: ctrlType = "slider"; break;
+ case CTRL_PANE: ctrlType = "pane"; break;
+ }
+ throw xBadVal(ctrlType, "anchor", "", 0, 0, fname);
+ }
+ }
+ all_resolved = false;
+ continue;
+ }
+ ctrl->relocateRelative(ctrl->frame.topLeft(), anchor, ctrl->horz, ctrl->vert);
+ ctrl->anchor.clear();
+ ctrl->horz = ctrl->vert = POS_ABS;
+ } else if(auto pane = dynamic_cast(ctrl)) {
+ pane->forEach([this, &all_resolved](const std::string&, cControl& ctrl) {
+ // TODO: Deduplicate this code (it's functionally identical to the above non-container code)
+ if(!ctrl.anchor.empty()) {
+ auto anchor = controls[ctrl.anchor];
+ if(!anchor->anchor.empty()) {
+ // Make sure it's not a loop!
+ std::vector refs{ctrl.anchor};
+ while(!anchor->anchor.empty()) {
+ refs.push_back(anchor->anchor);
+ anchor = controls[anchor->anchor];
+ if(std::find(refs.begin(), refs.end(), anchor->anchor) != refs.end()) {
+ std::string ctrlType;
+ switch(ctrl.getType()) {
+ case CTRL_UNKNOWN: ctrlType = "???"; break;
+ case CTRL_BTN: ctrlType = "button"; break;
+ case CTRL_LED: ctrlType = "led"; break;
+ case CTRL_PICT: ctrlType = "pict"; break;
+ case CTRL_FIELD: ctrlType = "field"; break;
+ case CTRL_TEXT: ctrlType = "text"; break;
+ case CTRL_GROUP: ctrlType = "group"; break;
+ case CTRL_STACK: ctrlType = "stack"; break;
+ case CTRL_SCROLL: ctrlType = "slider"; break;
+ case CTRL_PANE: ctrlType = "pane"; break;
+ }
+ throw xBadVal(ctrlType, "anchor", "", 0, 0, fname);
+ }
+ }
+ all_resolved = false;
+ return;
+ }
+ ctrl.relocateRelative(ctrl.frame.topLeft(), anchor, ctrl.horz, ctrl.vert);
+ ctrl.anchor.clear();
+ ctrl.horz = ctrl.vert = POS_ABS;
+ }
+ });
+ }
+ }
+ } while(!all_resolved);
+
// Set the default button.
if(hasControl(defaultButton))
getControl(defaultButton).attachKey(enterKey);
@@ -317,29 +406,43 @@ void cDialog::loadFromFile(std::string path){
// now calculate window rect
winRect = rectangle();
recalcRect();
- ctrlIter iter = controls.begin();
currentFocus = "";
- while(iter != controls.end()){
+ for(ctrlIter iter = controls.begin(); iter != controls.end(); iter++){
if(typeid(iter->second) == typeid(cTextField*)){
if(currentFocus.empty()) currentFocus = iter->first;
break;
}
- iter++;
}
}
void cDialog::recalcRect(){
- ctrlIter iter = controls.begin();
- while(iter != controls.end()){
+ bool haveRel = false;
+ for(ctrlIter iter = controls.begin(); iter != controls.end(); iter++) {
+ using namespace std::placeholders;
+ if(auto container = dynamic_cast(iter->second))
+ container->forEach(std::bind(&cControl::recalcRect, _2));
+ iter->second->recalcRect();
rectangle frame = iter->second->getBounds();
- if(frame.right > winRect.right)
+ haveRel = haveRel || iter->second->horz != POS_ABS || iter->second->vert != POS_ABS;
+ if(iter->second->horz != POS_REL_NEG && frame.right > winRect.right)
winRect.right = frame.right;
- if(frame.bottom > winRect.bottom)
+ if(iter->second->vert != POS_REL_NEG && frame.bottom > winRect.bottom)
winRect.bottom = frame.bottom;
- iter++;
}
winRect.right += 6;
winRect.bottom += 6;
+ if(!haveRel) return;
+ // Resolve any remaining relative positions
+ // Controls placed relative to the dialog's edges can go off the edge of the dialog
+ for(ctrlIter iter = controls.begin(); iter != controls.end(); iter++) {
+ location pos = iter->second->getBounds().topLeft();
+ if(iter->second->horz == POS_REL_NEG)
+ pos.x = winRect.right - pos.x;
+ if(iter->second->vert == POS_REL_NEG)
+ pos.y = winRect.bottom - pos.y;
+ iter->second->horz = iter->second->vert = POS_ABS;
+ iter->second->relocate(pos);
+ }
}
void cDialog::init(){
diff --git a/src/dialogxml/widgets/button.cpp b/src/dialogxml/widgets/button.cpp
index 0a2ec6d3..b60d5c7a 100644
--- a/src/dialogxml/widgets/button.cpp
+++ b/src/dialogxml/widgets/button.cpp
@@ -160,11 +160,12 @@ bool cButton::parseContent(ticpp::Node& content, int n, std::string tagName, std
}
return cControl::parseContent(content, n, tagName, fname, text);
}
+
+static const std::set labelledButtons{BTN_TINY, BTN_LED, BTN_PUSH};
void cButton::validatePostParse(ticpp::Element& elem, std::string fname, const std::set& attrs, const std::multiset& elems) {
cControl::validatePostParse(elem, fname, attrs, elems);
if(getType() == CTRL_BTN && !attrs.count("type")) throw xMissingAttr(elem.Value(), "type", elem.Row(), elem.Column(), fname);
- static const std::set labelledButtons{BTN_TINY, BTN_LED, BTN_PUSH};
if(labelledButtons.count(type)) {
if(!attrs.count("color") && !attrs.count("colour") && parent->getBg() == cDialog::BG_DARK)
setColour(sf::Color::White);
@@ -177,6 +178,17 @@ location cButton::getPreferredSize() {
return {btnRects[type][0].width(), btnRects[type][0].height()};
}
+void cButton::recalcRect() {
+ location bestSz = getPreferredSize();
+ if(labelledButtons.count(type)) {
+ if(frame.width() < bestSz.x) frame.width() = bestSz.x;
+ if(frame.height() < bestSz.y) frame.height() = bestSz.y;
+ } else {
+ frame.width() = bestSz.x;
+ frame.height() = bestSz.y;
+ }
+}
+
// Indices within the buttons array.
size_t cButton::btnGW[14] = {
0, // BTN_SM
@@ -404,6 +416,10 @@ bool cLed::parseContent(ticpp::Node& content, int n, std::string tagName, std::s
return cButton::parseContent(content, n, tagName, fname, text);
}
+location cLed::getPreferredSize() {
+ return {ledRects[0][0].width(), ledRects[0][0].height()};
+}
+
void cLedGroup::addChoice(cLed* ctrl, std::string key) {
choices[key] = ctrl;
if(ctrl->getState() != led_off)
@@ -649,6 +665,5 @@ bool cLedGroup::parseContent(ticpp::Node& content, int n, std::string tagName, s
void cLedGroup::validatePostParse(ticpp::Element& who, std::string fname, const std::set& attrs, const std::multiset& nodes) {
// Don't defer to super-class; groups are an abstract container that doesn't require a position.
//cControl::validatePostParse(who, fname, attrs, nodes);
- recalcRect();
frameStyle = FRM_NONE;
}
diff --git a/src/dialogxml/widgets/button.hpp b/src/dialogxml/widgets/button.hpp
index 7f6cddae..7f73e317 100644
--- a/src/dialogxml/widgets/button.hpp
+++ b/src/dialogxml/widgets/button.hpp
@@ -54,6 +54,7 @@ public:
bool parseContent(ticpp::Node& content, int n, std::string tagName, std::string fname, std::string& text) override;
void validatePostParse(ticpp::Element& elem, std::string fname, const std::set& attrs, const std::multiset& elems) override;
location getPreferredSize() override;
+ void recalcRect() override;
/// Set the type of this button.
/// @param newType The desired button type.
void setBtnType(eBtnType newType);
@@ -115,6 +116,7 @@ public:
static bool noAction(cDialog&,std::string,eKeyMod) {return true;}
bool parseAttribute(ticpp::Attribute& attr, std::string tagName, std::string fname) override;
bool parseContent(ticpp::Node& content, int n, std::string tagName, std::string fname, std::string& text) override;
+ location getPreferredSize() override;
storage_t store() override;
void restore(storage_t to) override;
/// Create a new LED button.
@@ -239,7 +241,7 @@ public:
/// Recalculate the LED group's bounding rect.
/// Call this after adding choices to the group to ensure that the choice is within the bounding rect.
/// If a choice is not within the bounding rect, it will not respond to clicks.
- void recalcRect();
+ void recalcRect() override;
void forEach(std::function callback) override;
/// A convenience type for making an iterator into the choice map.
typedef std::map::iterator ledIter;
diff --git a/src/dialogxml/widgets/control.cpp b/src/dialogxml/widgets/control.cpp
index 68b32e52..318a2d82 100644
--- a/src/dialogxml/widgets/control.cpp
+++ b/src/dialogxml/widgets/control.cpp
@@ -37,6 +37,29 @@ void cControl::relocate(location to) {
frame.offset(to.x - frame.left, to.y - frame.top);
}
+void cControl::relocateRelative(location to, cControl* anchor, ePosition h, ePosition v) {
+ if(anchor == nullptr) anchor = this;
+ // Determine the anchor point of the relocation
+ location anchorPoint;
+ switch(h) {
+ case POS_ABS: anchorPoint.x = 0; break;
+ case POS_REL_PLUS: anchorPoint.x = anchor->frame.right; break;
+ case POS_REL_NEG: anchorPoint.x = anchor->frame.left; to.x = -to.x; break;
+ case POS_CONT_PLUS: anchorPoint.x = anchor->frame.left; break;
+ case POS_CONT_NEG: anchorPoint.x = anchor->frame.right; to.x = -to.x; break;
+ }
+ switch(v) {
+ case POS_ABS: anchorPoint.y = 0; break;
+ case POS_REL_PLUS: anchorPoint.y = anchor->frame.bottom; break;
+ case POS_REL_NEG: anchorPoint.y = anchor->frame.top; to.y = -to.y; break;
+ case POS_CONT_PLUS: anchorPoint.y = anchor->frame.top; break;
+ case POS_CONT_NEG: anchorPoint.y = anchor->frame.bottom; to.y = -to.y; break;
+ }
+ to.x += anchorPoint.x;
+ to.y += anchorPoint.y;
+ relocate(to);
+}
+
const char* xHandlerNotSupported::msg[4] = {
"This control cannot handle click events.\n",
"This control cannot handle focus events.\n",
@@ -491,6 +514,51 @@ std::string cControl::parse(ticpp::Element& who, std::string fname) {
bool cControl::parseAttribute(ticpp::Attribute& attr, std::string tagName, std::string fname) {
std::string name;
attr.GetName(&name);
+ // Relative positioning
+ if(name == "relative") {
+ static auto space = " \t";
+ std::string rel = attr.Value();
+ const xBadVal err(tagName, name, rel, attr.Row(), attr.Column(), fname);
+ size_t border = rel.find_first_of(space);
+ if(border != std::string::npos) {
+ size_t border_end = rel.find_last_of(space);
+ // Error if any of [border, border_end] are not spaces
+ for(size_t i = border + 1; i < border_end; i++) {
+ if(rel[i] != ' ' && rel[i] != '\t') throw err;
+ }
+ std::string h = rel.substr(0, border), v = rel.substr(border_end + 1);
+ if(h == "abs") horz = POS_ABS;
+ else if(h == "pos") horz = POS_REL_PLUS;
+ else if(h == "neg") horz = POS_REL_NEG;
+ else if(h == "pos-in") horz = POS_CONT_PLUS;
+ else if(h == "neg-in") horz = POS_CONT_NEG;
+ else throw err;
+ if(v == "abs") vert = POS_ABS;
+ else if(v == "pos") vert = POS_REL_PLUS;
+ else if(v == "neg") vert = POS_REL_NEG;
+ else if(v == "pos-in") vert = POS_CONT_PLUS;
+ else if(v == "neg-in") vert = POS_CONT_NEG;
+ else throw err;
+ }
+ else if(rel == "abs") horz = vert = POS_ABS;
+ else if(rel == "pos") horz = vert = POS_REL_PLUS;
+ else if(rel == "neg") horz = vert = POS_REL_NEG;
+ else if(rel == "pos-in") horz = vert = POS_CONT_PLUS;
+ else if(rel == "neg-in") horz = vert = POS_CONT_NEG;
+ else throw err;
+ return true;
+ }
+ if(name == "anchor") {
+ anchor = attr.Value();
+ return true;
+ }
+ if(name == "rel-anchor") {
+ std::string val = attr.Value();
+ if(val == "next") anchor = "$$next$$";
+ else if(val == "prev") anchor = "$$prev$$";
+ else throw xBadVal(tagName, name, val, attr.Row(), attr.Column(), fname);
+ return true;
+ }
// Colour and formatting, if supported
if(name == "framed" && canFormat(TXT_FRAME)) {
std::string val;
@@ -559,6 +627,13 @@ bool cControl::parseContent(ticpp::Node&, int, std::string, std::string, std::st
void cControl::validatePostParse(ticpp::Element& elem, std::string fname, const std::set& attrs, const std::multiset&) {
if(!attrs.count("left")) throw xMissingAttr(elem.Value(), "left", elem.Row(), elem.Column(), fname);
if(!attrs.count("top")) throw xMissingAttr(elem.Value(), "top", elem.Row(), elem.Column(), fname);
+ if(attrs.count("relative") && !attrs.count("anchor") && !attrs.count("rel-anchor")) {
+ // If relative is specified, an anchor is required... unless it's abs or neg
+ if((horz != POS_ABS && horz != POS_REL_NEG) || (vert != POS_ABS && vert != POS_REL_NEG))
+ throw xMissingAttr(elem.Value(), "anchor", elem.Row(), elem.Column(), fname);
+ }
+ if(attrs.count("anchor") && attrs.count("rel-anchor"))
+ throw xBadAttr(elem.Value(), "(rel-)anchor", elem.Row(), elem.Column(), fname);
}
cControl::~cControl() {}
diff --git a/src/dialogxml/widgets/control.hpp b/src/dialogxml/widgets/control.hpp
index bf9a3e9b..17dee876 100644
--- a/src/dialogxml/widgets/control.hpp
+++ b/src/dialogxml/widgets/control.hpp
@@ -55,6 +55,14 @@ enum eControlType {
CTRL_PANE, ///< A scroll pane
};
+enum ePosition {
+ POS_ABS, ///< Absolute positioning (possibly relative to a container)
+ POS_REL_PLUS, ///< Positioned relative to another widget, measuring down from its bottom edge or right from its right edge
+ POS_REL_NEG, ///< Positioned relative to another widget, measuring up from its top edge or left from its left edge
+ POS_CONT_PLUS, ///< Positioned relative to another widget, measuring down from its top edge or right from its left edge
+ POS_CONT_NEG, ///< Positioned relative to another widget, measuering up from its bottom edge or left from its right edge
+};
+
/// Thrown when you try to set a handler that the control does not support.
class xHandlerNotSupported : public std::exception {
static const char* msg[4];
@@ -251,6 +259,13 @@ public:
/// Set the position of this control.
/// @param to The new position.
void relocate(location to);
+ /// Set the position of this control relative to another control.
+ /// @param to The new relative position.
+ /// @param anchor The position will be calculated relative to this control's position.
+ /// If nullptr, the control will be moved relative to its current position.
+ /// @param horz How to place the control on the horizontal axis.
+ /// @param vert How to place the control on the vertical axis.
+ void relocateRelative(location to, cControl* anchor, ePosition horz, ePosition vert);
/// Get the control's text as an integer.
/// @return The control's text, coerced to an integer.
long long getTextAsNum();
@@ -330,6 +345,10 @@ public:
cControl& operator=(cControl& other) = delete;
cControl(cControl& other) = delete;
protected:
+ /// If the control automatically determines its rect based on certain criteria, override this.
+ /// It will automatically be called during parsing.
+ /// When overridden, it should normally be public.
+ virtual void recalcRect() {}
/// Returns a list of event handlers that this control supports.
/// @return The list of handlers as a std::set.
///
@@ -426,6 +445,9 @@ private:
friend class cDialog; // TODO: This is only so it can access parseColour... hack!
eControlType type;
std::map event_handlers;
+ // Transient values only used during parsing
+ ePosition horz = POS_ABS, vert = POS_ABS;
+ std::string anchor;
};
/// A superclass to represent a control that contains other controls.
diff --git a/src/dialogxml/widgets/pict.hpp b/src/dialogxml/widgets/pict.hpp
index 377ca631..5843b9c8 100644
--- a/src/dialogxml/widgets/pict.hpp
+++ b/src/dialogxml/widgets/pict.hpp
@@ -47,7 +47,7 @@ public:
/// @param num The new icon index.
void setPict(pic_num_t num);
/// Automatically recalculate the icon's bounding rect based on its current picture.
- void recalcRect();
+ void recalcRect() override;
/// Get the current icon.
/// @return The number of the current icon.
pic_num_t getPicNum();
diff --git a/src/dialogxml/widgets/scrollpane.cpp b/src/dialogxml/widgets/scrollpane.cpp
index 10e483f4..f16aaa49 100644
--- a/src/dialogxml/widgets/scrollpane.cpp
+++ b/src/dialogxml/widgets/scrollpane.cpp
@@ -219,5 +219,4 @@ bool cScrollPane::parseContent(ticpp::Node& content, int n, std::string tagName,
void cScrollPane::validatePostParse(ticpp::Element& who, std::string fname, const std::set& attrs, const std::multiset& nodes) {
cContainer::validatePostParse(who, fname, attrs, nodes);
if(!attrs.count("style")) setStyle(SCROLL_LED);
- recalcRect();
}
diff --git a/src/dialogxml/widgets/scrollpane.hpp b/src/dialogxml/widgets/scrollpane.hpp
index 187783b4..9c834ca0 100644
--- a/src/dialogxml/widgets/scrollpane.hpp
+++ b/src/dialogxml/widgets/scrollpane.hpp
@@ -41,7 +41,7 @@ public:
/// @note This function is intended for internal use, which is why it takes a control pointer instead of a unique key.
void addChild(cControl* ctrl, std::string key);
/// Recalculate the pane's bounding rect based on its contained controls.
- void recalcRect();
+ void recalcRect() override;
/// Get the pane's current scroll position.
/// @return The current position.
long getPosition();
diff --git a/src/dialogxml/widgets/stack.cpp b/src/dialogxml/widgets/stack.cpp
index bf1af069..15a91879 100644
--- a/src/dialogxml/widgets/stack.cpp
+++ b/src/dialogxml/widgets/stack.cpp
@@ -201,6 +201,5 @@ bool cStack::parseContent(ticpp::Node& content, int n, std::string tagName, std:
void cStack::validatePostParse(ticpp::Element& who, std::string fname, const std::set& attrs, const std::multiset& nodes) {
validatePostParse(who, fname, attrs, nodes);
- recalcRect();
}
diff --git a/src/dialogxml/widgets/stack.hpp b/src/dialogxml/widgets/stack.hpp
index 10dbf537..3b8b31fe 100644
--- a/src/dialogxml/widgets/stack.hpp
+++ b/src/dialogxml/widgets/stack.hpp
@@ -68,7 +68,7 @@ public:
/// @return The number of pages
size_t getPageCount();
/// Recalculate the stack's bounding rect based on its contained controls.
- void recalcRect();
+ void recalcRect() override;
/// Adds any fields in this stack to the tab order building arrays.
/// Meant for internal use.
void fillTabOrder(std::vector& specificTabs, std::vector& reverseTabs);
diff --git a/src/doxy/mainpage.md b/src/doxy/mainpage.md
index 3c03651d..759c1a0b 100644
--- a/src/doxy/mainpage.md
+++ b/src/doxy/mainpage.md
@@ -27,7 +27,25 @@ The following attributes are allowed on all or most elements:
rect of the control within the dialog. All non-container controls
support these attributes, and in fact the `top` and `left` attributes
are required. Some controls may ignore the `width` and `height`
-attributes.
+attributes. All of these must be non-negative integers.
+* `relative` - Specifies how the location is computed; defaults to `"abs"`.
+Must be one or two (space-separated) of the following;
+if two are specified, they represent the mode used for calculating x and y respectively:
+ * `abs` - Computed in global dialog space, relative to the top left corner.
+ * `pos` - Computed relative to the reference widget's bottom right corner.
+ * `pos-in` - Computed relative to the reference widget's top left corner.
+ * `neg` - Computed relative to the reference widget's top left corner,
+with the axes inverted (as if top and left were negative).
+As a special case, if this is used with no reference widget,
+the widget's size does not contribute to computation of the dialog's size,
+and the widget is positioned relative to the dialog's bottom right corner.
+ * `neg-in` - Computed relative to the reference widget's bottom right corner,
+with the axes inverted.
+* `anchor` - Specifies the `name` of the reference widget for this widget's location.
+* `rel-anchor` - Set to `prev` or `next` to use the previous or next element in the XML ordering
+as the reference widget for this widget's location.
+This currently does not work for widgets in containers.
+Mutually exclusive with `anchor`.
* `def-key` - Specifies the default keyboard shortcut for the
control. See **Keyboard Shortcuts** below for more information on the
format of this attribute.