pull/1395/head
parent
c73c692aa2
commit
6b5c15ee66
@ -0,0 +1,449 @@ |
|||||||
|
package io.legado.app.ui.widget.code; |
||||||
|
|
||||||
|
import android.content.Context; |
||||||
|
import android.graphics.Canvas; |
||||||
|
import android.graphics.Paint; |
||||||
|
import android.graphics.Rect; |
||||||
|
import android.os.Handler; |
||||||
|
import android.text.Editable; |
||||||
|
import android.text.InputFilter; |
||||||
|
import android.text.Layout; |
||||||
|
import android.text.Spannable; |
||||||
|
import android.text.SpannableStringBuilder; |
||||||
|
import android.text.Spanned; |
||||||
|
import android.text.TextWatcher; |
||||||
|
import android.text.style.BackgroundColorSpan; |
||||||
|
import android.text.style.ForegroundColorSpan; |
||||||
|
import android.text.style.ReplacementSpan; |
||||||
|
import android.util.AttributeSet; |
||||||
|
|
||||||
|
import androidx.annotation.ColorInt; |
||||||
|
import androidx.annotation.NonNull; |
||||||
|
import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView; |
||||||
|
|
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.SortedMap; |
||||||
|
import java.util.TreeMap; |
||||||
|
import java.util.regex.Matcher; |
||||||
|
import java.util.regex.Pattern; |
||||||
|
|
||||||
|
@SuppressWarnings("unused") |
||||||
|
public class CodeView extends AppCompatMultiAutoCompleteTextView { |
||||||
|
|
||||||
|
private int tabWidth; |
||||||
|
private int tabWidthInCharacters; |
||||||
|
private int mUpdateDelayTime = 500; |
||||||
|
|
||||||
|
private boolean modified = true; |
||||||
|
private boolean highlightWhileTextChanging = true; |
||||||
|
|
||||||
|
private boolean hasErrors = false; |
||||||
|
private boolean mRemoveErrorsWhenTextChanged = true; |
||||||
|
|
||||||
|
private final Handler mUpdateHandler = new Handler(); |
||||||
|
private Tokenizer mAutoCompleteTokenizer; |
||||||
|
private final float displayDensity = getResources().getDisplayMetrics().density; |
||||||
|
|
||||||
|
private static final Pattern PATTERN_LINE = Pattern.compile("(^.+$)+", Pattern.MULTILINE); |
||||||
|
private static final Pattern PATTERN_TRAILING_WHITE_SPACE = Pattern.compile("[\\t ]+$", Pattern.MULTILINE); |
||||||
|
|
||||||
|
private final SortedMap<Integer, Integer> mErrorHashSet = new TreeMap<>(); |
||||||
|
private final Map<Pattern, Integer> mSyntaxPatternMap = new HashMap<>(); |
||||||
|
private List<Character> mIndentCharacterList = Arrays.asList('{', '+', '-', '*', '/', '='); |
||||||
|
|
||||||
|
public CodeView(Context context) { |
||||||
|
super(context); |
||||||
|
initEditorView(); |
||||||
|
} |
||||||
|
|
||||||
|
public CodeView(Context context, AttributeSet attrs) { |
||||||
|
super(context, attrs); |
||||||
|
initEditorView(); |
||||||
|
} |
||||||
|
|
||||||
|
public CodeView(Context context, AttributeSet attrs, int defStyleAttr) { |
||||||
|
super(context, attrs, defStyleAttr); |
||||||
|
initEditorView(); |
||||||
|
} |
||||||
|
|
||||||
|
private void initEditorView() { |
||||||
|
if (mAutoCompleteTokenizer == null) { |
||||||
|
mAutoCompleteTokenizer = new KeywordTokenizer(); |
||||||
|
} |
||||||
|
|
||||||
|
setTokenizer(mAutoCompleteTokenizer); |
||||||
|
|
||||||
|
setFilters(new InputFilter[]{ |
||||||
|
new InputFilter() { |
||||||
|
@Override |
||||||
|
public CharSequence filter(CharSequence source, int start, int end, |
||||||
|
Spanned dest, int dstart, int dend) { |
||||||
|
if (modified && |
||||||
|
end - start == 1 && |
||||||
|
start < source.length() && |
||||||
|
dstart < dest.length()) { |
||||||
|
char c = source.charAt(start); |
||||||
|
|
||||||
|
if (c == '\n') { |
||||||
|
return autoIndent(source, dest, dstart, dend); |
||||||
|
} |
||||||
|
} |
||||||
|
return source; |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
addTextChangedListener(mEditorTextWatcher); |
||||||
|
} |
||||||
|
|
||||||
|
private CharSequence autoIndent(CharSequence source, Spanned dest, int dstart, int dend) { |
||||||
|
String indent = ""; |
||||||
|
int istart = dstart - 1; |
||||||
|
|
||||||
|
boolean dataBefore = false; |
||||||
|
int pt = 0; |
||||||
|
|
||||||
|
for (; istart > -1; --istart) { |
||||||
|
char c = dest.charAt(istart); |
||||||
|
|
||||||
|
if (c == '\n') break; |
||||||
|
|
||||||
|
if (c != ' ' && c != '\t') { |
||||||
|
if (!dataBefore) { |
||||||
|
if (mIndentCharacterList.contains(c)) --pt; |
||||||
|
dataBefore = true; |
||||||
|
} |
||||||
|
|
||||||
|
if (c == '(') { |
||||||
|
--pt; |
||||||
|
} else if (c == ')') { |
||||||
|
++pt; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (istart > -1) { |
||||||
|
char charAtCursor = dest.charAt(dstart); |
||||||
|
int iend; |
||||||
|
|
||||||
|
for (iend = ++istart; iend < dend; ++iend) { |
||||||
|
char c = dest.charAt(iend); |
||||||
|
|
||||||
|
if (charAtCursor != '\n' && |
||||||
|
c == '/' && |
||||||
|
iend + 1 < dend && |
||||||
|
dest.charAt(iend) == c) { |
||||||
|
iend += 2; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
if (c != ' ' && c != '\t') { |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
indent += dest.subSequence(istart, iend); |
||||||
|
} |
||||||
|
|
||||||
|
if (pt < 0) { |
||||||
|
indent += "\t"; |
||||||
|
} |
||||||
|
|
||||||
|
return source + indent; |
||||||
|
} |
||||||
|
|
||||||
|
private void highlightSyntax(Editable editable) { |
||||||
|
if (mSyntaxPatternMap.isEmpty()) return; |
||||||
|
|
||||||
|
for (Pattern pattern : mSyntaxPatternMap.keySet()) { |
||||||
|
int color = mSyntaxPatternMap.get(pattern); |
||||||
|
for (Matcher m = pattern.matcher(editable); m.find(); ) { |
||||||
|
createForegroundColorSpan(editable, m, color); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void highlightErrorLines(Editable editable) { |
||||||
|
if (mErrorHashSet.isEmpty()) return; |
||||||
|
int maxErrorLineValue = mErrorHashSet.lastKey(); |
||||||
|
|
||||||
|
int lineNumber = 0; |
||||||
|
Matcher matcher = PATTERN_LINE.matcher(editable); |
||||||
|
while (matcher.find()) { |
||||||
|
if (mErrorHashSet.containsKey(lineNumber)) { |
||||||
|
int color = mErrorHashSet.get(lineNumber); |
||||||
|
createBackgroundColorSpan(editable, matcher, color); |
||||||
|
} |
||||||
|
lineNumber = lineNumber + 1; |
||||||
|
if (lineNumber > maxErrorLineValue) break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void createForegroundColorSpan(Editable editable, Matcher matcher, @ColorInt int color) { |
||||||
|
editable.setSpan(new ForegroundColorSpan(color), |
||||||
|
matcher.start(), matcher.end(), |
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
||||||
|
} |
||||||
|
|
||||||
|
private void createBackgroundColorSpan(Editable editable, Matcher matcher, @ColorInt int color) { |
||||||
|
editable.setSpan(new BackgroundColorSpan(color), |
||||||
|
matcher.start(), matcher.end(), |
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
||||||
|
} |
||||||
|
|
||||||
|
private Editable highlight(Editable editable) { |
||||||
|
if (editable.length() == 0) return editable; |
||||||
|
|
||||||
|
try { |
||||||
|
clearSpans(editable); |
||||||
|
highlightErrorLines(editable); |
||||||
|
highlightSyntax(editable); |
||||||
|
} catch (IllegalStateException e) { |
||||||
|
e.printStackTrace(); |
||||||
|
} |
||||||
|
return editable; |
||||||
|
} |
||||||
|
|
||||||
|
private void highlightWithoutChange(Editable editable) { |
||||||
|
modified = false; |
||||||
|
highlight(editable); |
||||||
|
modified = true; |
||||||
|
} |
||||||
|
|
||||||
|
public void setTextHighlighted(CharSequence text) { |
||||||
|
if (text == null || text.length() == 0) return; |
||||||
|
|
||||||
|
cancelHighlighterRender(); |
||||||
|
|
||||||
|
removeAllErrorLines(); |
||||||
|
|
||||||
|
modified = false; |
||||||
|
setText(highlight(new SpannableStringBuilder(text))); |
||||||
|
modified = true; |
||||||
|
} |
||||||
|
|
||||||
|
public void setTabWidth(int characters) { |
||||||
|
if (tabWidthInCharacters == characters) return; |
||||||
|
tabWidthInCharacters = characters; |
||||||
|
tabWidth = Math.round(getPaint().measureText("m") * characters); |
||||||
|
} |
||||||
|
|
||||||
|
private void clearSpans(Editable editable) { |
||||||
|
int length = editable.length(); |
||||||
|
ForegroundColorSpan[] foregroundSpans = editable.getSpans( |
||||||
|
0, length, ForegroundColorSpan.class); |
||||||
|
|
||||||
|
for (int i = foregroundSpans.length; i-- > 0; ) |
||||||
|
editable.removeSpan(foregroundSpans[i]); |
||||||
|
|
||||||
|
BackgroundColorSpan[] backgroundSpans = editable.getSpans( |
||||||
|
0, length, BackgroundColorSpan.class); |
||||||
|
|
||||||
|
for (int i = backgroundSpans.length; i-- > 0; ) |
||||||
|
editable.removeSpan(backgroundSpans[i]); |
||||||
|
} |
||||||
|
|
||||||
|
public void cancelHighlighterRender() { |
||||||
|
mUpdateHandler.removeCallbacks(mUpdateRunnable); |
||||||
|
} |
||||||
|
|
||||||
|
private void convertTabs(Editable editable, int start, int count) { |
||||||
|
if (tabWidth < 1) return; |
||||||
|
|
||||||
|
String s = editable.toString(); |
||||||
|
|
||||||
|
for (int stop = start + count; |
||||||
|
(start = s.indexOf("\t", start)) > -1 && start < stop; |
||||||
|
++start) { |
||||||
|
editable.setSpan(new TabWidthSpan(), |
||||||
|
start, |
||||||
|
start + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void setSyntaxPatternsMap(Map<Pattern, Integer> syntaxPatterns) { |
||||||
|
if (!mSyntaxPatternMap.isEmpty()) mSyntaxPatternMap.clear(); |
||||||
|
mSyntaxPatternMap.putAll(syntaxPatterns); |
||||||
|
} |
||||||
|
|
||||||
|
public void addSyntaxPattern(Pattern pattern, @ColorInt int Color) { |
||||||
|
mSyntaxPatternMap.put(pattern, Color); |
||||||
|
} |
||||||
|
|
||||||
|
public void removeSyntaxPattern(Pattern pattern) { |
||||||
|
mSyntaxPatternMap.remove(pattern); |
||||||
|
} |
||||||
|
|
||||||
|
public int getSyntaxPatternsSize() { |
||||||
|
return mSyntaxPatternMap.size(); |
||||||
|
} |
||||||
|
|
||||||
|
public void resetSyntaxPatternList() { |
||||||
|
mSyntaxPatternMap.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setAutoIndentCharacterList(List<Character> characterList) { |
||||||
|
mIndentCharacterList = characterList; |
||||||
|
} |
||||||
|
|
||||||
|
public void clearAutoIndentCharacterList() { |
||||||
|
mIndentCharacterList.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
public List<Character> getAutoIndentCharacterList() { |
||||||
|
return mIndentCharacterList; |
||||||
|
} |
||||||
|
|
||||||
|
public void addErrorLine(int lineNum, int color) { |
||||||
|
mErrorHashSet.put(lineNum, color); |
||||||
|
hasErrors = true; |
||||||
|
} |
||||||
|
|
||||||
|
public void removeErrorLine(int lineNum) { |
||||||
|
mErrorHashSet.remove(lineNum); |
||||||
|
hasErrors = mErrorHashSet.size() > 0; |
||||||
|
} |
||||||
|
|
||||||
|
public void removeAllErrorLines() { |
||||||
|
mErrorHashSet.clear(); |
||||||
|
hasErrors = false; |
||||||
|
} |
||||||
|
|
||||||
|
public int getErrorsSize() { |
||||||
|
return mErrorHashSet.size(); |
||||||
|
} |
||||||
|
|
||||||
|
public String getTextWithoutTrailingSpace() { |
||||||
|
return PATTERN_TRAILING_WHITE_SPACE |
||||||
|
.matcher(getText()) |
||||||
|
.replaceAll(""); |
||||||
|
} |
||||||
|
|
||||||
|
public void setAutoCompleteTokenizer(Tokenizer tokenizer) { |
||||||
|
mAutoCompleteTokenizer = tokenizer; |
||||||
|
} |
||||||
|
|
||||||
|
public void setRemoveErrorsWhenTextChanged(boolean removeErrors) { |
||||||
|
mRemoveErrorsWhenTextChanged = removeErrors; |
||||||
|
} |
||||||
|
|
||||||
|
public void reHighlightSyntax() { |
||||||
|
highlightSyntax(getEditableText()); |
||||||
|
} |
||||||
|
|
||||||
|
public void reHighlightErrors() { |
||||||
|
highlightErrorLines(getEditableText()); |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isHasError() { |
||||||
|
return hasErrors; |
||||||
|
} |
||||||
|
|
||||||
|
public void setUpdateDelayTime(int time) { |
||||||
|
mUpdateDelayTime = time; |
||||||
|
} |
||||||
|
|
||||||
|
public int getUpdateDelayTime() { |
||||||
|
return mUpdateDelayTime; |
||||||
|
} |
||||||
|
|
||||||
|
public void setHighlightWhileTextChanging(boolean updateWhileTextChanging) { |
||||||
|
this.highlightWhileTextChanging = updateWhileTextChanging; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void showDropDown() { |
||||||
|
int[] screenPoint = new int[2]; |
||||||
|
getLocationOnScreen(screenPoint); |
||||||
|
|
||||||
|
final Rect displayFrame = new Rect(); |
||||||
|
getWindowVisibleDisplayFrame(displayFrame); |
||||||
|
|
||||||
|
int position = getSelectionStart(); |
||||||
|
Layout layout = getLayout(); |
||||||
|
int line = layout.getLineForOffset(position); |
||||||
|
|
||||||
|
float verticalDistanceInDp = (750 + 140 * line) / displayDensity; |
||||||
|
setDropDownVerticalOffset((int) verticalDistanceInDp); |
||||||
|
|
||||||
|
float horizontalDistanceInDp = layout.getPrimaryHorizontal(position) / displayDensity; |
||||||
|
setDropDownHorizontalOffset((int) horizontalDistanceInDp); |
||||||
|
super.showDropDown(); |
||||||
|
} |
||||||
|
|
||||||
|
private final Runnable mUpdateRunnable = new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
Editable source = getText(); |
||||||
|
highlightWithoutChange(source); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
private final TextWatcher mEditorTextWatcher = new TextWatcher() { |
||||||
|
|
||||||
|
private int start; |
||||||
|
private int count; |
||||||
|
|
||||||
|
@Override |
||||||
|
public void beforeTextChanged(CharSequence charSequence, int start, int before, int count) { |
||||||
|
this.start = start; |
||||||
|
this.count = count; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onTextChanged(CharSequence charSequence, int start, int before, int count) { |
||||||
|
if (!modified) return; |
||||||
|
|
||||||
|
if (highlightWhileTextChanging) { |
||||||
|
if (mSyntaxPatternMap.size() > 0) { |
||||||
|
convertTabs(getEditableText(), start, count); |
||||||
|
mUpdateHandler.postDelayed(mUpdateRunnable, mUpdateDelayTime); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (mRemoveErrorsWhenTextChanged) removeAllErrorLines(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void afterTextChanged(Editable editable) { |
||||||
|
if (!highlightWhileTextChanging) { |
||||||
|
if (!modified) return; |
||||||
|
|
||||||
|
cancelHighlighterRender(); |
||||||
|
|
||||||
|
if (mSyntaxPatternMap.size() > 0) { |
||||||
|
convertTabs(getEditableText(), start, count); |
||||||
|
mUpdateHandler.postDelayed(mUpdateRunnable, mUpdateDelayTime); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
private final class TabWidthSpan extends ReplacementSpan { |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getSize( |
||||||
|
@NonNull Paint paint, |
||||||
|
CharSequence text, |
||||||
|
int start, |
||||||
|
int end, |
||||||
|
Paint.FontMetricsInt fm) { |
||||||
|
return tabWidth; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void draw( |
||||||
|
@NonNull Canvas canvas, |
||||||
|
CharSequence text, |
||||||
|
int start, |
||||||
|
int end, |
||||||
|
float x, |
||||||
|
int top, |
||||||
|
int y, |
||||||
|
int bottom, |
||||||
|
@NonNull Paint paint) { |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
package io.legado.app.ui.widget.code; |
||||||
|
|
||||||
|
import android.widget.MultiAutoCompleteTextView; |
||||||
|
|
||||||
|
public class KeywordTokenizer implements MultiAutoCompleteTextView.Tokenizer { |
||||||
|
|
||||||
|
@Override |
||||||
|
public int findTokenStart(CharSequence charSequence, int cursor) { |
||||||
|
String sequenceStr = charSequence.toString(); |
||||||
|
sequenceStr = sequenceStr.substring(0, cursor); |
||||||
|
|
||||||
|
int spaceIndex = sequenceStr.lastIndexOf(" "); |
||||||
|
int lineIndex = sequenceStr.lastIndexOf("\n"); |
||||||
|
int bracketIndex = sequenceStr.lastIndexOf("("); |
||||||
|
|
||||||
|
int index = Math.max(0, Math.max(spaceIndex, Math.max(lineIndex, bracketIndex))); |
||||||
|
if (index == 0) return 0; |
||||||
|
return (index + 1 < charSequence.length()) ? index + 1 : index; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int findTokenEnd(CharSequence charSequence, int cursor) { |
||||||
|
return charSequence.length(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public CharSequence terminateToken(CharSequence charSequence) { |
||||||
|
return charSequence; |
||||||
|
} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue