6.7.0 - Alpha19 - 降低代码编辑器保存失败率; 优化代码编辑器保存按钮状态逻辑

This commit is contained in:
SuperMonster003
2026-02-12 20:02:56 +08:00
parent 5240084b00
commit 0dbf0f1670
17 changed files with 286 additions and 31 deletions

View File

@@ -145,6 +145,8 @@
"代码编辑器保存文件失败时自动存为草稿并支持另存为新文件", "代码编辑器保存文件失败时自动存为草稿并支持另存为新文件",
"代码编辑器加载大文件时提升一定程度的流畅度", "代码编辑器加载大文件时提升一定程度的流畅度",
"代码编辑器 \"查找/替换\" 支持状态持久化及实时显示搜索计数信息", "代码编辑器 \"查找/替换\" 支持状态持久化及实时显示搜索计数信息",
"代码编辑器提示保存时确保保存成功后再退出编辑器以降低保存失败率",
"代码编辑器保存按钮的状态更符合用户主观逻辑",
"打包应用页面默认勾选必要权限 (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)_",
"脚本项目配置文件保存时增加键名冲突检测机制防止键名歧义", "脚本项目配置文件保存时增加键名冲突检测机制防止键名歧义",

View File

@@ -13,12 +13,14 @@ import android.text.TextPaint
import android.util.Log import android.util.Log
import android.util.TypedValue import android.util.TypedValue
import android.view.ActionMode import android.view.ActionMode
import android.view.Gravity
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.TextView import android.widget.TextView
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.core.view.get import androidx.core.view.get
import androidx.core.view.size import androidx.core.view.size
import com.afollestad.materialdialogs.DialogAction
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
@@ -32,6 +34,7 @@ import org.autojs.autojs.pio.PFiles
import org.autojs.autojs.storage.file.TmpScriptFiles import org.autojs.autojs.storage.file.TmpScriptFiles
import org.autojs.autojs.theme.widget.ThemeColorToolbar import org.autojs.autojs.theme.widget.ThemeColorToolbar
import org.autojs.autojs.ui.BaseActivity import org.autojs.autojs.ui.BaseActivity
import org.autojs.autojs.ui.error.ErrorDialogActivity
import org.autojs.autojs.ui.main.MainActivity import org.autojs.autojs.ui.main.MainActivity
import org.autojs.autojs.ui.main.scripts.EditableFileInfoDialogManager import org.autojs.autojs.ui.main.scripts.EditableFileInfoDialogManager
import org.autojs.autojs.util.DialogUtils import org.autojs.autojs.util.DialogUtils
@@ -48,8 +51,8 @@ import java.io.IOException
/** /**
* Created by Stardust on Jan 29, 2017. * Created by Stardust on Jan 29, 2017.
* Modified by SuperMonster003 as of Jan 21, 2023. * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 12, 2026.
* Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 8, 2026. * Modified by SuperMonster003 as of Feb 12, 2026.
*/ */
open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyActivity { open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyActivity {
@@ -290,7 +293,7 @@ open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyAc
} }
override fun finish() { override fun finish() {
if (mEditorView.isTextChanged) { if (mEditorView.saveStickyDirty) {
showExitConfirmDialog() showExitConfirmDialog()
} else { } else {
finishAndRemoveFromRecents() finishAndRemoveFromRecents()
@@ -305,6 +308,7 @@ open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyAc
} }
} }
@SuppressLint("CheckResult")
private fun showExitConfirmDialog() { private fun showExitConfirmDialog() {
DialogUtils.buildAndShowAdaptive { DialogUtils.buildAndShowAdaptive {
MaterialDialog.Builder(this) MaterialDialog.Builder(this)
@@ -312,17 +316,60 @@ open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyAc
.content(R.string.edit_exit_without_save_warn) .content(R.string.edit_exit_without_save_warn)
.neutralText(R.string.dialog_button_back) .neutralText(R.string.dialog_button_back)
.negativeText(R.string.text_exit_directly) .negativeText(R.string.text_exit_directly)
.onNeutral { d, _ -> d.dismiss() }
.negativeColorRes(R.color.dialog_button_caution) .negativeColorRes(R.color.dialog_button_caution)
.onNegative { _, _ -> .onNegative { d, _ ->
runCatching { d.dismiss() }
finishAndRemoveFromRecents() finishAndRemoveFromRecents()
} }
.positiveText(R.string.text_save_and_exit) .positiveText(R.string.text_save_and_exit)
.positiveColorRes(R.color.dialog_button_warn) .positiveColorRes(R.color.dialog_button_warn)
.onPositive { _, _ -> .onPositive { d, _ ->
mEditorView.saveFile() // Save is async, exit only after success.
finishAndRemoveFromRecents() // zh-CN: 保存是异步的, 保存成功后再退出.
d.apply {
setCancelable(false)
getActionButton(DialogAction.NEUTRAL).apply {
isEnabled = false
setTextColor(getColor(R.color.dialog_button_unavailable))
}
getActionButton(DialogAction.NEGATIVE).apply {
isEnabled = false
setTextColor(getColor(R.color.dialog_button_unavailable))
}
getActionButton(DialogAction.POSITIVE).apply {
isEnabled = false
setTextColor(getColor(R.color.dialog_button_unavailable))
}
contentView?.postDelayed({
contentView?.text = getString(R.string.text_saving)
}, 300)
}
mEditorView
.save()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
runCatching { d.dismiss() }
finishAndRemoveFromRecents()
}, { e: Throwable ->
// Save failed, keep editor open.
// zh-CN: 保存失败, 保持编辑器不退出.
e.printStackTrace()
runCatching { d.dismiss() }
ErrorDialogActivity.showErrorDialog(this@EditActivity, R.string.error_failed_to_save, e.message)
})
} }
.autoDismiss(false)
.build() .build()
.apply {
contentView?.apply {
setLineSpacing(0f, 1.2f)
setLines(2)
minLines = 2
gravity = Gravity.CENTER_VERTICAL
}
}
} }
} }

