Skip to content

Commit 24f612f

Browse files
committed
Add Simple Undo Redo feature in the example app
1 parent dcf1443 commit 24f612f

6 files changed

Lines changed: 242 additions & 7 deletions

File tree

app/src/main/java/com/amrdeveloper/codeviewlibrary/MainActivity.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.amrdeveloper.codeview.CodeView;
2121
import com.amrdeveloper.codeviewlibrary.plugin.CommentManager;
2222
import com.amrdeveloper.codeviewlibrary.plugin.SourcePositionListener;
23+
import com.amrdeveloper.codeviewlibrary.plugin.UndoRedoManager;
2324
import com.amrdeveloper.codeviewlibrary.syntax.ThemeName;
2425
import com.amrdeveloper.codeviewlibrary.syntax.LanguageName;
2526
import com.amrdeveloper.codeviewlibrary.syntax.LanguageManager;
@@ -34,6 +35,7 @@ public class MainActivity extends AppCompatActivity {
3435
private CodeView codeView;
3536
private LanguageManager languageManager;
3637
private CommentManager commentManager;
38+
private UndoRedoManager undoRedoManager;
3739

3840
private TextView languageNameText;
3941
private TextView sourcePositionText;
@@ -124,6 +126,9 @@ private void configCodeViewPlugins() {
124126
commentManager = new CommentManager(codeView);
125127
configCommentInfo();
126128

129+
undoRedoManager = new UndoRedoManager(codeView);
130+
undoRedoManager.connect();
131+
127132
languageNameText = findViewById(R.id.language_name_txt);
128133
configLanguageName();
129134

@@ -165,6 +170,8 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
165170
else if (menuItemId == R.id.comment) commentManager.commentSelected();
166171
else if (menuItemId == R.id.un_comment) commentManager.unCommentSelected();
167172
else if (menuItemId == R.id.clearText) codeView.setText("");
173+
else if (menuItemId == R.id.undo) undoRedoManager.undo();
174+
else if (menuItemId == R.id.redo) undoRedoManager.redo();
168175

169176
return super.onOptionsItemSelected(item);
170177
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package com.amrdeveloper.codeviewlibrary.plugin;
2+
3+
import android.text.Editable;
4+
import android.text.Selection;
5+
import android.text.TextUtils;
6+
import android.text.TextWatcher;
7+
import android.text.style.UnderlineSpan;
8+
import android.widget.TextView;
9+
10+
import java.util.LinkedList;
11+
12+
public class UndoRedoManager {
13+
14+
private final TextView textView;
15+
private final EditHistory editHistory;
16+
private final TextChangeWatcher textChangeWatcher;
17+
18+
private boolean isUndoOrRedo = false;
19+
20+
public UndoRedoManager(TextView textView) {
21+
this.textView = textView;
22+
editHistory = new EditHistory();
23+
textChangeWatcher = new TextChangeWatcher();
24+
}
25+
26+
public void undo() {
27+
EditNode edit = editHistory.getPrevious();
28+
if (edit == null) return;
29+
30+
Editable text = textView.getEditableText();
31+
int start = edit.start;
32+
int end = start + (edit.after != null ? edit.after.length() : 0);
33+
34+
isUndoOrRedo = true;
35+
text.replace(start, end, edit.before);
36+
isUndoOrRedo = false;
37+
38+
UnderlineSpan[] underlineSpans = text.getSpans(0, text.length(), UnderlineSpan.class);
39+
for (Object span : underlineSpans) text.removeSpan(span);
40+
41+
Selection.setSelection(text, edit.before == null ? start : (start + edit.before.length()));
42+
}
43+
44+
public void redo() {
45+
EditNode edit = editHistory.getNext();
46+
if (edit == null) return;
47+
48+
Editable text = textView.getEditableText();
49+
int start = edit.start;
50+
int end = start + (edit.before != null ? edit.before.length() : 0);
51+
52+
isUndoOrRedo = true;
53+
text.replace(start, end, edit.after);
54+
isUndoOrRedo = false;
55+
56+
UnderlineSpan[] underlineSpans = text.getSpans(0, text.length(), UnderlineSpan.class);
57+
for (Object span : underlineSpans) text.removeSpan(span);
58+
59+
Selection.setSelection(text, edit.after == null ? start : (start + edit.after.length()));
60+
}
61+
62+
public void connect() {
63+
textView.addTextChangedListener(textChangeWatcher);
64+
}
65+
66+
public void disconnect() {
67+
textView.removeTextChangedListener(textChangeWatcher);
68+
}
69+
70+
public void setMaxHistorySize(int maxSize) {
71+
editHistory.setMaxHistorySize(maxSize);
72+
}
73+
74+
public void clearHistory() {
75+
editHistory.clear();
76+
}
77+
78+
public boolean canUndo() {
79+
return editHistory.position > 0;
80+
}
81+
82+
public boolean canRedo() {
83+
return editHistory.position < editHistory.historyList.size();
84+
}
85+
86+
private static final class EditHistory {
87+
88+
private int position = 0;
89+
private int maxHistorySize = -1;
90+
91+
private final LinkedList<EditNode> historyList = new LinkedList<>();
92+
93+
private void clear() {
94+
position = 0;
95+
historyList.clear();
96+
}
97+
98+
private void add(EditNode item) {
99+
while (historyList.size() > position) historyList.removeLast();
100+
historyList.add(item);
101+
position++;
102+
if (maxHistorySize >= 0) trimHistory();
103+
}
104+
105+
private void setMaxHistorySize(int maxHistorySize) {
106+
this.maxHistorySize = maxHistorySize;
107+
if (this.maxHistorySize >= 0) trimHistory();
108+
}
109+
110+
private void trimHistory() {
111+
while (historyList.size() > maxHistorySize) {
112+
historyList.removeFirst();
113+
position--;
114+
}
115+
116+
if (position < 0) position = 0;
117+
}
118+
119+
private EditNode getCurrent() {
120+
if (position == 0) return null;
121+
return historyList.get(position - 1);
122+
}
123+
124+
private EditNode getPrevious() {
125+
if (position == 0) return null;
126+
position--;
127+
return historyList.get(position);
128+
}
129+
130+
private EditNode getNext() {
131+
if (position >= historyList.size()) return null;
132+
EditNode item = historyList.get(position);
133+
position++;
134+
return item;
135+
}
136+
}
137+
138+
private static final class EditNode {
139+
140+
private int start;
141+
private CharSequence before;
142+
private CharSequence after;
143+
144+
public EditNode(int start, CharSequence before, CharSequence after) {
145+
this.start = start;
146+
this.before = before;
147+
this.after = after;
148+
}
149+
}
150+
151+
private enum ActionType {
152+
INSERT, DELETE, PASTE, NOT_DEF;
153+
}
154+
155+
private final class TextChangeWatcher implements TextWatcher {
156+
157+
private CharSequence beforeChange;
158+
private CharSequence afterChange;
159+
private ActionType lastActionType = ActionType.NOT_DEF;
160+
161+
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
162+
if (isUndoOrRedo) return;
163+
beforeChange = s.subSequence(start, start + count);
164+
}
165+
166+
public void onTextChanged(CharSequence s, int start, int before, int count) {
167+
if (isUndoOrRedo) return;
168+
afterChange = s.subSequence(start, start + count);
169+
makeBatch(start);
170+
}
171+
172+
private void makeBatch(int start) {
173+
ActionType action = getActionType();
174+
EditNode currentNode = editHistory.getCurrent();
175+
if (lastActionType != action || ActionType.PASTE == action || currentNode == null) {
176+
editHistory.add(new EditNode(start, beforeChange, afterChange));
177+
} else {
178+
if (action == ActionType.DELETE) {
179+
currentNode.start = start;
180+
currentNode.before = TextUtils.concat(beforeChange, currentNode.before);
181+
} else {
182+
currentNode.after = TextUtils.concat(currentNode.after, afterChange);
183+
}
184+
}
185+
lastActionType = action;
186+
}
187+
188+
private ActionType getActionType() {
189+
if (!TextUtils.isEmpty(beforeChange) && TextUtils.isEmpty(afterChange)) {
190+
return ActionType.DELETE;
191+
} else if (TextUtils.isEmpty(beforeChange) && !TextUtils.isEmpty(afterChange)) {
192+
return ActionType.INSERT;
193+
} else {
194+
return ActionType.PASTE;
195+
}
196+
}
197+
198+
public void afterTextChanged(Editable s) {
199+
}
200+
}
201+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<vector android:height="24dp" android:tint="#FFFFFF"
2+
android:viewportHeight="24" android:viewportWidth="24"
3+
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
4+
<path android:fillColor="@android:color/white" android:pathData="M18.4,10.6C16.55,8.99 14.15,8 11.5,8c-4.65,0 -8.58,3.03 -9.96,7.22L3.9,16c1.05,-3.19 4.05,-5.5 7.6,-5.5 1.95,0 3.73,0.72 5.12,1.88L13,16h9V7l-3.6,3.6z"/>
5+
</vector>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<vector android:height="24dp" android:tint="#FFFFFF"
2+
android:viewportHeight="24" android:viewportWidth="24"
3+
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
4+
<path android:fillColor="@android:color/white" android:pathData="M12.5,8c-2.65,0 -5.05,0.99 -6.9,2.6L2,7v9h9l-3.62,-3.62c1.39,-1.16 3.16,-1.88 5.12,-1.88 3.54,0 6.55,2.31 7.6,5.5l2.37,-0.78C21.08,11.03 17.15,8 12.5,8z"/>
5+
</vector>

app/src/main/res/menu/menu_main.xml

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<menu xmlns:android="http://schemas.android.com/apk/res/android">
2+
<menu xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:app="http://schemas.android.com/apk/res-auto">
34

45
<item
5-
android:id="@+id/languages"
6+
android:id="@+id/undo"
7+
android:icon="@drawable/ic_undo"
8+
android:title="@string/undo"
69
android:orderInCategory="1"
10+
app:showAsAction="ifRoom" />
11+
12+
<item
13+
android:id="@+id/redo"
14+
android:icon="@drawable/ic_redo"
15+
android:title="@string/undo"
16+
android:orderInCategory="2"
17+
app:showAsAction="ifRoom" />
18+
19+
<item
20+
android:id="@+id/languages"
21+
android:orderInCategory="3"
722
android:title="@string/languages">
823
<menu>
924
<group android:id="@+id/group_languages">
@@ -25,7 +40,7 @@
2540

2641
<item
2742
android:id="@+id/themes"
28-
android:orderInCategory="2"
43+
android:orderInCategory="4"
2944
android:title="@string/themes">
3045
<menu>
3146
<group android:id="@+id/group_themes">
@@ -49,21 +64,21 @@
4964
<item
5065
android:id="@+id/findMenu"
5166
android:icon="@drawable/ic_find_in_page"
52-
android:orderInCategory="3"
67+
android:orderInCategory="5"
5368
android:title="@string/find_and_replace" />
5469

5570
<item
5671
android:id="@+id/comment"
57-
android:orderInCategory="4"
72+
android:orderInCategory="6"
5873
android:title="@string/comment" />
5974

6075
<item
6176
android:id="@+id/un_comment"
62-
android:orderInCategory="5"
77+
android:orderInCategory="7"
6378
android:title="@string/uncomment" />
6479

6580
<item
6681
android:id="@+id/clearText"
67-
android:orderInCategory="6"
82+
android:orderInCategory="8"
6883
android:title="@string/clear_text" />
6984
</menu>

app/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
<string name="clear_text">Clear Text</string>
1212
<string name="comment">Comment</string>
1313
<string name="uncomment">UnComment</string>
14+
<string name="undo">Undo</string>
15+
<string name="redo">Redo</string>
1416

1517
<!-- Programming Languages -->
1618
<string name="languages">Languages</string>

0 commit comments

Comments
 (0)