Files
oboe/src/dialogxml/field.cpp
Celtic Minstrel 6a08476d22 Create dialog for editing large terrain object definitions
- Remove keybindings from terrain dialog arrow buttons
- Fix arrow buttons not bound-checking fields

Dialog engine:
- Fix fields crashing after text is set while it has focus
- Trigger focus handler on untoast() to balance that in toast()
2015-01-22 10:22:59 -05:00

514 lines
16 KiB
C++

/*
* field.cpp
* BoE
*
* Created by Celtic Minstrel on 11/05/09.
*
*/
#include "field.hpp"
#include <sstream>
#include <map>
#include <numeric>
#include <boost/lexical_cast.hpp>
#include "dialog.hpp"
#include "dlogutil.hpp"
#include "graphtool.hpp"
#include "winutil.hpp"
#include "cursors.hpp"
void cTextField::attachClickHandler(click_callback_t) throw(xHandlerNotSupported){
throw xHandlerNotSupported(false);
}
void cTextField::attachFocusHandler(focus_callback_t f) throw(){
onFocus = f;
}
bool cTextField::triggerFocusHandler(cDialog& me, std::string id, bool losingFocus){
if(losingFocus && field_type != FLD_TEXT) {
try {
std::string contents = getText();
switch(field_type) {
case FLD_TEXT: break;
case FLD_INT:
boost::lexical_cast<long long>(contents);
break;
case FLD_UINT:
boost::lexical_cast<unsigned long long>(contents);
break;
case FLD_REAL:
boost::lexical_cast<long double>(contents);
break;
}
} catch(boost::bad_lexical_cast) {
static const std::map<const eFldType, const std::string> typeNames = {
{FLD_INT, "an integer"},
{FLD_UINT, "a positive integer"},
{FLD_REAL, "a number"},
};
giveError("You need to enter " + typeNames.at(field_type) + "!","",parent);
return false;
}
}
bool passed = true;
if(onFocus != NULL) passed = onFocus(me,id,losingFocus);
if(passed) haveFocus = !losingFocus;
if(haveFocus && insertionPoint < 0)
insertionPoint = getText().length();
return passed;
}
void cTextField::setText(std::string to) {
cControl::setText(to);
if(haveFocus)
insertionPoint = to.length();
else insertionPoint = -1;
selectionPoint = 0;
}
void cTextField::set_ip(location clickLoc, int cTextField::* insertionPoint) {
TextStyle style;
style.font = FONT_PLAIN;
style.pointSize = 12;
style.colour = sf::Color::Black;
style.lineHeight = 16;
size_t foundSnippet = snippets.size();
// Find snippet clicked.
for(size_t i = 0; i < snippets.size(); i++) {
short h, w = string_length(snippets[i].text, style, &h);
rectangle snipRect;
snipRect.top = snippets[i].at.y;
snipRect.left = snippets[i].at.x;
snipRect.width() = w;
snipRect.height() = h;
if(snipRect.contains(clickLoc)) {
foundSnippet = i;
break;
}
}
if(foundSnippet < snippets.size()) {
sf::Text snippet;
style.applyTo(snippet);
snippet.setString(snippets[foundSnippet].text);
snippet.setPosition(snippets[foundSnippet].at);
size_t charClicked = snippets[foundSnippet].text.length();
// Find character clicked. By now we know the Y position is okay, so just check X.
for(size_t i = 0; i < snippets[foundSnippet].text.length(); i++) {
if(clickLoc.x > snippet.findCharacterPos(i).x) charClicked = i;
else break;
}
if(charClicked < snippets[foundSnippet].text.length()) {
size_t pre_ip = std::accumulate(snippets.begin(), snippets.begin() + foundSnippet, 0, [](size_t sum, snippet_t& next) -> size_t {
return sum + next.text.length();
});
int left = snippet.findCharacterPos(charClicked).x;
int right;
if(charClicked + 1 == snippets[foundSnippet].text.length())
right = rectangle(snippet.getGlobalBounds()).right;
else right = snippet.findCharacterPos(charClicked + 1).x;
left = clickLoc.x - left;
right -= clickLoc.x;
if(left < right) this->*insertionPoint = pre_ip + charClicked;
else this->*insertionPoint = pre_ip + charClicked + 1;
}
}
}
bool cTextField::handleClick(location clickLoc) {
// TODO: Set the insertion point, handle selection, etc
if(!haveFocus && parent && !parent->setFocus(this)) return true;
haveFocus = true;
redraw(); // This ensures the snippets array is populated.
bool is_shift = sf::Keyboard::isKeyPressed(sf::Keyboard::LShift) || sf::Keyboard::isKeyPressed(sf::Keyboard::RShift);
set_ip(clickLoc, is_shift ? &cTextField::selectionPoint : &cTextField::insertionPoint);
if(!is_shift) selectionPoint = insertionPoint;
bool done = false;
sf::Event e;
while(!done) {
redraw();
if(!inWindow->pollEvent(e)) continue;
if(e.type == sf::Event::MouseButtonReleased){
done = true;
} else if(e.type == sf::Event::MouseMoved){
restore_cursor();
location newLoc(e.mouseMove.x, e.mouseMove.y);
set_ip(newLoc, &cTextField::selectionPoint);
}
}
redraw();
return true;
}
void cTextField::setFormat(eFormat prop, short) throw(xUnsupportedProp){
throw xUnsupportedProp(prop);
}
short cTextField::getFormat(eFormat prop) throw(xUnsupportedProp){
throw xUnsupportedProp(prop);
}
void cTextField::setColour(sf::Color clr) throw(xUnsupportedProp) {
color = clr;
}
sf::Color cTextField::getColour() throw(xUnsupportedProp) {
return color;
}
eFldType cTextField::getInputType() {
return field_type;
}
void cTextField::setInputType(eFldType type) {
field_type = type;
}
bool cTextField::isClickable(){
return true;
}
bool cTextField::hasFocus() {
return haveFocus;
}
cTextField::cTextField(cDialog* parent) :
cControl(CTRL_FIELD,*parent),
color(sf::Color::Black),
insertionPoint(-1),
selectionPoint(0),
haveFocus(false),
field_type(FLD_TEXT) {}
cTextField::~cTextField(){}
void cTextField::draw(){
if(!visible) return;
static const sf::Color hiliteClr = {127,127,127}, ipClr = {92, 92, 92};
inWindow->setActive();
rectangle outline = frame;
outline.inset(-2,-2);
fill_rect(*inWindow, outline, sf::Color::White);
frame_rect(*inWindow, outline, sf::Color::Black);
std::string contents = getText();
rectangle rect = frame;
rect.inset(2,2);
TextStyle style;
style.font = FONT_PLAIN;
style.pointSize = 12;
style.colour = sf::Color::Black;
style.lineHeight = 16;
size_t ip_offset = 0;
hilite_t hilite = {insertionPoint, selectionPoint};
if(selectionPoint < insertionPoint) std::swap(hilite.first,hilite.second);
if(haveFocus && contents.length() > 1) {
// Determine which line the insertion and selection points are on
clip_rect(*inWindow, {0,0,0,0}); // To prevent drawing
hilite_t tmp_hilite = hilite;
// Manipulate this to ensure that there is a hilited area
std::string dummy_str = contents + " ";
if(tmp_hilite.first >= tmp_hilite.second)
tmp_hilite.second = tmp_hilite.first + 1;
std::vector<rectangle> rects = draw_string_hilite(*inWindow, rect, dummy_str, style, {tmp_hilite}, {0,0,0});
if(!rects.empty()) {
// We only care about the first and last rects. Furthermore, we only really need one point
location ip_pos = rects[0].centre(), sp_pos = rects[rects.size() - 1].centre();
if(selectionPoint < insertionPoint) std::swap(ip_pos, sp_pos);
// Prioritize selection point being visible. If possible, also keep insertion point visible.
// We do this by first ensuring the insertion point is visible, then doing the same
// for the selection point.
while(!ip_pos.in(frame)) {
rect.offset(0,-14);
ip_pos.y -= 14;
sp_pos.y -= 14;
}
while(!sp_pos.in(frame)) {
int shift = selectionPoint < insertionPoint ? 14 : -14;
rect.offset(0,shift);
ip_pos.y += shift;
sp_pos.y += shift;
}
}
undo_clip(*inWindow);
}
clip_rect(*inWindow, frame);
if(haveFocus) {
snippets = draw_string_sel(*inWindow, rect, contents, style, {hilite}, hiliteClr);
int iSnippet = -1, sum = 0;
ip_offset = insertionPoint;
for(size_t i = 0; i < snippets.size(); i++) {
size_t snippet_len = snippets[i].text.length();
sum += snippet_len;
if(sum >= insertionPoint) {
iSnippet = i;
break;
}
ip_offset -= snippet_len;
}
std::string pre_ip = iSnippet >= 0 ? snippets[iSnippet].text.substr(0, ip_offset) : "";
ip_offset = string_length(pre_ip, style);
if(ip_timer.getElapsedTime().asMilliseconds() < 500) {
// printf("Blink on (%d); ", ip_timer.getElapsedTime().asMilliseconds());
rectangle ipRect = {0, 0, 15, 1};
if(iSnippet >= 0)
ipRect.offset(snippets[iSnippet].at.x + ip_offset, snippets[iSnippet].at.y + 1);
else ipRect.offset(frame.topLeft()), ipRect.offset(3,2);
fill_rect(*inWindow, ipRect, ipClr);
} else if(ip_timer.getElapsedTime().asMilliseconds() > 1000) {
// printf("Blink off (%d); ", ip_timer.getElapsedTime().asMilliseconds());
ip_timer.restart();
}
} else win_draw_string(*inWindow, rect, contents, eTextMode::WRAP, style);
undo_clip(*inWindow);
}
static cKey divineFunction(cKey key) {
/* Summary of platform-dependent navigation/edit keys:
Function | Mac | Windows
-----------+-------------------+-------------
Home | Home, Cmd-Left | Home
End | End, Cmd-Right | End
PgUp | PgUp, Alt-Up | PgUp
PgDn | PgDn, Alt-Down | PgDn
To Top | Cmd-Up, Cmd-Home| Ctrl-Up, Ctrl-Home
To Bottom | Cmd-Down, Cmd-End | Ctrl-Down, Ctrl-End
Word Left | Alt-Left | Ctrl-Left
Word Right | Alt-Right | Ctrl-Right
Word Del | Alt-Delete | Ctrl-Delete
Word Back | Alt-Backspace | Ctrl-Backspace
-----------+-------------------+----------------------
Cut | Cmd-X, Shift-Del | Ctrl-X, Shift-Del
Copy | Cmd-C, Cmd-Ins | Ctrl-C, Ctrl-Ins
Paste | Cmd-V, Shift-Ins | Ctrl-V, Shift-Ins
Select All | Cmd-A | Ctrl-A
Undo | Cmd-Z | Ctrl-Z
Redo | Cmd-Y, Shift-Cmd-Z| Ctrl-Y, Shift-Ctrl-Z
This is done to more closely emulate native Mac behaviour.
The Insert and Shift-Delete combos are included to more closely emulate
native Windows behaviour.
*/
if(!key.spec) {
if(mod_contains(key.mod, mod_ctrl)) {
if(key.c == 'c') {
key.spec = true;
key.k = key_copy;
} else if(key.c == 'x') {
key.spec = true;
key.k = key_cut;
} else if(key.c == 'v') {
key.spec = true;
key.k = key_paste;
} else if(key.c == 'a') {
key.spec = true;
key.k = key_selectall;
} else if(key.c == 'z') {
key.spec = true;
if(mod_contains(key.mod, mod_shift)) {
key.k = key_redo;
key.mod -= mod_shift;
} else key.k = key_undo;
} else if(key.c == 'y') {
key.spec = true;
key.k = key_redo;
}
}
if(key.spec) key.mod -= mod_ctrl;
} else {
eSpecKey former = key.k;
if(mod_contains(key.mod, mod_ctrl)) {
#ifdef __APPLE__
if(key.k == key_left) key.k = key_home;
else if(key.k == key_right) key.k = key_end;
#else
if(key.k == key_left) key.k = key_word_left;
else if(key.k == key_right) key.k = key_word_right;
#endif
else if(key.k == key_up) key.k = key_top;
else if(key.k == key_down) key.k = key_bottom;
else if(key.k == key_home) key.k = key_top;
else if(key.k == key_end) key.k = key_bottom;
else if(key.k == key_insert) key.k = key_copy;
#ifndef __APPLE__
else if(key.k == key_del) key.k = key_word_del;
else if(key.k == key_bsp) key.k = key_word_bsp;
#else
} else if(mod_contains(key.mod, mod_alt)) {
if(key.k == key_up) key.k = key_pgup;
else if(key.k == key_down) key.k = key_pgdn;
else if(key.k == key_left) key.k = key_word_left;
else if(key.k == key_right) key.k = key_word_right;
else if(key.k == key_del) key.k = key_word_del;
else if(key.k == key_bsp) key.k = key_word_bsp;
#endif
} else if(mod_contains(key.mod, mod_shift)) {
if(key.k == key_insert) key.k = key_paste;
else if(key.k == key_del) key.k = key_cut;
}
if(key.k != former) key.mod -= mod_ctrl;
}
return key;
}
void cTextField::handleInput(cKey key) {
bool select = mod_contains(key.mod, mod_shift);
bool haveSelection = insertionPoint != selectionPoint;
key = divineFunction(key);
size_t new_ip;
std::string contents = getText();
if(!key.spec) {
if(haveSelection) {
cKey deleteKey = key;
deleteKey.spec = true;
deleteKey.k = key_bsp;
handleInput(deleteKey);
contents = getText();
}
contents.insert(contents.begin() + insertionPoint, char(key.c));
selectionPoint = ++insertionPoint;
} else switch(key.k) {
case key_enter: break; // Shouldn't be receiving this anyway
// TODO: Implement all the other special keys
case key_left: case key_word_left:
if(haveSelection && !select) {
selectionPoint = insertionPoint = std::min(selectionPoint,insertionPoint);
break;
}
new_ip = select ? selectionPoint : insertionPoint;
if(new_ip == 0) break;
if(key.k == key_word_left) {
new_ip--;
while(new_ip > 0 && contents[new_ip - 1] != ' ')
new_ip--;
} else new_ip--;
(select ? selectionPoint : insertionPoint) = new_ip;
if(!select) selectionPoint = insertionPoint;
break;
case key_right: case key_word_right:
if(haveSelection && !select) {
selectionPoint = insertionPoint = std::max(selectionPoint,insertionPoint);
break;
}
new_ip = select ? selectionPoint : insertionPoint;
if(new_ip == contents.length()) break;
if(key.k == key_word_right) {
new_ip++;
while(new_ip < contents.length() && contents[new_ip + 1] != ' ')
new_ip++;
} else new_ip++;
(select ? selectionPoint : insertionPoint) = new_ip;
if(!select) selectionPoint = insertionPoint;
break;
case key_up:
case key_down:
break;
case key_bsp: case key_word_bsp:
if(haveSelection) {
if(key.k == key_word_bsp)
handleInput({true, key_word_right, mod_shift});
auto begin = contents.begin() + std::min(selectionPoint, insertionPoint);
auto end = contents.begin() + std::max(selectionPoint, insertionPoint);
auto result = contents.erase(begin, end);
selectionPoint = insertionPoint = result - contents.begin();
} else if(key.k == key_word_bsp) {
cKey selectKey = key;
selectKey.k = key_left;
handleInput(selectKey);
if(selectionPoint != insertionPoint)
handleInput(key);
} else {
if(insertionPoint == 0) break;
contents.erase(insertionPoint - 1,1);
selectionPoint = --insertionPoint;
}
break;
case key_del: case key_word_del:
if(haveSelection) {
if(key.k == key_word_del)
handleInput({true, key_word_left, mod_shift});
auto begin = contents.begin() + std::min(selectionPoint, insertionPoint);
auto end = contents.begin() + std::max(selectionPoint, insertionPoint);
auto result = contents.erase(begin, end);
selectionPoint = insertionPoint = result - contents.begin();
} else if(key.k == key_word_del) {
cKey selectKey = key;
selectKey.k = key_left;
handleInput(selectKey);
if(selectionPoint != insertionPoint)
handleInput(key);
} else {
if(insertionPoint == contents.length()) break;
contents.erase(insertionPoint,1);
}
break;
case key_top:
if(select) selectionPoint = 0;
else insertionPoint = 0;
break;
case key_bottom:
if(select) selectionPoint = contents.length();
else insertionPoint = contents.length();
break;
case key_end:
case key_home:
case key_pgup:
case key_pgdn:
break;
case key_copy:
case key_cut:
set_clipboard(contents.substr(std::min(insertionPoint,selectionPoint), abs(insertionPoint - selectionPoint)));
if(key.k == key_cut) {
cKey deleteKey = key;
deleteKey.k = key_bsp;
handleInput(deleteKey);
contents = getText();
}
break;
case key_paste:
if(!get_clipboard().empty()) {
cKey deleteKey = {true, key_bsp, mod_none};
handleInput(deleteKey);
contents = getText();
std::string toInsert = get_clipboard();
contents.insert(insertionPoint, toInsert);
insertionPoint += toInsert.length();
selectionPoint = insertionPoint;
}
break;
case key_undo:
case key_redo:
break;
case key_selectall:
selectionPoint = 0;
insertionPoint = contents.length();
break;
// These keys have no function in this context.
case key_esc:
case key_tab:
case key_help:
case key_insert:
break;
}
// Setting the text normally resets insertion/selection point, but we don't want that here.
int ip = insertionPoint, sp = selectionPoint;
setText(contents);
insertionPoint = ip;
selectionPoint = sp;
}
cControl::storage_t cTextField::store() {
storage_t storage = cControl::store();
storage["fld-ip"] = insertionPoint;
storage["fld-sp"] = selectionPoint;
return storage;
}
void cTextField::restore(storage_t to) {
cControl::restore(to);
if(to.find("fld-ip") != to.end())
insertionPoint = boost::any_cast<int>(to["fld-ip"]);
else insertionPoint = getText().length();
if(to.find("fld-sp") != to.end())
selectionPoint = boost::any_cast<int>(to["fld-sp"]);
else selectionPoint = 0;
}