pull/1395/head
gedoor 3 years ago
parent 2bc94c0f7b
commit 3f5ded2c4e
  1. 629
      app/src/main/java/io/legado/app/ui/widget/code/CodeView.kt
  2. 44
      app/src/main/java/io/legado/app/ui/widget/code/KeywordTokenizer.kt

@ -1,449 +1,422 @@
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() {
package io.legado.app.ui.widget.code
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Paint.FontMetricsInt
import android.graphics.Rect
import android.os.Handler
import android.os.Looper
import android.text.*
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.appcompat.widget.AppCompatMultiAutoCompleteTextView
import java.util.*
import java.util.regex.Matcher
import java.util.regex.Pattern
import kotlin.math.roundToInt
@Suppress("unused")
class CodeView : AppCompatMultiAutoCompleteTextView {
private var tabWidth = 0
private var tabWidthInCharacters = 0
private var mUpdateDelayTime = 500
private var modified = true
private var highlightWhileTextChanging = true
private var hasErrors = false
private var mRemoveErrorsWhenTextChanged = true
private val mUpdateHandler = Handler(Looper.getMainLooper())
private var mAutoCompleteTokenizer: Tokenizer? = null
private val displayDensity = resources.displayMetrics.density
private val mErrorHashSet: SortedMap<Int, Int> = TreeMap()
private val mSyntaxPatternMap: MutableMap<Pattern, Int> = HashMap()
private var mIndentCharacterList = mutableListOf('{', '+', '-', '*', '/', '=')
constructor(context: Context?) : super(context!!) {
initEditorView()
}
constructor(context: Context?, attrs: AttributeSet?) : super(
context!!, attrs
) {
initEditorView()
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context!!, attrs, defStyleAttr
) {
initEditorView()
}
private fun initEditorView() {
if (mAutoCompleteTokenizer == null) {
mAutoCompleteTokenizer = new KeywordTokenizer();
mAutoCompleteTokenizer = 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);
setTokenizer(mAutoCompleteTokenizer)
filters = arrayOf(
InputFilter { source, start, end, dest, dStart, dEnd ->
if (modified && end - start == 1 && start < source.length && dStart < dest.length) {
val c = source[start]
if (c == '\n') {
return autoIndent(source, dest, dstart, dend);
return@InputFilter autoIndent(source, dest, dStart, dEnd)
}
}
return source;
source
}
)
addTextChangedListener(mEditorTextWatcher)
}
});
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;
private fun autoIndent(
source: CharSequence,
dest: Spanned,
dStart: Int,
dEnd: Int
): CharSequence {
var indent = ""
var iStart = dStart - 1
var dataBefore = false
var pt = 0
while (iStart > -1) {
val c = dest[iStart]
if (c == '\n') break
if (c != ' ' && c != '\t') {
if (!dataBefore) {
if (mIndentCharacterList.contains(c)) --pt;
dataBefore = true;
if (mIndentCharacterList.contains(c)) --pt
dataBefore = true
}
if (c == '(') {
--pt;
--pt
} else if (c == ')') {
++pt;
++pt
}
}
--iStart
}
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 (iStart > -1) {
val charAtCursor = dest[dStart]
var iEnd: Int = ++iStart
while (iEnd < dEnd) {
val c = dest[iEnd]
if (charAtCursor != '\n' && c == '/' && iEnd + 1 < dEnd && dest[iEnd] == c) {
iEnd += 2
break
}
if (c != ' ' && c != '\t') {
break;
break
}
++iEnd
}
indent += dest.subSequence(istart, iend);
indent += dest.subSequence(iStart, iEnd)
}
if (pt < 0) {
indent += "\t";
indent += "\t"
}
return source + indent;
return source.toString() + 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 fun highlightSyntax(editable: Editable) {
if (mSyntaxPatternMap.isEmpty()) return
for (pattern in mSyntaxPatternMap.keys) {
val color = mSyntaxPatternMap[pattern]!!
val m = pattern.matcher(editable)
while (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);
private fun highlightErrorLines(editable: Editable) {
if (mErrorHashSet.isEmpty()) return
val maxErrorLineValue = mErrorHashSet.lastKey()
var lineNumber = 0
val matcher = PATTERN_LINE.matcher(editable)
while (matcher.find()) {
if (mErrorHashSet.containsKey(lineNumber)) {
int color = mErrorHashSet.get(lineNumber);
createBackgroundColorSpan(editable, matcher, color);
val color = mErrorHashSet[lineNumber]!!
createBackgroundColorSpan(editable, matcher, color)
}
lineNumber = lineNumber + 1;
if (lineNumber > maxErrorLineValue) break;
lineNumber += 1
if (lineNumber > maxErrorLineValue) break
}
}
private void createForegroundColorSpan(Editable editable, Matcher matcher, @ColorInt int color) {
editable.setSpan(new ForegroundColorSpan(color),
private fun createForegroundColorSpan(
editable: Editable,
matcher: Matcher,
@ColorInt color: Int
) {
editable.setSpan(
ForegroundColorSpan(color),
matcher.start(), matcher.end(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
private void createBackgroundColorSpan(Editable editable, Matcher matcher, @ColorInt int color) {
editable.setSpan(new BackgroundColorSpan(color),
private fun createBackgroundColorSpan(
editable: Editable,
matcher: Matcher,
@ColorInt color: Int
) {
editable.setSpan(
BackgroundColorSpan(color),
matcher.start(), matcher.end(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
private Editable highlight(Editable editable) {
if (editable.length() == 0) return editable;
private fun highlight(editable: Editable): Editable {
if (editable.isEmpty()) return editable
try {
clearSpans(editable);
highlightErrorLines(editable);
highlightSyntax(editable);
} catch (IllegalStateException e) {
e.printStackTrace();
clearSpans(editable)
highlightErrorLines(editable)
highlightSyntax(editable)
} catch (e: IllegalStateException) {
e.printStackTrace()
}
return editable;
return editable
}
private void highlightWithoutChange(Editable editable) {
modified = false;
highlight(editable);
modified = true;
private fun 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;
fun setTextHighlighted(text: CharSequence?) {
if (text.isNullOrEmpty()) return
cancelHighlighterRender()
removeAllErrorLines()
modified = false
setText(highlight(SpannableStringBuilder(text)))
modified = true
}
public void setTabWidth(int characters) {
if (tabWidthInCharacters == characters) return;
tabWidthInCharacters = characters;
tabWidth = Math.round(getPaint().measureText("m") * characters);
fun setTabWidth(characters: Int) {
if (tabWidthInCharacters == characters) return
tabWidthInCharacters = characters
tabWidth = (paint.measureText("m") * characters).roundToInt()
}
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]);
private fun clearSpans(editable: Editable) {
val length = editable.length
val foregroundSpans = editable.getSpans(
0, length, ForegroundColorSpan::class.java
)
run {
var i = foregroundSpans.size
while (i-- > 0) {
editable.removeSpan(foregroundSpans[i])
}
}
val backgroundSpans = editable.getSpans(
0, length, BackgroundColorSpan::class.java
)
var i = backgroundSpans.size
while (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();
fun cancelHighlighterRender() {
mUpdateHandler.removeCallbacks(mUpdateRunnable)
}
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);
private fun convertTabs(editable: Editable, start: Int, count: Int) {
var startIndex = start
if (tabWidth < 1) return
val s = editable.toString()
val stop = startIndex + count
while (s.indexOf("\t", startIndex).also { startIndex = it } > -1 && startIndex < stop) {
editable.setSpan(
TabWidthSpan(),
startIndex,
startIndex + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
++startIndex
}
}
public void setSyntaxPatternsMap(Map<Pattern, Integer> syntaxPatterns) {
if (!mSyntaxPatternMap.isEmpty()) mSyntaxPatternMap.clear();
mSyntaxPatternMap.putAll(syntaxPatterns);
fun setSyntaxPatternsMap(syntaxPatterns: Map<Pattern, Int>?) {
if (mSyntaxPatternMap.isNotEmpty()) mSyntaxPatternMap.clear()
mSyntaxPatternMap.putAll(syntaxPatterns!!)
}
public void addSyntaxPattern(Pattern pattern, @ColorInt int Color) {
mSyntaxPatternMap.put(pattern, Color);
fun addSyntaxPattern(pattern: Pattern, @ColorInt Color: Int) {
mSyntaxPatternMap[pattern] = Color
}
public void removeSyntaxPattern(Pattern pattern) {
mSyntaxPatternMap.remove(pattern);
fun removeSyntaxPattern(pattern: Pattern) {
mSyntaxPatternMap.remove(pattern)
}
public int getSyntaxPatternsSize() {
return mSyntaxPatternMap.size();
fun getSyntaxPatternsSize(): Int {
return mSyntaxPatternMap.size
}
public void resetSyntaxPatternList() {
mSyntaxPatternMap.clear();
fun resetSyntaxPatternList() {
mSyntaxPatternMap.clear()
}
public void setAutoIndentCharacterList(List<Character> characterList) {
mIndentCharacterList = characterList;
fun setAutoIndentCharacterList(characterList: MutableList<Char>) {
mIndentCharacterList = characterList
}
public void clearAutoIndentCharacterList() {
mIndentCharacterList.clear();
fun clearAutoIndentCharacterList() {
mIndentCharacterList.clear()
}
public List<Character> getAutoIndentCharacterList() {
return mIndentCharacterList;
fun getAutoIndentCharacterList(): List<Char> {
return mIndentCharacterList
}
public void addErrorLine(int lineNum, int color) {
mErrorHashSet.put(lineNum, color);
hasErrors = true;
fun addErrorLine(lineNum: Int, color: Int) {
mErrorHashSet[lineNum] = color
hasErrors = true
}
public void removeErrorLine(int lineNum) {
mErrorHashSet.remove(lineNum);
hasErrors = mErrorHashSet.size() > 0;
fun removeErrorLine(lineNum: Int) {
mErrorHashSet.remove(lineNum)
hasErrors = mErrorHashSet.size > 0
}
public void removeAllErrorLines() {
mErrorHashSet.clear();
hasErrors = false;
fun removeAllErrorLines() {
mErrorHashSet.clear()
hasErrors = false
}
public int getErrorsSize() {
return mErrorHashSet.size();
fun getErrorsSize(): Int {
return mErrorHashSet.size
}
public String getTextWithoutTrailingSpace() {
fun getTextWithoutTrailingSpace(): String {
return PATTERN_TRAILING_WHITE_SPACE
.matcher(getText())
.replaceAll("");
.matcher(text)
.replaceAll("")
}
public void setAutoCompleteTokenizer(Tokenizer tokenizer) {
mAutoCompleteTokenizer = tokenizer;
fun setAutoCompleteTokenizer(tokenizer: Tokenizer?) {
mAutoCompleteTokenizer = tokenizer
}
public void setRemoveErrorsWhenTextChanged(boolean removeErrors) {
mRemoveErrorsWhenTextChanged = removeErrors;
fun setRemoveErrorsWhenTextChanged(removeErrors: Boolean) {
mRemoveErrorsWhenTextChanged = removeErrors
}
public void reHighlightSyntax() {
highlightSyntax(getEditableText());
fun reHighlightSyntax() {
highlightSyntax(editableText)
}
public void reHighlightErrors() {
highlightErrorLines(getEditableText());
fun reHighlightErrors() {
highlightErrorLines(editableText)
}
public boolean isHasError() {
return hasErrors;
fun isHasError(): Boolean {
return hasErrors
}
public void setUpdateDelayTime(int time) {
mUpdateDelayTime = time;
fun setUpdateDelayTime(time: Int) {
mUpdateDelayTime = time
}
public int getUpdateDelayTime() {
return mUpdateDelayTime;
fun getUpdateDelayTime(): Int {
return mUpdateDelayTime
}
public void setHighlightWhileTextChanging(boolean updateWhileTextChanging) {
this.highlightWhileTextChanging = updateWhileTextChanging;
fun setHighlightWhileTextChanging(updateWhileTextChanging: Boolean) {
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();
override fun showDropDown() {
val screenPoint = IntArray(2)
getLocationOnScreen(screenPoint)
val displayFrame = Rect()
getWindowVisibleDisplayFrame(displayFrame)
val position = selectionStart
val layout = layout
val line = layout.getLineForOffset(position)
val verticalDistanceInDp = (750 + 140 * line) / displayDensity
dropDownVerticalOffset = verticalDistanceInDp.toInt()
val horizontalDistanceInDp = layout.getPrimaryHorizontal(position) / displayDensity
dropDownHorizontalOffset = horizontalDistanceInDp.toInt()
super.showDropDown()
}
private final Runnable mUpdateRunnable = new Runnable() {
@Override
public void run() {
Editable source = getText();
highlightWithoutChange(source);
private val mUpdateRunnable = Runnable {
val source = text
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;
private val mEditorTextWatcher: TextWatcher = object : TextWatcher {
private var start = 0
private var count = 0
override fun beforeTextChanged(
charSequence: CharSequence,
start: Int,
before: Int,
count: Int
) {
this.start = start
this.count = count
}
@Override
public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
if (!modified) return;
override fun onTextChanged(
charSequence: CharSequence,
start: Int,
before: Int,
count: Int
) {
if (!modified) return
if (highlightWhileTextChanging) {
if (mSyntaxPatternMap.size() > 0) {
convertTabs(getEditableText(), start, count);
mUpdateHandler.postDelayed(mUpdateRunnable, mUpdateDelayTime);
if (mSyntaxPatternMap.isNotEmpty()) {
convertTabs(editableText, start, count)
mUpdateHandler.postDelayed(mUpdateRunnable, mUpdateDelayTime.toLong())
}
}
if (mRemoveErrorsWhenTextChanged) removeAllErrorLines();
if (mRemoveErrorsWhenTextChanged) removeAllErrorLines()
}
@Override
public void afterTextChanged(Editable editable) {
override fun afterTextChanged(editable: Editable) {
if (!highlightWhileTextChanging) {
if (!modified) return;
cancelHighlighterRender();
if (mSyntaxPatternMap.size() > 0) {
convertTabs(getEditableText(), start, count);
mUpdateHandler.postDelayed(mUpdateRunnable, mUpdateDelayTime);
if (!modified) return
cancelHighlighterRender()
if (mSyntaxPatternMap.isNotEmpty()) {
convertTabs(editableText, start, count)
mUpdateHandler.postDelayed(mUpdateRunnable, mUpdateDelayTime.toLong())
}
}
}
}
};
private final class TabWidthSpan extends ReplacementSpan {
@Override
public int getSize(
@NonNull Paint paint,
CharSequence text,
int start,
int end,
Paint.FontMetricsInt fm) {
return tabWidth;
private inner class TabWidthSpan : ReplacementSpan() {
override fun getSize(
paint: Paint,
text: CharSequence,
start: Int,
end: Int,
fm: FontMetricsInt?
): Int {
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) {
override fun draw(
canvas: Canvas,
text: CharSequence,
start: Int,
end: Int,
x: Float,
top: Int,
y: Int,
bottom: Int,
paint: Paint
) {
}
}
companion object {
private val PATTERN_LINE = Pattern.compile("(^.+$)+", Pattern.MULTILINE)
private val PATTERN_TRAILING_WHITE_SPACE = Pattern.compile("[\\t ]+$", Pattern.MULTILINE)
}
}

@ -1,31 +1,25 @@
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;
package io.legado.app.ui.widget.code
import android.widget.MultiAutoCompleteTextView
import kotlin.math.max
class KeywordTokenizer : MultiAutoCompleteTextView.Tokenizer {
override fun findTokenStart(charSequence: CharSequence, cursor: Int): Int {
var sequenceStr = charSequence.toString()
sequenceStr = sequenceStr.substring(0, cursor)
val spaceIndex = sequenceStr.lastIndexOf(" ")
val lineIndex = sequenceStr.lastIndexOf("\n")
val bracketIndex = sequenceStr.lastIndexOf("(")
val index = max(0, max(spaceIndex, max(lineIndex, bracketIndex)))
if (index == 0) return 0
return if (index + 1 < charSequence.length) index + 1 else index
}
@Override
public int findTokenEnd(CharSequence charSequence, int cursor) {
return charSequence.length();
override fun findTokenEnd(charSequence: CharSequence, cursor: Int): Int {
return charSequence.length
}
@Override
public CharSequence terminateToken(CharSequence charSequence) {
return charSequence;
override fun terminateToken(charSequence: CharSequence): CharSequence {
return charSequence
}
}
Loading…
Cancel
Save