6.7.0 - Alpha19 - 代码编辑器 "查找/替换" 支持状态持久化及实时显示搜索计数信息
This commit is contained in:
@@ -142,6 +142,7 @@
|
|||||||
"文件管理器/任务面板支持显示文件/任务数量统计信息",
|
"文件管理器/任务面板支持显示文件/任务数量统计信息",
|
||||||
"代码编辑器保存文件失败时自动存为草稿并支持另存为新文件",
|
"代码编辑器保存文件失败时自动存为草稿并支持另存为新文件",
|
||||||
"代码编辑器加载大文件时提升一定程度的流畅度",
|
"代码编辑器加载大文件时提升一定程度的流畅度",
|
||||||
|
"代码编辑器 \"查找/替换\" 支持状态持久化及实时显示搜索计数信息",
|
||||||
"打包应用页面默认勾选必要权限 (WAKE_LOCK/INTERNET/WRITE_EXTERNAL_STORAGE) _[`issue #397`](http://issues.autojs6.com/397)_",
|
"打包应用页面默认勾选必要权限 (WAKE_LOCK/INTERNET/WRITE_EXTERNAL_STORAGE) _[`issue #397`](http://issues.autojs6.com/397)_",
|
||||||
"打包应用设置页面增加前台服务开关 _[`issue #406`](http://issues.autojs6.com/406)_",
|
"打包应用设置页面增加前台服务开关 _[`issue #406`](http://issues.autojs6.com/406)_",
|
||||||
"脚本项目配置文件保存时增加键名冲突检测机制防止键名歧义",
|
"脚本项目配置文件保存时增加键名冲突检测机制防止键名歧义",
|
||||||
|
|||||||
@@ -442,8 +442,24 @@ public class EditorMenu {
|
|||||||
mEditor.getSelection()
|
mEditor.getSelection()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(s -> {
|
.subscribe(s -> {
|
||||||
FindOrReplaceDialogBuilder builder = new FindOrReplaceDialogBuilder(mContext, mEditorView)
|
// Prefer restoring last search (even after exit search mode).
|
||||||
.setQueryIfNotEmpty(s);
|
// zh-CN: 优先恢复 "上一次搜索" (即使已退出搜索模式), 避免 selection (命中内容) 覆盖关键词.
|
||||||
|
String preferredQuery = mEditorView.getPreferredSearchQueryForDialogOrNull();
|
||||||
|
boolean preferredRegex = mEditorView.getPreferredSearchUsingRegexForDialog();
|
||||||
|
|
||||||
|
FindOrReplaceDialogBuilder builder = new FindOrReplaceDialogBuilder(mContext, mEditorView);
|
||||||
|
|
||||||
|
// Only override regex checkbox when we are restoring an actual previous query.
|
||||||
|
// Otherwise keep the persisted checkbox state from SharedPreferences (restoreState()).
|
||||||
|
//
|
||||||
|
// zh-CN: 仅当我们确实在恢复 "上一次 query" 时才覆盖 regex 勾选状态;
|
||||||
|
// 否则使用 SharedPreferences 中持久化的勾选状态.
|
||||||
|
if (preferredQuery != null) {
|
||||||
|
builder.setUsingRegex(preferredRegex);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setQueryIfNotEmpty(preferredQuery != null ? preferredQuery : s);
|
||||||
|
|
||||||
builder.positiveColorRes(R.color.dialog_button_attraction);
|
builder.positiveColorRes(R.color.dialog_button_attraction);
|
||||||
builder.show();
|
builder.show();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ import java.util.concurrent.ConcurrentLinkedQueue
|
|||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Stardust on Sep 28, 2017.
|
* Created by Stardust on Sep 28, 2017.
|
||||||
@@ -159,7 +160,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
get() = AutoJs.instance.scriptEngineService.getScriptExecution(scriptExecutionId)
|
get() = AutoJs.instance.scriptEngineService.getScriptExecution(scriptExecutionId)
|
||||||
|
|
||||||
// Whether the editor is currently loading text (including streaming append).
|
// Whether the editor is currently loading text (including streaming append).
|
||||||
// zh-CN: 编辑器是否正在加载文本(包括流式追加阶段).
|
// zh-CN: 编辑器是否正在加载文本 (包括流式追加阶段).
|
||||||
@Volatile
|
@Volatile
|
||||||
private var mEditorLoading: Boolean = false
|
private var mEditorLoading: Boolean = false
|
||||||
|
|
||||||
@@ -169,7 +170,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
private var mLargeFileMode: Boolean = false
|
private var mLargeFileMode: Boolean = false
|
||||||
|
|
||||||
// Active large-file loading cancel flag.
|
// Active large-file loading cancel flag.
|
||||||
// zh-CN: 大文件加载的取消标记(当前会话).
|
// zh-CN: 大文件加载的取消标记 (当前会话).
|
||||||
private val mLargeFileCancel = AtomicBoolean(false)
|
private val mLargeFileCancel = AtomicBoolean(false)
|
||||||
|
|
||||||
// Active IO subscription disposable for large-file loading.
|
// Active IO subscription disposable for large-file loading.
|
||||||
@@ -178,11 +179,11 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
private var mLargeFileIoDisposable: Disposable? = null
|
private var mLargeFileIoDisposable: Disposable? = null
|
||||||
|
|
||||||
// Active streaming source stream reference (for forced close).
|
// Active streaming source stream reference (for forced close).
|
||||||
// zh-CN: 流式读取的底层流引用(用于强制 close 以打断阻塞 read).
|
// zh-CN: 流式读取的底层流引用 (用于强制 close 以打断阻塞 read).
|
||||||
private val mLargeFileStreamRef = AtomicReference<InputStream?>(null)
|
private val mLargeFileStreamRef = AtomicReference<InputStream?>(null)
|
||||||
|
|
||||||
// Active Choreographer callback for streaming append.
|
// Active Choreographer callback for streaming append.
|
||||||
// zh-CN: 流式追加的 Choreographer 回调引用(用于取消帧回调).
|
// zh-CN: 流式追加的 Choreographer 回调引用 (用于取消帧回调).
|
||||||
@Volatile
|
@Volatile
|
||||||
private var mLargeFileFrameCallback: Choreographer.FrameCallback? = null
|
private var mLargeFileFrameCallback: Choreographer.FrameCallback? = null
|
||||||
|
|
||||||
@@ -263,6 +264,36 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
// zh-CN: 复用的窗口可见区域 Rect.
|
// zh-CN: 复用的窗口可见区域 Rect.
|
||||||
private val mTmpWindowVisibleRect = Rect()
|
private val mTmpWindowVisibleRect = Rect()
|
||||||
|
|
||||||
|
// Search mode UI state.
|
||||||
|
// zh-CN: 搜索模式 UI 状态.
|
||||||
|
private var mInSearchMode = false
|
||||||
|
private var mToolbarTitleBeforeSearch: CharSequence? = null
|
||||||
|
private var mToolbarSubtitleBeforeSearch: CharSequence? = null
|
||||||
|
private var mCurrentSearchQuery: String? = null
|
||||||
|
private var mCurrentSearchUsingRegex: Boolean = false
|
||||||
|
private var mCurrentSearchShowReplaceItem: Boolean = false
|
||||||
|
|
||||||
|
// Remember last successful search even after exiting search mode.
|
||||||
|
// zh-CN: 即使退出搜索模式, 也保留上一次成功搜索的 query/regex.
|
||||||
|
private var mLastSearchQuery: String? = null
|
||||||
|
private var mLastSearchUsingRegex: Boolean = false
|
||||||
|
|
||||||
|
// Search stats (global count) state.
|
||||||
|
// zh-CN: 搜索统计 (全局匹配数) 状态.
|
||||||
|
private var mSearchStatsGeneration: Int = 0
|
||||||
|
private var mSearchStatsDisposable: Disposable? = null
|
||||||
|
private var mSearchStatsCurrentOrdinal: Int? = null
|
||||||
|
private var mSearchStatsTotal: Int? = null
|
||||||
|
private var mSearchStatsTotalCapped: Boolean = false
|
||||||
|
private var mSearchStatsCounting: Boolean = false
|
||||||
|
|
||||||
|
private val mSearchStatsDebounceMs = 300L
|
||||||
|
private val mSearchStatsMaxTotal = 1000
|
||||||
|
|
||||||
|
private val mSearchStatsDebounceRunnable = Runnable {
|
||||||
|
restartSearchStatsAsync(reason = "debounce")
|
||||||
|
}
|
||||||
|
|
||||||
constructor(context: Context) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
@@ -331,7 +362,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Loading is finished for intent-handling stage (actual file loading may continue).
|
// Loading is finished for intent-handling stage (actual file loading may continue).
|
||||||
// zh-CN: Intent 处理阶段结束(实际文件加载可能仍在继续).
|
// zh-CN: Intent 处理阶段结束 (实际文件加载可能仍在继续).
|
||||||
mEditorLoading = false
|
mEditorLoading = false
|
||||||
syncPrimaryMenuState()
|
syncPrimaryMenuState()
|
||||||
}
|
}
|
||||||
@@ -383,20 +414,18 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
|
|
||||||
// Compute the real overlap between editor and the visible window area.
|
// Compute the real overlap between editor and the visible window area.
|
||||||
// zh-CN: 计算 editor 与窗口可见区域之间的真实重叠量.
|
// zh-CN: 计算 editor 与窗口可见区域之间的真实重叠量.
|
||||||
val targetBottom = if (imeVisible) {
|
val activity = findHostActivityOrNull()
|
||||||
val activity = findHostActivityOrNull()
|
val targetBottom = if (activity != null) {
|
||||||
if (activity != null) {
|
activity.window.decorView.getWindowVisibleDisplayFrame(mTmpWindowVisibleRect)
|
||||||
activity.window.decorView.getWindowVisibleDisplayFrame(mTmpWindowVisibleRect)
|
|
||||||
|
|
||||||
val loc = IntArray(2)
|
val loc = IntArray(2)
|
||||||
editor.getLocationOnScreen(loc)
|
editor.getLocationOnScreen(loc)
|
||||||
val editorBottomOnScreen = loc[1] + editor.height
|
val editorBottomOnScreen = loc[1] + editor.height
|
||||||
|
|
||||||
(editorBottomOnScreen - mTmpWindowVisibleRect.bottom).coerceAtLeast(0)
|
(editorBottomOnScreen - mTmpWindowVisibleRect.bottom).coerceAtLeast(0)
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
} else 0
|
|
||||||
|
|
||||||
if (targetBottom != mLastImeBottomInset) {
|
if (targetBottom != mLastImeBottomInset) {
|
||||||
mLastImeBottomInset = targetBottom
|
mLastImeBottomInset = targetBottom
|
||||||
@@ -419,6 +448,12 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun requestApplyInsetsSoon() {
|
||||||
|
// Helps fixing timing issues when IME hides/shows during dialog dismiss/focus change.
|
||||||
|
// zh-CN: 用于修复对话框 dismiss/焦点切换期间 IME 显隐导致的时序问题.
|
||||||
|
post { ViewCompat.requestApplyInsets(this@EditorView) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun ensureCursorVisibleIfPossible() {
|
private fun ensureCursorVisibleIfPossible() {
|
||||||
// Bring the caret into visible area when keyboard shows, without "scrolling back" on hide.
|
// Bring the caret into visible area when keyboard shows, without "scrolling back" on hide.
|
||||||
// zh-CN: 键盘弹起时将光标区域滚动到可见范围内, 键盘收起时不回滚.
|
// zh-CN: 键盘弹起时将光标区域滚动到可见范围内, 键盘收起时不回滚.
|
||||||
@@ -551,14 +586,13 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
val sizeOrNull = runCatching { queryContentLengthOrNull(uri) }.getOrNull()
|
val sizeOrNull = runCatching { queryContentLengthOrNull(uri) }.getOrNull()
|
||||||
if (sizeOrNull != null && sizeOrNull >= streamThresholdBytes) {
|
if (sizeOrNull != null && sizeOrNull >= streamThresholdBytes) {
|
||||||
// Large file: show loading in enhance bar, allow viewing/scrolling (read-only).
|
// Large file: show loading in enhance bar, allow viewing/scrolling (read-only).
|
||||||
// zh-CN: 大文件: 在增强栏显示加载提示, 允许查看/滚动(只读).
|
// zh-CN: 大文件: 在增强栏显示加载提示, 允许查看/滚动 (只读).
|
||||||
beginEditorLoadingUi(context.getString(R.string.text_loading_with_dots))
|
beginEditorLoadingUi(context.getString(R.string.text_loading_with_dots))
|
||||||
return loadUriStreamed(uri)
|
return loadUriStreamed(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Observable
|
return Observable
|
||||||
.fromCallable {
|
.fromCallable {
|
||||||
// ... existing code ...
|
|
||||||
val resolver = context.contentResolver
|
val resolver = context.contentResolver
|
||||||
val rawBytes = resolver.openInputStream(uri)?.use { it.readBytes() } ?: ByteArray(0)
|
val rawBytes = resolver.openInputStream(uri)?.use { it.readBytes() } ?: ByteArray(0)
|
||||||
|
|
||||||
@@ -638,6 +672,21 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
(context as? HostActivity)?.backPressedObserver?.unregisterHandler(mFunctionsKeyboardHelper)
|
(context as? HostActivity)?.backPressedObserver?.unregisterHandler(mFunctionsKeyboardHelper)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expose current/last search session for reopening find/replace dialog.
|
||||||
|
// zh-CN: 对外暴露当前/上一次搜索会话信息, 用于再次打开查找/替换对话框时恢复状态.
|
||||||
|
fun getCurrentSearchQueryOrNull(): String? = mCurrentSearchQuery
|
||||||
|
fun getCurrentSearchUsingRegex(): Boolean = mCurrentSearchUsingRegex
|
||||||
|
fun isInSearchMode(): Boolean = mInSearchMode
|
||||||
|
|
||||||
|
fun getLastSearchQueryOrNull(): String? = mLastSearchQuery
|
||||||
|
fun getLastSearchUsingRegex(): Boolean = mLastSearchUsingRegex
|
||||||
|
|
||||||
|
fun getPreferredSearchQueryForDialogOrNull(): String? =
|
||||||
|
(mCurrentSearchQuery ?: mLastSearchQuery)
|
||||||
|
|
||||||
|
fun getPreferredSearchUsingRegexForDialog(): Boolean =
|
||||||
|
(if (mInSearchMode) mCurrentSearchUsingRegex else mLastSearchUsingRegex)
|
||||||
|
|
||||||
// Cancel current large-file loading as soon as possible.
|
// Cancel current large-file loading as soon as possible.
|
||||||
// zh-CN: 尽快取消当前大文件加载.
|
// zh-CN: 尽快取消当前大文件加载.
|
||||||
fun cancelLargeFileLoading() {
|
fun cancelLargeFileLoading() {
|
||||||
@@ -666,7 +715,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup UI state best-effort (do NOT re-enable editing in read-only editor).
|
// Cleanup UI state best-effort (do NOT re-enable editing in read-only editor).
|
||||||
// zh-CN: 尽力清理 UI 状态(只读编辑器不要被重新启用编辑).
|
// zh-CN: 尽力清理 UI 状态 (只读编辑器不要被重新启用编辑).
|
||||||
post {
|
post {
|
||||||
runCatching {
|
runCatching {
|
||||||
endEditorLoadingUi()
|
endEditorLoadingUi()
|
||||||
@@ -704,34 +753,34 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
val bis = BufferedInputStream(counted)
|
val bis = BufferedInputStream(counted)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
bis.use { input ->
|
bis.use { input ->
|
||||||
if (mLargeFileCancel.get() || emitter.isDisposed) {
|
if (mLargeFileCancel.get() || emitter.isDisposed) {
|
||||||
// Cancel early.
|
// Cancel early.
|
||||||
// zh-CN: 提前取消.
|
// zh-CN: 提前取消.
|
||||||
return@use
|
return@use
|
||||||
}
|
}
|
||||||
|
|
||||||
val sampleLimitBytes = 64 * 1024
|
val sampleLimitBytes = 64 * 1024
|
||||||
input.mark(sampleLimitBytes + 8)
|
input.mark(sampleLimitBytes + 8)
|
||||||
|
|
||||||
val sample = ByteArray(sampleLimitBytes)
|
val sample = ByteArray(sampleLimitBytes)
|
||||||
val sampleRead = input.read(sample).coerceAtLeast(0)
|
val sampleRead = input.read(sample).coerceAtLeast(0)
|
||||||
val sampleBytes = if (sampleRead == sample.size) sample else sample.copyOf(sampleRead)
|
val sampleBytes = if (sampleRead == sample.size) sample else sample.copyOf(sampleRead)
|
||||||
|
|
||||||
val detected = StringUtils.detectCharset(sampleBytes)
|
val detected = StringUtils.detectCharset(sampleBytes)
|
||||||
mCurrentCharsetConfidence = detected.confidence ?: 0
|
mCurrentCharsetConfidence = detected.confidence ?: 0
|
||||||
mCurrentCharset = detected.charsetOrDefault()
|
mCurrentCharset = detected.charsetOrDefault()
|
||||||
mHadBom = StringUtils.hasBom(sampleBytes, mCurrentCharset)
|
mHadBom = StringUtils.hasBom(sampleBytes, mCurrentCharset)
|
||||||
|
|
||||||
runCatching { input.reset() }.getOrElse {
|
runCatching { input.reset() }.getOrElse {
|
||||||
if (!emitter.isDisposed && !mLargeFileCancel.get()) {
|
if (!emitter.isDisposed && !mLargeFileCancel.get()) {
|
||||||
emitter.onError(IllegalStateException("Stream reset failed for $uri"))
|
emitter.onError(IllegalStateException("Stream reset failed for $uri"))
|
||||||
}
|
}
|
||||||
return@use
|
return@use
|
||||||
}
|
}
|
||||||
|
|
||||||
streamDecodeAndEmit(input, emitter)
|
streamDecodeAndEmit(input, emitter)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Clear reference when finished/failed.
|
// Clear reference when finished/failed.
|
||||||
// zh-CN: 结束/失败后清理引用.
|
// zh-CN: 结束/失败后清理引用.
|
||||||
@@ -776,8 +825,8 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
}, { e ->
|
}, { e ->
|
||||||
done.set(true)
|
done.set(true)
|
||||||
if (!mLargeFileCancel.get()) {
|
if (!mLargeFileCancel.get()) {
|
||||||
post { endEditorLoadingUi() }
|
post { endEditorLoadingUi() }
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
done.set(true)
|
done.set(true)
|
||||||
@@ -811,7 +860,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
val choreographer = Choreographer.getInstance()
|
val choreographer = Choreographer.getInstance()
|
||||||
|
|
||||||
// Per-frame time budget for appending text (favor throughput).
|
// Per-frame time budget for appending text (favor throughput).
|
||||||
// zh-CN: 每帧用于追加文本的时间预算(优先吞吐).
|
// zh-CN: 每帧用于追加文本的时间预算 (优先吞吐).
|
||||||
val appendBudgetMsIdle = 12L
|
val appendBudgetMsIdle = 12L
|
||||||
val appendBudgetMsTouching = 3L
|
val appendBudgetMsTouching = 3L
|
||||||
|
|
||||||
@@ -1033,7 +1082,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
// editor.setProgress(true)
|
// editor.setProgress(true)
|
||||||
|
|
||||||
// Frame time budget in ms (tune for devices).
|
// Frame time budget in ms (tune for devices).
|
||||||
// zh-CN: 单帧时间预算(毫秒), 可按设备调参.
|
// zh-CN: 单帧时间预算 (毫秒), 可按设备调参.
|
||||||
val frameBudgetMs = 7L
|
val frameBudgetMs = 7L
|
||||||
|
|
||||||
// Adaptive chunk size to balance layout cost and responsiveness.
|
// Adaptive chunk size to balance layout cost and responsiveness.
|
||||||
@@ -1177,8 +1226,31 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
}
|
}
|
||||||
syncPrimaryMenuState()
|
syncPrimaryMenuState()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
editText.addTextChangedListener(SimpleTextWatcher { _ ->
|
||||||
|
// Live refresh search stats in search mode (debounced).
|
||||||
|
// Behavior:
|
||||||
|
// - Immediately show ".../..." to indicate recounting.
|
||||||
|
// - Debounce the actual background scan.
|
||||||
|
//
|
||||||
|
// zh-CN: 搜索模式下实时刷新统计 (带防抖).
|
||||||
|
// - 立刻把 subtitle 变成 ".../..." 表示重新统计中.
|
||||||
|
// - 后台扫描做 debounce, 避免频繁扫全文.
|
||||||
|
if (!mInSearchMode) return@SimpleTextWatcher
|
||||||
|
if (editText.isLoadingText() || mEditorLoading) return@SimpleTextWatcher
|
||||||
|
|
||||||
|
mSearchStatsCounting = true
|
||||||
|
mSearchStatsCurrentOrdinal = null
|
||||||
|
mSearchStatsTotal = null
|
||||||
|
mSearchStatsTotalCapped = false
|
||||||
|
updateSearchSubtitleBestEffort()
|
||||||
|
|
||||||
|
scheduleSearchStatsRefreshDebounced()
|
||||||
|
})
|
||||||
|
|
||||||
editText.textSize = getEditorTextSize(pxToSp(editText.textSize).toInt()).toFloat()
|
editText.textSize = getEditorTextSize(pxToSp(editText.textSize).toInt()).toFloat()
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.addCursorChangeCallback(object : CodeEditor.CursorChangeCallback {
|
editor.addCursorChangeCallback(object : CodeEditor.CursorChangeCallback {
|
||||||
override fun onCursorChange(line: String, cursor: Int) {
|
override fun onCursorChange(line: String, cursor: Int) {
|
||||||
autoComplete(line, cursor)
|
autoComplete(line, cursor)
|
||||||
@@ -1243,13 +1315,47 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
R.id.save -> saveFile()
|
R.id.save -> saveFile()
|
||||||
R.id.undo -> undo()
|
R.id.undo -> undo()
|
||||||
R.id.redo -> redo()
|
R.id.redo -> redo()
|
||||||
R.id.replace -> replace()
|
|
||||||
R.id.find_next -> findNext()
|
R.id.find_next -> {
|
||||||
R.id.find_prev -> findPrev()
|
// IMPORTANT: base "next" on current caret, not previous foundIndex.
|
||||||
R.id.cancel_search -> cancelSearch()
|
// zh-CN: 重要: "下一个" 必须以当前光标为起点, 而不是以上次 foundIndex 为起点.
|
||||||
|
editor.resetFoundIndexForFindNextFromCursor()
|
||||||
|
findNext()
|
||||||
|
onSearchStateMaybeChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.find_prev -> {
|
||||||
|
// IMPORTANT: base "prev" on current caret, not previous foundIndex.
|
||||||
|
// zh-CN: 重要: "上一个" 必须以当前光标为起点, 而不是以上次 foundIndex 为起点.
|
||||||
|
editor.resetFoundIndexForFindPrevFromCursor()
|
||||||
|
findPrev()
|
||||||
|
onSearchStateMaybeChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.cancel_search -> {
|
||||||
|
cancelSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.replace -> {
|
||||||
|
replace()
|
||||||
|
onSearchStateMaybeChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onSearchStateMaybeChanged() {
|
||||||
|
// After next/prev/replace, selection likely changed -> current ordinal changes.
|
||||||
|
// zh-CN: next/prev/replace 后 selection 可能变化 -> current 序号需要刷新.
|
||||||
|
if (!mInSearchMode) return
|
||||||
|
updateSearchSubtitleBestEffort()
|
||||||
|
scheduleSearchStatsRefreshDebounced()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleSearchStatsRefreshDebounced() {
|
||||||
|
mUiHandler.removeCallbacks(mSearchStatsDebounceRunnable)
|
||||||
|
mUiHandler.postDelayed(mSearchStatsDebounceRunnable, mSearchStatsDebounceMs)
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("CheckResult")
|
@SuppressLint("CheckResult")
|
||||||
fun runAndSaveFileIfNeeded() {
|
fun runAndSaveFileIfNeeded() {
|
||||||
saveToTmpFile()
|
saveToTmpFile()
|
||||||
@@ -1677,8 +1783,6 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
|
|
||||||
private fun findPrev() = editor.findPrev()
|
private fun findPrev() = editor.findPrev()
|
||||||
|
|
||||||
private fun cancelSearch() = showNormalToolbar()
|
|
||||||
|
|
||||||
private fun showNormalToolbar() {
|
private fun showNormalToolbar() {
|
||||||
activity.supportFragmentManager.beginTransaction()
|
activity.supportFragmentManager.beginTransaction()
|
||||||
.replace(R.id.toolbar_menu, mNormalToolbar)
|
.replace(R.id.toolbar_menu, mNormalToolbar)
|
||||||
@@ -1762,10 +1866,319 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getHostToolbarOrNull(): androidx.appcompat.widget.Toolbar? {
|
||||||
|
// The activity toolbar id is "toolbar".
|
||||||
|
// zh-CN: Activity Toolbar 的 id 为 toolbar.
|
||||||
|
return activity.findViewById(R.id.toolbar)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enterSearchMode(query: String, usingRegex: Boolean, showReplaceItem: Boolean) {
|
||||||
|
if (!mInSearchMode) {
|
||||||
|
val tb = getHostToolbarOrNull()
|
||||||
|
mToolbarTitleBeforeSearch = tb?.title
|
||||||
|
mToolbarSubtitleBeforeSearch = tb?.subtitle
|
||||||
|
}
|
||||||
|
|
||||||
|
mInSearchMode = true
|
||||||
|
mCurrentSearchQuery = query
|
||||||
|
mCurrentSearchUsingRegex = usingRegex
|
||||||
|
mCurrentSearchShowReplaceItem = showReplaceItem
|
||||||
|
|
||||||
|
// Persist last successful search so reopening dialog restores it even after exit.
|
||||||
|
// zh-CN: 记录最近一次成功搜索, 使退出后再次打开对话框仍可恢复.
|
||||||
|
mLastSearchQuery = query
|
||||||
|
mLastSearchUsingRegex = usingRegex
|
||||||
|
|
||||||
|
// Reset stats UI to "counting".
|
||||||
|
// zh-CN: 重置统计 UI 为 "统计中".
|
||||||
|
mSearchStatsCurrentOrdinal = null
|
||||||
|
mSearchStatsTotal = null
|
||||||
|
mSearchStatsTotalCapped = false
|
||||||
|
mSearchStatsCounting = true
|
||||||
|
|
||||||
|
val tb = getHostToolbarOrNull()
|
||||||
|
tb?.title = query
|
||||||
|
tb?.subtitle = null
|
||||||
|
|
||||||
|
// Start stats immediately.
|
||||||
|
// zh-CN: 立即启动统计.
|
||||||
|
restartSearchStatsAsync(reason = "enter")
|
||||||
|
requestApplyInsetsSoon()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun exitSearchModeAndRestoreToolbar() {
|
||||||
|
if (!mInSearchMode) return
|
||||||
|
|
||||||
|
mInSearchMode = false
|
||||||
|
mCurrentSearchQuery = null
|
||||||
|
mCurrentSearchUsingRegex = false
|
||||||
|
mCurrentSearchShowReplaceItem = false
|
||||||
|
|
||||||
|
cancelSearchStats()
|
||||||
|
|
||||||
|
val tb = getHostToolbarOrNull()
|
||||||
|
tb?.title = mToolbarTitleBeforeSearch
|
||||||
|
tb?.subtitle = mToolbarSubtitleBeforeSearch
|
||||||
|
|
||||||
|
requestApplyInsetsSoon()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelSearchStats() {
|
||||||
|
mSearchStatsGeneration++
|
||||||
|
mUiHandler.removeCallbacks(mSearchStatsDebounceRunnable)
|
||||||
|
mSearchStatsDisposable?.let { d -> runCatching { d.dispose() } }
|
||||||
|
mSearchStatsDisposable = null
|
||||||
|
mSearchStatsCounting = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSearchSubtitleText(): CharSequence? {
|
||||||
|
if (!mInSearchMode) return null
|
||||||
|
|
||||||
|
val current = mSearchStatsCurrentOrdinal
|
||||||
|
val total = mSearchStatsTotal
|
||||||
|
val counting = mSearchStatsCounting
|
||||||
|
|
||||||
|
fun formatTotal(): String {
|
||||||
|
if (total == null) return "..."
|
||||||
|
return if (mSearchStatsTotalCapped) "${mSearchStatsMaxTotal - 1}+" else total.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
val totalText = when {
|
||||||
|
total != null -> formatTotal()
|
||||||
|
counting -> "..."
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentText = when {
|
||||||
|
current != null -> current.toString()
|
||||||
|
counting -> "..."
|
||||||
|
total != null -> "..." // <-- key: total known but current unknown => show ".../total"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
return when {
|
||||||
|
currentText != null && totalText != null -> "$currentText/$totalText"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSearchSubtitleBestEffort() {
|
||||||
|
val tb = getHostToolbarOrNull() ?: return
|
||||||
|
if (!mInSearchMode) return
|
||||||
|
tb.subtitle = buildSearchSubtitleText()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNoSearchResultDialog(query: String) {
|
||||||
|
// Show a clear no-result prompt.
|
||||||
|
// zh-CN: 显示明确的无结果提示.
|
||||||
|
DialogUtils.buildAndShowAdaptive {
|
||||||
|
MaterialDialog.Builder(context)
|
||||||
|
.title(R.string.text_prompt)
|
||||||
|
.content(context.getString(R.string.text_no_search_results_for_keyword, query))
|
||||||
|
.positiveText(R.string.dialog_button_dismiss)
|
||||||
|
.positiveColorRes(R.color.dialog_button_default)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restartSearchStatsAsync(reason: String) {
|
||||||
|
if (!mInSearchMode) return
|
||||||
|
|
||||||
|
val query = mCurrentSearchQuery
|
||||||
|
if (query.isNullOrEmpty()) return
|
||||||
|
|
||||||
|
// Cancel previous task.
|
||||||
|
// zh-CN: 取消上一轮任务.
|
||||||
|
cancelSearchStats()
|
||||||
|
|
||||||
|
mSearchStatsCounting = true
|
||||||
|
mSearchStatsCurrentOrdinal = null
|
||||||
|
mSearchStatsTotal = null
|
||||||
|
mSearchStatsTotalCapped = false
|
||||||
|
|
||||||
|
updateSearchSubtitleBestEffort()
|
||||||
|
|
||||||
|
val gen = ++mSearchStatsGeneration
|
||||||
|
|
||||||
|
val usingRegex = mCurrentSearchUsingRegex
|
||||||
|
val selStart = editor.codeEditText.selectionStart
|
||||||
|
val selEnd = editor.codeEditText.selectionEnd
|
||||||
|
val textSnapshot = editor.text
|
||||||
|
|
||||||
|
mSearchStatsDisposable = Observable
|
||||||
|
.fromCallable {
|
||||||
|
computeSearchStats(
|
||||||
|
text = textSnapshot,
|
||||||
|
query = query,
|
||||||
|
usingRegex = usingRegex,
|
||||||
|
selectionStart = minOf(selStart, selEnd),
|
||||||
|
selectionEnd = maxOf(selStart, selEnd),
|
||||||
|
maxTotal = mSearchStatsMaxTotal,
|
||||||
|
generation = gen,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.subscribeOn(Schedulers.computation())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({ result ->
|
||||||
|
// Ignore stale results.
|
||||||
|
// zh-CN: 忽略过期结果.
|
||||||
|
if (gen != mSearchStatsGeneration) return@subscribe
|
||||||
|
if (!mInSearchMode) return@subscribe
|
||||||
|
|
||||||
|
mSearchStatsCounting = false
|
||||||
|
mSearchStatsCurrentOrdinal = result.currentOrdinal
|
||||||
|
mSearchStatsTotal = result.total
|
||||||
|
mSearchStatsTotalCapped = result.capped
|
||||||
|
|
||||||
|
updateSearchSubtitleBestEffort()
|
||||||
|
}, { e ->
|
||||||
|
if (gen != mSearchStatsGeneration) return@subscribe
|
||||||
|
mSearchStatsCounting = false
|
||||||
|
e.printStackTrace()
|
||||||
|
updateSearchSubtitleBestEffort()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class SearchStatsResult(
|
||||||
|
val currentOrdinal: Int?,
|
||||||
|
val total: Int,
|
||||||
|
val capped: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun computeSearchStats(
|
||||||
|
text: String,
|
||||||
|
query: String,
|
||||||
|
usingRegex: Boolean,
|
||||||
|
selectionStart: Int,
|
||||||
|
selectionEnd: Int,
|
||||||
|
maxTotal: Int,
|
||||||
|
generation: Int,
|
||||||
|
): SearchStatsResult {
|
||||||
|
// Note:
|
||||||
|
// - Non-regex: count non-overlapping occurrences.
|
||||||
|
// - Regex: count matcher.find() occurrences.
|
||||||
|
// - currentOrdinal: match whose range equals current selection range.
|
||||||
|
// zh-CN:
|
||||||
|
// - 非正则: 统计不重叠匹配次数.
|
||||||
|
// - 正则: 统计 matcher.find() 次数.
|
||||||
|
// - currentOrdinal: 匹配区间与当前 selection 区间相等时记录序号.
|
||||||
|
if (query.isEmpty()) return SearchStatsResult(null, 0, false)
|
||||||
|
|
||||||
|
fun cancelled(): Boolean = generation != mSearchStatsGeneration
|
||||||
|
|
||||||
|
var total = 0
|
||||||
|
var capped = false
|
||||||
|
var currentOrdinal: Int? = null
|
||||||
|
|
||||||
|
if (!usingRegex) {
|
||||||
|
var from = 0
|
||||||
|
while (from <= text.length) {
|
||||||
|
if (cancelled()) break
|
||||||
|
|
||||||
|
val idx = text.indexOf(query, from)
|
||||||
|
if (idx < 0) break
|
||||||
|
|
||||||
|
total++
|
||||||
|
val end = idx + query.length
|
||||||
|
if (idx == selectionStart && end == selectionEnd) {
|
||||||
|
currentOrdinal = total
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total >= maxTotal) {
|
||||||
|
capped = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-overlapping.
|
||||||
|
// zh-CN: 不重叠推进.
|
||||||
|
from = end
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val p = Pattern.compile(query)
|
||||||
|
val m = p.matcher(text)
|
||||||
|
while (m.find()) {
|
||||||
|
if (cancelled()) break
|
||||||
|
|
||||||
|
total++
|
||||||
|
val start = m.start()
|
||||||
|
val end = m.end()
|
||||||
|
if (start == selectionStart && end == selectionEnd) {
|
||||||
|
currentOrdinal = total
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total >= maxTotal) {
|
||||||
|
capped = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SearchStatsResult(
|
||||||
|
currentOrdinal = currentOrdinal,
|
||||||
|
total = total,
|
||||||
|
capped = capped,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to start "Find" and enter search mode. Returns true if any result is found.
|
||||||
|
// zh-CN: 尝试开始 "查找" 并进入搜索模式. 若找到任意结果则返回 true.
|
||||||
|
fun tryFind(keywords: String, usingRegex: Boolean): Boolean {
|
||||||
|
editor.find(keywords, usingRegex)
|
||||||
|
|
||||||
|
val hasResult = editor.getFoundIndex() >= 0
|
||||||
|
if (!hasResult) {
|
||||||
|
showNoSearchResultDialog(keywords)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
enterSearchMode(query = keywords, usingRegex = usingRegex, showReplaceItem = false)
|
||||||
|
showSearchToolbar(showReplaceItem = false)
|
||||||
|
updateSearchSubtitleBestEffort()
|
||||||
|
|
||||||
|
// Fix possible IME/insets timing issues after dialog confirm and IME hide.
|
||||||
|
// zh-CN: 修复对话框确认导致 IME 收起后的 insets 时序问题.
|
||||||
|
requestApplyInsetsSoon()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to start "Replace (single)" and enter search mode. Returns true if any result is found.
|
||||||
|
// zh-CN: 尝试开始 "替换 (单次)" 并进入搜索模式. 若找到任意结果则返回 true.
|
||||||
|
fun tryReplace(keywords: String, replacement: String, usingRegex: Boolean): Boolean {
|
||||||
|
editor.replace(keywords, replacement, usingRegex)
|
||||||
|
|
||||||
|
val hasResult = editor.getFoundIndex() >= 0
|
||||||
|
if (!hasResult) {
|
||||||
|
showNoSearchResultDialog(keywords)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
enterSearchMode(query = keywords, usingRegex = usingRegex, showReplaceItem = true)
|
||||||
|
showSearchToolbar(showReplaceItem = true)
|
||||||
|
updateSearchSubtitleBestEffort()
|
||||||
|
|
||||||
|
// Fix possible IME/insets timing issues after dialog confirm and IME hide.
|
||||||
|
// zh-CN: 修复对话框确认导致 IME 收起后的 insets 时序问题.
|
||||||
|
requestApplyInsetsSoon()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelSearch() {
|
||||||
|
exitSearchModeAndRestoreToolbar()
|
||||||
|
showNormalToolbar()
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(CheckedPatternSyntaxException::class)
|
@Throws(CheckedPatternSyntaxException::class)
|
||||||
fun find(keywords: String, usingRegex: Boolean) {
|
fun find(keywords: String, usingRegex: Boolean) {
|
||||||
editor.find(keywords, usingRegex)
|
// Keep old API for callers that don't care about result.
|
||||||
showSearchToolbar(false)
|
// zh-CN: 保留旧 API, 兼容不关心结果的调用方.
|
||||||
|
tryFind(keywords, usingRegex)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(CheckedPatternSyntaxException::class)
|
||||||
|
fun replace(keywords: String, replacement: String, usingRegex: Boolean) {
|
||||||
|
// Keep old API for callers that don't care about result.
|
||||||
|
// zh-CN: 保留旧 API, 兼容不关心结果的调用方.
|
||||||
|
tryReplace(keywords, replacement, usingRegex)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showSearchToolbar(showReplaceItem: Boolean) {
|
private fun showSearchToolbar(showReplaceItem: Boolean) {
|
||||||
@@ -1778,12 +2191,6 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
.commit()
|
.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(CheckedPatternSyntaxException::class)
|
|
||||||
fun replace(keywords: String, replacement: String, usingRegex: Boolean) {
|
|
||||||
editor.replace(keywords, replacement, usingRegex)
|
|
||||||
showSearchToolbar(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(CheckedPatternSyntaxException::class)
|
@Throws(CheckedPatternSyntaxException::class)
|
||||||
fun replaceAll(keywords: String, replacement: String, usingRegex: Boolean) {
|
fun replaceAll(keywords: String, replacement: String, usingRegex: Boolean) {
|
||||||
editor.replaceAll(keywords, replacement, usingRegex)
|
editor.replaceAll(keywords, replacement, usingRegex)
|
||||||
@@ -1936,6 +2343,10 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
|
|||||||
// zh-CN: 销毁前先取消加载, 再销毁编辑器相关资源.
|
// zh-CN: 销毁前先取消加载, 再销毁编辑器相关资源.
|
||||||
cancelLargeFileLoading()
|
cancelLargeFileLoading()
|
||||||
|
|
||||||
|
// Cancel search stats.
|
||||||
|
// zh-CN: 取消搜索统计任务.
|
||||||
|
cancelSearchStats()
|
||||||
|
|
||||||
editor.destroy()
|
editor.destroy()
|
||||||
mAutoCompletion?.shutdown()
|
mAutoCompletion?.shutdown()
|
||||||
if (mDebugging) {
|
if (mDebugging) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.text.TextWatcher
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.CheckBox
|
import android.widget.CheckBox
|
||||||
import androidx.appcompat.widget.AppCompatEditText
|
import androidx.appcompat.widget.AppCompatEditText
|
||||||
|
import androidx.core.content.edit
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import org.autojs.autojs.ui.edit.editor.CodeEditor.CheckedPatternSyntaxException
|
import org.autojs.autojs.ui.edit.editor.CodeEditor.CheckedPatternSyntaxException
|
||||||
@@ -17,6 +18,7 @@ import org.autojs.autojs6.databinding.DialogFindOrReplaceBinding
|
|||||||
* Created by Stardust on Sep 28, 2017.
|
* Created by Stardust on Sep 28, 2017.
|
||||||
* Modified by SuperMonster003 as of Dec 1, 2021.
|
* Modified by SuperMonster003 as of Dec 1, 2021.
|
||||||
* Transformed by SuperMonster003 on May 12, 2023.
|
* Transformed by SuperMonster003 on May 12, 2023.
|
||||||
|
* Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 8, 2026.
|
||||||
*/
|
*/
|
||||||
class FindOrReplaceDialogBuilder(context: Context, private val mEditorView: EditorView) : MaterialDialog.Builder(context) {
|
class FindOrReplaceDialogBuilder(context: Context, private val mEditorView: EditorView) : MaterialDialog.Builder(context) {
|
||||||
|
|
||||||
@@ -41,7 +43,9 @@ class FindOrReplaceDialogBuilder(context: Context, private val mEditorView: Edit
|
|||||||
val binding = DialogFindOrReplaceBinding.inflate(LayoutInflater.from(context))
|
val binding = DialogFindOrReplaceBinding.inflate(LayoutInflater.from(context))
|
||||||
|
|
||||||
mRegexCheckBox = binding.checkboxRegex
|
mRegexCheckBox = binding.checkboxRegex
|
||||||
mReplaceCheckBox = binding.checkboxReplace
|
mReplaceCheckBox = binding.checkboxReplace.apply {
|
||||||
|
setOnCheckedChangeListener { _, _ -> syncWithReplaceAllCheckBox() }
|
||||||
|
}
|
||||||
mKeywordsEditText = binding.keywords
|
mKeywordsEditText = binding.keywords
|
||||||
mReplaceAllCheckBox = binding.checkboxReplaceAll.apply {
|
mReplaceAllCheckBox = binding.checkboxReplaceAll.apply {
|
||||||
setOnCheckedChangeListener { _, _ -> syncWithReplaceCheckBox() }
|
setOnCheckedChangeListener { _, _ -> syncWithReplaceCheckBox() }
|
||||||
@@ -67,9 +71,10 @@ class FindOrReplaceDialogBuilder(context: Context, private val mEditorView: Edit
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun storeState() {
|
private fun storeState() {
|
||||||
PreferenceManager.getDefaultSharedPreferences(getContext()).edit()
|
PreferenceManager.getDefaultSharedPreferences(getContext()).edit {
|
||||||
.putString(KEY_KEYWORDS, mKeywordsEditText.text.toString())
|
putString(KEY_KEYWORDS, mKeywordsEditText.text.toString())
|
||||||
.apply()
|
putBoolean(KEY_USING_REGEX, mRegexCheckBox.isChecked)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreState() {
|
private fun restoreState() {
|
||||||
@@ -77,6 +82,14 @@ class FindOrReplaceDialogBuilder(context: Context, private val mEditorView: Edit
|
|||||||
.getString(KEY_KEYWORDS, "")
|
.getString(KEY_KEYWORDS, "")
|
||||||
.let { mKeywordsEditText.setText(it) }
|
.let { mKeywordsEditText.setText(it) }
|
||||||
|
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||||
|
.getBoolean(KEY_USING_REGEX, false)
|
||||||
|
.let { mRegexCheckBox.isChecked = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUsingRegex(usingRegex: Boolean): FindOrReplaceDialogBuilder {
|
||||||
|
mRegexCheckBox.isChecked = usingRegex
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun syncWithReplaceCheckBox() {
|
private fun syncWithReplaceCheckBox() {
|
||||||
@@ -85,6 +98,12 @@ class FindOrReplaceDialogBuilder(context: Context, private val mEditorView: Edit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun syncWithReplaceAllCheckBox() {
|
||||||
|
if (!mReplaceCheckBox.isChecked && mReplaceAllCheckBox.isChecked) {
|
||||||
|
mReplaceAllCheckBox.isChecked = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun findOrReplace(dialog: MaterialDialog) {
|
private fun findOrReplace(dialog: MaterialDialog) {
|
||||||
val keywords = mKeywordsEditText.text.toString()
|
val keywords = mKeywordsEditText.text.toString()
|
||||||
if (keywords.isEmpty()) {
|
if (keywords.isEmpty()) {
|
||||||
@@ -93,16 +112,22 @@ class FindOrReplaceDialogBuilder(context: Context, private val mEditorView: Edit
|
|||||||
try {
|
try {
|
||||||
val usingRegex = mRegexCheckBox.isChecked
|
val usingRegex = mRegexCheckBox.isChecked
|
||||||
if (!mReplaceCheckBox.isChecked) {
|
if (!mReplaceCheckBox.isChecked) {
|
||||||
mEditorView.find(keywords, usingRegex)
|
val ok = mEditorView.tryFind(keywords, usingRegex)
|
||||||
|
if (ok) {
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
val replacement = mReplacementEditText.text.toString()
|
val replacement = mReplacementEditText.text.toString()
|
||||||
if (mReplaceAllCheckBox.isChecked) {
|
if (mReplaceAllCheckBox.isChecked) {
|
||||||
mEditorView.replaceAll(keywords, replacement, usingRegex)
|
mEditorView.replaceAll(keywords, replacement, usingRegex)
|
||||||
|
dialog.dismiss()
|
||||||
} else {
|
} else {
|
||||||
mEditorView.replace(keywords, replacement, usingRegex)
|
val ok = mEditorView.tryReplace(keywords, replacement, usingRegex)
|
||||||
|
if (ok) {
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dialog.dismiss()
|
|
||||||
} catch (e: CheckedPatternSyntaxException) {
|
} catch (e: CheckedPatternSyntaxException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
mKeywordsEditText.error = getContext().getString(R.string.error_pattern_syntax)
|
mKeywordsEditText.error = getContext().getString(R.string.error_pattern_syntax)
|
||||||
@@ -117,6 +142,7 @@ class FindOrReplaceDialogBuilder(context: Context, private val mEditorView: Edit
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val KEY_KEYWORDS = "..."
|
private const val KEY_KEYWORDS = "..."
|
||||||
|
private const val KEY_USING_REGEX = "find_or_replace_using_regex"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -483,6 +483,55 @@ class CodeEditor : HVScrollView {
|
|||||||
|
|
||||||
fun redo() = mTextViewRedoUndo.redo()
|
fun redo() = mTextViewRedoUndo.redo()
|
||||||
|
|
||||||
|
// Expose current found index for UI logic (e.g., no-result prompt).
|
||||||
|
// zh-CN: 暴露当前命中位置, 供 UI 逻辑使用 (例如无结果提示).
|
||||||
|
fun getFoundIndex(): Int = mFoundIndex
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset search cursor for "Find Next" based on current caret/selection.
|
||||||
|
*
|
||||||
|
* Behavior:
|
||||||
|
* - Uses current selection start (min of start/end).
|
||||||
|
* - Sets mFoundIndex to (cursor - 1), so findNext() starts at cursor.
|
||||||
|
*
|
||||||
|
* zh-CN:
|
||||||
|
* 将 "查找下一个" 的起点对齐到当前光标/选择起点.
|
||||||
|
* - 使用 selectionStart/End 的较小值作为光标位置
|
||||||
|
* - mFoundIndex 设为 cursor-1, 使 findNext() 从 cursor 开始找
|
||||||
|
*/
|
||||||
|
fun resetFoundIndexForFindNextFromCursor() {
|
||||||
|
val textLen = codeEditText.text?.length ?: 0
|
||||||
|
|
||||||
|
// Use selection END as the starting anchor for "next",
|
||||||
|
// so when current match is selected we won't re-select it again.
|
||||||
|
//
|
||||||
|
// zh-CN: "下一个" 以 selectionEnd 作为起点锚点,
|
||||||
|
// 这样当当前命中被选中时不会再次选中同一条.
|
||||||
|
val cursor = maxOf(codeEditText.selectionStart, codeEditText.selectionEnd).coerceIn(0, textLen)
|
||||||
|
|
||||||
|
// Make findNext() start from `cursor`.
|
||||||
|
// zh-CN: 让 findNext() 从 cursor 开始找.
|
||||||
|
mFoundIndex = (cursor - 1).coerceAtLeast(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset search cursor for "Find Prev" based on current caret/selection.
|
||||||
|
*
|
||||||
|
* Behavior:
|
||||||
|
* - Uses current selection start (min of start/end).
|
||||||
|
* - Sets mFoundIndex to cursor, so findPrev() searches before cursor.
|
||||||
|
*
|
||||||
|
* zh-CN:
|
||||||
|
* 将 "查找上一个" 的起点对齐到当前光标/选择起点.
|
||||||
|
* - 使用 selectionStart/End 的较小值作为光标位置
|
||||||
|
* - mFoundIndex 设为 cursor, 使 findPrev() 从 cursor 往前找
|
||||||
|
*/
|
||||||
|
fun resetFoundIndexForFindPrevFromCursor() {
|
||||||
|
val textLen = codeEditText.text?.length ?: 0
|
||||||
|
val cursor = minOf(codeEditText.selectionStart, codeEditText.selectionEnd).coerceIn(0, textLen)
|
||||||
|
mFoundIndex = cursor
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(CheckedPatternSyntaxException::class)
|
@Throws(CheckedPatternSyntaxException::class)
|
||||||
fun find(keywords: String, usingRegex: Boolean) {
|
fun find(keywords: String, usingRegex: Boolean) {
|
||||||
if (usingRegex) {
|
if (usingRegex) {
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ public class HVScrollView extends FrameLayout {
|
|||||||
//
|
//
|
||||||
// When the child is not measured/layouted yet (width/height == 0), the original
|
// When the child is not measured/layouted yet (width/height == 0), the original
|
||||||
// bounds check may reject all ACTION_DOWN events, causing scrolling to be stuck.
|
// bounds check may reject all ACTION_DOWN events, causing scrolling to be stuck.
|
||||||
// zh-CN: 当子 View 尚未完成测量/布局时(width/height == 0), 原边界判断可能拒绝所有 ACTION_DOWN,
|
// zh-CN: 当子 View 尚未完成测量/布局时 (width/height == 0), 原边界判断可能拒绝所有 ACTION_DOWN,
|
||||||
// zh-CN: 从而导致滚动完全失效.
|
// zh-CN: 从而导致滚动完全失效.
|
||||||
final int childWidth = child.getWidth();
|
final int childWidth = child.getWidth();
|
||||||
final int childHeight = child.getHeight();
|
final int childHeight = child.getHeight();
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
android:id="@+id/checkbox_regex"
|
android:id="@+id/checkbox_regex"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
android:text="@string/text_regex" />
|
android:text="@string/text_regex" />
|
||||||
|
|
||||||
<org.autojs.autojs.theme.widget.ThemeColorTextInputLayout
|
<org.autojs.autojs.theme.widget.ThemeColorTextInputLayout
|
||||||
@@ -44,7 +45,8 @@
|
|||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="4dp">
|
||||||
|
|
||||||
<org.autojs.autojs.theme.widget.ThemeColorCheckBox
|
<org.autojs.autojs.theme.widget.ThemeColorCheckBox
|
||||||
android:id="@+id/checkbox_replace"
|
android:id="@+id/checkbox_replace"
|
||||||
|
|||||||
@@ -1287,4 +1287,5 @@
|
|||||||
<string name="text_write_system_settings">修改系统设置</string>
|
<string name="text_write_system_settings">修改系统设置</string>
|
||||||
<string name="text_xiaomi_background_popup_permission">后台弹出界面</string>
|
<string name="text_xiaomi_background_popup_permission">后台弹出界面</string>
|
||||||
<string name="text_loading_completed">加载完毕</string>
|
<string name="text_loading_completed">加载完毕</string>
|
||||||
|
<string name="text_no_search_results_for_keyword">未找到匹配项:\n\n%1$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1562,4 +1562,5 @@
|
|||||||
<string name="text_write_system_settings">Write system settings</string>
|
<string name="text_write_system_settings">Write system settings</string>
|
||||||
<string name="text_xiaomi_background_popup_permission">Display pop-up windows while running in the background</string>
|
<string name="text_xiaomi_background_popup_permission">Display pop-up windows while running in the background</string>
|
||||||
<string name="text_loading_completed">Loading completed</string>
|
<string name="text_loading_completed">Loading completed</string>
|
||||||
|
<string name="text_no_search_results_for_keyword">No results found for:\n\n%1$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#Sun Feb 08 15:05:14 CST 2026
|
#Sun Feb 08 19:50:56 CST 2026
|
||||||
BUILD_TIME=1770534314495
|
BUILD_TIME=1770551456669
|
||||||
COMPILE_SDK_VERSION=36
|
COMPILE_SDK_VERSION=36
|
||||||
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
||||||
IMAGE_QUANT_NDK_VERSION=26.1.10909125
|
IMAGE_QUANT_NDK_VERSION=26.1.10909125
|
||||||
@@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13
|
|||||||
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
|
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
|
||||||
TARGET_SDK_VERSION=36
|
TARGET_SDK_VERSION=36
|
||||||
TARGET_SDK_VERSION_INRT=29
|
TARGET_SDK_VERSION_INRT=29
|
||||||
VERSION_BUILD=3714
|
VERSION_BUILD=3715
|
||||||
VERSION_NAME=6.7.0 Alpha19
|
VERSION_NAME=6.7.0 Alpha19
|
||||||
VSCODE_EXT_REQUIRED_VERSION=1.0.13
|
VSCODE_EXT_REQUIRED_VERSION=1.0.13
|
||||||
|
|||||||
Reference in New Issue
Block a user