Implement undo/redo history for dialog text fields

- Also fixed a minor issue with pasting (the character before the insertion point was removed before pasting)
This commit is contained in:
2015-06-05 15:07:28 -04:00
parent 4fa22a34f1
commit d398bfaa80
5 changed files with 199 additions and 36 deletions

View File

@@ -58,6 +58,8 @@
2BF04B2C0BF51924006C0831 /* boe.startup.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2BF04B050BF51924006C0831 /* boe.startup.cpp */; };
2BF04B2D0BF51924006C0831 /* boe.text.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2BF04B070BF51924006C0831 /* boe.text.cpp */; };
2BF04B2E0BF51924006C0831 /* boe.town.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2BF04B090BF51924006C0831 /* boe.town.cpp */; };
91034D1D1B21DAC5008F01C1 /* undo.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 912283C80FD0E16C00B21642 /* undo.cpp */; };
91034D1F1B21DAC6008F01C1 /* undo.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 912283C80FD0E16C00B21642 /* undo.cpp */; };
9107074C18F1D18400F7BD7F /* scrollbar.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9191460018E63D8E005CF3A4 /* scrollbar.cpp */; };
9107074D18F1D18400F7BD7F /* scrollbar.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9191460018E63D8E005CF3A4 /* scrollbar.cpp */; };
9107074E18F1D18500F7BD7F /* scrollbar.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9191460018E63D8E005CF3A4 /* scrollbar.cpp */; };
@@ -1530,6 +1532,7 @@
914699011A747C6600F20F5E /* creature.cpp in Sources */,
91E30F2B1A74819C0057C54A /* fileio_party.cpp in Sources */,
91E30F2E1A7481C40057C54A /* fileio.cpp in Sources */,
91034D1F1B21DAC6008F01C1 /* undo.cpp in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1592,6 +1595,7 @@
91E30F311A748ABA0057C54A /* fileio_scen.cpp in Sources */,
9117A4111A7EC06700CD6EB4 /* living.cpp in Sources */,
91DBE9A81A873D3900ED006C /* specials_parse.cpp in Sources */,
91034D1D1B21DAC5008F01C1 /* undo.cpp in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@@ -373,6 +373,9 @@ void cTextField::handleInput(cKey key) {
style.pointSize = 12;
size_t new_ip;
std::string contents = getText();
if(current_action && hist_timer.getElapsedTime().asSeconds() > 5.0f)
history.add(current_action), current_action.reset();
hist_timer.restart();
if(!key.spec) {
if(haveSelection) {
cKey deleteKey = key;
@@ -381,12 +384,21 @@ void cTextField::handleInput(cKey key) {
handleInput(deleteKey);
contents = getText();
}
if(aTextInsert* ins = dynamic_cast<aTextInsert*>(current_action.get()))
ins->append(key.c);
else {
if(current_action) history.add(current_action);
aTextInsert* new_ins = new aTextInsert(*this, insertionPoint);
new_ins->append(key.c);
current_action.reset(new_ins);
}
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(current_action) history.add(current_action), current_action.reset();
if(haveSelection && !select) {
selectionPoint = insertionPoint = std::min(selectionPoint,insertionPoint);
break;
@@ -402,6 +414,7 @@ void cTextField::handleInput(cKey key) {
if(!select) selectionPoint = insertionPoint;
break;
case key_right: case key_word_right:
if(current_action) history.add(current_action), current_action.reset();
if(haveSelection && !select) {
selectionPoint = insertionPoint = std::max(selectionPoint,insertionPoint);
break;
@@ -417,6 +430,7 @@ void cTextField::handleInput(cKey key) {
if(!select) selectionPoint = insertionPoint;
break;
case key_up:
if(current_action) history.add(current_action), current_action.reset();
if(haveSelection && !select)
selectionPoint = insertionPoint = std::min(selectionPoint,insertionPoint);
if(snippets[ip_row].at.y == snippets[0].at.y) {
@@ -430,6 +444,7 @@ void cTextField::handleInput(cKey key) {
}
break;
case key_down:
if(current_action) history.add(current_action), current_action.reset();
if(haveSelection && !select)
selectionPoint = insertionPoint = std::max(selectionPoint,insertionPoint);
if(snippets[ip_row].at.y == snippets.back().at.y) {
@@ -443,13 +458,20 @@ void cTextField::handleInput(cKey key) {
}
break;
case key_bsp: case key_word_bsp:
case key_del: case key_word_del:
if(haveSelection) {
if(key.k == key_word_bsp)
handleInput({true, key_word_left, mod_shift});
else if(key.k == key_word_del)
handleInput({true, key_word_right, mod_shift});
auto begin = contents.begin() + std::min(selectionPoint, insertionPoint);
auto end = contents.begin() + std::max(selectionPoint, insertionPoint);
std::string removed(begin, end);
auto result = contents.erase(begin, end);
bool dir = insertionPoint < selectionPoint;
selectionPoint = insertionPoint = result - contents.begin();
if(current_action) history.add(current_action), current_action.reset();
history.add(action_ptr(new aTextDelete(*this, std::min(selectionPoint, insertionPoint), removed, dir)));
} else if(key.k == key_word_bsp) {
cKey selectKey = key;
selectKey.k = key_word_left;
@@ -459,20 +481,19 @@ void cTextField::handleInput(cKey key) {
if(selectionPoint != insertionPoint)
handleInput(key);
return;
} else {
} else if(key.k == key_bsp) {
if(insertionPoint == 0) break;
char c = contents[insertionPoint - 1];
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_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();
if(aTextDelete* del = dynamic_cast<aTextDelete*>(current_action.get()))
del->append_front(c);
else {
if(current_action) history.add(current_action);
aTextDelete* new_del = new aTextDelete(*this, insertionPoint + 1, insertionPoint + 1);
new_del->append_front(c);
current_action.reset(new_del);
}
} else if(key.k == key_word_del) {
cKey selectKey = key;
selectKey.k = key_word_right;
@@ -482,9 +503,18 @@ void cTextField::handleInput(cKey key) {
if(selectionPoint != insertionPoint)
handleInput(key);
return;
} else {
} else if(key.k == key_del) {
if(insertionPoint == contents.length()) break;
char c = contents[insertionPoint];
contents.erase(insertionPoint,1);
if(aTextDelete* del = dynamic_cast<aTextDelete*>(current_action.get()))
del->append_back(c);
else {
if(current_action) history.add(current_action);
aTextDelete* new_del = new aTextDelete(*this, insertionPoint, insertionPoint);
new_del->append_back(c);
current_action.reset(new_del);
}
}
break;
case key_top:
@@ -498,15 +528,18 @@ void cTextField::handleInput(cKey key) {
selectionPoint = contents.length();
break;
case key_end:
if(current_action) history.add(current_action), current_action.reset();
new_ip = snippets[ip_row].at.x + string_length(snippets[ip_row].text, style);
set_ip(loc(new_ip, snippets[ip_row].at.y), select ? &cTextField::selectionPoint : &cTextField::insertionPoint);
if(!select) selectionPoint = insertionPoint;
break;
case key_home:
if(current_action) history.add(current_action), current_action.reset();
set_ip(snippets[ip_row].at, select ? &cTextField::selectionPoint : &cTextField::insertionPoint);
if(!select) selectionPoint = insertionPoint;
break;
case key_pgup:
if(current_action) history.add(current_action), current_action.reset();
if(snippets[ip_row].at.y != snippets[0].at.y) {
int x = snippets[ip_row].at.x + ip_col, y = frame.top + 2;
set_ip(loc(x,y), select ? &cTextField::selectionPoint : &cTextField::insertionPoint);
@@ -514,6 +547,7 @@ void cTextField::handleInput(cKey key) {
}
break;
case key_pgdn:
if(current_action) history.add(current_action), current_action.reset();
if(snippets[ip_row].at.y != snippets.back().at.y) {
int x = snippets[ip_row].at.x + ip_col, y = frame.bottom - 2;
set_ip(loc(x,y), select ? &cTextField::selectionPoint : &cTextField::insertionPoint);
@@ -522,6 +556,7 @@ void cTextField::handleInput(cKey key) {
break;
case key_copy:
case key_cut:
if(current_action) history.add(current_action), current_action.reset();
set_clipboard(contents.substr(std::min(insertionPoint,selectionPoint), abs(insertionPoint - selectionPoint)));
if(key.k == key_cut) {
cKey deleteKey = key;
@@ -531,20 +566,30 @@ void cTextField::handleInput(cKey key) {
}
break;
case key_paste:
if(current_action) history.add(current_action), current_action.reset();
if(!get_clipboard().empty()) {
cKey deleteKey = {true, key_bsp, mod_none};
handleInput(deleteKey);
if(haveSelection) {
cKey deleteKey = {true, key_bsp, mod_none};
handleInput(deleteKey);
}
contents = getText();
std::string toInsert = get_clipboard();
contents.insert(insertionPoint, toInsert);
history.add(action_ptr(new aTextInsert(*this, insertionPoint, toInsert)));
insertionPoint += toInsert.length();
selectionPoint = insertionPoint;
}
break;
case key_undo:
if(current_action) history.add(current_action), current_action.reset();
history.undo();
return;
case key_redo:
break;
if(current_action) history.add(current_action), current_action.reset();
history.redo();
return;
case key_selectall:
if(current_action) history.add(current_action), current_action.reset();
selectionPoint = 0;
insertionPoint = contents.length();
break;
@@ -562,6 +607,59 @@ void cTextField::handleInput(cKey key) {
selectionPoint = sp;
}
aTextInsert::aTextInsert(cTextField& in, int at, std::string text) : cAction("insert text"), in(in), at(at), text(text) {}
void aTextInsert::undo() {
std::string contents = in.getText();
auto del_start = contents.begin() + at;
auto del_end = del_start + text.length();
auto result = contents.erase(del_start, del_end);
in.setText(contents);
in.selectionPoint = in.insertionPoint = result - contents.begin();
}
void aTextInsert::redo() {
std::string contents = in.getText();
contents.insert(at, text);
in.setText(contents);
in.selectionPoint = in.insertionPoint = at + text.length();
}
void aTextInsert::append(char c) {
text += c;
}
aTextDelete::aTextDelete(cTextField& in, int start, int end) : cAction("delete text"), in(in), start(start), end(end), ip(0) {}
aTextDelete::aTextDelete(cTextField& in, int start, std::string content, bool from_start) : cAction("delete text"), in(in), start(start), end(start + content.size()), text(content), ip(from_start ? 0 : content.size()) {}
void aTextDelete::undo() {
std::string contents = in.getText();
contents.insert(start, text);
in.setText(contents);
in.selectionPoint = in.insertionPoint = start + ip;
}
void aTextDelete::redo() {
std::string contents = in.getText();
auto del_start = contents.begin() + start;
auto del_end = contents.begin() + end;
auto result = contents.erase(del_start, del_end);
in.setText(contents);
in.selectionPoint = in.insertionPoint = result - contents.begin();
}
void aTextDelete::append_front(char c) {
text = c + text;
start--;
ip++;
}
void aTextDelete::append_back(char c) {
text += c;
end++;
}
cControl::storage_t cTextField::store() {
storage_t storage = cControl::store();
storage["fld-ip"] = insertionPoint;

View File

@@ -70,6 +70,8 @@ public:
long tabOrder = 0;
private:
void set_ip(location clickLoc, int cTextField::* insertionPoint);
cUndoList history;
action_ptr current_action;
eFldType field_type;
focus_callback_t onFocus;
bool haveFocus;
@@ -77,10 +79,37 @@ private:
int selectionPoint;
sf::Color color;
bool ip_visible;
sf::Clock ip_timer;
sf::Clock ip_timer, hist_timer;
bool changeMade = true;
rectangle text_rect;
std::vector<snippet_t> snippets;
int ip_row, ip_col;
friend class aTextInsert;
friend class aTextDelete;
};
class aTextInsert : public cAction {
cTextField& in;
int at;
std::string text;
public:
aTextInsert(cTextField& in, int at, std::string text = "");
void undo(), redo();
void append(char c);
~aTextInsert() {}
};
class aTextDelete : public cAction {
cTextField& in;
int start, end, ip;
std::string text;
public:
aTextDelete(cTextField& in, int start, int end);
aTextDelete(cTextField& in, int start, std::string content, bool from_start);
void undo(), redo();
void append_front(char c);
void append_back(char c);
~aTextDelete() {}
};
#endif

View File

@@ -8,23 +8,34 @@
#include "undo.hpp"
cAction::~cAction() {}
cUndoList::cUndoList(){
lastSave = cur = theList.begin();
}
size_t cUndoList::maxUndoSize = 0;
size_t cUndoList::maxUndoSize = 50;
// TODO: These functions should have error checking to ensure they do not access an out of bounds action
void cUndoList::undo(){
if(noUndo()) return;
(*cur)->undo();
cur--;
cur++;
}
void cUndoList::redo(){
cur++;
if(noRedo()) return;
cur--;
(*cur)->redo();
}
bool cUndoList::noUndo() {
return cur == theList.end();
}
bool cUndoList::noRedo() {
return cur == theList.begin();
}
void cUndoList::save(){
lastSave = cur;
}
@@ -33,8 +44,18 @@ void cUndoList::revert(){
while(cur != lastSave) undo();
}
void cUndoList::add(cAction* what){
theList.push_back(what);
num_actions++;
while(num_actions > maxUndoSize) theList.pop_front(), num_actions--;
void cUndoList::clear() {
theList.clear();
}
void cUndoList::add(action_ptr what){
if(!what) return;
theList.erase(theList.begin(), cur);
theList.push_front(what);
num_actions++;
while(num_actions > maxUndoSize) {
theList.pop_back();
num_actions--;
}
cur = theList.begin();
}

View File

@@ -10,27 +10,38 @@
*/
#include <list>
#include <memory>
#include <string>
class cAction {
std::string actname;
protected:
bool done = false;
public:
virtual void undo() = 0; // undoes this action if it has not already been undone
virtual void redo() = 0; // redoes this action if it has been undone
virtual bool isDone() = 0; // checks to see whether the action has been undone; returns false if it has
virtual std::string getActionName() = 0; // returns the name of this action for display in the Edit menu
cAction(std::string name) : actname(name) {}
virtual void undo() = 0; ///< Undoes this action if it has not already been undone
virtual void redo() = 0; ///< Redoes this action if it has been undone
bool isDone() {return done;}; ///< checks to see whether the action has been undone; returns false if it has
std::string getActionName() {return actname;} ///< returns the name of this action for display in the Edit menu
virtual ~cAction();
};
using action_ptr = std::shared_ptr<cAction>;
class cUndoList {
std::list<cAction*> theList;
std::list<cAction*>::iterator cur, lastSave;
size_t num_actions;
std::list<action_ptr> theList;
std::list<action_ptr>::iterator cur, lastSave;
size_t num_actions = 0;
public:
cUndoList();
void undo(); // undoes the current action and decrements the cur pointer
void redo(); // increments the cur pointer and redoes the current action
void save(); // sets the last saved action to the current action
void revert(); // undoes all actions back to (but excluding) the last saved action
void add(cAction* what);
void undo(); ///< Undoes the current action and decrements the cur pointer
void redo(); ///< Increments the cur pointer and redoes the current action
void save(); ///< Sets the last saved action to the current action
void revert(); ///< Undoes all actions back to (but excluding) the last saved action
void clear(); ///< Clears the list
bool noUndo(); ///< Check whether there's an action to undo
bool noRedo(); ///< Check whether there's an action to redo
void add(action_ptr what);
static size_t maxUndoSize;
};