Add a new tilemap control that replicates its children into a fixed grid.

Use it for the Edit Terrain Object dialog.
This commit is contained in:
2025-02-26 22:21:02 -05:00
committed by Celtic Minstrel
parent 2ee2a545ef
commit f018f051f6
16 changed files with 376 additions and 47 deletions

View File

@@ -25,6 +25,7 @@
#include "dialogxml/widgets/scrollbar.hpp"
#include "dialogxml/widgets/scrollpane.hpp"
#include "dialogxml/widgets/stack.hpp"
#include "dialogxml/widgets/tilemap.hpp"
#include "tools/keymods.hpp"
#include "tools/winutil.hpp"
#include "mathutil.hpp"
@@ -45,22 +46,14 @@ const short cDialog::BG_DARK = 5, cDialog::BG_LIGHT = 16;
short cDialog::defaultBackground = cDialog::BG_DARK;
cDialog* cDialog::topWindow = nullptr;
void (*cDialog::redraw_everything)() = nullptr;
std::mt19937 cDialog::ui_rand;
extern std::map<std::string,sf::Color> colour_map;
extern bool check_for_interrupt(std::string);
extern void showError(std::string str1, cDialog* parent = nullptr);
std::string cDialog::generateRandomString(){
// Not bothering to seed, because it doesn't actually matter if it's truly random.
int n_chars = ui_rand() % 100;
std::string s = "$";
while(n_chars > 0){
s += char(ui_rand() % 96) + ' '; // was 223 ...
n_chars--;
}
return s;
std::string cDialog::generateId(const std::string& explicitId) const {
return explicitId.empty() ? cControl::generateRandomString() : explicitId;
}
string cControl::dlogStringFilter(string toFilter) {
@@ -235,6 +228,10 @@ void cDialog::loadFromFile(const DialogDefn& file){
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 if(type == "tilemap") {
auto parsed = parse<cTilemap>(*node);
inserted = controls.insert(parsed).first;
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$$") {
@@ -271,6 +268,7 @@ void cDialog::loadFromFile(const DialogDefn& file){
case CTRL_STACK: ctrlType = "stack"; break;
case CTRL_SCROLL: ctrlType = "slider"; break;
case CTRL_PANE: ctrlType = "pane"; break;
case CTRL_MAP: ctrlType = "tilemap"; break;
}
throw xBadVal(ctrlType, "anchor", ctrl.anchor, 0, 0, fname);
}
@@ -293,6 +291,7 @@ void cDialog::loadFromFile(const DialogDefn& file){
case CTRL_STACK: ctrlType = "stack"; break;
case CTRL_SCROLL: ctrlType = "slider"; break;
case CTRL_PANE: ctrlType = "pane"; break;
case CTRL_MAP: ctrlType = "tilemap"; break;
}
throw xBadVal(ctrlType, "anchor", "<circular dependency>", 0, 0, fname);
}

View File

@@ -20,7 +20,6 @@
#include <exception>
#include <functional>
#include <deque>
#include <random>
#include "ticpp.h"
#include "dialogxml/keycodes.hpp"
@@ -68,14 +67,13 @@ class cDialog {
std::string currentFocus;
cDialog* parent;
cControl* findControl(std::string id);
std::string generateRandomString();
std::string generateId(const std::string& explicitId) const;
void loadFromFile(const DialogDefn& file);
void handleTab(bool reverse);
template<typename Iter> void handleTabOrder(std::string& itemHit, Iter begin, Iter end);
std::vector<std::pair<std::string,cTextField*>> tabOrder;
static cDialog* topWindow; // Tracks the frontmost dialog.
static bool initCalled;
static std::mt19937 ui_rand;
public:
static void (*redraw_everything)();
/// Performs essential startup initialization. Generally should not be called directly.
@@ -242,19 +240,21 @@ public:
/// Adds a new control described by the passed XML element.
/// @tparam Ctrl The type of control to add.
/// @param who The XML element describing the control.
/// @param parent The parent control, if any. Omit if there is no parent.
/// @note It is up to the caller to ensure that that the element
/// passed describes the type of control being requested.
template<class Ctrl> std::pair<std::string,Ctrl*> parse(ticpp::Element& who) {
template<class Ctrl, class Container> std::pair<std::string,Ctrl*> parse(ticpp::Element& who, Container* parent) {
std::pair<std::string,Ctrl*> p;
p.second = new Ctrl(*this);
p.first = p.second->parse(who, fname);
if(p.first == ""){
do{
p.first = generateRandomString();
}while(controls.find(p.first) != controls.end());
}
do{
p.first = parent->generateId(p.first);
}while(controls.find(p.first) != controls.end());
return p;
}
template<class Ctrl> std::pair<std::string,Ctrl*> parse(ticpp::Element& who) {
return parse<Ctrl, cDialog>(who, this);
}
cDialogIterator begin() {
return cDialogIterator(this);
}

View File

@@ -20,32 +20,32 @@ bool cContainer::parseChildControl(ticpp::Element& elem, std::map<std::string,cC
ctrlIter inserted;
std::string tag = elem.Value();
if(tag == "field") {
auto field = parent->parse<cTextField>(elem);
auto field = parent->parse<cTextField>(elem, this);
inserted = controls.insert(field).first;
parent->tabOrder.push_back(field);
id = field.first;
} else if(tag == "text") {
auto text = parent->parse<cTextMsg>(elem);
auto text = parent->parse<cTextMsg>(elem, this);
inserted = controls.insert(text).first;
id = text.first;
} else if(tag == "pict") {
auto pict = parent->parse<cPict>(elem);
auto pict = parent->parse<cPict>(elem, this);
inserted = controls.insert(pict).first;
id = pict.first;
} else if(tag == "slider") {
auto slide = parent->parse<cScrollbar>(elem);
auto slide = parent->parse<cScrollbar>(elem, this);
inserted = controls.insert(slide).first;
id = slide.first;
} else if(tag == "button") {
auto button = parent->parse<cButton>(elem);
auto button = parent->parse<cButton>(elem, this);
inserted = controls.insert(button).first;
id = button.first;
} else if(tag == "led") {
auto led = parent->parse<cLed>(elem);
auto led = parent->parse<cLed>(elem, this);
inserted = controls.insert(led).first;
id = led.first;
} else if(tag == "group") {
auto group = parent->parse<cLedGroup>(elem);
auto group = parent->parse<cLedGroup>(elem, this);
inserted = controls.insert(group).first;
id = group.first;
} else return false;

View File

@@ -23,6 +23,22 @@
// Hyperlink forward declaration
extern void launchURL(std::string url);
std::mt19937 cControl::ui_rand;
std::string cControl::generateId(const std::string& explicitId) const {
return explicitId.empty() ? generateRandomString() : explicitId;
}
std::string cControl::generateRandomString() {
// Not bothering to seed, because it doesn't actually matter if it's truly random.
int n_chars = ui_rand() % 100;
std::string s = "$";
while(n_chars > 0){
s += char(ui_rand() % 96) + ' '; // was 223 ...
n_chars--;
}
return s;
}
void cControl::setText(std::string l){
lbl = l;

View File

@@ -19,6 +19,7 @@
#include <functional>
#include <set>
#include <map>
#include <random>
#include <boost/any.hpp>
#include "dialogxml/dialogs/dlogevt.hpp"
#include "tools/framerate_limiter.hpp"
@@ -62,6 +63,7 @@ enum eControlType {
CTRL_STACK, ///< A group of controls that represents one element in an array
CTRL_SCROLL,///< A scrollbar
CTRL_PANE, ///< A scroll pane
CTRL_MAP, ///< A 2-dimensional grid of identical controls
};
enum ePosition {
@@ -461,6 +463,11 @@ protected:
void redraw();
/// Plays the proper sound for this control being clicked on
void playClickSound();
/// Generate a unique ID for a control. The explicitId is the ID specified in the XML, if any.
/// This may be called more than once, so it should not return the same value twice in a row,
/// unless it can guarantee the value is not already assigned to another control.
virtual std::string generateId(const std::string& explicitId) const;
static std::string generateRandomString();
private:
friend class cDialog; // This is so it can access parseColour and anchor
friend class cContainer; // This is so it can access anchor
@@ -472,6 +479,7 @@ private:
ePosition horz = POS_ABS, vert = POS_ABS;
std::string anchor;
bool is_link = false;
static std::mt19937 ui_rand;
};
#endif

View File

@@ -0,0 +1,210 @@
//
// tilemap.cpp
// BoE
//
// Created by Celtic Minstrel on 2025-02-01.
//
#include "tilemap.hpp"
#include "button.hpp"
#include "dialogxml/dialogs/dialog.hpp"
#include "field.hpp"
#include "message.hpp"
#include "pict.hpp"
#include "scrollbar.hpp"
#include "stack.hpp"
#include <climits>
std::string cTilemap::generateId(const std::string& baseId) const {
if(baseId.empty()) {
if(id_tries++ == 0) return current_cell;
return cControl::generateId(baseId) + "-" + current_cell;
}
return baseId + "-" + current_cell;
}
location cTilemap::getCell(const std::string& id) const {
size_t y_pos = id.find_last_of("y");
size_t x_pos = id.find_last_of("x");
std::string x_str = id.substr(x_pos + 1, y_pos);
std::string y_str = id.substr(y_pos + 1);
return location(std::stoi(x_str) * cellWidth, std::stoi(y_str) * cellHeight);
}
std::string cTilemap::buildId(const std::string& base, size_t x, size_t y) {
std::ostringstream sout;
if(!base.empty()) {
sout << base << '-';
}
sout << 'x' << x << 'y' << y;
return sout.str();
}
bool cTilemap::hasChild(std::string id) const {
return controls.find(id) != controls.end();
}
cControl& cTilemap::getChild(std::string id) {
if(!hasChild(id)) throw std::invalid_argument(id + " was not found in the tilemap");
return *controls[id];
}
bool cTilemap::hasChild(std::string id, size_t x, size_t y) const {
return hasChild(buildId(id, x, y));
}
cControl& cTilemap::getChild(std::string id, size_t x, size_t y) {
return getChild(buildId(id, x, y));
}
bool cTilemap::hasChild(size_t x, size_t y) const {
return hasChild("", x, y);
}
cControl& cTilemap::getChild(size_t x, size_t y) {
return getChild("", x, y);
}
bool cTilemap::manageFormat(eFormat prop, bool set, boost::any* val) {
switch(prop) {
case TXT_FRAME:
if(val) {
if(set) frameStyle = boost::any_cast<eFrameStyle>(*val);
else *val = frameStyle;
}
break;
// TODO: Colour is not supported
default: return false;
}
return true;
}
bool cTilemap::isClickable() const {
return true;
}
bool cTilemap::isFocusable() const {
return false;
}
bool cTilemap::isScrollable() const {
return false;
}
void cTilemap::draw() {
if(!isVisible()) return;
for(auto& ctrl : controls) {
rectangle localBounds = ctrl.second->getBounds();
rectangle globalBounds = localBounds;
globalBounds.offset(getBounds().topLeft());
globalBounds.offset(getCell(ctrl.first));
ctrl.second->setBounds(globalBounds);
ctrl.second->draw();
ctrl.second->setBounds(localBounds);
}
drawFrame(2, frameStyle);
}
void cTilemap::recalcRect() {
auto iter = controls.begin();
auto location = frame.topLeft();
frame = {INT_MAX, INT_MAX, 0, 0};
while(iter != controls.end()){
cControl& ctrl = *iter->second;
rectangle otherFrame = ctrl.getBounds();
if(otherFrame.right > frame.right)
frame.right = otherFrame.right;
if(otherFrame.bottom > frame.bottom)
frame.bottom = otherFrame.bottom;
if(otherFrame.left < frame.left)
frame.left = otherFrame.left;
if(otherFrame.top < frame.top)
frame.top = otherFrame.top;
iter++;
}
frame.offset(location);
frame.right += spacing;
frame.bottom += spacing;
cellWidth = frame.width();
cellHeight = frame.height();
frame.width() *= cols;
frame.height() *= rows;
frame.right -= spacing;
frame.bottom -= spacing;
}
void cTilemap::fillTabOrder(std::vector<int>& specificTabs, std::vector<int>& reverseTabs) {
for(auto p : controls) {
cControl& ctrl = *p.second;
if(ctrl.getType() == CTRL_FIELD) {
cTextField& field = dynamic_cast<cTextField&>(ctrl);
if(field.tabOrder > 0)
specificTabs.push_back(field.tabOrder);
else if(field.tabOrder < 0)
reverseTabs.push_back(field.tabOrder);
}
}
}
cTilemap::cTilemap(cDialog& parent) : cContainer(CTRL_MAP, parent) {}
void cTilemap::forEach(std::function<void(std::string,cControl&)> callback) {
for(auto ctrl : controls)
callback(ctrl.first, *ctrl.second);
}
bool cTilemap::parseAttribute(ticpp::Attribute& attr, std::string tagName, std::string fname) {
if(attr.Name() == "rows") {
try {
attr.GetValue(&rows);
} catch(ticpp::Exception&) {
throw xBadVal(tagName, attr.Name(), attr.Value(), attr.Row(), attr.Column(), fname);
}
return true;
} else if(attr.Name() == "cols") {
try {
attr.GetValue(&cols);
} catch(ticpp::Exception&) {
throw xBadVal(tagName, attr.Name(), attr.Value(), attr.Row(), attr.Column(), fname);
}
return true;
} else if(attr.Name() == "cellspacing") {
try {
attr.GetValue(&spacing);
} catch(ticpp::Exception&) {
throw xBadVal(tagName, attr.Name(), attr.Value(), attr.Row(), attr.Column(), fname);
}
return true;
}
return cContainer::parseAttribute(attr, tagName, fname);
}
bool cTilemap::parseContent(ticpp::Node& content, int n, std::string tagName, std::string fname, std::string& text) {
using namespace ticpp;
if(content.Type() == TiXmlNode::ELEMENT) {
std::string id;
auto& elem = dynamic_cast<Element&>(content);
id_tries = 0;
current_cell = "x0y0";
if(!parseChildControl(elem, controls, id, fname)) return false;
for(size_t x = 0; x < cols; x++) {
for(size_t y = 0; y < rows; y++) {
if(x == 0 && y == 0) continue; // already did this one
id_tries = 0;
std::ostringstream sout;
sout << "x" << x << "y" << y;
current_cell = sout.str();
parseChildControl(elem, controls, id, fname);
}
}
return true;
}
return cContainer::parseContent(content, n, tagName, fname, text);
}
void cTilemap::validatePostParse(ticpp::Element& who, std::string fname, const std::set<std::string>& attrs, const std::multiset<std::string>& nodes) {
cControl::validatePostParse(who, fname, attrs, nodes);
if(!attrs.count("rows")) throw xMissingAttr(who.Value(), "rows", who.Row(), who.Column(), fname);
if(!attrs.count("cols")) throw xMissingAttr(who.Value(), "cols", who.Row(), who.Column(), fname);
}

View File

@@ -0,0 +1,54 @@
//
// tilemap.hpp
// BoE
//
// Created by Celtic Minstrel on 2025-02-01.
//
#ifndef BoE_DIALOG_TILEMAP_HPP
#define BoE_DIALOG_TILEMAP_HPP
#include "container.hpp"
/// A tilemap defines a two-dimensional array of data using a repeated template.
class cTilemap : public cContainer {
std::map<std::string,cControl*> controls;
size_t rows, cols, spacing = 0, cellWidth, cellHeight;
bool manageFormat(eFormat prop, bool set, boost::any* val) override;
location getCell(const std::string& id) const;
static std::string buildId(const std::string& base, size_t x, size_t y);
std::string current_cell;
mutable int id_tries = 0;
public:
std::string generateId(const std::string& baseId) const override;
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;
void validatePostParse(ticpp::Element& who, std::string fname, const std::set<std::string>& attrs, const std::multiset<std::string>& nodes) override;
bool isClickable() const override;
bool isFocusable() const override;
bool isScrollable() const override;
void draw() override;
bool hasChild(std::string id) const override;
cControl& getChild(std::string id) override;
bool hasChild(std::string id, size_t x, size_t y) const;
cControl& getChild(std::string id, size_t x, size_t y);
bool hasChild(size_t x, size_t y) const;
cControl& getChild(size_t x, size_t y);
/// Recalculate the tilemap's bounding rect based on its contained controls.
void recalcRect() override;
/// Adds any fields in this tilemap to the tab order building arrays.
/// Meant for internal use.
void fillTabOrder(std::vector<int>& specificTabs, std::vector<int>& reverseTabs);
/// Create a new tilemap
/// @param parent The parent dialog.
cTilemap(cDialog& parent);
/// @copydoc cControl::getSupportedHandlers
///
/// @todo Document possible handlers
std::set<eDlogEvt> getSupportedHandlers() const override {
return {EVT_CLICK, EVT_FOCUS, EVT_DEFOCUS};
}
void forEach(std::function<void(std::string,cControl&)> callback) override;
};
#endif

View File

@@ -259,7 +259,7 @@ The `<stack>` tag
-----------------
The `<stack>` tag groups elements that represent a single entry in an array.
It can contain any elements except for nested `<stack>` or `<pane>` elements.
It can contain any elements except for nested `<stack>`, `<pane>`, or `<tilemap>` elements.
The `<stack>` tag accepts the following attributes:
@@ -287,7 +287,7 @@ The `<pane>` tag
----------------
The `<pane>` tag groups elements into a scrollable subpane.
It can contain any elements except for nested `<stack>` or `<pane>` elements.
It can contain any elements except for nested `<stack>`, `<pane>`, or `<tilemap>` elements.
The `<pane>` tag accepts the following attributes:
@@ -295,6 +295,20 @@ The `<pane>` tag accepts the following attributes:
* `outline` - See **Common Attributes** above.
* `style` - Same as for `<slider>`, see above. Applies to the pane's scrollbar.
The `<tilemap>` tag
-------------------
The `<tilemap>` tag represents a grid of identical elements based off the provided template.
It can contain any elements except for nested `<stack>`, `<pane>`, or `<tilemap>` elements.
The `<tilemap>` tag accepts the following attributes:
* `framed` - See **Common Attributes** above. Defaults to `false`.
* `outline` - See **Common Attributes** above.
* `rows` - The number of rows to generate. Required.
* `cols` - The number of columns to generate. Required.
* `cellspacing` - If specified, adds a buffer of this size between each cell.
Keyboard Shortcuts
------------------

View File

@@ -22,6 +22,7 @@
#include "dialogxml/dialogs/dialog.hpp"
#include "dialogxml/widgets/control.hpp"
#include "dialogxml/widgets/button.hpp"
#include "dialogxml/widgets/tilemap.hpp"
#include "dialogxml/dialogs/strdlog.hpp"
#include "dialogxml/dialogs/3choice.hpp"
#include "dialogxml/dialogs/strchoice.hpp"
@@ -517,10 +518,10 @@ static bool edit_ter_obj(cDialog& me, ter_num_t which_ter) {
obj[check.obj_pos.x][check.obj_pos.y] = check.picture;
}
obj[me["x"].getTextAsNum()][me["y"].getTextAsNum()] = pic;
cTilemap& map = dynamic_cast<cTilemap&>(me["map"]);
for(int x = 0; x < 4; x++) {
for(int y = 0; y < 4; y++) {
std::string id = "x" + std::to_string(x) + "y" + std::to_string(y);
dynamic_cast<cPict&>(me[id]).setPict(obj[x][y]);
dynamic_cast<cPict&>(map.getChild(x,y)).setPict(obj[x][y]);
}
}
return true;