QtSpell  0.8.0
Spell checking for Qt text widgets
UndoRedoStack.cpp
1 /* QtSpell - Spell checking for Qt text widgets.
2  * Copyright (c) 2014 Sandro Mani
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along
15  * with this program; if not, write to the Free Software Foundation, Inc.,
16  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17  */
18 
19 #include "UndoRedoStack.hpp"
20 #include "TextEditChecker_p.hpp"
21 #include <QTextDocument>
22 
23 namespace QtSpell {
24 
25 struct UndoRedoStack::Action {
26  virtual ~Action(){}
27 };
28 
29 struct UndoRedoStack::UndoableInsert : public UndoRedoStack::Action {
30  QString text;
31  int pos;
32  bool isWhitespace;
33  bool isMergeable;
34 
35  UndoableInsert(int _pos, const QString& _text){
36  pos = _pos;
37  text = _text;
38  isWhitespace = text.length() == 1 && text[0].isSpace();
39  isMergeable = (text.length() == 1);
40  }
41 };
42 
43 struct UndoRedoStack::UndoableDelete : public UndoRedoStack::Action {
44  QString text;
45  int start, end;
46  bool deleteKeyUsed;
47  bool isWhitespace;
48  bool isMergeable;
49 
50  UndoableDelete(int _start, int _end, const QString& _text, bool _deleteKeyUsed){
51  start = _start;
52  end = _end;
53  text = _text;
54  deleteKeyUsed = _deleteKeyUsed;
55  isWhitespace = text.length() == 1 && text[0].isSpace();
56  isMergeable = (text.length() == 1);
57  }
58 };
59 
60 UndoRedoStack::UndoRedoStack(TextEditProxy* textEdit)
61  : m_actionInProgress(false), m_textEdit(textEdit)
62 {
63  // We need to keep undo/redo enabled to retreive the deleted text in onContentsChange...
64  if(m_textEdit){
65  m_textEdit->document()->setUndoRedoEnabled(true);
66  }
67 }
68 
69 void UndoRedoStack::clear()
70 {
71  qDeleteAll(m_undoStack);
72  qDeleteAll(m_redoStack);
73  m_undoStack.clear();
74  m_redoStack.clear();
75  emit undoAvailable(false);
76  emit redoAvailable(false);
77 }
78 
79 void UndoRedoStack::handleContentsChange(int pos, int removed, int added)
80 {
81  if(m_actionInProgress || (added == 0 && removed == 0)){
82  return;
83  }
84  // Qt Bug? Apparently, when contents is pasted at pos = 0, added and removed are too large by 1
85  QTextCursor c(m_textEdit->textCursor());
86  c.movePosition(QTextCursor::End);
87  int len = c.position();
88  if(pos == 0 && added > len){
89  --added;
90  --removed;
91  }
92  qDeleteAll(m_redoStack);
93  m_redoStack.clear();
94  if(removed > 0){
95  m_textEdit->document()->undo();
96  bool deleteWasUsed = (c.anchor() == c.position() && c.position() == pos);
97  c.setPosition(pos);
98  c.setPosition(pos + removed, QTextCursor::KeepAnchor);
99  UndoableDelete* undoAction = new UndoableDelete(pos, pos + removed, c.selectedText(), deleteWasUsed);
100  m_textEdit->document()->redo();
101  if(m_undoStack.empty() || !dynamic_cast<UndoableDelete*>(m_undoStack.top())){
102  m_undoStack.push(undoAction);
103  }else{
104  UndoableDelete* prevDelete = static_cast<UndoableDelete*>(m_undoStack.top());
105  if(deleteMergeable(prevDelete, undoAction)){
106  if(prevDelete->start == undoAction->start){ // Delete key used
107  prevDelete->text += undoAction->text;
108  prevDelete->end += (undoAction->end - undoAction->start);
109  }else{ // Backspace used
110  prevDelete->text = undoAction->text + prevDelete->text;
111  prevDelete->start = undoAction->start;
112  }
113  }else{
114  m_undoStack.push(undoAction);
115  }
116  }
117  }
118  if(added > 0){
119  QTextCursor c(m_textEdit->textCursor());
120  c.setPosition(pos);
121  c.setPosition(pos + added, QTextCursor::KeepAnchor);
122  UndoableInsert* undoAction = new UndoableInsert(pos, c.selectedText());
123  if(m_undoStack.empty() || !dynamic_cast<UndoableInsert*>(m_undoStack.top())){
124  m_undoStack.push(undoAction);
125  }else{
126  UndoableInsert* prevInsert = static_cast<UndoableInsert*>(m_undoStack.top());
127  if(insertMergeable(prevInsert, undoAction)){
128  prevInsert->text += undoAction->text;
129  }else{
130  m_undoStack.push(undoAction);
131  }
132  }
133  }
134  // We are only interested in the previous step for delete, no point in storing the rest
135  if(added > 0 || removed > 0){
136  m_textEdit->document()->clearUndoRedoStacks();
137  }
138  emit redoAvailable(false);
139  emit undoAvailable(true);
140 }
141 
142 void UndoRedoStack::undo()
143 {
144  if(m_undoStack.empty()){
145  return;
146  }
147  m_actionInProgress = true;
148  Action* undoAction = m_undoStack.pop();
149  m_redoStack.push(undoAction);
150  QTextCursor c(m_textEdit->textCursor());
151  if(dynamic_cast<UndoableInsert*>(undoAction)){
152  UndoableInsert* insertAction = static_cast<UndoableInsert*>(undoAction);
153  c.setPosition(insertAction->pos);
154  c.setPosition(insertAction->pos + insertAction->text.length(), QTextCursor::KeepAnchor);
155  c.removeSelectedText();
156  }else{
157  UndoableDelete* deleteAction = static_cast<UndoableDelete*>(undoAction);
158  c.setPosition(deleteAction->start);
159  c.insertText(deleteAction->text);
160  if(deleteAction->deleteKeyUsed){
161  c.setPosition(deleteAction->start);
162  }
163  }
164  m_textEdit->setTextCursor(c);
165  emit undoAvailable(!m_undoStack.empty());
166  emit redoAvailable(!m_redoStack.empty());
167  m_actionInProgress = false;
168 }
169 
170 void UndoRedoStack::redo()
171 {
172  if(m_redoStack.empty()){
173  return;
174  }
175  m_actionInProgress = true;
176  Action* redoAction = m_redoStack.top();
177  m_redoStack.pop();
178  m_undoStack.push(redoAction);
179  QTextCursor c(m_textEdit->textCursor());
180  if(dynamic_cast<UndoableInsert*>(redoAction)){
181  UndoableInsert* insertAction = static_cast<UndoableInsert*>(redoAction);
182  c.setPosition(insertAction->pos);
183  c.insertText(insertAction->text);
184  }else{
185  UndoableDelete* deleteAction = static_cast<UndoableDelete*>(redoAction);
186  c.setPosition(deleteAction->start);
187  c.setPosition(deleteAction->end, QTextCursor::KeepAnchor);
188  c.removeSelectedText();
189  }
190  m_textEdit->setTextCursor(c);
191  emit undoAvailable(!m_undoStack.empty());
192  emit redoAvailable(!m_redoStack.empty());
193  m_actionInProgress = false;
194 }
195 
196 bool UndoRedoStack::insertMergeable(const UndoableInsert* prev, const UndoableInsert* cur) const
197 {
198  return (cur->pos == prev->pos + prev->text.length()) &&
199  (cur->isWhitespace == prev->isWhitespace) &&
200  (cur->isMergeable && prev->isMergeable);
201 }
202 
203 bool UndoRedoStack::deleteMergeable(const UndoableDelete* prev, const UndoableDelete* cur) const
204 {
205  return (prev->deleteKeyUsed == cur->deleteKeyUsed) &&
206  (cur->isWhitespace == prev->isWhitespace) &&
207  (cur->isMergeable && prev->isMergeable) &&
208  (prev->start == cur->start || prev->start == cur->end);
209 }
210 
211 } // QtSpell
QtSpell namespace.
Definition: Checker.cpp:56