capacitor-dex-editor 0.0.38 → 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/assets/availableSyntax.json +54 -0
- package/android/src/main/assets/c.json +42 -0
- package/android/src/main/assets/colors.json +21 -0
- package/android/src/main/assets/cpp.json +79 -0
- package/android/src/main/assets/dart.json +108 -0
- package/android/src/main/assets/java.json +46 -0
- package/android/src/main/assets/js.json +54 -0
- package/android/src/main/assets/json.json +33 -0
- package/android/src/main/assets/kotlin.json +53 -0
- package/android/src/main/assets/lua.json +54 -0
- package/android/src/main/assets/php.json +69 -0
- package/android/src/main/assets/python.json +87 -0
- package/android/src/main/assets/rust.json +139 -0
- package/android/src/main/assets/smali.json +131 -0
- package/android/src/main/assets/xml.json +96 -0
- package/android/src/main/java/com/aetherlink/dexeditor/DexEditorPluginPlugin.java +48 -0
- package/android/src/main/java/com/aetherlink/dexeditor/SmaliEditorActivity.java +205 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/EditView.java +4022 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/WordWrapLayout.java +275 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/buffer/BufferCache.java +113 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/buffer/GapBuffer.java +685 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/component/ClipboardPanel.java +1380 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/component/Magnifier.java +363 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/Candidate.java +52 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/CommentDef.java +47 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/LineResult.java +49 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/MHSyntaxHighlightEngine.java +841 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/Rule.java +53 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/Token.java +48 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/listener/OnTextChangedListener.java +6 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/utils/Pair.java +80 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/utils/ScreenUtils.java +63 -0
- package/android/src/main/res/drawable/abc_text_cursor_material.xml +9 -0
- package/android/src/main/res/drawable/abc_text_select_handle_left_mtrl.png +0 -0
- package/android/src/main/res/drawable/abc_text_select_handle_middle_mtrl.png +0 -0
- package/android/src/main/res/drawable/abc_text_select_handle_right_mtrl.png +0 -0
- package/android/src/main/res/drawable/ic_arrow_back.xml +12 -0
- package/android/src/main/res/drawable/ic_copy.xml +11 -0
- package/android/src/main/res/drawable/ic_cut.xml +10 -0
- package/android/src/main/res/drawable/ic_delete.xml +12 -0
- package/android/src/main/res/drawable/ic_edit_white_24dp.xml +10 -0
- package/android/src/main/res/drawable/ic_goto.xml +10 -0
- package/android/src/main/res/drawable/ic_launcher_background.xml +186 -0
- package/android/src/main/res/drawable/ic_look_white_24dp.xml +11 -0
- package/android/src/main/res/drawable/ic_more.xml +10 -0
- package/android/src/main/res/drawable/ic_open_link.xml +11 -0
- package/android/src/main/res/drawable/ic_paste.xml +11 -0
- package/android/src/main/res/drawable/ic_redo_white_24dp.xml +10 -0
- package/android/src/main/res/drawable/ic_select.xml +11 -0
- package/android/src/main/res/drawable/ic_select_all.xml +10 -0
- package/android/src/main/res/drawable/ic_share.xml +11 -0
- package/android/src/main/res/drawable/ic_toggle_comment.xml +12 -0
- package/android/src/main/res/drawable/ic_translate.xml +10 -0
- package/android/src/main/res/drawable/ic_undo_white_24dp.xml +11 -0
- package/android/src/main/res/drawable/magnifier_bg.xml +5 -0
- package/android/src/main/res/drawable/popup_background.xml +8 -0
- package/android/src/main/res/drawable/ripple_effect.xml +9 -0
- package/android/src/main/res/drawable/selection_menu_background.xml +5 -0
- package/android/src/main/res/layout/custom_selection_menu.xml +44 -0
- package/android/src/main/res/layout/expand_button.xml +23 -0
- package/android/src/main/res/layout/item_autocomplete.xml +25 -0
- package/android/src/main/res/layout/magnifier_popup.xml +17 -0
- package/android/src/main/res/layout/menu_item.xml +30 -0
- package/android/src/main/res/layout/text_selection_menu.xml +36 -0
- package/package.json +1 -1
|
@@ -0,0 +1,4022 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* MH-TextEditor - An Advanced and optimized TextEditor for android
|
|
3
|
+
* Copyright 2025, developer-krushna
|
|
4
|
+
*
|
|
5
|
+
* Redistribution and use in source and binary forms, with or without
|
|
6
|
+
* modification, are permitted provided that the following conditions are
|
|
7
|
+
* met:
|
|
8
|
+
*
|
|
9
|
+
* * Redistributions of source code must retain the above copyright
|
|
10
|
+
* notice, this list of conditions and the following disclaimer.
|
|
11
|
+
* * Redistributions in binary form must reproduce the above
|
|
12
|
+
* copyright notice, this list of conditions and the following disclaimer
|
|
13
|
+
* in the documentation and/or other materials provided with the
|
|
14
|
+
* distribution.
|
|
15
|
+
* * Neither the name of developer-krushna nor the names of its
|
|
16
|
+
* contributors may be used to endorse or promote products derived from
|
|
17
|
+
* this software without specific prior written permission.
|
|
18
|
+
*
|
|
19
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
20
|
+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
21
|
+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
22
|
+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
23
|
+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
24
|
+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
25
|
+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
26
|
+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
27
|
+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
28
|
+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
* Please contact Krushna by email modder-hub@zohomail.in if you need
|
|
33
|
+
* additional information or have any questions
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
package com.aetherlink.dexeditor.editor;
|
|
37
|
+
|
|
38
|
+
import android.content.ClipData;
|
|
39
|
+
import android.content.ClipDescription;
|
|
40
|
+
import android.content.ClipboardManager;
|
|
41
|
+
import android.content.Context;
|
|
42
|
+
import android.content.Intent;
|
|
43
|
+
import android.graphics.Canvas;
|
|
44
|
+
import android.graphics.Color;
|
|
45
|
+
import android.graphics.Paint;
|
|
46
|
+
import android.graphics.Rect;
|
|
47
|
+
import android.graphics.Typeface;
|
|
48
|
+
import android.graphics.drawable.Drawable;
|
|
49
|
+
import android.os.Handler;
|
|
50
|
+
import android.text.InputType;
|
|
51
|
+
import android.text.SpannableString;
|
|
52
|
+
import android.text.Spanned;
|
|
53
|
+
import android.text.TextPaint;
|
|
54
|
+
import android.text.TextUtils;
|
|
55
|
+
import android.text.style.ForegroundColorSpan;
|
|
56
|
+
import android.util.AttributeSet;
|
|
57
|
+
import android.util.Log;
|
|
58
|
+
import android.util.Pair;
|
|
59
|
+
import android.view.GestureDetector;
|
|
60
|
+
import android.view.HapticFeedbackConstants;
|
|
61
|
+
import android.view.KeyEvent;
|
|
62
|
+
import android.view.MotionEvent;
|
|
63
|
+
import android.view.ScaleGestureDetector;
|
|
64
|
+
import android.view.View;
|
|
65
|
+
import android.view.ViewGroup;
|
|
66
|
+
import android.view.animation.AnimationUtils;
|
|
67
|
+
import android.view.inputmethod.BaseInputConnection;
|
|
68
|
+
import android.view.inputmethod.EditorInfo;
|
|
69
|
+
import android.view.inputmethod.InputConnection;
|
|
70
|
+
import android.view.inputmethod.InputMethodManager;
|
|
71
|
+
import android.widget.AdapterView;
|
|
72
|
+
import android.widget.ArrayAdapter;
|
|
73
|
+
import android.widget.ListPopupWindow;
|
|
74
|
+
import android.widget.OverScroller;
|
|
75
|
+
import android.widget.PopupWindow;
|
|
76
|
+
import android.widget.TextView;
|
|
77
|
+
import java.util.ArrayList;
|
|
78
|
+
import java.util.Collections;
|
|
79
|
+
import java.util.Comparator;
|
|
80
|
+
import java.util.HashSet;
|
|
81
|
+
import java.util.List;
|
|
82
|
+
import java.util.Set;
|
|
83
|
+
import java.util.regex.Matcher;
|
|
84
|
+
import java.util.regex.Pattern;
|
|
85
|
+
import com.aetherlink.dexeditor.R;
|
|
86
|
+
import com.aetherlink.dexeditor.editor.buffer.GapBuffer;
|
|
87
|
+
import com.aetherlink.dexeditor.editor.component.ClipboardPanel;
|
|
88
|
+
import com.aetherlink.dexeditor.editor.component.Magnifier;
|
|
89
|
+
import com.aetherlink.dexeditor.editor.highlight.MHSyntaxHighlightEngine;
|
|
90
|
+
import com.aetherlink.dexeditor.editor.listener.OnTextChangedListener;
|
|
91
|
+
import com.aetherlink.dexeditor.editor.utils.ScreenUtils;
|
|
92
|
+
|
|
93
|
+
/* Author : Krushna Chandra Maharna(@developer-krushna)
|
|
94
|
+
This project was actually started by some one using gap buffer
|
|
95
|
+
But i forgot his name and his repository link because i was started
|
|
96
|
+
working on this project in 2024 .. During that time due to some personal problem
|
|
97
|
+
I closed this project and saved it in sdcard for future task .
|
|
98
|
+
But unfortunately i unable to save the original author name. I am really sorry.
|
|
99
|
+
But if you are the creator then please let me know so that i can update this
|
|
100
|
+
part. Thank You
|
|
101
|
+
|
|
102
|
+
Optmization , code refactorinh and comments are made by ChatGPT
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
/*
|
|
106
|
+
* I have not included many useful helper methods as i was working for something
|
|
107
|
+
* Feel free to include them
|
|
108
|
+
* But basic fetures are already introduced so no need to worry about it Lol
|
|
109
|
+
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
public class EditView extends View {
|
|
113
|
+
|
|
114
|
+
private static final String COPYRIGHT = "MH-TextEditor\nCopyright (C) Krushna Chandra modder-hub@zohomail.in\nThis project is distributed under the LGPL v2.1 license";
|
|
115
|
+
|
|
116
|
+
private final String TAG = this.getClass().getSimpleName();
|
|
117
|
+
|
|
118
|
+
// ---------- Fields (state, resources, helpers) ----------
|
|
119
|
+
private Paint mPaint;
|
|
120
|
+
private TextPaint mTextPaint;
|
|
121
|
+
private GapBuffer mGapBuffer;
|
|
122
|
+
|
|
123
|
+
// cursor and select handle drawable resources
|
|
124
|
+
private Drawable mDrawableCursorRes;
|
|
125
|
+
private Drawable mTextSelectHandleLeftRes;
|
|
126
|
+
private Drawable mTextSelectHandleRightRes;
|
|
127
|
+
private Drawable mTextSelectHandleMiddleRes;
|
|
128
|
+
|
|
129
|
+
private int mCursorPosX, mCursorPosY;
|
|
130
|
+
private int mCursorLine, mCursorIndex;
|
|
131
|
+
private int mCursorWidth, mCursorHeight;
|
|
132
|
+
private int screenWidth, screenHeight;
|
|
133
|
+
private int lineWidth, spaceWidth;
|
|
134
|
+
private int handleMiddleWidth, handleMiddleHeight;
|
|
135
|
+
private int selectionStart, selectionEnd;
|
|
136
|
+
private int selectHandleWidth, selectHandleHeight;
|
|
137
|
+
private int selectHandleLeftX, selectHandleLeftY;
|
|
138
|
+
private int selectHandleRightX, selectHandleRightY;
|
|
139
|
+
|
|
140
|
+
private int mMetaState = 0;
|
|
141
|
+
|
|
142
|
+
private OnTextChangedListener mTextListener;
|
|
143
|
+
private OverScroller mScroller;
|
|
144
|
+
private GestureDetector mGestureDetector;
|
|
145
|
+
private GestureListener mGestureListener;
|
|
146
|
+
private ScaleGestureDetector mScaleGestureDetector;
|
|
147
|
+
private ClipboardManager mClipboard;
|
|
148
|
+
private ArrayList<Pair<Integer, Integer>> mReplaceList;
|
|
149
|
+
|
|
150
|
+
private boolean mCursorVisiable = true;
|
|
151
|
+
private boolean mHandleMiddleVisable = false;
|
|
152
|
+
private boolean isEditedMode = true;
|
|
153
|
+
private boolean isSelectMode = false;
|
|
154
|
+
|
|
155
|
+
private long mLastScroll;
|
|
156
|
+
// record last single tap time
|
|
157
|
+
private long mLastTapTime;
|
|
158
|
+
// left margin for draw text
|
|
159
|
+
private final int SPACEING = 2;
|
|
160
|
+
// animation duration 250ms
|
|
161
|
+
private final int DEFAULT_DURATION = 250;
|
|
162
|
+
// cursor blink BLINK_TIMEOUT 500ms
|
|
163
|
+
private final int BLINK_TIMEOUT = 500;
|
|
164
|
+
|
|
165
|
+
// Magnifier
|
|
166
|
+
private Magnifier mMagnifier;
|
|
167
|
+
private boolean mMagnifierEnabled = true;
|
|
168
|
+
private float mMagnifierX, mMagnifierY;
|
|
169
|
+
private boolean mIsMagnifierShowing = false;
|
|
170
|
+
|
|
171
|
+
private ClipboardPanel mClipboardPanel;
|
|
172
|
+
|
|
173
|
+
private MHSyntaxHighlightEngine mHighlighter;
|
|
174
|
+
private boolean isSyntaxDarkMode = false;
|
|
175
|
+
|
|
176
|
+
// Auto-complete
|
|
177
|
+
private Set<String> mWordSet = new HashSet<>();
|
|
178
|
+
private ListPopupWindow mAutoCompletePopup;
|
|
179
|
+
private ArrayAdapter<String> mAutoCompleteAdapter;
|
|
180
|
+
|
|
181
|
+
private static final Pattern WORD_PATTERN = Pattern.compile("\\w+");
|
|
182
|
+
private static final int MIN_WORD_LEN = 2; // Filter short words
|
|
183
|
+
private static final int WORD_UPDATE_DELAY = 200; // ms throttle
|
|
184
|
+
private Runnable mWordUpdateRunnable = new Runnable() {
|
|
185
|
+
@Override
|
|
186
|
+
public void run() {
|
|
187
|
+
updateWordSet();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
private String mCurrentPrefix = "";
|
|
191
|
+
private long mLastInputTime = 0;
|
|
192
|
+
private String mLastCommittedText = "";
|
|
193
|
+
private boolean mProcessingInput = false;
|
|
194
|
+
private final long INPUT_DEBOUNCE_DELAY = 50; // ms
|
|
195
|
+
|
|
196
|
+
private boolean mAutoIndentEnabled = true; // Default on
|
|
197
|
+
|
|
198
|
+
private int mFirstSelectedLine = -1;
|
|
199
|
+
private int mSecondSelectedLine = -1;
|
|
200
|
+
private boolean mWaitingForSecondSelection = false;
|
|
201
|
+
private int mStartSelectionLine = -1;
|
|
202
|
+
private int mEndSelectionLine = -1;
|
|
203
|
+
private boolean mIsLineSelectionMode = false;
|
|
204
|
+
private Runnable mClearSelectionRunnable = new Runnable() {
|
|
205
|
+
@Override
|
|
206
|
+
public void run() {
|
|
207
|
+
// If user doesn't select second line within timeout, clear selection
|
|
208
|
+
mWaitingForSecondSelection = false;
|
|
209
|
+
mFirstSelectedLine = -1;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// ---------- Blink / auto-hide ----------
|
|
214
|
+
// cursor blink runnable toggling visibility
|
|
215
|
+
private Runnable blinkAction = new Runnable() {
|
|
216
|
+
@Override
|
|
217
|
+
public void run() {
|
|
218
|
+
// TODO: Implement this method
|
|
219
|
+
mCursorVisiable = !mCursorVisiable;
|
|
220
|
+
postDelayed(blinkAction, BLINK_TIMEOUT);
|
|
221
|
+
|
|
222
|
+
if (System.currentTimeMillis() - mLastTapTime >= 5 * BLINK_TIMEOUT) {
|
|
223
|
+
mHandleMiddleVisable = false;
|
|
224
|
+
}
|
|
225
|
+
postInvalidate();
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
private Handler mSelectionHandler = new Handler();
|
|
230
|
+
private Runnable mUpdateSelectionPosition = new Runnable() {
|
|
231
|
+
@Override
|
|
232
|
+
public void run() {
|
|
233
|
+
if (mClipboardPanel != null && (isSelectMode || mHandleMiddleVisable)) {
|
|
234
|
+
mClipboardPanel.updatePosition();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
private Runnable mAutoHideRunnable = new Runnable() {
|
|
240
|
+
@Override
|
|
241
|
+
public void run() {
|
|
242
|
+
if (!isSelectMode && !mHandleMiddleVisable) {
|
|
243
|
+
hideTextSelectionWindow();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// ---------- Constructors ----------
|
|
249
|
+
// Constructor (Context)
|
|
250
|
+
public EditView(Context context) {
|
|
251
|
+
super(context);
|
|
252
|
+
initView(context);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Constructor (Context, AttributeSet)
|
|
256
|
+
public EditView(Context context, AttributeSet attrs) {
|
|
257
|
+
super(context, attrs);
|
|
258
|
+
initView(context);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Constructor (Context, AttributeSet, defStyle)
|
|
262
|
+
public EditView(Context context, AttributeSet attrs, int defStyle) {
|
|
263
|
+
super(context, attrs, defStyle);
|
|
264
|
+
initView(context);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private void initView(Context context) {
|
|
268
|
+
Log.v(TAG, COPYRIGHT);
|
|
269
|
+
|
|
270
|
+
// Initialize Gapbuffer
|
|
271
|
+
mGapBuffer = new GapBuffer();
|
|
272
|
+
mCursorLine = getLineCount();
|
|
273
|
+
setBackgroundColor(Color.WHITE);
|
|
274
|
+
|
|
275
|
+
screenWidth = ScreenUtils.getScreenWidth(context);
|
|
276
|
+
screenHeight = ScreenUtils.getScreenHeight(context);
|
|
277
|
+
|
|
278
|
+
mDrawableCursorRes = context.getDrawable(R.drawable.abc_text_cursor_material);
|
|
279
|
+
mDrawableCursorRes.setTint(Color.BLACK);
|
|
280
|
+
|
|
281
|
+
mCursorWidth = mDrawableCursorRes.getIntrinsicWidth();
|
|
282
|
+
mCursorHeight = mDrawableCursorRes.getIntrinsicHeight();
|
|
283
|
+
|
|
284
|
+
mClipboardPanel = new ClipboardPanel(this);
|
|
285
|
+
|
|
286
|
+
// Initialize magnifier
|
|
287
|
+
mMagnifier = new Magnifier(this);
|
|
288
|
+
|
|
289
|
+
// Reduce cursor width and make it responsive
|
|
290
|
+
int density = (int) getResources().getDisplayMetrics().density;
|
|
291
|
+
mCursorWidth = Math.max(2, density); // Minimum 2px, scales with density
|
|
292
|
+
if (mCursorWidth > 5) mCursorWidth = 5; // Max 4px
|
|
293
|
+
|
|
294
|
+
// handle left - scale down selection handles
|
|
295
|
+
mTextSelectHandleLeftRes = context.getDrawable(R.drawable.abc_text_select_handle_left_mtrl);
|
|
296
|
+
mTextSelectHandleLeftRes.setTint(Color.parseColor("#63B5F7"));
|
|
297
|
+
|
|
298
|
+
// Scale down selection handles based on screen density
|
|
299
|
+
selectHandleWidth = (int) (mTextSelectHandleLeftRes.getIntrinsicWidth() * 0.3f);
|
|
300
|
+
selectHandleHeight = (int) (mTextSelectHandleLeftRes.getIntrinsicHeight() * 0.3f);
|
|
301
|
+
|
|
302
|
+
// handle right
|
|
303
|
+
mTextSelectHandleRightRes = context.getDrawable(R.drawable.abc_text_select_handle_right_mtrl);
|
|
304
|
+
mTextSelectHandleRightRes.setTint(Color.parseColor("#63B5F7"));
|
|
305
|
+
|
|
306
|
+
// handle middle - scale down
|
|
307
|
+
mTextSelectHandleMiddleRes = context.getDrawable(R.drawable.abc_text_select_handle_middle_mtrl);
|
|
308
|
+
mTextSelectHandleMiddleRes.setTint(Color.parseColor("#63B5F7"));
|
|
309
|
+
handleMiddleWidth = (int) (mTextSelectHandleMiddleRes.getIntrinsicWidth() * 0.5f);
|
|
310
|
+
handleMiddleHeight = (int) (mTextSelectHandleMiddleRes.getIntrinsicHeight() * 0.5f);
|
|
311
|
+
|
|
312
|
+
mGestureListener = new GestureListener();
|
|
313
|
+
mGestureDetector = new GestureDetector(context, mGestureListener);
|
|
314
|
+
mScaleGestureDetector = new ScaleGestureDetector(context, new ScaleGestureListener());
|
|
315
|
+
|
|
316
|
+
mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
|
|
317
|
+
mTextPaint.setColor(Color.parseColor("#B0B0B0"));
|
|
318
|
+
|
|
319
|
+
setTextSize(ScreenUtils.dip2px(context, 18));
|
|
320
|
+
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
321
|
+
mPaint.setColor(Color.parseColor("#FFFAE3"));
|
|
322
|
+
mPaint.setStrokeWidth(0);
|
|
323
|
+
|
|
324
|
+
mScroller = new OverScroller(context);
|
|
325
|
+
mClipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
|
326
|
+
mReplaceList = new ArrayList<>();
|
|
327
|
+
|
|
328
|
+
spaceWidth = (int) mTextPaint.measureText(" ");
|
|
329
|
+
|
|
330
|
+
// Explicitly set initial scroll position to (0, 0)
|
|
331
|
+
scrollTo(0, 0);
|
|
332
|
+
|
|
333
|
+
requestFocus();
|
|
334
|
+
setFocusable(true);
|
|
335
|
+
setFocusableInTouchMode(true);
|
|
336
|
+
postDelayed(blinkAction, BLINK_TIMEOUT);
|
|
337
|
+
|
|
338
|
+
mAutoCompletePopup = new ListPopupWindow(getContext());
|
|
339
|
+
mAutoCompleteAdapter = new ArrayAdapter<String>(
|
|
340
|
+
getContext(),
|
|
341
|
+
R.layout.item_autocomplete,
|
|
342
|
+
R.id.text1, // 👈 explicitly tell it which TextView to use
|
|
343
|
+
new ArrayList<String>()
|
|
344
|
+
) {
|
|
345
|
+
@Override
|
|
346
|
+
public View getView(int position, View convertView, ViewGroup parent) {
|
|
347
|
+
View view = super.getView(position, convertView, parent);
|
|
348
|
+
TextView textView = view.findViewById(R.id.text1);
|
|
349
|
+
String item = getItem(position);
|
|
350
|
+
if (item != null && !mCurrentPrefix.isEmpty()) {
|
|
351
|
+
int index = item.toLowerCase().indexOf(mCurrentPrefix.toLowerCase());
|
|
352
|
+
if (index >= 0) {
|
|
353
|
+
SpannableString spannable = new SpannableString(item);
|
|
354
|
+
spannable.setSpan(new ForegroundColorSpan(Color.parseColor("#2196F3")),
|
|
355
|
+
index, index + mCurrentPrefix.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
356
|
+
textView.setText(spannable);
|
|
357
|
+
} else {
|
|
358
|
+
textView.setText(item);
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
textView.setText(item);
|
|
362
|
+
}
|
|
363
|
+
return view;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
mAutoCompletePopup.setAdapter(mAutoCompleteAdapter);
|
|
367
|
+
mAutoCompletePopup.setHeight(ScreenUtils.dip2px(context, 150)); // fits about 3-4 rows
|
|
368
|
+
mAutoCompletePopup.setModal(false); // allow typing while shown
|
|
369
|
+
mAutoCompletePopup.setAnchorView(this);
|
|
370
|
+
|
|
371
|
+
mAutoCompletePopup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
|
372
|
+
@Override
|
|
373
|
+
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
|
374
|
+
String selected = (String) parent.getItemAtPosition(position);
|
|
375
|
+
if (selected != null) {
|
|
376
|
+
replacePrefixWithWord(selected);
|
|
377
|
+
dismissAutoComplete();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
mAutoCompletePopup.setOnDismissListener(new PopupWindow.OnDismissListener() {
|
|
382
|
+
@Override
|
|
383
|
+
public void onDismiss() {
|
|
384
|
+
mCurrentPrefix = ""; // Reset on dismiss
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
// Initial word set
|
|
388
|
+
post(mWordUpdateRunnable);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ---------- Lifecycle ----------
|
|
392
|
+
// Called when view detached from window
|
|
393
|
+
@Override
|
|
394
|
+
protected void onDetachedFromWindow() {
|
|
395
|
+
removeCallbacks(mWordUpdateRunnable);
|
|
396
|
+
dismissAutoComplete();
|
|
397
|
+
super.onDetachedFromWindow();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Called when view size/layout changes
|
|
401
|
+
@Override
|
|
402
|
+
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
|
403
|
+
super.onLayout(changed, left, top, right, bottom);
|
|
404
|
+
if (changed) {
|
|
405
|
+
// Only adjust scroll if cursor is significantly out of view
|
|
406
|
+
adjustCursorPosition();
|
|
407
|
+
if (isSelectMode) {
|
|
408
|
+
adjustSelectRange(selectionStart, selectionEnd);
|
|
409
|
+
}
|
|
410
|
+
// Check if initial scroll is needed
|
|
411
|
+
/* if (getScrollY() > 0 || getScrollX() > 0) {
|
|
412
|
+
scrollToVisable();
|
|
413
|
+
} else {
|
|
414
|
+
// Ensure we're at the top-left initially
|
|
415
|
+
scrollTo(0, 0);
|
|
416
|
+
}*/
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ---------- Rendering / Drawing ----------
|
|
421
|
+
// Top-level draw method
|
|
422
|
+
@Override
|
|
423
|
+
protected void onDraw(Canvas canvas) {
|
|
424
|
+
super.onDraw(canvas);
|
|
425
|
+
canvas.save();
|
|
426
|
+
canvas.clipRect(getScrollX(),
|
|
427
|
+
getScrollY(),
|
|
428
|
+
getScrollX() + getWidth() - getPaddingRight(),
|
|
429
|
+
getScrollY() + getHeight() - getPaddingBottom());
|
|
430
|
+
|
|
431
|
+
canvas.translate(getPaddingLeft(), getPaddingTop());
|
|
432
|
+
|
|
433
|
+
Drawable background = getBackground();
|
|
434
|
+
if (background != null) {
|
|
435
|
+
background.draw(canvas);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
drawMatchText(canvas);
|
|
439
|
+
drawLineBackground(canvas);
|
|
440
|
+
drawEditableText(canvas); // Remove the background drawing from this method
|
|
441
|
+
drawSelectHandle(canvas);
|
|
442
|
+
drawCursor(canvas);
|
|
443
|
+
|
|
444
|
+
canvas.restore();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Draw the editor content (helper)
|
|
448
|
+
public void drawEditorContent(Canvas canvas, int captureTop, int captureBottom) {
|
|
449
|
+
int saveCount = canvas.save();
|
|
450
|
+
canvas.translate(getScrollX(), getScrollY());
|
|
451
|
+
drawMatchText(canvas);
|
|
452
|
+
drawLineBackground(canvas);
|
|
453
|
+
drawEditableText(canvas);
|
|
454
|
+
drawCursor(canvas);
|
|
455
|
+
drawSelectHandle(canvas);
|
|
456
|
+
canvas.restoreToCount(saveCount);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Draw current line background or selection highlight
|
|
460
|
+
public void drawLineBackground(Canvas canvas) {
|
|
461
|
+
if (mIsLineSelectionMode && mStartSelectionLine > 0 && mEndSelectionLine > 0) {
|
|
462
|
+
mPaint.setColor(Color.parseColor("#E3F2FD")); // Light blue background
|
|
463
|
+
int lineNumberWidth = getLineNumberWidth() + SPACEING * 2;
|
|
464
|
+
|
|
465
|
+
for (int i = mStartSelectionLine; i <= mEndSelectionLine; i++) {
|
|
466
|
+
int top = (i - 1) * getLineHeight();
|
|
467
|
+
int bottom = i * getLineHeight();
|
|
468
|
+
|
|
469
|
+
canvas.drawRect(getPaddingLeft(), top,
|
|
470
|
+
getPaddingLeft() + lineNumberWidth, bottom, mPaint);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (!isSelectMode) {
|
|
475
|
+
// draw current line background
|
|
476
|
+
mPaint.setColor(Color.parseColor("#FFFAE3"));
|
|
477
|
+
int left = getPaddingLeft() + getLineNumberWidth() + SPACEING;
|
|
478
|
+
canvas.drawRect(left,
|
|
479
|
+
getPaddingTop() + mCursorPosY,
|
|
480
|
+
getScrollX() + getWidth(),
|
|
481
|
+
mCursorPosY + getLineHeight(),
|
|
482
|
+
mPaint);
|
|
483
|
+
} else {
|
|
484
|
+
// draw select text background - BLOCK STYLE (like before)
|
|
485
|
+
mPaint.setColor(Color.parseColor("#B3DBFB"));
|
|
486
|
+
|
|
487
|
+
int left = getLeftSpace();
|
|
488
|
+
int lineHeight = getLineHeight();
|
|
489
|
+
|
|
490
|
+
int startLine = getOffsetLine(selectionStart);
|
|
491
|
+
int endLine = getOffsetLine(selectionEnd);
|
|
492
|
+
|
|
493
|
+
// Only draw visible selection ranges to reduce lag
|
|
494
|
+
int visibleStartLine = Math.max(startLine, canvas.getClipBounds().top / lineHeight);
|
|
495
|
+
int visibleEndLine = Math.min(endLine, canvas.getClipBounds().bottom / lineHeight + 1);
|
|
496
|
+
|
|
497
|
+
// start line < end line
|
|
498
|
+
if (startLine != endLine) {
|
|
499
|
+
for (int i = visibleStartLine; i <= visibleEndLine; i++) {
|
|
500
|
+
int lineWidth = getLineWidth(i) + spaceWidth;
|
|
501
|
+
if (i == startLine) {
|
|
502
|
+
// First line - from selection start to end of line
|
|
503
|
+
int lineStart = getLineStart(startLine);
|
|
504
|
+
String beforeText = mGapBuffer.substring(lineStart, selectionStart);
|
|
505
|
+
int selectStartX = left + (int) mTextPaint.measureText(beforeText);
|
|
506
|
+
|
|
507
|
+
canvas.drawRect(selectStartX,
|
|
508
|
+
(startLine - 1) * lineHeight,
|
|
509
|
+
left + lineWidth,
|
|
510
|
+
startLine * lineHeight,
|
|
511
|
+
mPaint);
|
|
512
|
+
} else if (i == endLine) {
|
|
513
|
+
// Last line - from line start to selection end
|
|
514
|
+
int lineStart = getLineStart(endLine);
|
|
515
|
+
String beforeText = mGapBuffer.substring(lineStart, selectionEnd);
|
|
516
|
+
int selectEndX = left + (int) mTextPaint.measureText(beforeText);
|
|
517
|
+
|
|
518
|
+
canvas.drawRect(left,
|
|
519
|
+
(endLine - 1) * lineHeight,
|
|
520
|
+
selectEndX,
|
|
521
|
+
endLine * lineHeight,
|
|
522
|
+
mPaint);
|
|
523
|
+
} else {
|
|
524
|
+
// Middle lines - full line width
|
|
525
|
+
canvas.drawRect(left,
|
|
526
|
+
(i - 1) * lineHeight,
|
|
527
|
+
left + lineWidth,
|
|
528
|
+
i * lineHeight,
|
|
529
|
+
mPaint);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
} else {
|
|
533
|
+
// start line = end line - single line selection
|
|
534
|
+
int lineStart = getLineStart(startLine);
|
|
535
|
+
String beforeStartText = mGapBuffer.substring(lineStart, selectionStart);
|
|
536
|
+
String selectedText = mGapBuffer.substring(selectionStart, selectionEnd);
|
|
537
|
+
|
|
538
|
+
int selectStartX = left + (int) mTextPaint.measureText(beforeStartText);
|
|
539
|
+
int selectEndX = selectStartX + (int) mTextPaint.measureText(selectedText);
|
|
540
|
+
|
|
541
|
+
canvas.drawRect(selectStartX,
|
|
542
|
+
(startLine - 1) * lineHeight,
|
|
543
|
+
selectEndX,
|
|
544
|
+
startLine * lineHeight,
|
|
545
|
+
mPaint);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Draw select handles (left/right)
|
|
551
|
+
public void drawSelectHandle(Canvas canvas) {
|
|
552
|
+
if (isSelectMode) {
|
|
553
|
+
mTextSelectHandleLeftRes.setBounds(selectHandleLeftX - selectHandleWidth + selectHandleWidth / 4,
|
|
554
|
+
selectHandleLeftY,
|
|
555
|
+
selectHandleLeftX + selectHandleWidth / 4,
|
|
556
|
+
selectHandleLeftY + selectHandleHeight
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
mTextSelectHandleLeftRes.draw(canvas);
|
|
560
|
+
|
|
561
|
+
// select handle right
|
|
562
|
+
mTextSelectHandleRightRes.setBounds(selectHandleRightX - selectHandleWidth / 4,
|
|
563
|
+
selectHandleRightY,
|
|
564
|
+
selectHandleRightX + selectHandleWidth - selectHandleWidth / 4,
|
|
565
|
+
selectHandleRightY + selectHandleHeight
|
|
566
|
+
);
|
|
567
|
+
mTextSelectHandleRightRes.draw(canvas);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Draw match/replace highlights
|
|
572
|
+
public void drawMatchText(Canvas canvas) {
|
|
573
|
+
if (isSelectMode) {
|
|
574
|
+
int size = mReplaceList.size();
|
|
575
|
+
int left = getLeftSpace();
|
|
576
|
+
|
|
577
|
+
for (int i = 0; i < size; ++i) {
|
|
578
|
+
int start = mReplaceList.get(i).first;
|
|
579
|
+
int end = mReplaceList.get(i).second;
|
|
580
|
+
|
|
581
|
+
if (start == selectionStart && end == selectionEnd)
|
|
582
|
+
mPaint.setColor(Color.YELLOW);
|
|
583
|
+
else
|
|
584
|
+
mPaint.setColor(Color.parseColor("#FFFD54"));
|
|
585
|
+
|
|
586
|
+
int line = mGapBuffer.findLineNumber(start);
|
|
587
|
+
int lineStart = getLineStart(line);
|
|
588
|
+
|
|
589
|
+
canvas.drawRect(left + measureText(mGapBuffer.substring(lineStart, start)),
|
|
590
|
+
(line - 1) * getLineHeight(),
|
|
591
|
+
left + measureText(mGapBuffer.substring(lineStart, end)),
|
|
592
|
+
line * getLineHeight(),
|
|
593
|
+
mPaint
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Draw the cursor and middle handle
|
|
600
|
+
public void drawCursor(Canvas canvas) {
|
|
601
|
+
if (mCursorVisiable) {
|
|
602
|
+
int left = getLeftSpace();
|
|
603
|
+
int half = 0;
|
|
604
|
+
if (mCursorPosX >= left) {
|
|
605
|
+
half = mCursorWidth / 2;
|
|
606
|
+
} else {
|
|
607
|
+
mCursorPosX = left;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// draw text cursor
|
|
611
|
+
mDrawableCursorRes.setBounds(mCursorPosX - half,
|
|
612
|
+
getPaddingTop() + mCursorPosY,
|
|
613
|
+
mCursorPosX - half + mCursorWidth,
|
|
614
|
+
mCursorPosY + getLineHeight()
|
|
615
|
+
);
|
|
616
|
+
mDrawableCursorRes.draw(canvas);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (mHandleMiddleVisable) {
|
|
620
|
+
// draw text select handle middle
|
|
621
|
+
mTextSelectHandleMiddleRes.setBounds(mCursorPosX - handleMiddleWidth / 2,
|
|
622
|
+
mCursorPosY + getLineHeight(),
|
|
623
|
+
mCursorPosX + handleMiddleWidth / 2,
|
|
624
|
+
mCursorPosY + getLineHeight() + handleMiddleHeight
|
|
625
|
+
);
|
|
626
|
+
mTextSelectHandleMiddleRes.draw(canvas);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Draw editable text lines with numbers and syntax highlighting
|
|
631
|
+
|
|
632
|
+
public void drawEditableText(Canvas canvas) {
|
|
633
|
+
int startLine = Math.max(canvas.getClipBounds().top / getLineHeight(), 1);
|
|
634
|
+
int endLine = Math.min(canvas.getClipBounds().bottom / getLineHeight() + 1, getLineCount());
|
|
635
|
+
|
|
636
|
+
int lineNumberWidth = getLineNumberWidth();
|
|
637
|
+
lineWidth = getWidth() - lineNumberWidth;
|
|
638
|
+
|
|
639
|
+
int totalContentHeight = getLineCount() * getLineHeight();
|
|
640
|
+
|
|
641
|
+
// Draw full-height line number bar background
|
|
642
|
+
mPaint.setColor(Color.parseColor("#F8F8F8"));
|
|
643
|
+
canvas.drawRect(
|
|
644
|
+
getPaddingLeft(),
|
|
645
|
+
0,
|
|
646
|
+
getPaddingLeft() + lineNumberWidth + SPACEING * 2,
|
|
647
|
+
Math.max(getHeight(), totalContentHeight),
|
|
648
|
+
mPaint
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
// Draw separator line
|
|
652
|
+
int separatorWidth = 2;
|
|
653
|
+
int separatorX = getPaddingLeft() + lineNumberWidth + SPACEING * 2 - separatorWidth;
|
|
654
|
+
mPaint.setColor(Color.parseColor("#E4E4E4"));
|
|
655
|
+
mPaint.setStrokeWidth(separatorWidth);
|
|
656
|
+
canvas.drawLine(
|
|
657
|
+
separatorX,
|
|
658
|
+
0,
|
|
659
|
+
separatorX,
|
|
660
|
+
Math.max(getHeight(), totalContentHeight),
|
|
661
|
+
mPaint
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
// Margins
|
|
665
|
+
int leftMargin = 10; // ✅ Space from left edge for line numbers
|
|
666
|
+
int rightMargin = 13; // Space between line numbers and separator
|
|
667
|
+
|
|
668
|
+
for (int i = startLine; i <= endLine; i++) {
|
|
669
|
+
int paintY = i * getLineHeight() - (int) mTextPaint.descent();
|
|
670
|
+
mTextPaint.setColor(Color.parseColor("#B0B0B0"));
|
|
671
|
+
|
|
672
|
+
String lineNumberText = String.valueOf(i);
|
|
673
|
+
int textWidth = (int) mTextPaint.measureText(lineNumberText);
|
|
674
|
+
|
|
675
|
+
// RIGHT aligned, with left and right margins respected
|
|
676
|
+
int lineNumberX = getPaddingLeft() + leftMargin
|
|
677
|
+
+ (lineNumberWidth - rightMargin - textWidth);
|
|
678
|
+
|
|
679
|
+
canvas.drawText(lineNumberText, lineNumberX, paintY, mTextPaint);
|
|
680
|
+
|
|
681
|
+
// Draw text content
|
|
682
|
+
int contentStartX = separatorX + separatorWidth + 10;
|
|
683
|
+
String text = getLine(i);
|
|
684
|
+
lineWidth = Math.max(measureText(text), lineWidth);
|
|
685
|
+
|
|
686
|
+
if (mHighlighter != null && text != null && !text.isEmpty()) {
|
|
687
|
+
int lineHeight = getLineHeight();
|
|
688
|
+
int top = (i - 1) * lineHeight;
|
|
689
|
+
int bottom = i * lineHeight;
|
|
690
|
+
int left = getPaddingLeft() + getLineNumberWidth() + SPACEING;
|
|
691
|
+
int right = getScrollX() + getWidth();
|
|
692
|
+
// Special Cases, like for smali print method line bg
|
|
693
|
+
// Bugs : You cant see the selected visual when you select method line
|
|
694
|
+
mHighlighter.drawLineBackground(canvas, text, i, left, top, right, bottom);
|
|
695
|
+
// Draw line text
|
|
696
|
+
mHighlighter.drawLineText(canvas, text, i, contentStartX, paintY);
|
|
697
|
+
} else {
|
|
698
|
+
mTextPaint.setColor(Color.BLACK);
|
|
699
|
+
canvas.drawText(text, contentStartX, paintY, mTextPaint);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
mPaint.setColor(Color.parseColor("#FFFAE3"));
|
|
704
|
+
mPaint.setStrokeWidth(0);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ---------- Input Handling (touch/keyboard/IME) ----------
|
|
708
|
+
// Handle touch events (dispatch gesture detectors)
|
|
709
|
+
@Override
|
|
710
|
+
public boolean onTouchEvent(MotionEvent event) {
|
|
711
|
+
// Reset auto-hide timer on any touch
|
|
712
|
+
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
|
713
|
+
requestFocus();
|
|
714
|
+
resetAutoHideTimer();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
switch (event.getAction()) {
|
|
718
|
+
case MotionEvent.ACTION_DOWN:
|
|
719
|
+
mScroller.abortAnimation();
|
|
720
|
+
break;
|
|
721
|
+
case MotionEvent.ACTION_UP:
|
|
722
|
+
mGestureListener.onUp(event);
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
mGestureDetector.onTouchEvent(event);
|
|
727
|
+
mScaleGestureDetector.onTouchEvent(event);
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Reset clipboard auto-hide timer helper
|
|
732
|
+
private void resetAutoHideTimer() {
|
|
733
|
+
if (mClipboardPanel != null && (isSelectMode || mHandleMiddleVisable)) {
|
|
734
|
+
scheduleAutoHide();
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Handle key down events for editing keys
|
|
739
|
+
@Override
|
|
740
|
+
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
741
|
+
if (!isEditedMode) return super.onKeyDown(keyCode, event);
|
|
742
|
+
|
|
743
|
+
// Skip if we're already processing input to avoid duplicates
|
|
744
|
+
if (mProcessingInput) {
|
|
745
|
+
Log.d(TAG, "Skipping onKeyDown - processing input");
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT ||
|
|
750
|
+
keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
|
|
751
|
+
mMetaState |= KeyEvent.META_SHIFT_ON;
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (keyCode == KeyEvent.KEYCODE_CTRL_LEFT ||
|
|
756
|
+
keyCode == KeyEvent.KEYCODE_CTRL_RIGHT) {
|
|
757
|
+
mMetaState |= KeyEvent.META_CTRL_ON;
|
|
758
|
+
return true;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (keyCode == KeyEvent.KEYCODE_ALT_LEFT ||
|
|
762
|
+
keyCode == KeyEvent.KEYCODE_ALT_RIGHT) {
|
|
763
|
+
mMetaState |= KeyEvent.META_ALT_ON;
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
|
768
|
+
Log.d(TAG, "onKeyDown: keyCode=" + keyCode + ", unicode=" + event.getUnicodeChar());
|
|
769
|
+
|
|
770
|
+
switch (keyCode) {
|
|
771
|
+
case KeyEvent.KEYCODE_ENTER:
|
|
772
|
+
case KeyEvent.KEYCODE_NUMPAD_ENTER:
|
|
773
|
+
mProcessingInput = true;
|
|
774
|
+
insert("\n");
|
|
775
|
+
postDelayed(new Runnable() {
|
|
776
|
+
@Override
|
|
777
|
+
public void run() {
|
|
778
|
+
mProcessingInput = false;
|
|
779
|
+
}
|
|
780
|
+
}, INPUT_DEBOUNCE_DELAY);
|
|
781
|
+
return true;
|
|
782
|
+
|
|
783
|
+
case KeyEvent.KEYCODE_DEL:
|
|
784
|
+
mProcessingInput = true;
|
|
785
|
+
delete();
|
|
786
|
+
postDelayed(new Runnable() {
|
|
787
|
+
@Override
|
|
788
|
+
public void run() {
|
|
789
|
+
mProcessingInput = false;
|
|
790
|
+
}
|
|
791
|
+
}, INPUT_DEBOUNCE_DELAY);
|
|
792
|
+
return true;
|
|
793
|
+
|
|
794
|
+
case KeyEvent.KEYCODE_FORWARD_DEL:
|
|
795
|
+
mProcessingInput = true;
|
|
796
|
+
handleForwardDelete();
|
|
797
|
+
postDelayed(new Runnable() {
|
|
798
|
+
@Override
|
|
799
|
+
public void run() {
|
|
800
|
+
mProcessingInput = false;
|
|
801
|
+
}
|
|
802
|
+
}, INPUT_DEBOUNCE_DELAY);
|
|
803
|
+
return true;
|
|
804
|
+
|
|
805
|
+
case KeyEvent.KEYCODE_SPACE:
|
|
806
|
+
mProcessingInput = true;
|
|
807
|
+
insert(" ");
|
|
808
|
+
postDelayed(new Runnable() {
|
|
809
|
+
@Override
|
|
810
|
+
public void run() {
|
|
811
|
+
mProcessingInput = false;
|
|
812
|
+
}
|
|
813
|
+
}, INPUT_DEBOUNCE_DELAY);
|
|
814
|
+
return true;
|
|
815
|
+
|
|
816
|
+
case KeyEvent.KEYCODE_TAB:
|
|
817
|
+
mProcessingInput = true;
|
|
818
|
+
insert("\t");
|
|
819
|
+
postDelayed(new Runnable() {
|
|
820
|
+
@Override
|
|
821
|
+
public void run() {
|
|
822
|
+
mProcessingInput = false;
|
|
823
|
+
}
|
|
824
|
+
}, INPUT_DEBOUNCE_DELAY);
|
|
825
|
+
return true;
|
|
826
|
+
|
|
827
|
+
// Let the input connection handle these to avoid duplicates
|
|
828
|
+
case KeyEvent.KEYCODE_0:
|
|
829
|
+
case KeyEvent.KEYCODE_1:
|
|
830
|
+
case KeyEvent.KEYCODE_2:
|
|
831
|
+
case KeyEvent.KEYCODE_3:
|
|
832
|
+
case KeyEvent.KEYCODE_4:
|
|
833
|
+
case KeyEvent.KEYCODE_5:
|
|
834
|
+
case KeyEvent.KEYCODE_6:
|
|
835
|
+
case KeyEvent.KEYCODE_7:
|
|
836
|
+
case KeyEvent.KEYCODE_8:
|
|
837
|
+
case KeyEvent.KEYCODE_9:
|
|
838
|
+
case KeyEvent.KEYCODE_NUMPAD_0:
|
|
839
|
+
case KeyEvent.KEYCODE_NUMPAD_1:
|
|
840
|
+
case KeyEvent.KEYCODE_NUMPAD_2:
|
|
841
|
+
case KeyEvent.KEYCODE_NUMPAD_3:
|
|
842
|
+
case KeyEvent.KEYCODE_NUMPAD_4:
|
|
843
|
+
case KeyEvent.KEYCODE_NUMPAD_5:
|
|
844
|
+
case KeyEvent.KEYCODE_NUMPAD_6:
|
|
845
|
+
case KeyEvent.KEYCODE_NUMPAD_7:
|
|
846
|
+
case KeyEvent.KEYCODE_NUMPAD_8:
|
|
847
|
+
case KeyEvent.KEYCODE_NUMPAD_9:
|
|
848
|
+
return false; // Let input connection handle via commitText
|
|
849
|
+
|
|
850
|
+
default:
|
|
851
|
+
// For other keys, let the input connection handle them
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return super.onKeyDown(keyCode, event);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Handle key up events for modifier keys
|
|
859
|
+
@Override
|
|
860
|
+
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
|
861
|
+
if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT ||
|
|
862
|
+
keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
|
|
863
|
+
mMetaState &= ~KeyEvent.META_SHIFT_ON;
|
|
864
|
+
return true;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (keyCode == KeyEvent.KEYCODE_CTRL_LEFT ||
|
|
868
|
+
keyCode == KeyEvent.KEYCODE_CTRL_RIGHT) {
|
|
869
|
+
mMetaState &= ~KeyEvent.META_CTRL_ON;
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (keyCode == KeyEvent.KEYCODE_ALT_LEFT ||
|
|
874
|
+
keyCode == KeyEvent.KEYCODE_ALT_RIGHT) {
|
|
875
|
+
mMetaState &= ~KeyEvent.META_ALT_ON;
|
|
876
|
+
return true;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return super.onKeyUp(keyCode, event);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Create input connection for IME
|
|
883
|
+
@Override
|
|
884
|
+
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
|
885
|
+
// TODO: Implement this method
|
|
886
|
+
outAttrs.inputType = InputType.TYPE_CLASS_TEXT
|
|
887
|
+
| InputType.TYPE_TEXT_FLAG_MULTI_LINE;
|
|
888
|
+
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_ENTER_ACTION
|
|
889
|
+
| EditorInfo.IME_ACTION_DONE
|
|
890
|
+
| EditorInfo.IME_FLAG_NO_EXTRACT_UI;
|
|
891
|
+
|
|
892
|
+
return new TextInputConnection(this, true);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Toggle software keyboard
|
|
896
|
+
public void showSoftInput(boolean show) {
|
|
897
|
+
if (isEditedMode) {
|
|
898
|
+
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
899
|
+
if (show)
|
|
900
|
+
imm.showSoftInput(this, 0);
|
|
901
|
+
else
|
|
902
|
+
imm.hideSoftInputFromWindow(getWindowToken(), 0);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// ---------- Text and Buffer Operations ----------
|
|
907
|
+
// Set an external buffer
|
|
908
|
+
public void setBuffer(GapBuffer buffer) {
|
|
909
|
+
mGapBuffer = buffer;
|
|
910
|
+
clearSyntaxCache();
|
|
911
|
+
dismissAutoComplete();
|
|
912
|
+
invalidate();
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Get current buffer
|
|
916
|
+
public GapBuffer getBuffer() {
|
|
917
|
+
return this.mGapBuffer;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Set text directly
|
|
921
|
+
public void setText(String text) {
|
|
922
|
+
mGapBuffer = new GapBuffer(text);
|
|
923
|
+
clearSyntaxCache();
|
|
924
|
+
dismissAutoComplete();
|
|
925
|
+
invalidate();
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Set font size in px with bounds and adjust scroll to keep relative position
|
|
929
|
+
public void setTextSize(float px) {
|
|
930
|
+
float min = ScreenUtils.dip2px(getContext(), 10);
|
|
931
|
+
float max = ScreenUtils.dip2px(getContext(), 30);
|
|
932
|
+
|
|
933
|
+
if (px < min) px = min;
|
|
934
|
+
if (px > max) px = max;
|
|
935
|
+
|
|
936
|
+
if (px == mTextPaint.getTextSize()) return;
|
|
937
|
+
|
|
938
|
+
int currentScrollY = getScrollY();
|
|
939
|
+
int currentLine = Math.max(1, currentScrollY / getLineHeight());
|
|
940
|
+
float lineFraction = (float) (currentScrollY % getLineHeight()) / getLineHeight();
|
|
941
|
+
|
|
942
|
+
mTextPaint.setTextSize(px);
|
|
943
|
+
|
|
944
|
+
adjustCursorPosition();
|
|
945
|
+
if (isSelectMode) {
|
|
946
|
+
adjustSelectRange(selectionStart, selectionEnd);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Update magnifier position if active
|
|
950
|
+
if (mIsMagnifierShowing && mMagnifierEnabled) {
|
|
951
|
+
// updateMagnifier(mCursorPosX, mCursorPosY + getLineHeight());
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
int newLineHeight = getLineHeight();
|
|
955
|
+
int newScrollY = (int) (currentLine * newLineHeight + lineFraction * newLineHeight);
|
|
956
|
+
newScrollY = Math.max(0, Math.min(newScrollY, getMaxScrollY()));
|
|
957
|
+
int newScrollX = getScrollX();
|
|
958
|
+
|
|
959
|
+
scrollTo(newScrollX, newScrollY);
|
|
960
|
+
postInvalidate();
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Toggle edit mode
|
|
964
|
+
public void setEditedMode(boolean editMode) {
|
|
965
|
+
isEditedMode = editMode;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Get edit mode
|
|
969
|
+
public boolean getEditedMode() {
|
|
970
|
+
return isEditedMode;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Set typeface
|
|
974
|
+
public void setTypeface(Typeface typeface) {
|
|
975
|
+
mTextPaint.setTypeface(typeface);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Set listener for text change
|
|
979
|
+
public void setOnTextChangedListener(OnTextChangedListener listener) {
|
|
980
|
+
mTextListener = listener;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Return left padding + line number width
|
|
984
|
+
|
|
985
|
+
public int getLeftSpace() {
|
|
986
|
+
int lineNumberWidth = getLineNumberWidth();
|
|
987
|
+
int separatorWidth = 2;
|
|
988
|
+
int contentPadding = 10;
|
|
989
|
+
|
|
990
|
+
return getPaddingLeft() + lineNumberWidth + SPACEING * 2 + separatorWidth + contentPadding;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Measure text width
|
|
994
|
+
public int measureText(String text) {
|
|
995
|
+
return (int) Math.ceil(mTextPaint.measureText(text));
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Get single line height in px
|
|
999
|
+
public int getLineHeight() {
|
|
1000
|
+
TextPaint.FontMetricsInt metrics = mTextPaint.getFontMetricsInt();
|
|
1001
|
+
return metrics.bottom - metrics.top;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Get the start offset of a line
|
|
1005
|
+
private int getLineStart(int lineNumber) {
|
|
1006
|
+
return mGapBuffer.getLineOffset(lineNumber);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Get the last line offset of a line
|
|
1010
|
+
private int getLineEnd(int line) {
|
|
1011
|
+
int nextLine = line + 1;
|
|
1012
|
+
|
|
1013
|
+
if (nextLine > getLineCount()) {
|
|
1014
|
+
// last line → return end of buffer
|
|
1015
|
+
return mGapBuffer.length();
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
return getLineStart(nextLine) - 1; // end before newline
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Find which line an offset is on
|
|
1022
|
+
private int getOffsetLine(int offset) {
|
|
1023
|
+
return mGapBuffer.findLineNumber(offset);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Get text size px
|
|
1027
|
+
public float getTextSize() {
|
|
1028
|
+
return mTextPaint.getTextSize();
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Get number of lines
|
|
1032
|
+
public int getLineCount() {
|
|
1033
|
+
return mGapBuffer.getLineCount();
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Width needed to draw line numbers
|
|
1037
|
+
private int getLineNumberWidth() {
|
|
1038
|
+
return measureText(Integer.toString(getLineCount()));
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// get the column where the cursor is placed
|
|
1042
|
+
public int getColumn() {
|
|
1043
|
+
if (mCursorLine < 1 || mCursorLine > getLineCount()) {
|
|
1044
|
+
return 0;
|
|
1045
|
+
}
|
|
1046
|
+
int lineStart = getLineStart(mCursorLine);
|
|
1047
|
+
return mCursorIndex - lineStart;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// Helper used by clipboard panel to show selection
|
|
1051
|
+
public void selectText(boolean enable) {
|
|
1052
|
+
if (!enable) clearSelectionMenu();
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Get bounding box for caret at index (for popup positioning)
|
|
1056
|
+
public Rect getBoundingBox(int index) {
|
|
1057
|
+
int left = getLeftSpace();
|
|
1058
|
+
int line = getOffsetLine(index);
|
|
1059
|
+
int lineStart = getLineStart(line);
|
|
1060
|
+
String text = mGapBuffer.substring(lineStart, Math.min(index, mGapBuffer.length()));
|
|
1061
|
+
int x = left + measureText(text);
|
|
1062
|
+
int y = (line - 1) * getLineHeight();
|
|
1063
|
+
|
|
1064
|
+
// Translate for scroll and padding
|
|
1065
|
+
int viewX = x - getScrollX() + getPaddingLeft();
|
|
1066
|
+
int viewY = y - getScrollY() + getPaddingTop();
|
|
1067
|
+
|
|
1068
|
+
// Make sure panel appears above caret line
|
|
1069
|
+
return new Rect(viewX, viewY - getLineHeight() * 2, viewX + getLineHeight(), viewY);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Get caret index
|
|
1073
|
+
public int getCaretPosition() {
|
|
1074
|
+
return mCursorIndex;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Get selected text string
|
|
1078
|
+
public String getSelectedText() {
|
|
1079
|
+
if (isSelectMode && selectionStart < selectionEnd) {
|
|
1080
|
+
return mGapBuffer.substring(selectionStart, selectionEnd);
|
|
1081
|
+
}
|
|
1082
|
+
return "";
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Getting the selected text length
|
|
1086
|
+
public int getSelectionLength() {
|
|
1087
|
+
if (!isSelectMode) return 0;
|
|
1088
|
+
return selectionEnd - selectionStart;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Insert text at caret, with auto-indent, autocomplete, and blinking handling
|
|
1092
|
+
public void insertText(String text) {
|
|
1093
|
+
if (text == null || text.isEmpty()) return;
|
|
1094
|
+
|
|
1095
|
+
int insertAt = mCursorIndex;
|
|
1096
|
+
|
|
1097
|
+
// If selection exists → replace it
|
|
1098
|
+
if (isSelectMode && selectionStart != selectionEnd) {
|
|
1099
|
+
|
|
1100
|
+
int s = Math.min(selectionStart, selectionEnd);
|
|
1101
|
+
int e = Math.max(selectionStart, selectionEnd);
|
|
1102
|
+
|
|
1103
|
+
mGapBuffer.beginBatchEdit();
|
|
1104
|
+
mGapBuffer.markSelectionBefore(selectionStart, selectionEnd, true);
|
|
1105
|
+
|
|
1106
|
+
// Replace selection in one undo-able step
|
|
1107
|
+
mGapBuffer.replace(s, e, text, true);
|
|
1108
|
+
|
|
1109
|
+
// Set new cursor after inserted text
|
|
1110
|
+
insertAt = s + text.length();
|
|
1111
|
+
isSelectMode = false;
|
|
1112
|
+
selectionStart = selectionEnd = -1;
|
|
1113
|
+
|
|
1114
|
+
mGapBuffer.markSelectionAfter(insertAt, insertAt, false);
|
|
1115
|
+
mGapBuffer.endBatchEdit();
|
|
1116
|
+
|
|
1117
|
+
} else {
|
|
1118
|
+
|
|
1119
|
+
// No selection → simple insert with undo snapshot
|
|
1120
|
+
mGapBuffer.beginBatchEdit();
|
|
1121
|
+
mGapBuffer.markSelectionBefore(mCursorIndex, mCursorIndex, false);
|
|
1122
|
+
|
|
1123
|
+
mGapBuffer.insert(mCursorIndex, text, true);
|
|
1124
|
+
insertAt = mCursorIndex + text.length();
|
|
1125
|
+
|
|
1126
|
+
mGapBuffer.markSelectionAfter(insertAt, insertAt, false);
|
|
1127
|
+
mGapBuffer.endBatchEdit();
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
mCursorIndex = insertAt;
|
|
1131
|
+
mCursorLine = getOffsetLine(mCursorIndex);
|
|
1132
|
+
|
|
1133
|
+
adjustCursorPosition();
|
|
1134
|
+
scrollToVisable();
|
|
1135
|
+
postInvalidate();
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// Selection getters
|
|
1139
|
+
public int getSelectionStart() {
|
|
1140
|
+
return selectionStart;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
public int getSelectionEnd() {
|
|
1144
|
+
return selectionEnd;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Delete selected text if in selection mode
|
|
1148
|
+
public void deleteSelectedText() {
|
|
1149
|
+
if (isSelectMode && selectionStart < selectionEnd) {
|
|
1150
|
+
mGapBuffer.delete(selectionStart, selectionEnd, true);
|
|
1151
|
+
mCursorIndex = selectionStart;
|
|
1152
|
+
isSelectMode = false;
|
|
1153
|
+
adjustCursorPosition();
|
|
1154
|
+
scrollToVisable();
|
|
1155
|
+
dismissAutoComplete();
|
|
1156
|
+
postDelayed(mWordUpdateRunnable, WORD_UPDATE_DELAY);
|
|
1157
|
+
postInvalidate();
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Select word at caret
|
|
1162
|
+
public void selectWordAtCursor() {
|
|
1163
|
+
if (mGapBuffer.length() == 0) return;
|
|
1164
|
+
|
|
1165
|
+
int start = mCursorIndex;
|
|
1166
|
+
int end = mCursorIndex;
|
|
1167
|
+
|
|
1168
|
+
// Find word start
|
|
1169
|
+
while (start > 0 && Character.isJavaIdentifierPart(mGapBuffer.charAt(start - 1))) {
|
|
1170
|
+
start--;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Find word end
|
|
1174
|
+
while (end < mGapBuffer.length() && Character.isJavaIdentifierPart(mGapBuffer.charAt(end))) {
|
|
1175
|
+
end++;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
if (start < end) {
|
|
1179
|
+
selectionStart = start;
|
|
1180
|
+
selectionEnd = end;
|
|
1181
|
+
isSelectMode = true;
|
|
1182
|
+
adjustSelectRange(start, end);
|
|
1183
|
+
postInvalidate();
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Move caret to next word start
|
|
1188
|
+
public void moveToNextWord() {
|
|
1189
|
+
if (mCursorIndex >= mGapBuffer.length()) return;
|
|
1190
|
+
|
|
1191
|
+
int newPos = mCursorIndex;
|
|
1192
|
+
|
|
1193
|
+
// Skip current word if we're in one
|
|
1194
|
+
while (newPos < mGapBuffer.length() && Character.isJavaIdentifierPart(mGapBuffer.charAt(newPos))) {
|
|
1195
|
+
newPos++;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// Skip non-word characters
|
|
1199
|
+
while (newPos < mGapBuffer.length() && !Character.isJavaIdentifierPart(mGapBuffer.charAt(newPos))) {
|
|
1200
|
+
newPos++;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
setCursorPosition(newPos);
|
|
1204
|
+
scrollToVisable();
|
|
1205
|
+
postInvalidate();
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Move caret to previous word start
|
|
1209
|
+
public void moveToPreviousWord() {
|
|
1210
|
+
if (mCursorIndex <= 0) return;
|
|
1211
|
+
|
|
1212
|
+
int newPos = mCursorIndex - 1;
|
|
1213
|
+
|
|
1214
|
+
// Skip non-word characters backwards
|
|
1215
|
+
while (newPos > 0 && !Character.isJavaIdentifierPart(mGapBuffer.charAt(newPos))) {
|
|
1216
|
+
newPos--;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Skip word characters backwards
|
|
1220
|
+
while (newPos > 0 && Character.isJavaIdentifierPart(mGapBuffer.charAt(newPos - 1))) {
|
|
1221
|
+
newPos--;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
setCursorPosition(newPos);
|
|
1225
|
+
scrollToVisable();
|
|
1226
|
+
postInvalidate();
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// Select all text
|
|
1230
|
+
public void selectAll() {
|
|
1231
|
+
removeCallbacks(blinkAction);
|
|
1232
|
+
mCursorVisiable = false;
|
|
1233
|
+
mHandleMiddleVisable = false; // ← CHANGE TO FALSE
|
|
1234
|
+
isSelectMode = true;
|
|
1235
|
+
|
|
1236
|
+
// at first index
|
|
1237
|
+
selectionStart = 0;
|
|
1238
|
+
// at last index
|
|
1239
|
+
selectionEnd = mGapBuffer.length();
|
|
1240
|
+
|
|
1241
|
+
// set handle left at first position
|
|
1242
|
+
selectHandleLeftX = getLeftSpace();
|
|
1243
|
+
selectHandleLeftY = getLineHeight();
|
|
1244
|
+
|
|
1245
|
+
// set handle right at last position
|
|
1246
|
+
selectHandleRightX = getLeftSpace() + getLineWidth(getLineCount());
|
|
1247
|
+
selectHandleRightY = getLineCount() * getLineHeight();
|
|
1248
|
+
|
|
1249
|
+
// set cursor index and position
|
|
1250
|
+
setCursorPosition(selectionEnd);
|
|
1251
|
+
|
|
1252
|
+
if (!mReplaceList.isEmpty())
|
|
1253
|
+
mReplaceList.clear();
|
|
1254
|
+
smoothScrollTo(0, Math.max(getLineCount() * getLineHeight() - getHeight() + getLineHeight() * 2, 0));
|
|
1255
|
+
showTextSelectionWindow();
|
|
1256
|
+
postInvalidate();
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// Helper method to check if all texts are selected
|
|
1260
|
+
public boolean isAllTextSelected() {
|
|
1261
|
+
return selectionStart == 0 &&
|
|
1262
|
+
selectionEnd == mGapBuffer.length() &&
|
|
1263
|
+
selectionStart != selectionEnd &&
|
|
1264
|
+
mGapBuffer.length() > 0;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// Clear selection and related UI
|
|
1268
|
+
public void clearSelectionMenu() {
|
|
1269
|
+
isSelectMode = false;
|
|
1270
|
+
mHandleMiddleVisable = false;
|
|
1271
|
+
mIsLineSelectionMode = false;
|
|
1272
|
+
mStartSelectionLine = -1;
|
|
1273
|
+
mEndSelectionLine = -1;
|
|
1274
|
+
mFirstSelectedLine = -1;
|
|
1275
|
+
mSecondSelectedLine = -1;
|
|
1276
|
+
mWaitingForSecondSelection = false;
|
|
1277
|
+
mSelectionHandler.removeCallbacks(mClearSelectionRunnable);
|
|
1278
|
+
dismissAutoComplete();
|
|
1279
|
+
onCursorOrSelectionChanged();
|
|
1280
|
+
postInvalidate();
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Check selection mode
|
|
1284
|
+
public boolean isSelectMode() {
|
|
1285
|
+
return isSelectMode;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// Check if focused on line number area
|
|
1289
|
+
private boolean isInLineNumberArea(float x, float y) {
|
|
1290
|
+
int lineNumberWidth = getLineNumberWidth() + SPACEING * 2;
|
|
1291
|
+
return x >= getPaddingLeft() && x <= getPaddingLeft() + lineNumberWidth;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// Get line from Y dir.
|
|
1295
|
+
private int getLineFromY(float y) {
|
|
1296
|
+
int line = (int) (y / getLineHeight()) + 1;
|
|
1297
|
+
return Math.max(1, Math.min(line, getLineCount()));
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// Get raw line string
|
|
1301
|
+
public String getLine(int lineNumber) {
|
|
1302
|
+
return mGapBuffer.getLine(lineNumber);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// Width of specified line text
|
|
1306
|
+
private int getLineWidth(int lineNumber) {
|
|
1307
|
+
return measureText(getLine(lineNumber));
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// Width of lines
|
|
1311
|
+
public int getLineWidth() {
|
|
1312
|
+
return lineWidth;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Width of space
|
|
1316
|
+
public int getSpaceWidth() {
|
|
1317
|
+
return spaceWidth;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// Get maximum scrollable X
|
|
1321
|
+
public int getMaxScrollX() {
|
|
1322
|
+
return Math.max(0, getLeftSpace() + lineWidth + spaceWidth * 4 - getWidth());
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// Get maximum scrollable Y
|
|
1326
|
+
public int getMaxScrollY() {
|
|
1327
|
+
return Math.max(0, (getLineCount() + 2) * getLineHeight() - getHeight());
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// ---------- Magnifier ----------
|
|
1331
|
+
// Enable or disable magnifier usage, only for Android 8+
|
|
1332
|
+
public void setMagnifierEnabled(boolean enabled) {
|
|
1333
|
+
mMagnifierEnabled = enabled;
|
|
1334
|
+
if (!enabled && mIsMagnifierShowing) {
|
|
1335
|
+
dismissMagnifier();
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// Check magnifier enabled
|
|
1340
|
+
public boolean isMagnifierEnabled() {
|
|
1341
|
+
return mMagnifierEnabled;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// Show magnifier centered near content coords
|
|
1345
|
+
private void showMagnifier(float x, float y) {
|
|
1346
|
+
if (!mMagnifierEnabled || mMagnifier == null) return;
|
|
1347
|
+
try {
|
|
1348
|
+
hideTextSelectionWindow();
|
|
1349
|
+
// Convert content coordinates to screen coordinates
|
|
1350
|
+
float adjustedX = x - getScrollX();
|
|
1351
|
+
float adjustedY = y - getScrollY();
|
|
1352
|
+
|
|
1353
|
+
// Adjust Y to center magnifier on the current line's text
|
|
1354
|
+
float lineHeight = getLineHeight();
|
|
1355
|
+
adjustedY -= lineHeight * 0.5f; // Center on the current line (baseline)
|
|
1356
|
+
|
|
1357
|
+
// Account for padding and canvas translation
|
|
1358
|
+
adjustedY += getPaddingTop();
|
|
1359
|
+
|
|
1360
|
+
// Ensure magnifier stays within view bounds
|
|
1361
|
+
adjustedX = Math.max(50, Math.min(adjustedX, getWidth() - 50));
|
|
1362
|
+
adjustedY = Math.max(50, Math.min(adjustedY, getHeight() - 50));
|
|
1363
|
+
|
|
1364
|
+
// Debug logging
|
|
1365
|
+
Log.d(TAG, "showMagnifier: x=" + adjustedX + ", y=" + adjustedY + ", rawX=" + x + ", rawY=" + y + ", scrollY=" + getScrollY() + ", lineHeight=" + lineHeight);
|
|
1366
|
+
|
|
1367
|
+
mMagnifier.show((int) adjustedX, (int) adjustedY);
|
|
1368
|
+
mMagnifierX = adjustedX;
|
|
1369
|
+
mMagnifierY = adjustedY;
|
|
1370
|
+
mIsMagnifierShowing = true;
|
|
1371
|
+
} catch (Exception e) {
|
|
1372
|
+
Log.e(TAG, "Error showing magnifier: " + e.getMessage());
|
|
1373
|
+
dismissMagnifier();
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Update magnifier position smoothly
|
|
1378
|
+
private void updateMagnifier(float x, float y) {
|
|
1379
|
+
if (!mIsMagnifierShowing || !mMagnifierEnabled || mMagnifier == null) return;
|
|
1380
|
+
try {
|
|
1381
|
+
// Convert content coordinates to screen coordinates
|
|
1382
|
+
float adjustedX = x - getScrollX();
|
|
1383
|
+
float adjustedY = y - getScrollY();
|
|
1384
|
+
|
|
1385
|
+
// Adjust Y to center magnifier on the current line's text
|
|
1386
|
+
float lineHeight = getLineHeight();
|
|
1387
|
+
adjustedY -= lineHeight * 0.5f; // Center on the current line (baseline)
|
|
1388
|
+
|
|
1389
|
+
// Account for padding and canvas translation
|
|
1390
|
+
adjustedY += getPaddingTop();
|
|
1391
|
+
|
|
1392
|
+
// Ensure magnifier stays within view bounds
|
|
1393
|
+
adjustedX = Math.max(50, Math.min(adjustedX, getWidth() - 50));
|
|
1394
|
+
adjustedY = Math.max(50, Math.min(adjustedY, getHeight() - 50));
|
|
1395
|
+
|
|
1396
|
+
// Smooth update only if significant movement
|
|
1397
|
+
if (Math.abs(adjustedX - mMagnifierX) > 1 || Math.abs(adjustedY - mMagnifierY) > 1) {
|
|
1398
|
+
Log.d(TAG, "updateMagnifier: x=" + adjustedX + ", y=" + adjustedY + ", rawX=" + x + ", rawY=" + y + ", scrollY=" + getScrollY() + ", lineHeight=" + lineHeight);
|
|
1399
|
+
|
|
1400
|
+
mMagnifier.show((int) adjustedX, (int) adjustedY);
|
|
1401
|
+
mMagnifierX = adjustedX;
|
|
1402
|
+
mMagnifierY = adjustedY;
|
|
1403
|
+
}
|
|
1404
|
+
} catch (Exception e) {
|
|
1405
|
+
Log.e(TAG, "Error updating magnifier: " + e.getMessage());
|
|
1406
|
+
dismissMagnifier();
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// Dismiss magnifier if visible
|
|
1411
|
+
private void dismissMagnifier() {
|
|
1412
|
+
if (mIsMagnifierShowing && mMagnifier != null) {
|
|
1413
|
+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
|
1414
|
+
try {
|
|
1415
|
+
mMagnifier.dismiss();
|
|
1416
|
+
mIsMagnifierShowing = false;
|
|
1417
|
+
} catch (Exception e) {
|
|
1418
|
+
Log.e(TAG, "Error dismissing magnifier: " + e.getMessage());
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// Syntax Helpers
|
|
1425
|
+
public void setSyntaxLanguageFileName(String languageFile) {
|
|
1426
|
+
mHighlighter = new MHSyntaxHighlightEngine(getContext(), mTextPaint, languageFile, isSyntaxDarkMode);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// Syntax dark mode helper
|
|
1430
|
+
public void setSyntaxDarkMode(boolean isDark) {
|
|
1431
|
+
isSyntaxDarkMode = isDark;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// Get syntax comment block of exists
|
|
1435
|
+
public String getCommentBlock() {
|
|
1436
|
+
String block = mHighlighter.getCommentSyntaxBlock();
|
|
1437
|
+
if (block == null) return "";
|
|
1438
|
+
return block;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
public void setMenuStyle(ClipboardPanel.MenuDisplayMode mode) {
|
|
1442
|
+
mClipboardPanel.setMenuDisplayMode(mode);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// ---------- Scrolling Helpers ----------
|
|
1446
|
+
/**
|
|
1447
|
+
* Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
|
|
1448
|
+
*
|
|
1449
|
+
* @param dx the number of pixels to scroll by on the X axis
|
|
1450
|
+
* @param dy the number of pixels to scroll by on the Y axis
|
|
1451
|
+
*/
|
|
1452
|
+
public final void smoothScrollBy(int dx, int dy) {
|
|
1453
|
+
if (getHeight() == 0) {
|
|
1454
|
+
// Nothing to do.
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
|
|
1458
|
+
if (duration > DEFAULT_DURATION) {
|
|
1459
|
+
mScroller.startScroll(getScrollX(), getScrollY(), dx, dy);
|
|
1460
|
+
postInvalidateOnAnimation();
|
|
1461
|
+
} else {
|
|
1462
|
+
if (!mScroller.isFinished()) {
|
|
1463
|
+
mScroller.abortAnimation();
|
|
1464
|
+
}
|
|
1465
|
+
scrollBy(dx, dy);
|
|
1466
|
+
}
|
|
1467
|
+
mLastScroll = AnimationUtils.currentAnimationTimeMillis();
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Like {@link #scrollTo}, but scroll smoothly instead of immediately.
|
|
1472
|
+
*
|
|
1473
|
+
* @param x the position where to scroll on the X axis
|
|
1474
|
+
* @param y the position where to scroll on the Y axis
|
|
1475
|
+
*/
|
|
1476
|
+
public final void smoothScrollTo(int x, int y) {
|
|
1477
|
+
smoothScrollBy(x - getScrollX(), y - getScrollY());
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// Compute fling/scroll animation progress
|
|
1481
|
+
@Override
|
|
1482
|
+
public void computeScroll() {
|
|
1483
|
+
super.computeScroll();
|
|
1484
|
+
if (mScroller.computeScrollOffset()) {
|
|
1485
|
+
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
|
|
1486
|
+
postInvalidate();
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// Scroll to ensure caret is visible with margins
|
|
1491
|
+
private void scrollToVisable() {
|
|
1492
|
+
// Only scroll if cursor is significantly out of view
|
|
1493
|
+
int dx = 0;
|
|
1494
|
+
int leftMargin = spaceWidth * 3;
|
|
1495
|
+
int rightMargin = screenWidth - spaceWidth * 2;
|
|
1496
|
+
|
|
1497
|
+
if (mCursorPosX - getScrollX() < leftMargin) {
|
|
1498
|
+
dx = mCursorPosX - getScrollX() - leftMargin;
|
|
1499
|
+
} else if (mCursorPosX - getScrollX() > rightMargin) {
|
|
1500
|
+
dx = mCursorPosX - getScrollX() - rightMargin;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
int dy = 0;
|
|
1504
|
+
int topMargin = getLineHeight();
|
|
1505
|
+
int bottomMargin = getHeight() - getLineHeight();
|
|
1506
|
+
|
|
1507
|
+
if (mCursorPosY - getScrollY() < topMargin) {
|
|
1508
|
+
dy = mCursorPosY - getScrollY() - topMargin;
|
|
1509
|
+
} else if (mCursorPosY - getScrollY() > bottomMargin) {
|
|
1510
|
+
dy = mCursorPosY - getScrollY() - bottomMargin;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// Only scroll if necessary
|
|
1514
|
+
if (dx != 0 || dy != 0) {
|
|
1515
|
+
smoothScrollBy(dx, dy);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// ---------- Syntax / Highlights ----------
|
|
1520
|
+
// Clear syntax highlight cache
|
|
1521
|
+
private void clearSyntaxCache() {
|
|
1522
|
+
if (mHighlighter != null) {
|
|
1523
|
+
mHighlighter.clearCache();
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// Call this method whenever the text content changes
|
|
1528
|
+
public void onTextChanged() {
|
|
1529
|
+
clearSyntaxCache(); // keep cache fresh
|
|
1530
|
+
mTextListener.onTextChanged();
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// ---------- Insert / Delete with handling ----------
|
|
1534
|
+
// Insert text with auto-indent, autocomplete and selection handling
|
|
1535
|
+
private void insert(String text) {
|
|
1536
|
+
if (!isEditedMode) return; // nothing to do
|
|
1537
|
+
if (isSelectMode) {
|
|
1538
|
+
mGapBuffer.beginBatchEdit();
|
|
1539
|
+
delete();
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
if (text.equals("\n") && mAutoIndentEnabled) {
|
|
1543
|
+
String indent = getAutoIndent();
|
|
1544
|
+
if (!indent.isEmpty()) {
|
|
1545
|
+
text = "\n" + indent;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
removeCallbacks(blinkAction);
|
|
1550
|
+
mCursorVisiable = true;
|
|
1551
|
+
mHandleMiddleVisable = false;
|
|
1552
|
+
|
|
1553
|
+
mCurrentPrefix = getCurrentPrefix();
|
|
1554
|
+
|
|
1555
|
+
mGapBuffer.insert(mCursorIndex, text, true);
|
|
1556
|
+
|
|
1557
|
+
// Handle auto-complete
|
|
1558
|
+
if (!text.trim().isEmpty()) {
|
|
1559
|
+
if (!mCurrentPrefix.isEmpty()) {
|
|
1560
|
+
Log.d(TAG, "Prefix: " + mCurrentPrefix);
|
|
1561
|
+
filterAndShowSuggestions(mCurrentPrefix + text);
|
|
1562
|
+
} else {
|
|
1563
|
+
dismissAutoComplete();
|
|
1564
|
+
}
|
|
1565
|
+
} else {
|
|
1566
|
+
dismissAutoComplete();
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
if (mGapBuffer.isBatchEdit())
|
|
1570
|
+
mGapBuffer.endBatchEdit();
|
|
1571
|
+
|
|
1572
|
+
// calculate the cursor index and line
|
|
1573
|
+
int length = text.length();
|
|
1574
|
+
mCursorIndex += length;
|
|
1575
|
+
mCursorLine = getOffsetLine(mCursorIndex);
|
|
1576
|
+
adjustCursorPosition();
|
|
1577
|
+
|
|
1578
|
+
onTextChanged();
|
|
1579
|
+
scrollToVisable();
|
|
1580
|
+
postInvalidate();
|
|
1581
|
+
postDelayed(blinkAction, BLINK_TIMEOUT);
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// Insert words from typed text to word set
|
|
1585
|
+
private void extractWordsFromText(String newText) {
|
|
1586
|
+
if (newText == null || newText.isEmpty()) return;
|
|
1587
|
+
mWordSet.add(newText); // Fast O(1) operation
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// Delete char before caret or currently selected range
|
|
1591
|
+
public void delete() {
|
|
1592
|
+
if (!isEditedMode) return; // nothing to do
|
|
1593
|
+
if (mCursorIndex <= 0) return;
|
|
1594
|
+
|
|
1595
|
+
removeCallbacks(blinkAction);
|
|
1596
|
+
mCursorVisiable = true;
|
|
1597
|
+
mHandleMiddleVisable = false;
|
|
1598
|
+
|
|
1599
|
+
if (isSelectMode) {
|
|
1600
|
+
isSelectMode = false;
|
|
1601
|
+
mGapBuffer.delete(selectionStart, selectionEnd, true);
|
|
1602
|
+
mCursorIndex -= selectionEnd - selectionStart;
|
|
1603
|
+
} else {
|
|
1604
|
+
mGapBuffer.delete(mCursorIndex - 1, mCursorIndex, true);
|
|
1605
|
+
mCursorIndex--;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// calculate cursor index and line
|
|
1609
|
+
mCursorLine = getOffsetLine(mCursorIndex);
|
|
1610
|
+
adjustCursorPosition();
|
|
1611
|
+
onCursorOrSelectionChanged();
|
|
1612
|
+
|
|
1613
|
+
onTextChanged();
|
|
1614
|
+
scrollToVisable();
|
|
1615
|
+
postInvalidate();
|
|
1616
|
+
postDelayed(blinkAction, BLINK_TIMEOUT);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// Handle forward delete (placeholder call used earlier)
|
|
1620
|
+
private void handleForwardDelete() {
|
|
1621
|
+
if (!isEditedMode) return;
|
|
1622
|
+
if (mCursorIndex >= mGapBuffer.length()) return;
|
|
1623
|
+
|
|
1624
|
+
removeCallbacks(blinkAction);
|
|
1625
|
+
mCursorVisiable = true;
|
|
1626
|
+
mHandleMiddleVisable = false;
|
|
1627
|
+
|
|
1628
|
+
if (isSelectMode) {
|
|
1629
|
+
isSelectMode = false;
|
|
1630
|
+
mGapBuffer.delete(selectionStart, selectionEnd, true);
|
|
1631
|
+
mCursorIndex = selectionStart;
|
|
1632
|
+
} else {
|
|
1633
|
+
mGapBuffer.delete(mCursorIndex, mCursorIndex + 1, true);
|
|
1634
|
+
// Cursor index stays the same when forward deleting
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
mCursorLine = getOffsetLine(mCursorIndex);
|
|
1638
|
+
adjustCursorPosition();
|
|
1639
|
+
|
|
1640
|
+
onCursorOrSelectionChanged();
|
|
1641
|
+
|
|
1642
|
+
clearSyntaxCache();
|
|
1643
|
+
onTextChanged();
|
|
1644
|
+
scrollToVisable();
|
|
1645
|
+
|
|
1646
|
+
mCurrentPrefix = getCurrentPrefix();
|
|
1647
|
+
if (mCurrentPrefix.isEmpty()) {
|
|
1648
|
+
dismissAutoComplete();
|
|
1649
|
+
} else {
|
|
1650
|
+
filterAndShowSuggestions(mCurrentPrefix);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
postInvalidate();
|
|
1654
|
+
postDelayed(blinkAction, BLINK_TIMEOUT);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// ---------- Clipboard (copy/cut/paste/share) ----------
|
|
1658
|
+
// Copy selected text to clipboard
|
|
1659
|
+
public void copy() {
|
|
1660
|
+
String text = getSelectedText();
|
|
1661
|
+
if (text != null && !text.equals("")) {
|
|
1662
|
+
ClipData data = ClipData.newPlainText("content", text);
|
|
1663
|
+
mClipboard.setPrimaryClip(data);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// Cut selected text to clipboard (copy then delete)
|
|
1668
|
+
public void cut() {
|
|
1669
|
+
copy();
|
|
1670
|
+
delete();
|
|
1671
|
+
isSelectMode = false;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// Paste from clipboard at caret
|
|
1675
|
+
public void paste() {
|
|
1676
|
+
if (mClipboard.hasPrimaryClip()) {
|
|
1677
|
+
ClipDescription description = mClipboard.getPrimaryClipDescription();
|
|
1678
|
+
if (description.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
|
|
1679
|
+
ClipData data = mClipboard.getPrimaryClip();
|
|
1680
|
+
ClipData.Item item = data.getItemAt(0);
|
|
1681
|
+
String text = item.getText().toString();
|
|
1682
|
+
insert(text);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// Share currently selected text via ACTION_SEND
|
|
1688
|
+
public void shareText() {
|
|
1689
|
+
if (isSelectMode) {
|
|
1690
|
+
String selectedText = getSelectedText();
|
|
1691
|
+
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
|
1692
|
+
shareIntent.setType("text/plain");
|
|
1693
|
+
shareIntent.putExtra(Intent.EXTRA_TEXT, selectedText);
|
|
1694
|
+
getContext().startActivity(Intent.createChooser(shareIntent, "Share text"));
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// ---------- Search / Replace operations ----------
|
|
1699
|
+
// Scroll to the found match position
|
|
1700
|
+
private void scrollToFindPosition(int curr) {
|
|
1701
|
+
int first = mReplaceList.get(curr).first;
|
|
1702
|
+
int second = mReplaceList.get(curr).second;
|
|
1703
|
+
|
|
1704
|
+
setCursorPosition(second);
|
|
1705
|
+
adjustSelectRange(first, second);
|
|
1706
|
+
|
|
1707
|
+
smoothScrollTo(Math.max(0, selectHandleLeftX - getWidth() / 2),
|
|
1708
|
+
Math.max(0, selectHandleLeftY - getHeight() / 2));
|
|
1709
|
+
postInvalidate();
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// Find current replace list index for selectionStart/End via binary search
|
|
1713
|
+
private int current() {
|
|
1714
|
+
// Comparator implementation
|
|
1715
|
+
Comparator<Pair<Integer, Integer>> comparator = new Comparator<Pair<Integer, Integer>>() {
|
|
1716
|
+
@Override
|
|
1717
|
+
public int compare(Pair<Integer, Integer> a, Pair<Integer, Integer> b) {
|
|
1718
|
+
int result = a.first - b.first;
|
|
1719
|
+
return result == 0 ? a.second - b.second : result;
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
|
|
1723
|
+
// binarySearch using the comparator
|
|
1724
|
+
return Collections.binarySearch(mReplaceList,
|
|
1725
|
+
new Pair<Integer, Integer>(selectionStart, selectionEnd),
|
|
1726
|
+
comparator);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// Move to previous match
|
|
1730
|
+
public void previous() {
|
|
1731
|
+
int currIndex = current();
|
|
1732
|
+
int prev = --currIndex;
|
|
1733
|
+
if (prev < 0) {
|
|
1734
|
+
prev = mReplaceList.size() - 1;
|
|
1735
|
+
}
|
|
1736
|
+
scrollToFindPosition(prev);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// Move to next match
|
|
1740
|
+
public void next() {
|
|
1741
|
+
int currIndex = current();
|
|
1742
|
+
int next = ++currIndex;
|
|
1743
|
+
if (next >= mReplaceList.size()) {
|
|
1744
|
+
next = 0;
|
|
1745
|
+
}
|
|
1746
|
+
scrollToFindPosition(next);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// Find all matches for regex in buffer
|
|
1750
|
+
public void find(String regex) {
|
|
1751
|
+
if (!mReplaceList.isEmpty())
|
|
1752
|
+
mReplaceList.clear();
|
|
1753
|
+
|
|
1754
|
+
Matcher matcher = Pattern.compile(regex).matcher(mGapBuffer.toString());
|
|
1755
|
+
|
|
1756
|
+
while (matcher.find()) {
|
|
1757
|
+
mReplaceList.add(new Pair<Integer, Integer>(matcher.start(), matcher.end()));
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// Replace first match
|
|
1762
|
+
public void replaceFirst(String replacement) {
|
|
1763
|
+
if (!mReplaceList.isEmpty() && isEditedMode) {
|
|
1764
|
+
int start = mReplaceList.get(0).first;
|
|
1765
|
+
int end = mReplaceList.get(0).second;
|
|
1766
|
+
|
|
1767
|
+
mGapBuffer.beginBatchEdit();
|
|
1768
|
+
mGapBuffer.replace(start, end, replacement, true);
|
|
1769
|
+
mGapBuffer.endBatchEdit();
|
|
1770
|
+
|
|
1771
|
+
int length = replacement.length();
|
|
1772
|
+
setCursorPosition(start + length);
|
|
1773
|
+
adjustSelectRange(start + length, start + length);
|
|
1774
|
+
|
|
1775
|
+
// remove the first item
|
|
1776
|
+
mReplaceList.remove(0);
|
|
1777
|
+
|
|
1778
|
+
int delta = start + length - end;
|
|
1779
|
+
// do not use the find(regex) method to re-find
|
|
1780
|
+
// recalculate replace list by index
|
|
1781
|
+
for (int i = 0; i < mReplaceList.size(); ++i) {
|
|
1782
|
+
int first = (int) mReplaceList.get(i).first + delta;
|
|
1783
|
+
int second = (int) mReplaceList.get(i).second + delta;
|
|
1784
|
+
mReplaceList.set(i, new Pair<Integer, Integer>(first, second));
|
|
1785
|
+
}
|
|
1786
|
+
} else {
|
|
1787
|
+
// if the replace Lists is empty
|
|
1788
|
+
// set the select mode false
|
|
1789
|
+
isSelectMode = false;
|
|
1790
|
+
}
|
|
1791
|
+
postInvalidate();
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// Replace all matches iteratively
|
|
1795
|
+
public void replaceAll(String replacement) {
|
|
1796
|
+
while (!mReplaceList.isEmpty() && isEditedMode) {
|
|
1797
|
+
replaceFirst(replacement);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// ---------- Cursor positioning ----------
|
|
1802
|
+
// Goto line number (1-based)
|
|
1803
|
+
public void gotoLine(int line) {
|
|
1804
|
+
line = Math.min(Math.max(line, 1), getLineCount());
|
|
1805
|
+
|
|
1806
|
+
// Clear any active selection to cancel it before navigating
|
|
1807
|
+
if (isSelectMode) {
|
|
1808
|
+
clearSelectionMenu();
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
mCursorIndex = getLineStart(line);
|
|
1812
|
+
mCursorLine = line;
|
|
1813
|
+
mCursorPosX = getLeftSpace();
|
|
1814
|
+
mCursorPosY = (line - 1) * getLineHeight();
|
|
1815
|
+
|
|
1816
|
+
smoothScrollTo(0, Math.max(line * getLineHeight() - getHeight() + getLineHeight() * 2, 0));
|
|
1817
|
+
postInvalidate(); // Ensure immediate redraw after clearing selection
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// Check undo available
|
|
1821
|
+
public boolean canUndo() {
|
|
1822
|
+
return mGapBuffer.canUndo();
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// Check redo available
|
|
1826
|
+
public boolean canRedo() {
|
|
1827
|
+
return mGapBuffer.canRedo();
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// Undo operation and restore cursor
|
|
1831
|
+
public void undo() {
|
|
1832
|
+
int index = mGapBuffer.undo();
|
|
1833
|
+
if (index >= 0) {
|
|
1834
|
+
mCursorIndex = index;
|
|
1835
|
+
mCursorLine = getOffsetLine(index);
|
|
1836
|
+
adjustCursorPosition();
|
|
1837
|
+
onTextChanged();
|
|
1838
|
+
scrollToVisable();
|
|
1839
|
+
|
|
1840
|
+
// restore selection (GapBuffer exposes the snapshot)
|
|
1841
|
+
int s = mGapBuffer.getLastUndoSelectionStart();
|
|
1842
|
+
int e = mGapBuffer.getLastUndoSelectionEnd();
|
|
1843
|
+
boolean mode = mGapBuffer.getLastUndoSelectionMode();
|
|
1844
|
+
|
|
1845
|
+
if (s >= 0 && e >= 0) {
|
|
1846
|
+
selectionStart = Math.max(0, Math.min(s, mGapBuffer.length()));
|
|
1847
|
+
selectionEnd = Math.max(0, Math.min(e, mGapBuffer.length()));
|
|
1848
|
+
isSelectMode = mode;
|
|
1849
|
+
updateSelectionHandles();
|
|
1850
|
+
} else {
|
|
1851
|
+
// no selection snapshot stored for this undo — clear selection
|
|
1852
|
+
isSelectMode = false;
|
|
1853
|
+
selectionStart = selectionEnd = -1;
|
|
1854
|
+
}
|
|
1855
|
+
postInvalidate();
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// Redo operation and restore cursor
|
|
1860
|
+
public void redo() {
|
|
1861
|
+
int index = mGapBuffer.redo();
|
|
1862
|
+
if (index >= 0) {
|
|
1863
|
+
mCursorIndex = index;
|
|
1864
|
+
mCursorLine = getOffsetLine(index);
|
|
1865
|
+
adjustCursorPosition();
|
|
1866
|
+
onTextChanged();
|
|
1867
|
+
scrollToVisable();
|
|
1868
|
+
|
|
1869
|
+
// restore selection for redo
|
|
1870
|
+
int s = mGapBuffer.getLastRedoSelectionStart();
|
|
1871
|
+
int e = mGapBuffer.getLastRedoSelectionEnd();
|
|
1872
|
+
boolean mode = mGapBuffer.getLastRedoSelectionMode();
|
|
1873
|
+
|
|
1874
|
+
if (s >= 0 && e >= 0) {
|
|
1875
|
+
selectionStart = Math.max(0, Math.min(s, mGapBuffer.length()));
|
|
1876
|
+
selectionEnd = Math.max(0, Math.min(e, mGapBuffer.length()));
|
|
1877
|
+
isSelectMode = mode;
|
|
1878
|
+
updateSelectionHandles();
|
|
1879
|
+
} else {
|
|
1880
|
+
isSelectMode = false;
|
|
1881
|
+
selectionStart = selectionEnd = -1;
|
|
1882
|
+
}
|
|
1883
|
+
postInvalidate();
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// ---------- Selection handle updates ----------
|
|
1888
|
+
// Update selection handle screen coordinates and caret
|
|
1889
|
+
private void updateSelectionHandles() {
|
|
1890
|
+
if (!isSelectMode) return;
|
|
1891
|
+
|
|
1892
|
+
int left = getLeftSpace();
|
|
1893
|
+
|
|
1894
|
+
// Start handle
|
|
1895
|
+
int startLine = getOffsetLine(selectionStart);
|
|
1896
|
+
int lineStart = getLineStart(startLine);
|
|
1897
|
+
String startText = mGapBuffer.substring(lineStart, Math.min(selectionStart, mGapBuffer.length()));
|
|
1898
|
+
selectHandleLeftX = left + (int) mTextPaint.measureText(startText);
|
|
1899
|
+
selectHandleLeftY = startLine * getLineHeight();
|
|
1900
|
+
|
|
1901
|
+
// End handle
|
|
1902
|
+
int endLine = getOffsetLine(selectionEnd);
|
|
1903
|
+
lineStart = getLineStart(endLine);
|
|
1904
|
+
String endText = mGapBuffer.substring(lineStart, Math.min(selectionEnd, mGapBuffer.length()));
|
|
1905
|
+
selectHandleRightX = left + (int) mTextPaint.measureText(endText);
|
|
1906
|
+
selectHandleRightY = endLine * getLineHeight();
|
|
1907
|
+
|
|
1908
|
+
// Update middle handle position
|
|
1909
|
+
mCursorPosX = (selectHandleLeftX + selectHandleRightX) / 2;
|
|
1910
|
+
mCursorPosY = (selectHandleLeftY + selectHandleRightY) / 2;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// Adjust cursor's screen coordinates based on mCursorLine and mCursorIndex
|
|
1914
|
+
private void adjustCursorPosition() {
|
|
1915
|
+
int start = getLineStart(mCursorLine);
|
|
1916
|
+
String text = mGapBuffer.substring(start, mCursorIndex);
|
|
1917
|
+
|
|
1918
|
+
// Use precise text measurement with the current text paint
|
|
1919
|
+
mCursorPosX = getLeftSpace() + (int) mTextPaint.measureText(text);
|
|
1920
|
+
mCursorPosY = (mCursorLine - 1) * getLineHeight();
|
|
1921
|
+
|
|
1922
|
+
// Ensure cursor stays within bounds
|
|
1923
|
+
if (mCursorPosX < getLeftSpace()) {
|
|
1924
|
+
mCursorPosX = getLeftSpace();
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
// Adjust selected range and update handles
|
|
1929
|
+
public void adjustSelectRange(int start, int end) {
|
|
1930
|
+
selectionStart = start;
|
|
1931
|
+
selectionEnd = end;
|
|
1932
|
+
updateSelectionHandles(); // Call the new method
|
|
1933
|
+
onCursorOrSelectionChanged();
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
// ---------- Auto-indent ----------
|
|
1937
|
+
// Get current line's leading whitespace for auto-indent
|
|
1938
|
+
private String getAutoIndent() {
|
|
1939
|
+
if (!mAutoIndentEnabled || mCursorIndex == 0) return "";
|
|
1940
|
+
|
|
1941
|
+
try {
|
|
1942
|
+
// Find the start of current line
|
|
1943
|
+
int lineStart = getLineStart(mCursorLine);
|
|
1944
|
+
if (lineStart < 0 || lineStart >= mGapBuffer.length()) return "";
|
|
1945
|
+
|
|
1946
|
+
String currentLine = mGapBuffer.substring(lineStart, Math.min(mCursorIndex, mGapBuffer.length()));
|
|
1947
|
+
|
|
1948
|
+
// Count leading spaces/tabs
|
|
1949
|
+
StringBuilder indent = new StringBuilder();
|
|
1950
|
+
for (int i = 0; i < currentLine.length(); i++) {
|
|
1951
|
+
char c = currentLine.charAt(i);
|
|
1952
|
+
if (c == ' ' || c == '\t') {
|
|
1953
|
+
indent.append(c);
|
|
1954
|
+
} else {
|
|
1955
|
+
break;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
return indent.toString();
|
|
1960
|
+
} catch (Exception e) {
|
|
1961
|
+
Log.e(TAG, "Error in getAutoIndent: " + e.getMessage());
|
|
1962
|
+
return "";
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// Set cursor position by index and adjust coordinates
|
|
1967
|
+
private void setCursorPosition(int index) {
|
|
1968
|
+
// calculate the cursor index and position
|
|
1969
|
+
mCursorIndex = index;
|
|
1970
|
+
mCursorLine = getOffsetLine(index);
|
|
1971
|
+
|
|
1972
|
+
String text = mGapBuffer.substring(getLineStart(mCursorLine), index);
|
|
1973
|
+
int width = measureText(text);
|
|
1974
|
+
mCursorPosX = getLeftSpace() + width;
|
|
1975
|
+
mCursorPosY = (mCursorLine - 1) * getLineHeight();
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// Set selection of texts from start to end
|
|
1979
|
+
private void setSelection(int start, int end) {
|
|
1980
|
+
selectionStart = Math.max(0, Math.min(start, mGapBuffer.length()));
|
|
1981
|
+
selectionEnd = Math.max(0, Math.min(end, mGapBuffer.length()));
|
|
1982
|
+
isSelectMode = true;
|
|
1983
|
+
mHandleMiddleVisable = false;
|
|
1984
|
+
updateSelectionHandles();
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// Set cursor position by pixel coordinates and compute nearest index
|
|
1988
|
+
public void setCursorPosition(float x, float y) {
|
|
1989
|
+
// calculation the cursor y coordinate
|
|
1990
|
+
mCursorPosY = (int) y / getLineHeight() * getLineHeight();
|
|
1991
|
+
int bottom = getLineCount() * getLineHeight();
|
|
1992
|
+
|
|
1993
|
+
if (mCursorPosY < getPaddingTop())
|
|
1994
|
+
mCursorPosY = getPaddingTop();
|
|
1995
|
+
|
|
1996
|
+
if (mCursorPosY > bottom - getLineHeight())
|
|
1997
|
+
mCursorPosY = bottom - getLineHeight();
|
|
1998
|
+
|
|
1999
|
+
// estimate the cursor x position
|
|
2000
|
+
int left = getLeftSpace();
|
|
2001
|
+
|
|
2002
|
+
int prev = left;
|
|
2003
|
+
int next = left;
|
|
2004
|
+
|
|
2005
|
+
mCursorLine = mCursorPosY / getLineHeight() + 1;
|
|
2006
|
+
mCursorIndex = getLineStart(mCursorLine);
|
|
2007
|
+
|
|
2008
|
+
String text = getLine(mCursorLine);
|
|
2009
|
+
int length = text.length();
|
|
2010
|
+
|
|
2011
|
+
float[] widths = new float[length];
|
|
2012
|
+
mTextPaint.getTextWidths(text, widths);
|
|
2013
|
+
|
|
2014
|
+
for (int i = 0; next < x && i < length; ++i) {
|
|
2015
|
+
if (i > 0) {
|
|
2016
|
+
prev += widths[i - 1];
|
|
2017
|
+
}
|
|
2018
|
+
next += widths[i];
|
|
2019
|
+
}
|
|
2020
|
+
onCursorOrSelectionChanged();
|
|
2021
|
+
|
|
2022
|
+
// calculation the cursor x coordinate
|
|
2023
|
+
if (Math.abs(x - prev) <= Math.abs(next - x)) {
|
|
2024
|
+
mCursorPosX = prev;
|
|
2025
|
+
} else {
|
|
2026
|
+
mCursorPosX = next;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
// calculation the cursor index
|
|
2030
|
+
if (mCursorPosX > left) {
|
|
2031
|
+
for (int j = 0; left < mCursorPosX && j < length; ++j) {
|
|
2032
|
+
left += widths[j];
|
|
2033
|
+
++mCursorIndex;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// Called when cursor or selection changed (placeholder for external UI)
|
|
2039
|
+
private void onCursorOrSelectionChanged() {
|
|
2040
|
+
scheduleSelectionUpdate();
|
|
2041
|
+
// Auto-hide after 5 seconds if no interaction
|
|
2042
|
+
if (mClipboardPanel != null) {
|
|
2043
|
+
mSelectionHandler.removeCallbacks(mAutoHideRunnable);
|
|
2044
|
+
mSelectionHandler.postDelayed(mAutoHideRunnable, 5000); // 5 seconds
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// ---------- Autocomplete / Word extraction ----------
|
|
2049
|
+
// Update word set from buffer asynchronously
|
|
2050
|
+
private void updateWordSet() {
|
|
2051
|
+
removeCallbacks(mWordUpdateRunnable);
|
|
2052
|
+
if (!isEditedMode) return;
|
|
2053
|
+
|
|
2054
|
+
new Thread(new Runnable() {
|
|
2055
|
+
@Override
|
|
2056
|
+
public void run() {
|
|
2057
|
+
// Off UI for large files
|
|
2058
|
+
String fullText = mGapBuffer.toString();
|
|
2059
|
+
final Set<String> words = new HashSet<>();
|
|
2060
|
+
java.util.regex.Matcher matcher = WORD_PATTERN.matcher(fullText);
|
|
2061
|
+
|
|
2062
|
+
while (matcher.find()) {
|
|
2063
|
+
String word = matcher.group();
|
|
2064
|
+
if (word.length() >= MIN_WORD_LEN) {
|
|
2065
|
+
words.add(word);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
post(new Runnable() {
|
|
2070
|
+
@Override
|
|
2071
|
+
public void run() {
|
|
2072
|
+
// Back to UI
|
|
2073
|
+
mWordSet = words;
|
|
2074
|
+
if (!mCurrentPrefix.isEmpty()) {
|
|
2075
|
+
showAutoComplete(mCurrentPrefix);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
}).start();
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
// Get the current identifier-like prefix near caret
|
|
2084
|
+
private String getCurrentPrefix() {
|
|
2085
|
+
if (mCursorIndex <= 0) return "";
|
|
2086
|
+
|
|
2087
|
+
// Don't convert entire buffer to string - work with the buffer directly
|
|
2088
|
+
int start = mCursorIndex;
|
|
2089
|
+
|
|
2090
|
+
// Move backwards to find the start of the word
|
|
2091
|
+
while (start > 0) {
|
|
2092
|
+
char prevChar = mGapBuffer.charAt(start - 1);
|
|
2093
|
+
if (!(Character.isLetterOrDigit(prevChar) || prevChar == '_')) {
|
|
2094
|
+
break;
|
|
2095
|
+
}
|
|
2096
|
+
start--;
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// Extract only the needed substring
|
|
2100
|
+
if (start < mCursorIndex) {
|
|
2101
|
+
return mGapBuffer.substring(start, mCursorIndex);
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
return "";
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// Filter word set and show suggestions
|
|
2108
|
+
private void filterAndShowSuggestions(String prefix) {
|
|
2109
|
+
if (prefix.isEmpty()) {
|
|
2110
|
+
dismissAutoComplete();
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
List<String> priorityList = new ArrayList<>();
|
|
2115
|
+
List<String> containsList = new ArrayList<>();
|
|
2116
|
+
|
|
2117
|
+
for (String word : mWordSet) {
|
|
2118
|
+
String lowerWord = word.toLowerCase();
|
|
2119
|
+
String lowerPrefix = prefix.toLowerCase();
|
|
2120
|
+
|
|
2121
|
+
if (lowerWord.startsWith(lowerPrefix) && !word.equals(prefix)) {
|
|
2122
|
+
priorityList.add(word);
|
|
2123
|
+
} else if (lowerWord.contains(lowerPrefix) && !word.equals(prefix)) {
|
|
2124
|
+
containsList.add(word);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
List<String> suggestions = new ArrayList<>(priorityList);
|
|
2129
|
+
suggestions.addAll(containsList);
|
|
2130
|
+
|
|
2131
|
+
if (suggestions.isEmpty()) {
|
|
2132
|
+
dismissAutoComplete();
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
// If list is already shown, just update silently
|
|
2137
|
+
if (mAutoCompletePopup.isShowing()) {
|
|
2138
|
+
mAutoCompleteAdapter.clear();
|
|
2139
|
+
mAutoCompleteAdapter.addAll(suggestions);
|
|
2140
|
+
mAutoCompleteAdapter.notifyDataSetChanged();
|
|
2141
|
+
} else {
|
|
2142
|
+
mAutoCompleteAdapter.clear();
|
|
2143
|
+
mAutoCompleteAdapter.addAll(suggestions);
|
|
2144
|
+
mAutoCompleteAdapter.notifyDataSetChanged();
|
|
2145
|
+
showAutoComplete(prefix);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
// Show the autocomplete popup near the caret
|
|
2150
|
+
private void showAutoComplete(String prefix) {
|
|
2151
|
+
Rect cursorRect = getBoundingBox(mCursorIndex);
|
|
2152
|
+
|
|
2153
|
+
hideTextSelectionWindow();
|
|
2154
|
+
|
|
2155
|
+
// Calculate full width with margin
|
|
2156
|
+
int margin = (int) (getResources().getDisplayMetrics().density * 8); // 8dp margin on each
|
|
2157
|
+
// side
|
|
2158
|
+
int popupWidth = getWidth() - (margin * 2);
|
|
2159
|
+
|
|
2160
|
+
// Dynamically size the height — wrap up to 4 visible items
|
|
2161
|
+
int itemHeight = (int) (getResources().getDisplayMetrics().density * 40); // ≈40dp per item
|
|
2162
|
+
int visibleCount = Math.min(mAutoCompleteAdapter.getCount(), 4);
|
|
2163
|
+
int popupHeight = visibleCount == 0 ? 0 : itemHeight * visibleCount;
|
|
2164
|
+
if (popupHeight == 0) popupHeight = ListPopupWindow.WRAP_CONTENT;
|
|
2165
|
+
|
|
2166
|
+
// If already visible, only update position and size — don’t recreate (prevents flicker)
|
|
2167
|
+
if (mAutoCompletePopup.isShowing()) {
|
|
2168
|
+
mAutoCompletePopup.setHeight(popupHeight);
|
|
2169
|
+
mAutoCompleteAdapter.notifyDataSetChanged();
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
// Configure initial show
|
|
2174
|
+
mAutoCompletePopup.setWidth(popupWidth);
|
|
2175
|
+
mAutoCompletePopup.setHeight(popupHeight);
|
|
2176
|
+
mAutoCompletePopup.setAnchorView(this);
|
|
2177
|
+
mAutoCompletePopup.setModal(false); // Allow typing
|
|
2178
|
+
mAutoCompletePopup.setBackgroundDrawable(
|
|
2179
|
+
getResources().getDrawable(android.R.drawable.dialog_holo_light_frame)
|
|
2180
|
+
);
|
|
2181
|
+
|
|
2182
|
+
// Offset popup slightly below cursor, centered with margins
|
|
2183
|
+
mAutoCompletePopup.setHorizontalOffset(margin);
|
|
2184
|
+
mAutoCompletePopup.setVerticalOffset(cursorRect.bottom + (int) (getLineHeight() * 0.6f));
|
|
2185
|
+
|
|
2186
|
+
mAutoCompletePopup.show();
|
|
2187
|
+
mCurrentPrefix = prefix;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// Replace current prefix with a chosen completion
|
|
2191
|
+
private void replacePrefixWithWord(String fullWord) {
|
|
2192
|
+
String prefix = getCurrentPrefix();
|
|
2193
|
+
if (prefix.isEmpty()) {
|
|
2194
|
+
insertText(fullWord); // Insert if no prefix
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
int prefixStart = mCursorIndex - prefix.length();
|
|
2199
|
+
mGapBuffer.replace(prefixStart, mCursorIndex, fullWord, true); // Replace via GapBuffer
|
|
2200
|
+
mCursorIndex = prefixStart + fullWord.length();
|
|
2201
|
+
adjustCursorPosition();
|
|
2202
|
+
scrollToVisable();
|
|
2203
|
+
postInvalidate();
|
|
2204
|
+
onTextChanged();
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// Dismiss autocomplete popup
|
|
2208
|
+
private void dismissAutoComplete() {
|
|
2209
|
+
if (mAutoCompletePopup.isShowing()) {
|
|
2210
|
+
mAutoCompletePopup.dismiss();
|
|
2211
|
+
}
|
|
2212
|
+
mCurrentPrefix = "";
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// ---------- Selection UI / Clipboard Panel ----------
|
|
2216
|
+
// Placeholder: show text selection window (clipboard panel)
|
|
2217
|
+
public void showTextSelectionWindow() {
|
|
2218
|
+
if (mClipboardPanel != null && (isSelectMode || mHandleMiddleVisable)) {
|
|
2219
|
+
post(new Runnable() {
|
|
2220
|
+
@Override
|
|
2221
|
+
public void run() {
|
|
2222
|
+
Rect optimalRect = getOptimalClipboardPosition();
|
|
2223
|
+
mClipboardPanel.showAtLocation(optimalRect);
|
|
2224
|
+
|
|
2225
|
+
// Schedule auto-hide
|
|
2226
|
+
scheduleAutoHide();
|
|
2227
|
+
}
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// Hide text selection window (clipboard panel)
|
|
2233
|
+
public void hideTextSelectionWindow() {
|
|
2234
|
+
if (mClipboardPanel != null) {
|
|
2235
|
+
// Cancel any pending auto-hide
|
|
2236
|
+
mSelectionHandler.removeCallbacks(mAutoHideRunnable);
|
|
2237
|
+
|
|
2238
|
+
post(new Runnable() {
|
|
2239
|
+
@Override
|
|
2240
|
+
public void run() {
|
|
2241
|
+
mClipboardPanel.hide();
|
|
2242
|
+
}
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
// Schedule auto-hide for selection UI
|
|
2248
|
+
private void scheduleAutoHide() {
|
|
2249
|
+
mSelectionHandler.removeCallbacks(mAutoHideRunnable);
|
|
2250
|
+
if (!isSelectMode && !mHandleMiddleVisable) {
|
|
2251
|
+
mSelectionHandler.postDelayed(mAutoHideRunnable, 3000); // 3 seconds for normal taps
|
|
2252
|
+
} else {
|
|
2253
|
+
mSelectionHandler.postDelayed(mAutoHideRunnable, 5000); // 5 seconds for selections
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
private Rect getOptimalClipboardPosition() {
|
|
2258
|
+
if (isSelectMode) {
|
|
2259
|
+
// For selections, position near the middle using your direct variables
|
|
2260
|
+
int middle = (selectionStart + selectionEnd) / 2;
|
|
2261
|
+
Rect middleRect = getBoundingBox(middle);
|
|
2262
|
+
if (middleRect != null) {
|
|
2263
|
+
// Position above selection middle
|
|
2264
|
+
middleRect.top -= getLineHeight() * 3;
|
|
2265
|
+
middleRect.bottom = middleRect.top + getLineHeight();
|
|
2266
|
+
return middleRect;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// For cursor, position above cursor
|
|
2271
|
+
Rect cursorRect = getBoundingBox(mCursorIndex);
|
|
2272
|
+
if (cursorRect != null) {
|
|
2273
|
+
cursorRect.top -= getLineHeight() * 3;
|
|
2274
|
+
cursorRect.bottom = cursorRect.top + getLineHeight();
|
|
2275
|
+
return cursorRect;
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
return null; // Let ClipboardPanel calculate automatically
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
@Override
|
|
2282
|
+
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
|
|
2283
|
+
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
|
|
2284
|
+
if (!gainFocus) dismissAutoComplete();
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
@Override
|
|
2288
|
+
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
2289
|
+
|
|
2290
|
+
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
2291
|
+
|
|
2292
|
+
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
|
|
2293
|
+
MeasureSpec.getSize(heightMeasureSpec));
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
// ---------- Gesture related helpers ----------
|
|
2297
|
+
// Auto scroll select handle and cursor when dragging near edge
|
|
2298
|
+
private void onMove(int slopX, int slopY) {
|
|
2299
|
+
int dx = 0;
|
|
2300
|
+
|
|
2301
|
+
if (mGapBuffer == null || mGapBuffer.length() == 0) {
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
if (mCursorPosX - getScrollX() <= slopX) {
|
|
2306
|
+
if (mCursorIndex > 0 && mCursorIndex - 1 < mGapBuffer.length()) {
|
|
2307
|
+
try {
|
|
2308
|
+
char prevChar = mGapBuffer.charAt(mCursorIndex - 1);
|
|
2309
|
+
dx = -measureText(String.valueOf(prevChar));
|
|
2310
|
+
} catch (Exception e) {
|
|
2311
|
+
dx = -spaceWidth;
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
} else if (mCursorPosX - getScrollX() >= screenWidth - slopX) {
|
|
2315
|
+
if (mCursorIndex >= 0 && mCursorIndex < mGapBuffer.length()) {
|
|
2316
|
+
try {
|
|
2317
|
+
char nextChar = mGapBuffer.charAt(mCursorIndex);
|
|
2318
|
+
dx = measureText(String.valueOf(nextChar));
|
|
2319
|
+
} catch (Exception e) {
|
|
2320
|
+
dx = spaceWidth;
|
|
2321
|
+
}
|
|
2322
|
+
} else if (mCursorIndex == mGapBuffer.length()) {
|
|
2323
|
+
dx = spaceWidth;
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
if (getHeight() > screenHeight / 2) {
|
|
2328
|
+
slopY = slopY * 3;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
int dy = 0;
|
|
2332
|
+
if (mCursorPosY - getScrollY() <= 0) {
|
|
2333
|
+
dy = -getLineHeight();
|
|
2334
|
+
} else if (mCursorPosY - getScrollY() >= getHeight() - slopY) {
|
|
2335
|
+
dy = getLineHeight();
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
int newScrollX = getScrollX() + dx;
|
|
2339
|
+
int newScrollY = getScrollY() + dy;
|
|
2340
|
+
|
|
2341
|
+
newScrollX = Math.max(0, Math.min(newScrollX, getMaxScrollX()));
|
|
2342
|
+
newScrollY = Math.max(0, Math.min(newScrollY, getMaxScrollY()));
|
|
2343
|
+
|
|
2344
|
+
smoothScrollTo(newScrollX, newScrollY);
|
|
2345
|
+
|
|
2346
|
+
// Update magnifier during auto-scroll
|
|
2347
|
+
if (mIsMagnifierShowing && mMagnifierEnabled) {
|
|
2348
|
+
updateMagnifier(mCursorPosX, mCursorPosY + getLineHeight());
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// ---------- Gesture listener inner class ----------
|
|
2353
|
+
class GestureListener extends GestureDetector.SimpleOnGestureListener {
|
|
2354
|
+
|
|
2355
|
+
private boolean touchOnSelectHandleMiddle = false;
|
|
2356
|
+
private boolean touchOnSelectHandleLeft = false;
|
|
2357
|
+
private boolean touchOnSelectHandleRight = false;
|
|
2358
|
+
|
|
2359
|
+
private boolean mIsMagnifierActive = false;
|
|
2360
|
+
|
|
2361
|
+
private int mInitialLine = -1;
|
|
2362
|
+
|
|
2363
|
+
// for auto scroll select handle
|
|
2364
|
+
private Runnable moveAction = new Runnable() {
|
|
2365
|
+
@Override
|
|
2366
|
+
public void run() {
|
|
2367
|
+
try {
|
|
2368
|
+
if (mIsMagnifierActive && mMagnifierEnabled) {
|
|
2369
|
+
// Use cursor position for magnifier
|
|
2370
|
+
updateMagnifier(mCursorPosX, mCursorPosY + getLineHeight());
|
|
2371
|
+
}
|
|
2372
|
+
onMove(spaceWidth * 4, getLineHeight());
|
|
2373
|
+
if (EditView.this.isAttachedToWindow()) {
|
|
2374
|
+
postDelayed(moveAction, DEFAULT_DURATION);
|
|
2375
|
+
}
|
|
2376
|
+
} catch (Exception e) {
|
|
2377
|
+
Log.e(TAG, "Error in moveAction: " + e.getMessage());
|
|
2378
|
+
dismissMagnifier();
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
};
|
|
2382
|
+
|
|
2383
|
+
// Swap left/right select handle coordinates and selection indices
|
|
2384
|
+
private void reverse() {
|
|
2385
|
+
selectHandleLeftX = selectHandleLeftX ^ selectHandleRightX;
|
|
2386
|
+
selectHandleRightX = selectHandleLeftX ^ selectHandleRightX;
|
|
2387
|
+
selectHandleLeftX = selectHandleLeftX ^ selectHandleRightX;
|
|
2388
|
+
|
|
2389
|
+
selectHandleLeftY = selectHandleLeftY ^ selectHandleRightY;
|
|
2390
|
+
selectHandleRightY = selectHandleLeftY ^ selectHandleRightY;
|
|
2391
|
+
selectHandleLeftY = selectHandleLeftY ^ selectHandleRightY;
|
|
2392
|
+
|
|
2393
|
+
selectionStart = selectionStart ^ selectionEnd;
|
|
2394
|
+
selectionEnd = selectionStart ^ selectionEnd;
|
|
2395
|
+
selectionStart = selectionStart ^ selectionEnd;
|
|
2396
|
+
|
|
2397
|
+
touchOnSelectHandleLeft = !touchOnSelectHandleLeft;
|
|
2398
|
+
touchOnSelectHandleRight = !touchOnSelectHandleRight;
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
// when single tap to check the select region
|
|
2402
|
+
private boolean checkSelectRange(float x, float y) {
|
|
2403
|
+
|
|
2404
|
+
if (y < selectHandleLeftY - getLineHeight() || y > selectHandleRightY)
|
|
2405
|
+
return false;
|
|
2406
|
+
|
|
2407
|
+
// on the same line
|
|
2408
|
+
if (selectHandleLeftY == selectHandleRightY) {
|
|
2409
|
+
if (x < selectHandleLeftX || x > selectHandleRightX)
|
|
2410
|
+
return false;
|
|
2411
|
+
} else {
|
|
2412
|
+
// not on the same line
|
|
2413
|
+
int left = getLeftSpace();
|
|
2414
|
+
int line = (int) y / getLineHeight() + 1;
|
|
2415
|
+
int width = getLineWidth(line) + spaceWidth;
|
|
2416
|
+
// select start line
|
|
2417
|
+
if (line == selectHandleLeftY / getLineHeight()) {
|
|
2418
|
+
if (x < selectHandleLeftX || x > left + width)
|
|
2419
|
+
return false;
|
|
2420
|
+
} else if (line == selectHandleRightY / getLineHeight()) {
|
|
2421
|
+
// select end line
|
|
2422
|
+
if (x < left || x > selectHandleRightX)
|
|
2423
|
+
return false;
|
|
2424
|
+
} else {
|
|
2425
|
+
if (x < left || x > left + width)
|
|
2426
|
+
return false;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
return true;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
@Override
|
|
2433
|
+
public boolean onDown(MotionEvent e) {
|
|
2434
|
+
float x = e.getX() + getScrollX();
|
|
2435
|
+
float y = e.getY() + getScrollY();
|
|
2436
|
+
|
|
2437
|
+
// touch handle middle (show clipboard panel)
|
|
2438
|
+
if (mHandleMiddleVisable &&
|
|
2439
|
+
x >= mCursorPosX - handleMiddleWidth / 2 &&
|
|
2440
|
+
x <= mCursorPosX + handleMiddleWidth / 2 &&
|
|
2441
|
+
y >= mCursorPosY + getLineHeight() &&
|
|
2442
|
+
y <= mCursorPosY + getLineHeight() + handleMiddleHeight) {
|
|
2443
|
+
|
|
2444
|
+
touchOnSelectHandleMiddle = true;
|
|
2445
|
+
removeCallbacks(blinkAction);
|
|
2446
|
+
mCursorVisiable = mHandleMiddleVisable = true;
|
|
2447
|
+
|
|
2448
|
+
// 🔹 Show clipboard panel exactly above this handle
|
|
2449
|
+
showTextSelectionWindow();
|
|
2450
|
+
|
|
2451
|
+
// 🔹 Keep magnifier logic working
|
|
2452
|
+
if (mMagnifierEnabled) {
|
|
2453
|
+
mIsMagnifierActive = true;
|
|
2454
|
+
showMagnifier(mCursorPosX, mCursorPosY + getLineHeight());
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// ✅ Prevent cursor from moving or triggering a new position
|
|
2458
|
+
return true;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// touch handle left
|
|
2462
|
+
if (isSelectMode && x >= selectHandleLeftX - selectHandleWidth + selectHandleWidth / 4
|
|
2463
|
+
&& x <= selectHandleLeftX + selectHandleWidth / 4
|
|
2464
|
+
&& y >= selectHandleLeftY && y <= selectHandleLeftY + selectHandleHeight) {
|
|
2465
|
+
touchOnSelectHandleLeft = true;
|
|
2466
|
+
removeCallbacks(blinkAction);
|
|
2467
|
+
mCursorVisiable = mHandleMiddleVisable = false;
|
|
2468
|
+
|
|
2469
|
+
showTextSelectionWindow();
|
|
2470
|
+
if (mMagnifierEnabled) {
|
|
2471
|
+
mIsMagnifierActive = true;
|
|
2472
|
+
showMagnifier(selectHandleLeftX, selectHandleLeftY);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
// touch handle right
|
|
2477
|
+
if (isSelectMode && x >= selectHandleRightX - selectHandleWidth / 4
|
|
2478
|
+
&& x <= selectHandleRightX + selectHandleWidth - selectHandleWidth / 4
|
|
2479
|
+
&& y >= selectHandleRightY && y <= selectHandleRightY + selectHandleHeight) {
|
|
2480
|
+
touchOnSelectHandleRight = true;
|
|
2481
|
+
removeCallbacks(blinkAction);
|
|
2482
|
+
mCursorVisiable = mHandleMiddleVisable = false;
|
|
2483
|
+
showTextSelectionWindow();
|
|
2484
|
+
if (mMagnifierEnabled) {
|
|
2485
|
+
mIsMagnifierActive = true;
|
|
2486
|
+
showMagnifier(selectHandleRightX, selectHandleRightY);
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
return super.onDown(e);
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
@Override
|
|
2494
|
+
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
|
2495
|
+
try {
|
|
2496
|
+
float x = e2.getX() + getScrollX();
|
|
2497
|
+
float y = e2.getY() + getScrollY();
|
|
2498
|
+
|
|
2499
|
+
if (mIsMagnifierActive && mMagnifierEnabled) {
|
|
2500
|
+
if (touchOnSelectHandleMiddle) {
|
|
2501
|
+
updateMagnifier(mCursorPosX, mCursorPosY + getLineHeight());
|
|
2502
|
+
} else if (touchOnSelectHandleLeft) {
|
|
2503
|
+
updateMagnifier(selectHandleLeftX, selectHandleLeftY);
|
|
2504
|
+
} else if (touchOnSelectHandleRight) {
|
|
2505
|
+
updateMagnifier(selectHandleRightX, selectHandleRightY);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
if (touchOnSelectHandleMiddle) {
|
|
2510
|
+
removeCallbacks(moveAction);
|
|
2511
|
+
post(moveAction);
|
|
2512
|
+
setCursorPosition(x, y - getLineHeight() - Math.min(getLineHeight(), selectHandleHeight) / 2);
|
|
2513
|
+
} else if (touchOnSelectHandleLeft) {
|
|
2514
|
+
removeCallbacks(moveAction);
|
|
2515
|
+
post(moveAction);
|
|
2516
|
+
setCursorPosition(x, y - getLineHeight() - Math.min(getLineHeight(), selectHandleHeight) / 2);
|
|
2517
|
+
selectHandleLeftX = mCursorPosX;
|
|
2518
|
+
selectHandleLeftY = mCursorPosY + getLineHeight();
|
|
2519
|
+
selectionStart = mCursorIndex;
|
|
2520
|
+
} else if (touchOnSelectHandleRight) {
|
|
2521
|
+
removeCallbacks(moveAction);
|
|
2522
|
+
post(moveAction);
|
|
2523
|
+
setCursorPosition(x, y - getLineHeight() - Math.min(getLineHeight(), selectHandleHeight) / 2);
|
|
2524
|
+
selectHandleRightX = mCursorPosX;
|
|
2525
|
+
selectHandleRightY = mCursorPosY + getLineHeight();
|
|
2526
|
+
selectionEnd = mCursorIndex;
|
|
2527
|
+
} else {
|
|
2528
|
+
if (Math.abs(distanceY) > Math.abs(distanceX))
|
|
2529
|
+
distanceX = 0;
|
|
2530
|
+
else
|
|
2531
|
+
distanceY = 0;
|
|
2532
|
+
|
|
2533
|
+
int newX = (int) distanceX + getScrollX();
|
|
2534
|
+
if (newX < 0) {
|
|
2535
|
+
newX = 0;
|
|
2536
|
+
} else if (newX > getMaxScrollX()) {
|
|
2537
|
+
newX = getMaxScrollX();
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
int newY = (int) distanceY + getScrollY();
|
|
2541
|
+
if (newY < 0) {
|
|
2542
|
+
newY = 0;
|
|
2543
|
+
} else if (newY > getMaxScrollY()) {
|
|
2544
|
+
newY = getMaxScrollY();
|
|
2545
|
+
}
|
|
2546
|
+
smoothScrollTo(newX, newY);
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
if (isSelectMode && ((selectHandleLeftY > selectHandleRightY)
|
|
2550
|
+
|| (selectHandleLeftY == selectHandleRightY && selectHandleLeftX > selectHandleRightX))) {
|
|
2551
|
+
reverse();
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
postInvalidate();
|
|
2555
|
+
} catch (Exception e) {
|
|
2556
|
+
dismissMagnifier();
|
|
2557
|
+
mIsMagnifierActive = false;
|
|
2558
|
+
removeCallbacks(moveAction);
|
|
2559
|
+
Log.e(TAG, "Error in onScroll: " + e.getMessage());
|
|
2560
|
+
}
|
|
2561
|
+
return super.onScroll(e1, e2, distanceX, distanceY);
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
@Override
|
|
2565
|
+
public boolean onSingleTapUp(MotionEvent e) {
|
|
2566
|
+
float x = e.getX() + getScrollX();
|
|
2567
|
+
float y = e.getY() + getScrollY();
|
|
2568
|
+
if (isEditedMode) {
|
|
2569
|
+
showSoftInput(true);
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
showTextSelectionWindow();
|
|
2573
|
+
if (!isSelectMode || !checkSelectRange(x, y)) {
|
|
2574
|
+
// stop cursor blink
|
|
2575
|
+
removeCallbacks(blinkAction);
|
|
2576
|
+
mCursorVisiable = mHandleMiddleVisable = true;
|
|
2577
|
+
isSelectMode = false;
|
|
2578
|
+
|
|
2579
|
+
if (!mReplaceList.isEmpty())
|
|
2580
|
+
mReplaceList.clear();
|
|
2581
|
+
|
|
2582
|
+
setCursorPosition(x, y);
|
|
2583
|
+
postInvalidate();
|
|
2584
|
+
mLastTapTime = System.currentTimeMillis();
|
|
2585
|
+
// clear long selection process
|
|
2586
|
+
clearLineSelection();
|
|
2587
|
+
// cursor start blink
|
|
2588
|
+
postDelayed(blinkAction, BLINK_TIMEOUT);
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
return super.onSingleTapUp(e);
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
@Override
|
|
2595
|
+
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
|
2596
|
+
// TODO: Implement this method
|
|
2597
|
+
if (Math.abs(velocityY) > Math.abs(velocityX))
|
|
2598
|
+
velocityX = 0;
|
|
2599
|
+
else
|
|
2600
|
+
velocityY = 0;
|
|
2601
|
+
|
|
2602
|
+
mScroller.fling(getScrollX(), getScrollY(), (int) -velocityX, (int) -velocityY,
|
|
2603
|
+
0, getMaxScrollX(), 0, getMaxScrollY());
|
|
2604
|
+
|
|
2605
|
+
postInvalidate();
|
|
2606
|
+
return super.onFling(e1, e2, velocityX, velocityY);
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
@Override
|
|
2610
|
+
public void onLongPress(MotionEvent e) {
|
|
2611
|
+
super.onLongPress(e);
|
|
2612
|
+
float x = e.getX() + getScrollX();
|
|
2613
|
+
float y = e.getY() + getScrollY();
|
|
2614
|
+
|
|
2615
|
+
removeCallbacks(blinkAction);
|
|
2616
|
+
showTextSelectionWindow();
|
|
2617
|
+
mCursorVisiable = mHandleMiddleVisable = true;
|
|
2618
|
+
|
|
2619
|
+
// Handle line number long press
|
|
2620
|
+
if (isInLineNumberArea(x, y)) {
|
|
2621
|
+
int currentLine = getLineFromY(y);
|
|
2622
|
+
handleLineNumberLongPress(currentLine);
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
if (!touchOnSelectHandleMiddle && mGapBuffer.length() > 0) {
|
|
2627
|
+
|
|
2628
|
+
setCursorPosition(x, y);
|
|
2629
|
+
|
|
2630
|
+
String selectWord = findNearestWord();
|
|
2631
|
+
if (selectWord != null) {
|
|
2632
|
+
removeCallbacks(blinkAction);
|
|
2633
|
+
mCursorVisiable = mHandleMiddleVisable = false;
|
|
2634
|
+
isSelectMode = true;
|
|
2635
|
+
|
|
2636
|
+
int left = getLeftSpace();
|
|
2637
|
+
int lineStart = getLineStart(mCursorLine);
|
|
2638
|
+
selectHandleLeftX = left + measureText(mGapBuffer.substring(lineStart, selectionStart));
|
|
2639
|
+
selectHandleRightX = left + measureText(mGapBuffer.substring(lineStart, selectionEnd));
|
|
2640
|
+
selectHandleLeftY = selectHandleRightY = mCursorPosY + getLineHeight();
|
|
2641
|
+
|
|
2642
|
+
setCursorPosition(selectionEnd);
|
|
2643
|
+
|
|
2644
|
+
if (mMagnifierEnabled) {
|
|
2645
|
+
mIsMagnifierActive = true;
|
|
2646
|
+
showMagnifier(selectHandleRightX, selectHandleRightY);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
postInvalidate();
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
@Override
|
|
2654
|
+
public boolean onDoubleTap(MotionEvent e) {
|
|
2655
|
+
super.onDoubleTap(e);
|
|
2656
|
+
|
|
2657
|
+
float x = e.getX() + getScrollX();
|
|
2658
|
+
float y = e.getY() + getScrollY();
|
|
2659
|
+
|
|
2660
|
+
removeCallbacks(blinkAction);
|
|
2661
|
+
mCursorVisiable = mHandleMiddleVisable = true;
|
|
2662
|
+
showTextSelectionWindow();
|
|
2663
|
+
|
|
2664
|
+
if (!touchOnSelectHandleMiddle && mGapBuffer.length() > 0) {
|
|
2665
|
+
setCursorPosition(x, y);
|
|
2666
|
+
|
|
2667
|
+
String selectWord = findNearestWord();
|
|
2668
|
+
if (selectWord != null) {
|
|
2669
|
+
removeCallbacks(blinkAction);
|
|
2670
|
+
mCursorVisiable = mHandleMiddleVisable = false;
|
|
2671
|
+
isSelectMode = true;
|
|
2672
|
+
|
|
2673
|
+
int left = getLeftSpace();
|
|
2674
|
+
int lineStart = getLineStart(mCursorLine);
|
|
2675
|
+
selectHandleLeftX = left + measureText(mGapBuffer.substring(lineStart, selectionStart));
|
|
2676
|
+
selectHandleRightX = left + measureText(mGapBuffer.substring(lineStart, selectionEnd));
|
|
2677
|
+
selectHandleLeftY = selectHandleRightY = mCursorPosY + getLineHeight();
|
|
2678
|
+
|
|
2679
|
+
setCursorPosition(selectionEnd);
|
|
2680
|
+
|
|
2681
|
+
// Show what was selected (for debugging)
|
|
2682
|
+
Log.d(TAG, "Double tap selected: '" + selectWord + "'");
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
postInvalidate();
|
|
2686
|
+
return super.onDoubleTap(e);
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
// Expose onUp for outside calls
|
|
2690
|
+
public void onUp(MotionEvent e) {
|
|
2691
|
+
if (mIsMagnifierActive) {
|
|
2692
|
+
dismissMagnifier();
|
|
2693
|
+
mIsMagnifierActive = false;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
if (touchOnSelectHandleLeft || touchOnSelectHandleRight || touchOnSelectHandleMiddle) {
|
|
2697
|
+
removeCallbacks(moveAction);
|
|
2698
|
+
touchOnSelectHandleMiddle = false;
|
|
2699
|
+
touchOnSelectHandleLeft = false;
|
|
2700
|
+
touchOnSelectHandleRight = false;
|
|
2701
|
+
|
|
2702
|
+
if (isSelectMode) {
|
|
2703
|
+
setCursorPosition(selectionEnd);
|
|
2704
|
+
// Show immediately and schedule auto-hide
|
|
2705
|
+
showTextSelectionWindow();
|
|
2706
|
+
scheduleAutoHide();
|
|
2707
|
+
} else {
|
|
2708
|
+
mLastTapTime = System.currentTimeMillis();
|
|
2709
|
+
postDelayed(blinkAction, BLINK_TIMEOUT);
|
|
2710
|
+
// Hide after delay if not in selection mode
|
|
2711
|
+
scheduleAutoHide();
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
private void handleLineNumberLongPress(int currentLine) {
|
|
2717
|
+
if (!mWaitingForSecondSelection) {
|
|
2718
|
+
// First long press - select single line and wait for second selection
|
|
2719
|
+
selectSingleLine(currentLine);
|
|
2720
|
+
mFirstSelectedLine = currentLine;
|
|
2721
|
+
mWaitingForSecondSelection = true;
|
|
2722
|
+
|
|
2723
|
+
// Show visual hint that we're waiting for second selection
|
|
2724
|
+
showSelectionHint();
|
|
2725
|
+
|
|
2726
|
+
} else {
|
|
2727
|
+
// Second long press - select range between first and current line
|
|
2728
|
+
mSecondSelectedLine = currentLine;
|
|
2729
|
+
selectLineRange(mFirstSelectedLine, mSecondSelectedLine);
|
|
2730
|
+
mWaitingForSecondSelection = false;
|
|
2731
|
+
mSelectionHandler.removeCallbacks(mClearSelectionRunnable);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
private void scheduleSelectionUpdate() {
|
|
2737
|
+
mSelectionHandler.removeCallbacks(mUpdateSelectionPosition);
|
|
2738
|
+
mSelectionHandler.postDelayed(mUpdateSelectionPosition, 100); // Small delay for smoothness
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
private void performHapticFeedback() {
|
|
2742
|
+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O_MR1) {
|
|
2743
|
+
performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
|
|
2744
|
+
} else {
|
|
2745
|
+
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
private void selectLineRange(int startLine, int endLine) {
|
|
2750
|
+
if (startLine < 1 || endLine < 1 || startLine > getLineCount() || endLine > getLineCount()) {
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
removeCallbacks(blinkAction);
|
|
2755
|
+
mCursorVisiable = false;
|
|
2756
|
+
mHandleMiddleVisable = false;
|
|
2757
|
+
isSelectMode = true;
|
|
2758
|
+
mIsLineSelectionMode = true;
|
|
2759
|
+
|
|
2760
|
+
// Calculate the actual selection range
|
|
2761
|
+
int actualStartLine = Math.min(startLine, endLine);
|
|
2762
|
+
int actualEndLine = Math.max(startLine, endLine);
|
|
2763
|
+
|
|
2764
|
+
// Start from the beginning of first line
|
|
2765
|
+
selectionStart = getLineStart(actualStartLine);
|
|
2766
|
+
|
|
2767
|
+
// End at the end of last line (including newline if present)
|
|
2768
|
+
int endLineStart = getLineStart(actualEndLine);
|
|
2769
|
+
String endLineText = getLine(actualEndLine);
|
|
2770
|
+
selectionEnd = endLineStart + endLineText.length();
|
|
2771
|
+
|
|
2772
|
+
// Include newline character if it's not the last line
|
|
2773
|
+
if (actualEndLine < getLineCount()) {
|
|
2774
|
+
selectionEnd++;
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
mStartSelectionLine = actualStartLine;
|
|
2778
|
+
mEndSelectionLine = actualEndLine;
|
|
2779
|
+
|
|
2780
|
+
updateSelectionHandles();
|
|
2781
|
+
showTextSelectionWindow();
|
|
2782
|
+
postInvalidate();
|
|
2783
|
+
|
|
2784
|
+
// Haptic feedback
|
|
2785
|
+
performHapticFeedback();
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
private void showSelectionHint() {
|
|
2789
|
+
// Flash the selected line or show some visual feedback
|
|
2790
|
+
post(new Runnable() {
|
|
2791
|
+
@Override
|
|
2792
|
+
public void run() {
|
|
2793
|
+
// You can change the background color or add animation
|
|
2794
|
+
mHandleMiddleVisable = false;
|
|
2795
|
+
postInvalidate();
|
|
2796
|
+
|
|
2797
|
+
// Reset after a short time
|
|
2798
|
+
postDelayed(new Runnable() {
|
|
2799
|
+
@Override
|
|
2800
|
+
public void run() {
|
|
2801
|
+
if (mWaitingForSecondSelection) {
|
|
2802
|
+
mHandleMiddleVisable = false;
|
|
2803
|
+
postInvalidate();
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
}, 500);
|
|
2807
|
+
}
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
private void selectSingleLine(int line) {
|
|
2812
|
+
if (line < 1 || line > getLineCount()) return;
|
|
2813
|
+
|
|
2814
|
+
removeCallbacks(blinkAction);
|
|
2815
|
+
mCursorVisiable = false;
|
|
2816
|
+
mHandleMiddleVisable = false;
|
|
2817
|
+
isSelectMode = true;
|
|
2818
|
+
mIsLineSelectionMode = true;
|
|
2819
|
+
|
|
2820
|
+
// Calculate selection range for the entire line
|
|
2821
|
+
int lineStart = getLineStart(line);
|
|
2822
|
+
int lineEnd = lineStart + getLine(line).length();
|
|
2823
|
+
|
|
2824
|
+
// If it's the last line, include the newline character if present
|
|
2825
|
+
if (line < getLineCount()) {
|
|
2826
|
+
lineEnd++; // Include the newline character
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
selectionStart = lineStart;
|
|
2830
|
+
selectionEnd = lineEnd;
|
|
2831
|
+
mStartSelectionLine = line;
|
|
2832
|
+
mEndSelectionLine = line;
|
|
2833
|
+
|
|
2834
|
+
updateSelectionHandles();
|
|
2835
|
+
showTextSelectionWindow();
|
|
2836
|
+
postInvalidate();
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
// clear long selection process
|
|
2840
|
+
private void clearLineSelection() {
|
|
2841
|
+
mWaitingForSecondSelection = false;
|
|
2842
|
+
mFirstSelectedLine = -1;
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
// Helper method that used for selecting nearby word or special symbol
|
|
2846
|
+
public void selectNearestWord() {
|
|
2847
|
+
String selectWord = findNearestWord();
|
|
2848
|
+
if (selectWord != null) {
|
|
2849
|
+
removeCallbacks(blinkAction);
|
|
2850
|
+
mCursorVisiable = false;
|
|
2851
|
+
mHandleMiddleVisable = false;
|
|
2852
|
+
isSelectMode = true;
|
|
2853
|
+
mIsLineSelectionMode = true;
|
|
2854
|
+
|
|
2855
|
+
int left = getLeftSpace();
|
|
2856
|
+
int lineStart = getLineStart(mCursorLine);
|
|
2857
|
+
selectHandleLeftX = left + measureText(mGapBuffer.substring(lineStart, selectionStart));
|
|
2858
|
+
selectHandleRightX = left + measureText(mGapBuffer.substring(lineStart, selectionEnd));
|
|
2859
|
+
selectHandleLeftY = selectHandleRightY = mCursorPosY + getLineHeight();
|
|
2860
|
+
|
|
2861
|
+
setCursorPosition(selectHandleLeftX, selectHandleRightX);
|
|
2862
|
+
setCursorPosition(selectionEnd);
|
|
2863
|
+
showTextSelectionWindow();
|
|
2864
|
+
}
|
|
2865
|
+
postInvalidate();
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
// when on long press to select a word
|
|
2869
|
+
public String findNearestWord() {
|
|
2870
|
+
int length = mGapBuffer.length();
|
|
2871
|
+
if (length == 0) return null;
|
|
2872
|
+
|
|
2873
|
+
if (mCursorIndex >= length) {
|
|
2874
|
+
mCursorIndex = Math.max(0, length - 1);
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
if (isCurrentLineEmptyOrWhitespace()) {
|
|
2878
|
+
return null;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
// FIRST: Always try to find single special character
|
|
2882
|
+
String selected = findSingleSpecialChar();
|
|
2883
|
+
if (selected != null) return selected;
|
|
2884
|
+
|
|
2885
|
+
// SECOND: Then try word selection
|
|
2886
|
+
selected = findWordAtCursor();
|
|
2887
|
+
if (selected != null) return selected;
|
|
2888
|
+
|
|
2889
|
+
selected = findWordInVicinity();
|
|
2890
|
+
if (selected != null) return selected;
|
|
2891
|
+
|
|
2892
|
+
selected = findAnyNonWhitespace();
|
|
2893
|
+
return selected;
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
// Only select single special characters
|
|
2897
|
+
private String findSingleSpecialChar() {
|
|
2898
|
+
// Check current cursor position
|
|
2899
|
+
if (mCursorIndex < mGapBuffer.length()) {
|
|
2900
|
+
char currentChar = mGapBuffer.charAt(mCursorIndex);
|
|
2901
|
+
if (isSpecialChar(currentChar)) {
|
|
2902
|
+
selectionStart = mCursorIndex;
|
|
2903
|
+
selectionEnd = mCursorIndex + 1;
|
|
2904
|
+
return String.valueOf(currentChar);
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
// Check position before cursor
|
|
2909
|
+
if (mCursorIndex > 0) {
|
|
2910
|
+
char prevChar = mGapBuffer.charAt(mCursorIndex - 1);
|
|
2911
|
+
if (isSpecialChar(prevChar)) {
|
|
2912
|
+
selectionStart = mCursorIndex - 1;
|
|
2913
|
+
selectionEnd = mCursorIndex;
|
|
2914
|
+
return String.valueOf(prevChar);
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
return null;
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
// Special character detection
|
|
2922
|
+
private boolean isSpecialChar(char c) {
|
|
2923
|
+
// All special characters that should be selected individually
|
|
2924
|
+
String specialChars = ":;\"\'`.,!?@#$%^&*()-+=[]{}<>/~|\\";
|
|
2925
|
+
return specialChars.indexOf(c) >= 0;
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
// WORD selection only - stops at special characters
|
|
2929
|
+
private String expandSelectionFrom(int position) {
|
|
2930
|
+
int length = mGapBuffer.length();
|
|
2931
|
+
if (position < 0 || position >= length) return null;
|
|
2932
|
+
|
|
2933
|
+
char startChar = mGapBuffer.charAt(position);
|
|
2934
|
+
|
|
2935
|
+
// If it's a special char, don't expand - let findSingleSpecialChar handle it
|
|
2936
|
+
if (isSpecialChar(startChar)) {
|
|
2937
|
+
return null;
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
// Expand left until whitespace OR special char
|
|
2941
|
+
selectionStart = position;
|
|
2942
|
+
while (selectionStart > 0) {
|
|
2943
|
+
char c = mGapBuffer.charAt(selectionStart - 1);
|
|
2944
|
+
if (Character.isWhitespace(c) || isSpecialChar(c)) break;
|
|
2945
|
+
selectionStart--;
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
// Expand right until whitespace OR special char
|
|
2949
|
+
selectionEnd = position;
|
|
2950
|
+
while (selectionEnd < length) {
|
|
2951
|
+
char c = mGapBuffer.charAt(selectionEnd);
|
|
2952
|
+
if (Character.isWhitespace(c) || isSpecialChar(c)) break;
|
|
2953
|
+
selectionEnd++;
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
if (selectionStart < selectionEnd) {
|
|
2957
|
+
return mGapBuffer.substring(selectionStart, selectionEnd);
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
return null;
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
// Find word at cursor (EXCLUDING special chars)
|
|
2964
|
+
private String findWordAtCursor() {
|
|
2965
|
+
// First, check if cursor is directly on a WORD character (not special char)
|
|
2966
|
+
if (mCursorIndex < mGapBuffer.length()) {
|
|
2967
|
+
char currentChar = mGapBuffer.charAt(mCursorIndex);
|
|
2968
|
+
if (!Character.isWhitespace(currentChar) && !isSpecialChar(currentChar)) {
|
|
2969
|
+
return expandSelectionFrom(mCursorIndex);
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
// Check character before cursor (only if it's a WORD character)
|
|
2974
|
+
if (mCursorIndex > 0) {
|
|
2975
|
+
char prevChar = mGapBuffer.charAt(mCursorIndex - 1);
|
|
2976
|
+
if (!Character.isWhitespace(prevChar) && !isSpecialChar(prevChar)) {
|
|
2977
|
+
return expandSelectionFrom(mCursorIndex - 1);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
return null;
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
// Find word in vicinity (EXCLUDING special chars)
|
|
2985
|
+
private String findWordInVicinity() {
|
|
2986
|
+
int length = mGapBuffer.length();
|
|
2987
|
+
if (length == 0) return null;
|
|
2988
|
+
|
|
2989
|
+
for (int radius = 1; radius <= 20; radius++) {
|
|
2990
|
+
// Check forward
|
|
2991
|
+
int forwardPos = mCursorIndex + radius;
|
|
2992
|
+
if (forwardPos < length) {
|
|
2993
|
+
char c = mGapBuffer.charAt(forwardPos);
|
|
2994
|
+
// If it's a special char, select it individually
|
|
2995
|
+
if (isSpecialChar(c)) {
|
|
2996
|
+
selectionStart = forwardPos;
|
|
2997
|
+
selectionEnd = forwardPos + 1;
|
|
2998
|
+
return String.valueOf(c);
|
|
2999
|
+
}
|
|
3000
|
+
// If it's a word char (not special, not whitespace), expand word
|
|
3001
|
+
if (!Character.isWhitespace(c) && !isSpecialChar(c)) {
|
|
3002
|
+
return expandSelectionFrom(forwardPos);
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
// Check backward
|
|
3007
|
+
int backwardPos = mCursorIndex - radius;
|
|
3008
|
+
if (backwardPos >= 0) {
|
|
3009
|
+
char c = mGapBuffer.charAt(backwardPos);
|
|
3010
|
+
// If it's a special char, select it individually
|
|
3011
|
+
if (isSpecialChar(c)) {
|
|
3012
|
+
selectionStart = backwardPos;
|
|
3013
|
+
selectionEnd = backwardPos + 1;
|
|
3014
|
+
return String.valueOf(c);
|
|
3015
|
+
}
|
|
3016
|
+
// If it's a word char (not special, not whitespace), expand word
|
|
3017
|
+
if (!Character.isWhitespace(c) && !isSpecialChar(c)) {
|
|
3018
|
+
return expandSelectionFrom(backwardPos);
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
return null;
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
// Find any non-whitespace (but still respect special chars)
|
|
3027
|
+
private String findAnyNonWhitespace() {
|
|
3028
|
+
// Check if current line is empty first
|
|
3029
|
+
String currentLine = getLine(mCursorLine);
|
|
3030
|
+
if (currentLine == null || currentLine.trim().isEmpty()) {
|
|
3031
|
+
return null;
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
int lineStart = getLineStart(mCursorLine);
|
|
3035
|
+
int lineEnd = lineStart + getLine(mCursorLine).length();
|
|
3036
|
+
|
|
3037
|
+
for (int i = lineStart; i < lineEnd; i++) {
|
|
3038
|
+
if (i < mGapBuffer.length()) {
|
|
3039
|
+
char c = mGapBuffer.charAt(i);
|
|
3040
|
+
if (!Character.isWhitespace(c)) {
|
|
3041
|
+
// If it's a special char, select only that char
|
|
3042
|
+
if (isSpecialChar(c)) {
|
|
3043
|
+
selectionStart = i;
|
|
3044
|
+
selectionEnd = i + 1;
|
|
3045
|
+
return String.valueOf(c);
|
|
3046
|
+
}
|
|
3047
|
+
// Otherwise expand word (will stop at special chars)
|
|
3048
|
+
return expandSelectionFrom(i);
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
return null;
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
// helper method to check line is empty or not
|
|
3057
|
+
private boolean isCurrentLineEmptyOrWhitespace() {
|
|
3058
|
+
String currentLine = getLine(mCursorLine);
|
|
3059
|
+
if (currentLine == null || currentLine.isEmpty()) {
|
|
3060
|
+
return true;
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
// Check if line contains only whitespace
|
|
3064
|
+
for (int i = 0; i < currentLine.length(); i++) {
|
|
3065
|
+
if (!Character.isWhitespace(currentLine.charAt(i))) {
|
|
3066
|
+
return false;
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
return true;
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
// ===== Editor Custom Functionalities ===== //
|
|
3073
|
+
|
|
3074
|
+
// copy line texts
|
|
3075
|
+
public void copyLine() {
|
|
3076
|
+
if (isSelectMode) {
|
|
3077
|
+
int[] sel = normalizeSelection();
|
|
3078
|
+
int s = sel[0], e = sel[1];
|
|
3079
|
+
int[] range = fullLineRangeForSelection(s, e);
|
|
3080
|
+
if (range != null) {
|
|
3081
|
+
copyRangeToClipboard(range[0], range[1], "lines");
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
3084
|
+
// degenerate -> fallthrough to no-selection behavior
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
// no selection path: copy current line and select it in UI
|
|
3088
|
+
int[] range = fullLineRangeForCursorLine(mCursorLine);
|
|
3089
|
+
copyRangeToClipboard(range[0], range[1], "line");
|
|
3090
|
+
setSelection(range[0], range[1]);
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
// cut line texts
|
|
3094
|
+
public void cutLine() {
|
|
3095
|
+
if (isSelectMode) {
|
|
3096
|
+
int[] sel = normalizeSelection();
|
|
3097
|
+
int s = sel[0], e = sel[1];
|
|
3098
|
+
int[] range = fullLineRangeForSelection(s, e);
|
|
3099
|
+
if (range != null) {
|
|
3100
|
+
// copy then delete as a batch (single undo)
|
|
3101
|
+
copyRangeToClipboard(range[0], range[1], "lines");
|
|
3102
|
+
batchDeleteWithSelectionSnapshot(range[0], range[1],
|
|
3103
|
+
true, selectionStart, selectionEnd,
|
|
3104
|
+
false, -1, -1);
|
|
3105
|
+
clearSelection();
|
|
3106
|
+
mCursorIndex = range[0];
|
|
3107
|
+
mCursorLine = getOffsetLine(mCursorIndex);
|
|
3108
|
+
adjustCursorPosition();
|
|
3109
|
+
onTextChanged();
|
|
3110
|
+
postInvalidate();
|
|
3111
|
+
return;
|
|
3112
|
+
}
|
|
3113
|
+
// degenerate -> fallthrough to single-line cut
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
// no selection: cut current line
|
|
3117
|
+
int[] range = fullLineRangeForCursorLine(mCursorLine);
|
|
3118
|
+
copyRangeToClipboard(range[0], range[1], "line");
|
|
3119
|
+
batchDeleteWithSelectionSnapshot(range[0], range[1],
|
|
3120
|
+
false, selectionStart, selectionEnd,
|
|
3121
|
+
false, -1, -1);
|
|
3122
|
+
mCursorIndex = range[0];
|
|
3123
|
+
mCursorLine = getOffsetLine(mCursorIndex);
|
|
3124
|
+
adjustCursorPosition();
|
|
3125
|
+
clearSelectionMenu();
|
|
3126
|
+
onTextChanged();
|
|
3127
|
+
postInvalidate();
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
// Replace line with clipboard text
|
|
3131
|
+
public void replaceLine() {
|
|
3132
|
+
if (!mClipboard.hasPrimaryClip()) return;
|
|
3133
|
+
ClipData data = mClipboard.getPrimaryClip();
|
|
3134
|
+
if (data == null || data.getItemCount() == 0) return;
|
|
3135
|
+
ClipData.Item item = data.getItemAt(0);
|
|
3136
|
+
CharSequence raw = item.getText();
|
|
3137
|
+
if (raw == null) return;
|
|
3138
|
+
String clipboardText = raw.toString();
|
|
3139
|
+
|
|
3140
|
+
if (isSelectMode) {
|
|
3141
|
+
int[] sel = normalizeSelection();
|
|
3142
|
+
int s = sel[0], e = sel[1];
|
|
3143
|
+
int[] range = fullLineRangeForSelection(s, e);
|
|
3144
|
+
if (range != null) {
|
|
3145
|
+
int replaceStart = range[0];
|
|
3146
|
+
int replaceEnd = range[1];
|
|
3147
|
+
batchReplaceWithSelectionSnapshot(replaceStart, replaceEnd, clipboardText,
|
|
3148
|
+
true, selectionStart, selectionEnd,
|
|
3149
|
+
true, replaceStart, replaceStart + clipboardText.length());
|
|
3150
|
+
// update UI to reflect selection of replaced block
|
|
3151
|
+
setSelection(replaceStart, replaceStart + clipboardText.length());
|
|
3152
|
+
clearSelectionMenu(); // optional; original logic kept
|
|
3153
|
+
mCursorIndex = selectionEnd;
|
|
3154
|
+
mCursorLine = getOffsetLine(mCursorIndex);
|
|
3155
|
+
adjustCursorPosition();
|
|
3156
|
+
onTextChanged();
|
|
3157
|
+
postInvalidate();
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3160
|
+
// degenerate -> fallthrough
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
// no selection: replace current line content
|
|
3164
|
+
int[] range = fullLineRangeForCursorLine(mCursorLine);
|
|
3165
|
+
int lineStart = range[0];
|
|
3166
|
+
int lineEnd = range[1];
|
|
3167
|
+
batchReplaceWithSelectionSnapshot(lineStart, lineEnd, clipboardText,
|
|
3168
|
+
false, selectionStart, selectionEnd,
|
|
3169
|
+
false, -1, -1);
|
|
3170
|
+
clearSelection();
|
|
3171
|
+
mCursorIndex = lineStart + clipboardText.length();
|
|
3172
|
+
mCursorLine = getOffsetLine(mCursorIndex);
|
|
3173
|
+
adjustCursorPosition();
|
|
3174
|
+
onTextChanged();
|
|
3175
|
+
postInvalidate();
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
// clear line texts
|
|
3179
|
+
public void deleteLine() {
|
|
3180
|
+
if (isSelectMode) {
|
|
3181
|
+
int[] sel = normalizeSelection();
|
|
3182
|
+
int s = sel[0], e = sel[1];
|
|
3183
|
+
int[] range = fullLineRangeForSelection(s, e);
|
|
3184
|
+
if (range != null) {
|
|
3185
|
+
batchDeleteWithSelectionSnapshot(range[0], range[1],
|
|
3186
|
+
true, selectionStart, selectionEnd,
|
|
3187
|
+
false, -1, -1);
|
|
3188
|
+
clearSelection();
|
|
3189
|
+
mCursorIndex = Math.min(range[0], mGapBuffer.length());
|
|
3190
|
+
mCursorLine = getOffsetLine(mCursorIndex);
|
|
3191
|
+
adjustCursorPosition();
|
|
3192
|
+
onTextChanged();
|
|
3193
|
+
postInvalidate();
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
// degenerate -> fallthrough
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
// no selection: delete current line
|
|
3200
|
+
int[] range = fullLineRangeForCursorLine(mCursorLine);
|
|
3201
|
+
batchDeleteWithSelectionSnapshot(range[0], range[1],
|
|
3202
|
+
false, selectionStart, selectionEnd,
|
|
3203
|
+
false, -1, -1);
|
|
3204
|
+
mCursorIndex = Math.min(range[0], mGapBuffer.length());
|
|
3205
|
+
mCursorLine = getOffsetLine(mCursorIndex);
|
|
3206
|
+
adjustCursorPosition();
|
|
3207
|
+
clearSelectionMenu();
|
|
3208
|
+
onTextChanged();
|
|
3209
|
+
postInvalidate();
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
// Clear line texts but keep line number
|
|
3213
|
+
public void emptyLine() {
|
|
3214
|
+
if (isSelectMode) {
|
|
3215
|
+
int[] sel = normalizeSelection();
|
|
3216
|
+
int s = sel[0], e = sel[1];
|
|
3217
|
+
int[] range = fullLineRangeForSelection(s, e);
|
|
3218
|
+
if (range != null) {
|
|
3219
|
+
// delete each line's content but keep newlines (we delete content range per-line)
|
|
3220
|
+
mGapBuffer.markSelectionBefore(selectionStart, selectionEnd, true);
|
|
3221
|
+
mGapBuffer.beginBatchEdit();
|
|
3222
|
+
int firstLine = getOffsetLine(s);
|
|
3223
|
+
int lastLine = getOffsetLine(Math.max(0, e - 1));
|
|
3224
|
+
for (int line = firstLine; line <= lastLine; line++) {
|
|
3225
|
+
int start = getLineStart(line);
|
|
3226
|
+
int end = start + getLine(line).length();
|
|
3227
|
+
if (end > start) {
|
|
3228
|
+
mGapBuffer.delete(start, end, true);
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
mGapBuffer.markSelectionAfter(-1, -1, false);
|
|
3232
|
+
mGapBuffer.endBatchEdit();
|
|
3233
|
+
|
|
3234
|
+
clearSelection();
|
|
3235
|
+
mCursorLine = getOffsetLine(range[0]);
|
|
3236
|
+
mCursorIndex = getLineStart(mCursorLine);
|
|
3237
|
+
adjustCursorPosition();
|
|
3238
|
+
onTextChanged();
|
|
3239
|
+
postInvalidate();
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
// degenerate -> fallthrough
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
// no selection: empty current line
|
|
3246
|
+
int[] range = fullLineRangeForCursorLine(mCursorLine);
|
|
3247
|
+
int lineStart = range[0];
|
|
3248
|
+
int lineEnd = range[1];
|
|
3249
|
+
if (lineEnd > lineStart) {
|
|
3250
|
+
mGapBuffer.beginBatchEdit();
|
|
3251
|
+
mGapBuffer.delete(lineStart, lineEnd, true);
|
|
3252
|
+
mGapBuffer.endBatchEdit();
|
|
3253
|
+
}
|
|
3254
|
+
mCursorIndex = lineStart;
|
|
3255
|
+
adjustCursorPosition();
|
|
3256
|
+
clearSelectionMenu();
|
|
3257
|
+
onTextChanged();
|
|
3258
|
+
postInvalidate();
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
public void duplicateLine() {
|
|
3262
|
+
// --- CASE 1: Selection Mode ---
|
|
3263
|
+
if (isSelectMode) {
|
|
3264
|
+
|
|
3265
|
+
int start = Math.min(selectionStart, selectionEnd);
|
|
3266
|
+
int end = Math.max(selectionStart, selectionEnd);
|
|
3267
|
+
|
|
3268
|
+
// Expand selection to full lines
|
|
3269
|
+
int startLine = getOffsetLine(start);
|
|
3270
|
+
int endLine = getOffsetLine(end);
|
|
3271
|
+
|
|
3272
|
+
int lineStart = getLineStart(startLine);
|
|
3273
|
+
int lineEnd = getLineEnd(endLine);
|
|
3274
|
+
|
|
3275
|
+
String block = mGapBuffer.substring(lineStart, lineEnd);
|
|
3276
|
+
|
|
3277
|
+
// Insert duplicated block (ALWAYS add newline before block)
|
|
3278
|
+
mGapBuffer.insert(lineEnd, "\n" + block, true);
|
|
3279
|
+
|
|
3280
|
+
// Reselect duplicated block
|
|
3281
|
+
int dupStart = lineEnd + 1;
|
|
3282
|
+
int dupEnd = dupStart + block.length();
|
|
3283
|
+
|
|
3284
|
+
selectionStart = dupStart;
|
|
3285
|
+
selectionEnd = dupEnd;
|
|
3286
|
+
|
|
3287
|
+
updateSelectionHandles();
|
|
3288
|
+
onTextChanged();
|
|
3289
|
+
postInvalidate();
|
|
3290
|
+
return;
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
// --- CASE 2: No Selection (Cursor only) ---
|
|
3294
|
+
int currentLine = mCursorLine;
|
|
3295
|
+
|
|
3296
|
+
int lineStart = getLineStart(currentLine);
|
|
3297
|
+
int lineEnd = getLineEnd(currentLine);
|
|
3298
|
+
|
|
3299
|
+
String lineText = mGapBuffer.substring(lineStart, lineEnd);
|
|
3300
|
+
|
|
3301
|
+
// Insert duplicated line
|
|
3302
|
+
mGapBuffer.insert(lineEnd, "\n" + lineText, true);
|
|
3303
|
+
|
|
3304
|
+
// Maintain cursor column
|
|
3305
|
+
int col = getColumn();
|
|
3306
|
+
|
|
3307
|
+
// Move cursor to new duplicated line
|
|
3308
|
+
int newLine = currentLine + 1;
|
|
3309
|
+
int newLineStart = getLineStart(newLine);
|
|
3310
|
+
|
|
3311
|
+
int target = newLineStart + col;
|
|
3312
|
+
|
|
3313
|
+
// Clamp column if line shorter
|
|
3314
|
+
int newLineEnd = getLineEnd(newLine);
|
|
3315
|
+
if (target > newLineEnd) {
|
|
3316
|
+
target = newLineEnd;
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
// APPLY cursor move
|
|
3320
|
+
setCursorPosition(target);
|
|
3321
|
+
|
|
3322
|
+
// Ensure no selection mode active
|
|
3323
|
+
isSelectMode = false;
|
|
3324
|
+
clearLineSelection();
|
|
3325
|
+
|
|
3326
|
+
onTextChanged();
|
|
3327
|
+
postInvalidate();
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
// Case conversion methods
|
|
3331
|
+
public void convertSelectionToLowerCase() {
|
|
3332
|
+
if (isSelectMode) {
|
|
3333
|
+
// normalize & clamp selection (defensive)
|
|
3334
|
+
int s = Math.max(0, Math.min(selectionStart, selectionEnd));
|
|
3335
|
+
int e = Math.max(0, Math.max(selectionStart, selectionEnd));
|
|
3336
|
+
if (s == e) return;
|
|
3337
|
+
|
|
3338
|
+
String selectedText = mGapBuffer.substring(s, e);
|
|
3339
|
+
if (selectedText != null && !selectedText.isEmpty()) {
|
|
3340
|
+
String lowerCaseText = selectedText.toLowerCase();
|
|
3341
|
+
|
|
3342
|
+
mGapBuffer.beginBatchEdit(); // <<< group undo
|
|
3343
|
+
mGapBuffer.replace(s, e, lowerCaseText, true);
|
|
3344
|
+
mGapBuffer.endBatchEdit(); // <<< end group
|
|
3345
|
+
|
|
3346
|
+
// update selection to cover replaced text (handles length changes)
|
|
3347
|
+
selectionStart = s;
|
|
3348
|
+
selectionEnd = s + lowerCaseText.length();
|
|
3349
|
+
|
|
3350
|
+
// clamp
|
|
3351
|
+
selectionStart = Math.max(0, Math.min(selectionStart, mGapBuffer.length()));
|
|
3352
|
+
selectionEnd = Math.max(0, Math.min(selectionEnd, mGapBuffer.length()));
|
|
3353
|
+
|
|
3354
|
+
updateSelectionHandles();
|
|
3355
|
+
}
|
|
3356
|
+
} else {
|
|
3357
|
+
// no selection: operate on current line
|
|
3358
|
+
int lineStart = getLineStart(mCursorLine);
|
|
3359
|
+
String currentLine = getLine(mCursorLine);
|
|
3360
|
+
if (currentLine != null && !currentLine.isEmpty()) {
|
|
3361
|
+
String lowerCaseText = currentLine.toLowerCase();
|
|
3362
|
+
|
|
3363
|
+
mGapBuffer.beginBatchEdit();
|
|
3364
|
+
mGapBuffer.replace(lineStart, lineStart + currentLine.length(), lowerCaseText, true);
|
|
3365
|
+
mGapBuffer.endBatchEdit();
|
|
3366
|
+
|
|
3367
|
+
clearSelectionMenu();
|
|
3368
|
+
|
|
3369
|
+
// move cursor to end of the replaced line
|
|
3370
|
+
mCursorIndex = lineStart + lowerCaseText.length();
|
|
3371
|
+
mCursorLine = getOffsetLine(mCursorIndex);
|
|
3372
|
+
adjustCursorPosition();
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
onTextChanged();
|
|
3376
|
+
postInvalidate();
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
public void convertSelectionToUpperCase() {
|
|
3380
|
+
if (isSelectMode) {
|
|
3381
|
+
// normalize & clamp selection (defensive)
|
|
3382
|
+
int s = Math.max(0, Math.min(selectionStart, selectionEnd));
|
|
3383
|
+
int e = Math.max(0, Math.max(selectionStart, selectionEnd));
|
|
3384
|
+
if (s == e) return;
|
|
3385
|
+
|
|
3386
|
+
String selectedText = mGapBuffer.substring(s, e);
|
|
3387
|
+
if (selectedText != null && !selectedText.isEmpty()) {
|
|
3388
|
+
String upperCaseText = selectedText.toUpperCase();
|
|
3389
|
+
|
|
3390
|
+
mGapBuffer.beginBatchEdit(); // <<< group undo
|
|
3391
|
+
mGapBuffer.replace(s, e, upperCaseText, true);
|
|
3392
|
+
mGapBuffer.endBatchEdit(); // <<< end group
|
|
3393
|
+
|
|
3394
|
+
// update selection to cover replaced text (handles length changes)
|
|
3395
|
+
selectionStart = s;
|
|
3396
|
+
selectionEnd = s + upperCaseText.length();
|
|
3397
|
+
|
|
3398
|
+
// clamp
|
|
3399
|
+
selectionStart = Math.max(0, Math.min(selectionStart, mGapBuffer.length()));
|
|
3400
|
+
selectionEnd = Math.max(0, Math.min(selectionEnd, mGapBuffer.length()));
|
|
3401
|
+
|
|
3402
|
+
updateSelectionHandles();
|
|
3403
|
+
}
|
|
3404
|
+
} else {
|
|
3405
|
+
// no selection: operate on current line
|
|
3406
|
+
int lineStart = getLineStart(mCursorLine);
|
|
3407
|
+
String currentLine = getLine(mCursorLine);
|
|
3408
|
+
if (currentLine != null && !currentLine.isEmpty()) {
|
|
3409
|
+
String upperCaseText = currentLine.toUpperCase();
|
|
3410
|
+
|
|
3411
|
+
mGapBuffer.beginBatchEdit();
|
|
3412
|
+
mGapBuffer.replace(lineStart, lineStart + currentLine.length(), upperCaseText, true);
|
|
3413
|
+
mGapBuffer.endBatchEdit();
|
|
3414
|
+
|
|
3415
|
+
clearSelectionMenu();
|
|
3416
|
+
|
|
3417
|
+
// move cursor to end of the replaced line
|
|
3418
|
+
mCursorIndex = lineStart + upperCaseText.length();
|
|
3419
|
+
mCursorLine = getOffsetLine(mCursorIndex);
|
|
3420
|
+
adjustCursorPosition();
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
onTextChanged();
|
|
3424
|
+
postInvalidate();
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
// Indentation methods
|
|
3428
|
+
public void increaseIndent() {
|
|
3429
|
+
mGapBuffer.beginBatchEdit(); // UNDO batch start
|
|
3430
|
+
|
|
3431
|
+
if (isSelectMode) {
|
|
3432
|
+
int startLine = getOffsetLine(selectionStart);
|
|
3433
|
+
int endLine = getOffsetLine(selectionEnd);
|
|
3434
|
+
|
|
3435
|
+
for (int i = startLine; i <= endLine; i++) {
|
|
3436
|
+
int lineStart = getLineStart(i);
|
|
3437
|
+
mGapBuffer.insert(lineStart, " ", true); // record in undo
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
int indentSize = 4;
|
|
3441
|
+
selectionStart += indentSize;
|
|
3442
|
+
selectionEnd += (endLine - startLine + 1) * indentSize;
|
|
3443
|
+
updateSelectionHandles();
|
|
3444
|
+
} else {
|
|
3445
|
+
int lineStart = getLineStart(mCursorLine);
|
|
3446
|
+
mGapBuffer.insert(lineStart, " ", true);
|
|
3447
|
+
|
|
3448
|
+
mCursorIndex += 4;
|
|
3449
|
+
adjustCursorPosition();
|
|
3450
|
+
clearSelectionMenu();
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
mGapBuffer.endBatchEdit(); // UNDO batch end
|
|
3454
|
+
|
|
3455
|
+
// Restore selection state after undo/redo cycles
|
|
3456
|
+
if (isSelectMode) {
|
|
3457
|
+
selectionStart = Math.min(mGapBuffer.length(), selectionStart);
|
|
3458
|
+
selectionEnd = Math.min(mGapBuffer.length(), selectionEnd);
|
|
3459
|
+
updateSelectionHandles();
|
|
3460
|
+
} else {
|
|
3461
|
+
isSelectMode = false;
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
onTextChanged();
|
|
3465
|
+
postInvalidate();
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
// Decrease indent
|
|
3469
|
+
public void decreaseIndent() {
|
|
3470
|
+
if (!isSelectMode) {
|
|
3471
|
+
// single-line path
|
|
3472
|
+
mGapBuffer.beginBatchEdit();
|
|
3473
|
+
|
|
3474
|
+
int lineStart = getLineStart(mCursorLine);
|
|
3475
|
+
String line = getLine(mCursorLine);
|
|
3476
|
+
|
|
3477
|
+
int spacesToRemove = 0;
|
|
3478
|
+
if (line.startsWith(" ")) spacesToRemove = 4;
|
|
3479
|
+
else if (line.startsWith(" ")) spacesToRemove = 2;
|
|
3480
|
+
else if (line.startsWith("\t")) spacesToRemove = 1;
|
|
3481
|
+
else if (line.startsWith(" ")) spacesToRemove = 1;
|
|
3482
|
+
|
|
3483
|
+
if (spacesToRemove > 0) {
|
|
3484
|
+
mGapBuffer.delete(lineStart, lineStart + spacesToRemove, true);
|
|
3485
|
+
mCursorIndex = Math.max(0, mCursorIndex - spacesToRemove);
|
|
3486
|
+
adjustCursorPosition();
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
mGapBuffer.endBatchEdit();
|
|
3490
|
+
onTextChanged();
|
|
3491
|
+
postInvalidate();
|
|
3492
|
+
return;
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
// --- Multi-line selection
|
|
3496
|
+
int selStartOrig = Math.max(0, Math.min(selectionStart, selectionEnd));
|
|
3497
|
+
int selEndOrig = Math.max(0, Math.max(selectionStart, selectionEnd));
|
|
3498
|
+
|
|
3499
|
+
int startLine = getOffsetLine(selStartOrig);
|
|
3500
|
+
int endLine = getOffsetLine(selEndOrig == 0 ? 0 : Math.max(0, selEndOrig - 1));
|
|
3501
|
+
// build snapshot of line starts and text BEFORE any edits
|
|
3502
|
+
int lineCount = endLine - startLine + 1;
|
|
3503
|
+
int[] lineStarts = new int[lineCount];
|
|
3504
|
+
String[] lineTexts = new String[lineCount];
|
|
3505
|
+
|
|
3506
|
+
for (int i = 0; i < lineCount; i++) {
|
|
3507
|
+
int lineNo = startLine + i;
|
|
3508
|
+
lineStarts[i] = getLineStart(lineNo);
|
|
3509
|
+
lineTexts[i] = getLine(lineNo);
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
mGapBuffer.beginBatchEdit();
|
|
3513
|
+
|
|
3514
|
+
int totalRemoved = 0; // cumulative removed so far (affects later delete positions)
|
|
3515
|
+
|
|
3516
|
+
for (int i = 0; i < lineCount; i++) {
|
|
3517
|
+
int origLineStart = lineStarts[i];
|
|
3518
|
+
String line = lineTexts[i];
|
|
3519
|
+
|
|
3520
|
+
int spacesToRemove = 0;
|
|
3521
|
+
if (line.startsWith(" ")) spacesToRemove = 4;
|
|
3522
|
+
else if (line.startsWith(" ")) spacesToRemove = 2;
|
|
3523
|
+
else if (line.startsWith("\t")) spacesToRemove = 1;
|
|
3524
|
+
else if (line.startsWith(" ")) spacesToRemove = 1;
|
|
3525
|
+
else continue; // nothing to remove on this line
|
|
3526
|
+
|
|
3527
|
+
// delete position must be adjusted by how many chars we removed earlier
|
|
3528
|
+
int deleteStart = origLineStart - totalRemoved;
|
|
3529
|
+
// defensive clamp
|
|
3530
|
+
deleteStart = Math.max(0, Math.min(deleteStart, mGapBuffer.length()));
|
|
3531
|
+
|
|
3532
|
+
mGapBuffer.delete(deleteStart, deleteStart + spacesToRemove, true);
|
|
3533
|
+
|
|
3534
|
+
// update cumulative removed
|
|
3535
|
+
totalRemoved += spacesToRemove;
|
|
3536
|
+
|
|
3537
|
+
// adjust original selection endpoints relative to original line starts:
|
|
3538
|
+
// if the deleted area was strictly before the original selection start/end, shift them
|
|
3539
|
+
// left
|
|
3540
|
+
if (origLineStart < selStartOrig)
|
|
3541
|
+
selStartOrig = Math.max(0, selStartOrig - spacesToRemove);
|
|
3542
|
+
if (origLineStart < selEndOrig) selEndOrig = Math.max(0, selEndOrig - spacesToRemove);
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
mGapBuffer.endBatchEdit();
|
|
3546
|
+
|
|
3547
|
+
// Apply adjusted selection values and clamp to buffer
|
|
3548
|
+
selectionStart = Math.max(0, Math.min(selStartOrig, mGapBuffer.length()));
|
|
3549
|
+
selectionEnd = Math.max(0, Math.min(selEndOrig, mGapBuffer.length()));
|
|
3550
|
+
updateSelectionHandles();
|
|
3551
|
+
|
|
3552
|
+
onTextChanged();
|
|
3553
|
+
postInvalidate();
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
// Add Comment
|
|
3557
|
+
public void toggleComment() {
|
|
3558
|
+
// Determine the affected lines depending on selection mode
|
|
3559
|
+
int startLine, endLine;
|
|
3560
|
+
|
|
3561
|
+
if (isSelectMode) {
|
|
3562
|
+
// ensure selection indices are normalized and clamped
|
|
3563
|
+
int s = Math.max(0, Math.min(selectionStart, selectionEnd));
|
|
3564
|
+
int e = Math.max(0, Math.max(selectionStart, selectionEnd));
|
|
3565
|
+
startLine = getOffsetLine(s);
|
|
3566
|
+
endLine = getOffsetLine(e == 0 ? 0 : Math.max(0, e - 1)); // ensure we pick the line
|
|
3567
|
+
// that contains e-1
|
|
3568
|
+
} else {
|
|
3569
|
+
// single line: operate only on cursor line
|
|
3570
|
+
startLine = endLine = mCursorLine;
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
mGapBuffer.beginBatchEdit(); // group all changes into one undo step
|
|
3574
|
+
|
|
3575
|
+
boolean allCommented = true;
|
|
3576
|
+
for (int i = startLine; i <= endLine; i++) {
|
|
3577
|
+
String line = getLine(i);
|
|
3578
|
+
if (!isLineCommented(line) && !line.trim().isEmpty()) {
|
|
3579
|
+
allCommented = false;
|
|
3580
|
+
break;
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
// If all lines are commented -> remove, else add comments for uncommented lines
|
|
3585
|
+
for (int i = startLine; i <= endLine; i++) {
|
|
3586
|
+
String line = getLine(i);
|
|
3587
|
+
if (allCommented) {
|
|
3588
|
+
// only remove if commented
|
|
3589
|
+
if (isLineCommented(line)) {
|
|
3590
|
+
removeCommentFromLine(i);
|
|
3591
|
+
}
|
|
3592
|
+
} else {
|
|
3593
|
+
// add comment if not commented and not blank
|
|
3594
|
+
if (!isLineCommented(line) && !line.trim().isEmpty()) {
|
|
3595
|
+
addCommentToLine(i);
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
mGapBuffer.endBatchEdit(); // end grouping
|
|
3601
|
+
|
|
3602
|
+
// After editing, update UI and selection/cursor
|
|
3603
|
+
if (isSelectMode) {
|
|
3604
|
+
// Keep selection mode, but selection offsets were adjusted in add/remove helpers.
|
|
3605
|
+
updateSelectionHandles();
|
|
3606
|
+
} else {
|
|
3607
|
+
clearSelectionMenu();
|
|
3608
|
+
adjustCursorPosition();
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
onTextChanged();
|
|
3612
|
+
postInvalidate();
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
private boolean isLineCommented(String line) {
|
|
3616
|
+
if (line == null || line.isEmpty()) return false;
|
|
3617
|
+
|
|
3618
|
+
// Find the position after leading whitespace
|
|
3619
|
+
int contentStart = 0;
|
|
3620
|
+
while (contentStart < line.length() && Character.isWhitespace(line.charAt(contentStart))) {
|
|
3621
|
+
contentStart++;
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
// Check if there's a comment after whitespace
|
|
3625
|
+
return contentStart < line.length() && line.charAt(contentStart) == '#';
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
public void addComment() {
|
|
3629
|
+
if (isSelectMode) {
|
|
3630
|
+
int s = Math.max(0, Math.min(selectionStart, selectionEnd));
|
|
3631
|
+
int e = Math.max(0, Math.max(selectionStart, selectionEnd));
|
|
3632
|
+
int startLine = getOffsetLine(s);
|
|
3633
|
+
int endLine = getOffsetLine(e == 0 ? 0 : Math.max(0, e - 1));
|
|
3634
|
+
|
|
3635
|
+
mGapBuffer.beginBatchEdit();
|
|
3636
|
+
for (int i = startLine; i <= endLine; i++) {
|
|
3637
|
+
String line = getLine(i);
|
|
3638
|
+
if (!isLineCommented(line) && !line.trim().isEmpty()) {
|
|
3639
|
+
addCommentToLine(i);
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
mGapBuffer.endBatchEdit();
|
|
3643
|
+
|
|
3644
|
+
updateSelectionHandles();
|
|
3645
|
+
} else {
|
|
3646
|
+
String line = getLine(mCursorLine);
|
|
3647
|
+
if (!isLineCommented(line) && !line.trim().isEmpty()) {
|
|
3648
|
+
mGapBuffer.beginBatchEdit();
|
|
3649
|
+
addCommentToLine(mCursorLine);
|
|
3650
|
+
mGapBuffer.endBatchEdit();
|
|
3651
|
+
}
|
|
3652
|
+
clearSelectionMenu();
|
|
3653
|
+
adjustCursorPosition();
|
|
3654
|
+
}
|
|
3655
|
+
onTextChanged();
|
|
3656
|
+
postInvalidate();
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
public void removeComment() {
|
|
3660
|
+
if (isSelectMode) {
|
|
3661
|
+
int s = Math.max(0, Math.min(selectionStart, selectionEnd));
|
|
3662
|
+
int e = Math.max(0, Math.max(selectionStart, selectionEnd));
|
|
3663
|
+
int startLine = getOffsetLine(s);
|
|
3664
|
+
int endLine = getOffsetLine(e == 0 ? 0 : Math.max(0, e - 1));
|
|
3665
|
+
|
|
3666
|
+
mGapBuffer.beginBatchEdit();
|
|
3667
|
+
for (int i = startLine; i <= endLine; i++) {
|
|
3668
|
+
String line = getLine(i);
|
|
3669
|
+
if (isLineCommented(line)) {
|
|
3670
|
+
removeCommentFromLine(i);
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
mGapBuffer.endBatchEdit();
|
|
3674
|
+
|
|
3675
|
+
updateSelectionHandles();
|
|
3676
|
+
} else {
|
|
3677
|
+
String line = getLine(mCursorLine);
|
|
3678
|
+
if (isLineCommented(line)) {
|
|
3679
|
+
mGapBuffer.beginBatchEdit();
|
|
3680
|
+
removeCommentFromLine(mCursorLine);
|
|
3681
|
+
mGapBuffer.endBatchEdit();
|
|
3682
|
+
}
|
|
3683
|
+
clearSelectionMenu();
|
|
3684
|
+
adjustCursorPosition();
|
|
3685
|
+
}
|
|
3686
|
+
onTextChanged();
|
|
3687
|
+
postInvalidate();
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
private void addCommentToLine(int lineNumber) {
|
|
3691
|
+
int lineStart = getLineStart(lineNumber);
|
|
3692
|
+
String line = getLine(lineNumber);
|
|
3693
|
+
|
|
3694
|
+
if (line == null || line.trim().isEmpty() || isLineCommented(line)) return;
|
|
3695
|
+
|
|
3696
|
+
int contentStart = 0;
|
|
3697
|
+
while (contentStart < line.length() && Character.isWhitespace(line.charAt(contentStart))) {
|
|
3698
|
+
contentStart++;
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
int insertPos = lineStart + contentStart; // logical offset where comment marker should be
|
|
3702
|
+
// inserted
|
|
3703
|
+
|
|
3704
|
+
// Insert marker and capture undo
|
|
3705
|
+
mGapBuffer.insert(insertPos, getCommentBlock() + " ", true);
|
|
3706
|
+
|
|
3707
|
+
// If selection exists, shift selection indices forward when they are at/after insert
|
|
3708
|
+
if (isSelectMode) {
|
|
3709
|
+
// Use >= so selection that starts exactly at insert will include the inserted text
|
|
3710
|
+
if (selectionStart >= insertPos) selectionStart += 2;
|
|
3711
|
+
if (selectionEnd >= insertPos) selectionEnd += 2;
|
|
3712
|
+
// clamp
|
|
3713
|
+
selectionStart = Math.max(0, Math.min(selectionStart, mGapBuffer.length()));
|
|
3714
|
+
selectionEnd = Math.max(0, Math.min(selectionEnd, mGapBuffer.length()));
|
|
3715
|
+
} else {
|
|
3716
|
+
// If single-line and cursor is on the same line, advance cursor offset so cursor stays
|
|
3717
|
+
// after inserted marker
|
|
3718
|
+
int cursorOffset = getLineStart(lineNumber) + contentStart;
|
|
3719
|
+
// if you keep cursor as an offset, update it here; otherwise adjust mCursorLine related
|
|
3720
|
+
// state via adjustCursorPosition()
|
|
3721
|
+
// (call adjustCursorPosition() at the caller after editing)
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
private void removeCommentFromLine(int lineNumber) {
|
|
3726
|
+
int lineStart = getLineStart(lineNumber);
|
|
3727
|
+
String line = getLine(lineNumber);
|
|
3728
|
+
|
|
3729
|
+
if (line == null || line.isEmpty()) return;
|
|
3730
|
+
|
|
3731
|
+
int contentStart = 0;
|
|
3732
|
+
while (contentStart < line.length() &&
|
|
3733
|
+
Character.isWhitespace(line.charAt(contentStart))) {
|
|
3734
|
+
contentStart++;
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
String block = getCommentBlock();
|
|
3738
|
+
int blockLen = block.length();
|
|
3739
|
+
|
|
3740
|
+
// check if line starts with comment block
|
|
3741
|
+
if (contentStart + blockLen <= line.length() &&
|
|
3742
|
+
line.startsWith(block, contentStart)) {
|
|
3743
|
+
|
|
3744
|
+
int deleteStart = lineStart + contentStart;
|
|
3745
|
+
int deleteEnd = deleteStart + blockLen;
|
|
3746
|
+
|
|
3747
|
+
// If there is a space after the comment, delete that too
|
|
3748
|
+
int nextCharIndex = contentStart + blockLen;
|
|
3749
|
+
if (nextCharIndex < line.length() && line.charAt(nextCharIndex) == ' ') {
|
|
3750
|
+
deleteEnd++; // remove the extra space
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
mGapBuffer.delete(deleteStart, deleteEnd, true);
|
|
3754
|
+
|
|
3755
|
+
// update selection
|
|
3756
|
+
int removedCount = deleteEnd - deleteStart;
|
|
3757
|
+
|
|
3758
|
+
if (isSelectMode) {
|
|
3759
|
+
if (selectionStart > deleteStart) selectionStart -= removedCount;
|
|
3760
|
+
if (selectionEnd > deleteStart) selectionEnd -= removedCount;
|
|
3761
|
+
|
|
3762
|
+
selectionStart = Math.max(0, Math.min(selectionStart, mGapBuffer.length()));
|
|
3763
|
+
selectionEnd = Math.max(0, Math.min(selectionEnd, mGapBuffer.length()));
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
// Normalize and clamp selection; returns [start, end] with start<=end
|
|
3769
|
+
private int[] normalizeSelection() {
|
|
3770
|
+
int s = Math.max(0, Math.min(selectionStart, selectionEnd));
|
|
3771
|
+
int e = Math.max(0, Math.max(selectionStart, selectionEnd));
|
|
3772
|
+
s = Math.max(0, Math.min(s, mGapBuffer.length()));
|
|
3773
|
+
e = Math.max(0, Math.min(e, mGapBuffer.length()));
|
|
3774
|
+
return new int[]{s, e};
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
// Compute full-line range [startOffset, endOffset] that covers selection [s,e].
|
|
3778
|
+
// If e==s this returns null (degenerate).
|
|
3779
|
+
// endOffset is content-end (not including trailing newline) for last line.
|
|
3780
|
+
private int[] fullLineRangeForSelection(int s, int e) {
|
|
3781
|
+
if (s >= e) return null;
|
|
3782
|
+
int firstLine = getOffsetLine(s);
|
|
3783
|
+
int lastLine = getOffsetLine(Math.max(0, e - 1));
|
|
3784
|
+
int start = getLineStart(firstLine);
|
|
3785
|
+
int lastLineStart = getLineStart(lastLine);
|
|
3786
|
+
int end = lastLineStart + getLine(lastLine).length(); // content-end
|
|
3787
|
+
start = Math.max(0, Math.min(start, mGapBuffer.length()));
|
|
3788
|
+
end = Math.max(0, Math.min(end, mGapBuffer.length()));
|
|
3789
|
+
if (start > end) end = start;
|
|
3790
|
+
return new int[]{start, end};
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
// Compute full-line range for the current cursor line (no newline included).
|
|
3794
|
+
private int[] fullLineRangeForCursorLine(int line) {
|
|
3795
|
+
int start = getLineStart(line);
|
|
3796
|
+
int end = start + getLine(line).length();
|
|
3797
|
+
start = Math.max(0, Math.min(start, mGapBuffer.length()));
|
|
3798
|
+
end = Math.max(0, Math.min(end, mGapBuffer.length()));
|
|
3799
|
+
if (start > end) end = start;
|
|
3800
|
+
return new int[]{start, end};
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3803
|
+
// Helper: mark selection before, begin batch, run replace once, mark after, end batch.
|
|
3804
|
+
// keepSelectAfter indicates whether to set selection to [outStart, outEnd] after replace
|
|
3805
|
+
private void batchReplaceWithSelectionSnapshot(int replaceStart, int replaceEnd, String text,
|
|
3806
|
+
boolean wasSelectBefore, int selBeforeStart, int selBeforeEnd,
|
|
3807
|
+
boolean setSelectionAfter, int selAfterStart, int selAfterEnd) {
|
|
3808
|
+
mGapBuffer.markSelectionBefore(selBeforeStart, selBeforeEnd, wasSelectBefore);
|
|
3809
|
+
mGapBuffer.beginBatchEdit();
|
|
3810
|
+
mGapBuffer.replace(replaceStart, replaceEnd, text, true);
|
|
3811
|
+
if (setSelectionAfter) {
|
|
3812
|
+
mGapBuffer.markSelectionAfter(selAfterStart, selAfterEnd, true);
|
|
3813
|
+
} else {
|
|
3814
|
+
mGapBuffer.markSelectionAfter(-1, -1, false);
|
|
3815
|
+
}
|
|
3816
|
+
mGapBuffer.endBatchEdit();
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
// Helper: delete a range with selection snapshot grouping
|
|
3820
|
+
private void batchDeleteWithSelectionSnapshot(int delStart, int delEnd,
|
|
3821
|
+
boolean wasSelectBefore, int selBeforeStart, int selBeforeEnd,
|
|
3822
|
+
boolean setSelectionAfter, int selAfterStart, int selAfterEnd) {
|
|
3823
|
+
mGapBuffer.markSelectionBefore(selBeforeStart, selBeforeEnd, wasSelectBefore);
|
|
3824
|
+
mGapBuffer.beginBatchEdit();
|
|
3825
|
+
mGapBuffer.delete(delStart, delEnd, true);
|
|
3826
|
+
if (setSelectionAfter) {
|
|
3827
|
+
mGapBuffer.markSelectionAfter(selAfterStart, selAfterEnd, true);
|
|
3828
|
+
} else {
|
|
3829
|
+
mGapBuffer.markSelectionAfter(-1, -1, false);
|
|
3830
|
+
}
|
|
3831
|
+
mGapBuffer.endBatchEdit();
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
// Helper: copy a buffer-range to clipboard
|
|
3835
|
+
private void copyRangeToClipboard(int start, int end, String label) {
|
|
3836
|
+
start = Math.max(0, Math.min(start, mGapBuffer.length()));
|
|
3837
|
+
end = Math.max(0, Math.min(end, mGapBuffer.length()));
|
|
3838
|
+
if (start > end) end = start;
|
|
3839
|
+
String text = mGapBuffer.substring(start, end);
|
|
3840
|
+
if (text != null && !text.isEmpty()) {
|
|
3841
|
+
ClipData data = ClipData.newPlainText(label, text);
|
|
3842
|
+
mClipboard.setPrimaryClip(data);
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
|
|
3846
|
+
// Helper: clear selection UI (common)
|
|
3847
|
+
private void clearSelection() {
|
|
3848
|
+
clearSelectionMenu();
|
|
3849
|
+
isSelectMode = false;
|
|
3850
|
+
selectionStart = selectionEnd = -1;
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
// ===== SCALE GESTURE LISTENER CLASS =====
|
|
3854
|
+
class ScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
|
|
3855
|
+
@Override
|
|
3856
|
+
public boolean onScale(ScaleGestureDetector detector) {
|
|
3857
|
+
float factor = detector.getScaleFactor();
|
|
3858
|
+
setTextSize(mTextPaint.getTextSize() * factor);
|
|
3859
|
+
return true;
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
// ---------- Text input connection (IME) ----------
|
|
3864
|
+
class TextInputConnection extends BaseInputConnection {
|
|
3865
|
+
|
|
3866
|
+
public TextInputConnection(View view, boolean fullEditor) {
|
|
3867
|
+
super(view, fullEditor);
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
@Override
|
|
3871
|
+
public boolean commitText(CharSequence text, int newCursorPosition) {
|
|
3872
|
+
long currentTime = System.currentTimeMillis();
|
|
3873
|
+
|
|
3874
|
+
if (currentTime - mLastInputTime < INPUT_DEBOUNCE_DELAY &&
|
|
3875
|
+
text.toString().equals(mLastCommittedText)) {
|
|
3876
|
+
return true;
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3879
|
+
if (text != null && text.length() > 0) {
|
|
3880
|
+
mProcessingInput = true;
|
|
3881
|
+
mLastCommittedText = text.toString();
|
|
3882
|
+
mLastInputTime = currentTime;
|
|
3883
|
+
|
|
3884
|
+
// Call insert to handle the text and trigger auto-complete
|
|
3885
|
+
insert(text.toString());
|
|
3886
|
+
|
|
3887
|
+
postDelayed(new Runnable() {
|
|
3888
|
+
@Override
|
|
3889
|
+
public void run() {
|
|
3890
|
+
mProcessingInput = false;
|
|
3891
|
+
}
|
|
3892
|
+
}, INPUT_DEBOUNCE_DELAY);
|
|
3893
|
+
return true;
|
|
3894
|
+
}
|
|
3895
|
+
return super.commitText(text, newCursorPosition);
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
@Override
|
|
3899
|
+
public boolean setComposingText(CharSequence text, int newCursorPosition) {
|
|
3900
|
+
Log.d(TAG, "setComposingText: '" + text + "', newCursor=" + newCursorPosition);
|
|
3901
|
+
|
|
3902
|
+
// For composing text, we still want to handle it but not debounce
|
|
3903
|
+
if (text != null && text.length() > 0) {
|
|
3904
|
+
mProcessingInput = true;
|
|
3905
|
+
insert(text.toString());
|
|
3906
|
+
postDelayed(new Runnable() {
|
|
3907
|
+
@Override
|
|
3908
|
+
public void run() {
|
|
3909
|
+
mProcessingInput = false;
|
|
3910
|
+
}
|
|
3911
|
+
}, INPUT_DEBOUNCE_DELAY);
|
|
3912
|
+
return true;
|
|
3913
|
+
}
|
|
3914
|
+
return super.setComposingText(text, newCursorPosition);
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
@Override
|
|
3918
|
+
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
|
|
3919
|
+
Log.d(TAG, "deleteSurroundingText: before=" + beforeLength + ", after=" + afterLength);
|
|
3920
|
+
|
|
3921
|
+
if (mProcessingInput) {
|
|
3922
|
+
Log.d(TAG, "Skipping delete - processing input");
|
|
3923
|
+
return true;
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
if (beforeLength > 0) {
|
|
3927
|
+
for (int i = 0; i < beforeLength; i++) {
|
|
3928
|
+
delete();
|
|
3929
|
+
}
|
|
3930
|
+
return true;
|
|
3931
|
+
} else if (afterLength > 0) {
|
|
3932
|
+
for (int i = 0; i < afterLength; i++) {
|
|
3933
|
+
handleForwardDelete();
|
|
3934
|
+
}
|
|
3935
|
+
return true;
|
|
3936
|
+
}
|
|
3937
|
+
return super.deleteSurroundingText(beforeLength, afterLength);
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
@Override
|
|
3941
|
+
public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
|
|
3942
|
+
Log.d(TAG, "deleteSurroundingTextInCodePoints: before=" + beforeLength + ", after=" + afterLength);
|
|
3943
|
+
return deleteSurroundingText(beforeLength, afterLength);
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
@Override
|
|
3947
|
+
public boolean sendKeyEvent(KeyEvent event) {
|
|
3948
|
+
Log.d(TAG, "sendKeyEvent: " + event.getKeyCode() + ", action=" + event.getAction());
|
|
3949
|
+
|
|
3950
|
+
// Skip key events if we're already processing input to avoid duplicates
|
|
3951
|
+
if (mProcessingInput && event.getAction() == KeyEvent.ACTION_DOWN) {
|
|
3952
|
+
Log.d(TAG, "Skipping key event - processing input");
|
|
3953
|
+
return true;
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
// Let onKeyDown handle most keys, but ensure numbers and special chars work
|
|
3957
|
+
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
|
3958
|
+
int keyCode = event.getKeyCode();
|
|
3959
|
+
|
|
3960
|
+
// Handle keys that might not be properly handled by onKeyDown
|
|
3961
|
+
if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9 ||
|
|
3962
|
+
keyCode >= KeyEvent.KEYCODE_NUMPAD_0 && keyCode <= KeyEvent.KEYCODE_NUMPAD_9 ||
|
|
3963
|
+
keyCode == KeyEvent.KEYCODE_SPACE) {
|
|
3964
|
+
|
|
3965
|
+
int unicodeChar = event.getUnicodeChar();
|
|
3966
|
+
if (unicodeChar != 0) {
|
|
3967
|
+
mProcessingInput = true;
|
|
3968
|
+
insert(String.valueOf((char) unicodeChar));
|
|
3969
|
+
postDelayed(new Runnable() {
|
|
3970
|
+
@Override
|
|
3971
|
+
public void run() {
|
|
3972
|
+
mProcessingInput = false;
|
|
3973
|
+
}
|
|
3974
|
+
}, INPUT_DEBOUNCE_DELAY);
|
|
3975
|
+
return true;
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
return onKeyDown(keyCode, event);
|
|
3980
|
+
}
|
|
3981
|
+
return super.sendKeyEvent(event);
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
@Override
|
|
3985
|
+
public boolean finishComposingText() {
|
|
3986
|
+
Log.d(TAG, "finishComposingText");
|
|
3987
|
+
return true;
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3990
|
+
@Override
|
|
3991
|
+
public CharSequence getTextBeforeCursor(int length, int flags) {
|
|
3992
|
+
try {
|
|
3993
|
+
int start = Math.max(0, mCursorIndex - length);
|
|
3994
|
+
String text = mGapBuffer.substring(start, mCursorIndex);
|
|
3995
|
+
Log.d(TAG, "getTextBeforeCursor: " + text);
|
|
3996
|
+
return text;
|
|
3997
|
+
} catch (Exception e) {
|
|
3998
|
+
Log.e(TAG, "Error in getTextBeforeCursor", e);
|
|
3999
|
+
return "";
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
@Override
|
|
4004
|
+
public CharSequence getTextAfterCursor(int length, int flags) {
|
|
4005
|
+
try {
|
|
4006
|
+
int end = Math.min(mGapBuffer.length(), mCursorIndex + length);
|
|
4007
|
+
String text = mGapBuffer.substring(mCursorIndex, end);
|
|
4008
|
+
Log.d(TAG, "getTextAfterCursor: " + text);
|
|
4009
|
+
return text;
|
|
4010
|
+
} catch (Exception e) {
|
|
4011
|
+
Log.e(TAG, "Error in getTextAfterCursor", e);
|
|
4012
|
+
return "";
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
@Override
|
|
4017
|
+
public int getCursorCapsMode(int reqModes) {
|
|
4018
|
+
// This helps with auto-capitalization
|
|
4019
|
+
return TextUtils.getCapsMode(mGapBuffer.toString(), mCursorIndex, reqModes);
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
}
|