View File

@@ -120,7 +120,7 @@ import java.util.regex.Pattern
* Created by Stardust on Sep 28, 2017. * Created by Stardust on Sep 28, 2017.
* Transformed by SuperMonster003 on May 1, 2023. * Transformed by SuperMonster003 on May 1, 2023.
* Modified by SuperMonster003 as of Feb 3, 2026. * Modified by SuperMonster003 as of Feb 3, 2026.
* Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 8, 2026. * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 12, 2026.
*/ */
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFragment.OnMenuItemClickListener { class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFragment.OnMenuItemClickListener {
@@ -135,6 +135,46 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
private var _name: String? = null private var _name: String? = null
// Sticky save dirty flag.
// Behavior:
// - Set to true on ANY text change (including undo/redo).
// - Reset to false only after successful save or when a new baseline text is loaded.
//
// zh-CN:
// 保存按钮的 sticky 脏标记.
// 行为:
// - 任意文本变化 (包括 undo/redo) 都会置为 true.
// - 仅在保存成功或加载新基线文本时重置为 false.
@Volatile
var saveStickyDirty: Boolean = false
// Whether we have had any direct edits since last save/baseline.
// Direct edits mean changes NOT caused by undo/redo buttons.
//
// zh-CN:
// 自上次保存/建立基线以来是否发生过任何直接编辑.
// 直接编辑指不是由撤销/重做按钮触发的文本变化.
@Volatile
private var mHadDirectEditSinceSave: Boolean = false
// Guard flag to mark text changes caused by undo/redo button actions.
// This is used to distinguish direct edits from history navigation.
//
// zh-CN:
// 用于标记由撤销/重做按钮动作导致的文本变化的哨兵标记.
// 用于区分直接编辑与历史导航.
@Volatile
private var mUndoRedoButtonInProgress: Boolean = false
// Whether user has touched/moved caret during large file loading.
// If true, we should NOT force caret to 0 on load completion.
//
// zh-CN:
// 用户是否在大文件加载期间触摸过/移动过光标.
// 若为 true, 则加载完成时不要强制把光标设为 0.
@Volatile
private var mUserMovedCursorDuringLoading: Boolean = false
var name: String var name: String
get() = _name ?: "[ ${this.context.getString(text_unknown)} ]" get() = _name ?: "[ ${this.context.getString(text_unknown)} ]"
set(value) { set(value) {
@@ -232,7 +272,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
// Delay showing loading bar to avoid flicker. // Delay showing loading bar to avoid flicker.
// zh-CN: 延迟显示加载提示条, 避免闪烁. // zh-CN: 延迟显示加载提示条, 避免闪烁.
private val mLoadingBarShowDelayMs = 1500L private val mLoadingBarShowDelayMs = 300L
// Once shown, keep it visible for at least this duration. // Once shown, keep it visible for at least this duration.
// zh-CN: 一旦显示, 则保证最短展示时间. // zh-CN: 一旦显示, 则保证最短展示时间.
@@ -333,6 +373,8 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
// Initialize menu state early to avoid "first render" flicker. // Initialize menu state early to avoid "first render" flicker.
// zh-CN: 尽早初始化菜单状态, 避免首次渲染闪烁. // zh-CN: 尽早初始化菜单状态, 避免首次渲染闪烁.
mEditorLoading = true mEditorLoading = true
saveStickyDirty = false
mHadDirectEditSinceSave = false
syncPrimaryMenuState() syncPrimaryMenuState()
val name = intent.getStringExtra(EXTRA_NAME) val name = intent.getStringExtra(EXTRA_NAME)
@@ -851,12 +893,14 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
mLargeFileMode = true mLargeFileMode = true
mEditorLoading = true mEditorLoading = true
mUserMovedCursorDuringLoading = false
syncPrimaryMenuState() syncPrimaryMenuState()
editText.setLoadingText(true) editText.setLoadingText(true)
editor.setRedoUndoEnabled(false) editor.setRedoUndoEnabled(false)
editText.setLoadingGutterDigits(7) editText.setLoadingGutterDigits(3)
editText.setText("") editText.setText("")
editText.setSelection(0)
val choreographer = Choreographer.getInstance() val choreographer = Choreographer.getInstance()
@@ -901,8 +945,19 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
endEditorLoadingUi() endEditorLoadingUi()
// Ensure caret/focus after loading is done. // Ensure caret/focus after loading is done.
// zh-CN: 加载结束后确保光标/焦点. // - Default: caret at 0 for "read from top".
post { ensureEditorHasCursorAfterLoadIfNeeded() } // - If user touched during loading: keep current caret (do not force to 0).
//
// zh-CN:
// 加载结束后确保光标/焦点.
// - 默认: 光标置于 0, 方便从头阅读.
// - 若用户在加载期间触摸过: 保持当前光标, 不强制跳回 0.
post {
if (!mUserMovedCursorDuringLoading) {
runCatching { editText.setSelection(0) }
}
ensureEditorHasCursorAfterLoadIfNeeded()
}
mLargeFileFrameCallback = null mLargeFileFrameCallback = null
return return
@@ -996,6 +1051,12 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
editor.markUndoRedoBaselineAsUnchanged() editor.markUndoRedoBaselineAsUnchanged()
editor.setRedoUndoEnabled(!mLargeFileMode) editor.setRedoUndoEnabled(!mLargeFileMode)
// Reset sticky dirty because we just established a new baseline.
// zh-CN: 因刚刚建立了新的基线, 重置 sticky 脏标记.
saveStickyDirty = false
mHadDirectEditSinceSave = false
syncPrimaryMenuState()
// Refresh highlight for restored text if size allows. // Refresh highlight for restored text if size allows.
// zh-CN: 若大小允许, 则对恢复文本刷新一次高亮. // zh-CN: 若大小允许, 则对恢复文本刷新一次高亮.
post { editor.refreshHighlightTokensIfAllowed() } post { editor.refreshHighlightTokensIfAllowed() }
@@ -1016,11 +1077,19 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
try { try {
editText.setText(text) editText.setText(text)
editor.markUndoRedoBaselineAsUnchanged() editor.markUndoRedoBaselineAsUnchanged()
// Reset sticky dirty because we just established a new baseline.
// zh-CN: 因刚刚建立了新的基线, 重置 sticky 脏标记.
saveStickyDirty = false
mHadDirectEditSinceSave = false
} finally { } finally {
editor.setRedoUndoEnabled(true) editor.setRedoUndoEnabled(true)
editText.setLoadingText(false) editText.setLoadingText(false)
} }
mEditorLoading = false
syncPrimaryMenuState()
// Ensure caret/focus after fast load. // Ensure caret/focus after fast load.
// zh-CN: 快速加载完成后确保光标/焦点. // zh-CN: 快速加载完成后确保光标/焦点.
post { ensureEditorHasCursorAfterLoadIfNeeded() } post { ensureEditorHasCursorAfterLoadIfNeeded() }
@@ -1048,7 +1117,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
setMenuItemStatus(R.id.run, true) setMenuItemStatus(R.id.run, true)
setMenuItemStatus(R.id.undo, false) setMenuItemStatus(R.id.undo, false)
setMenuItemStatus(R.id.redo, false) setMenuItemStatus(R.id.redo, false)
setMenuItemStatus(R.id.save, editor.isTextChanged) setMenuItemStatus(R.id.save, saveStickyDirty)
} }
else -> { else -> {
// Normal mode. // Normal mode.
@@ -1056,7 +1125,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
setMenuItemStatus(R.id.run, true) setMenuItemStatus(R.id.run, true)
setMenuItemStatus(R.id.undo, editor.canUndo()) setMenuItemStatus(R.id.undo, editor.canUndo())
setMenuItemStatus(R.id.redo, editor.canRedo()) setMenuItemStatus(R.id.redo, editor.canRedo())
setMenuItemStatus(R.id.save, editor.isTextChanged) setMenuItemStatus(R.id.save, saveStickyDirty)
} }
} }
} }
@@ -1224,12 +1293,40 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
private fun setUpEditor() { private fun setUpEditor() {
editor.let { editor -> editor.let { editor ->
editor.codeEditText.let { editText -> editor.codeEditText.let { editText ->
// Observe user touch in text area to preserve caret position after large-file load.
// zh-CN: 监听用户在文本区域的触摸, 以便大文件加载完成后保留光标位置.
editText.onUserTouchInTextArea = {
if (mEditorLoading && mLargeFileMode) {
mUserMovedCursorDuringLoading = true
}
}
editText.addTextChangedListener(SimpleTextWatcher { _ -> editText.addTextChangedListener(SimpleTextWatcher { _ ->
// Skip menu state updates during progressive loading. // Skip menu state updates during progressive loading.
// zh-CN: 渐进式加载期间跳过菜单状态更新. // zh-CN: 渐进式加载期间跳过菜单状态更新.
if (editText.isLoadingText() || mEditorLoading) { if (editText.isLoadingText() || mEditorLoading) {
return@SimpleTextWatcher return@SimpleTextWatcher
} }
// If this change is not caused by undo/redo buttons, treat it as a direct edit.
// Direct edits keep Save sticky until next successful save.
//
// zh-CN:
// 若本次变化并非由撤销/重做按钮触发, 则视为直接编辑.
// 直接编辑会使保存按钮保持 sticky, 直到下一次保存成功.
if (!mUndoRedoButtonInProgress) {
mHadDirectEditSinceSave = true
saveStickyDirty = true
} else {
// Undo/redo navigation:
// Save should reflect (baseline changed) OR (there has been any direct edit since save).
//
// zh-CN:
// undo/redo 历史导航:
// 保存按钮应反映 (相对基线有变化) 或 (自保存以来发生过直接编辑).
saveStickyDirty = editor.isTextChanged || mHadDirectEditSinceSave
}
syncPrimaryMenuState() syncPrimaryMenuState()
}) })
@@ -1268,6 +1365,8 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
// zh-CN: 初始化为 "新建文件" 预期状态: 仅运行可用. // zh-CN: 初始化为 "新建文件" 预期状态: 仅运行可用.
mEditorLoading = false mEditorLoading = false
mLargeFileMode = false mLargeFileMode = false
saveStickyDirty = false
mHadDirectEditSinceSave = false
setMenuItemStatus(R.id.run, true) setMenuItemStatus(R.id.run, true)
setMenuItemStatus(R.id.undo, false) setMenuItemStatus(R.id.undo, false)
setMenuItemStatus(R.id.redo, false) setMenuItemStatus(R.id.redo, false)
@@ -1287,7 +1386,9 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
val imeBarBackgroundColor = it.imeBarBackgroundColor val imeBarBackgroundColor = it.imeBarBackgroundColor
editor.setTheme(it) editor.setTheme(it)
mInputMethodEnhanceBar.setBackgroundColor(imeBarBackgroundColor) mInputMethodEnhanceBar.setBackgroundColor(imeBarBackgroundColor)
mLoadingBarContainer.setBackgroundColor(imeBarBackgroundColor)
run { run {
val adjustedImageContrastColor = ColorUtils.adjustColorForContrast(imeBarBackgroundColor, appThemeColor, 3.6) val adjustedImageContrastColor = ColorUtils.adjustColorForContrast(imeBarBackgroundColor, appThemeColor, 3.6)
@@ -1390,9 +1491,43 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
return run(true, file, uri?.path) return run(true, file, uri?.path)
} }
private fun undo() = editor.undo() private fun undo() {
// Mark undo/redo button action so TextWatcher can distinguish it.
// zh-CN: 标记撤销/重做按钮动作, 以便 TextWatcher 区分来源.
mUndoRedoButtonInProgress = true
try {
editor.undo()
} finally {
mUndoRedoButtonInProgress = false
}
private fun redo() = editor.redo() // Recompute Save state after history navigation.
// zh-CN: 历史导航后重新计算保存按钮状态.
saveStickyDirty = editor.isTextChanged || mHadDirectEditSinceSave
syncPrimaryMenuState()
}
private fun redo() {
// Mark undo/redo button action so TextWatcher can distinguish it.
// zh-CN: 标记撤销/重做按钮动作, 以便 TextWatcher 区分来源.
mUndoRedoButtonInProgress = true
try {
editor.redo()
} finally {
mUndoRedoButtonInProgress = false
}
// Recompute Save state after history navigation.
// This makes "edit -> save -> undo -> redo" turn Save off again,
// because we are back to baseline and there was no direct edit since save.
//
// zh-CN:
// 历史导航后重新计算保存按钮状态.
// 这会使 "编辑 -> 保存 -> 撤销 -> 重做" 再次熄灭保存按钮,
// 因为已回到基线, 且保存后没有发生直接编辑.
saveStickyDirty = editor.isTextChanged || mHadDirectEditSinceSave
syncPrimaryMenuState()
}
fun save(): Observable<String> = fun save(): Observable<String> =
Observable Observable
@@ -1414,6 +1549,12 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext { .doOnNext {
editor.markTextAsSaved() editor.markTextAsSaved()
// Reset sticky dirty only on successful save.
// zh-CN: 仅在保存成功后重置 sticky 脏标记.
saveStickyDirty = false
mHadDirectEditSinceSave = false
setMenuItemStatus(R.id.save, false) setMenuItemStatus(R.id.save, false)
} }
@@ -1695,9 +1836,13 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
uri = uri, uri = uri,
onRestoreToEditor = { restoredText -> onRestoreToEditor = { restoredText ->
editor.text = restoredText editor.text = restoredText
// Restoring changes content => mark sticky dirty.
// zh-CN: 恢复版本会改变内容, 因此置 sticky 脏标记.
saveStickyDirty = true
}, },
onRestoredUi = { onRestoredUi = {
setMenuItemStatus(R.id.save, true) syncPrimaryMenuState()
showSnack(this@EditorView, R.string.text_done) showSnack(this@EditorView, R.string.text_done)
}, },
) )

View File

@@ -83,6 +83,15 @@ class CodeEditText : AppCompatEditText {
private var mDebuggingLine = -1 private var mDebuggingLine = -1
private var mCursorChangeCallbacks: CopyOnWriteArrayList<CursorChangeCallback>? = null private var mCursorChangeCallbacks: CopyOnWriteArrayList<CursorChangeCallback>? = null
// Callback when user touches the editor text area (not gutter).
// This is used by large-file loader to avoid forcing caret to 0 after user interaction.
//
// zh-CN:
// 当用户触摸编辑器文本区域 (非行号区域) 时的回调.
// 用于大文件加载逻辑: 若用户已交互, 则避免加载完成后强制将光标跳到 0.
@Volatile
var onUserTouchInTextArea: (() -> Unit)? = null
// Read-only state. // Read-only state.
// zh-CN: 只读状态. // zh-CN: 只读状态.
private var mReadOnly = false private var mReadOnly = false
@@ -649,6 +658,12 @@ class CodeEditText : AppCompatEditText {
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean { override fun onTouchEvent(event: MotionEvent): Boolean {
// Notify outer layer early when user touches inside text area (not gutter).
// zh-CN: 当用户触摸文本区域 (非行号区域) 时尽早通知外层.
if (event.action == MotionEvent.ACTION_DOWN && event.x >= paddingLeft) {
onUserTouchInTextArea?.invoke()
}
// 如果行号区域被按下 // 如果行号区域被按下
if (event.action == MotionEvent.ACTION_DOWN && event.x < paddingLeft) { if (event.action == MotionEvent.ACTION_DOWN && event.x < paddingLeft) {
// 则计算当前行, 如果行号有效, 记录起来 // 则计算当前行, 如果行号有效, 记录起来

View File

@@ -4,7 +4,6 @@ package org.autojs.autojs.ui.edit.editor;
* THIS CLASS IS PROVIDED TO THE PUBLIC DOMAIN FOR FREE WITHOUT ANY * THIS CLASS IS PROVIDED TO THE PUBLIC DOMAIN FOR FREE WITHOUT ANY
* RESTRICTIONS OR ANY WARRANTY. * RESTRICTIONS OR ANY WARRANTY.
*/ */
import java.util.LinkedList;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor; import android.content.SharedPreferences.Editor;
@@ -15,10 +14,12 @@ import android.text.TextWatcher;
import android.text.style.UnderlineSpan; import android.text.style.UnderlineSpan;
import android.widget.TextView; import android.widget.TextView;
import java.util.LinkedList;
/** /**
* A generic undo/redo implementation for TextViews. * A generic undo/redo implementation for TextViews.
* * <p>
* Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 8, 2026. * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 12, 2026.
*/ */
public class TextViewUndoRedo { public class TextViewUndoRedo {
@@ -48,6 +49,10 @@ public class TextViewUndoRedo {
private int mInitialHistoryStackSize; private int mInitialHistoryStackSize;
// Baseline position for "unchanged" state.
// zh-CN: "未修改" 状态的基线位置.
private int mInitialHistoryPosition;
private final Handler mHandler = new Handler(); private final Handler mHandler = new Handler();
private int mTextChangeId = 0; private int mTextChangeId = 0;
private final boolean mTextChanging = false; private final boolean mTextChanging = false;
@@ -76,14 +81,14 @@ public class TextViewUndoRedo {
/** /**
* Reset undo/redo history without modifying the current text. * Reset undo/redo history without modifying the current text.
* * <p>
* This is useful after bulk loading text progressively, where the text is already in the TextView * This is useful after bulk loading text progressively, where the text is already in the TextView
* and we only want to treat it as the "baseline" (unchanged) state. * and we only want to treat it as the "baseline" (unchanged) state.
* * <p>
* zh-CN: * zh-CN:
* * <p>
* 在不修改当前文本的前提下重置 undo/redo 历史. * 在不修改当前文本的前提下重置 undo/redo 历史.
* * <p>
* 适用于渐进式批量加载文本后: 文本已经在 TextView 中, 此时只需要把它视为 "基线" (未修改) 状态. * 适用于渐进式批量加载文本后: 文本已经在 TextView 中, 此时只需要把它视为 "基线" (未修改) 状态.
*/ */
public final void resetHistoryAsUnchanged() { public final void resetHistoryAsUnchanged() {
@@ -110,12 +115,21 @@ public class TextViewUndoRedo {
mIsUndoOrRedo = false; mIsUndoOrRedo = false;
} }
public boolean isTextChanged(){ public boolean isTextChanged() {
return mInitialHistoryStackSize != mEditHistory.size(); // IMPORTANT:
// Use history position rather than history size.
// Undo/redo changes mmPosition while keeping history size the same.
//
// zh-CN:
// 重要:
// 使用 history position 而不是 history size.
// undo/redo 会改变 mmPosition, 但 history size 往往不变.
return mInitialHistoryPosition != mEditHistory.mmPosition;
} }
public void markTextAsUnchanged() { public void markTextAsUnchanged() {
mInitialHistoryStackSize = mEditHistory.size(); mInitialHistoryStackSize = mEditHistory.size();
mInitialHistoryPosition = mEditHistory.mmPosition;
} }
// =================================================================== // // =================================================================== //
@@ -141,6 +155,7 @@ public class TextViewUndoRedo {
public void clearHistory() { public void clearHistory() {
mEditHistory.clear(); mEditHistory.clear();
mInitialHistoryStackSize = 0; mInitialHistoryStackSize = 0;
mInitialHistoryPosition = 0;
} }
/** /**
@@ -339,7 +354,7 @@ public class TextViewUndoRedo {
} }
} }
public int size(){ public int size() {
return mmHistory.size(); return mmHistory.size();
} }
@@ -446,7 +461,7 @@ public class TextViewUndoRedo {
mTextChangeId++; mTextChangeId++;
mEditHistory.add(new EditItem(start, mBeforeChange, mAfterChange)); mEditHistory.add(new EditItem(start, mBeforeChange, mAfterChange));
int textChangeId = mTextChangeId; int textChangeId = mTextChangeId;
//TODO 增加连续输入文字当成一次撤销的功能 // TODO 增加连续输入文字当成一次撤销的功能
} }
public void afterTextChanged(Editable s) { public void afterTextChanged(Editable s) {
@@ -456,6 +471,15 @@ public class TextViewUndoRedo {
if (mEditHistory.size() < mInitialHistoryStackSize) { if (mEditHistory.size() < mInitialHistoryStackSize) {
mInitialHistoryStackSize = 0; mInitialHistoryStackSize = 0;
} }
// Keep baseline position within valid range after trims.
// zh-CN: 在 trim 等场景后, 保持基线 position 落在合法范围内.
if (mInitialHistoryPosition > mEditHistory.mmHistory.size()) {
mInitialHistoryPosition = mEditHistory.mmHistory.size();
}
if (mInitialHistoryPosition < 0) {
mInitialHistoryPosition = 0;
}
} }
} }
} }

View File

@@ -1331,4 +1331,6 @@
<string name="text_xiaomi_background_popup_permission">النوافذ المنبثقة في الخلفية</string> <string name="text_xiaomi_background_popup_permission">النوافذ المنبثقة في الخلفية</string>
<string name="warn_continue_operation_without_saving_settings">لم يتم حفظ الاعدادات. هل تريد المتابعة?</string> <string name="warn_continue_operation_without_saving_settings">لم يتم حفظ الاعدادات. هل تريد المتابعة?</string>
<string name="warn_exit_without_saving_settings">لم يتم حفظ الاعدادات. هل تريد الخروج?</string> <string name="warn_exit_without_saving_settings">لم يتم حفظ الاعدادات. هل تريد الخروج?</string>
<string name="error_failed_to_save">فشل الحفظ</string>
<string name="text_saving" tools:ignore="TypographyEllipsis">جارٍ الحفظ...</string>
</resources> </resources>

View File

@@ -1326,4 +1326,6 @@
<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="warn_continue_operation_without_saving_settings">The settings has not been saved, are you sure to continue the operation?</string> <string name="warn_continue_operation_without_saving_settings">The settings has not been saved, are you sure to continue the operation?</string>
<string name="warn_exit_without_saving_settings">The settings has not been saved, are you sure to exit?</string> <string name="warn_exit_without_saving_settings">The settings has not been saved, are you sure to exit?</string>
<string name="error_failed_to_save">Failed to save</string>
<string name="text_saving" tools:ignore="TypographyEllipsis">Saving...</string>
</resources> </resources>

View File

@@ -1329,4 +1329,6 @@
<string name="text_xiaomi_background_popup_permission">Ventanas emergentes en segundo plano</string> <string name="text_xiaomi_background_popup_permission">Ventanas emergentes en segundo plano</string>
<string name="warn_continue_operation_without_saving_settings">La configuracion no se ha guardado. ¿Seguro que quieres continuar?</string> <string name="warn_continue_operation_without_saving_settings">La configuracion no se ha guardado. ¿Seguro que quieres continuar?</string>
<string name="warn_exit_without_saving_settings">La configuracion no se ha guardado. ¿Seguro que quieres salir?</string> <string name="warn_exit_without_saving_settings">La configuracion no se ha guardado. ¿Seguro que quieres salir?</string>
<string name="error_failed_to_save">Error al guardar</string>
<string name="text_saving" tools:ignore="TypographyEllipsis">Guardando...</string>
</resources> </resources>

View File

@@ -1329,4 +1329,6 @@
<string name="text_xiaomi_background_popup_permission">Fenêtres contextuelles en arrière-plan</string> <string name="text_xiaomi_background_popup_permission">Fenêtres contextuelles en arrière-plan</string>
<string name="warn_continue_operation_without_saving_settings">Les parametres ne sont pas enregistres. Continuer?</string> <string name="warn_continue_operation_without_saving_settings">Les parametres ne sont pas enregistres. Continuer?</string>
<string name="warn_exit_without_saving_settings">Les parametres ne sont pas enregistres. Quitter?</string> <string name="warn_exit_without_saving_settings">Les parametres ne sont pas enregistres. Quitter?</string>
<string name="error_failed_to_save">Echec de l\'enregistrement</string>
<string name="text_saving" tools:ignore="TypographyEllipsis">Enregistrement...</string>
</resources> </resources>

View File

@@ -1330,4 +1330,6 @@
<string name="text_xiaomi_background_popup_permission">バックグラウンドでのポップアップ表示</string> <string name="text_xiaomi_background_popup_permission">バックグラウンドでのポップアップ表示</string>
<string name="warn_continue_operation_without_saving_settings">設定が保存されていません. 続行しますか?</string> <string name="warn_continue_operation_without_saving_settings">設定が保存されていません. 続行しますか?</string>
<string name="warn_exit_without_saving_settings">設定が保存されていません. 終了しますか?</string> <string name="warn_exit_without_saving_settings">設定が保存されていません. 終了しますか?</string>
<string name="error_failed_to_save">保存に失敗しました</string>
<string name="text_saving" tools:ignore="TypographyEllipsis">保存中...</string>
</resources> </resources>

View File

@@ -1331,4 +1331,6 @@
<string name="text_xiaomi_background_popup_permission">백그라운드 팝업</string> <string name="text_xiaomi_background_popup_permission">백그라운드 팝업</string>
<string name="warn_continue_operation_without_saving_settings">설정이 저장되지 않았습니다. 계속할까요?</string> <string name="warn_continue_operation_without_saving_settings">설정이 저장되지 않았습니다. 계속할까요?</string>
<string name="warn_exit_without_saving_settings">설정이 저장되지 않았습니다. 종료할까요?</string> <string name="warn_exit_without_saving_settings">설정이 저장되지 않았습니다. 종료할까요?</string>
<string name="error_failed_to_save">저장에 실패했습니다</string>
<string name="text_saving" tools:ignore="TypographyEllipsis">저장 중...</string>
</resources> </resources>

View File

@@ -1329,4 +1329,6 @@
<string name="text_xiaomi_background_popup_permission">Всплывающие окна в фоне</string> <string name="text_xiaomi_background_popup_permission">Всплывающие окна в фоне</string>
<string name="warn_continue_operation_without_saving_settings">Настройки не сохранены. Продолжить?</string> <string name="warn_continue_operation_without_saving_settings">Настройки не сохранены. Продолжить?</string>
<string name="warn_exit_without_saving_settings">Настройки не сохранены. Выйти?</string> <string name="warn_exit_without_saving_settings">Настройки не сохранены. Выйти?</string>
<string name="error_failed_to_save">Не удалось сохранить</string>
<string name="text_saving" tools:ignore="TypographyEllipsis">Сохранение...</string>
</resources> </resources>

View File

@@ -1325,4 +1325,6 @@
<string name="text_xiaomi_background_popup_permission">後台彈出界面</string> <string name="text_xiaomi_background_popup_permission">後台彈出界面</string>
<string name="warn_continue_operation_without_saving_settings">設置尚未保存, 確定要繼續操作嗎</string> <string name="warn_continue_operation_without_saving_settings">設置尚未保存, 確定要繼續操作嗎</string>
<string name="warn_exit_without_saving_settings">設置尚未保存, 確定要退出嗎</string> <string name="warn_exit_without_saving_settings">設置尚未保存, 確定要退出嗎</string>
<string name="error_failed_to_save">保存失敗</string>
<string name="text_saving" tools:ignore="TypographyEllipsis">正在保存...</string>
</resources> </resources>

View File

@@ -1325,4 +1325,6 @@
<string name="text_xiaomi_background_popup_permission">後臺彈出介面</string> <string name="text_xiaomi_background_popup_permission">後臺彈出介面</string>
<string name="warn_continue_operation_without_saving_settings">設定尚未儲存, 確定要繼續操作嗎</string> <string name="warn_continue_operation_without_saving_settings">設定尚未儲存, 確定要繼續操作嗎</string>
<string name="warn_exit_without_saving_settings">設定尚未儲存, 確定要退出嗎</string> <string name="warn_exit_without_saving_settings">設定尚未儲存, 確定要退出嗎</string>
<string name="error_failed_to_save">儲存失敗</string>
<string name="text_saving" tools:ignore="TypographyEllipsis">正在儲存...</string>
</resources> </resources>

View File

@@ -1326,4 +1326,6 @@
<string name="text_xiaomi_background_popup_permission">后台弹出界面</string> <string name="text_xiaomi_background_popup_permission">后台弹出界面</string>
<string name="warn_continue_operation_without_saving_settings">设置尚未保存, 确定要继续操作吗</string> <string name="warn_continue_operation_without_saving_settings">设置尚未保存, 确定要继续操作吗</string>
<string name="warn_exit_without_saving_settings">设置尚未保存, 确定要退出吗</string> <string name="warn_exit_without_saving_settings">设置尚未保存, 确定要退出吗</string>
<string name="error_failed_to_save">保存失败</string>
<string name="text_saving" tools:ignore="TypographyEllipsis">正在保存...</string>
</resources> </resources>

View File

@@ -1600,4 +1600,6 @@
<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="warn_continue_operation_without_saving_settings">The settings has not been saved, are you sure to continue the operation?</string> <string name="warn_continue_operation_without_saving_settings">The settings has not been saved, are you sure to continue the operation?</string>
<string name="warn_exit_without_saving_settings">The settings has not been saved, are you sure to exit?</string> <string name="warn_exit_without_saving_settings">The settings has not been saved, are you sure to exit?</string>
<string name="error_failed_to_save">Failed to save</string>
<string name="text_saving" tools:ignore="TypographyEllipsis">Saving...</string>
</resources> </resources>

View File

@@ -1,5 +1,5 @@
#Thu Feb 12 17:13:51 CST 2026 #Thu Feb 12 19:57:00 CST 2026
BUILD_TIME=1770887631678 BUILD_TIME=1770897420318
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=3734 VERSION_BUILD=3735
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