6.7.0 - Alpha19 - 修复代码编辑器在只读模式下已崩溃且依然可以编辑代码内容的问题

This commit is contained in:
SuperMonster003
2026-02-08 00:41:07 +08:00
parent 4dc0f7c60b
commit 7307c29711
7 changed files with 363 additions and 107 deletions

View File

@@ -1,7 +1,7 @@
{ {
"$data": { "$data": {
"v6.7.0": { "v6.7.0": {
"released_date": "2026/02/04", "released_date": "2026/02/08",
"feature": [ "feature": [
"插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮/主页标签页)", "插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮/主页标签页)",
"版本历史功能, 支持查看/恢复可编辑文件的历史版本 (入口: 主页抽屉按钮/文件管理器菜单/代码编辑器菜单)", "版本历史功能, 支持查看/恢复可编辑文件的历史版本 (入口: 主页抽屉按钮/文件管理器菜单/代码编辑器菜单)",
@@ -96,6 +96,9 @@
"发行历史页面部分系统因字体差别导致统计数据显示不完整的问题", "发行历史页面部分系统因字体差别导致统计数据显示不完整的问题",
"部分设备无法正常初始化 MLKit Google OCR 的问题 (试修) _[`issue #8`](http://issues.autojs6.com/8#issuecomment-3117061768)_", "部分设备无法正常初始化 MLKit Google OCR 的问题 (试修) _[`issue #8`](http://issues.autojs6.com/8#issuecomment-3117061768)_",
"部分设备无法正常触发文件管理器功能按钮点击事件的问题 (试修) _[`issue #465`](http://issues.autojs6.com/465)_", "部分设备无法正常触发文件管理器功能按钮点击事件的问题 (试修) _[`issue #465`](http://issues.autojs6.com/465)_",
"部分设备代码编辑器空行显示方框字符的问题 (试修)",
"代码编辑器在只读模式下依然可以编辑代码内容的问题",
"代码编辑器在只读模式下点击标题区域及部分菜单项导致应用崩溃的问题",
"ErrorDialogActivity 可能无法正常启动或短时间自动消失的问题 _[`issue #479`](http://issues.autojs6.com/479)_ _[`issue #471`](http://issues.autojs6.com/471)_ _[`issue #414`](http://issues.autojs6.com/414)_ _[`issue #340`](http://issues.autojs6.com/340#issuecomment-2973485826)_", "ErrorDialogActivity 可能无法正常启动或短时间自动消失的问题 _[`issue #479`](http://issues.autojs6.com/479)_ _[`issue #471`](http://issues.autojs6.com/471)_ _[`issue #414`](http://issues.autojs6.com/414)_ _[`issue #340`](http://issues.autojs6.com/340#issuecomment-2973485826)_",
"Canvas 构造函数可接受的参数类型错误 _[`issue #402`](http://issues.autojs6.com/402)_", "Canvas 构造函数可接受的参数类型错误 _[`issue #402`](http://issues.autojs6.com/402)_",
"崩溃报告页面复制详细信息功能失效的问题", "崩溃报告页面复制详细信息功能失效的问题",

View File

@@ -28,8 +28,6 @@ import org.autojs.autojs.app.OnActivityResultDelegate.DelegateHost
import org.autojs.autojs.core.permission.OnRequestPermissionsResultCallback import org.autojs.autojs.core.permission.OnRequestPermissionsResultCallback
import org.autojs.autojs.core.permission.PermissionRequestProxyActivity import org.autojs.autojs.core.permission.PermissionRequestProxyActivity
import org.autojs.autojs.core.permission.RequestPermissionCallbacks import org.autojs.autojs.core.permission.RequestPermissionCallbacks
import org.autojs.autojs.util.ViewUtils.setOnTitleViewClickListener
import org.autojs.autojs.util.ViewUtils.titleView
import org.autojs.autojs.pio.PFiles 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
@@ -40,6 +38,8 @@ import org.autojs.autojs.util.IntentUtils.startSafely
import org.autojs.autojs.util.Observers import org.autojs.autojs.util.Observers
import org.autojs.autojs.util.ViewUtils.onceGlobalLayout import org.autojs.autojs.util.ViewUtils.onceGlobalLayout
import org.autojs.autojs.util.ViewUtils.setMenuIconsColorByThemeColorLuminance import org.autojs.autojs.util.ViewUtils.setMenuIconsColorByThemeColorLuminance
import org.autojs.autojs.util.ViewUtils.setOnTitleViewClickListener
import org.autojs.autojs.util.ViewUtils.titleView
import org.autojs.autojs6.R import org.autojs.autojs6.R
import org.autojs.autojs6.databinding.ActivityEditBinding import org.autojs.autojs6.databinding.ActivityEditBinding
import java.io.File import java.io.File
@@ -51,6 +51,8 @@ import java.io.IOException
*/ */
open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyActivity { open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyActivity {
private var mReadOnly: Boolean = false
private val mOnBackPressedCallback = object : OnBackPressedCallback(true) { private val mOnBackPressedCallback = object : OnBackPressedCallback(true) {
// override fun onBackPressed() { // override fun onBackPressed() {
@@ -85,12 +87,17 @@ open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyAc
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
mReadOnly = intent.getBooleanExtra(EditorView.EXTRA_READ_ONLY, false)
val binding = ActivityEditBinding.inflate(layoutInflater).also { setContentView(it.root) } val binding = ActivityEditBinding.inflate(layoutInflater).also { setContentView(it.root) }
mToolbar = findViewById<ThemeColorToolbar>(R.id.toolbar).apply { mToolbar = findViewById<ThemeColorToolbar>(R.id.toolbar).apply {
setTitleTextAppearance(this@EditActivity, R.style.TextAppearanceEditorTitle) setTitleTextAppearance(this@EditActivity, R.style.TextAppearanceEditorTitle)
setOnTitleViewClickListener { setOnTitleViewClickListener {
EditableFileInfoDialogManager.showEditableFileInfoDialog(this@EditActivity, mEditorView.uri.path?.let { File(it) }) { val path = mEditorView.uri?.path
mEditorView.editor.text if (path != null) {
EditableFileInfoDialogManager.showEditableFileInfoDialog(this@EditActivity, File(path)) {
mEditorView.editor.text
}
} }
} }
} }
@@ -99,7 +106,7 @@ open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyAc
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(Observers.emptyConsumer()) { ex: Throwable -> onLoadFileError(ex.message) } .subscribe(Observers.emptyConsumer()) { ex: Throwable -> onLoadFileError(ex.message) }
} }
mEditorMenu = EditorMenu(mEditorView) mEditorMenu = EditorMenu(mEditorView, mReadOnly)
mNewTask = intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK != 0 mNewTask = intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK != 0
setUpToolbar() setUpToolbar()
@@ -130,6 +137,11 @@ open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyAc
return true return true
} }
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
mEditorMenu.prepareOptionsMenu(menu)
return super.onPrepareOptionsMenu(menu)
}
private fun TextView.adjustTitleTextView() = this.post { private fun TextView.adjustTitleTextView() = this.post {
ValueAnimator.ofFloat(this.textSize, calculatedTextSize(this) ?: return@post).let { animator -> ValueAnimator.ofFloat(this.textSize, calculatedTextSize(this) ?: return@post).let { animator ->
animator.duration = 120L animator.duration = 120L
@@ -247,7 +259,9 @@ open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyAc
val menu = mode.menu val menu = mode.menu
val item = menu[menu.size - 1] val item = menu[menu.size - 1]
addMenuItem(menu, item.groupId, R.id.action_delete_line, 10000, R.string.text_delete_line) { mEditorMenu.deleteLine() } if (!mReadOnly) {
addMenuItem(menu, item.groupId, R.id.action_delete_line, 10000, R.string.text_delete_line) { mEditorMenu.deleteLine() }
}
addMenuItem(menu, item.groupId, R.id.action_copy_line, 20000, R.string.text_copy_line) { mEditorMenu.copyLine() } addMenuItem(menu, item.groupId, R.id.action_copy_line, 20000, R.string.text_copy_line) { mEditorMenu.copyLine() }
super.onActionModeStarted(mode) super.onActionModeStarted(mode)

View File

@@ -2,15 +2,17 @@ package org.autojs.autojs.ui.edit;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import android.text.InputType; import android.text.InputType;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.afollestad.materialdialogs.MaterialDialog; import com.afollestad.materialdialogs.MaterialDialog;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import org.autojs.autojs.util.DialogUtils;
import org.autojs.autojs.core.pref.Language; import org.autojs.autojs.core.pref.Language;
import org.autojs.autojs.core.pref.Pref; import org.autojs.autojs.core.pref.Pref;
import org.autojs.autojs.util.MaterialDialogUtils;
import org.autojs.autojs.model.indices.AndroidClass; import org.autojs.autojs.model.indices.AndroidClass;
import org.autojs.autojs.model.indices.ClassSearchingItem; import org.autojs.autojs.model.indices.ClassSearchingItem;
import org.autojs.autojs.script.JavaScriptFileSource; import org.autojs.autojs.script.JavaScriptFileSource;
@@ -35,18 +37,56 @@ import static org.autojs.autojs.util.StringUtils.key;
/** /**
* Created by Stardust on Sep 28, 2017. * Created by Stardust on Sep 28, 2017.
*/ */
@SuppressWarnings("ResultOfMethodCallIgnored") @SuppressWarnings({"ResultOfMethodCallIgnored", "unused"})
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
public class EditorMenu { public class EditorMenu {
private final EditorView mEditorView; private final EditorView mEditorView;
private final Context mContext; private final Context mContext;
private final CodeEditor mEditor; private final CodeEditor mEditor;
private final boolean mReadOnly;
public EditorMenu(EditorView editorView) { public EditorMenu(EditorView editorView) {
this(editorView, false);
}
public EditorMenu(EditorView editorView, boolean readOnly) {
mEditorView = editorView; mEditorView = editorView;
mContext = editorView.getContext(); mContext = editorView.getContext();
mEditor = editorView.editor; mEditor = editorView.editor;
mReadOnly = readOnly;
}
public void prepareOptionsMenu(Menu menu) {
if (menu == null) {
return;
}
if (!mReadOnly) {
return;
}
setMenuItemInvisible(menu, R.id.action_find_or_replace);
setMenuItemInvisible(menu, R.id.action_copy_line);
setMenuItemInvisible(menu, R.id.action_paste);
setMenuItemInvisible(menu, R.id.action_delete_line);
setMenuItemInvisible(menu, R.id.action_clear);
setMenuItemInvisible(menu, R.id.action_comment);
setMenuItemInvisible(menu, R.id.action_beautify);
setMenuItemInvisible(menu, R.id.action_jump);
setMenuItemInvisible(menu, R.id.action_debug);
setMenuItemInvisible(menu, R.id.action_build_apk);
setMenuItemInvisible(menu, R.id.action_console);
setMenuItemInvisible(menu, R.id.action_import_java_class);
setMenuItemInvisible(menu, R.id.action_file_details);
setMenuItemInvisible(menu, R.id.action_version_history);
setMenuItemInvisible(menu, R.id.action_fx_keyboard);
setMenuItemInvisible(menu, R.id.action_open_by_other_apps);
}
private void setMenuItemInvisible(Menu menu, int itemId) {
MenuItem item = menu.findItem(itemId);
if (item != null) {
item.setVisible(false);
}
} }
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
@@ -75,7 +115,7 @@ public class EditorMenu {
.content(R.string.hint_long_click_run_to_debug) .content(R.string.hint_long_click_run_to_debug)
.positiveText(R.string.dialog_button_dismiss) .positiveText(R.string.dialog_button_dismiss)
.positiveColorRes(R.color.dialog_button_default); .positiveColorRes(R.color.dialog_button_default);
MaterialDialogUtils.widgetThemeColor(builder); DialogUtils.widgetThemeColor(builder);
builder.show(); builder.show();
return tryDoing(mEditorView::debug); return tryDoing(mEditorView::debug);
} }
@@ -216,7 +256,22 @@ public class EditorMenu {
} }
private void startBuildApkActivity() { private void startBuildApkActivity() {
BuildActivity.launch(mContext, mEditorView.uri.getPath()); Uri uri = mEditorView.getUri();
String path;
if (uri == null) {
path = null;
} else {
path = uri.getPath();
}
if (TextUtils.isEmpty(path)) {
DialogUtils.buildAndShowAdaptive(new MaterialDialog.Builder(mContext)
.title(R.string.text_prompt)
.content(R.string.error_unable_to_package_application_as_current_file_path_is_unknown)
.positiveText(R.string.dialog_button_dismiss)
.positiveColorRes(R.color.dialog_button_default)::build);
return;
}
BuildActivity.launch(mContext, path);
} }
private void setPinchToZoomStrategy() { private void setPinchToZoomStrategy() {
@@ -256,7 +311,7 @@ public class EditorMenu {
.onPositive((dialog, which) -> dialog.dismiss()) .onPositive((dialog, which) -> dialog.dismiss())
.autoDismiss(false); .autoDismiss(false);
MaterialDialogUtils.choiceWidgetThemeColor(builder); DialogUtils.choiceWidgetThemeColor(builder);
// TODO by SuperMonster003 on Oct 17, 2022. // TODO by SuperMonster003 on Oct 17, 2022.
// ! Implementation for "scale view". // ! Implementation for "scale view".
@@ -338,12 +393,26 @@ public class EditorMenu {
builder.positiveColorRes(R.color.dialog_button_attraction); builder.positiveColorRes(R.color.dialog_button_attraction);
builder.negativeText(R.string.dialog_button_cancel); builder.negativeText(R.string.dialog_button_cancel);
builder.negativeColorRes(R.color.dialog_button_default); builder.negativeColorRes(R.color.dialog_button_default);
MaterialDialogUtils.widgetThemeColor(builder); DialogUtils.widgetThemeColor(builder);
builder.show(); builder.show();
} }
private void showFileDetails() { private void showFileDetails() {
var path = mEditorView.uri.getPath(); Uri uri = mEditorView.getUri();
String path;
if (uri != null) {
path = uri.getPath();
} else {
path = null;
}
if (TextUtils.isEmpty(path)) {
DialogUtils.buildAndShowAdaptive(new MaterialDialog.Builder(mContext)
.title(R.string.text_prompt)
.content(R.string.error_unable_to_display_file_details_as_current_file_path_is_unknown)
.positiveText(R.string.dialog_button_dismiss)
.positiveColorRes(R.color.dialog_button_default)::build);
return;
}
EditableFileInfoDialogManager.showEditableFileInfoDialog(mContext, new File(path), mEditor::getText); EditableFileInfoDialogManager.showEditableFileInfoDialog(mContext, new File(path), mEditor::getText);
} }

View File

@@ -26,7 +26,7 @@ import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import org.autojs.autojs.AutoJs import org.autojs.autojs.AutoJs
import org.autojs.autojs.app.DialogUtils import org.autojs.autojs.util.DialogUtils
import org.autojs.autojs.core.pref.Pref.getEditorTextSize import org.autojs.autojs.core.pref.Pref.getEditorTextSize
import org.autojs.autojs.core.pref.Pref.setEditorTextSize import org.autojs.autojs.core.pref.Pref.setEditorTextSize
import org.autojs.autojs.engine.JavaScriptEngine import org.autojs.autojs.engine.JavaScriptEngine
@@ -47,6 +47,7 @@ import org.autojs.autojs.model.script.Scripts.runWithBroadcastSender
import org.autojs.autojs.pio.PFiles.getNameWithoutExtension import org.autojs.autojs.pio.PFiles.getNameWithoutExtension
import org.autojs.autojs.pio.PFiles.write import org.autojs.autojs.pio.PFiles.write
import org.autojs.autojs.storage.file.TmpScriptFiles import org.autojs.autojs.storage.file.TmpScriptFiles
import org.autojs.autojs.storage.history.HistoryPrefs
import org.autojs.autojs.storage.history.HistoryRepository import org.autojs.autojs.storage.history.HistoryRepository
import org.autojs.autojs.storage.history.HistoryUriUtils import org.autojs.autojs.storage.history.HistoryUriUtils
import org.autojs.autojs.storage.history.VersionHistoryController import org.autojs.autojs.storage.history.VersionHistoryController
@@ -73,12 +74,14 @@ import org.autojs.autojs.ui.widget.SimpleTextWatcher
import org.autojs.autojs.util.ClipboardUtils import org.autojs.autojs.util.ClipboardUtils
import org.autojs.autojs.util.DisplayUtils.pxToSp import org.autojs.autojs.util.DisplayUtils.pxToSp
import org.autojs.autojs.util.DocsUtils.getUrl import org.autojs.autojs.util.DocsUtils.getUrl
import org.autojs.autojs.util.MaterialDialogUtils.choiceWidgetThemeColor import org.autojs.autojs.util.DialogUtils.choiceWidgetThemeColor
import org.autojs.autojs.util.Observers import org.autojs.autojs.util.Observers
import org.autojs.autojs.util.StringUtils import org.autojs.autojs.util.StringUtils
import org.autojs.autojs.util.ViewUtils
import org.autojs.autojs.util.ViewUtils.showSnack import org.autojs.autojs.util.ViewUtils.showSnack
import org.autojs.autojs.util.ViewUtils.showToast import org.autojs.autojs.util.ViewUtils.showToast
import org.autojs.autojs6.R import org.autojs.autojs6.R
import org.autojs.autojs6.R.string.text_unknown
import org.autojs.autojs6.databinding.EditorViewBinding import org.autojs.autojs6.databinding.EditorViewBinding
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@@ -104,9 +107,15 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
@JvmField @JvmField
val debugBar: DebugBar = binding.debugBar val debugBar: DebugBar = binding.debugBar
lateinit var name: String private var _name: String? = null
lateinit var uri: Uri var name: String
get() = _name ?: "[ ${this.context.getString(text_unknown)} ]"
set(value) {
_name = value
}
var uri: Uri? = null
var scriptExecutionId = 0 var scriptExecutionId = 0
private set private set
@@ -204,19 +213,31 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
} }
fun handleIntent(intent: Intent): Observable<String> { fun handleIntent(intent: Intent): Observable<String> {
intent.getStringExtra(EXTRA_NAME)?.let { name = it } val name = intent.getStringExtra(EXTRA_NAME)
if (name != null) {
this.name = name
}
val readOnly = intent.getBooleanExtra(EXTRA_READ_ONLY, false).also {
mReadOnly = it
}
if (readOnly) {
mInputMethodEnhanceBar.visibility = GONE
}
return handleText(intent) return handleText(intent)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext { .doOnNext {
mReadOnly = intent.getBooleanExtra(EXTRA_READ_ONLY, false)
val saveEnabled = intent.getBooleanExtra(EXTRA_SAVE_ENABLED, true) val saveEnabled = intent.getBooleanExtra(EXTRA_SAVE_ENABLED, true)
if (mReadOnly || !saveEnabled) { if (!saveEnabled) {
findViewById<View>(R.id.save).visibility = GONE
} else if (readOnly) {
findViewById<View>(R.id.undo).visibility = GONE
findViewById<View>(R.id.redo).visibility = GONE
findViewById<View>(R.id.save).visibility = GONE findViewById<View>(R.id.save).visibility = GONE
} }
if (!intent.getBooleanExtra(EXTRA_RUN_ENABLED, true)) { if (!intent.getBooleanExtra(EXTRA_RUN_ENABLED, true)) {
findViewById<View>(R.id.run).visibility = GONE findViewById<View>(R.id.run).visibility = GONE
} }
if (mReadOnly) { if (readOnly) {
editor.setReadOnly(true) editor.setReadOnly(true)
} }
} }
@@ -228,21 +249,28 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
} }
private fun handleText(intent: Intent): Observable<String> { private fun handleText(intent: Intent): Observable<String> {
val path = intent.getStringExtra(EXTRA_PATH)
val content = intent.getStringExtra(EXTRA_CONTENT) val content = intent.getStringExtra(EXTRA_CONTENT)
if (content != null) { if (content != null) {
setInitialText(content) setInitialText(content)
return Observable.just(content)
} }
uri = if (path == null) {
intent.data ?: return Observable.error(IllegalArgumentException("path and content is empty")) val path = intent.getStringExtra(EXTRA_PATH)
} else { val uri = path?.let {
Uri.fromFile(File(path)) Uri.fromFile(File(it))
} ?: intent.data
this.uri = uri
if (_name == null && uri != null) {
uri.path?.let {
name = getNameWithoutExtension(it)
}
} }
if (!::name.isInitialized) {
name = getNameWithoutExtension(uri.path!!) return when {
content != null -> Observable.just(content)
uri != null -> loadUri(uri)
else -> Observable.error(IllegalArgumentException("path and content is empty"))
} }
return loadUri(uri)
} }
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
@@ -295,7 +323,12 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
private fun initNormalToolbar() { private fun initNormalToolbar() {
mNormalToolbar.apply { mNormalToolbar.apply {
setOnMenuItemClickListener(this@EditorView) setOnMenuItemClickListener(this@EditorView)
setOnMenuItemLongClickListener { id -> if (id == R.id.run) true.also { debug() } else false } setOnMenuItemLongClickListener { id ->
when (id) {
R.id.run if !mReadOnly -> true.also { debug() }
else -> false
}
}
} }
activity.supportFragmentManager.findFragmentById(R.id.toolbar_menu) ?: showNormalToolbar() activity.supportFragmentManager.findFragmentById(R.id.toolbar_menu) ?: showNormalToolbar()
} }
@@ -375,6 +408,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
mCodeCompletionBar.setTextColor(textColor) mCodeCompletionBar.setTextColor(textColor)
mSymbolBar.setTextColor(textColor) mSymbolBar.setTextColor(textColor)
mShowFunctionsButton.setColorFilter(textColor) mShowFunctionsButton.setColorFilter(textColor)
ViewUtils.setNavigationBarBackgroundColor(activity, it.imeBarBackgroundColor)
invalidate() invalidate()
} }
} }
@@ -412,7 +446,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
} }
@JvmOverloads @JvmOverloads
fun run(showMessage: Boolean, file: File? = uri.path?.let { File(it) }, overriddenFullPath: String? = null): ScriptExecution? { fun run(showMessage: Boolean, file: File? = uri?.path?.let { File(it) }, overriddenFullPath: String? = null): ScriptExecution? {
file ?: return null file ?: return null
if (showMessage) { if (showMessage) {
showSnack(this, R.string.text_start_running) showSnack(this, R.string.text_start_running)
@@ -420,7 +454,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
// TODO by Stardust on Oct 24, 2018. // TODO by Stardust on Oct 24, 2018.
val execution = runWithBroadcastSender( val execution = runWithBroadcastSender(
file, file,
workingDirectory = uri.path?.let { File(it).parent }, workingDirectory = uri?.path?.let { File(it).parent },
overriddenFullPath, overriddenFullPath,
) ?: return null ) ?: return null
scriptExecutionId = execution.id scriptExecutionId = execution.id
@@ -428,8 +462,8 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
return execution return execution
} }
private fun runTmpFile(file: File? = uri.path?.let { File(it) }): ScriptExecution? { private fun runTmpFile(file: File? = uri?.path?.let { File(it) }): ScriptExecution? {
return run(true, file, uri.path) return run(true, file, uri?.path)
} }
private fun undo() = editor.undo() private fun undo() = editor.undo()
@@ -439,10 +473,17 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
fun save(): Observable<String> = fun save(): Observable<String> =
Observable Observable
.fromCallable { .fromCallable {
// Use a transactional save flow for both file:// and content://. when (val uri = uri) {
// zh-CN: 对 file:// 与 content:// 统一使用事务式保存流程. null -> {
editor.text.apply { throw IllegalStateException(context.getString(R.string.error_unable_to_save_file_as_current_file_path_is_unknown))
writeTextWithCharsetTransactional(uri, this) }
else -> {
// Use a transactional save flow for both file:// and content://.
// zh-CN: 对 file:// 与 content:// 统一使用事务式保存流程.
editor.text.apply {
writeTextWithCharsetTransactional(uri, this)
}
}
} }
} }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@@ -506,7 +547,6 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
if (logicalPath != null && shouldTrackHistory(oldBytes, newBytes) && oldBytes != null) { if (logicalPath != null && shouldTrackHistory(oldBytes, newBytes) && oldBytes != null) {
runCatching { runCatching {
HistoryRepository(context.applicationContext).recordSavePre( HistoryRepository(context.applicationContext).recordSavePre(
uri = uri,
logicalPath = logicalPath, logicalPath = logicalPath,
oldBytes = oldBytes, oldBytes = oldBytes,
encodingName = targetCharset.name(), encodingName = targetCharset.name(),
@@ -689,7 +729,9 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
private fun shouldTrackHistory(oldBytes: ByteArray?, newBytes: ByteArray): Boolean { private fun shouldTrackHistory(oldBytes: ByteArray?, newBytes: ByteArray): Boolean {
val oldSize = oldBytes?.size ?: 0 val oldSize = oldBytes?.size ?: 0
val newSize = newBytes.size val newSize = newBytes.size
return oldSize <= MAX_FILE_SIZE_TO_TRACK_BYTES && newSize <= MAX_FILE_SIZE_TO_TRACK_BYTES val limit = HistoryPrefs.maxFileSizeToTrackBytes().coerceAtLeast(0L)
return oldSize.toLong() <= limit && newSize.toLong() <= limit
} }
private fun sha256(bytes: ByteArray): ByteArray { private fun sha256(bytes: ByteArray): ByteArray {
@@ -711,18 +753,32 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
} }
fun showVersionHistoryDialog() { fun showVersionHistoryDialog() {
// Delegate to controller to unify editor/explorer/history page behavior. when (val uri = uri) {
// zh-CN: 委托给 Controller, 统一 editor/explorer/history page 的行为. null -> {
VersionHistoryController(context).showForEditor( DialogUtils.buildAndShowAdaptive {
uri = uri, MaterialDialog.Builder(context)
onRestoreToEditor = { restoredText -> .title(R.string.text_prompt)
editor.text = restoredText .content(R.string.error_unable_to_display_version_history_as_current_file_path_is_unknown)
}, .positiveText(R.string.dialog_button_dismiss)
onRestoredUi = { .positiveColorRes(R.color.dialog_button_default)
setMenuItemStatus(R.id.save, true) .build()
showSnack(this@EditorView, R.string.text_done) }
}, }
) else -> {
// Delegate to controller to unify editor/explorer/history page behavior.
// zh-CN: 委托给 Controller, 统一 editor/explorer/history page 的行为.
VersionHistoryController(context).showForEditor(
uri = uri,
onRestoreToEditor = { restoredText ->
editor.text = restoredText
},
onRestoredUi = {
setMenuItemStatus(R.id.save, true)
showSnack(this@EditorView, R.string.text_done)
},
)
}
}
} }
// A minimal emergency draft store. // A minimal emergency draft store.
@@ -757,9 +813,10 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
// 1) Remove expired (older than 7 days). // 1) Remove expired.
// zh-CN: 1) 删除过期草稿 (超过 7 天). // zh-CN: 1) 删除过期草稿.
val expiredBefore = now - DRAFT_MAX_DAYS_MS val maxDraftLifetime = HistoryPrefs.draftsMaxDays().coerceAtLeast(0).toLong() * 24L * 60L * 60L * 1000L
val expiredBefore = now - maxDraftLifetime
files.forEach { f -> files.forEach { f ->
if (f.lastModified() < expiredBefore) { if (f.lastModified() < expiredBefore) {
// noinspection ResultOfMethodCallIgnored // noinspection ResultOfMethodCallIgnored
@@ -772,10 +829,12 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
val remained = draftsDir.listFiles()?.toList()?.sortedByDescending { it.lastModified() } ?: return val remained = draftsDir.listFiles()?.toList()?.sortedByDescending { it.lastModified() } ?: return
var total = remained.sumOf { it.length().coerceAtLeast(0L) } var total = remained.sumOf { it.length().coerceAtLeast(0L) }
if (total <= DRAFT_MAX_TOTAL_BYTES) return val draftLimit = HistoryPrefs.draftsMaxTotalBytes().coerceAtLeast(0L)
if (total <= draftLimit) return
for (f in remained.asReversed()) { for (f in remained.asReversed()) {
if (total <= DRAFT_MAX_TOTAL_BYTES) break if (total <= draftLimit) break
val len = f.length().coerceAtLeast(0L) val len = f.length().coerceAtLeast(0L)
if (f.delete()) { if (f.delete()) {
total -= len total -= len
@@ -823,7 +882,16 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
} }
fun openByOtherApps() { fun openByOtherApps() {
openByOtherApps(uri) uri?.let { openByOtherApps(it) } ?: run {
DialogUtils.buildAndShowAdaptive {
MaterialDialog.Builder(context)
.title(R.string.text_prompt)
.content(R.string.error_unable_to_open_with_other_apps_as_current_file_path_is_unknown)
.positiveText(R.string.dialog_button_dismiss)
.positiveColorRes(R.color.dialog_button_default)
.build()
}
}
} }
fun beautifyCode() { fun beautifyCode() {
@@ -843,10 +911,12 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
} }
fun selectTextSize() { fun selectTextSize() {
TextSizeSettingDialogBuilder(context) DialogUtils.buildAndShowAdaptive {
.initialValue(pxToSp(editor.codeEditText.textSize).toInt()) TextSizeSettingDialogBuilder(context)
.callback { value: Int -> setTextSize(value) } .initialValue(pxToSp(editor.codeEditText.textSize).toInt())
.show() .callback { value: Int -> setTextSize(value) }
.build()
}
} }
fun setTextSize(value: Int) { fun setTextSize(value: Int) {
@@ -860,22 +930,24 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
if (i < 0) { if (i < 0) {
i = 0 i = 0
} }
MaterialDialog.Builder(context) DialogUtils.buildAndShowAdaptive {
.title(R.string.text_editor_theme) MaterialDialog.Builder(context)
.items(themes) .title(R.string.text_editor_theme)
.choiceWidgetThemeColor() .items(themes)
.itemsCallbackSingleChoice(i) { _, _, which, _ -> .choiceWidgetThemeColor()
themes[which]?.let { .itemsCallbackSingleChoice(i) { _, _, which, _ ->
setTheme(it) themes[which]?.let {
Themes.setCurrent(it.name) setTheme(it)
Themes.setCurrent(it.name)
}
true
} }
true .negativeText(R.string.dialog_button_cancel)
} .negativeColorRes(R.color.dialog_button_default)
.negativeText(R.string.dialog_button_cancel) .positiveText(R.string.dialog_button_confirm)
.negativeColorRes(R.color.dialog_button_default) .positiveColorRes(R.color.dialog_button_attraction)
.positiveText(R.string.dialog_button_confirm) .build()
.positiveColorRes(R.color.dialog_button_attraction) }
.show()
} }
@Throws(CheckedPatternSyntaxException::class) @Throws(CheckedPatternSyntaxException::class)
@@ -910,7 +982,9 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
.replace(R.id.toolbar_menu, DebugToolbarFragment()) .replace(R.id.toolbar_menu, DebugToolbarFragment())
.commit() .commit()
debugBar.visibility = VISIBLE debugBar.visibility = VISIBLE
mInputMethodEnhanceBar.visibility = GONE if (!mReadOnly) {
mInputMethodEnhanceBar.visibility = GONE
}
mDebugging = true mDebugging = true
} }
@@ -923,7 +997,9 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
showNormalToolbar() showNormalToolbar()
editor.setDebuggingLine(-1) editor.setDebuggingLine(-1)
debugBar.visibility = GONE debugBar.visibility = GONE
mInputMethodEnhanceBar.visibility = VISIBLE if (!mReadOnly) {
mInputMethodEnhanceBar.visibility = VISIBLE
}
mDebugging = false mDebugging = false
} }
@@ -1071,18 +1147,6 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
private const val MIN_CONFIDENCE_TO_WRITE_FILE = 90 private const val MIN_CONFIDENCE_TO_WRITE_FILE = 90
private val DEFAULT_CHARSET_TO_WRITE_FILE = StandardCharsets.UTF_8 private val DEFAULT_CHARSET_TO_WRITE_FILE = StandardCharsets.UTF_8
// Max file size to track history snapshots.
// zh-CN: 纳管历史快照的最大文件大小.
private const val MAX_FILE_SIZE_TO_TRACK_BYTES: Int = 20 * 1024 * 1024
// Emergency draft cleanup window (7 days).
// zh-CN: 草稿清理时间窗口 (7 天).
private const val DRAFT_MAX_DAYS_MS: Long = 7L * 24L * 60L * 60L * 1000L
// Emergency draft total size limit (200MB).
// zh-CN: 草稿总容量上限 (200MB).
private const val DRAFT_MAX_TOTAL_BYTES: Long = 200L * 1024L * 1024L
// Internal storage root (no external SD). // Internal storage root (no external SD).
// zh-CN: 内部存储根目录 (不访问外置 SD). // zh-CN: 内部存储根目录 (不访问外置 SD).
private const val INTERNAL_STORAGE_ROOT: String = "/storage/emulated/0" private const val INTERNAL_STORAGE_ROOT: String = "/storage/emulated/0"
@@ -1094,4 +1158,4 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag
const val EXTRA_SAVE_ENABLED = "saveEnabled" const val EXTRA_SAVE_ENABLED = "saveEnabled"
const val EXTRA_RUN_ENABLED = "runEnabled" const val EXTRA_RUN_ENABLED = "runEnabled"
} }
} }

View File

@@ -33,7 +33,11 @@ import android.os.Parcelable
import android.text.Layout import android.text.Layout
import android.util.AttributeSet import android.util.AttributeSet
import android.view.Gravity import android.view.Gravity
import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputConnectionWrapper
import android.widget.TextViewHelper import android.widget.TextViewHelper
import androidx.appcompat.widget.AppCompatEditText import androidx.appcompat.widget.AppCompatEditText
import androidx.core.graphics.withTranslation import androidx.core.graphics.withTranslation
@@ -68,6 +72,14 @@ class CodeEditText : AppCompatEditText {
private var mDebuggingLine = -1 private var mDebuggingLine = -1
private var mCursorChangeCallbacks: CopyOnWriteArrayList<CursorChangeCallback>? = null private var mCursorChangeCallbacks: CopyOnWriteArrayList<CursorChangeCallback>? = null
// Read-only state.
// zh-CN: 只读状态.
private var mReadOnly = false
// Backup of the original KeyListener, used to restore editable behavior.
// zh-CN: 备份原始 KeyListener, 用于恢复可编辑行为.
private var mOriginalKeyListener = keyListener
private val currentLine: Int private val currentLine: Int
get() = layout?.let { LayoutHelper.getLineOfChar(it, selectionStart) } ?: -1 get() = layout?.let { LayoutHelper.getLineOfChar(it, selectionStart) } ?: -1
@@ -99,6 +111,86 @@ class CodeEditText : AppCompatEditText {
importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO
} }
mCursorChangeCallbacks = CopyOnWriteArrayList() mCursorChangeCallbacks = CopyOnWriteArrayList()
// Ensure selection is possible by default.
// zh-CN: 默认确保可以进行文本选择.
setTextIsSelectable(true)
isLongClickable = true
}
// Public API to toggle read-only mode.
// zh-CN: 切换只读模式的公开接口.
fun setReadOnly(readOnly: Boolean) {
if (mReadOnly == readOnly) return
mReadOnly = readOnly
if (readOnly) {
// Keep enabled/focusable so the user can select and copy text.
// zh-CN: 保持 enabled/focusable, 让用户可以选择并复制文本.
isEnabled = true
isFocusable = true
isFocusableInTouchMode = true
setTextIsSelectable(true)
isLongClickable = true
isCursorVisible = true
// Disable soft keyboard editing by removing KeyListener.
// zh-CN: 通过移除 KeyListener 禁用软键盘编辑.
if (mOriginalKeyListener == null) {
mOriginalKeyListener = keyListener
}
keyListener = null
showSoftInputOnFocus = false
} else {
// Restore editable behavior.
// zh-CN: 恢复可编辑行为.
keyListener = mOriginalKeyListener
showSoftInputOnFocus = true
}
}
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
val ic = super.onCreateInputConnection(outAttrs) ?: return null
if (!mReadOnly) return ic
// Block all text-mutating operations at the InputConnection layer.
// zh-CN: 在 InputConnection 层阻止所有会修改文本的操作.
return object : InputConnectionWrapper(ic, true) {
override fun commitText(text: CharSequence?, newCursorPosition: Int): Boolean = false
override fun setComposingText(text: CharSequence?, newCursorPosition: Int): Boolean = false
override fun finishComposingText(): Boolean = false
override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean = false
override fun deleteSurroundingTextInCodePoints(beforeLength: Int, afterLength: Int): Boolean = false
}
}
override fun onTextContextMenuItem(id: Int): Boolean {
if (mReadOnly) {
// Allow copy/select actions, block cut/paste/replace actions.
// zh-CN: 允许复制/选择相关操作, 阻止剪切/粘贴/替换等修改操作.
when (id) {
android.R.id.cut,
android.R.id.paste,
android.R.id.pasteAsPlainText,
android.R.id.replaceText -> return false
}
}
return super.onTextContextMenuItem(id)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (mReadOnly) {
// Block hardware-keyboard editing shortcuts.
// zh-CN: 阻止硬件键盘的编辑快捷键.
if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
return true
}
if (event.isCtrlPressed && (keyCode == KeyEvent.KEYCODE_V || keyCode == KeyEvent.KEYCODE_X)) {
return true
}
}
return super.onKeyDown(keyCode, event)
} }
@SuppressLint("WrongConstant") @SuppressLint("WrongConstant")
@@ -237,23 +329,32 @@ class CodeEditText : AppCompatEditText {
// Draw code // Draw code
val lineStart = layout.getLineStart(line) val lineStart = layout.getLineStart(line)
val lineVisibleEnd = layout.getLineVisibleEnd(line)
// Never draw line-break control characters.
// zh-CN: 永远不要绘制换行等控制字符.
val safeText = text ?: continue
var lineEnd = layout.getLineEnd(line).coerceAtMost(safeText.length)
while (lineEnd > lineStart) {
val ch = safeText[lineEnd - 1]
if (ch == '\n' || ch == '\r') {
lineEnd--
} else {
break
}
}
if (lineStart >= textLength) continue if (lineStart >= textLength) continue
if (lineVisibleEnd > textLength) continue if (lineEnd > textLength) continue
// @Reference to LYS86 (https://github.com/LYS86) by SuperMonster003 on Apr 17, 2025. // If this is an empty line (or the line only contains line-break chars), skip drawing.
// ! https://github.com/LYS86/AutoJs/blob/05a7e48a8d5b0c6207b3d2974f762c050156298c/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditText.java#L232 // zh-CN: 如果这是空白行(或该行只包含换行字符), 则跳过绘制.
if (lineStart == lineVisibleEnd) continue if (lineStart >= lineEnd) continue
val lineEnd = lineVisibleEnd.coerceAtMost(highlightTokens.colors.size) val localColors = highlightTokens.colors
val visibleCharStart = getVisibleCharIndex(paint, scrollX, lineStart, lineEnd) val visibleCharStart = getVisibleCharIndex(paint, scrollX, lineStart, lineEnd)
var visibleCharEnd = getVisibleCharIndex(paint, scrollX + mParentScrollView!!.width, lineStart, lineEnd) + 1 var visibleCharEnd = getVisibleCharIndex(paint, scrollX + mParentScrollView!!.width, lineStart, lineEnd) + 1
val safeText = text ?: continue
val localColors = highlightTokens.colors
if (visibleCharStart >= visibleCharEnd) continue if (visibleCharStart >= visibleCharEnd) continue
if (visibleCharStart >= safeText.length) continue if (visibleCharStart >= safeText.length) continue
if (visibleCharStart < 0) continue if (visibleCharStart < 0) continue
@@ -290,7 +391,7 @@ class CodeEditText : AppCompatEditText {
} }
paint.color = previousColor paint.color = previousColor
visibleCharEnd = visibleCharEnd.coerceAtMost(textLength) visibleCharEnd = minOf(visibleCharEnd.coerceAtMost(textLength), lineEnd)
if (previousColorPos >= visibleCharEnd) continue if (previousColorPos >= visibleCharEnd) continue
val currentText = text ?: continue val currentText = text ?: continue

View File

@@ -51,8 +51,9 @@ import kotlin.math.floor
* Modified by project: https://github.com/980008027/JsDroidEditor * Modified by project: https://github.com/980008027/JsDroidEditor
*/ */
/** /**
* Modified by SuperMonster003 as of Jul 16, 2023.
* Transformed by SuperMonster003 on Jul 16, 2023. * Transformed by SuperMonster003 on Jul 16, 2023.
* Modified by SuperMonster003 as of Jul 16, 2023.
* Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 6, 2026.
*/ */
class CodeEditor : HVScrollView { class CodeEditor : HVScrollView {
@@ -293,7 +294,7 @@ class CodeEditor : HVScrollView {
} }
fun setReadOnly(readOnly: Boolean) { fun setReadOnly(readOnly: Boolean) {
codeEditText.isEnabled = !readOnly codeEditText.setReadOnly(readOnly)
} }
fun setRedoUndoEnabled(enabled: Boolean) { fun setRedoUndoEnabled(enabled: Boolean) {

View File

@@ -1,5 +1,6 @@
package org.autojs.autojs.ui.edit.toolbar; package org.autojs.autojs.ui.edit.toolbar;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
@@ -93,7 +94,10 @@ public class DebugToolbarFragment extends ToolbarFragment<FragmentDebugToolbarBi
mDebugger = DebuggerSingleton.get(); mDebugger = DebuggerSingleton.get();
mDebugger.setWeakDebugCallback(new WeakReference<>(this)); mDebugger.setWeakDebugCallback(new WeakReference<>(this));
setInterrupted(false); setInterrupted(false);
mCurrentEditorSourceUrl = mInitialEditorSourceUrl = mEditorView.uri.toString(); Uri uri = mEditorView.getUri();
if (uri != null) {
mCurrentEditorSourceUrl = mInitialEditorSourceUrl = uri.toString();
}
mInitialEditorSource = mEditorView.editor.getText(); mInitialEditorSource = mEditorView.editor.getText();
setupEditor(); setupEditor();
ScriptExecution execution = mEditorView.run(false); ScriptExecution execution = mEditorView.run(false);