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