1112 lines
33 KiB
C++
1112 lines
33 KiB
C++
/*
|
|
* dialog.cpp
|
|
* BoE
|
|
*
|
|
* Created by Celtic Minstrel on 11/05/09.
|
|
*
|
|
*/
|
|
|
|
#include <cmath>
|
|
#include <stdexcept>
|
|
#include <functional>
|
|
#include "dialog.hpp"
|
|
#include "gfx/tiling.hpp" // for bg
|
|
#include "fileio/resmgr/res_dialog.hpp"
|
|
#include "sounds.hpp"
|
|
#include "dialogxml/widgets/pict.hpp"
|
|
#include "dialogxml/widgets/button.hpp"
|
|
#include "dialogxml/widgets/field.hpp"
|
|
#include "dialogxml/widgets/ledgroup.hpp"
|
|
#include "dialogxml/widgets/message.hpp"
|
|
#include "dialogxml/widgets/scrollbar.hpp"
|
|
#include "dialogxml/widgets/scrollpane.hpp"
|
|
#include "dialogxml/widgets/stack.hpp"
|
|
#include "tools/keymods.hpp"
|
|
#include "tools/winutil.hpp"
|
|
#include "mathutil.hpp"
|
|
#include "tools/cursors.hpp"
|
|
#include "tools/prefs.hpp"
|
|
#include "tools/framerate_limiter.hpp"
|
|
|
|
using namespace std;
|
|
using namespace ticpp;
|
|
|
|
// TODO: Would be nice if I could avoid depending on mainPtr
|
|
extern sf::RenderWindow mainPtr;
|
|
|
|
extern sf::Texture bg_gworld;
|
|
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::string cDialog::generateRandomString(){
|
|
// Not bothering to seed, because it doesn't actually matter if it's truly random.
|
|
// Though, this will be called after srand() is called in main() anyway.
|
|
int n_chars = rand() % 100;
|
|
std::string s = "$";
|
|
while(n_chars > 0){
|
|
s += char(rand() % 96) + ' '; // was 223 ...
|
|
n_chars--;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
string cControl::dlogStringFilter(string toFilter) {
|
|
string filtered;
|
|
bool found_nl = false;
|
|
for(char c : toFilter) {
|
|
if(c == '\n' || c == '\r') found_nl = true;
|
|
else if(c != '\t' && c != 0) {
|
|
if(found_nl && !filtered.empty()) filtered += ' ';
|
|
found_nl = false;
|
|
filtered += c;
|
|
}
|
|
}
|
|
return filtered;
|
|
}
|
|
|
|
sf::Color cControl::parseColor(string what){
|
|
sf::Color clr;
|
|
if(what[0] == '#'){
|
|
unsigned int r,g,b;
|
|
if(sscanf(what.c_str(),"#%2x%2x%2x",&r,&g,&b) < 3) {
|
|
if(sscanf(what.c_str(),"#%1x%1x%1x",&r,&g,&b) < 3)
|
|
throw -1;
|
|
else {
|
|
r *= 0x11;
|
|
g *= 0x11;
|
|
b *= 0x11;
|
|
}
|
|
}
|
|
clr.r = r, clr.g = g, clr.b = b;
|
|
}else if(what == "black")
|
|
clr.r = 0x00, clr.g = 0x00, clr.b = 0x00;
|
|
else if(what == "red")
|
|
clr.r = 0xFF, clr.g = 0x00, clr.b = 0x00;
|
|
else if(what == "lime")
|
|
clr.r = 0x00, clr.g = 0xFF, clr.b = 0x00;
|
|
else if(what == "blue")
|
|
clr.r = 0x00, clr.g = 0x00, clr.b = 0xFF;
|
|
else if(what == "yellow")
|
|
clr.r = 0xFF, clr.g = 0xFF, clr.b = 0x00;
|
|
else if(what == "aqua")
|
|
clr.r = 0x00, clr.g = 0xFF, clr.b = 0xFF;
|
|
else if(what == "fuchsia")
|
|
clr.r = 0xFF, clr.g = 0x00, clr.b = 0xFF;
|
|
else if(what == "white")
|
|
clr.r = 0xFF, clr.g = 0xFF, clr.b = 0xFF;
|
|
else if(what == "gray" || what == "grey")
|
|
clr.r = 0x80, clr.g = 0x80, clr.b = 0x80;
|
|
else if(what == "maroon")
|
|
clr.r = 0x80, clr.g = 0x00, clr.b = 0x00;
|
|
else if(what == "green")
|
|
clr.r = 0x00, clr.g = 0x80, clr.b = 0x00;
|
|
else if(what == "navy")
|
|
clr.r = 0x00, clr.g = 0x00, clr.b = 0x80;
|
|
else if(what == "olive")
|
|
clr.r = 0x80, clr.g = 0x80, clr.b = 0x00;
|
|
else if(what == "teal")
|
|
clr.r = 0x00, clr.g = 0x80, clr.b = 0x80;
|
|
else if(what == "purple")
|
|
clr.r = 0x80, clr.g = 0x00, clr.b = 0x80;
|
|
else if(what == "silver")
|
|
clr.r = 0xC0, clr.g = 0xC0, clr.b = 0xC0;
|
|
else throw -1;
|
|
return clr;
|
|
}
|
|
|
|
cKey cControl::parseKey(string what){
|
|
cKey key;
|
|
key.spec = false;
|
|
key.c = 0;
|
|
key.mod = mod_none;
|
|
if(what == "none") return key;
|
|
istringstream sin(what);
|
|
string parts[4];
|
|
sin >> parts[0] >> parts[1] >> parts[2] >> parts[3];
|
|
for(int i = 0; i < 4; i++){
|
|
if(parts[i] == "ctrl") key.mod += mod_ctrl;
|
|
else if(parts[i] == "alt") key.mod += mod_alt;
|
|
else if(parts[i] == "shift") key.mod += mod_shift;
|
|
else if(parts[i] == "left") {
|
|
key.spec = true;
|
|
key.k = key_left;
|
|
break;
|
|
}else if(parts[i] == "right") {
|
|
key.spec = true;
|
|
key.k = key_right;
|
|
break;
|
|
}else if(parts[i] == "up") {
|
|
key.spec = true;
|
|
key.k = key_up;
|
|
break;
|
|
}else if(parts[i] == "down") {
|
|
key.spec = true;
|
|
key.k = key_down;
|
|
break;
|
|
}else if(parts[i] == "esc") {
|
|
key.spec = true;
|
|
key.k = key_esc;
|
|
break;
|
|
}else if(parts[i] == "enter" || parts[i] == "return") {
|
|
key.spec = true;
|
|
key.k = key_enter;
|
|
break;
|
|
}else if(parts[i] == "tab") {
|
|
key.spec = true;
|
|
key.k = key_tab;
|
|
break;
|
|
}else if(parts[i] == "help") {
|
|
key.spec = true;
|
|
key.k = key_help;
|
|
break;
|
|
}else if(parts[i] == "space") {
|
|
key.c = ' ';
|
|
break;
|
|
}else if(parts[i].length() == 1) {
|
|
key.c = parts[i][0];
|
|
break;
|
|
}else throw -1;
|
|
}
|
|
return key;
|
|
}
|
|
|
|
cDialog::cDialog(cDialog* p) : parent(p) {}
|
|
|
|
cDialog::cDialog(const DialogDefn& file, cDialog* p) : parent(p) {
|
|
loadFromFile(file);
|
|
}
|
|
|
|
extern fs::path progDir;
|
|
void cDialog::loadFromFile(const DialogDefn& file){
|
|
static const cKey enterKey = {true, key_enter};
|
|
bg = defaultBackground;
|
|
fname = file.id;
|
|
try{
|
|
const Document& xml = file.defn;
|
|
|
|
Iterator<Attribute> attr;
|
|
Iterator<Element> node;
|
|
string type, name, val, defaultButton;
|
|
|
|
xml.FirstChildElement()->GetValue(&type);
|
|
if(type != "dialog") throw xBadNode(type,xml.FirstChildElement()->Row(),xml.FirstChildElement()->Column(),fname);
|
|
for(attr = attr.begin(xml.FirstChildElement()); attr != attr.end(); attr++){
|
|
attr->GetName(&name);
|
|
attr->GetValue(&val);
|
|
if(name == "skin"){
|
|
if(val == "light") bg = BG_LIGHT;
|
|
else if(val == "dark") bg = BG_DARK;
|
|
else{
|
|
istringstream sin(val);
|
|
sin >> bg;
|
|
if(sin.fail()) throw xBadVal(type,name,val,attr->Row(),attr->Column(),fname);
|
|
}
|
|
}else if(name == "fore"){
|
|
sf::Color clr;
|
|
try{
|
|
clr = cControl::parseColor(val);
|
|
}catch(int){
|
|
throw xBadVal("text",name,val,attr->Row(),attr->Column(),fname);
|
|
}
|
|
defTextClr = clr;
|
|
} else if(name == "defbtn") {
|
|
defaultButton = val;
|
|
}else if(name != "debug")
|
|
throw xBadAttr(type,name,attr->Row(),attr->Column(),fname);
|
|
}
|
|
|
|
vector<int> specificTabs, reverseTabs;
|
|
std::pair<std::string,cControl*> 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<cTextField>(*node);
|
|
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")
|
|
inserted = controls.insert(parse<cTextMsg>(*node)).first;
|
|
else if(type == "pict")
|
|
inserted = controls.insert(parse<cPict>(*node)).first;
|
|
else if(type == "slider")
|
|
inserted = controls.insert(parse<cScrollbar>(*node)).first;
|
|
else if(type == "button")
|
|
inserted = controls.insert(parse<cButton>(*node)).first;
|
|
else if(type == "led")
|
|
inserted = controls.insert(parse<cLed>(*node)).first;
|
|
else if(type == "group")
|
|
inserted = controls.insert(parse<cLedGroup>(*node)).first;
|
|
else if(type == "stack") {
|
|
auto parsed = parse<cStack>(*node);
|
|
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<cScrollPane>(*node);
|
|
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", "<circular dependency>", 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;
|
|
// Needed to correctly resolve relative positioning
|
|
inserted->second->recalcRect();
|
|
}
|
|
|
|
// Resolve relative positioning
|
|
bool all_resolved = true;
|
|
|
|
std::function<void(const std::string&, cControl&)> process_ctrl;
|
|
process_ctrl = [this, &all_resolved, &process_ctrl](const std::string&, cControl& ctrl) {
|
|
if(!ctrl.anchor.empty()) {
|
|
auto anchor = controls[ctrl.anchor];
|
|
if(!anchor->anchor.empty()) {
|
|
// Make sure it's not a loop!
|
|
std::vector<std::string> 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", "<circular dependency>", 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;
|
|
} else if(auto pane = dynamic_cast<cContainer*>(&ctrl)) {
|
|
pane->forEach(process_ctrl);
|
|
}
|
|
};
|
|
|
|
do {
|
|
all_resolved = true;
|
|
for(auto& p : controls) {
|
|
auto ctrl = p.second;
|
|
|
|
process_ctrl(p.first, *ctrl);
|
|
}
|
|
} while(!all_resolved);
|
|
|
|
// Set the default button.
|
|
if(hasControl(defaultButton))
|
|
getControl(defaultButton).attachKey(enterKey);
|
|
|
|
// Sort by tab order
|
|
// First, fill any gaps that might have been left, using ones that had no specific tab order
|
|
// Of course, if there are not enough without a specific tab order, there could still be gaps
|
|
using fld_t = decltype(tabOrder)::value_type;
|
|
auto noTabOrder = [](fld_t x) {return x.second->tabOrder == 0;};
|
|
if(!specificTabs.empty()) {
|
|
int max = *max_element(specificTabs.begin(), specificTabs.end());
|
|
for(int i = 1; i < max; i++) {
|
|
auto check = find(specificTabs.begin(), specificTabs.end(), i);
|
|
if(check != specificTabs.end()) continue;
|
|
auto change = find_if(tabOrder.begin(), tabOrder.end(), noTabOrder);
|
|
if(change == tabOrder.end()) break;
|
|
change->second->tabOrder = i;
|
|
}
|
|
}
|
|
if(!reverseTabs.empty()) {
|
|
int max = -*min_element(reverseTabs.begin(), reverseTabs.end());
|
|
for(int i = 1; i < max; i++) {
|
|
auto check = find(reverseTabs.begin(), reverseTabs.end(), -i);
|
|
if(check != reverseTabs.end()) continue;
|
|
auto change = find_if(tabOrder.begin(), tabOrder.end(), noTabOrder);
|
|
if(change == tabOrder.end()) break;
|
|
change->second->tabOrder = -i;
|
|
}
|
|
}
|
|
// Then, sort - first, positive tab order ascending; then zeros; then negative tab order descending.
|
|
stable_sort(tabOrder.begin(), tabOrder.end(), [](fld_t a, fld_t b) -> bool {
|
|
bool a_neg = a.second->tabOrder < 0, b_neg = b.second->tabOrder < 0;
|
|
if(a_neg && !b_neg) return false;
|
|
else if(!a_neg && b_neg) return true;
|
|
bool a_pos = a.second->tabOrder > 0, b_pos = b.second->tabOrder > 0;
|
|
if(a_pos && !b_pos) return true;
|
|
else if(!a_pos && b_pos) return false;
|
|
return a.second->tabOrder < b.second->tabOrder;
|
|
});
|
|
} catch(Exception& x){ // XML processing exception
|
|
std::cerr << x.what();
|
|
throw;
|
|
} catch(xBadVal& x){ // Invalid value for an attribute
|
|
std::cerr << x.what();
|
|
throw;
|
|
} catch(xBadAttr& x){ // Invalid attribute for an element
|
|
std::cerr << x.what();
|
|
throw;
|
|
} catch(xBadNode& x){ // Invalid element
|
|
std::cerr << x.what();
|
|
throw;
|
|
} catch(xMissingAttr& x){ // Invalid element
|
|
std::cerr << x.what();
|
|
throw;
|
|
} catch(std::exception& x){ // Other uncaught exception
|
|
std::cerr << x.what();
|
|
throw;
|
|
}
|
|
dialogNotToast = true;
|
|
if(bg == BG_DARK) defTextClr = sf::Color::White;
|
|
// now calculate window rect
|
|
recalcRect();
|
|
currentFocus = "";
|
|
for(ctrlIter iter = controls.begin(); iter != controls.end(); iter++){
|
|
if(typeid(iter->second) == typeid(cTextField*)){
|
|
currentFocus = iter->first;
|
|
break;
|
|
}
|
|
if(iter->second->isContainer()){
|
|
cContainer* tmp = dynamic_cast<cContainer*>(iter->second);
|
|
tmp->forEach([this, iter](std::string key, cControl& child) {
|
|
if (typeid(&child) == typeid(cTextField*)) {
|
|
if (currentFocus.empty()) currentFocus = iter->first;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
void cDialog::recalcRect(){
|
|
bool haveRel = false;
|
|
winRect = rectangle();
|
|
for(ctrlIter iter = controls.begin(); iter != controls.end(); iter++) {
|
|
using namespace std::placeholders;
|
|
if(auto container = dynamic_cast<cContainer*>(iter->second))
|
|
container->forEach(std::bind(&cControl::recalcRect, _2));
|
|
iter->second->recalcRect();
|
|
rectangle frame = iter->second->getBounds();
|
|
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(iter->second->vert != POS_REL_NEG && frame.bottom > winRect.bottom)
|
|
winRect.bottom = frame.bottom;
|
|
}
|
|
winRect.right += 6;
|
|
winRect.bottom += 6;
|
|
if(haveRel) {
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
winRect.right *= ui_scale();
|
|
winRect.bottom *= ui_scale();
|
|
}
|
|
|
|
bool cDialog::initCalled = false;
|
|
|
|
void cDialog::init(){
|
|
cButton::init();
|
|
cLed::init();
|
|
cPict::init();
|
|
cScrollbar::init();
|
|
initCalled = true;
|
|
}
|
|
|
|
cDialog::~cDialog(){
|
|
ctrlIter iter = controls.begin();
|
|
while(iter != controls.end()){
|
|
delete iter->second;
|
|
iter++;
|
|
}
|
|
win.close();
|
|
}
|
|
|
|
bool cDialog::add(cControl* what, rectangle ctrl_frame, std::string key){
|
|
// First make sure the key is not already present.
|
|
// If it is, we can't add the control, so return false.
|
|
if(controls.find(key) != controls.end()) return false;
|
|
what->setBounds(ctrl_frame);
|
|
controls.insert(std::make_pair(key,what));
|
|
return true;
|
|
}
|
|
|
|
bool cDialog::remove(std::string key){
|
|
ctrlIter ctrl = controls.find(key);
|
|
if(ctrl == controls.end()) return false;
|
|
delete ctrl->second;
|
|
controls.erase(ctrl);
|
|
return true;
|
|
}
|
|
|
|
bool cDialog::sendInput(cKey key) {
|
|
if(topWindow == nullptr) return false;
|
|
std::string field = topWindow->currentFocus;
|
|
if(field.empty()) return true;
|
|
dynamic_cast<cTextField&>(topWindow->getControl(field)).handleInput(key);
|
|
return true;
|
|
}
|
|
|
|
void cDialog::run(std::function<void(cDialog&)> onopen){
|
|
cDialog* formerTop = topWindow;
|
|
// TODO: The introduction of the static topWindow means I may be able to use this instead of parent->win; do I still need parent?
|
|
sf::RenderWindow* parentWin = &(parent ? parent->win : mainPtr);
|
|
auto parentPos = parentWin->getPosition();
|
|
auto parentSz = parentWin->getSize();
|
|
cursor_type former_curs = Cursor::current;
|
|
dialogNotToast = true;
|
|
set_cursor(sword_curs);
|
|
sf::Event currentEvent;
|
|
// Focus the first text field, if there is one
|
|
if(!tabOrder.empty()) {
|
|
auto iter = std::find_if(tabOrder.begin(), tabOrder.end(), [](std::pair<std::string,cTextField*> ctrl){
|
|
return ctrl.second->isVisible();
|
|
});
|
|
if(iter != tabOrder.end()) {
|
|
iter->second->triggerFocusHandler(*this, iter->first, false);
|
|
currentFocus = iter->first;
|
|
}
|
|
}
|
|
// Sometimes it seems like the Cocoa menu handling clobbers the active rendering context.
|
|
// For whatever reason, delaying 100 milliseconds appears to fix this.
|
|
sf::sleep(sf::milliseconds(100));
|
|
// So this little section of code is a real-life valley of dying things.
|
|
// Instantiating a window and then closing it seems to fix the update error, because magic.
|
|
win.create(sf::VideoMode(1,1),"");
|
|
win.close();
|
|
win.create(sf::VideoMode(winRect.width(), winRect.height()), "Dialog", sf::Style::Titlebar);
|
|
winLastX = parentPos.x + (int(parentSz.x) - winRect.width()) / 2;
|
|
winLastY = parentPos.y + (int(parentSz.y) - winRect.height()) / 2;
|
|
win.setPosition({winLastX, winLastY});
|
|
draw();
|
|
makeFrontWindow(parent ? parent-> win : mainPtr);
|
|
makeFrontWindow(win);
|
|
// This is a loose modal session, as it doesn't prevent you from clicking away,
|
|
// but it does prevent editing other dialogs, and it also keeps this window on top
|
|
// even when it loses focus.
|
|
ModalSession dlog(win, *parentWin);
|
|
if(onopen) onopen(*this);
|
|
animTimer.restart();
|
|
|
|
handle_events();
|
|
|
|
win.setVisible(false);
|
|
while(parentWin->pollEvent(currentEvent));
|
|
set_cursor(former_curs);
|
|
topWindow = formerTop;
|
|
makeFrontWindow(*parentWin);
|
|
}
|
|
|
|
// This method is a main event event loop of the dialog.
|
|
void cDialog::handle_events() {
|
|
sf::Event currentEvent;
|
|
cFramerateLimiter fps_limiter;
|
|
|
|
while(dialogNotToast) {
|
|
while(win.pollEvent(currentEvent)) handle_one_event(currentEvent);
|
|
|
|
// Ideally, this should be the only draw call that is done in a cycle.
|
|
draw();
|
|
|
|
// Prevent the loop from executing too fast.
|
|
fps_limiter.frame_finished();
|
|
}
|
|
}
|
|
|
|
// This method handles one event received by the dialog.
|
|
void cDialog::handle_one_event(const sf::Event& currentEvent) {
|
|
using Key = sf::Keyboard::Key;
|
|
|
|
cKey key;
|
|
// HACK: This needs to be stored between consecutive invocations of this function
|
|
static cKey pendingKey = {true};
|
|
std::string itemHit = "";
|
|
location where;
|
|
|
|
if(kb.handleModifier(currentEvent)) return;
|
|
|
|
switch(currentEvent.type) {
|
|
case sf::Event::KeyPressed:
|
|
switch(currentEvent.key.code){
|
|
case Key::Up:
|
|
key.spec = true;
|
|
key.k = key_up;
|
|
break;
|
|
case Key::Right:
|
|
key.spec = true;
|
|
key.k = key_right;
|
|
break;
|
|
case Key::Left:
|
|
key.spec = true;
|
|
key.k = key_left;
|
|
break;
|
|
case Key::Down:
|
|
key.spec = true;
|
|
key.k = key_down;
|
|
break;
|
|
case Key::Escape:
|
|
key.spec = true;
|
|
key.k = key_esc;
|
|
break;
|
|
case Key::Return: // TODO: Also enter (keypad)
|
|
key.spec = true;
|
|
key.k = key_enter;
|
|
break;
|
|
case Key::BackSpace:
|
|
key.spec = true;
|
|
key.k = key_bsp;
|
|
break;
|
|
case Key::Delete:
|
|
key.spec = true;
|
|
key.k = key_del;
|
|
break;
|
|
case Key::Tab:
|
|
key.spec = true;
|
|
key.k = key_tab;
|
|
break;
|
|
case Key::Insert:
|
|
key.spec = true;
|
|
key.k = key_insert;
|
|
break;
|
|
case Key::F1:
|
|
key.spec = true;
|
|
key.k = key_help;
|
|
break;
|
|
case Key::Home:
|
|
key.spec = true;
|
|
key.k = key_home;
|
|
break;
|
|
case Key::End:
|
|
key.spec = true;
|
|
key.k = key_end;
|
|
break;
|
|
case Key::PageUp:
|
|
key.spec = true;
|
|
key.k = key_pgup;
|
|
break;
|
|
case Key::PageDown:
|
|
key.spec = true;
|
|
key.k = key_pgdn;
|
|
break;
|
|
default:
|
|
key.spec = false;
|
|
key.c = keyToChar(currentEvent.key.code, false);
|
|
break;
|
|
}
|
|
key.mod = mod_none;
|
|
if(currentEvent.key.*systemKey)
|
|
key.mod += mod_ctrl;
|
|
if(currentEvent.key.shift) key.mod += mod_shift;
|
|
if(currentEvent.key.alt) key.mod += mod_alt;
|
|
process_keystroke(key);
|
|
// Now check for focused fields.
|
|
if(currentFocus.empty()) break;
|
|
// If it's a tab, handle tab order
|
|
if(key.spec && key.k == key_tab) {
|
|
// Could use key.mod, but this is slightly easier.
|
|
if(currentEvent.key.shift) {
|
|
handleTabOrder(currentFocus, tabOrder.rbegin(), tabOrder.rend());
|
|
} else {
|
|
handleTabOrder(currentFocus, tabOrder.begin(), tabOrder.end());
|
|
}
|
|
} else {
|
|
// If it's a character key, and the system key (control/command) is not pressed,
|
|
// we have an upcoming TextEntered event which contains more information.
|
|
// Otherwise, handle it right away. But never handle enter or escape.
|
|
if((key.spec && key.k != key_enter && key.k != key_esc) || mod_contains(key.mod, mod_ctrl))
|
|
dynamic_cast<cTextField&>(getControl(currentFocus)).handleInput(key);
|
|
pendingKey = key;
|
|
if(key.k != key_enter && key.k != key_esc) itemHit = "";
|
|
}
|
|
break;
|
|
case sf::Event::TextEntered:
|
|
if(!pendingKey.spec && !currentFocus.empty()) {
|
|
pendingKey.c = currentEvent.text.unicode;
|
|
if(pendingKey.c != '\t')
|
|
dynamic_cast<cTextField&>(getControl(currentFocus)).handleInput(pendingKey);
|
|
}
|
|
break;
|
|
case sf::Event::MouseButtonPressed:
|
|
key.mod = mod_none;
|
|
if(kb.isCtrlPressed()) key.mod += mod_ctrl;
|
|
if(kb.isMetaPressed()) key.mod += mod_ctrl;
|
|
if(kb.isAltPressed()) key.mod += mod_alt;
|
|
if(kb.isShiftPressed()) key.mod += mod_shift;
|
|
where = {(int)(currentEvent.mouseButton.x / ui_scale()), (int)(currentEvent.mouseButton.y / ui_scale())};
|
|
process_click(where, key.mod);
|
|
break;
|
|
default: // To silence warning of unhandled enum values
|
|
break;
|
|
case sf::Event::GainedFocus:
|
|
case sf::Event::MouseMoved:
|
|
// Did the window move, potentially dirtying the canvas below it?
|
|
auto winPosition = win.getPosition();
|
|
if (winLastX != winPosition.x || winLastY != winPosition.y) {
|
|
if (redraw_everything != NULL)
|
|
redraw_everything();
|
|
}
|
|
winLastX = winPosition.x;
|
|
winLastY = winPosition.y;
|
|
|
|
bool inField = false;
|
|
for(auto& ctrl : controls) {
|
|
if(ctrl.second->getType() == CTRL_FIELD && ctrl.second->getBounds().contains(currentEvent.mouseMove.x, currentEvent.mouseMove.y)) {
|
|
set_cursor(text_curs);
|
|
inField = true;
|
|
break;
|
|
}
|
|
}
|
|
if(!inField) set_cursor(sword_curs);
|
|
break;
|
|
}
|
|
}
|
|
|
|
template<typename Iter> void cDialog::handleTabOrder(string& itemHit, Iter begin, Iter end) {
|
|
auto cur = find_if(begin, end, [&itemHit](pair<string,cTextField*>& a) {
|
|
return a.first == itemHit;
|
|
});
|
|
if(cur == end) return; // Unlikely, but let's be safe
|
|
if(!cur->second->triggerFocusHandler(*this,itemHit,true)) return;
|
|
string wasFocus = currentFocus;
|
|
auto iter = std::next(cur);
|
|
if(iter == end) iter = begin;
|
|
while(iter != cur){
|
|
// If tab order is explicitly specified for all fields, gaps are possible
|
|
if(iter->second == nullptr) continue;
|
|
if(iter->second->isFocusable() && iter->second->isVisible()){
|
|
if(iter->second->triggerFocusHandler(*this,iter->first,false)){
|
|
currentFocus = iter->first;
|
|
}
|
|
break;
|
|
}
|
|
iter++;
|
|
if(iter == end) iter = begin;
|
|
}
|
|
if(iter == cur) // no focus change occured!
|
|
currentFocus = wasFocus; // TODO: Surely something should happen here?
|
|
}
|
|
|
|
void cDialog::setBg(short n){
|
|
bg = n;
|
|
}
|
|
|
|
short cDialog::getBg() const {
|
|
return bg;
|
|
}
|
|
|
|
void cDialog::setDefTextClr(sf::Color clr){
|
|
defTextClr = clr;
|
|
}
|
|
|
|
sf::Color cDialog::getDefTextClr() const {
|
|
return defTextClr;
|
|
}
|
|
|
|
bool cDialog::toast(bool triggerFocus){
|
|
if(triggerFocus && !currentFocus.empty()) {
|
|
if(!this->getControl(currentFocus).triggerFocusHandler(*this, currentFocus, true)) return false;
|
|
}
|
|
dialogNotToast = false;
|
|
didAccept = triggerFocus;
|
|
return true;
|
|
}
|
|
|
|
void cDialog::untoast() {
|
|
dialogNotToast = true;
|
|
if(!currentFocus.empty())
|
|
this->getControl(currentFocus).triggerFocusHandler(*this, currentFocus, false);
|
|
}
|
|
|
|
bool cDialog::accepted() const {
|
|
return didAccept;
|
|
}
|
|
|
|
bool cDialog::setFocus(cTextField* newFocus, bool force) {
|
|
if(!force && !currentFocus.empty()) {
|
|
if(!this->getControl(currentFocus).triggerFocusHandler(*this, currentFocus, true)) return false;
|
|
}
|
|
if(newFocus == nullptr) {
|
|
currentFocus = "";
|
|
return true;
|
|
}
|
|
for(auto iter = controls.begin(); iter != controls.end(); iter++) {
|
|
if(iter->second == newFocus) {
|
|
if (!force && !newFocus->triggerFocusHandler(*this, iter->first, false)) return false;
|
|
currentFocus = iter->first;
|
|
return true;
|
|
}
|
|
if(iter->second->isContainer()) {
|
|
cContainer* tmp = dynamic_cast<cContainer*>(iter->second);
|
|
std::string foundKey = "";
|
|
tmp->forEach([newFocus, &foundKey](std::string key, cControl& child) {
|
|
if (&child == newFocus) {
|
|
foundKey = key;
|
|
}
|
|
});
|
|
if(!foundKey.empty()) {
|
|
if(!force && !newFocus->triggerFocusHandler(*this, foundKey, false)) return false;
|
|
currentFocus = foundKey;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
cTextField* cDialog::getFocus() {
|
|
if (currentFocus.empty()) return nullptr;
|
|
return dynamic_cast<cTextField*>(&getControl(currentFocus));
|
|
}
|
|
|
|
void cDialog::attachClickHandlers(std::function<bool(cDialog&,std::string,eKeyMod)> handler, std::vector<std::string> controls) {
|
|
cDialog& me = *this;
|
|
for(std::string control : controls) {
|
|
me[control].attachClickHandler(handler);
|
|
}
|
|
}
|
|
|
|
void cDialog::attachFocusHandlers(std::function<bool(cDialog&,std::string,bool)> handler, std::vector<std::string> controls) {
|
|
cDialog& me = *this;
|
|
for(std::string control : controls) {
|
|
me[control].attachFocusHandler(handler);
|
|
}
|
|
}
|
|
|
|
bool cDialog::addLabelFor(std::string key, std::string label, eLabelPos where, short offset, bool bold) {
|
|
cControl& ctrl = this->getControl(key);
|
|
key += "-label";
|
|
rectangle labelRect = ctrl.getBounds();
|
|
switch(where) {
|
|
case LABEL_LEFT:
|
|
labelRect.right = labelRect.left;
|
|
labelRect.left -= 2 * offset;
|
|
break;
|
|
case LABEL_ABOVE:
|
|
labelRect.bottom = labelRect.top;
|
|
labelRect.top -= 2 * offset;
|
|
break;
|
|
case LABEL_RIGHT:
|
|
labelRect.left = labelRect.right;
|
|
labelRect.right += 2 * offset;
|
|
break;
|
|
case LABEL_BELOW:
|
|
labelRect.top = labelRect.bottom;
|
|
labelRect.bottom += 2 * offset;
|
|
break;
|
|
}
|
|
if(labelRect.height() < 14){
|
|
int expand = (14 - labelRect.height()) / 2 + 1;
|
|
labelRect.inset(0, -expand);
|
|
} else labelRect.offset(0, labelRect.height() / 6);
|
|
cControl* labelCtrl;
|
|
auto iter = controls.find(key);
|
|
if(iter != controls.end()) {
|
|
labelCtrl = iter->second;
|
|
} else {
|
|
labelCtrl = new cTextMsg(*this);
|
|
}
|
|
labelCtrl->setText(label);
|
|
labelCtrl->setFormat(TXT_FONT, bold ? FONT_BOLD : FONT_PLAIN);
|
|
if(bg == BG_DARK && dynamic_cast<cButton*>(&ctrl) != nullptr)
|
|
labelCtrl->setColour(defTextClr);
|
|
else labelCtrl->setColour(ctrl.getColour());
|
|
ctrl.setLabelCtrl(labelCtrl);
|
|
return add(labelCtrl, labelRect, key);
|
|
}
|
|
|
|
void cDialog::process_keystroke(cKey keyHit){
|
|
ctrlIter iter = controls.begin();
|
|
while(iter != controls.end()){
|
|
if(iter->second->isVisible() && iter->second->isClickable() && iter->second->getAttachedKey() == keyHit){
|
|
iter->second->setActive(true);
|
|
draw();
|
|
iter->second->playClickSound();
|
|
iter->second->setActive(false);
|
|
draw();
|
|
sf::sleep(sf::milliseconds(8));
|
|
iter->second->triggerClickHandler(*this,iter->first,mod_none);
|
|
return;
|
|
}
|
|
iter++;
|
|
}
|
|
// If you get an escape and it isn't processed, make it an enter.
|
|
if(keyHit.spec && keyHit.k == key_esc){
|
|
keyHit.k = key_enter;
|
|
process_keystroke(keyHit);
|
|
}
|
|
}
|
|
|
|
void cDialog::process_click(location where, eKeyMod mods){
|
|
// TODO: Return list of all controls whose bounding rect contains the clicked point.
|
|
// Then the return value of the click handler can mean "Don't pass this event on to other things below me".
|
|
ctrlIter iter = controls.begin();
|
|
while(iter != controls.end()){
|
|
if(iter->second->isVisible() && iter->second->isClickable() && where.in(iter->second->getBounds())){
|
|
if(iter->second->handleClick(where))
|
|
break;
|
|
else return;
|
|
}
|
|
iter++;
|
|
}
|
|
if(iter != controls.end())
|
|
iter->second->triggerClickHandler(*this,iter->first,mods);
|
|
}
|
|
|
|
xBadNode::xBadNode(std::string t, int r, int c, std::string dlg) throw() :
|
|
type(t),
|
|
row(r),
|
|
col(c),
|
|
msg(nullptr),
|
|
dlg(dlg) {}
|
|
|
|
const char* xBadNode::what() const throw() {
|
|
if(msg == nullptr){
|
|
char* s = new (nothrow) char[200];
|
|
if(s == nullptr){
|
|
std::cerr << "Allocation of memory for error message failed, bailing out..." << std::endl;
|
|
abort();
|
|
}
|
|
sprintf(s,"XML Parse Error: Unknown element %s encountered (file %s, line %d, column %d).",type.c_str(),dlg.c_str(),row,col);
|
|
msg = s;
|
|
}
|
|
return msg;
|
|
}
|
|
|
|
xBadNode::~xBadNode() throw(){
|
|
if(msg != nullptr) delete[] msg;
|
|
}
|
|
|
|
xBadAttr::xBadAttr(std::string t, std::string n, int r, int c, std::string dlg) throw() :
|
|
type(t),
|
|
name(n),
|
|
row(r),
|
|
col(c),
|
|
msg(nullptr),
|
|
dlg(dlg) {}
|
|
|
|
const char* xBadAttr::what() const throw() {
|
|
if(msg == nullptr){
|
|
char* s = new (nothrow) char[200];
|
|
if(s == nullptr){
|
|
std::cerr << "Allocation of memory for error message failed, bailing out..." << std::endl;
|
|
abort();
|
|
}
|
|
sprintf(s,"XML Parse Error: Unknown attribute %s encountered on element %s (file %s, line %d, column %d).",name.c_str(),type.c_str(),dlg.c_str(),row,col);
|
|
msg = s;
|
|
}
|
|
return msg;
|
|
}
|
|
|
|
xBadAttr::~xBadAttr() throw(){
|
|
if(msg != nullptr) delete[] msg;
|
|
}
|
|
|
|
xMissingAttr::xMissingAttr(std::string t, std::string n, int r, int c, std::string dlg) throw() :
|
|
type(t),
|
|
name(n),
|
|
row(r),
|
|
col(c),
|
|
msg(nullptr),
|
|
dlg(dlg) {}
|
|
|
|
const char* xMissingAttr::what() const throw() {
|
|
if(msg == nullptr){
|
|
char* s = new (nothrow) char[200];
|
|
if(s == nullptr){
|
|
std::cerr << "Allocation of memory for error message failed, bailing out..." << std::endl;
|
|
abort();
|
|
}
|
|
sprintf(s,"XML Parse Error: Required attribute %s missing on element %s (file %s, line %d, column %d).",name.c_str(),type.c_str(),dlg.c_str(),row,col);
|
|
msg = s;
|
|
}
|
|
return msg;
|
|
}
|
|
|
|
xMissingAttr::~xMissingAttr() throw(){
|
|
if(msg != nullptr) delete[] msg;
|
|
}
|
|
|
|
xMissingElem::xMissingElem(std::string p, std::string t, int r, int c, std::string dlg) throw() :
|
|
parent(p),
|
|
name(t),
|
|
row(r),
|
|
col(c),
|
|
msg(nullptr),
|
|
dlg(dlg) {}
|
|
|
|
const char* xMissingElem::what() const throw() {
|
|
if(msg == nullptr){
|
|
char* s = new (nothrow) char[200];
|
|
if(s == nullptr){
|
|
std::cerr << "Allocation of memory for error message failed, bailing out..." << std::endl;
|
|
abort();
|
|
}
|
|
sprintf(s,"XML Parse Error: Required element %s missing in element %s (file %s, line %d, column %d).",name.c_str(),parent.c_str(),dlg.c_str(),row,col);
|
|
msg = s;
|
|
}
|
|
return msg;
|
|
}
|
|
|
|
xMissingElem::~xMissingElem() throw(){
|
|
if(msg != nullptr) delete[] msg;
|
|
}
|
|
|
|
xBadVal::xBadVal(std::string t, std::string n, std::string v, int r, int c, std::string dlg) throw() :
|
|
type(t),
|
|
name(n),
|
|
val(v),
|
|
row(r),
|
|
col(c),
|
|
msg(nullptr),
|
|
dlg(dlg) {}
|
|
|
|
const char* xBadVal::what() const throw() {
|
|
if(msg == nullptr){
|
|
char* s = new (nothrow) char[200];
|
|
if(s == nullptr){
|
|
std::cerr << "Allocation of memory for error message failed, bailing out..." << std::endl;
|
|
abort();
|
|
}
|
|
sprintf(s,"XML Parse Error: Invalid value %s for attribute %s encountered on element %s (file %s, line %d, column %d).",val.c_str(),name.c_str(),type.c_str(),dlg.c_str(),row,col);
|
|
msg = s;
|
|
}
|
|
return msg;
|
|
}
|
|
|
|
xBadVal::~xBadVal() throw(){
|
|
if(msg != nullptr) delete[] msg;
|
|
}
|
|
|
|
bool cDialog::doAnimations = false;
|
|
|
|
void cDialog::draw(){
|
|
win.setActive(false);
|
|
tileImage(win,winRect,::bg[bg]);
|
|
if(doAnimations && animTimer.getElapsedTime().asMilliseconds() >= 500) {
|
|
cPict::advanceAnim();
|
|
animTimer.restart();
|
|
}
|
|
|
|
// Scale dialogs:
|
|
sf::View view = win.getDefaultView();
|
|
view.setViewport(sf::FloatRect(0, 0, ui_scale(), ui_scale()));
|
|
win.setView(view);
|
|
|
|
ctrlIter iter = controls.begin();
|
|
while(iter != controls.end()){
|
|
iter->second->draw();
|
|
iter++;
|
|
}
|
|
|
|
win.setActive();
|
|
win.display();
|
|
}
|
|
|
|
cControl& cDialog::operator[](std::string id){
|
|
return getControl(id);
|
|
}
|
|
|
|
const cControl& cDialog::operator[](std::string id) const {
|
|
return const_cast<cDialog&>(*this).getControl(id);
|
|
}
|
|
|
|
cControl& cDialog::getControl(std::string id) {
|
|
ctrlIter iter = controls.find(id);
|
|
if(iter != controls.end()) return *(iter->second);
|
|
|
|
iter = controls.begin();
|
|
while(iter != controls.end()){
|
|
if(iter->second->isContainer()){
|
|
cContainer* tmp = dynamic_cast<cContainer*>(iter->second);
|
|
if(tmp->hasChild(id))
|
|
return tmp->getChild(id);
|
|
}
|
|
iter++;
|
|
}
|
|
throw std::invalid_argument(id + " does not exist in dialog " + fname);
|
|
}
|
|
|
|
bool cDialog::hasControl(std::string id) const {
|
|
auto iter = controls.find(id);
|
|
if(iter != controls.end()) return true;
|
|
|
|
iter = controls.begin();
|
|
while(iter != controls.end()){
|
|
if(iter->second->isContainer()){
|
|
cContainer* tmp = dynamic_cast<cContainer*>(iter->second);
|
|
if(tmp->hasChild(id))
|
|
return true;
|
|
}
|
|
iter++;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const char*const xBadVal::CONTENT = "<content>";
|
|
|
|
cDialogIterator::cDialogIterator() : parent(nullptr) {}
|
|
|
|
cDialogIterator::cDialogIterator(cDialog* parent) : parent(parent), current(parent->controls.begin()) {}
|
|
|
|
std::pair<std::string, cControl*>& cDialogIterator::dereference() const {
|
|
// Ugly fugly... especially the reinterpret_cast...
|
|
using value_type = std::pair<std::string, cControl*>;
|
|
if(children.empty()) return *reinterpret_cast<value_type*>(&*current);
|
|
return const_cast<value_type&>(children.front());
|
|
}
|
|
|
|
bool cDialogIterator::equal(const cDialogIterator& other) const {
|
|
if(parent == nullptr) return other.parent == nullptr;
|
|
return parent == other.parent && current == other.current && std::equal(children.begin(), children.end(), other.children.begin());
|
|
}
|
|
|
|
void cDialogIterator::increment() {
|
|
if(children.empty()) {
|
|
if(current->second->isContainer()) {
|
|
cContainer& box = dynamic_cast<cContainer&>(*current->second);
|
|
std::vector<std::pair<std::string, cControl*>> newChildren;
|
|
box.forEach([&newChildren](std::string id, cControl& ctrl) {
|
|
newChildren.emplace_back(id, &ctrl);
|
|
});
|
|
children.insert(children.begin(), newChildren.begin(), newChildren.end());
|
|
}
|
|
++current;
|
|
} else {
|
|
children.pop_front();
|
|
}
|
|
if(children.empty() && current == parent->controls.end()) {
|
|
parent = nullptr;
|
|
}
|
|
}
|