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:
@@ -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;
|
||||
};
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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;
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user