diff --git a/src/main/java/org/billthefarmer/editor/Editor.java b/src/main/java/org/billthefarmer/editor/Editor.java index 77f5fee..0e5676d 100644 --- a/src/main/java/org/billthefarmer/editor/Editor.java +++ b/src/main/java/org/billthefarmer/editor/Editor.java @@ -44,6 +44,7 @@ import android.print.PrintAttributes; import android.print.PrintDocumentAdapter; import android.print.PrintManager; +import android.support.v4.content.FileProvider; import android.text.Editable; import android.text.Html; import android.text.InputType; @@ -67,7 +68,6 @@ import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; -import android.view.ViewTreeObserver; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.webkit.WebView; @@ -81,12 +81,10 @@ import android.widget.SeekBar; import android.widget.TextView; -import android.support.v4.content.FileProvider; - import com.ibm.icu.text.CharsetDetector; import com.ibm.icu.text.CharsetMatch; -import org.commonmark.node.*; +import org.commonmark.node.Node; import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; @@ -96,30 +94,21 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.FileReader; import java.io.FileWriter; -import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; -import java.io.Reader; - import java.lang.ref.WeakReference; - import java.nio.charset.Charset; - import java.text.DateFormat; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; - import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -146,6 +135,7 @@ public class Editor extends Activity public final static String PREF_THEME = "pref_theme"; public final static String PREF_TYPE = "pref_type"; public final static String PREF_WRAP = "pref_wrap"; + public final static String PREF_LINE_NUMBERS = "pref_line_numbers"; public final static String DOCUMENTS = "Documents"; public final static String FOLDER = "Folder"; @@ -428,6 +418,7 @@ public class Editor extends Activity private MenuItem searchItem; private SearchView searchView; private ScrollView scrollView; + private LineNumbersTextView lineNumbersView; private Runnable updateHighlight; private Runnable updateWordCount; @@ -445,6 +436,7 @@ public class Editor extends Activity private boolean view = false; private boolean wrap = false; + private boolean lineNumbers = false; private boolean suggest = true; private boolean changed = false; @@ -456,6 +448,8 @@ public class Editor extends Activity private int type = MONO; private int syntax; + private long highlightRefreshTime = 0; // Syntax highlighting refresh time on scroll changed + private long lineNumbersRefreshTime = 0; // Line numbers refresh time on scroll changed // onCreate @Override @@ -474,6 +468,7 @@ protected void onCreate(Bundle savedInstanceState) view = preferences.getBoolean(PREF_VIEW, true); last = preferences.getBoolean(PREF_LAST, false); wrap = preferences.getBoolean(PREF_WRAP, false); + lineNumbers = preferences.getBoolean(PREF_LINE_NUMBERS, false); suggest = preferences.getBoolean(PREF_SUGGEST, true); highlight = preferences.getBoolean(PREF_HIGH, false); @@ -561,6 +556,10 @@ else if (!suggest) setSizeAndTypeface(size, type); + lineNumbersView = findViewById(R.id.lineNumbersView); + lineNumbersView.setEditText(textView); + lineNumbersView.setLineNumbersEnabled(lineNumbers); + Intent intent = getIntent(); Uri uri = intent.getData(); @@ -755,8 +754,15 @@ public void onTextChanged(CharSequence s, // onScrollChange scrollView.getViewTreeObserver().addOnScrollChangedListener(() -> { - if (updateHighlight != null) - { + final long time = System.currentTimeMillis(); + + if (lineNumbers && time - lineNumbersRefreshTime > 125) { + lineNumbersRefreshTime = time; + lineNumbersView.forceRefresh(); + } + + if (updateHighlight != null && time - highlightRefreshTime > 125) { + highlightRefreshTime = time; textView.removeCallbacks(updateHighlight); textView.postDelayed(updateHighlight, UPDATE_DELAY); } @@ -826,6 +832,7 @@ public void onPause() editor.putBoolean(PREF_VIEW, view); editor.putBoolean(PREF_LAST, last); editor.putBoolean(PREF_WRAP, wrap); + editor.putBoolean(PREF_LINE_NUMBERS, lineNumbers); editor.putBoolean(PREF_SUGGEST, suggest); editor.putBoolean(PREF_HIGH, highlight); @@ -907,6 +914,7 @@ public boolean onPrepareOptionsMenu(Menu menu) menu.findItem(R.id.openLast).setChecked(last); menu.findItem(R.id.autoSave).setChecked(save); menu.findItem(R.id.wrap).setChecked(wrap); + menu.findItem(R.id.lineNumbers).setChecked(lineNumbers); menu.findItem(R.id.suggest).setChecked(suggest); menu.findItem(R.id.highlight).setChecked(highlight); @@ -1080,6 +1088,9 @@ public boolean onOptionsItemSelected(MenuItem item) case R.id.wrap: wrapClicked(item); break; + case R.id.lineNumbers: + lineNumbersClicked(item); + break; case R.id.suggest: suggestClicked(item); break; @@ -1766,14 +1777,17 @@ public void onProgressChanged(SeekBar seekBar, { if (fromUser) listener.onProgressChanged(seekBar, progress); + + if (lineNumbers) { + lineNumbersView.forceRefresh(); + } } @Override public void onStartTrackingTouch (SeekBar seekBar) {} @Override - public void onStopTrackingTouch (SeekBar seekBar) - { + public void onStopTrackingTouch (SeekBar seekBar) { dialog.dismiss(); } }); @@ -1815,8 +1829,7 @@ public void onPageFinished(WebView view, String url) } }); - String htmlDocument = - HTML_HEAD + Html.toHtml(textView.getText()) + HTML_TAIL; + String htmlDocument = HTML_HEAD + Html.toHtml(textView.getText()) + HTML_TAIL; webView.loadData(htmlDocument, TEXT_HTML, UTF_8); } @@ -1894,6 +1907,14 @@ private void wrapClicked(MenuItem item) recreate(this); } + // lineNumbersClicked + private void lineNumbersClicked(MenuItem item) + { + lineNumbers = !lineNumbers; + item.setChecked(lineNumbers); + lineNumbersView.setLineNumbersEnabled(lineNumbers); + } + // suggestClicked private void suggestClicked(MenuItem item) { @@ -3562,7 +3583,7 @@ protected List doInBackground(String... params) File entry = new File(path); entries.add(entry); } - + // Check the entries for (File file : entries) { diff --git a/src/main/java/org/billthefarmer/editor/LineNumbersTextView.java b/src/main/java/org/billthefarmer/editor/LineNumbersTextView.java new file mode 100644 index 0000000..d1f7c05 --- /dev/null +++ b/src/main/java/org/billthefarmer/editor/LineNumbersTextView.java @@ -0,0 +1,293 @@ +package org.billthefarmer.editor; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.text.Editable; +import android.text.Layout; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.widget.EditText; +import android.widget.TextView; + +/** + * @author Li Guanglin + */ +public class LineNumbersTextView extends TextView { + private boolean lineNumbersEnabled; + private LineNumbersDrawer lineNumbersDrawer; + + public LineNumbersTextView(Context context) { + super(context); + } + + public LineNumbersTextView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LineNumbersTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (lineNumbersEnabled) { + lineNumbersDrawer.draw(canvas); + } + } + + public void forceRefresh() { + setText(""); // Use setText("") to activate LineNumbersTextView refresh + } + + public void setEditText(final EditText editText) { + lineNumbersDrawer = new LineNumbersDrawer(editText, this); + } + + public void setLineNumbersEnabled(final boolean enabled) { + lineNumbersEnabled = enabled; + if (lineNumbersEnabled) { + lineNumbersDrawer.prepare(); + } else { + lineNumbersDrawer.reset(); + } + } + + // public boolean isLineNumbersEnabled() { return lineNumbersEnabled; } + + static class LineNumbersDrawer { + public final EditText editText; + public final LineNumbersTextView textView; + + private final Paint paint = new Paint(); + + private static final int NUMBER_PADDING_LEFT = 1; + private static final int NUMBER_PADDING_RIGHT = 12; + + private final Rect visibleArea = new Rect(); + private final Rect lineNumbersArea = new Rect(); + + private int fenceX; + private int numberX; + private int maxNumber = 1; // To gauge the width of line numbers fence + private int maxNumberDigits; + private int lastMaxNumber; + private int lastLayoutLineCount; + private float lastTextSize; + + private final int[] startLine = {0, 1}; // {line index, actual line number} + + private final TextWatcher lineTrackingWatcher = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + maxNumber -= countLines(s, start, start + count); + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + maxNumber += countLines(s, start, start + count); + } + + @Override + public void afterTextChanged(Editable editable) { + if (isLayoutLineCountChanged() || isMaxNumberChanged()) { + textView.forceRefresh(); + } + } + }; + + public LineNumbersDrawer(final EditText editText, final LineNumbersTextView textView) { + this.editText = editText; + this.textView = textView; + paint.setColor(0xFF999999); + paint.setTextAlign(Paint.Align.RIGHT); + } + + private boolean isOutOfLineNumbersArea() { + final int margin = (int) (visibleArea.height() * 0.5f); + final int top = visibleArea.top - margin; + final int bottom = visibleArea.bottom + margin; + + if (top < lineNumbersArea.top || bottom > lineNumbersArea.bottom) { + // Set line numbers area + // height of line numbers area = (1.5 + 1 + 1.5) * height of visible area + lineNumbersArea.top = top - visibleArea.height(); + lineNumbersArea.bottom = bottom + visibleArea.height(); + return true; + } else { + return false; + } + } + + private boolean isTextSizeChanged() { + if (editText.getTextSize() == lastTextSize) { + return false; + } else { + lastTextSize = editText.getTextSize(); + paint.setTextSize(lastTextSize); + return true; + } + } + + private boolean isMaxNumberChanged() { + if (maxNumber == lastMaxNumber) { + return false; + } else { + lastMaxNumber = maxNumber; + return true; + } + } + + private boolean isMaxNumberDigitsChanged() { + int digits; + if (maxNumber < 10) { + digits = 1; + } else if (maxNumber < 100) { + digits = 2; + } else if (maxNumber < 1000) { + digits = 3; + } else if (maxNumber < 10000) { + digits = 4; + } else { + digits = 5; + } + + if (digits == maxNumberDigits) { + return false; + } + + maxNumberDigits = digits; + return true; + } + + private boolean isLayoutLineCountChanged() { + final Layout layout = editText.getLayout(); + if (layout == null) { + return true; + } + + final int lineCount = layout.getLineCount(); + if (lineCount == lastLayoutLineCount) { + return false; + } else { + lastLayoutLineCount = lineCount; + return true; + } + } + + private int countLines(final CharSequence s, int start, int end) { + int count = 0; + for (; start < end; start++) { + if (s.charAt(start) == '\n') { + count++; + } + } + return count; + } + + private void lineTracking(boolean enabled) { + editText.removeTextChangedListener(lineTrackingWatcher); + + if (enabled) { + maxNumber = 1; + final CharSequence text = editText.getText(); + if (text != null) { + maxNumber += countLines(text, 0, text.length()); + } + editText.addTextChangedListener(lineTrackingWatcher); + } + } + + /** + * Prepare for drawing line numbers. + */ + public void prepare() { + lineTracking(true); + textView.setVisibility(VISIBLE); + } + + /** + * Draw line numbers. + * + * @param canvas The canvas on which the line numbers will be drawn. + */ + public void draw(final Canvas canvas) { + if (!editText.getLocalVisibleRect(visibleArea)) { + return; + } + + final CharSequence text = editText.getText(); + final Layout layout = editText.getLayout(); + if (text == null || layout == null) { + return; + } + + // If text size or the max line number of digits changed, update related variables + if (isTextSizeChanged() || isMaxNumberDigitsChanged()) { + numberX = NUMBER_PADDING_LEFT + (int) paint.measureText(String.valueOf(maxNumber)); + fenceX = numberX + NUMBER_PADDING_RIGHT; + textView.setWidth(fenceX + 1); + } + + // If current visible area is out of current line numbers area, + // will recalculate the start line + boolean invalid = false; + if (isOutOfLineNumbersArea()) { + invalid = true; + startLine[0] = 0; + startLine[1] = 1; + } + + // Draw right border of the fence + canvas.drawLine(fenceX, lineNumbersArea.top, fenceX, lineNumbersArea.bottom, paint); + + // Draw line numbers + int i = startLine[0]; + int number = startLine[1]; + int y = layout.getLineBaseline(i); + final int count = layout.getLineCount(); + final int offsetY = editText.getPaddingTop(); + + if (y > lineNumbersArea.top) { + if (invalid) { + invalid = false; + startLine[0] = i; + startLine[1] = number; + } + canvas.drawText(String.valueOf(number), numberX, layout.getLineBaseline(i) + offsetY, paint); + } + i++; + number++; + + for (; i < count; i++) { + if (text.charAt(layout.getLineStart(i) - 1) == '\n') { + y = layout.getLineBaseline(i); + if (y > lineNumbersArea.top) { + if (invalid) { + invalid = false; + startLine[0] = i; + startLine[1] = number; + } + canvas.drawText(String.valueOf(number), numberX, y + offsetY, paint); + if (y > lineNumbersArea.bottom) { + break; + } + } + number++; + } + } + } + + /** + * Reset states related line numbers. + */ + public void reset() { + lineTracking(false); + maxNumberDigits = 0; + textView.setWidth(0); + textView.setVisibility(GONE); + } + } +} diff --git a/src/main/res/layout/edit.xml b/src/main/res/layout/edit.xml index 81b08d7..98d7fbf 100644 --- a/src/main/res/layout/edit.xml +++ b/src/main/res/layout/edit.xml @@ -1,6 +1,5 @@ - - - - + android:orientation="horizontal"> + + + + + + + + - + diff --git a/src/main/res/layout/wrap.xml b/src/main/res/layout/wrap.xml index 8205192..80bd57c 100644 --- a/src/main/res/layout/wrap.xml +++ b/src/main/res/layout/wrap.xml @@ -1,6 +1,5 @@ - - + + + + + + + diff --git a/src/main/res/menu/main.xml b/src/main/res/menu/main.xml index 656f01a..85aa6c2 100644 --- a/src/main/res/menu/main.xml +++ b/src/main/res/menu/main.xml @@ -105,6 +105,11 @@ android:checkable="true" android:showAsAction="never" android:title="@string/wrap" /> + - 编辑器 + 编辑器 - - 打开文件 - 名字 - 路径 - 创造 - 编辑 - 查看 - 打开文件… - 保存 - 存储空间 - 打开最近文本 - 清除列表 - 查找… - 全局搜索… - 保存为… - 检测 - 转到… - 打印… - 查看 Markdown… - 选项 - 查看文件 - 最后打开 - 自动保存 - 自动换行 - 建议 - 突出显示语法 - 主题 - - - 系统 - - 黑色的 - 复古 - 文本大小 - - - - 字体 - 等宽 - 变宽 - 无衬线 - 衬线 - 关于 + 新建文件 + 打开文件 + 名称 + 路径 + 创建 + 编辑 + 查看 + 打开 + 保存 + 存储 + 打开最近 + 清除列表 + 查找 + 查找全部 + 另存为 + 检测 + 转到 + 打印 + 查看 Markdown + 选项 + 查看文件 + 打开最后 + 自动保存 + 自动换行 + 行号 + 建议 + 语法高亮 + 主题 + 明亮 + 黑暗 + 系统 + 白色 + 黑色 + 复古 + 文本大小 + + + + 字体 + 等宽 + 比例 + 无衬线 + 衬线 + 关于 - %s\n\n - 编译时间 %s\n\n版权 \u00A9 2017 Bill Farmer\n\n中文(简体)译者Lu Chang (ludoux)\n\n开源协议GNU GPLv3 - + + %s\n\n + 编译时间 %s\n\n + Copyright \u00A9 2017 Bill Farmer\n\n + 简体中文译者\n + Lu Chang (ludoux)\n + Li Guanglin\n\n + 开源协议 GNU GPLv3 + - 太大了 %s - 选择一个文件 - 加载中… + 过大 %s + 选择 + 加载中 - - 你有未保存的更改。你想保存这些更改吗? - + + 有未保存的更改。是否保存更改? + - - 文件已被更改,你想重载它吗? - + + 文件已被更改,是否重新加载? + - - 文件已被更改,你想覆写它吗? + + 文件已被更改,是否覆盖写入? - OK - 重载 - 覆写 - 放弃更改 - 取消 + OK + 重载 + 覆写 + 丢弃 + 取消 diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 9d540e0..295ef51 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -30,6 +30,7 @@ Open last Auto save Word wrap + Line numbers Suggestions Highlight syntax Theme