diff --git a/app/build.gradle b/app/build.gradle index 8904cc6ef..447260280 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -203,7 +203,7 @@ dependencies { implementation("org.nanohttpd:nanohttpd-websocket:$nanoHttpdVersion") //代码编辑 - implementation('com.github.AmrDeveloper:CodeView:1.0.1') + //implementation('com.github.AmrDeveloper:CodeView:1.0.1') //二维码 implementation('com.github.jenly1314:zxing-lite:2.1.1') diff --git a/app/src/main/java/io/legado/app/ui/widget/code/CodeView.java b/app/src/main/java/io/legado/app/ui/widget/code/CodeView.java new file mode 100644 index 000000000..ee0da2ce3 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/code/CodeView.java @@ -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 mErrorHashSet = new TreeMap<>(); + private final Map mSyntaxPatternMap = new HashMap<>(); + private List 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 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 characterList) { + mIndentCharacterList = characterList; + } + + public void clearAutoIndentCharacterList() { + mIndentCharacterList.clear(); + } + + public List 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) { + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/code/KeywordTokenizer.java b/app/src/main/java/io/legado/app/ui/widget/code/KeywordTokenizer.java new file mode 100644 index 000000000..938e2e124 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/code/KeywordTokenizer.java @@ -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; + } + +} diff --git a/app/src/main/java/io/legado/app/utils/CodeViewExtensions.kt b/app/src/main/java/io/legado/app/utils/CodeViewExtensions.kt index 7078eaab9..fa258be09 100644 --- a/app/src/main/java/io/legado/app/utils/CodeViewExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/CodeViewExtensions.kt @@ -2,8 +2,9 @@ package io.legado.app.utils -import com.amrdeveloper.codeview.CodeView + import io.legado.app.lib.theme.accentColor +import io.legado.app.ui.widget.code.CodeView import splitties.init.appCtx import java.util.regex.Pattern diff --git a/app/src/main/res/layout/dialog_code_view.xml b/app/src/main/res/layout/dialog_code_view.xml index bee860958..baffc4129 100644 --- a/app/src/main/res/layout/dialog_code_view.xml +++ b/app/src/main/res/layout/dialog_code_view.xml @@ -4,7 +4,7 @@ android:layout_height="match_parent" android:background="@color/background"> - - + android:layout_height="wrap_content" + android:inputType="textMultiLine" /> \ No newline at end of file